Drag&Drop with widgets 2.3 3.0

In the we did a simple tweeter. Now what if we’d like to dump an item? Besides that twitter doesn’t allow deleting (do they?), let’s start writing a trashbin widget.

Dragndrop

When dropping an item the bin widget deletes it from the database. To stay in sync with our model, the list should also redraw. You’ll learn how to do that using event triggering in this lesson.

Dragging items

Drag&Drop with JavaScript is well done in jQuery UI. We need to include the new JS lib in our project.

app/views/layouts/application.html.haml
1%html
2%head
3  = javascript_include_tag "jquery-1.4.2.min"
4  = javascript_include_tag "jquery-ui-1.8.4.custom.min"
5  = stylesheet_link_tag "app"
6
7%body
8  = yield

Next, the listed items have to be draggable.

app/cells/twitter_widget/display_form.html.haml
 1- widget_div do
 2  %ul
 3    - for tweet in @tweets
 4      %li{'data-id' => tweet.id}
 5        = tweet.text
 6
 7  %p
 8    What are you up to?
 9  
10  - form_tag "", "data-event-url" => url_for_event(:submit) do
11    = text_field_tag 'text'
12  
13    = submit_tag "Tweet!"
14  
15  :javascript
16    var form = $("##{widget_id} form");
17  
18    form.submit(function() {
19      $.ajax({url: form.attr("data-event-url"), data: form.serialize()})
20      return false;
21    });
22    
23    $("##{widget_id} li").draggable({revert: "invalid"});

This happens in line 23 using the Draggable API.

Creating the trashbin widget

The bin widget is just a generator away.

$ script/generate widget trashbin_widget display --haml
    exists  app/cells/
    create  app/cells/trashbin_widget
    exists  test/widgets
    create  app/cells/trashbin_widget.rb
    create  app/cells/trashbin_widget/display.html.haml
    create  test/widgets/trashbin_widget_test.rb

The TrashbinWidget is bloody simple.

app/cells/trashbin_widget.rb
 1class TrashbinWidget < Apotomo::Widget
 2  responds_to_event :drop, :with => :trash
 3  
 4  def display
 5    render
 6  end
 7  
 8  def trash
 9    Tweet.find(params[:id]).delete
10    trigger :tweetDeleted
11    
12    replace :view => :display
13  end
14end

Our standard view draws the bin image that I’d stole somewhere.

app/cells/trashbin_widget/display.html.haml
 1- widget_div do
 2  = image_tag "trashbin_empty.png", 'data-event-url' => url_for_event(:drop), :id => "trashbin"
 3  
 4  :javascript
 5    $("#trashbin").droppable({
 6      drop: function(event, ui) {
 7        $.ajax({url: $("#trashbin").attr("data-event-url") + "&id=" + ui.draggable.attr("data-id")});
 8      }
 9    });

In line 5 we define the image from line 3 as Droppable.

On drop, we send an AJAX request to the widget’s :drop event url appending the dropped item’s id.

Catching the dropped

Remember that the trashbin widget handles drop events.

app/cells/trashbin_widget.rb (snippet)
 1class TrashbinWidget < Apotomo::Widget
 2  responds_to_event :drop, :with => :trash

Simply put, when the bin sees a drop event, it will invoke its trash state.

app/cells/trashbin_widget.rb (snippet)
 8  def trash
 9    Tweet.find(params[:id]).delete
10    trigger :tweetDeleted
11    
12    replace :view => :display
13  end

In line 12 we replace the current widget on the page with the display view.

Triggering events

What’s that trigger statement in line 10?

Well, we simply inform the outer world that we altered the tweets table – since we ruthlessly deleted an item.

Bubbling events

According to Apotomo’s bubbling event concept, the tweetDeleted event will bubble up to root. Why not instruct the list widget to catch it and update itself?

app/cells/twitter_widget.rb
 1class TwitterWidget < Apotomo::Widget
 2  responds_to_event :submit, :with => :process_tweet
 3  
 4  def display_form
 5    @tweets = Tweet.find(:all)
 6    render
 7  end
 8  
 9  def process_tweet
10    Tweet.new(:text => param(:text)).save
11    
12    @tweets = Tweet.find(:all) # this is wet!
13    replace :view => :display_form
14  end
15  
16  def redraw
17    replace :state => :display_form
18  end
19  
20end

We just add the redraw state (line 16) which invokes display_form and replaces the widget content in the browser (line 17).

Since events bubble up, we need to attach an event handler to root. That can be done in the controller.

app/controllers/dashboard_controller.rb
 1class DashboardController < ApplicationController
 2  include Apotomo::Rails::ControllerMethods
 3
 4  has_widgets do |root|
 5    root << widget(:twitter_widget, 'parrot', :display_form)
 6    root << widget(:trashbin_widget, 'bin', :display)
 7    root.respond_to_event :tweetDeleted, :with => :redraw, :on => 'parrot'
 8  end
 9
10  def index
11  end
12end

Line 7 instructs root to call updating on the list widget 'parrot' when encountering the respective event.

However, putting observer instructions in the controller is not very clean and breaks widget encapsing, since the controller knows details about the widgets’ internals.

There are two ways to make this suck less.

Both approaches use Apotomo’s event bubbling.


blog comments powered by Disqus