Sproutcore + Rails, part ii

July 26, 2008

Merry X-mas from Down Under...

In my previous post I showed how to get sproutcore talking to rails. In this post I'll explain how to get all the basic CRUDs working.

But before we go into the details, first a few points you should be aware of.

As mentioned at the end of my previous post sproutcore doesn't really have a REST API. So currently sproutcore won't map nicely on for example your map.resources :contacts resource route definition. Instead we'll use a simple map.connect route that will map all sproutcore requests.

The requests made by sproutcore are either GET or POST requests. Currently it doesn't do PUT nor DELETE requests.

There is the intention to have a clean REST API in sproutcore, hopefully before Sproutcore v1.0.

Sproutcore works with collections, not so much with members. Rails on the other hand works mainly with members: /show/12345, /destroy/12345, etc. Sproutcore works more like /show?ids=1,2,3,4,5 and /destroy?ids=9,8,7,6 etc. But if you tell sproutcore to show or destroy one record, it will make the request like /show/12345 resp. /destroy/12345.

Responses back to sproutcore are always in JSON format. Like the requests, the responses will consist of collections.

Sproutcore of course doesn't send an authenticity token that rails 2.x expects. You either have to disable it in rails, explicitly send the authenticity token in the requests to rails, or hack server.js a tiny bit to automatically always include the token.

The main class to look at is sproutcore/foundation/server.js. In there you see the following main methods that allow communication with the backend:

  • listFor
  • createRecords
  • refreshRecords
  • commitRecords
  • destroyRecords

Normally you wouldn't need to call any of those directly, except possibly for listFor. The other methods are automatically called for you when you work with your models. For example when you call myContact.destroy(); in sproutcore, it will call the destroyRecords method in server.js.

Now on to the code. I'll assume you've followed the steps in the previous post first. Then in rails, have you're routes.rb file look like this:

ActionController::Routing::Routes.draw do |map|
  map.with_options(:path_prefix => "sc") do |m|
    m.connect ':controller/:action'
    m.connect ':controller/show/:id', :action => "show", :method => :get
    m.connect ':controller/destroy/:id', :action => "destroy", :method => :post
  end
end

(Note: when you copy-paste code from this website be aware that you may need to edit the single and double quote characters as this blog turns them into fancy characters...)

Following is the complete controller for your contacts

class ContactsController < ApplicationController
  protect_from_forgery :only => [:foo]

  def list
    respond_to do |wants|
      wants.json do
        contacts = Contact.all(:order => params[:order])
        render :text => { :records => to_hash(contacts),
                          :ids => contacts.map(&:id),
                          :count => contacts.size }.to_json
      end
    end
  end

  def show
    respond_to do |wants|
      wants.json do
        contacts = Contact.find(params[:id] || params[:ids].split(","))
        render :text => to_hash(contacts).to_json
      end
    end
  end

  def create
    respond_to do |wants|
      wants.json {
        response = []
        params[:records].each_pair do |record_id, record|
          record.delete(:id)
          guid = record.delete(:_guid)
          contact = Contact.new(record)
          contact.save
          response << {:_guid => guid, :id => contact.id}
        end

        render :text => response.to_json
      }
    end
  end

  def update
    respond_to do |wants|
      wants.json {
        params[:records].each_pair do |record_id, record|
          contact = Contact.find(record.delete(:id))
          contact.attributes = record
          contact.save
        end

        head :ok
      }
    end
  end

  def destroy
    respond_to do |wants|
      wants.json {
        Contact.destroy(params[:id] || params[:ids].split(","))
        head :ok
      }
    end
  end

  private
    def to_hash(contacts)
      contacts = [contacts] unless contacts.is_a?(Array)
      contacts.map do |contact|
        { :id        => contact.id,
          :type      => contact.class.name,
          :firstName => contact.first_name,
          :lastName  => contact.last_name
        }
      end
    end

end

The above doesn't take error handling into account. I leave that to you.. <grin>

Start script/server, and go to sproutcore. In sproutcore-samples, edit /clients/contacts/models/contact.js, and copy-paste the following code:

require('core');

Contacts.Contact = SC.Record.extend(
/** @scope Contacts.Contact.prototype */ {

  dataSource: Contacts.server,
  resourceURL: 'contacts',
  properties: ['guid','firstName','lastName'],
  primaryKey: 'guid',

  fullName: function() {
    return [this.get('firstName'), this.get('lastName')].compact().join(' ');
  }.property('firstName', 'lastName')

});

Now go into /clients/contacts/main.js.

To get a list of contacts:

Contacts.server.listFor({recordType: Contacts.Contact});

To create a new contact:

var obj = {firstName:"Lawrence", lastName:"Pit"};
var newrecord = Contacts.Contact.newRecord(obj, Contacts.server);
newrecord.commit();

To update a contact:

contact.set("firstName", "Laurens");
contact.dataSource = Contacts.server;
contact.commit();

To delete a contact:

mycontact.destroy();

Or to delete a bunch of contacts:

Contacts.server.destroyRecords([contact1, contact2]);

To refresh (show) a contact:

contact.refresh();

Or to refresh (show) a bunch of contacts:

Contacts.server.refreshRecords([contact1, contact2]);

Last tip: while you're testing, in the console of firebug see what's currently in the store via:

SC.Store.findRecords(Contacts.Contact)

Comments

  1. This is great. Exactly what I was looking for.

    — Danko on July 26, 2008 at 9:03 pm

  2. Great work!
    Thanks for figuring this stuff out for us, it's really helpfull.

    Maybe you could add this to the sproutcore wiki?

    — Roy van der Meij on July 28, 2008 at 4:37 pm

  3. [...] see also part ii in this series to see how to use the full range of CRUD [...]

    Sproutcore + Rails | Lambda @ Copa on August 4, 2008 at 2:50 am

  4. [...] how to query information from a rails server and display it within a Sproutcore application. In part ii I explained how to wire the other basic CRUD operations [...]

    Spoutcore + Rails, part iii | Lambda @ Copa on August 4, 2008 at 3:54 am

  5. Your blog is interesting!

    Keep up the good work!

    Alex on August 16, 2008 at 7:18 am

  6. That last part to accomplish the CRUD, do we copy that code into main.js? Or do we need to do something like....

    Contacts.server.createRecords(var newrecord = Contacts.Contact.newRecord({firstName:"Lawrence", lastName:"Pit"}, Contacts.server);
    newrecord.commit();)

    Sorry am really new to JS.....

    — Matt on August 28, 2008 at 8:48 pm

  7. A RESTful AJAX API would be critical and very cool. I recently rewrote my first rails app, HomeMarks, in full JS OO with no RJS and inline JS. One thing that I found is that I had to bake my own RESTful AJAX class as part of my base JS class for the app along a global JS var for the authentication token.

    By the time I finished the project I really felt like there was another level I could take the JS too, basically having all DOM objects on the page rendered initally from JSON data vs a rails view. SproutCore really looks like a GREAT way of doing that too. Keep the tutorials up.

    Ken Collins on September 8, 2008 at 4:34 am

  8. Good Tutorial. But integrating the CRUD Actions to the actual Contacts App would have been really useful. I'm a total Sproutcore beginner and so I've got CRUD actions but no idea how to integrate them in my app.

    — Saile on September 26, 2008 at 6:01 pm

Leave a comment