These days single page apps are all the rage. There are many technologies emerging that enable slick web UI without having to do much, if anything, on the server side.

Historically, Rails used the Prototype and recently jQuery library to handle progressive enhancement.

AngularJS is a frontend javascript framework developed by Google, it acts like an MVC framework but at a different layer of abstraction. Angular is a declarative tool.

A few words on AngularJS …

AngularJS is a toolset for building the framework most suited to your application development. It is fully extensible and works well with other libraries. Every feature can be modified or replaced to suit your unique development workflow and feature needs. - angularjs.org A few words on progressive enhancement … Progressive enhancement is a strategy for web design that emphasizes accessibility, semantic HTML markup, and external stylesheet and scripting technologies. Progressive enhancement uses web technologies in a layered fashion that allows everyone to access the basic content and functionality of a web page, using any browser or Internet connection, while also providing an enhanced version of the page to those with more advanced browser software or greater bandwidth. - Wikipedia

OK! So we want to progressively enhance our Rails form with AngularJS, for whatever our reasons are :) - Lets get started!

First, implement a basic Rails scaffold.

For the purposes of this tutorial we will be using Rails 4.0.1 and Angular version 1.2.9.

rails new rangular
cd rangular
echo "gem 'haml-rails'" >> Gemfile
bundle
rails g scaffold author name:string email:string
rake db:migrate
rails s

This will generate a basic Rails app ( with author model, database and form views ) and boot the basic rails server.

Now, navigate to the new author form in your browser. You will see a basic non progressive-enhancement form that you can submit to.

Next, angularize the app.

1) Open the file: app/assets/javascripts/author.js.coffee and add the following code:

app = angular.module 'authorsApp', []

2) Now, create a new layout template for the authors resource by copying the existing one…

cp app/views/layouts/application.html.erb app/views/layouts/authors.html.erb

By default rails will look for a layout with the same name as the controller before it falls back to application layout.

3) Open the file: app/views/layouts/authors.html.erb and add the angular cdn to the header:

<head>
...
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.9/angular.min.js"></script>
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.9/angular-route.js"></script>
...

4) then rewrite the <body> tag so it reads:

<body ng-app='authorsApp'>

You can see the file in that state.

Enhance!

Now we can begin to enhance the form.

1) Open the file: app/assets/javascripts/author.js.coffee and replace the contents with the following code:

do (angular)->
  angular.module 'authorsApp', [
    'authorsApp.controllers'
  ]

  class FormController
    constructor: (@$scope, @$http)->
      $scope.submitForm = ->
        $http.post('/authors')

  angular.module('authorsApp.controllers',[])
    .controller 'FormController',['$scope','$http', FormController
    ]

Now edit the cooresponding form template app/views/authors/_form.html.haml like so, you can see the diff here:

= form_for @author, html:{'ng-controller' => 'FormController', 'ng-submit' => 'submitForm()' } { |f|
  - if @author.errors.any?
    #error_explanation
      %h2= "#{pluralize(@author.errors.count, "error")} prohibited this author from being saved:"
      %ul
        - @author.errors.full_messages.each do |msg|
          %li= msg
  .field
    = f.label :name
    = f.text_field :name, 'ng-model' => 'name'
    %span{'ng-bind' => 'name'}
  .field
    = f.label :email
    = f.text_field :email, 'ng-model' => 'email'
    %span{'ng-bind' => 'email'}
  .actions
    = f.submit 'Save'

Now, you will notice when you submit the form, that it still does a traditional full page post, this is obviously not what we want. To get round this issue we need to use an angular directive to replace the submit button with something a bit more friendly.

First, add the directive to the coffeescript.

angular.module('authorsApp.directives', [])
  .directive('peSubmit', (@$compile)->
    return {
      link: (scope, element, attrs)->
        @html = '<a ng-click="submitForm()">Save</a>'
        @e = $compile(@html)(scope)
        element.replaceWith(@e)
    }
  )

Then, bind the forms submit button to the directive, like so…

  .actions
    = f.submit 'Save', 'pe-submit' => true

Note the camelcase to hypenated conversion between the angular directive and the template binding.

Now angular will happily replace the submit button with a link that is bound to the FormControllers submitForm() function, and when you click this link, it will submit the form behind the scenes via ajax, right?

Just one more thing …

We need to set up the XSRF, XRW and Accept headers in order for rails to properly negotiate the request. Also we need to specify the values we are going to send back to rails. Add the following to the angular code. See the diff for details.

angular.module('authorsApp').config(['$httpProvider', (@$httpProvider)->
  @metatag = document.querySelectorAll("meta[name=\"csrf-token\"]")[0]
  @authToken = if metatag then metatag.content else null
  $httpProvider.defaults.headers.common["X-CSRF-TOKEN"] = @authToken
  $httpProvider.defaults.headers['common']['Accept'] = 'application/json'
  $httpProvider.defaults.headers['common']['X-Requested-With'] = 'XMLHttpRequest'
  ])

And in the FormController change the submitForm functions http post to:

$http.post('/authors', author:{name:$scope.name, email:$scope.email})

And there you go. Progressive enhancement for JS users and Plain Old Posts for non JS users!

In the next part(s)

We will look at handlng validations, implementing a nested form and how fields_for correleates quite nicely to Angulars ng-repeat.

  • handle validations with angular
  • implement form service to handle scopes across controllers.
  • implement nested model and its relation to ng-repeat.

Thanks for reading! You can check out the source for this tutorial here

Citing: