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.
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 projectsCreate an erb template named app/views/projects/step1.html.erb to make sure we’re cooking.
<h1>Step1</h1>
Go ahead and start your server. You should see something like this
Step 2: Auto-generating Ext components in Rails
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: Orgainizing 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.



Posted on September 30th, 2009 at 12:29 pm
I went down this route of generating Ext code with Rails some time ago but never got it working in an elegant way…
Generating JavaScript from Ruby felt like generating Ruby from PHP – you can do it but it tends to restrict you when you need to do that elaborate customisation or hack, or when you just want to mess around with JavaScript in the browser.
That said, you’re probably rather more intelligent than me so good luck! It’s great to see a novel-sized amount of information about tying together these two technologies
Posted on September 30th, 2009 at 12:32 pm
[...] This post was mentioned on Twitter by Colin Ramsay and Nils Dehl. Nils Dehl said: RT @extjs New blog post: Ext JS on Rails: A ComprehensiveTutorial http://bit.ly/3jj4h0 [...]
Posted on September 30th, 2009 at 12:45 pm
@Ed Spencer This is my 3rd trip down this road in both PHP and Rails1.x with Ext-1.x.
This method will is far less intrusive, I think. Using Ext’s xtype/ptype configurations and one plain-old-ruby object, ExtJS::Component, the whole library of components can be rendered, instead of trying to wrap the entire ExtJS component hierarchy in Ruby, PHP, etc.
Posted on September 30th, 2009 at 1:01 pm
while it feels more Rails like, I, in general have found code generation to be of limited use. Very soon you start running into special cases that require even more flags to be added to the generator. A better approach , IMO, is to start out with skeletons, then modify each component individually. If something is truly generic for your project, isolate it as a base component. Or isolate frequently used methods in your project in an object and then ‘inject’ those methods in your components. Sort of like Traits. I posted this technique at my blog
Almost all modern IDEs support skeletons and I’ve my own Emacs snippets for Grids and Forms etc.
Posted on September 30th, 2009 at 1:26 pm
@Praveen Your “Traits” are compatible and encouraged with this pattern. Simply define an xtype for the components that you augment with some Trait.
Posted on September 30th, 2009 at 1:39 pm
Hey Chris,
very nice write up! There are definitely a ton of nice ideas in your gem!
Any chance of open sourcing the demo application on github to get a better feeling of the whole integration?
Cheers,
Steffen
Posted on September 30th, 2009 at 2:35 pm
@Steffen Done. Added demo-app.tar.gz to the gem in version 0.1.31
Posted on September 30th, 2009 at 5:00 pm
Oops, found a recursion bug in ExtJS::Model#to_record. was assuming the belongs_to :user existed. I forgot to add “allowBlank”: false on the user-field in TaskForm, doh!
Posted on September 30th, 2009 at 6:33 pm
I think that it’s a very good starting point. But my choice was lipsiadmin. http://lipsiadmin.com
Posted on September 30th, 2009 at 8:21 pm
Interesting!
I was looking for an elegant way of tying together these two, for long time. But I will have to wait until an ext-3.0.1+ public release is available to actually play around it.
Best
Posted on September 30th, 2009 at 8:29 pm
It’s great to see a number of other Github projects out there, especially ones using Ruby generators.
I think ext-mvc is pretty interesting and worth a serious look.
http://github.com/extmvc/extmvc
Unfortunately, my Gem, extjs-mvc, is very similarly named.
Posted on October 1st, 2009 at 12:02 am
Great gem. Noticed this:
@param {Boolean} script_tag [true] Not yet implemented. Always renders tags.
I implemented it.
Any plans to do a store that can do pagination for a grid component?
Aloha,
Kevin
Posted on October 1st, 2009 at 6:18 am
Pity that the Zend Framework is not a good and stable support for extjs.
Posted on October 1st, 2009 at 8:03 am
[...] Ext JS on Rails: A ComprehensiveTutorial (tags: rails extjs) TagsCategoriasmiudezas Uncategorized [...]
Posted on October 1st, 2009 at 9:57 am
Good work chris, but I haven’t seen any testing, a little shoulda but I would like to know how can you work using TDD/BDD to create ExtJS applications with your gem. Are you planning to create a test suite for testing.
Thanx
Posted on October 1st, 2009 at 11:01 am
[...] Ext JS – Blog (tags: extjs rails) [...]
Posted on October 1st, 2009 at 11:28 am
@Kevin English Feel free to send a pull-request to me and I’ll integrate your changes. About doing a “store with pagination”, I think that’s beyond the scope of this Gem currently. I’ll setup a paging-grid and see what it takes.
@Boris Yea, no testing yet. The gem is pretty much an infant. It’s due.
Posted on October 2nd, 2009 at 2:48 pm
Hey that was splendid and we need of this detailed n helpful tutorials
thanks to u all extjs team for such great i dear
Posted on October 3rd, 2009 at 7:09 pm
Hi!
First of all — it’s a great work, thank you.
I have kind of a big project, based on ExtJS 3.0 plus Rails 2.3.*. When i looked at previous solution to wrap JS development in Ruby — they looked pretty wired, too abstract and too much limits. Your approach is much better, but still — the JS code generated on server looks weird. And i’m pretty sure it’s harder to test.
What do i mean?
I mean it’s really more structured and easy to test/spec in isolation — your Ruby code and the Javascript code.
You do understand, that even in the js part, datastores and components are tested via JSUnit, jsSpec or friends (i use inspec), but still, buttons and interface interactions are better covered with selenium? Thats basically it! You do not need to mashup erevything in your server side, or it will become a huge dinosaur, unstable and uncontrollable at all.
My methods here:
1) Write a clean, smart Ruby code, test everything in isolation with RSpec. (REST API)
2) Write a clean JS code with server mocked up (change URLs to .json files) and test it with JS BDD framwerowk + Selenium
3) Use Cucumber + Webrat to test everything in integration.
It’s way too expensive for small little projects, but for really big systems, or just in case you’re building such a systems from day to day — that’s a good solution, isn’t it?
Posted on October 6th, 2009 at 8:41 am
This is a well detailed instruction of Extic JS topic that I’m looking for. I tried to follow all the codes and got it working. Now I can move on with smoking my favorite cigars online because I made it with no hassle. Thanks for sharing!
Posted on October 6th, 2009 at 9:41 am
@Andrew Blunt: Awesome, good to hear it works on someone else’s platform.
Posted on October 6th, 2009 at 6:15 pm
Phew! This post kind of took my breath away!
I agree with Ed on this one, that Rails generated code, while very well executed here (best I’ve seen by a long shot), I think negates the real benefits of domain abstraction, ie Server vs. Client side. The Ext side can stay the same while you switch out the backend server (PHP, Rails, etc). Now, practically speaking, it’s a rare thing for that to happen, but it does and can cause problems if your JS is tied intimately to your server.
For small simple widgets, this might be very helpful, although from my experience, Ext is complex enough that merging ExtJS and Rails might be overkill (”Something doesn’t work, who can I blame!?!?!?)
But, I love innovation, and this is definitely something to be proud of. Keep up the good work Chris, and I will look into employing it in my app! I know I’m very close to using the rails-direct adapter in my latest project, so that is definitely solid work.
Thanks!
Posted on October 6th, 2009 at 7:33 pm
@Adam Grant: Thanks kindly for the wise feedback.
I like Ed’s stuff too, it’s stunningly great. It’s like Rails ported to ExtJS. Lots of great things to do done there. I was playing around running his stuff with Rack/Thin instead of Apache.
I disagree that being able to migrate between dev platforms, PHP, Rails. etc is *very* important.
extjs-mvc works with Merb/DataMapper now, as v 0.2.0.
It’s mostly about rendering the ORM.
Posted on October 7th, 2009 at 1:24 pm
Ext JS on Rails: A ComprehensiveTutorial…
Ext JS on Rails: A ComprehensiveTutorial…
Posted on October 12th, 2009 at 12:06 pm
This is a well detailed instruction of Extic JS topic that I’m looking for. I tried to follow all the codes and got it working. Now I can move on with smoking my favorite cigars online because I made it with no hassle. Thanks for sharing!
Posted on October 15th, 2009 at 3:18 pm
This is exactly what I looking for (a month ago T T), but I will use this for my next projects.
Thanks
Posted on October 16th, 2009 at 10:52 am
hey i am unable to store users into database plz help me out
Posted on October 23rd, 2009 at 11:05 am
Good work chris have a good day
Posted on October 24th, 2009 at 2:09 am
This is a well detailed instruction of Extic JS topic that I’m looking for.
Thanks
Posted on November 5th, 2009 at 10:19 pm
about rails-extjs-direct
question:rails Back to the data not the data needed for extjs
Use firebug view the post is “(” action “:” Cptcategory “,” method “:” index “,” data “: [" 0 "],” type “:” rpc “,” tid “: 2)”
Response: is html language other than the data they need
rails code is:
include Rails::ExtJS::Direct::Controller
direct_actions:index
def index(id=[params[:node])
puts id.to_i
plcs = Cptcategory.find_children(id)
data =plcs.map {|r| r.attributes}
return data
end
I want to ask, how is the rails to use rails-extjs-direct reply is extjs be able to parse data?
Please contact me, thanks email: fy_name@yahoo.com.cn
Posted on November 13th, 2009 at 5:25 pm
This method will is far less intrusive
Posted on November 18th, 2009 at 5:12 pm
I think this is great stuff — one of the main problems (if you agree that you want to use ruby and rails to control and generate the javascript) of using plain ol’ javascript is that you end up really living in two worlds, but in a bad way.
What ends up happening is ‘poltergeist’ anti-pattern stuff where since your data model can easily diverge, the logic can diverge along with it; and thus it might require a lot of ‘context specific’ handling which then leads to a poltergeist or two. Bad because the code gets brittle and quality suffers.
At least, since we do have to live in a post-pure-html world, if we can coordinate the generation of the javascript side of things with the actual normal functioning and code of the underlying MVC implementation (rails in my case) then we have minimized the possibility of getting haunted …
Great work, and thanks so much for not only making this available, but writing up such a comprehensive tutorial!
I want to encourage the further development of syntax like what you ended with:
class TasksController “task”, :writer => {”encode” => false}
end
I will try to contribute something towards this end, as my time allows.
cheers,
e
Posted on November 19th, 2009 at 10:08 am
Quick note, at the end of step1 the URL to hit is going to be http://localhost:3000/projects/step1
since no other route has been defined at this point in the tutorial.
Posted on November 19th, 2009 at 11:36 am
Would it be possible to update this for extjs 3.0.3 ? Building this tutorial manually is yielding some problems with 3.0.3 differences, and can be a block to folks who don’t know extjs well yet (like me):
1. the user.json data load takes > 500ms when run all locally, this is for a one-record dataset in sqlite3. This is very, very slow performance.
2. the USersGrid doesn’t actually load the data even though its received via the Store.
3. in the section ‘RESTful, writable stores’ its not clear where the snippet goes (so I may have put it in the wrong place):
“users”,
:writer => {
:encode => false
},
:config => {
“autoLoad” => true,
“autoSave” => true, # true, #