Composed widgets 2.3 3.0

Heart

Being real web components, Apotomo widgets can be nested. This allows building more complex widget trees where widgets are put into containers.

We need a new feature: Tweets should be markable as “loved” if we like them.

Clicking on the heart icon flags the respective Tweet row as loved and redraws the list item. Let’s make the TwitterWidget a container where each loveable tweet item is a separate child widget.

Making tweets loveable

First add the new field to the tweets table.

script/generate migration add_loved_flag_to_tweet loved:boolean

And execute the migration

rake db:migrate

Now it’s time for another widget, yeah!

Creating a new widget

The generator strikes back – we call it TweetWidget as it displays one item, only.

script/generate widget tweet_widget display --haml
    exists  app/cells/
    create  app/cells/tweet_widget
    exists  test/widgets
    create  app/cells/tweet_widget.rb
    create  app/cells/tweet_widget/display.html.haml
    create  test/widgets/tweet_widget_test.rb

The new class is pretty straight-forward.

app/cells/tweet_widget.rb
1class TweetWidget < Apotomo::Widget
2  def display
3    @tweet = @opts[:tweet]
4    render
5  end
6  
7end

Somebody has to pass in the Tweet instance we have to display – that’s usually the container’s job.

Containing some JavaScript capable of improvement, the view is still simple. I never said I’m good at JavaScript.

app/cells/tweet_widget/display.html.haml
 1%li{:id => widget_id, 'data-id' => @tweet.id, 'data-event-url' => url_for_event(:heart)}
 2  = @tweet.text
 3  
 4  - if @tweet.loved?
 5    = image_tag "heart.png"
 6  - else
 7    = image_tag "heart-faded.png"
 8  
 9:javascript
10  $("##{widget_id} img").click(function() {
11    $.ajax({url: $("##{widget_id}").attr("data-event-url") + "&id=" + $("##{widget_id}").attr("data-id")})
12  });

Basically, if the heart is clicked, an AJAX request is issued (line 10-11) to the url provided by #url_for_event (line 1). Also, we append the id of the loved Tweet object. I never said I’m good at JavaScript.

Embedding widgets into containers

A container widget is no special thing, it’s just adding children to itself and rendering ’em. The old TwitterWidget will be the container.

app/cells/twitter_widget.rb (snippet)
 8  def display_form
 9    for t in Tweet.find(:all)
10      self << widget(:tweet_widget, "tweet-#{t.id}", :display, :tweet => t)
11    end
12    
13    render :layout => 'portlet'
14  end

So far, only #display_form changed. For each tweet it appends a new TweetWidget instance passing in the :tweet as parameter (line 10).

It is important to understand that you can use the composing methods like << or remove! not in controllers has_widget blocks only, but in widgets, too.

Rendering children

When calling render without the :render_children => false option, children will be rendered automatically.

app/cells/twitter_widget/display_form.html.haml (snippet)
 1%ul
 2  = rendered_children.values.join("\n")
 3
 4%p
 5  What are you up to?

Nothing really changed here as well – except that we simply access the rendered_children hash which contains the rendered child views already (line 2). Pretty handy.

With hearts

This looks good. If we click a heart icon… an AJAX request is triggered but brings us back an exception.

Source "tweet-26" non-existent.

Well, we are in a stateless setup, so naturally in the triggered state the container widget forgets all the children we added earlier. The problem: when we trigger an event from a tweet widget (by clicking the heart), Apotomo tries to find this widget instance to start bubbling the event. But it’s already gone as it didn’t survive the request.

Lucky us we already read about that.

Statefulness in a stateless world

The has_widgets block in widgets is our substitution for the lack of statefulness here (hey, you chose to derive from Widget instead of StatefulWidget!) – it is run every time the widget is instantiated.

app/cells/twitter_widget.rb (snippet)
 1class TwitterWidget < Apotomo::Widget
 2  responds_to_event :submit, :with => :process_tweet
 3  
 4  after_add do |me, parent|
 5    me.root.respond_to_event :tweetDeleted, :with => :redraw, :on => me.name
 6  end
 7  
 8  has_widgets do |me|
 9    for t in Tweet.find(:all)
10      me << widget(:tweet_widget, "tweet-#{t.id}", :display, :tweet => t)
11    end
12  end
13  
14  def display_form
15    render :layout => 'portlet'
16  end

Instead of adding the kids in an explicit state (that was in #display_form before) we put the setup on the class layer (line 8-12).

Clicking the heart now works… nothing happens, though, cause we didn’t setup an event handler.

Catching the heart beat

The click should be processed in the TweetWidget which fired it as well.

app/cells/tweet_widget.rb
 1class TweetWidget < Apotomo::Widget
 2  responds_to_event :heart, :with => :toggle
 3  
 4  def display
 5    @tweet = @opts[:tweet]
 6    render
 7  end
 8  
 9  def toggle
10    @tweet = Tweet.find(param(:id))
11    @tweet.toggle_loved
12    
13    replace :view => :display
14  end
15  
16end

Hearting apotomo

We already know that shit. First create an observer (line 1). In #toggle we, well, toggle the loved state of the desired tweet and replace the content with a re-rendered display view (line 9-14).

And voilà – clicking the heart icon updates the tweet.


blog comments powered by Disqus