Sproutcore + Rails

July 19, 2008

I wasn't at the WWDC, but even if I had been there, I probably would have missed this too: Sproutcore.

Because of MobileMe and Live Mesh (the latter being Microsoft's latest weapon to be a big boy on the internet, and I predict this time they will finally succeed big time), I found out that MobileMe is actually based on this open source javascript framework named Sproutcore.

You have to look for yourself what it's all about. This is not like Prototype, jQuery or all those others. This takes MVC to the web browser, advocating fat clients. It does this with Cocoa inspired features.

I wanted Sproutcore to communicate with my rails app. It's not as obvious at the moment unfortunately. In rails follow these steps:

rails contacts
cd contacts

Edit /config/routes.rb to look like this:

ActionController::Routing::Routes.draw do |map|
  map.connect 'sc/:controller/:action'
end

In other words, all sproutcore ajax calls will be expected to hit our site relative to /sc.

Create a simple models file:

class Contact < ActiveRecord::Base
end

Create a migrations file:

class CreateContacts < ActiveRecord::Migration
  def self.up
    create_table :contacts do |t|
      t.string :first_name, :last_name
      t.timestamps
    end
  end

  def self.down
    drop_table :contacts
  end
end

Run rake db:migrate. The contacts table is now created. Run script/console.
And then create two contacts so we have some data:

Contact.create(:first_name => "Lawrence", :last_name => "Pit")
Contact.create(:first_name => "Holy", :last_name => "Cow")

Exit the console. Now for our controller:

class ContactsController < ApplicationController
  def list
    contacts = Contact.all(:order => params[:order])
    respond_to do |wants|
      wants.js {
        records = contacts.map do |contact|
          { :guid      => contact.id,
            :type      => contact.class.name,
            :firstName => contact.first_name,
            :lastName  => contact.last_name
          }
        end
        render :text => { :records => records, :ids => contacts.map(&:id) }.to_json
      }
    end
  end
end

Sproutcore will send a Ajax GET request like http://localhost:3000/sc/contacts/list?order=id. So we only need a wants.js block. The above code can be generalized with helper methods to make this more DRY for our other controllers, but that's for later...

On the rails side we're pretty much done. Fire up script/server and test the above URL. You should see a simple json formatted string as output with the two contacts you created in the database. Keep your rails server running...

Now to sproutcore. The easiest way to show this is by checking out the sproutcore-samples:

cd ..
sudo gem install sproutcore
git clone git://github.com/sproutit/sproutcore-samples.git
cd sproutcore-samples
git submodule init
git submodule update
sc-server

Now point your browser to http://localhost:4020/contacts. You should see a simple demo of sproutcore showing a few contact persons. The persons you see are loaded from fixtures by sproutcore. But we want our contacts from rails. To do this, follow these steps:

open /clients/contacts/core.js. Add this line to the end:

Contacts.server.set("urlFormat", "/sc/%@/%@");

or alternatively, create the server like this:

server: SC.Server.create({ prefix: ['Contacts'], urlFormat: "/sc/%@/%@" }),

This basically tells sproutcore that when a Ajax request is made the url should look like /sc/<controller>/<action>.

Next, edit /clients/contacts/main.js. There's a line that says to load the contacts from the fixtures (as defined in /client/contacts/fixtures/contact.js). But we want the real data. So replace that line:

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

This basically says that when the page is loaded it should do an Ajax GET request to get the list of contacts.

Edit /clients/contacts/models/contact.js, and add the lines that define dataSource and resourceURL:

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

  dataSource: Contacts.server,
  resourceURL: 'contacts',

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

The value for resourceURL is what is used in the URL to make the call to rails. The datasource tells which server to use when sending the Ajax requests. Aside note: I would expect future versions of sproutcore wouldn't need these two lines as they could be defaults.

And now the last part, the real tick to this whole thing:

# This will proxy all requests to /sc/* -> http://localhost:3000/*
proxy '/sc', :to => 'localhost:3000'

Update (thnx joel): this piece goes into the file sproutcore-samples/sc-config.

This tells sc-server to send any requests that start with /sc to http://localhost:3000 which runs our rails server.

Now refresh your browser at http://localhost:4020/contacts and lo and behold, the contacts are now coming from rails.

Last note: currently Sproutcore in not at v1.0, and it's REST API is not well developed. I'm pretty sure this will be fixed quite soon though. This is a winner!

Update: see also part ii in this series to see how to use the full range of CRUD statements.


Comments

  1. Hi I followed all the steps and I have such a error in Rails's app logs:

    ActionController::RoutingError (No route matches "/sc/contacts/list" with {:method=>:get}):
    /Library/Ruby/Gems/1.8/gems/actionpack-2.1.0/lib/action_controller/routing/recognition_optimisation.rb:67:in `recognize_path'

    For Some reason /sc is being added to URL for Rails app and it can not resolve it. Any clue what might be wrong?

    Cheers

    zibi on July 20, 2008 at 11:49 pm

  2. I guess you didn't modify your /config/routes.rb file as described above.

    Lawrence Pit on July 21, 2008 at 12:08 am

  3. Hello, Lawrence!

    I'm experiencing the same problem of zibi:

    Routing Error

    No route matches "/sc/contacts/list" with {:method=>:get}

    And I'm pretty sure that I modified the /config/routes.rb as you described.

    Any other guesses?

    Cheers

    — lucas on July 21, 2008 at 4:56 am

  4. When you start up script/server, and point your browser to http://localhost:3000/sc/contacts/list you get this routing error?

    I just tried again from scratch and found no issue. Are you sure map.connect 'sc/:controller/:action' is at the top of your config/routes.rb?

    Lawrence Pit on July 21, 2008 at 5:56 am

  5. Thanks Lawrence, nice tutorial on a subject that that really needs more examples.

    I also had a similar problem as zibi and lucas when I pointed my browser to:
    http://localhost:3000/sc/contacts/list?order=id

    I finally figured out that I named my contacts controller incorrectly and the app couldn't find it. I correctly named it contacts_controller.rb and now it works perfect.

    Thanks again!

    — Randall on July 21, 2008 at 6:39 am

  6. One point of clarification that might help some folks out--the final (and crucial!) proxy configuration goes in the sproutcore-samples/sc-config file (you can add it after the existing twitter proxy).

    Also, the contact-finding-code (contacts = Contact.all(:order => params[:order])) can be replaced by the slightly more verbose .find(:all) method (contacts = Contact.find(:all, :order => params[:order])) in case your ActiveRecord doesn't know about the 'all' public method. Mine doesn't, possibly because it's an older install.

    With those in place, things worked perfectly. Nice writeup!

    joel boonstra on July 21, 2008 at 5:00 pm

  7. [...] Sproutcore + Rails Sproutcore frontend to a Rails application howto. [...]

    A Right Barrel Load Of Articles | Programmer's Log on July 21, 2008 at 11:02 pm

  8. I tried this example by copying and pasting the code sections from the web page into TextMate. Everything worked up to the Contacts.Contact = SC.Record.extend... section, but I got lots of JavaScript errors from that section in Safari. Not being a JavaScript expert, it took a while to figure out what was wrong. The first two lines (dataSource:..., resourceURL:...) are fine, but two lines of the code inside of fullName() contain a few typographer's quotes (I think that's the right term), ie, "’" and "‘" (UTF codes e2 80 99 and e2 80 98) instead of regular ASCII single-quotes "'" (0x27), which cause problems in Safari. Replacing those quotes (or just pasting the first two lines instead of the entire function) fixed the problem. The difference in the shape of the glyphs is subtle, and I didn't notice it until I looked at the file using hexdump. (TextMate's "Show Invisibles" didn't reveal anything, since those characters are visible anyway.)

    — Calvin Teague on July 22, 2008 at 6:34 am

  9. Randall and joel just solved my problems! ;)

    And thanks to Lawrence for this guide, too.

    — lucas on July 22, 2008 at 7:14 am

  10. Thanks for the great tutorial; exactly what I've been looking for.

    The very last step, the proxy to localhost:3000, is a bit unclear as to exactly where it is to be typed. Can you provide any clarification?

    I've tried it in couple of spots that seem to make sense, and my sproutcore app at 4020 still loads fine, but it does not pull in the Rails data.

    Thanks :)

    — Eric on July 22, 2008 at 2:47 pm

  11. Never mind, my bad...just saw the above comment addressing this very issue :P

    — Eric on July 22, 2008 at 2:52 pm

  12. [...] Read the rest… [...]

    Sproutcore + Rails « Andata e ritorno on July 22, 2008 at 3:06 pm

  13. I'm not sure where else to talk to people working with Rails and Sproutcore! I'm hitting an odd issue with Firefox/Sproutcore. The application will work on every other request in Firefox. I can tell what's happening now, but not sure why. The HTTP response Sproutcore reports flips back and forth between 200 and 304. When it's 200, the application loads fine, when 304, no joy. Has anyone else experienced this?

    Brandon on July 23, 2008 at 6:52 pm

  14. Yes, this has been reported on the sproutcore mailing list. It's a bug in either sproutcore or the thin server. Strangely enough when I add a second request in main.js, the issue is gone.

    Lawrence Pit on July 24, 2008 at 12:06 am

  15. This is AWESOME! Thanks so much!

    But now for what may be a tougher question. SC handles Create, Updates, and Destroys using batches of records. I'm a newbie to Rails and SC. All the Rails literature I can find seems to only handle single records at a time. Can someone give an example how the Rails controller for CREATE, UPDATE, DESTROY in a way that can be used the the SC Server class.

    — Danko on July 26, 2008 at 6:53 am

  16. [...] Sproutcore + Rails [...]

    Nome do Jogo » Artigo » Rails Podcast Brasil - Epis on July 29, 2008 at 5:14 pm

  17. Great write-up; however I noticed that writing data back to the server was not covered.

    I have been thinking a lot about this and Rails, as-is, is not very well suited for SproutCore unless you stick to the Rails ActiveRecord JSON marshaling conventions. While your example shows how to convert a Rails model into custom SproutCore JSON data structures; that same mapping cannot be reveresed and saved back into ActiveRecord.

    A 1-to-1 mapping between ActiveRecord attributes and JSON (or XML -- you pick... at the end of the day they're just hierarchical hash structures) seems grossly over simplified. A great example of that would be an is_admin attribute on a User. This is information that you don't necessarily want to share over the wire for everybody to see via JSON but may be necessary for an app to function properly. There should be some way to mark attributes as non-serializable.

    This raises the more fundamental issue of the role a web server plays in this new "MVC-stack-in-the-browser" world. For the example you gave above, JSON was being treated as a "view". Should data that is sent to MVC browser stacks be treated as such? Or should they just be treated as serialized data?

    Brad Gessler on August 3, 2008 at 6:49 am

  18. Hi Brad, see part ii for a description of how to save SproutCore data back into ActiveRecord. It's very little code and can be even further DRY'ed if you really wanted to. There really isn't that much difference with data that is received from 'regular' HTML pages.

    I don't see sending e.g. is_admin over the wire as an issue. You also do that (implicitely) when you send content to your clients that is for admins only. There's no difference really. You just need to take care that when someone requests admin data from your server that the request is really from an admin, and not from someone who has flipped the is_admin bit on the client. Here too, you already do this anyways with your plain jane HTML pages.

    Anything that a rails controller returns is always a "view". Serialized data to me is just another view of the data.

    Lawrence Pit on August 4, 2008 at 3:14 am

  19. [...] part i of the Sproutcore + Rails series I described how to query information from a rails server and [...]

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

  20. Hi Lawrence,

    Thanks for your tutorial.
    But I can't find 'sc-server' after I clone the all sproutcore-samples.
    And which directory I should do:
    (git clone git://github.com/sproutit/sproutcore-samples.git)
    In /contacts/app/ or /contacts/ ?
    I am really confuse now.

    So don't know how to start the sc-server without this file.

    Cheers

    — Shuoling on August 26, 2008 at 12:44 pm

  21. When you have installed sproutcore via sudo gem install sproutcore, then all you have to do is sit in the root of the sproutcore-samples project and run: sc-server

    Lawrence Pit on August 26, 2008 at 6:28 pm

Leave a comment