Higher Order Blog

home

Keeping it dry: Generating JavaScript models from Rails models

12 Aug 2008

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.