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.

 

 

http://www.sencha.com/blog/2009/09/30/ext-js-on-rails-a-comprehensivetutorial/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值