Ext

Ext JS on Rails: A ComprehensiveTutorial

September 30, 2009 by Chris Scott

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.

34 Responses to “Ext JS on Rails: A ComprehensiveTutorial”

  1. Ed Spencer

    I went down this route of generating Ext code with Rails some time ago but never got it working in an elegant way…

    Generating JavaScript from Ruby felt like generating Ruby from PHP – you can do it but it tends to restrict you when you need to do that elaborate customisation or hack, or when you just want to mess around with JavaScript in the browser.

    That said, you’re probably rather more intelligent than me so good luck! It’s great to see a novel-sized amount of information about tying together these two technologies :)

  2. Tweets that mention Ext JS - Blog -- Topsy.com

    [...] This post was mentioned on Twitter by Colin Ramsay and Nils Dehl. Nils Dehl said: RT @extjs New blog post: Ext JS on Rails: A ComprehensiveTutorial http://bit.ly/3jj4h0 [...]

  3. Chris Scott

    @Ed Spencer This is my 3rd trip down this road in both PHP and Rails1.x with Ext-1.x.

    This method will is far less intrusive, I think. Using Ext’s xtype/ptype configurations and one plain-old-ruby object, ExtJS::Component, the whole library of components can be rendered, instead of trying to wrap the entire ExtJS component hierarchy in Ruby, PHP, etc.

  4. Praveen

    while it feels more Rails like, I, in general have found code generation to be of limited use. Very soon you start running into special cases that require even more flags to be added to the generator. A better approach , IMO, is to start out with skeletons, then modify each component individually. If something is truly generic for your project, isolate it as a base component. Or isolate frequently used methods in your project in an object and then ‘inject’ those methods in your components. Sort of like Traits. I posted this technique at my blog

    Almost all modern IDEs support skeletons and I’ve my own Emacs snippets for Grids and Forms etc.

  5. Chris Scott

    @Praveen Your “Traits” are compatible and encouraged with this pattern. Simply define an xtype for the components that you augment with some Trait.

  6. Steffen Hiller

    Hey Chris,

    very nice write up! There are definitely a ton of nice ideas in your gem!

    Any chance of open sourcing the demo application on github to get a better feeling of the whole integration?

    Cheers,
    Steffen

  7. Chris Scott

    @Steffen Done. Added demo-app.tar.gz to the gem in version 0.1.31

  8. Chris Scott

    Oops, found a recursion bug in ExtJS::Model#to_record. was assuming the belongs_to :user existed. I forgot to add “allowBlank”: false on the user-field in TaskForm, doh!

  9. Jerry

    I think that it’s a very good starting point. But my choice was lipsiadmin. http://lipsiadmin.com

  10. Sawood Alam

    Interesting!

    I was looking for an elegant way of tying together these two, for long time. But I will have to wait until an ext-3.0.1+ public release is available to actually play around it.

    Best

  11. Chris Scott

    It’s great to see a number of other Github projects out there, especially ones using Ruby generators.

    I think ext-mvc is pretty interesting and worth a serious look.
    http://github.com/extmvc/extmvc

    Unfortunately, my Gem, extjs-mvc, is very similarly named.

  12. Kevin English

    Great gem. Noticed this:

    @param {Boolean} script_tag [true] Not yet implemented. Always renders tags.

    I implemented it.

    Any plans to do a store that can do pagination for a grid component?

    Aloha,
    Kevin

  13. miki

    Pity that the Zend Framework is not a good and stable support for extjs.

  14. links for 2009-10-01 « pabloidz

    [...] Ext JS on Rails: A ComprehensiveTutorial (tags: rails extjs) TagsCategoriasmiudezas Uncategorized   [...]

  15. Boris Barroso

    Good work chris, but I haven’t seen any testing, a little shoulda but I would like to know how can you work using TDD/BDD to create ExtJS applications with your gem. Are you planning to create a test suite for testing.
    Thanx

  16. links for 2009-10-01 « sySolution

    [...] Ext JS – Blog (tags: extjs rails) [...]

  17. Chris Scott

    @Kevin English Feel free to send a pull-request to me and I’ll integrate your changes. About doing a “store with pagination”, I think that’s beyond the scope of this Gem currently. I’ll setup a paging-grid and see what it takes.

    @Boris Yea, no testing yet. The gem is pretty much an infant. It’s due.

  18. Ebencosoft

    Hey that was splendid and we need of this detailed n helpful tutorials
    thanks to u all extjs team for such great i dear

  19. Nat Gadgibalaev

    Hi!

    First of all — it’s a great work, thank you.

    I have kind of a big project, based on ExtJS 3.0 plus Rails 2.3.*. When i looked at previous solution to wrap JS development in Ruby — they looked pretty wired, too abstract and too much limits. Your approach is much better, but still — the JS code generated on server looks weird. And i’m pretty sure it’s harder to test.

    What do i mean?
    I mean it’s really more structured and easy to test/spec in isolation — your Ruby code and the Javascript code.
    You do understand, that even in the js part, datastores and components are tested via JSUnit, jsSpec or friends (i use inspec), but still, buttons and interface interactions are better covered with selenium? Thats basically it! You do not need to mashup erevything in your server side, or it will become a huge dinosaur, unstable and uncontrollable at all.

    My methods here:

    1) Write a clean, smart Ruby code, test everything in isolation with RSpec. (REST API)
    2) Write a clean JS code with server mocked up (change URLs to .json files) and test it with JS BDD framwerowk + Selenium
    3) Use Cucumber + Webrat to test everything in integration.

    It’s way too expensive for small little projects, but for really big systems, or just in case you’re building such a systems from day to day — that’s a good solution, isn’t it?

  20. andrew blunt

    This is a well detailed instruction of Extic JS topic that I’m looking for. I tried to follow all the codes and got it working. Now I can move on with smoking my favorite cigars online because I made it with no hassle. Thanks for sharing!

  21. Chris Scott

    @Andrew Blunt: Awesome, good to hear it works on someone else’s platform.

  22. Adam Grant

    Phew! This post kind of took my breath away!

    I agree with Ed on this one, that Rails generated code, while very well executed here (best I’ve seen by a long shot), I think negates the real benefits of domain abstraction, ie Server vs. Client side. The Ext side can stay the same while you switch out the backend server (PHP, Rails, etc). Now, practically speaking, it’s a rare thing for that to happen, but it does and can cause problems if your JS is tied intimately to your server.

    For small simple widgets, this might be very helpful, although from my experience, Ext is complex enough that merging ExtJS and Rails might be overkill (”Something doesn’t work, who can I blame!?!?!?)

    But, I love innovation, and this is definitely something to be proud of. Keep up the good work Chris, and I will look into employing it in my app! I know I’m very close to using the rails-direct adapter in my latest project, so that is definitely solid work.

    Thanks!

  23. Chris Scott

    @Adam Grant: Thanks kindly for the wise feedback.

    I like Ed’s stuff too, it’s stunningly great. It’s like Rails ported to ExtJS. Lots of great things to do done there. I was playing around running his stuff with Rack/Thin instead of Apache.

    I disagree that being able to migrate between dev platforms, PHP, Rails. etc is *very* important.

    extjs-mvc works with Merb/DataMapper now, as v 0.2.0.

    It’s mostly about rendering the ORM.

  24. alldevnet.com

    Ext JS on Rails: A ComprehensiveTutorial…

    Ext JS on Rails: A ComprehensiveTutorial…

  25. cigars online

    This is a well detailed instruction of Extic JS topic that I’m looking for. I tried to follow all the codes and got it working. Now I can move on with smoking my favorite cigars online because I made it with no hassle. Thanks for sharing!

  26. Art

    This is exactly what I looking for (a month ago T T), but I will use this for my next projects.

    Thanks

  27. suresh

    hey i am unable to store users into database plz help me out

  28. Get Girlfriend

    Good work chris have a good day

  29. yog

    This is a well detailed instruction of Extic JS topic that I’m looking for.

    Thanks

  30. fyname

    about rails-extjs-direct

    question:rails Back to the data not the data needed for extjs
    Use firebug view the post is “(” action “:” Cptcategory “,” method “:” index “,” data “: [" 0 "],” type “:” rpc “,” tid “: 2)”

    Response: is html language other than the data they need

    rails code is:

    include Rails::ExtJS::Direct::Controller
    direct_actions:index

    def index(id=[params[:node])

    puts id.to_i

    plcs = Cptcategory.find_children(id)
    data =plcs.map {|r| r.attributes}
    return data
    end

    I want to ask, how is the rails to use rails-extjs-direct reply is extjs be able to parse data?
    Please contact me, thanks email: fy_name@yahoo.com.cn

  31. beat making software

    This method will is far less intrusive

  32. erik scheirer

    I think this is great stuff — one of the main problems (if you agree that you want to use ruby and rails to control and generate the javascript) of using plain ol’ javascript is that you end up really living in two worlds, but in a bad way.

    What ends up happening is ‘poltergeist’ anti-pattern stuff where since your data model can easily diverge, the logic can diverge along with it; and thus it might require a lot of ‘context specific’ handling which then leads to a poltergeist or two. Bad because the code gets brittle and quality suffers.

    At least, since we do have to live in a post-pure-html world, if we can coordinate the generation of the javascript side of things with the actual normal functioning and code of the underlying MVC implementation (rails in my case) then we have minimized the possibility of getting haunted …

    Great work, and thanks so much for not only making this available, but writing up such a comprehensive tutorial!

    I want to encourage the further development of syntax like what you ended with:

    class TasksController “task”, :writer => {”encode” => false}
    end

    I will try to contribute something towards this end, as my time allows.

    cheers,
    e

  33. erik scheirer

    Quick note, at the end of step1 the URL to hit is going to be http://localhost:3000/projects/step1

    since no other route has been defined at this point in the tutorial.

  34. erik scheirer

    Would it be possible to update this for extjs 3.0.3 ? Building this tutorial manually is yielding some problems with 3.0.3 differences, and can be a block to folks who don’t know extjs well yet (like me):

    1. the user.json data load takes > 500ms when run all locally, this is for a one-record dataset in sqlite3. This is very, very slow performance.

    2. the USersGrid doesn’t actually load the data even though its received via the Store.

    3. in the section ‘RESTful, writable stores’ its not clear where the snippet goes (so I may have put it in the wrong place):

    “users”,
    :writer => {
    :encode => false
    },
    :config => {
    “autoLoad” => true,
    “autoSave” => true, # true, #

Leave a Reply

To prove that you're not a bot, please answer this question:



© 2006-2009 Ext, LLC