In this third and final part of my "Merging ember-cli with Rails" blog series, I will talk about how I connected the Ember frontend with the Rails backend so that beer rankings could be persisted in the database.###Writing the AJAX call.In Part 2 you may recall the swap controller action I put in my ballot controller. Well, that wasn't sending anything back to the Rails API. Every time I reloaded the browser, my beers were back in their original order. Let’s fix that.EmberApp/app/controllers/ballot.jsimport Ember from 'ember';
export default Ember.Controller.extend({
sortProperties: ['weight'],
actions: {
swap: function(line_item_id_1, line_item_id_2) {
var li1, li1_weight, li2, li2_weight;
li1 = this.model.get('line_items').findBy('id', line_item_id_1);
li2 = this.model.get('line_items').findBy('id', line_item_id_2);
li1_weight = li1.get('weight');
li2_weight = li2.get('weight');
li1.set('weight', li2_weight);
li2.set('weight', li1_weight);
return Ember.$.ajax({
url: "/api/v1/ballots/" + (this.model.get('id')) + "/swap/" + (li1.get('id')) + "/" +(li2.get('id')), type: 'put'
}).done(function() {}).fail(function() {
li1.rollback();
return li2.rollback();
});
}
}
});
Note the addition of the AJAX call at the end of the swap action in the ballot controller. There is likely a cleaner and more Ember(y) way to do this, and I would love to hear it in the comments below; however, that call got the job done for me and it's good enough to finish out this drag and drop prototype. I opened my console and was pleased to see an AJAX PUT network request after a drop event. Let’s set up our Rails API endpoint.###The CouplingIn my head, I know I want the interaction between my Ember and Rails apps to look like this:---------------- -------------------
| | PUT: /api/v1/ballots/1/swap/1/2 | |
| Ember Frontend | ==============================> | Rails API Backend |
| | | |
---------------- -------------------
That is really all we need. A single endpoint that says "swap the weight of line item 1 and line item 2 on ballot 1." That way, we keep the data on our backend API in sync with that is going on in the Ember frontend.So, let’s do a little Rails to get this all set up. We are going to generate a ballot controller, then update the routes to access the request to /api/v1/ballots/:id/swap/:li1_id/:li2_id$ rails g controller Api::V1::Ballots
config/routes.rbnamespace :api do
namespace :v1 do
resources :ballots do
put '/swap/:li1/:li2', to: 'ballots#swap'
end
end
end
Then make yourself a controller action for swap.app/controllers/ballots_controller.rbclass Api::V1::BallotsController < ApplicationController
respond_to :json
def swap
ballot = Ballot.find(params[:ballot_id])
ballot.swap(LineItem.find(params[:li1]), LineItem.find(params[:li2]))
if ballot.save
render json: {}
else
render status: 500
end
end
end
Lastly, I added this method to my Ballot model to handle swapping line items.app/models/ballots.rbdef swap(li1, li2)
li1_weight = li1.weight
li2_weight = li2.weight
li1.weight = li2_weight
li2.weight = li1_weight
li1.save && li2.save
end
So that should work well. Right?###Dealing with Rails CSRFThat AJAX PUT should have worked but our Rails API is throwing back a crazy error.ActionController::InvalidAuthenticityToken
What is going on now is that Rails is kicking back our request due to a missing CSRF token. Luckily, there is a CSRF ember npm package we can install that will handle setting the token in all our request headers.$ npm install --save rails-csrf
Once that is done running, update your app.js to include your new module and your routes/index.js to request the rails CSRF token before page load:EmberApp/app/app.jsimport Ember from 'ember';
import Resolver from 'ember/resolver';
import loadInitializers from 'ember/load-initializers';
import config from './config/environment';
Ember.MODEL_FACTORY_INJECTIONS = true;
var App = Ember.Application.extend({
modulePrefix: config.modulePrefix,
podModulePrefix: config.podModulePrefix,
Resolver: Resolver
});
loadInitializers(App, config.modulePrefix);
//Initialize Rails CSRF
loadInitializers(App, 'rails-csrf');
import { setCsrfUrl } from 'rails-csrf/config';
setCsrfUrl('api/v1/csrf');
export default App;
EmberApp/app/routes/index.jsimport Ember from 'ember';
export default Ember.Route.extend({
controllerName: 'ballot',
beforeModel: function() {
return this.csrf.fetchToken();
},
model: function() {
return this.store.find('ballot', 1);
},
setupController: function(controller, model){
controller.set('model', model);
}
});
Note the addition of "beforeModel." This will request the csrf token from the backend before every page load, but we are not done yet. We need to support this request to /api/v1/csrf in our Rails application.First, we add this line to our routes.rb file underneath the :api and :v1 namespace declarations.routes.rbget :csrf, to: 'csrf#index'
Then we will generate a simple CSRF controller to handle this request.$ rails g controller Api::V1::Csrf index
csrf_controller.rbclass Api::V1::CsrfController < ApplicationController def index render json: { request_forgery_protection_token => form_authenticity_token }.to_json
end
end
Once that is complete. Restart your rails server then reload your Ember app. Now start dragging and dropping to modify your beer rankings. You should see that, on every drop event, an AJAX request is being sent back to the Rails application and updating the line items weights. Now, when you refresh your browser, your updates to beer rankings will be persisted!##ConclusionWhile it does take a little work to wire up a Rails/Ember application, they play quite well together! Ember-Data's near perfect compatibility with ActiveModelSerializers makes keeping your models up to date crazy simple. And, with the Rails API becoming a first class citizen with Rails 5, I can see this flow becoming even easier in the coming future. So, if you are not a big fan of integrated frameworks like meteor or volt than using Rails and Ember is a very elegant way to get a lot of work done fast!##Resources
We are Smashing Boxes. We don’t just build great products, we help build great companies. LET’S CHAT.