How to Integrate AngularJS With Rails 4 - Blog - Shelly Cloud

Embed Size (px)

Citation preview

  • 8/11/2014 How to integrate AngularJS with Rails 4 - Blog - Shelly Cloud

    https://shellycloud.com/blog/2013/10/how-to-integrate-angularjs-with-rails-4 1/11

    Shelly CloudPricingSupportDocsBlog

    Sign upLog in

    How to integrate AngularJS with Rails 4Posted on October 1, 2013 by Micha Kwiatkowski and has 34 comments

    Building most single-page applications (SPAs for short) is a two-step process: first you create a JSON API in a backendtechnology of choice and then you use that API in the JavaScript application. Here we'll be using Ruby on Rails on thebackend and AngularJS on the frontend.

    The main pain point of any kind of integration is making sure that everything fits together well. This post will not take youthrough building the whole application. Instead, it will focus on making sure all the integration points are handled properly.I will also share with you some practical advice on the topic.

    Code examples used in this post come from a Todo list management application. This text summarizes all the lessonslearned during writing of that app.

    Building a JSON API in RailsBuilding an API in Rails is easy, so we'll roll our own from scratch. Note that if you decide to use a specialized library likeangularjs-rails-resource some details will differ, but the general idea will remain the same.

    Routing

    Let's start by defining routes for our API.

    namespace :api, defaults: {format: :json} do resources :task_lists, only: [:index] do resources :tasks, only: [:index, :create, :update, :destroy] endend

    This is all pretty standard. We can get all lists through the task_lists#index action, get a task listing for a specific list viatasks#index action and operate on specific tasks via create, update and destroy actions. Using format: :json is a handydefault.

    If we run rake routes now, we will get an output similar to this:

    GET /api/task_lists/:task_list_id/tasks(.:format) api/tasks#index {:format=>:json}POST /api/task_lists/:task_list_id/tasks(.:format) api/tasks#create {:format=>:json}PATCH /api/task_lists/:task_list_id/tasks/:id(.:format) api/tasks#update {:format=>:json}PUT /api/task_lists/:task_list_id/tasks/:id(.:format) api/tasks#update {:format=>:json}DELETE /api/task_lists/:task_list_id/tasks/:id(.:format) api/tasks#destroy {:format=>:json}GET /api/task_lists(.:format) api/task_lists#index {:format=>:json}

    There are two HTTP verbs corresponding to the update action: PATCH and PUT. Supporting PATCH is a new featureadded in Rails 4.0. You can read more about it on the offical blog.

    Request parameters

    Rails 4 also changed the way the mass-assignment protection is done. Instead of whitelisting/blacklisting parameters in themodel, you now have to do it in the controller, using require and permit methods. I like to create a helper method thancan be used both in create and update actions:

    def safe_params params.require(:task).permit(:description, :priority, :completed)end

  • 8/11/2014 How to integrate AngularJS with Rails 4 - Blog - Shelly Cloud

    https://shellycloud.com/blog/2013/10/how-to-integrate-angularjs-with-rails-4 2/11

    With this definition, action implementation looks as simple as this:

    def create task = task_list.tasks.create!(safe_params) render json: task, status: 201end

    def update task.update_attributes(safe_params) render nothing: true, status: 204end

    Generating JSON

    The previous example already hinted at this: returning JSON output should be as simple as writing render json:object. I like to use active_model_serializers gem which greatly simplifies the process. Whenever you render an object ora collection of objects to json, a proper serializer will be used. In case of our Todo list application, the following will renderan array of tasks:

    render json: TaskList.find(params[:id]).tasks

    To get the exact format we want (that will be easy to consume by AngularJS), after installing the gem, we also need toconfigure it. Put the following into config/initializers/active_model_serializers.rb:

    ActiveSupport.on_load(:active_model_serializers) do # Disable for all serializers (except ArraySerializer) ActiveModel::Serializer.root = false

    # Disable for ArraySerializer ActiveModel::ArraySerializer.root = falseend

    With this configuration in place and a serializer defined like that:

    # app/serializers/task_serializer.rbclass TaskSerializer < ActiveModel::Serializer attributes :id, :description, :priority, :due_date, :completedend

    we'll get the following output:

    [ {'id' => 123, 'description' => 'Send newsletter', 'priority' => 2, 'due_date' => '2013-09-10', 'completed' => true}, {'id' => 124, 'description' => 'Prepare presentation', 'priority' => 1, 'due_date' => '2013-09-17', 'completed' => false}]

    Testing

    All respectable APIs have to be well tested. Fortunately, Rails makes writing automated tests really easy. In case of aJSON API, controller tests are the way to go. That's how a sample test may look like, using RSpec syntax:

    describe Api::TasksController do it "should be able to create a new record" do post :create, task_list_id: task_list.id, task: {description: "New task"}, format: :json response.should be_success JSON.parse(response.body).should == {'id' => 123, ...} endend

    An important detail to note here is the use of format: :json. This makes sure that the parameters are passed andinterpreted as JSON.

  • 8/11/2014 How to integrate AngularJS with Rails 4 - Blog - Shelly Cloud

    https://shellycloud.com/blog/2013/10/how-to-integrate-angularjs-with-rails-4 3/11

    When writing more tests like this, you may find it useful to define a helper method for parsing the response. Put thefollowing into your spec_helper.rb:

    module JsonApiHelpers def json_response @json_response ||= JSON.parse(response.body) endend

    RSpec.configure do |config| config.include JsonApiHelpers, type: :controllerend

    With this code in place, instead of:

    JSON.parse(response.body).should == {...}

    you can now write:

    json_response.should == {...}

    which is a little cleaner, you must admit. It also has an added bonus that the response will be only parsed once, even if youmake multiple assertions on the output.

    Building AngularJS applicationSince the API is ready it's finally time to move on to building the AngularJS application. There is a breadth of tutorials towatch and read, so I'm not going to repeat that here, instead focusing solely on the integration with Rails.

    Including AngularJS files

    The fastest way to get started is putting the JavaScript include tags for AngularJS directly into layout. At the time of writingthis post, 1.0.8 is the latest stable version, so if you want to use that, put the following two lines intoapp/views/layouts/application.html.slim:

    = javascript_include_tag "//ajax.googleapis.com/ajax/libs/angularjs/1.0.8/angular.min.js"= javascript_include_tag "//ajax.googleapis.com/ajax/libs/angularjs/1.0.8/angular-resource.min.js"

    Of course you can also download the files and put them somewhere in app/assets/javascripts/. Unfortunately theasset pipeline may break some of your AngularJS code due to renaming. To prevent that, put the following line into yourconfig/environments/production.rb:

    config.assets.js_compressor = Uglifier.new(mangle: false)

    This will disable name mangling during JavaScript minification. You can read more about this topic in the official tutorial(scroll down to "A Note on Minification").

    Structuring the AngularJS code

    Each AngularJS application consists of the main application module and some controllers, directives and services. As longas you keep everything under app/assets/javascripts/ the asset pipeline will put them all together without a problem.Ultimately it's up to you where to put each of them, but here's how I've done it.

    First, my application.js lists all the external requirements (like jQuery or AngularJS itself), then the file containing themain application module, to finally use the require_tree directive:

    //= require jquery//= require jquery_ujs//= require turbolinks//= require lib/angular.min//= require lib/angular-resource.min//= require todoApp//= require_tree .

    With that in mind, the main application module is defined in todoApp.js.coffee and looks like this:

    todoApp = angular.module('todoApp', ['ngResource'])

  • 8/11/2014 How to integrate AngularJS with Rails 4 - Blog - Shelly Cloud

    https://shellycloud.com/blog/2013/10/how-to-integrate-angularjs-with-rails-4 4/11

    I keep the rest of the files in suitable subdirectories: controllers, directives and services for standard elements of anAngularJS app, and lib for any other dependencies.

    Defining the service

    The Rails API can be accessed from the AngularJS app through the ngResource module. Instead of using the resourcedirectly in the controller, it's a good practice to define a service around it. This way you can abstract away some peskydetails of accessing data, much like you would do with Rails models.

    Below is a basic service for accessing tasks, written in CoffeeScript.

    angular.module('todoApp').factory 'Task', ($resource) -> class Task constructor: (taskListId) -> @service = $resource('/api/task_lists/:task_list_id/tasks/:id', {task_list_id: taskListId, id: '@id'})

    create: (attrs) -> new @service(task: attrs).$save (task) -> attrs.id = task.id attrs

    all: -> @service.query()

    For example, to get a list of all tasks from a given list, you'd do the following:

    $scope.tasks = Task(taskListId).all()

    It cannot get any easier than this.

    Making it work with CSRF protection

    Rails come with cross-site request forgery protection in the form of a token embeded in the head section of each page. Tomake forms work in AngularJS you need to use that token in all API requests. Put the following three lines into the mainapplication file (todoApp.js.coffee in our case):

    todoApp.config ($httpProvider) -> authToken = $("meta[name=\"csrf-token\"]").attr("content") $httpProvider.defaults.headers.common["X-CSRF-TOKEN"] = authToken

    Making it work with turbolinks

    Turbolinks which became a default in Rails 4 may cause some problems to AngularJS applications, especially if you needto support different SPAs on multiple pages. To overcome this problem, put the following into the main application file:

    $(document).on 'page:load', -> $('[ng-app]').each -> module = $(this).attr('ng-app') angular.bootstrap(this, [module])

    This will make sure the AngularJS application is properly initialized each time a turbolink does its fetch&replace magic.

    Making updates using the PATCH method

    The new PATCH method mentioned in the beginning of this post is not supported by ngResource by default, but it's easyenough to make it work. First, put the following code into the main application file:

    defaults = $http.defaults.headersdefaults.patch = defaults.patch || {}defaults.patch['Content-Type'] = 'application/json'

    It will ensure any PATCH requests are made with application/json content type.

    After that, modify the resource definition from before, so that it specifies PATCH as a prefered verb for the update action.

    $resource('/api/task_lists/:task_list_id/tasks/:id', {task_list_id: taskListId, id: '@id'},

  • 8/11/2014 How to integrate AngularJS with Rails 4 - Blog - Shelly Cloud

    https://shellycloud.com/blog/2013/10/how-to-integrate-angularjs-with-rails-4 5/11

    {update: {method: 'PATCH'}})

    Now, whenever you issue an update on the resource, it will properly submit a PATCH request with JSON content.

    Testing

    Just as Rails, AngularJS has a great testing story. Thanks to its focus on Dependency Injection, unit testing components ofan AngularJS application is a breeze.

    The official tutorial walks you through setting up testing infrastructure, using Karma, so I'm not going to repeat that here. Ifound it easy to use with Jasmine which I already knew and with angular-mocks which helps with mocking some featuresof a web browser.

    Debugging

    When testing fails it's often useful to be able to boot up the browser and poke around manually. As I was learningAngularJS and figuring out integration problems, Misko Hevery's answer on Stackoverflow was a big help to me.

    Turns out, inspecting AngularJS app internals from the browser is not that complicated. All you need to do is to grab anelement with jQuery. For example, that's how you can access scope in the context of the taskDescription element:

    $("#taskDescription").scope()

    From there you can traverse the complete state of your controller.

    Another tool that may come in handy is AngularJS Batarang, a Chrome extension that allows you to inspect and profileyour SPA's internals.

    Now go and build!That should get you through the initial steps of building your dream single-page application.

    Leave your thoughts in the comments and if you need a hosting for your Rails backend you don't have to look far. :)

    Shelly Cloud is a platform for hosting Ruby and Ruby on Rails applications. You can focus on development withoutgetting distracted by deployment, optimization and maintenance.

    Deploy your app now, for free

    Tweet 166

    40

    submit

    34 Comments Shelly Cloud Blog Login

    Sort by Best Share

    Join the discussion

    Reply

    dude 10 months agoExpect syntax, yo.

    9

    Alexander Ross 9 months agoInstead of `config.assets.js_compressor = Uglifier.new(mangle: false)` you can tell angular which object youare injecting. Se attached image.

    Favorite

    Share

    45Like Share

  • 8/11/2014 How to integrate AngularJS with Rails 4 - Blog - Shelly Cloud

    https://shellycloud.com/blog/2013/10/how-to-integrate-angularjs-with-rails-4 6/11

    Reply 6

    Reply

    Micha Kwiatkowski 9 months ago Alexander RossTrue, although I personally prefer to modify one setting than to use a more verbose syntax in all ofmy modules. ;)

    3

    Reply

    mattdrobertson 10 months agoNice post! It would have been nice if you went a little more in depth about how you handle angular views /templates (where do you put them? do you serve them from the asset pipeline? etc).

    Also routing is interesting because it is provided both by Angular and by Rails 3

    Reply

    Micha Kwiatkowski 9 months ago mattdrobertsonThanks!

    Handling AngularJS views in Rails is probably a topic worth of another blog post. ;) In the context ofthis todo app, Gosia did the necessary work in her pull request: https://github.com/mkwiatkowsk...Especially the following two commits are relevant to what you're asking about:

    - https://github.com/mkwiatkowsk...- https://github.com/mkwiatkowsk...

    I hope that helps.

    Reply

    alex 9 months agoGreat post and nice to see it all integrated in the todo app complete with the csrf and devise.

    1

    Reply

    andreareginato 10 months agoI would love to see more about testing Angualar stuff inside a rails integration test, if you do so. And if youdon't, it would be great to grab the code and better understand how you handle this.

    1

    Reply

    Micha Kwiatkowski 9 months ago andreareginatoTesting AngularJS apps inside Rails integration test doesn't differ from testing any other pages withJS components. Personally I like to use capybara with poltergeist driver.

    Reply

    Selasie Hanson 10 months agoIn order to prevent the asset pipeline from breaking your code due to how angular code is written, you canadd ngmin-rails to your gem file. https://github.com/jasonm/ngmi...

    1

    Reply

    Micha Kwiatkowski 9 months ago Selasie HansonSure, that's an alternative to initializing Uglifier with mangle set to false.

    Nick Shook 10 months agofantastic article! I have been using the same API namespacing approach in your routes and active model

    Share

    Share

    Share

    Share

    Share

    Share

    Share

    Share

    Share

  • 8/11/2014 How to integrate AngularJS with Rails 4 - Blog - Shelly Cloud

    https://shellycloud.com/blog/2013/10/how-to-integrate-angularjs-with-rails-4 7/11

    Reply

    fantastic article! I have been using the same API namespacing approach in your routes and active modelserializers, but I have been using yeoman to compile to the views/application folder. What are yourthoughts on yeoman?

    1

    Reply

    Micha Kwiatkowski 10 months ago Nick ShookThanks Nick!

    Getting out of asset pipeline and rails templates is another way to go, and have its own set ofadvantages and disadvantages. It's certainly easier to manage javascript dependencies withyeoman and the workflow is more polished and optimized to frontend development (while Rails stillputs more focus on the backend).

    OTOH if you want to use AngularJS as part of a bigger web application (so not SPA in a strictsense) I believe doing it the way I described is the way to go. It's certainly easier to grasp to a Railsdeveloper that's still learning frontend technologies.

    1

    Reply

    barillax 7 months ago Micha KwiatkowskiMentioned this above in another reply, but curious what you think of our approach to theproblem - replacing the asset pipeline entirely with Grunt, and managing front-enddependencies with Bower. Our blog post about it: http://bit.ly/KzNoMw

    It survived an upgrade from Rails 3 to 4 without any issues, so it seems reasonablydecoupled. Then again, we're not using many gems other than Devise that manage front-end views, so it wasn't a big deal for us to give up niceties like Rails view helpers.

    Reply

    Micha Kwiatkowski 7 months ago barillaxYour approach certainly makes sense and you raised some very good points.Ultimately it comes down to what gives you more value: grunt's focus on JS supportor your existing Rails code.

    If you write most of your application in Javascript using Rails only as an API I'd gowith your solution. I found that my solution works best when integrating AngularJSwith an existing Rails project.

    Worth mentioning is the new kid on the block: https://rails-assets.org/ being a middleground between those two alternatives.

    Reply

    Nick Shook 10 months ago Micha KwiatkowskiI agree. fwiw, another point that my friend made why your approach is preferred is that apage refresh every now and then is good for handling sessions. I def am not too happy withhow you handle 401 requests from a pure angular front-end.

    Reply

    nXqd 9 months agoI am sorry but this is not a fully angularjs integrated. The route is served by rails which means you will makean http request instead of xmlhttp request. And it completely ruins the idea of single page application.

    Micha Kwiatkowski 9 months ago nXqdThank you for your feedback.

    I have to disagree though, because it really depends. SPA is a new concept and there is no "onetrue way", if there ever will be one. While you may contain your whole app within a single HTMLpage, I also see in the wild a hybrid approach: having multiple pages and small SPAs contained on

    Share

    Share

    Share

    Share

    Share

    Share

  • 8/11/2014 How to integrate AngularJS with Rails 4 - Blog - Shelly Cloud

    https://shellycloud.com/blog/2013/10/how-to-integrate-angularjs-with-rails-4 8/11

    Reply

    page, I also see in the wild a hybrid approach: having multiple pages and small SPAs contained onsome of them.

    You may imagine a blog application where posts management area is a SPA while the rest of thepages are non-SPA. It's certainly easier to introduce AngularJS to an existing project this way.

    One of the benefits of that for my todo app was that I was able to use devise for authentication, agem that most Rails programmers also already know. This way I could focus on implementing thecore functionality in AngularJS, and leave the boring parts in Rails. I believe it was a pragmaticchoice, and a right one for learning purposes.

    I encourage you to check out the demo, because I don't think your argument about routes is valid.Rails recognizes my SPA routes, but doesn't handle them: it's all done in AngularJS. Once you're inSPA no page reload happens unless you step outside (e.g. when logging out).

    Reply

    nXqd 8 months ago Micha KwiatkowskiThanks for your reply.

    I agree your idea on there is no "one true way" to implement a SPA. But still, I think it alwaysbetter to point this out in the tutorial so novice programmers won't get confused.

    Btw, thanks for the nice post :)

    Reply

    Nexar 3 months agoHi I'm at a slightly different stage i.e. I am still trying to decide whether using Rails as the back end for anAngular app is the right way to go. Please would you be able to comment or point me to any discussionsyou are aware of regarding this.

    Thanks

    Reply

    Micha Kwiatkowski 3 months ago NexarIt depends on what you want from your backend and how comfortable you are with a giventechnology. If it's just going to be a simple JSON API you can't go wrong with Sinatra or evensomething like Firebase (see http://angularfire.com/ ). If you need something more, then Rails maybe the way to go.

    Reply

    boriscy 4 months agoThanks this post solved my issue working with angular and turbolinks.

    Reply

    Max 5 months agoIn your example above you run

    Task(taskListId).all()

    how come I have to run

    new Task(taskListId).all()

    calling Task().all() giving has no method 'all'

    winescout 5 months agoNice Post, thanks. I know I'm late to party, but wanted to bring up the Angular Service you have written. IfI'm reading it correctly, you will get the raw attrs, and not a $resource instance back from your create

    Share

    Share

    Share

    Share

    Share

    Share

  • 8/11/2014 How to integrate AngularJS with Rails 4 - Blog - Shelly Cloud

    https://shellycloud.com/blog/2013/10/how-to-integrate-angularjs-with-rails-4 9/11

    Reply

    I'm reading it correctly, you will get the raw attrs, and not a $resource instance back from your createfunction, and thus will not be able to call update on it without first instantiating a new instance, passing theattrs back in.

    create: (attrs) ->new @service(task: attrs).$save (task) ->attrs.id = task.idattrs

    Why not just return the promise you get back from $save, like so?

    create: (attrs) ->new @service(task: attrs).$save()

    Reply

    indykish 9 months agoDear,

    I see that you have this in your application.js. Have you manually downloaded angular.*.js files intolib/assets dir ? //= require lib/angular.min//= require lib/angular-resource.min

    Reply

    Micha Kwiatkowski 9 months ago indykishYes, section "Including AngularJS files" contains details on how to make that work.

    Reply

    Guest 9 months agoVery nice article, but I don't think that we really need turbo links in this case. We can handle by usingangular router :)

    Reply

    Micha Kwiatkowski 9 months ago GuestThanks!

    Tubrolinks workaround may come in handy when you need to support different SPAs on multiplepages, just as I described in the post. :)

    1

    Reply

    Fred 9 months agoSweet! This is a quick and easy read on integrating Angular with a Rails API. Glad I stumbled across it.Thanks for sharing.

    Reply

    wwwoodall 10 months agoShort and sweet. Thanks!

    Reply

    Pedro 10 months agoThis is great! thank you so much for sharing.

    Christian 10 months agoNice article, I just want to mention two things:

    * If you use a namespace for your api, you probably want to add a version too, like /api/v1

    Share

    Share

    Share

    Share

    Share

    Share

    Share

    Share

  • 8/11/2014 How to integrate AngularJS with Rails 4 - Blog - Shelly Cloud

    https://shellycloud.com/blog/2013/10/how-to-integrate-angularjs-with-rails-4 10/11

    Reply

    * If you use a namespace for your api, you probably want to add a version too, like /api/v1

    * You should not respond with an Array as the JSON root object in the response, as a CSRF attack couldoverwrite the Array constructor in JS and intercept all server responses. Always use an object iteral as rootobject.

    Reply

    Micha Kwiatkowski 10 months ago ChristianChristian, thank you for reading!

    Yeah, good point about the URL. Versioning APIs is another can of worms, so didn't want tomuddle the topic with that.

    Abusing Array constructor is an interesting attack, thanks for pointing that out! I'm gonna learnmore about this and update the code and the article at some point.

    Reply

    Mike Bard 10 months ago Micha KwiatkowskiMichal, I see some changes in repo. Have you free time to update post? )

    Reply

    Micha Kwiatkowski 10 months ago Mike BardHey Mike.

    I wanted for this tutorial to be general in nature. Although I use examples from thetodo project so that the text is more concrete and understandable, their specifics arenot important here - the lessons learned are.

    So I won't be updating the code here. I feel it would unnecessarily muddle the focusof the article.

    I think the new code in the repo, especially the last pull request, is a good example ofhow a basic application like this may evolve into something bigger. You are free toask questions about the design decisions I made: here or on github, whatever youprefer.

    Subscribe Add Disqus to your site

    Share

    Share

    Share

    Share

    Subscribe RSSFollow on TwitterFollow on Google+

  • 8/11/2014 How to integrate AngularJS with Rails 4 - Blog - Shelly Cloud

    https://shellycloud.com/blog/2013/10/how-to-integrate-angularjs-with-rails-4 11/11

    Get most interesting Ruby and DevOps articles curated by our team delivered to your email once a month.

    Your email

    Join our newsletter

    Recent posts

    28 JulNew: Shelly Cloud status feed10 AprHeartbleed vulnerability21 FebHow you can ruin your business by choosing self-managed hosting17 FebHow to create a "Follow Us" pop-up box using the ngAnimate library

    CompanyAbout UsPrivacy PolicyTerms of Service

    PlatformSupportDocumentationPricingPricing comparisonOwn servers comparisonStatus

    CommunityBlogTwitterGoogle+Facebook

    AccountSign upLog in

    Shelly Cloud 2014 Shelly Cloud

    This site uses cookies. By continuing to browse the site, you are agreeing to our use of cookies. Review our Privacy Policyfor more details.

    Talk to a human