Using views and states

In this Chapter render :view, render :state
Github code render-state
Versions
Next chapter

In the introducing chapter we got our twittering working. However, the #display view does two jobs at once.

It’s listing the tweets and managing the form for new posts.

Why not have a look at it.

app/widgets/twitter/display.html.haml
 1- widget_div do
 2  %ul
 3    - for tweet in @tweets
 4      %li
 5        = tweet.text
 6 
 7  %p
 8    What are you up to?
 9   
10  - form_tag url_for_event(:submit), :remote => true do
11    = text_field_tag 'text'
12   
13    = submit_tag "Tweet!"

In a component-oriented architecture things should be separated for good encapsulation so let’s learn two simple steps to do just that.

Extracting partials

Often it is good practice to move view fragment to partials. A partial is reusable and helps limiting the scope of variables used in the view.

In Cells (and Apotomo) we don’t distinguish between view and partial – there’s no need for making thing more complex. Everything is a view… or a partial. We simply call it view.

Let’s extract the list to a separate view. Or partial. Whatever.

app/widgets/twitter/list.haml
1%ul
2  - for tweet in tweets
3    %li{'data-id' => tweet.id}
4      = tweet.text

Notice how we refer to tweets as a local, and not as instance variable anymore (line 2). Overally, it is cleaner to work with locals. Instance variables are dangerous.

The new view must be rendered in the display view.

app/widgets/twitter/display.html.haml
 1- widget_div do
 2  = render :view => :list, :locals => {:tweets => @tweets}
 3 
 4  %p
 5    What are you up to?
 6   
 7  - form_tag url_for_event(:submit), :remote => true do
 8    = text_field_tag 'text'
 9   
10    = submit_tag "Tweet!"

The render :view call looks and feels similar to partial rendering (line 2).

Apotomo’s #render is identical to the one found in controllers. It accepts two additional options, :view and :state. Check the API to learn more.

Rendering states vs. helpers

Partials are nice and a quick way to DRY up things. Rendering states gives you even more power. It allows rendering and executing an associated piece of code. Sounds like a helper, doesn’t it?

Rendering states in views is really comparable to using plain helpers. But – ha! – states are object-oriented, can be inherited and overwriten. How does that look like?

app/widgets/twitter/display.html.haml
 1- widget_div do
 2  = render({:state => :list}, @tweets)
 3 
 4  %p
 5    What are you up to?
 6   
 7  - form_tag url_for_event(:submit), :remote => true do
 8    = text_field_tag 'text'
 9   
10    = submit_tag "Tweet!"

I replace the render :view with a render :state. The cool thing is, I can pass any options to the state method (line 2).

app/widgets/twitter_widget.rb
 1class TwitterWidget < Apotomo::Widget
 2  responds_to_event :submit, :with => :process_tweet
 3
 4  def display
 5    @tweets = Tweet.find(:all)
 6    render
 7  end
 8  
 9  def process_tweet(evt)
10    Tweet.new(:text => evt[:text]).save
11
12    @tweets = Tweet.find(:all) # this is wet!
13    replace :view => :display
14  end
15  
16  def list(tweets)
17    render :locals => {:tweets => tweets}
18  end
19end

One thing changed: We got a new state #list (line 16-18)! It requires one argument tweets. So, when we render this state, we have to pass in a tweets list. It’s important to see how states urge you to define interfaces – and this is a good thing.

Summing up the rendering cycle

So, what’s going on here, step-by-step.

app/widgets/twitter/list.haml
1%ul
2  - for tweet in tweets
3    %li{'data-id' => tweet.id}
4      = tweet.text

Extracting the form to a separate state

Why not extract the form to another state and see how it looks? We will soon learn that this absolutely makes sense.

app/widgets/twitter_widget.rb (snippet)
15  def list(tweets)
16    render :locals => {:tweets => tweets}
17  end
18  
19  def form
20    render
21  end

First, I define a new state method that just renders (line 19-21). We then need an appropriate view.

app/widgets/twitter/form.haml
1.form
2  What are you up to?
3 
4  - form_tag url_for_event(:submit), :remote => true do
5    = text_field_tag 'text'
6   
7    = submit_tag "Tweet!"

Nothing new, just the form in a separate partial file.

The last step is to render the form state in our main view.

app/widgets/twitter/display.html.haml
1- widget_div do
2  = render({:state => :list}, @tweets)
3 
4  = render :state => :form

Much simpler now, what was the actual form is now a delegation to the #form state (line 4).

DRYing up the widget class

After all this work, we can also clean-up our TwitterWidget and remove redundant queries to the Tweet model.

app/widgets/twitter_widget.rb
 1class TwitterWidget < Apotomo::Widget
 2  responds_to_event :submit, :with => :process_tweet
 3
 4  def display
 5    @tweets = Tweet.find(:all)
 6    render
 7  end
 8  
 9  def process_tweet(evt)
10    Tweet.new(:text => evt[:text]).save
11
12    replace :state => :display
13  end
14  
15  def list(tweets)
16    render :locals => {:tweets => tweets}
17  end
18  
19  def form
20    render
21  end
22  
23end

Look what happens now in #process_state – after a tweet submission is consumed we simply re-run the #display state and let it manage the rendering of the widget (line 12).

That looks quite ok now. A quick discussion when to use states and then we’re done for today!

Views vs. States

Don’t confuse the :view and :state option. The first simply renders the view whereas the latter invokes a state method, which itself renders. Both are helpful and it’s a matter of design where to use which.

Another interesting concept is extracting logic and views to separate states, as we did with the form and the tweet list. As a rule of thumb, it is nothing wrong to move things to discrete states. Nevertheless, you could also create a separate widget class. We will learn more about that soon.

In the next lesson we discover AJAX validations.
blog comments powered by Disqus