Archive for September, 2009

Ext JS on Rails: A ComprehensiveTutorial

Wednesday, September 30th, 2009

I’ve had my eyes on Ruby-based ExtJS code-generation tools for a few years now. Back in Ext-1.0 days, I even took a shot at creating a large Rails wrapper framework, mapping Ext UI widgets such as Windows, Grids, Trees, Forms and so on, to plain-old Ruby-objects which could be stored in YAML files and rendered into views. However, with Ext-2.0 came new ideas which brought many changes to the framework (great new component-model, plugins, xtype, normalized component configuration-objects) and the Rails wrapper framework was rendered immediately obsolete. Until recently, I gave up on auto-generating ExtJS code and concentrated upon writing good Ext plugins and base-classes.

New possibilities

One of the more tedious processes of creating Ext apps is rendering the DataReader, DataProxy and Store components without the luxury of javascript helpers:

//The traditional way
 
// JsonReader
var reader = new Ext.data.JsonReader({
    root: 'data',
    idProperty: 'id',
    successProperty: 'success'
}, [{
    name: 'id'
}, {
    name: 'email',
    allowBlank: false
}, {
    name: 'first',
    allowBlank: false
}, {
    name: 'last',
    allowBlank: false
}, {
    name: 'created_at',
    type: 'date',
    dateFormat: 'c'
}, {
    name: 'updated_at',
    type: 'date',
    dateFormat: 'c'
}]);
// JsonWriter
var writer = new Ext.data.JsonWriter({
    encode: false
});
// HttpProxy
var proxy = new Ext.data.HttpProxy({
    url: '/users.json'
});
// Typical Store
var store = new Ext.data.Store({
    name: 'users',
    id: 'user',
    restful: true,
    proxy: proxy,
    reader: reader,
    writer: writer,
    autoLoad: true,
    autoSave: true
});

Over the past few weeks, I dusted off my rusty ruby hat and took a fresh look at augmenting ActiveRecord, ActionController and ActionView to help us auto-render a few Ext JS components.

Ext MVC for Rails

I’ve published a new gem at Github named extjs-mvc, a rather simple gem consisting of three mixins for ActionController, ActiveRecord and a View Helper. However, the Gem is hosted at Gem Cutter, “the next generation of RubyGem hosts”. To install the extjs-mvc Gem, first install the gemcutter Gem.

$ sudo gem install gemcutter
$ gem tumble
Thanks for using Gemcutter!
Your gem sources are now:
- http://gemcutter.org
- http://gems.rubyforge.org/
- http://gems.github.com

The gem tumble line adds Gemcutter as your primary Gem-source. gem tumble operates as a toggle — executing it again will remove Gemcutter as a source. Gemcutter seems like a fantastic service — no more naming issues with Github-hosted, username-prefixed gems!

So with Gemcutter added as your primary Gem-source, go ahead and install the extjs-mvc Gem:

$ sudo gem install extjs-mvc

Using the extjs-mvc gem, the Store above can now be rendered in a more Rails/Merb-friendly manner using the extjs_store helper method inside an erb template:

<div id="grid"></div>
<script>
<%= extjs_store({
    :controller => "users",
    :format => "json",
    :config => {
        "autoLoad" => true,
        "restful" => true,
        "writer" => {
            "encode" => false
        }
    }
})%>
</script>

Which renders a tidy little JsonStore instance.

new Ext.data.JsonStore({
    "restful": true,
    "url": "/users.json", // url comes from inflecting upon the controller name + :format
    "fields": [{               //  Fields come from ActiveRecord::Base#columns
        "type": "int",
        "allowBlank": false,
        "name": "id"
    }, {
        "type": "string",
        "allowBlank": false,
        "name": "first"
    }, {
        "type": "string",
        "allowBlank": false,
        "name": "last"
    }, {
        "type": "string",
        "allowBlank": false,
        "name": "email"
    }, {
        "type": "date",
        "allowBlank": true,
        "name": "created_at",
        "dateFormat": "c"
    }, {
        "type": "date",
        "allowBlank": true,
        "name": "updated_at",
        "dateFormat": "c"
    }],
    "messageProperty": "message",
    "root": "data",
    "successProperty": "success",
    "idProperty": "id",     // ActiveRecord::Base#primary_key
    "storeId": "user",      // Inflecting upon controller name
    "autoLoad": true
});

I’ve created an in-depth tutorial on how to create an Ext JS application without compromising your Rails methodology.

rails

Step 1: A Fresh Start

Let’s create a new rails app and take it from the top (Note: using the extjs-mvc gem requires ext-3.0.1+):

$ cd workspace
$ rails extonrails
$ cd extonrails
$ ln -s /www/shared/js/ext-3.0.1 public/javascripts/ext-3.0.1
$ rm public/index.html

Next, configure the extjs-mvc gem in environment.rb within the Rails::Initializer.

Rails::Initializer.run do |config|
    config.gem "extjs-mvc"
end

Edit views/layouts/application.html.erb and link-up the Ext framework in the usual manner.

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
    <meta http-equiv="imagetoolbar" content="no" />
    <meta name="MSSmartTagsPreventParsing" content="true" />
 
    <title>Ext on Rails</title>
 
    <%= stylesheet_link_tag '/javascripts/ext-3.0.1/resources/css/ext-all', 'app', 'silk' %>
    <%= javascript_include_tag 'ext-3.0.1/adapter/ext/ext-base', 'ext-3.0.1/ext-all' %>
</head>
 
<body>
    <div id="hd">
        <img src="/images/rails.png" />
        <h1>ExtJS on Rails</h1>
        <div class="x-clear">&nbsp;</div>
    </div>
    <div id="nav"></div>
    <div id="bd"><%= @content_for_layout %></div>
    <div id="ft"></div>
</body>
</html>

There are a few extra stylesheets included here, app.css and silk.css. Download them if you wish. If you include silk.css, you’ll have to download the silk icon pack from famfamfam. silk.css expects the icons to exist in a folder named /images/icons/silk.

Now let’s generate a controller named projects

$ script/generate controller projects

Create an erb template named app/views/projects/step1.html.erb to make sure we’re cooking.

<h1>Step1</h1>

Go ahead and start your server. You should see something like this

Step 2: Auto-generating Ext components in Rails

The extjs-mvc Gem includes a helper named ExtJS::Helpers::Component which can auto-generate Ext Component configurations compatible with Ext.ComponentMgr#create.

Add a new action to projects_controller.rb named step2. Include the helper ExtJS::Helpers::Component from the extjs-mvc Gem.

class ProjectsController < ApplicationController
  helper ExtJS::Helpers::Component
end

Create a new erb template projects/step2.html.erb.

<div id="panel"></div>
 
<%
    panel = extjs_component(
        "xtype" => 'panel',
        "iconCls" => "silk-application",
        "title" => 'Auto-generated Ext.Panel!',
        "renderTo" => 'panel',
        "frame" => true,
        "width" => 300,
        "height" => 100,
        "html" => 'Why are my feet so far from my head?',
        "collapsible" => true,
        "buttonAlign" => "center",
        "buttons" => [
            {"text" => "Save", "iconCls" => "silk-disk"},
            {"text" => "Cancel", "iconCls" => "silk-cancel"}
        ]
    )
 
    extjs_onready(panel)
%>
 
<%= extjs_render %>

View /projects/step2 in your browser. It should look something like Step2. Let’s step through it:

    panel = extjs_component(...)

The helper-method extjs_component included in the helper ExtJS::Helpers::Component accepts the same configuration options as any Ext Component. extjs_component actually returns an instance of a plain old ruby-object named ExtJS::Component. ExtJS::Component has just three public methods, add, to_json and render.

    extjs_onready(panel)

This line adds the Component instance to the on_ready queue provided by the helper ExtJS::Helpers::Component.

<%= extjs_render %>

extjs_render iterates the on_ready queue and converts each added component to json. You must always call this method in your view or nothing will render.

View the source of /projects/step2 and have a look at the result of extjs_render:

Ext.onReady(function() {
    Ext.ComponentMgr.create({
        "html": "Why are my feet so far from my head?",
        "buttons": [
            {
                "text": "Save",
                "iconCls": "silk-disk"
            },
            {
                "text": "Cancel",
                "iconCls": "silk-cancel"
            }
        ],
        "title": "Auto-generated Ext.Panel!",
        "items": [],
        "frame": true,
        "height": 100,
        "xtype": "panel",
        "buttonAlign": "center",
        "collapsible": true,
        "width": 300,
        "renderTo": "panel",
        "iconCls": "silk-application"
    });
});

Step 3: Adding Event Handling

Attaching event-listeners using extjs_component presents a bit of problem, since json-conversion forces each key/value pair to be surrounded in double-quotes. The problem was easily solved though. Have a look at the render method of ExtJS::Component.

def render
  # If there are any listeners attached in json, we have to get rid of double-quotes in order to expose
  # the javascript object.
  # eg:  "listeners":"SomeController.listeners.grid" -&gt; {"listeners":SomeController.listeners.grid, ...}
  json = @config.to_json.gsub(/\"(listeners|handler|scope)\":\s?\"([a-zA-Z\.\[\]\(\)]+)\"/, '"\1":\2')
  "Ext.ComponentMgr.create(#{json});"
end

Notice how it performs a gsub upon the rendered json-string, removing double-quotes from all values having the keys listeners, handler and scope. Let’s try it out –create a new view-action in your projects_controller named app/view/projects/step3.html.erb

<div id="panel"></div>
 
<%
    panel = extjs_component(
        "xtype" => 'panel',
        "iconCls" => "silk-application",
        "title" => 'Auto-generated Ext.Panel!',
        "renderTo" => 'panel',
        "frame" => true,
        "width" => 300,
        "height" => 100,
        "html" => 'Why are my feet so far from my head?',
        "collapsible" => true,
        "buttonAlign" => "center",
        "buttons" => [{
            "text" => "Save",
            "iconCls" => "silk-disk",
            "handler" => "Controller.onSave"
        }, {
            "text" => "Cancel",
            "iconCls" => "silk-cancel",
            "handler" => "Controller.onCancel"
        }]
    )
 
    extjs_onready(panel)
%>
 
<%= extjs_render %>
 
<script>
    Controller = function() {
        return {
            onSave : function() {
                Ext.Msg.alert('Controller says', 'You clicked Save');
            },
            onCancel : function() {
                Ext.Msg.alert('Controller says', 'You clicked Cancel');
            }
        }
    }();
 
</script>

Check out /projects/step3. Notice here how we’ve defined a simple singleton named Controller where we’ve housed the button-handlers for our panel. Again, take a look at the rendered json /projects/step3. Notice how the handler configuration-property has had its double-quotes removed. This technique will work for the listeners configuration-parameter as well.

Ext.onReady(function() {
    Ext.ComponentMgr.create({
        "html": "Why are my feet so far from my head?",
        "buttons": [
            {
                "text": "Save",
                "handler": Controller.onSave,  // --- double-quotes removed
                "iconCls": "silk-disk"
            },
            {
                "text": "Cancel",
                "handler": Controller.onCancel, // --- double-quotes removed
                "iconCls": "silk-cancel"
            }
        ],
        "title": "Auto-generated Ext.Panel!",
        "items": [
 
        ],
        "frame": true,
        "height": 100,
        "xtype": "panel",
        "buttonAlign": "center",
        "collapsible": true,
        "width": 300,
        "renderTo": "panel",
        "iconCls": "silk-application"
    });
});

Step 4: Setting the Stage with an Ext.Window

We’ll start to build up the stage for this simple projects app now by rendering an Ext.Window having layout: "border" with regions west and center. Create a new view named app/views/projects/step4.html.erb as follows:

<%
    window = extjs_component(
        "xtype" => "window",
        "id" => "projects",
        "title" => "Project Manager",
        "iconCls" => "silk-calendar",
        "closeAction" => "hide",
        "layout" => "border",
        "height" => 480,
        "width" => 800
    )
    window.add(extjs_component(
        "xtype" => "panel",
        "region" => "west",
        "width" => 300,
        "margins" => "5 5 5 5",
        "title" => "West"
    ))
    window.add(extjs_component(
        "xtype" => "panel",
        "id" => "workspace",
        "title" => "Center",
        "margins" => "5 5 5 0",
        "region" => "center",
        "layout" => "card",
        "activeItem" => 0,
        "layoutConfig" =>{
            "layoutOnCardChange" => true
        }
    ))
 
    extjs_onready(window)
%>
 
<%= extjs_render %>
 
<script>
 
    Ext.onReady(function() {
        var projects = Ext.getCmp('projects');
        new Ext.Toolbar({
            renderTo: 'nav',
            items: [{
                text: 'Projects',
                iconCls: 'silk-calendar',
                handler: function(btn, ev) {
                    projects.show(btn.el);
                }
            }, '-', '->', 'Logged in as Your Mother (<a href="#">Logout</a>)']
        });
    });
 
</script>

Browsing to /projects/step4, you should see a toolbar rendered–click the [Projects] button.

NOTE: If you get the following error, you’re experiencing cross-talks with JSON libraries.

wrong number of arguments (1 for 0)

This issues seems to be fixed with Rails version 2.3.4.

Moving right-along then, notice that we can define nested layouts using the add method of ExtJS::Component, just like its javascript cousin Ext.Component.

Step 5: Orgainizing your components into partials

One of the powerful feature of the extjs-mvc is that you can very easily add nested components to some container component by using partials. Create a new controller named users.

$ script/generate controller users

Now create a simple partial app/views/users/_foo.html.erb.

<% extjs_component(
    :container => container,
    "xtype" => 'panel',
    "title" => "I was rendered from the partial views/users/_foo.html.erb"
) %>

Note the addition of the parameter :container here, which I’ll soon explain.

Create a new erb template /views/projects/step5.html.erb.

<%
    window = extjs_component(
        "xtype" => "window",
        "id" => "projects",
        "title" => "Project Manager",
        "iconCls" => "silk-calendar",
        "closeAction" => "hide",
        "layout" => "border",
        "height" => 480,
        "width" => 800
    )
    window.add(extjs_component(
        "xtype" => "panel",
        "region" => "west",
        "width" => 300,
        "margins" => "5 5 5 5",
        "title" => "West"
    ))
    center = window.add(extjs_component(
        "xtype" => "panel",
        "id" => "workspace",
        "title" => "Center",
        "margins" => "5 5 5 0",
        "region" => "center",
        "layout" => "card",
        "activeItem" => 0,
        "layoutConfig" =>{
            "layoutOnCardChange" => true
        }
    ))
    extjs_onready(window)
%>
 
<%= center.add(:partial => "/users/foo.html.erb",
    "html" => "HTML content for the partial"
) %>
 
<%= extjs_render %>
 
<script>
    Ext.onReady(function() {
        var projects = Ext.getCmp('projects');
        new Ext.Toolbar({
            renderTo: 'nav',
            items: [{
                text: 'Projects',
                iconCls: 'silk-calendar',
                handler: function(btn, ev) {
                    projects.show(btn.el);
                }
            }, '-', '->', 'Logged in as Your Mother (<a href="#">Logout</a>)']
        });
    });
</script>
 
<%= extjs_render %>

See /projects/step5. The only difference between step5 and step4 is the addition of the following line:

<%= center.add(:partial => "/users/foo.html.erb",
    "html" => "HTML content for the partial"
) %>

It’s important to realize that when adding a partial to a containing component that the line be rendered in a separate block as you normally render a partial in order for the content to be captured into the main view.

Let’s have a brief look at the method ExtJS::Component#add in the extjs-mvc gem to see what’s happening here.

def add(*config)
    options = config.extract_options!
    if !options.keys.empty?
      if url = options.delete(:partial)
        # rendering a partial, cache the config until partial calls #add method.  @see else.
        @partial_config = options
        return @controller.render(:partial => url, :locals => {:container => self})
      else
        options.merge!(@partial_config) unless @partial_config.nil?
        options[:controller] = @controller unless @controller.nil?
        cmp = ExtJS::Component.new(options)
        @partial_config = nil
        @config[:items] << cmp
        return cmp
      end
    elsif !config.empty? && config.first.kind_of?(ExtJS::Component)
      cmp = config.first
      cmp.apply(@partial_config) unless @partial_config.nil?
      @partial_config = nil
      @config[:items] << cmp.config
      return cmp
    end
  end

Note when the :partial parameter is detected, the Rails partial method is called upon the @controller instance variable in the standard manner adding the local partial-variable :container set with a reference to the containing component. Also note how any extra component-configuration params are stored in the instance variable @partial_config. These params will automatically be applied when the partial creates a new component using the helper method extjs_component. Reviewing /view/users/_foo.html.erb:

<% extjs_component(
    :container => container,
    "xtype" => 'panel',
    "title" => "I was rendered from the partial views/users/_foo.html.erb"
) %>

The configuration-params xtype and title are the @partial_config. These params get applied to the component rendered in the partial in the same manner that Ext.apply(config, partialConfig); works.

Step 6: Creating your Model, Controller, and Stores

The extjs-mvc gem contains an ActiveRecord mixin named ExtJS::Model (DataMapper to come…). Include the mixin into any Model you wish to display in an Ext Store. ExtJS::Model contains two class-methods #extjs_fields, #extjs_record and one instance method #to_record. Use the class-method #extjs_fields to define those model attributes which will be used to compose an Ext.data.Record.

Let’s create a User model now:

$ script/generate model user first:string last:string email:string

We’ll tweak the user migration a bit to add some NOT NULL columns:

class CreateUsers < ActiveRecord::Migration
  def self.up
    create_table :users do |t|
      t.string :first, :null => false
      t.string :last, :null => false
      t.string :email, :null => false
      t.timestamps
    end
  end
 
  def self.down
    drop_table :users
  end
end

Go ahead anddb:migrate.

Map the users resource in routes.rb

ActionController::Routing::Routes.draw do |map|
  map.resources :users
  .
  .
  .
end

Include the mixin ExtJS::Model into the User model in app/models/user.rb

class User < ActiveRecord::Base
  include ExtJS::Model
  extjs_fields :id, :first, :last, :email, :updated_at, :created_at
  #OR extjs_fields :exclude => [:id]    A Bug in Ext-3.0.1 requires id as a field currently
end

In script/console, take a quick look at what the class-method ExtJS::Model#extjs_record provides.

$ script/console
Loading development environment (Rails 2.3.4)
>> User.extjs_record
=> {
    "fields"=>[
          {:type=>:string, :allowBlank=>false, :name=>"first"},
          {:type=>:string, :allowBlank=>false, :name=>"last"},
          {:type=>:string, :allowBlank=>false, :name=>"email"},
          {:type=>:datetime, :allowBlank=>true, :name=>"created_at", :dateFormat=>"c"},
          {:type=>:datetime, :allowBlank=>true, :name=>"updated_at", :dateFormat=>"c"}
    ],
    "idProperty"=>"id"
}
>>

Nice. An auto-generated Ext.data.Record definition already. You shouldn’t need to use this method directly though–you’ll render your Store with a View-helper method instead–but it’s interesting to see how this works. I must stress that the extjs-mvc gem is the result of just two days of hacking. I’m hoping some full-time Rubyists might offer suggestions on improving the current implementation or contribute to the Git project.

While we’re in script/console, let’s create a few sample users.

>>User.create(:first => 'Caroline', :last => 'Schnapp', :email => 'caro@schnapp.com')
.
.
.

Creating your Controller

The extjs-mvc gem provides an ActionController mixin named ExtJS::Controller as well as an ActionView helper called ExtJS::Helpers::Store. Let’s modify our projects_controller a bit now.

class ProjectsController < ApplicationController
  include ExtJS::Controller
  helper ExtJS::Helpers::Store
  helper ExtJS::Helpers::Component
  .
  .
  .
end

Let’s move on to users_controller. Add a simple index action which grabs all the users.

class UsersController < ApplicationController
    include ExtJS::Controller
 
    def index
        rs = User.all.collect {|u| u.to_record}
        render(:json => {:success => true, :data => rs})
    end
end

The method #to_record come from the ExtJS::Model mixin that we included into User. #to_record uses the fields defined by the class-method ExtJS::Model#extjs_fields to decide which attributes to return.

class User < ActiveRecord::Base
    include ExtJS::Model
    extjs_fields :id, :first, :last, :email
end

There’s nothing mysterious going on in ExtJS::Model#to_record; it attaches the primary_key in addition to all the fields defined by extjs_fields.

def to_record
  data = {self.class.primary_key => self.send(self.class.primary_key)}
  self.class.extjs_record_fields.each do |f|
    if refl = self.class.reflections[f]
      if refl.macro === :belongs_to
        data[f] = self.send(f).to_record
      elsif refl.macro === :has_many
        data[f] = self.send(f).collect {|r| r.to_record}  # <-- careful with this one!
      end
    else
      data[f] = self.send(f)
    end
  end
  data
end

#extjs_record will check for any :belongs_to and :has_many relationships and recursively gather their attributes as well.

Auto-generating a Store

Ok, we’re just about ready to get into the fun stuff. Let’s create a new users partial named /views/users/_grid.html.erb and render it into our projects_controller. The users_controller is not going to be routed-to at all in the traditional manner — instead, it’ll act more like a json-service to our application entry-point, the projects_controller. I like to use partials named “grid” and “form” to render the UI components for each Controller. Go ahead and create the partial /views/users/_grid.html.erb.

<%= javascript_include_tag "app/users/UsersGrid" %>
 
<%= extjs_store(
    :controller => "users",
    :writer => {"encode" => false},
    :config => {
        "autoLoad" => true,
        "autoSave" => true,
        "restful" => true
    }
).render %>
 
<% extjs_component(
    :container => container,
    "xtype" => 'users-grid',
    "storeId" => 'user',
    "title" => "Users",
    "iconCls" => 'silk-group'
) %>

The method extjs_store is provided by the helper ExtJS::Helpers::Store. This method will return an instance of a plain-old-ruby-object named ExtJS::Data::Store. This object accepts identical configuration-properties as its javascript cousin, Ext.data.Store through the :config parameter. The :controller configuration-param is most important though — this parameter will constantize its related model and inspect its column meta-data in order to render an Ext.data.DataReader. If your model cannot be inflected based upon controller-name, simply provide the :model parameter. The Ext.data.DataReader meta-information parameters such as root, successProperty and messageProperty can be defined using controller class-methods provided by ExtJS::Component

class UsersController < ApplicationController::Base
  include ExtJS::Controller
  helper ExtJS::Helpers::Store
 
  extjs_root :records  # defaults to :data
  extjs_success_property :shes_good_eh  #defaults to :success
  extjs_message_property :msg  # defaults to :message
end

Note: I think I’ll scrap the :config parameter and just place all config-options on the same level, simply discriminating between Ext-parameters by symbolized params.

Or application-wide by modifying the class ExtJS::MVC in something like environment.rb:

ExtJS::MVC.success_property = :success_o_rama
ExtJS::MVC.root = :rooty_toot
ExtJS::MVC.message_property = :une_message_pour_vous

Moving right along then, notice this partial includes a javascript file named app/users/UserGrid. We’ll create this file next but first we must create a few directories in public/javascripts. I like to organize my javascript in a similar directory structure as Rails/Merb’ /app directory (The extjs-mvc Gem could use some generators if anyone has any ideas).

$ mkdir public/javascripts/app
$ mkdir public/javascripts/app/projects
$ mkdir public/javascripts/app/users

Now create the following javascript file /javascripts/app/users/UsersGrid.js.

Ext.ns("users");
/**
 * @class users.Grid
 */
users.Grid = Ext.extend(Ext.grid.EditorGridPanel, {
    buttonAlign: "center",
    autoExpandColumn: 0,
 
    initComponent: function(){
        this.store = Ext.StoreMgr.get(this.storeId);
        this.columns = this.buildColumns();
        this.tbar = this.buildUI();
 
        this.viewConfig = {
            forceFit: true
        };
 
        users.Grid.superclass.initComponent.call(this);
    },
 
    buildUI : function() {
        return [{
            text: "Add",
            iconCls: "silk-add",
            handler: this.onAddRecord,
            scope: this
        }, "-", {
            text: "Remove",
            iconCls: "silk-delete",
            handler: this.onRemoveRecord,
            scope: this
        }, "-"];
    },
 
    buildColumns : function() {
        return [{
            header: "First",
            dataIndex: "first",
            editor: new Ext.form.TextField({})
        }, {
            header: "Last",
            dataIndex: "last",
            editor: new Ext.form.TextField({})
        }, {
            header: "Email",
            dataIndex: "email",
            editor: new Ext.form.TextField({})
        }];
    },
 
    onAddRecord: function(){
        var rec = new this.store.recordType({});
        this.store.insert(0, rec);
        this.startEditing(0, 0);
    },
 
    onRemoveRecord: function(){
        var index = this.getSelectionModel().getSelectedCell();
        if (!index) {
            return false;
        }
        var rec = this.store.getAt(index[0]);
        this.store.remove(rec);
    }
});
Ext.reg("users-grid", users.Grid);

Finally, wire-up the partial in a new view-template /views/projects/step6.html.erb

<%
    window = extjs_component(
        "xtype" => "window",
        "id" => "projects",
        "title" => "Project Manager",
        "iconCls" => "silk-calendar",
        "closeAction" => "hide",
        "layout" => "border",
        "height" => 480,
        "width" => 800
    )
 
    window.add(extjs_component(
        "xtype" => "panel",
        "id" => "workspace",
        "title" => "Center",
        "margins" => "5 5 5 0",
        "region" => "center",
        "layout" => "card",
        "activeItem" => 0,
        "layoutConfig" =>{
            "layoutOnCardChange" => true
        }
    ))
    extjs_onready(window)
%>
 
<%= window.add(:partial => '/users/grid',
    "region" => 'west',
    "width" => 300,
    "margins" => '5 5 5 5',
    "cmargins" => '5 5 5 5',
    "collapsible" => true
) %>
 
<%= extjs_render %>
 
<script>
    Ext.onReady(function() {
        var projects = Ext.getCmp('projects');
        new Ext.Toolbar({
            renderTo: 'nav',
            items: [{
                text: 'Projects',
                iconCls: 'silk-calendar',
                handler: function(btn, ev) {
                    projects.show(btn.el);
                }
            }, '-', '->', 'Logged in as Your Mother (<a href="#">Logout</a>)']
        });
    });
</script>
 
<%= extjs_render %>

Ok, restart your server and navigate to /projects/step6. If you don’t see any rows in your grid, check the NET tab in Firebug: there should be a successful GET request reported. Inspect the response&mdashIs the success property true? Did you add any records in script/console?

$ User.create(:first => "Hunky", :last => "Bill", :email => "hunkybill@perogie.com")

Try viewing the HTML source-code to have a look at the auto-generated JsonStore.

new Ext.data.JsonStore({
    "restful": true,
    "autoSave": true,
    "url": "/users.json",
    "fields": [
        {
            "type": "int",
            "allowBlank": true,
            "name": "id"
        },
        {
            "type": "string",
            "allowBlank": false,
            "name": "first"
        },
        {
            "type": "string",
            "allowBlank": false,
            "name": "last"
        },
        {
            "type": "string",
            "allowBlank": false,
            "name": "email"
        },
        {
            "type": "date",
            "allowBlank": true,
            "dateFormat": "c",
            "name": "created_at"
        },
        {
            "type": "date",
            "allowBlank": true,
            "dateFormat": "c",
            "name": "updated_at"
        }
    ],
    "messageProperty": "message",
    "root": "data",
    "successProperty": "success",
    "idProperty": "id",
    "storeId": "user",
    "autoLoad": true,
    writer: new Ext.data.JsonWriter({
        "encode": false
    })
});

RESTful, Writable Stores

Ext-3.0 introduced new data-package features for automating the process of writing record-data back to the server. In the past, this had to be done by manually composing Ajax requests. With a writer-enabled Store, one interacts with the Store and its records using the same Ext-2.0 API except Ajax requests will be automatically executed. For example, the following code will all result in Ajax requests to the server:

var record = store.getAt(0);   // Grab the first record.
record.set('email', 'foo@bar.com');   // PUT
store.remove(record);   // DELETE
new_record = new store.recordType({first:'chris', last: 'scott', email:'chris@scott.com'});
store.add(new_record);   // POST

Creating a Writable store is very easy — simply plug an instance of Ext.data.JsonWriter or Ext.data.XmlWriter into your Store just as you would a JsonReader/XmlReader. With the Store helper included in the extjs-mvc gem, it’s even easier.

<script>
<%= extjs_store({
    :controller => "users",
    :writer => {
      :encode => false
    },
    :config => {
        "autoLoad" => true,
        "autoSave" => true,  #<-- Enable cell-level updates
        "restful" => true,  # <-- RESTful mode        
    }
}) %>

If you try interacting with the grid from /projects/step6, you should see Ajax requests firing all over the place. They’ll all be 404 currently since we haven’t implemented any write-actions in the users_controller. Let’s do that now.

class UsersController < ApplicationController
    include ExtJS::Controller
 
    def index
        rs = User.all.collect {|u| u.to_record}
        render(:json => {:success => true, :data => rs})
    end
 
    def create
        u = User.create(params["data"])
        render(:json => {:success => true, :data => u.to_record})
    end
 
    def update
        u = User.find(params[:id])
        render(:text => '', :status => (u.update_attributes(params["data"])) ? 204 : 500)
    end
 
    def destroy
        u = User.find(params[:id])
        render(:text => '', :status => (u.destroy) ? 204 : 500)
    end
end

Refresh your app and try interacting with /projects/step6 once again (double-click rows to edit; use the [Add] and [Remove] buttons on the top-toolbar). These actions are pretty weak on error-checking but you get the idea — automated Stores.

Step 7: Adding a Direct Router

Rails/Merb Ext.Direct support is available through the gems rails-extjs-direct and merb-extjs-direct hosted at Rubyforge.

$ sudo gem install rails-extjs-direct
$ sudo gem install merb-extjs-direct

However, I’m currently in the process of moving these gems to Github intead. I’m also thinking about combining both the Merb and Rails gems into a single gem named extjs-direct then sniffing which framework they’re being included into, including appropriate files for each.

The Rails Ext.Direct gem is implemented with Rack so the first step is to plug it in as middleware in environment.rb after configuring the rails-extjs-direct gem:

Rails::Initializer.run do |config|
  config.gem "extjs-mvc"
  config.gem "rails-extjs-direct"
  config.middleware.use "Rails::ExtJS::Direct::RemotingProvider", "/direct"
  .
  .
  .
end

The parameter “/direct” tells the RemotingProvider to handle all requests coming in on that url.

Next, include the Rails::ExtJS::Direct::Controller mixin into each controller you wish to be “Directable”. We’ll create a new controller now called tasks along with an associated model Task.

$ script/generate controller tasks
$ script/generate model task title:string description:text user_id:integer

Again, tweak the migration slightly to add some NOT NULL columns.

class CreateTasks < ActiveRecord::Migration
  def self.up
    create_table :tasks do |t|
      t.integer :user_id, :null => false
      t.string :title, :null => false
      t.text :description, :null => false
      t.timestamps
    end
  end
 
  def self.down
    drop_table :tasks
  end
end

Do a db:migrate.

Add the ExtJS::Model mixin to the new Task model.

class Task < ActiveRecord::Base
    include ExtJS::Model
    extjs_fields :id, :user_id, :user, :title, :description
 
    belongs_to :user
end

Notice how we’ve added the belongs_to association :user to the list of extjs_fields.

Next, include Ext.Direct controller Mixin Rails::ExtJS::Direct::Controller which provides the class-method #direct_actions. This list should be specified in the specific order of C,R,U,D to help map to the Proxy API.

class TasksController < ApplicationController
  include Rails::ExtJS::Direct::Controller
  include ExtJS::Controller
  helper ExtJS::Helpers::Store
 
  direct_actions :create, :load, :update, :destroy
  #                  C        R        U        D
  def load
    @xresponse.result = {
      :data => Task.all.collect {|u| u.to_record}
    }
    @xresponse.status = true
    render :json => @xresponse
  end
 
  def create
    @xresponse.status = true
    @xresponse.message = "Created Task"
    render :json => @xresponse
  end
 
  def update
    @xresponse.status = true,
    @xresponse.message = "Updated Task"
    render :json => @xresponse
  end
 
  def destroy
    @xresponse.status = true
    @xresponse.message = "Destroyed Task"
    render :json => @xresponse
  end
end

The instance variable @xrequest and @xresponse are provided by the Rails::ExtJS::Direct::Controller using a :before_filter. The @xresponse variable contains all the parameters sent by the client-side request, as seen in Firebug. The @xresponse variable must always be returned via render(:json => @xresponse)

Ok, now that we’ve got some actions defined, let’s configure our client-side DirectProvider. Start by creating a new tasks partial named views/tasks/_direct.html.erb. We’ll create our direct-provider by hand to test it out and auto-generate it later.

<% extjs_component(:container => container,
    "xtype" => "panel",
    "html" => "<p>In Firebug, try exploring ..."
) %>
 
<script>
Ext.Direct.addProvider({
    url: '/direct',    // --- There's our direct-url again
    type: 'remoting',
    actions: {
        Tasks: [{    // --- There's our controller and actions
            name: 'load',
            len: 1
        }, {
            name: 'update',
            len: 1
        }, {
            name: 'create',
            len: 1
        }, {
            name: 'destroy',
            len: 1
        }]
    }
});
</script>

Finally, wire-up the partial /views/tasks/_direct.html.erb as a new projects_controller view in /views/projects/step7.html.erb.

<%
    window = extjs_component(
        "xtype" => "window",
        "id" => "projects",
        "title" => "Project Manager",
        "iconCls" => "silk-calendar",
        "closeAction" => "hide",
        "layout" => "border",
        "height" => 480,
        "width" => 800
    )
 
    workspace = window.add(extjs_component(
        "xtype" => "panel",
        "id" => "workspace",
        "border" => false,
        "margins" => "5 5 5 0",
        "region" => "center",
        "layout" => "card",
        "activeItem" => 0,
        "layoutConfig" =>{
            "layoutOnCardChange" => true
        }
    ))
    extjs_onready(window)
%>
 
<!-- /users/grid -->
<%= window.add(:partial => '/users/grid',
    "region" => 'west',
    "width" => 300,
    "margins" => '5 5 5 5',
    "cmargins" => '5 5 5 5',
    "collapsible" => true
) %>
 
<!-- /tasks/direct -->
<%= workspace.add(:partial => '/tasks/direct',
    "title" => "Wiring up a DirectProvider by hand",
    "bodyStyle" => "padding: 10px"
) %>
 
<%= extjs_render %>
 
<script>
    Ext.onReady(function() {
        var projects = Ext.getCmp('projects');
        new Ext.Toolbar({
            renderTo: 'nav',
            items: [{
                text: 'Projects',
                iconCls: 'silk-calendar',
                handler: function(btn, ev) {
                    projects.show(btn.el);
                }
            }, '-', '->', 'Logged in as Your Mother (<a href="#">Logout</a>)']
        });
    });
</script>
 
<%= extjs_render %>

Open /projects/step7 and type the controller-name Tasks into the Firebug console. You should see an Object containing the four methods load, update, create, destroy as defined above. Try executing these methods in the console:

$ Tasks.load({});
$ Tasks.create({});
$ Tasks.update({});
$ Tasks.destroy({});

Notice how the http-protocol has been completely abstracted. No more Ext.Ajax.request. This is not unlike interacting with your models in irb console. In Firebug console, observe what happens when you execute multiple direct-transactions in one line:

$ Tasks.load();Tasks.create();Tasks.destroy();Tasks.update();Tasks.load();

These simultaneous Direct-transactions within a configurable duration will be queued into the same Ajax request.

Step 8: Auto-generating the direct-provider

Well, that’s all pretty neat but how to centralize this and auto-generate it? How will each rendered partial inform the parent controller (inventory_controller in our case) that it has an API to add to the Ext.Direct RemotingProvider? What we want to do is somehow hand our controller to some centralized object and let it deal with rendering the actual javascript. Let’s go to the projects_controller and add the Rails::ExtJS::Direct::Controller mixin to it.

class ProjectsController < ApplicationController
  include ExtJS::Controller
  include Rails::ExtJS::Direct::Controller
  helper ExtJS::Helpers::Store
  helper ExtJS::Helpers::Component
  .
  .
  .
end

Create a new projects_controller view-template /views/projects/step8.html.erb and use the helper method get_extjs_direct_provider to create an Ext.Direct provider instance available throughout all the partials:

<% @provider = get_extjs_direct_provider("remoting", "/direct") %>
 
<%
    window = extjs_component(
        "xtype" => "window",
        "id" => "projects",
        "title" => "Project Manager",
        "iconCls" => "silk-calendar",
        "closeAction" => "hide",
        "layout" => "border",
        "height" => 480,
        "width" => 800
    )
 
    workspace = window.add(extjs_component(
        "xtype" => "panel",
        "id" => "workspace",
        "border" => false,
        "margins" => "5 5 5 0",
        "region" => "center",
        "layout" => "card",
        "activeItem" => 0,
        "layoutConfig" =>{
            "layoutOnCardChange" => true
        }
    ))
    extjs_onready(window)
%>
 
<!-- /users/grid -->
<%= window.add(:partial => '/users/grid',
    "itemId" => 'users-grid',
    "region" => 'west',
    "width" => 300,
    "margins" => '5 5 5 5',
    "cmargins" => '5 5 5 5',
    "collapsible" => true
) %>
 
<!-- /tasks/grid -->
<%= workspace.add(:partial => '/tasks/grid',
    "itemId" => 'tasks-grid'
) %>
 
<!-- Render the Direct provider after all partials have run -->
<%= @provider.render %>
 
<%= extjs_render %>
 
<script>
    Ext.onReady(function() {
        var projects = Ext.getCmp('projects');
        new Ext.Toolbar({
            renderTo: 'nav',
            items: [{
                text: 'Projects',
                iconCls: 'silk-calendar',
                handler: function(btn, ev) {
                    projects.show(btn.el);
                }
            }, '-', '->', 'Logged in as Your Mother (<a href="#">Logout</a>)']
        });
    });
</script>
 
<%= extjs_render %>

Note how the @provider must be manually rendered after all partials have been rendered:

<%= @provider.render %>
<%= extjs_render %>

Create a new tasks partial named /views/tasks/_grid.html.erb and add its controller API to the @provider. We’ll also include a new javascript file /javascripts/app/tasks/TasksGrid.js.

<%= javascript_include_tag ("app/tasks/TasksGrid") %>
<% @provider.add_controller("tasks") %>
 
<% @store = extjs_store(
    :controller => "tasks",
    :proxy => 'direct',
    :writer => {:encode => false},
    :config => {
        "autoLoad" => true,
        "restful" => true
    }
) %>
 
<%= @store.render %>
 
<% extjs_component(
    :container => container,
    "xtype" => 'tasks-grid',
    "title" => 'Tasks',
    "iconCls" => "silk-date",
    "storeId" => @store.id
) %>

Notice that creating a direct-enabled store with the helper-method extjs_store is as simple as setting the configuration-parameter :proxy => "direct".

Finally, create the javascript file /javascripts/app/tasks/TasksGrid.js:

Ext.ns("tasks");
tasks.Grid = Ext.extend(Ext.grid.GridPanel, {
    autoExpandColumn: 1,
    initComponent: function(){
        this.store = Ext.StoreMgr.get(this.storeId);
        this.columns = [{
            header: "Title",
            dataIndex: "title",
            editor: new Ext.form.TextField({})
        }, {
            header: "Description",
            dataIndex: "description",
            editor: new Ext.form.TextField({})
        }, {
            header: "User",
            dataIndex: "user",
            renderer: function(user) {  // --- custom-renderer for belongs_to :user
                return user.first + " " + user.last;
            }
        }];
        this.tbar = [{
            text: "Add",
            iconCls: "silk-add",
            handler: this.onAddRecord,
            scope: this
        }, "-", {
            text: "Remove",
            iconCls: "silk-delete",
            handler: this.onRemoveRecord,
            scope: this
        }, "-"];
 
        this.viewConfig = {
            forceFit: true
        };
 
        this.addEvents("add-record");
 
        tasks.Grid.superclass.initComponent.call(this);
    },
 
    onAddRecord: function(){
        this.fireEvent("add-record", this);
    },
 
    onRemoveRecord: function(){
        var record = this.getSelectionModel().getSelected();
        this.store.remove(record);
    }
});
Ext.reg("tasks-grid", tasks.Grid);

Restart your server and browse to /projects/step8.

Step 9: Finishing up with a Tasks Form

What’s an app without a form? Let’s finish up this tutorial by implementing a simple Tasks form. Create a new partial /views/tasks/_form.html.erb.

<%= javascript_include_tag "ux/FormPanel/Storable/Ext.ux.FormPanel.Storable" %>
 
<%
    extjs_component(
        :container => container,
        "title" => "Task",
        "xtype" => 'form',
        "plugins" => {
            "ptype" => "form-storable",
            "storeId" => "task",
            "saveButton" => "buttons.btn-save",
            "cancelButton" => "buttons.btn-cancel"
        },
        "labelAlign" => 'top',
        "bodyStyle" => "padding:10px",
        "buttonAlign" => "center",
        "buttons" => [{
            "text" => "Save",
            "itemId" => "btn-save"
        }, {
            "text" => "Cancel",
            "itemId" => "btn-cancel"
        }],
        :items => [{
            "xtype" => 'textfield',
            "anchor" => '100%',
            "fieldLabel" => 'title',
            "allowBlank" => false,
            "name" => 'title'
        }, {
            "xtype" =>'combo',
            "store" => "user",
            "anchor" => '100%',
            "fieldLabel" => 'Assigned to',
            "storeId" => 'user',
            "hiddenName" => 'user_id',
            "displayField" => 'email',
            "valueField" => 'id',
            "mode" => 'local',
            "forceSelection" => true,
            "triggerAction" => 'all'
        }, {
            "xtype" => 'textarea',
            "fieldLabel" => 'Description',
            "grow" => true,
            "allowBlank" => false,
            "growMin" => 200,
            "anchor" => '100%',
            "name" => 'description'
        }]
    )
%>

Here we’re rendering a vanilla Ext.form.FormPanel using the "xtype" => "form" configuration. The extjs-mvc Gem does not currently have any form-building features but I’m open to any ideas on how to pursue this. Take note of the second field having "xtype" => "combo" in the :items array — Notice how it connects the user Store to itself ("store" => "user") — this references the same Store used for the UsersGrid in the window’s west region.

Also note the form is using a plugin created for this tutorial named form-storable. This plugin is a bit long but don’t be discouraged–it’s built to work with any FormPanel you wish to bind to a Store and saves your from having to write the same Store-binding code over and over again. DRY, right? It’s also a good example of how to organize your own custom plugins into a ux directory-structure.

First create the ux directory-structure to house the plugin:

$ mkdir public/javascripts/ux
$ mkdir public/javascripts/ux/FormPanel
$ mkdir public/javascripts/ux/FormPanel/Storable

Now create the file public/javascripts/ux/FormPanel/Storable/Ext.ux.FormPanel.Storable.js.

Ext.ns("Ext.ux", "Ext.ux.FormPanel");
/**
 * @class tasks.FormPanel
 * A plugin which adds Store/Record binding methods to a FormPanel instance.
 */
Ext.ux.FormPanel.Storable = function(param) {
    Ext.apply(this, param);
}
Ext.ux.FormPanel.Storable.prototype = {
    /**
     * @cfg {String} storeId ID of Store instance to bind to panel.
     */
    storeId: undefined,
    /**
     * @cfg {String} saveButton (optional) Specifiy a dot-separted string where the left-hand-side specifies the button
     * locations [tbar|bbar|buttons] and the right-hand-side specifies the button"s itemId.
     * eg:  saveButton: "tbar.btn-save", "bbar.btn-save", "buttons.btn.save".  When the save button is located,
     * it will have an automated save-handler applied to it from Ext.ux.FormPanel.Storable.InstanceMethods#onStorableSave
     */
    saveButton: undefined,
     /**
     * @cfg {String} cancelButton (optional) Specifiy a dot-separted string where the left-hand-side specifies the button
     * locations [tbar|bbar|buttons] and the right-hand-side specifies the button"s itemId.
     * eg:  cancelButton: "tbar.btn-cancel", "bbar.btn-cancel", "buttons.btn.cancel".  When the cancel button is located,
     * it will have an automated cancel-handler applied to it from Ext.ux.FormPanel.Storable.InstanceMethods#onStorableCancel
     */
    cancelButton: undefined,
 
    init : function(panel) {
        // mixin InstanceMethods
        Ext.apply(panel, Ext.ux.FormPanel.Storable.InstanceMethods.prototype);
 
        panel.bind(Ext.StoreMgr.lookup(this.storeId));
 
        if (this.saveButton) {
            this.setHandler("save", this.saveButton, panel);
        }
        if (this.cancelButton) {
            this.setHandler("cancel", this.cancelButton, panel);
        }
 
        panel.addEvents(
            /**
             * @event storable-save
             */
            "storable-save",
            /**
             * @event storable-cancel
             */
            "storable-cancel"
        );
    },
 
    setHandler : function(action, info, panel) {
        var ids = info.split(".");
        var btn = undefined;
        if (ids[0] === "buttons") {
            for (var n = 0, len = panel.buttons.length; n < len; n++) {
                if (panel.buttons[n].itemId === ids[1]) {
                    btn = panel.buttons[n];
                    break;
                }
            }
        } else {
            var pos = (ids[0] === "tbar") ? "Top" : "Bottom";
            var toolbar = this["get" + pos + "Toolbar"]();
            btn = toolbar.getComponent(ids[1]);
        }
        if (!btn) {
            throw new Error("Ext.ux.FormPanel.Storable failed to find button " + ids[1] + " on " + ids[0]);
        }
        btn.setHandler(panel["onStorable"+ Ext.util.Format.capitalize(action)].createDelegate(panel));
    }
};
 
/**
 * @class Ext.ux.FormPanel.Storable.InstanceMethods
 * Mixin for FormPanel
 */
Ext.ux.FormPanel.Storable.InstanceMethods = function() {};
Ext.ux.FormPanel.Storable.InstanceMethods.prototype = {
 
    storableMask: undefined,
 
    /**
     * binds an Ext.data.Store to the Panel
     * @param {Ext.data.Store} store
     */
    bind : function(store) {
        // bind a Store to the form.  Fire the "form-save" event when write-actions are successful
        this.store = store;
 
        // Add store-listeners to show/hide load-mask.
        this.store.un("beforewrite", this.onStorableBeforeWrite, this);
        this.store.on("beforewrite", this.onStorableBeforeWrite, this);
 
        this.store.un("write", this.onStorableWrite, this);
        this.store.on("write", this.onStorableWrite, this);
 
    },
 
    /**
     * Loads a record.  Sets record pointer.  Sets title.
     * @param {Object} record
     */
    loadRecord: function(record) {
        this.record = record;
        this.getForm().loadRecord(record);
 
        // TODO:  Need to be able to customize title.
        this.setTitle("Edit Record");
    },
    /**
     * Resets underlying BasicForm.  Nullifies record pointer.
     */
    reset: function() {
        this.record = null;
        this.getForm().reset();
 
        // TODO:  Need to customize this.
        this.setTitle("Create Record");
    },
 
    // private
    onStorableBeforeWrite : function(proxy, action) {
        if (!this.mask) {
            this.mask = new Ext.LoadMask(this.el, {});
        }
        // quick and dirty verb present-tense inflector.
        var verb = (action[action.length-1] === "e") ? action.substr(0, action.length-1) + "ing" : action + "ing";
        this.mask.msg = verb + " record.  Please wait...";
        this.mask.show();
    },
 
    // private
    onStorableWrite : function(proxy, action) {
        this.mask.hide();
        this.fireEvent("storable-save", this);
    },
 
    // protected
    onStorableCancel : function(btn, ev) {
        this.fireEvent("storable-cancel", this, ev);
    },
 
    // protected
    onStorableSave : function(btn, ev) {
        var form = this.getForm();
        // First, validate the form...
        if (!form.isValid()) {
            Ext.Msg.alert("Error", "Form is invalid");
            return false;
        }
 
        // She"s all good.
        if (this.record === null) { // -- CREATE
            var Task = this.store.recordType;
            this.store.add(new Task(form.getValues()));
        } else {    // -- UPDDATE
            form.updateRecord(this.record);
        }
    }
};
 
Ext.preg("form-storable", Ext.ux.FormPanel.Storable);

Note how the plugin mixes several instance-methods into the FormPanel its plugged into, including #bind, #loadRecord, #reset, #storableOnSave, #storableOnCancel in addition to adding two events storable-save and storable-cancel. The nice thing about this plugin is that it saved us from having to make a custom FormPanel extension like TasksForm, for example. Plugins are not unlike ruby Mixins.

Ext.apply(panel, Ext.ux.FormPanel.Storable.InstanceMethods.prototype);

Let’s fill-out our direct-API in tasks_controller. Edit /controllers/tasks_controller.rb

class TasksController < ApplicationController
  include Rails::ExtJS::Direct::Controller
  include ExtJS::Controller
  helper ExtJS::Helpers::Store
 
  direct_actions :create, :load, :update, :destroy
  def load
    @xresponse.result = {
      :data => Task.all.collect {|u| u.to_record}
    }
    @xresponse.status = true
    render :json => @xresponse
  end
 
  def create
    data = params["data"] || params
    t = Task.create(data)
    @xresponse.result = t.to_record
    @xresponse.status = true
    @xresponse.message = "Created Task"
    render :json => @xresponse
  end
 
  def update
    data = params["data"] || params
    t = Task.find(data["id"])
    t.update_attributes(params)
    @xresponse.status = true
    @xresponse.message = "Updated Task"
    render :json => @xresponse
  end
 
  def destroy
    t = Task.find(params["id"])
    t.destroy
    @xresponse.status = true
    @xresponse.message = "Destroyed Task"
    render :json => @xresponse
  end
end

Note: Due to a bug in Ext.data.DirectProxy when using a Ext.data.JsonWriter, the POST params are not structured correctly. Thus each action in TasksController has to use this code:

    data = params["data"] || params

Once Ext-3.0.3 is released, the problem will be fixed and POST data will be placed into a key as specified by the JsonReader root property (eg: “data”).

Finally, create the last project_controller view, /views/projects/step9.html.erb.

<% @provider = get_extjs_direct_provider("remoting", "/direct") %>
 
<!-- build the application layout -->
<%
    window = extjs_component(
        "xtype" => "window",
        "id" => "projects",
        "title" => "Project Manager",
        "iconCls" => "silk-calendar",
        "closeAction" => "hide",
        "layout" => "border",
        "height" => 480,
        "width" => 800
    )
 
    workspace = window.add(extjs_component(
        "xtype" => "panel",
        "itemId" => "workspace",
        "border" => false,
        "margins" => "5 5 5 0",
        "region" => "center",
        "layout" => "card",
        "activeItem" => 0,
        "layoutConfig" =>{
            "layoutOnCardChange" => true
        }
    ))
    extjs_onready(window)
%>
 
<!-- partial:  /users/_grid -->
<%= window.add(:partial => '/users/grid',
    "itemId" => 'users-grid',
    "region" => 'west',
    "width" => 300,
    "margins" => '5 5 5 5',
    "cmargins" => '5 5 5 5',
    "collapsible" => true
) %>
 
<!-- partial:  tasks/_grid -->
<%= workspace.add(:partial => '/tasks/grid',
    "itemId" => 'tasks-grid',
    "listeners" => "ProjectsController.listeners.tasks.grid")
%>
 
<!-- partial: tasks/_form -->
<%= workspace.add(:partial => '/tasks/form',
    "itemId" => 'tasks-form',
    "listeners" => "ProjectsController.listeners.tasks.form"
) %>
 
 
 
<!-- Render the Direct provider after all partials have run -->
<%= @provider.render %>
 
<%= extjs_render %>
 
<script>
    ProjectsController = function() {
 
        var workspace = null;
        Ext.onReady(function() {
            var projects = Ext.getCmp('projects');
 
            // set workspace pointer for convenience.
            workspace = projects.getComponent('workspace');
 
            new Ext.Toolbar({
                renderTo: 'nav',
                items: [{
                    text: 'Projects',
                    iconCls: 'silk-calendar',
                    handler: function(btn, ev) {
                        projects.show(btn.el);
                    }
                }, '-', '->', 'Logged in as SomeOne (<a href="#">Logout</a>)']
            });
        });
 
        return {
            listeners: {
                tasks: {
                    grid: {
                        'add-record' : function(grid) {
                            var fpanel = workspace.getComponent('tasks-form');
                            fpanel.reset();
                            workspace.getLayout().setActiveItem(fpanel);
                        },
                        'rowdblclick' : function(grid, index, ev) {
                            var record = grid.store.getAt(index);
                            var fpanel = workspace.getComponent('tasks-form');
                            fpanel.loadRecord(record);
                            workspace.getLayout().setActiveItem(fpanel);
                        }
                    },
                    form : {
                        'storable-save' : function(fpanel) {
                            var grid = workspace.getComponent('tasks-grid');
                            workspace.getLayout().setActiveItem(grid);
                        },
                        'storable-cancel' : function(fpanel) {
                            var grid = workspace.getComponent('tasks-grid');
                            workspace.getLayout().setActiveItem(grid);
                        }
                    }
                }
            }
        }
    }();
</script>

See /projects/step9. Click the [Add] button to create a new Task and double-click grid-rows to edit. Important to note in this last example are the attached listeners from ProjectsController.listeners.task.*. Notice how we’re able to interact with all the components rendered through partials from the parent-controller, projects–our components are loosely coupled. Each partial is like an actor on a stage presented by projects_controller. Each actor is controlled by the attached listeners on ProjectsController.listeners.*.

Summary

With the relatively small extjs-mvc Gem, produced in just a few days, automated ExtJS Javascript component-rendering is now possible in a Rail/Merb friendly way. By taking advantage of the xtype mechanism introduced in Ext-2.0 along with the new ptype (plugin-type) mechanism in Ext-3.0, the entire library of ExtJS widgets can be rendered using a single plain-old-ruby-object ExtJS::Component, accessed through the helper-method extjs_component made available by the helper ExtJS::Helpers::Component . In addition, the new Writer features of Ext-3.0 are easily implemented in Rails/Merb by using the helper ExtJS::Helpers::Store, allowing one to implement both RESTful and Ext.Direct-enabled Stores.

The extjs-mvc is very new and in need of some help by a few full-time ruby-developers. It’s my hope that some of you out there will fork the Github project and help tidy things up. One interesting idea I had was to move the extjs_store method from the helper into an ActionController mixin, allowing one to define the Store in the controller. For example:

class TasksController < ApplicationController
    extjs_store :model => "task", :writer => {"encode" => false}
end

This could possibly allow for automated CRUD handling on any model without having to define the same CRUD actions over-and-over. An intelligent mixin could constantize the model defined on the extjs_store and provide :before_action, :after_action hooks.

Another idea to explore might be to move the component-configurations fed to the helper-method extjs_component from the view-templates into YAML files or the database. This would allow for easy configuration of the application-components through a backend interface. Also, components could be easily tweaked / plugins added based upon a user’s role/permissions.

A few other areas to explore would be form-building and grid-column-building helpers. So fork the Github Project. If you have some good ideas, send me a pull request.

5 Steps to Understanding Drag and Drop with Ext JS

Sunday, September 13th, 2009

One of the most powerful interaction design patterns available to developers is “Drag and Drop.” We utilize Drag and Drop without really giving it much thought – especially when its done right. Here are 5 easy steps to ensure an elegant implementation.

Defining drag and drop

A drag operation, essentially, is a click gesture on some UI element while the mouse button is held down and the mouse is moved. A drop operation occurs when the mouse button is released after a drag operation.

From a high level, drag and drop decisions can be summed up by the following flow chart.
Drag and drop decision flow chart.

To speed up our development, Ext JS provides us with the Ext.dd classes to manage the basic decisions for us. In this post, we will cover coding for the appearance and removal of the drop invitation, invalid drop repair and what happens when a successful drop occurs.

Organzing the drag and drop classes

A first glance of the classes in the Ext.dd documentation might seem a bit intimidating.  But, if we take a quick moment to look at the classes, we see that they all stem from the DragDrop class and most can be categorized into Drag or Drop groups.  With a bit more time and digging, we can see that the classes can be further categorized into single node and multiple node drag or drop interactions.

Ext JS Drag and drop class diagram.

In order to learn about the basics of drag and drop we’ll focus on applying single drag and drop interactions to DOM nodes.  To do this, we’ll utilize the DD and DDTarget classes, which provide the base implementations for their respective drag and drop behaviors.

However, we need to discuss what our objectives are before we can start implementing drag and drop.

The task at hand

Lets say we’ve been asked to develop an application that will provide a rental car company the ability to place their cars and trucks in one of three states:  available, rented or in repair status.  The cars and trucks are only allowed to be placed in their respective “available” container.

Drag and drop story.

To get started, we must make the cars and trucks “dragable”. For this, we’ll use DD. We’ll need to make the rented, repair and vehicle containers “drop targets”.  For this we’ll use DDTarget.  Lastly, we’ll use different drag drop groups to help enforce the requirement that cars and trucks can only be dropped into their respective “available” containers.

The HTML and CSS for this example is already constructed and can be downloaded here.  With that downloaded, we can begin coding by adding drag operations to the cars and trucks.

Step 1: Starting with drag

To configure the vehicle DIVs elements as dragable, we’ll need to obtain a list and loop through it to instantiate new instances of DD.  Here’s how we do it.

// Create an object that we'll use to implement and override drag behaviors a little later
var overrides = {};
// Configure the cars to be draggable
var carElements = Ext.get('cars').select('div');
Ext.each(carElements.elements, function(el) {
    var dd = new Ext.dd.DD(el, 'carsDDGroup', {
        isTarget  : false
    });
    //Apply the overrides object to the newly created instance of DD
    Ext.apply(dd, overrides);
});
 
var truckElements = Ext.get('trucks').select('div');
Ext.each(truckElements.elements, function(el) {
    var dd = new Ext.dd.DD(el, 'trucksDDGroup', {
        isTarget  : false
    });
    Ext.apply(dd, overrides);
});

All drag and drop classes are designed to be implemented by means of overriding its methods. That’s why in the above code segment, we have create an empty object called overrides, which will be filled in later with overrides specific to the action we need.

We get of list of car and truck elements by leveraging the DomQuery select method to query the cars container for all the child div elements.

To make the cars and truck elements dragable, we create a new instance of DD, passing in the car or truck element to be dragged and the drag drop group that it is to participate in. Notice that the vehicle types have their own respective drag drop group. This will be important to remember later when we setup the rented and repair containers as drop targets.

Also notice that we’re applying the overrides object to the newly created instances of DD using Ext.apply., which is a handy way to add properties or methods to an existing object.

Before we can continue with our implementation, we need to take a quick moment to analyze what happens when you drag an element on screen. With this understanding, the rest of the implementation will fall into place.

Peeking at how drag nodes are affected

The first thing you’ll notice when dragging the car or truck elements around is that they will stick wherever they are dropped. This is OK for now because we’ve just begun our implementation. What is important is to understand how the drag nodes are being affected. This will aid us in coding for the return to their original positions when they are dropped on anything that is a valid drop target, which is known as an “invalid drop”.

The below illustration uses FireBug’s HTML inspection panel and highlights the changes being made by when a drag operation is applied to the Camaro element.

Looking at how the drag operation changes the drag element's style
Click the above image to test the drag operation.

While inspecting the drag element during a drag operation, we can see a style attribute added to the element with three CSS values populated: position, top and left. Further inspection reveals that the position attribute set to relative and top and left attributes updating while the node is being dragged around.

After a the drag gesture completes, the style attribute remains along with the styles contained therein. This is what we have to clean up when we code for the repair of an invalid drop. Until we setup proper drop targets, all drop operations are considered invalid.

Step 2: Repairing an invalid drop

The path of least resistance is to repair an invalid drop by reseting the style attribute that is applied during the drag operation. This means that the drag element would disappear from under the mouse and reappear where it originated and would be quite boring. To make it smoother, we’ll use Ext.Fx to animate this action.

Remember that the drag and drop classes were designed to have methods overridden. To implement repair, we’ll need to override the b4StartDrag, onInvalidDrop and endDrag methods.

Lets add the following methods to our overrides object above and we’ll discuss what they are and do.

// Called the instance the element is dragged.
b4StartDrag : function() {
    // Cache the drag element
    if (!this.el) {
        this.el = Ext.get(this.getEl());
    }
 
    //Cache the original XY Coordinates of the element, we'll use this later.
    this.originalXY = this.el.getXY();
},
// Called when element is dropped not anything other than a dropzone with the same ddgroup
onInvalidDrop : function() {
    // Set a flag to invoke the animated repair
    this.invalidDrop = true;
},
// Called when the drag operation completes
endDrag : function() {
    // Invoke the animation if the invalidDrop flag is set to true
    if (this.invalidDrop === true) {
        // Remove the drop invitation
        this.el.removeClass('dropOK');
 
        // Create the animation configuration object
        var animCfgObj = {
            easing   : 'elasticOut',
            duration : 1,
            scope    : this,
            callback : function() {
                // Remove the position attribute
                this.el.dom.style.position = '';
            }
        };
 
        // Apply the repair animation
        this.el.moveTo(this.originalXY[0], this.originalXY[1], animCfgObj);
        delete this.invalidDrop;
    }
 
},

In the above code, we begin by overriding the b4StartDrag method, which is called the instant the drag element starts being dragged around screen and makes it an ideal place to cache the drag element and original XY coordinates – which we will use later on in this process.

Next, we override onInvalidDrop, which is is called when a drag node is dropped on anything other than a drop target that is participating in the same drag drop group. This override simply sets a local invalidDrop property to true, which will be used in the next method.

The last method we override is endDrag, which is called when the drag element is no longer being dragged around screen and the drag element is no longer being controlled by the mouse movements. This override will move the drag element back to its original X and Y position using animation. We configured the animation to use the elasticOut easing to provide a cool and fun bouncy effect at end of the animation.

Looking at how the drag repair operation works
Click the above image to view the animated repair operation in action.

OK, now we have the repair operation complete. In order for it to work on the drop invitation and valid drop operations, we need to setup the drop targets.

Step 3: Configuring the drop targets

Our requirements dictate that we will allow cars and trucks to be in be dropped in the rented and repair containers as well as their respective original containers. To do this, we’ll need to instantiate instances of the DDTarget class.

Here’s how its done.

//Instantiate instances of Ext.dd.DDTarget for the cars and trucks container
var carsDDTarget    = new Ext.dd.DDTarget('cars','carsDDGroup');
var trucksDDTarget = new Ext.dd.DDTarget('trucks', 'trucksDDGroup');
 
//Instantiate instnaces of DDTarget for the rented and repair drop target elements
var rentedDDTarget = new Ext.dd.DDTarget('rented', 'carsDDGroup');
var repairDDTarget = new Ext.dd.DDTarget('repair', 'carsDDGroup');
 
//Ensure that the rented and repair DDTargets will participate in the trucksDDGroup 
rentedDDTarget.addToGroup('trucksDDGroup');
repairDDTarget.addToGroup('trucksDDGroup');

In the above code snippet, we have setup drop targets for the cars, trucks, rented and repair elements. Notice that the cars container element only participates in the “carsDDGroup” and the trucks container element participates in the “trucksDDGroup”. This helps enforce the requirement that cars and trucks can only be dropped in their originating container.

Next, we instantiate instances DDTarget for the rented and repair elements. Initially, they are configured to only participate in the “carsDDGroup”. In order to allow them to participate in the “trucksDDGroup”, we have to add it by means of addToGroup.

OK, now we’ve configured our drop targets. Lets see what happens when we drop the cars or trucks on a valid drop element.

Click to see the partially completed drop operation in action.
Click the above image see the progress thus far.

In exercising the drop targets, we see that the drag element stays exactly its dropped. That is, images can be dropped anywhere on a drop target and stay there. This means that our drop implementation is not complete.

To complete it, we need to actually code for the “complete drop” operation, by means of another override for the instances of DD that we created some time ago.

Step 4: Completing the drop

To complete the drop, we will need to actually drag the element from its parent element to the drop target element using DOM tools. This is accomplished by overriding the DD onDragDrop method.

Add the following method to the overrides object.

// Called upon successful drop of an element on a DDTarget with the same
onDragDrop : function(evtObj, targetElId) {
    // Wrap the drop target element with Ext.Element
    var dropEl = Ext.get(targetElId);
 
    // Perform the node move only if the drag element's 
    // parent is not the same as the drop target
    if (this.el.dom.parentNode.id != targetElId) {
 
        // Move the element
        dropEl.appendChild(this.el);
 
        // Remove the drag invitation
        this.onDragOut(evtObj, targetElId);
 
        // Clear the styles
        this.el.dom.style.position ='';
        this.el.dom.style.top = '';
        this.el.dom.style.left = '';
    }
    else {
        // This was an invalid drop, initiate a repair
        this.onInvalidDrop();
    }

In the above override, the drag element is moved to the drop target element, but only if it is not the same as the drag element’s parent node. After the drag element is moved, the styles are cleared from it.

If the drop element is the same as the drag element’s parent, we ensure a repair operation occurs by calling this.onInvalidDrop.

Click to see the completed drop operation in action.
Click the above image to see the complete drop operation in action.

Upon a successful drop, the drag elements will now will be moved from their parent element to the drop target.

How does the user know if they are hovering above a valid drop target? We’ll give the user some visual feedback by configuring the drop invitation.

Step 5: Adding drop invitation

In order to make drag and drop a bit more useful, we need to provide feedback to the user on whether or not a drop operation can successfully occur. This means that we’ll have to override the onDragEnter and onDragOut methods

Add these last two methods to the overrides object.

// Only called when the drag element is dragged over the a drop target with the same ddgroup
onDragEnter : function(evtObj, targetElId) {
    // Colorize the drag target if the drag node's parent is not the same as the drop target
    if (targetElId != this.el.dom.parentNode.id) {
        this.el.addClass('dropOK');
    }
    else {
        // Remove the invitation
        this.onDragOut();
    }
},
// Only called when element is dragged out of a dropzone with the same ddgroup
onDragOut : function(evtObj, targetElId) {
    this.el.removeClass('dropOK');
}

In the above code, we override the onDragEnter and onDragOut methods, both of which are only utilized when the drag element is interacting with a drop target participating in the same drag drop group.

The onDragEnter method is only called when the mouse cursor first intersects the boundaries of a drop target while a drag item is in drag mode. Likewise, onDragOut is called when the mouse cursor is first dragged outside the boundaries of the drop target while in drag mode.

Click to see the drop invitation.
Click the above image to see the drop invitation.

By adding overrides to the onDragEnter and onDragOut methods we can see that the background of the drag element will turn green when the mouse cursor first intersects a valid drop target and will lose its green background when it leaves the drop target or is dropped. This completes our implementation of drag and drop with DOM elements.

It doesn’t stop here

Drag and drop can be a can be applied to mostly everything in the Ext JS framework. Here are a few examples that you can use to learn how to implement drag and drop with various widgets:

Summary

Today, we learned how to implement end to end drag and drop of DOM nodes using the first-level drag and drop implementation classes. From a high-level, we defined and discussed what drag and drop is and how to think about it in terms of the framework.

We also learned that the drag and drop classes can be grouped by drag or drop behaviors and whether or not they support single or multiple drag or drop operations. While implementing this behavior, we illustrated that the dd classes help make some of the behavioral decisions, and that we are responsible for coding the end-behaviors.

We hope you’ve enjoyed this thorough look at some fundamental drag and drop operations with DOM nodes. We look forward to bringing you more articles about this topic in the future.