Keeping it dry: Generating JavaScript models from Rails models

August 12, 2008 – 8:22 pm

As I’ve mentioned before, I advocate using a Model-View-Controller pattern for certain types of JavaScript-heavy web-app clients. In spite of recent licensing issues, I still think ExtJS is among the better libraries supporting MVC. For example, (if you don’t know what namespace/using are, please read this)

namespace('dk.okooko.model');
using(dk.okooko.model).run(function(m){
        
        m.OrderItem = Ext.data.Record.create([
          {"type": "int", "name": "id"},
          {"type": "int", "name": "product_id"},
          {"type": "int", "name": "quantity"},
          {"type": "int", "name": "order_id"},
          {"type": "date", "format": "d/m/Y-H:i", "name": "created_at"},
          {"type": "date", "format": "d/m/Y-H:i", "name": "updated_at"}
        ]);

});

The ‘Record.create’ function gives a way of succinctly creating constructor functions for model objects which are supported throughout the Ext functions; you can read more about it here.

If you are using Ruby on Rails at server-side (something which I have been doing quite a bit recently), then you realize that you are manually duplicating all the rails model classes in JavaScript. For example, here is the corresponding migration:

class CreateOrderItems < ActiveRecord::Migration
  def self.up
    create_table :order_items do |t|
      t.integer :product_id
      t.integer :quantity
      t.integer :order_id

      t.timestamps
    end
  end

  def self.down
    drop_table :order_items
  end
end

I was annoyed by this lack of dryness: whenever I changed the migration, I’d have to manually change the corresponding JS model. So I slapped together a really simple code generator which can create the model files from the rails models. It adds a method acts_as_jsmodel which is intended to be called by a rails model class, e.g.,

class OrderItem < ActiveRecord::Base
  belongs_to :order
  belongs_to :product
  has_one :order_item_status
  acts_as_jsmodel
end

So you can annotate model classes that you’d like to generate js models for. The acts_as_jsmodel causes a file to be written to public/javascripts/dk/okooko/model (generally, it depends on a constant APP_NAMESPACE).

Here is the code for the generator:

module Trifork
  module JSModel 
          
    JS_MODEL_DIR = "public/javascripts/#{APP_NAMESPACE}/model".gsub!('.','/')
    MODEL_PACKAGE = APP_NAMESPACE + '.model'
    def acts_as_jsmodel
      
      export_record "#{JS_MODEL_DIR}/#{self}.js" 
      rescue 
        nil
    end
      
    def export_record(fn)
      File.open(fn,'w') do |f|
        s =<<END
namespace('#{MODEL_PACKAGE}');
using(#{MODEL_PACKAGE}).run(function(m){
        
        m.#{self} = Ext.data.Record.create([
          #{self.to_record}
        ]);

});
END
        f.write s
      end
    end
      
      
    def to_record(spec = {},datefm=nil)
      spec[:exclude] ||= []
      keys = spec[:only] ? [*spec[:only]] : 
              (columns.map &:name).reject {|a| spec[:exclude].include?a}
      props = columns_hash
      
      keys.map do |k|
        p = props[k.to_s]
        if p.nil?
          raise "Property #{p} is not a column of #{self}"
        end
        {:name => p.name}.merge!(Trifork::JSModel::to_ext_type(p.type,datefm)).to_json
      end.join(",\n          ")
      
    end
     
    
    def self.to_ext_type(t,datefm)
      type = case t
      when :integer then 'int'
      when :string, :text then 'string' 
      when :boolean then 'bool' 
      when :datetime then 'date'
      else
        'auto'
      end
      res = {:type => type}
      res.merge!(:format => (datefm || 'd/m/Y-H:i')) if t==:datetime
      res
    end
    
  end
end

You can download the files from this entry here.

  1. 5 Responses to “Keeping it dry: Generating JavaScript models from Rails models”

  2. I’ve had a similar idea in the past but found that you can’t safely regenerate your JS models this way (or at least I can’t) because the JS models often contain additional model logic. I’ve been creating an MVC framework on top of ExtJS for a few months and a typical model looks like this:

    AdFunded.models.AdvertisingCategory = new Ext.ux.MVC.model.Base(‘advertising_category’, {
    fields: [
    { name: 'id', type: 'int'},
    { name: 'name', type: 'string'},
    { name: 'description', type: 'string'}
    ],
    controller_name: ‘AdvertisingCategoriesController’,
    human_plural_name: ‘Advertising Categories’,
    url_name: ‘advertising_categories’
    });

    There’s a bit of magic going on under the covers here but essentially the model creates an Ext.data.Record with the fields you pass it in the background. Trouble is – to regenerate this would lose the additional config options I’ve given the model (and some models have much more inside them than this one).

    To counter this I have been writing a Schema library for Ext MVC, which converts Rails migrations (or potentially any other kind) into a JS form and runs them in sequence at run time. That would mean an end to specifying the fields in the model, which makes migration easier, but it’s also nice to have them there in the model file (like with the rails annotated models plugin).

    I’m still not sure what the best way to go on that one is – any ideas?

    By Ed Spencer on Aug 13, 2008

  3. Hello Ed,

    Ah, yes I see the problem. I’m not sure how your MVC.model.Base works; however, if you were just using regular models (i.e. models created by Ext.Record.create), then I think you could get around this using inheritance.

    The strategy could be: code-generate the base model using the approach above (or your own adapted version), then create a model which extends the base. If you put all your extra logic in the extended class then you can safely regenerate the base class when your migration changes — the base would be overridden, but the extending model would remain the same.

    Incidentally, you could use the code I posted on extending models created by Ext.Record.create in this blog posting.

    Think that would work out for you?

    /Karl

    By krukow on Aug 13, 2008

  4. Hi Karl,

    Thanks for that. MVC.model.Base is somewhat akin to ActiveRecord::Base – you just tell it what your model is called and what fields to include and from that it knows how to map each model to a URL resource, how to pluralize/inflect (in most instances anyway), as well as providing convenient Stores for both an individual model (e.g. /my_resource/1.json) or a collection (/my_resource.json).

    Model was one of the first classes I wrote and is in need of some major refactoring – I’m just trying to figure out the best way at the moment. I like the approach you take in your post about extending Ext.data.Record – I think I’ll give it a go that way. I’m really trying to achieve a Rails-like model definition so I’ll see how far that goes…

    The code is on Github at http://github.com/edspencer/ext-mvc/tree/master/model/base.js if you’re interested. Model.Base is not pretty (yet) but some of the other bits are!

    One final point about style – from your examples you seem to usually downcase everything except the last part of the class name (e.g. com.trifork.tribook.model.Reservation). ExtJs seems to be quite random though – e.g. Ext.data.Record, Ext.form.Action.Load, Ext.ColorPalette all seem to follow a different convention. ‘Ext’ is always capitalized, the next segment is usually not (except for some classes like Ext.Ajax and Ext.ColorPalette), then the third segment is usually capitalized again. Do you know of a ‘universal’ convention for this? Why not just capitalize everything?

    Thanks for maintaining a great blog!

    Ed

    By Ed Spencer on Aug 13, 2008

  5. Hi again, Ed.

    I like that you are attempting to mimick ActiveRecord::Base in JavaScript, it sounds interesting and useful; I’ll have a look at your code at some point… ;-)

    One final point about style – from your examples you seem to usually downcase everything except the last part of the class name…

    My convention is certainly not universal. I try to follow these primary conventions for code that is written in what Douglas Crockford calls “Pseudo classical” style (i.e., how JavaScript is usually written):

    1. Package-objects are always lowercase (a package object is what is created by the namespace function).
    2. Constructor functions and singleton objects start with upper case.
    3. Utility functions are lowercase.

    I haven’t checked this, but I believe that Ext is actually consistent in its naming conventions: Ext is a singleton object (which lives in the global namespace) ✓ Ext.data plays the role of a package object so it is not capitalized ✓ Ext.data.Record is a constructor function ✓

    I think the only other rule that governs this is: if the object/function consists of two words then each word is uppercase, e.g., ColorPalette.

    Thanks for maintaining a great blog!

    Thanks for that comment! Great to get some interaction, so keep commenting ;-)

    /Karl

    By admin on Aug 13, 2008

  1. 1 Trackback(s)

  2. Aug 21, 2008: Recent Links Tagged With "mvc" - JabberTags

You must be logged in to post a comment.