Sunday, June 5, 2011

Rendering a different view using partials

Remember yesterday when I was talking about having a good day followed by a bad day with Rails? Looks like today is going to be a good day!

Something I've been wanting to do with Chorenivore is change the view so it only displays the information for a task (Done, Task, Description, Created) if there actually IS a task. Otherwise, this information shouldn't be displayed, right?

This morning I woke up with the idea of creating a couple of partials and then rendering them with an if else statement. So right away I got started. I broke out the code displayed in the view into 'yestask', then creating a partial called 'notask' that didn't display anything but a method for creating a new task.

Here's what I tried at first:

<% if @task.blank? %>
<%= render 'notask' %>
<% else %>
<%= render 'yestask' %>
<% end %>


This code doesn't throw an error, but it doesn't do what I want. In this case, it bypasses 'yestask' altogether and only renders 'notask'. I dug around in Tasks controller and realized I should be writing the code like so:

<% if @tasks.blank? %>
<%= render 'notask' %>
<% else %>
<%= render 'yestask' %>
<% end %>


I get why this version works, because it's checking if there are any instances of @task. I'm still not sure why just calling @task.blank? didn't work. Is it because @task is a method, while @tasks is defined in the controller as Task.all?

Definitely feels good to make some progress, even if I'm not 100% sure about why the method I tried first didn't work.

Another general question: Is using partials in this way a good practice? I kind of feel like I understand partials pretty well, so every problem appears to be something that can be solved with partials. There's a metaphor buried in there somewhere.

4 comments:

  1. At first, you should try whatever you think might work. It's a great way to learn.

    @... are always instance variables and can never be a method call. Instance variables are the preferred way of sharing data between a controller and view, so you should be using them where possible, not method calls.

    Funky thing about instance variables, is that they don't need to exist like regular variables. So if you just call @i_just_made_this_up.blank? it will always be true, because it makes up a new variable that is set to nil, which is blank. If you tried that with a regular variable, you would get an error. This can make debugging around instance variables a bit tricky but you'll get the hang of it.

    RE [@tasks working] "I get why this version works, because it's checking if there are any instances of @task". Careful, as far as Ruby is concerned, @task and @tasks are just two variable names that have absolutely no relationship to each other. Also keep in mind that you can only have instances of a Class, never anything else like a variable. A variable can be an instance of a class, but not the other way around. Or more plainly, classes are abstract things, instances are concrete things. You can't touch the idea of a Person, but you can touch an instance of a Person.

    ReplyDelete
  2. @Eli Here's a follow up question. I've got this definition in the Tasks controller:

    def index
      @tasks = Task.all
    end

    So since @tasks is defined as Task.all, Ruby IS treating them as having a relationship, right? It's just there's no inherent relationship for singular and plural variable names?

    Would changing out <% if @tasks.blank? %>for <% if index.blank? %> work, since index is defined as @task = Task.all ? Is this a method call as you described and shouldn't be used?

    ReplyDelete
  3. So this is a common oddity in programming languages, it's called pass-by-value OR pass-by-reference. I'm going to give you some advice for Ruby that will take you far, but there is a lot to this problem.

    It is dangerous to say that @tasks is defined as Task.all. Imagine that more tasks were added after @tasks was set, if you think of it as you describe, you would expect to see these new tasks in @tasks, but this is not the case.

    It will be be far easier if you think of @tasks = Task.all as "@tasks is set to the value of the method Task.all". This is as close to reality as is easily describable and will make your life as a programmer much easier.

    So, since we're just talking about values returned from methods, you can see that there is no inherent relationship between these things. Try this experiment:

    start = "1"
    finish = start.to_i
    start = "2"

    As you described, we would expect finish to be set to 2, but a quick test in Ruby shows that this sets finish to 1. This is why I say it can be dangerous to think that there is a relationship between these variables, as it implies something that does not happen. Instead if we talk about setting variables to the value of a method our thinking is aligned with how Ruby handles this case.

    Ruby is actually a bit funny here because it passes strings, numbers, booleans and a few others as values, but other objects as references. Example:

    start = [1, 2, 3]
    finish = start
    start.delete(2)
    finish # => [1, 3]

    Since Arrays are pass-by-reference in Ruby, we are passing around a reference to the object, therefore we now have two references to the same thing that we can manipulate directly and see changes in the other. You'll sometimes see calls to thing.dup to "duplicate" an object and force the variables to refer to two different objects.

    Going back to the original example, Task.all. This is an object that masquerades as a Ruby object but is hiding much more complexity. It's only the fact that it talks to the database that makes these points valid. If the data was stored in Ruby, the pass-by-reference stuff would mean that Task.all would always contain all tasks. Because it talks to the database, it's a one time event and we behave more like pass-by-value.

    You don't really need to understand all this yet, but you just need to know it exists so that you will try multiple routes when seeing this problem, not just assuming everything works one way.

    RE index.blank? You should borrow a book of mine called The Rails Way which describes very nicely the way Rails handles requests from startup to sending the result to the browser. TL;DR version, Rails calls the action method for you, then captures the instance variables and gives them to the view. It's kind of a cheat case to make a very common action super easy. What it means though is that although it looks like since you have access to the instance variables, you must be in the same object, and therefore able to call methods, this is not the case because you are in two completely different objects/scopes. You can hack around it and do it, but it's not recommended.

    All that to say, you had it right from the start: if @tasks.blank? is what you should be using.

    ReplyDelete
  4. I can see how it would make sense that you could do that, but you cannot (at least not easily). Inside the view, the helpers and methods you have access to are different than in the controller or model.

    Pretty much you can only communicate between the controller and view through @-variables (which are actually just copied from the controller into the view context.

    Of course, in models and helpers, you can have methods that stand-in like you describe.

    Grossly oversimplified, but the general trace of how things go down in Rails is:

    Browser asks for /orcs/1
    Dispatcher sees request and looks it up in the routes and matches on resources :orcs
    the 'resources' matcher knows that /orcs/1 means you want the show action of the orcs controller, so it runs that.
    orcs_controller#show fires, and sets up your instance variables that get passed to the view.
    It also makes some assumptions that you mean to render /app/views/orcs/show.html.erb.
    Render is silently called, and the variables set up in the controller are made available as your view is chewed up and html is spit out.

    So, in the controller, the method names are pretty much mappings to routes and are called by the router, but shouldn't be called by you.

    Of course you could by calling @controller.index (say in the show view), but that just isn't right. You are defeating the purpose of clean separation of duties. Model is for building your domain and business logic (and persistence), controller is just setup and plumbing, and views are the display part. Helpers keep the views from getting too many conditionals and messy code blocks. Views have access to helpers and models (and the methods within), but shouldn't touch controllers (unless in the form of ajaxing in some info from a webservice route).

    ReplyDelete