Ext JS - AJAX Framework

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”> </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.

Step1

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” -> {“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: Organizing 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.

If you like this post, share it with your friends!

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

  1. Ed Spencer says:

    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. Chris Scott says:

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

  3. Praveen says:

    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.

  4. Chris Scott says:

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

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

  6. Chris Scott says:

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

  7. Chris Scott says:

    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!

  8. Jerry says:

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

  9. Sawood Alam says:

    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

  10. Chris Scott says:

    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.

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

  12. miki says:

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

  13. Boris Barroso says:

    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

  14. Chris Scott says:

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

  15. Ebencosoft says:

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

  16. 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?

  17. andrew blunt says:

    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!

  18. Chris Scott says:

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

  19. Adam Grant says:

    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!

  20. Chris Scott says:

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

  21. 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!

  22. Art says:

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

    Thanks

  23. suresh says:

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

  24. Good work chris have a good day

  25. yog says:

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

    Thanks

  26. fyname says:

    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

  27. erik scheirer says:

    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

  28. erik scheirer says:

    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.

  29. erik scheirer says:

    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, #

  30. NP says:

    I tried doing this tutorial to step 9. I can’t seem to get the grid to populate with data in the user table or successfully save a task that’s entered. it just keeps showing saving / loading icon. The “GET” call is successful as i see the data in Firebug. In earlier steps, i didn’t see data populating to the user grid either? Any help would be appreciated?

  31. Jason Boxman says:

    I also came across this alternate implementation, which seems to let you go wild with many ActiveRecord methods. You can pass raw SQL to find, but I don’t suggest it for obvious reasons…

    http://github.com/stonegao/active-direct

  32. Jason Boxman says:

    Also, you’d want to look at resource_controller on github for ideas on how to automagically provide the crud methods. While it’s for restful actions, the same kind of logic can be applied here.

  33. smbrant says:

    Hi Chris! Very nice article.

    I’d like to show you an other way to implement the same functionality, using the rwt plugin.

    If you have time, please take a look and send me a comment. Thank you.

    http://access.net.br/wiki/rwt/Translating_Chris_Scott%27s_post_to_Rwt

  34. jeux gratuit says:

    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!

  35. STE says:

    This tutorial is very instructive and well explained and simple to understand for extjs’ newbies like me. OTOH, i’m facing a problem I can’t solve. I’m stuck at the step6. The user list works for read, update, delete but there is nothing to do for adding a new record to the database. I did again the whole tutorial both on a macosx (with ruby, safari…) and on a windows (with jruby, IE8..). I’ve tried to debug with some alerts message. The EditorGridPanel sends well the new record to the JsonStore but then nothing is sent to the server even a small alert or failure message… I’m probably missing something.

    PS: I’m running with all the latests updates I could find :
    - Extjs 3.1.0
    - Rails 3.2.5
    - extjs-mvc 0.2.8

  36. STE says:

    I solved my problem by removing the :id from the User Model class in the extjs_fields. The only explanation I can give is to quote Chris’ comment : “A Bug in Ext-3.0.1 requires id as a field currently”.

    Step6
    app\models\user.rb :

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

  37. Software says:

    Intersting! I make only the first steps in programming. I need to know it for my job. I’m an IT controller and to fullfill my job well I have to learn to read and make up logarithms. It turned out to be a retty tough task. I found some books in programming at the rapidshare http://billgable.com but sometimes I have even more questions than before reading. The case you gave here is of some interest to me as a professional. So thanks for practice.

  38. lida says:

    n to take advantage of more of the ssnative functc

  39. lida says:

    n to take advantage of more of the ssnative functe

  40. Chris Scott says:

    @Jason Boxman That ActiveDirect looks wicked, much better than my first Rails implementation. I was hoping someone would come up with something better.

  41. Chris Scott says:

    @erik scheirer: Thanks dood.

  42. Chris Scott says:

    @erik scheirer Seems I borked something with a recent, unrelated update to the Heroku app. I made some significant changes to the extjs-mvc gem since this post.

  43. Hi all,

    I really like what I’m seeing, but is there (or will there ever be) the possibility to use extjs-mvc with ActiveLdap?

  44. ks says:

    Thanks for this. Really good stuff.

  45. Anton says:

    Hi!
    Thank your for this post… maybe, you can help me with that:

    Following your tutorial, faced this problems:

    render(:json => {:success => true, :data => rs}) : when i do like that in controller, my brouser offers me to save or open json file, nothing to show.

    Doing like that helps:

    def index
    @users= (User.find :all, :o rder => “goal_id”).collect {|n| n.to_record}
    respond_to do |format|
    format.html # index.html.erb
    format.json { render(:json => {:success => true, :data => @users})}
    end
    end

    I see grid, in firebug i see data, but no data in grid! Forebug reports about error in ExtJS “this.ds is unfefined ” Maybe, it’s a reason?

    What can it be?
    Thank you in advance!

Leave a Reply

© 2006-2010 Ext JS, Inc.