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.
Step 1: A Fresh Start
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
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
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
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
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
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-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
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
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/