There are a lot of JavaScript frameworks for web applications, but few of them are nicely integrated with Ruby on Rails and most of them are quite heavy. This is about crafting your own JS application/framework to interact with a Rails app, or any server side app.

Why am I writing this? Well I was looking for a JS framework tightly bound with Ruby on Rails, but I didn’t find anything that I really liked… Some frameworks are valid but they are not crafted for Rails and many of these are quite heavy. Recently I discovered  CompoundJS which is really great, but it is too much client side, as it almost is a Rails porting in JS.

The goal is to have a classic Rails application on the server and a JS interface on the client side, conscious of the routes, the models and the views, so (in JS) I can do something like friends = User.find(10).friends() to receive, all the friends of user 10 as a JSON response and then if I type html = friends.view(‘list’) I render them with the list view, finally I inject the HTML into the DOM.

Routes

I think that the most useful feature is to have access to Rails routes in JS. There exist many gems for this and after some trials I chose js-routes, which creates a JS object (by default called Routes) containing all the routes…simple and effective! If for some reason you want to do it by hand, here’s what I did at the beginning:

MyApp = {
paths: {
  users: function()     { return '/users' },
  user:  function(user) { return '/users/'+user },

          user_types:function(u)      { return this.user(u)+'/types' },
     show_user_type: function(u,type) { return this.user(u)+'/types/'+type },
   create_user_type: function(u)      { return this.user(u)+'/types' },
   update_user_type: function(u,type) { return this.user(u)+'/types/'+type },
  destroy_user_type: function(u,type) { return this.user(u)+'/types/'+type },

  index_user_activities: function(u)            { return this.user(u)+'/activities' },
   show_user_activity:   function(u,activities) { return this.user(u)+'/activities/'+activities }
} }

Then it’s useful to build some helper functions for AJAX calls:

MyApp = {
_get: function(path, args, callback) {
$.get(this.url.root + path, args, callback, 'json');
},

_post: function(path, args, callback) {
$.post(this.url.root + path, args, callback, 'json');
},

_put: function(path, args, callback) {
$.put(this.url.root + path, args, callback, 'json');
},

_delete: function(path, args, callback) {
args._method = 'delete'; this._post(path, args, callback);
} }

Now you need a function for executing actions:

MyApp = {
  do: function(model, action, args, callback) {
    if (action == 'index') { // workaround for singular/plural only names
      MyApp._get(Routes[model+'_index_path']() || Routes[model+'_path']());
    } elseif (action == 'show') { 
      MyApp._get(Routes[action+'_'+model+'_path'], args, callback); 
    } elseif (action == 'destroy') { 
      MyApp._delete(Routes[action+'_'+model+'_path'], args, callback); 
    } // etc... 
} }

…but wait this is just a helper method, you will not call it directly, to see something more useful continue reading…

Models

Now you have routes and you know how to use them, but to build a real framework you need much more! First of all we need all Rails Models available on the client side: as there exists the User Model in Ruby,

class User < ActiveRecord::Base
  attr_accessible :email, :name, :password, :password_confirmation, :avatar
end

here it is in JS…

function User(attr){
  this.id = attr.id;
  this.name = attr.name;
  this.avatar = attr.avatar;
  # ...well, not even the password!
};
// Inheritance
User.prototype = new Model();
User.prototype.constructor = User;

Please, notice the inheritance from Model: I would like to have a handful of methods to call on all Models subclasses for rendering, getting URLs, execute actions (destroy, update) and find them. The Model class (…yeah…prototype!) is also useful for configurations common to all models:

function Model(){
  this.some_opts = default_val;
}

The most interesting part resides in Models methods:

Model.prototype = {
  // Views methods
  view: function(action, layout) {},

  // Find methods
  find: function() {},

  // Controllers methods
  create: function() {},
  destroy: function() {}
};

Controllers

OK…Now we have Models and Routes…let’s combine them together so when you call MyApp.do(‘user’,’index’, {}, callback) you get back a nice User instance with id, name etc…, But be aware that now you will call Model.do(User, ‘show’, {id: 5}, callback) 

Model.do = function(klass, action, args, callback) {
  var results = [];
  MyApp.do(klass.name.toLowerCase(), action, args, function(data){ 
    for(i=0; i<data.length; i++) {
      // Create an instance of the model and initialize it with the data fetched from server
      results.push(new klass(data[i])); 
    } 
    callback.call(results); 
  }
};
Model.index = function (klass, args, callback) {
  this.do(klass, 'index', args, callback);
}

Simple but powerful! It takes as argument a class (User) and a function to call back when the request succeeds.

 

Views

Well…the missing feature is the .view() function: given an instance of Model or its subclasses, it searches for the right view and call it with the Model instance as argument: the method directly returns HTML code.

The starting point is a file with .jst extension (JS Template) in the JavaScript assets, Rails (sprocket) will treat it as a view template, that is, it takes the content of that file and saves it in the variable JST[‘file_name’] as a string…check it in the browser console on your Rails app! The real power comes when coupled with a template processor, basically you can embed JS code (variables, functions call…) in the view, just like you do in .erb  files:

<a href="tesladocet.com/user/<%= id %>">
<h1><%= name %></h1>
</a>
<img src="<%= avatar %>"><br>

Calling  JST[‘user’] will print the exact content of the file, but calling _.template(JST[‘user’], args) will replace the  <%= %> with the result of execution of the JS code contained in it. To be honest the manual parsing procedure is boring and slow, as the template must be parsed all the time, so is better to precompile it, so you can just call  JST[‘user’](args). The most efficient solution is to automatically precompile server side, so just install the gem of a template engine  like EJS, Handlebars, etc., and append the right extension to the JST file (i.e.: app/assets/javascript/user.jst.ejs for EJS): I chose EJS, which is the entry level processor (but works well!) and has the same syntax as ERB. 

Now it comes the magic: you can pass as argument a Model itself (JST takes as argument an object, not a list of vars!!)

// Find a user
MyApp.show(User,{id:1}, function(u) {
  // Use it as argument for JST
  html_to_inject = JST['user'](u);
});
<!-- This is the output -->
<a href="tesladocet.com/user/1">
    <h1>Luca</h1> </a>
<img src="my_photo.jpg">

Finally the .view() method of the Model class can be filled:

view: function(action, layout) {
    action = action || 'show';
    this._view_opt.name = this._view_opt.name || this.constructor.name.toLowerCase();

    // Draw template
    html = JST[this._view_opt.name+'/'+action](this)

    // Draw layout, if any
    if (layout) return this.drawLayout(layout, html);
    else return html;
}
drawLayout(layout, html) {
    return JST['layout/'+layout](html)
} 

// So this: 
JST['user'](u) 
// Becomes this:
u.view() // u.view('show')

Note that I structured JST files in folder like app/assets/javascript/<model_name>/<action>.jst.ejs and JST preserves this strucure using / in the template name. The .drawLayout() method uses the layout for layout tempaltes, so if you would create a Model called Layout, just change the folder name to something else!

A little, annoying, problem… When you fetch more than one instance from the server, you get an Array of instances, so the u.view() doesn’t work, the rough solution is to loop the array and call the view on each element…well here’s a trick:

Array.prototype.view = function(action,layout) {
  this.map(function(inst){
    inst.view(action, layout);
  })
};

Server side

I’m pretty sure the server side shouldn’t be touched for most of you reading this article! Anyway simply check the controller: it needs to serve a XHR requests with JSON responses. Be only aware of Rails associations: for example I have a News model defined by the attributes user_id, action, entity_id…basically a News belongs_to Users and Entities and represents something like <‘User#1′, ‘created’, ‘Post#1′>. So when I retrieve the News I have an useless field user_id = 1 in the JS News Model and I should make a new request for the User#1…don’t worry Rails is smart enough:

def index
  @news = current_user.news.all(limit: 10, include: [:user,:entity])

  respond_to do |f|
      f.js {
        render json: @news, # No info about user and entity
          include: { # Rails belongs_to magic!
            user: {only: [:name,:avatar]}, # no password or email!!
            entity: {} # Get every entity's attribute
          }
      }
      f.html {}
    end
  end

This automatically include the :user and :entity attributes (with their own name, id etc…) in the News Model and convert to_json everything! To handle the associated Models, a Model must include the appropriate fields:

function News(attr){
  this.id = attr.id;
  this.user_id = attr.user_id;
  this.user = attr.user; // belongs_to :user
  this.action = attr.action;
  this.entity_id = attr.entity_id;
  this.entity = attr.entity; // <- belongs_to :entity
};
News.prototype = new Model();

Well…this is it! Hope to be useful! Please give me some feedback and tell me your solutions!

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>