Composed widgets 2.3 3.0 apotomo-1.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.

rails 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.

rails generate apotomo:widget tweet display -e haml
    exists  app/widgets/
    create  app/widgets/tweet_widget
    exists  test/widgets
    create  app/widgets/tweet_widget.rb
    create  app/widgets/tweet/display.html.haml
    create  test/widgets/tweet_widget_test.rb

The new class is pretty straight-forward.

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

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/widgets/tweet/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=" + 
12      $("##{widget_id}").attr("data-id")})
13  });

Basically, if the heart is clicked, an AJAX request is issued (line 10-12) 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/widgets/twitter_widget.rb (snippet)
 9  def display
10    for t in Tweet.find(:all)
11      self << widget(:tweet, "tweet-#{t.id}", :tweet => t)
12    end
13    
14    render :layout => "portlet"
15  end

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

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

To render widgets within state views you may use #render_widget.

app/widgets/twitter/display.html.haml (snippet)
 1%ul
 2  - for tweet in children do
 3    = render_widget tweet
 4
 5%p

Nothing really changed here as well – except that we simply call children (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/widgets/twitter_widget.rb (snippet)
 1class TwitterWidget < Apotomo::Widget
 2  responds_to_event :submit, :with => :process_tweet
 3  
 4  after_add do
 5    root.respond_to_event :tweetDeleted, :with => :redraw, :on => widget_id
 6  end
 7  
 8  has_widgets do
 9    for t in Tweet.find(:all)
10      self << widget(:tweet, "tweet-#{t.id}", :tweet => t)
11    end
12  end
13  
14  
15  def display
16    render :layout => "portlet"

Instead of adding the kids in an explicit state (that was in #display 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/widgets/tweet_widget.rb
 1class TweetWidget < Apotomo::Widget
 2  responds_to_event :heart, :with => :toggle
 3  
 4  def display
 5    @tweet = options[:tweet]
 6    render
 7  end
 8  
 9  def toggle(evt)
10    @tweet = Tweet.find(evt[:id])
11    @tweet.toggle_loved
12     
13    replace :view => :display
14  end
15end

Hearting apotomo

We already know that shit. First create an observer (line 2). 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