About Rails
From its site: "Ruby on Rails is an open-source web framework that's optimized for programmer happiness and sustainable productivity. It lets you write beautiful code by favoring convention over configuration."
Installation
Make sure you have installed Rails 2.3.4 or above and the sqlite gem
$ sudo gem install sqlite3-ruby
$ sudo gem install rails
Create the project
A small rails application will serve as a backend for the Todos application.
Create a new Todos app by entering the following command into the console:
$ rails todos
$ cd todos
Make sure that you do issue this command outside your SproutCore application folder!
Setup the data model
We can use Rails generators to greate a basic RESTful interface for the Task model by typing
$ script/generate scaffold Task description:string isDone:boolean order:integer
Now you need to update your database to conform the model. For that, type
$ rake db:migrate
Adjust JSON communication
Sproutcore uses the field guid for objects ids, but Rails calls this field id.
You have two options on how to convert between these naming conventions:
Option 1: Adjust Rails JSON output
To customize the JSON output of an object, write a json_to_task protected method just before the last "end" line in TasksController (app/controllers/tasks_controller.rb):
protected
def json_for_task(task)
{ :guid => task_path(task),
:description => task.description,
:isDone => task.isDone
}
end
Option 2: Set primaryKey in your Sproutcore model class
You can subclass SC.Record and set the primaryKey of your sublcass to "id". Your Todo-Object then needs to extend from e.g. App.Rails.Record.
App.Rails.Record = SC.Record.extend({
primaryKey : "id" // Extend your records from App.Rails.Record instead of SC.Record.
});
(Thanks to Giacomo Luca Maioli for this idea.)
This tutorial sticks to Option 1. The rails-code for option 2 is the same, just omit calling json_for_task().
Writing the controller
Open the file app/controllers/tasks_controllers.rb, and modify the index method like this:
def index
@tasks = Task.all
respond_to do |format|
tasks = @tasks.map {|task| json_for_task(task) }
format.json { render :json => { :content => tasks } }
format.html
format.xml { render :xml => @tasks }
end
end
Start the server from the Rails app directory:
$ script/server
To fill your database, create some tasks by visiting http://localhost:3000/tasks and entering some test data.
To test your server now, open the following address in your web browser: http://localhost:3000/tasks?format=json. You should receive a plain-text-file, with the datafields of your test data.
Implementing CRUD operations
The following sections show how to implement the CRUD (create, read, update, delete) operations for our Task model.
We will start with read (or also GET), as it is the easiest. Extend the code like this:
Read or GET Task
def show
@task = Task.find(params[:id])
respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => @task }
format.json do
render :json => {
:content => json_for_task(@task),
:location => task_path(@task)
}
end
end
end
Try the outcome by calling http://localhost:3000/tasks/show/1?format=json.
Create Task
def create
@task = Task.new(params[:task])
respond_to do |format|
if @task.save
flash[:notice] = 'Task was successfully created.'
format.json do
render :json => { :content => json_for_task(@task) }, :status => :created,
:location => task_path(@task)
end
format.html { redirect_to(@task) }
format.xml { render :xml => @task, :status => :created, :location => @task }
else
format.html { render :action => "new" }
format.xml { render :xml => @task.errors, :status => :unprocessable_entity }
end
end
end
Update Task
def update
@task = Task.find(params[:id])
respond_to do |format|
params[:task].delete(:guid)
if @task.update_attributes(params[:task])
flash[:notice] = 'Task was successfully updated.'
format.json do
render :json => { :content => json_for_task(@task) }, :location => task_path(@task)
end
format.html { redirect_to(@task) }
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => @task.errors, :status => :unprocessable_entity }
end
end
end
Delete Task
def destroy
@task = Task.find(params[:id])
@task.destroy
respond_to do |format|
format.json { head :ok }
format.html { redirect_to(tasks_url) }
format.xml { head :ok }
end
end
Customize the data source
To fullfill Ruby on Rails expectations, modify the send() call in the createRecord and updateRecord in the data_source/task.js file in your SproutCore project like this:
.send({ task: store.readDataHash(storeKey) });
Setup your proxy
Add the following line to the Buildfile file in your SproutCore project:
proxy "/tasks", :to => "localhost:3000"
You’re Done!
Continue to next step: Step 7: Hooking Up to the Backend »
Comments (18)
Brandon Tennant said
at 8:55 am on Oct 5, 2009
@ReinerPittinger Following this method, I always get a single task in my task list with no description or isDone property. I'm not entirely sure why. Are you getting the same results.
Brandon Tennant said
at 8:50 am on Oct 6, 2009
So by default, rails will produce a JSON output of the above in the following structure which would require a change in the merb_data_source.js file to pull each task object out into a temporary array in the form that is expected.
{
"content":[
{
"task":{
"isDone":true,
"created_at":"2009-10-06T05:19:25Z",
"updated_at":"2009-10-06T05:19:25Z",
"id":1,
"description":"This is a description"
}
},
{
"task":{
"isDone":false,
"created_at":"2009-10-06T05:20:10Z",
"updated_at":"2009-10-06T05:20:10Z",
"id":2,
"description":"This is another description"
}
}
]
}
fetchDidComplete: in merb_data_source.js is updated to include the following (kinda hacky)
/* Here we're creating a new array in the form expected by loadRecords */
var newContentArray = new Array();
for(i = 0; i < response.content.length; i++)
{
newContentArray[i] = response.content[i].dont;
}
response.content = newContentArray;
storeKeys = params.store.loadRecords(fetchKey.get('recordType'), response.content);
// update the storeKeyArray to contain the store keys so that all of the observers
// will be updated with the keys of the newly loaded data. this is where the magic
// of SproutCore happens.
params.storeKeyArray.replace(0,0,storeKeys);
This gets us as far as to load the data in from the rails server. However for me, it's only ever loading 1 record. Further debugging shows each loaded record is being assigned a storeKey value of 1 and I'm not at all sure why.
Spencer Dillard said
at 1:59 am on Nov 17, 2009
I'm a newb for sure, but one thing I started doing to better handle the problem of keeping my model in synch is to create a partial for the model that is just the types. In my generated model, I do sc_require('models'/{model_name}_partial'); And in that file, I keep all the type definitions. Next step is to write a script that will inspect a list of models in rails and generate the partial so that this can be done via rake.
One note -- I am non rails 2.2.2 and your json changes don't work for me, since element was added later (2.3?) -- just an FYI in case someone else starts to take the time to hunt that down.
But thanks -- got me on the way so far.
And one question. I am a little confused how the properties get translated. In rails I have something like zip_code, and in sc, I have zipCode. Is there somewhere that this mystical translation happens?
Erich Ocean said
at 7:39 am on Nov 17, 2009
You have to translate property names in your SC.Record subclass definition, e.g.
zipCode: SC.Record.attr(String, { key: 'zip_code' })
Giacomo Luca Maioli said
at 8:42 am on Dec 2, 2009
I managed to make a rails json backend to work withoud redefine "to_json" in ActiveRecord::Base subclasses. I made it by setting the primaryKey of my extension of SC.Record to "id" instead of "guid" (why guid anyway?).
App.Rails.Record = SC.Record.extend(
primaryKey : "id" // I'll extend my app records from App.Rails.Record instead of SC.Record.
});
I don't know if this has any collateral effect, but seems to work fine.
Even though I've wrote my own version of "ResourceController" (that only serve json :-D) you guys can (skip?) accelerate development of json responders using http://github.com/giraffesoft/resource_controller
this one, along with resource mapping routes, should get you to work with rails as a backend in (no?) less time.
Cheers.
Charles Jolley said
at 2:23 pm on Dec 10, 2009
I removed the section at the top of this page that talks about Rails suitability for SproutCore. This tutorial is not the place to make these kinds of arguments. Feel free to argue about it on your blog or in the mailing list/IRC channel.
Peter Wagenet said
at 9:48 am on Dec 11, 2009
Good call Charles. I probably should have just done that rather than modifying it.
Steve B. said
at 11:09 pm on Mar 3, 2010
This is a bit awkward because the "hook up" part is on the next Step but I'm having some difficulties specific to Rails. I followed Option #1.
Listing tasks ("XHR finished loading: "http://localhost:4020/tasks") works fine as does deleting a task.
Creating a task, at least the first part, works fine as well (I end up with a new task called "New Task"). However, when I attempt to edit it, I see: "XHR finished loading: "http://localhost:4020/undefined"" in the debugger window. I do not see anything on the server (Rails side). So this is problem #1.
When I try to update an existing task, I see "XHR finished loading: "http://localhost:4020/tasks/7"" in the debugger window but on the server (Rails side), there is an internal server error because of the "guid" property not belonging to Task. This is problem #2.
So I attempted to follow Option #2...except I'm not sure where to put "App.Rails.Record" (or should it be "Todos.Rails.Record"?). I tried to put:
Todos.Rails.Record = SC.Record.extend( { primaryKey: "id"});
in main.js and made the necessary change in models/task.js but that didn't work. It pretty much well bombed everywhere saying Todos.Rails.Record wasn't defined which caused Todos.Task to fail and so on down the line.
At this point, I'm really engaging in cargo cult coding...any ideas?
Steve B. said
at 11:58 am on Mar 4, 2010
I've solved problem #2. I looked around the internets and found code by Mike Sublesky for an earlier version of SproutCore. The Rails side, however, did have some code that was useful. To the tasks_controller.rb, I added at the top:
before_filter :clean_params, :only => [:create,:update]
and at the bottom, I added:
private
def clean_params
params[ :task].delete( :guid) if params[ :task]
end
This fixes updating to Tasks that both sides know about.
Problem #1 remains...if you create a new task, the server side is informed of the new task (with description: "NewTask", isDone: false) which is created but the client is somehow not informed of any information about this task. Therefore any subsequent editing fails. The initial editing fails quietly (you change the text on the client but it's not updated on the server). Any subsequent editing fails hard with a SC.Error:sc33:Not found (-1) in the JS debugger.
Steve B. said
at 1:50 pm on Mar 4, 2010
And now I've solved problem #2.
In the actions above, in several places, you will note that the render => :json has the following form:
render :json => { :content => json_for_task(@task) }, :status => :created, :location => task_path(@task)
Note where the Hash ends ("}"). In the next step, for didCreateTask in todos/data_sources/task.js, you will write code that looks for the location in the following manner:
var url = response.header( 'Location');
I narrowed the problem down to this line of code. The client simply isn't getting that "url". I tried changing the string from "Location" to "location" but that didn't work either. What I finally did was change the hash that is returned by the create action in tasks_controller.rb to:
render :json => { :content => json_for_task(@task), :status => :created, :location => task_path(@task)}
Note where the Hash ends now. Then in todos/data_sources/task.js, I changed that one line in didCreateTask to:
var url = response.get( 'body').location;
Everything now works like a charm. Right now I feel like I'm hacking because I don't know why what was described didn't work.
Steve B. said
at 6:42 pm on Mar 5, 2010
Just for the fun (?) of it, I started looking into option #2. Thanks to #sproutcore for helping clarify some things for me. Note that there are lot of changes to be made between Option #1 and Option #2 because...
1. The code as written assumes "guid" is the RESTful path to the Task. When you change the code to use "id" instead of "guid", this has ramifications throughout the code as presented.
2. As was previously noticed, Rails' to_json method returns a different formatting than SproutCore expects. Specifically, the JSON for @tasks is:
{"content":[{"task":{"isDone":false,"id":5,"description":"learn more SproutCore."}}]}
instead of:
{"content":[{"isDone":false,"id":5,"description":"learn more SproutCore."}]}
so you either need to have code on the front-end that handles it or code on the back-end that handles it. I picked the back-end because my ruby-fu is better than my javascript-fu.
Step 1. Create the SC.Record subclass and use it.
Todos.Rails = {} ;
Todos.Rails.Record = SC.Record.extend( primaryKey : "id"}) ;
should go in core.js. I originally put it in main.js but it doesn't get executed soon enough. In core.js, it does. Change SC.Record to Todos.Rails.Record in models/task.js.
Step 2. Edit json_for_task:
def json_for_task( task)
task.attributes
end
this won't necessarily scale well if properties are themselves ActiveRecord objects. It seems like the right thing to do is override and rewrite to_json but I didn't (yet).
Steve B. said
at 6:42 pm on Mar 5, 2010
Step 3. Fix places where store.idFor(storeKey) is expected return a RESTful path to the Task (through 'guid').
At this point fetch & didFetchTasks will work but not much else. The problems are going to be lines like these:
in retrieveTask:
var url = store.idFor(storeKey);
SC.Request.getUrl(url).json().notify(this, 'didRetrieveTask', store, storeKey).send();
because url isn't the RESTful URL anymore. This needs to be changed to:
var url = "/tasks/" + store.idFor(storeKey) ;
SC.Request.getUrl(url).json().notify(this, 'didRetrieveTask', store, storeKey).send();
Similarly, in didCreateTask:
var url = response.get( 'body').location;
store.dataSourceDidComplete( storeKey, null, url); // update url
needs to be changed to:
var id = response.get( 'body').content.id;
store.dataSourceDidComplete( storeKey, null, id);
And similarly throughout the CRUD methods and callbacks.
Step 3. Fix places where store.idFor(storeKey) is expected return a RESTful path to the Task (through 'guid') by adding "/tasks/" in front of it.
At this point fetch & didFetchTasks will work but not much else. The problems are going to be lines like these:
in retrieveTask:
var url = store.idFor(storeKey);
SC.Request.getUrl(url).json().notify(this, 'didRetrieveTask', store, storeKey).send();
because url isn't the RESTful URL anymore. This needs to be changed to:
var url = "/tasks/" + store.idFor(storeKey) ;
SC.Request.getUrl(url).json().notify(this, 'didRetrieveTask', store, storeKey).send();
Similarly, in didCreateTask:
var url = response.get( 'body').location;
store.dataSourceDidComplete( storeKey, null, url); // update url
needs to be changed to:
var id = response.get( 'body').content.id;
store.dataSourceDidComplete( storeKey, null, id);
And similarly throughout the CRUD methods and callbacks.
Steve B. said
at 6:44 pm on Mar 5, 2010
ugh...step 3. is listed twice and no "edit."
Hugh Evans said
at 4:44 am on Apr 3, 2010
You can also simply add in guid as an alias for id in to_json:
alias_attribute :guid, :id
alias_method :ar_to_json, :to_json
def to_json(options = {}, &block)
default_options = { :methods => :guid }
self.ar_to_json(options.merge(default_options), &block)
end
Specs:
def build_task
task = Task.new
task.save(:validate => false)
task
end
describe Task do
describe 'Customising #to_json for Sprout Core' do
it 'should have an guid alias for id' do
@task = build_task
@task.guid.should == @task.id
end
it 'should include guid in #to_json' do
@task = build_task
json = ActiveSupport::JSON.decode(@task.to_json)
json.has_key?('guid').should be_true
json['guid'].should == @task.id
end
end
end
Oliver said
at 10:18 am on Apr 11, 2010
I feel like a complete idiot asking this but I cant find the directory data_source when I'm supposed to modify the send() call in the createRecord and updateRecord in the data_source/task.js file in your SproutCore project like this:
.send({ task: store.readDataHash(storeKey) });
Devin said
at 12:04 am on May 2, 2010
For everyone who has this problem, it is actually addressed first thing next chapter. that is when the data_source/task.js is created. Or, you can just run:
sc-gen data-source Todos.TaskDataSource
now, in the todos directory and skip the first part of the next chapter.
Rob Bartholomew said
at 8:30 pm on May 10, 2010
This may be completely obvious to most, but I'll state it for anyone having grief: Restart your sc-server after changing the proxy.
Arunjit Singh said
at 7:46 am on Jun 13, 2010
In reference to problem#1
I did some digging around in the console and can see that when I create, a new task appears on the server AND can be accessed in the SC application's store. So, it's just not appearing on the UI for me to edit!
All else works!
You don't have permission to comment on this page.