Stickies

Rails form enhancement with AngularJS -- Part Two

In the previous post we looked at using AngularJS to progressively enhance a basic Rails form.

In this article we’ll expand on that to build a more complex example with dynamic fields that map one to many models, that you can add or remove inline with Angular, then we will look at handling form state for the non js ( Plain Old Post ) form’s add/remove in Rails, to give a percieved ‘inline’ effect similar to the Angular functionality.

And in the next article ( Part 3 ), we’ll look at progressively enhancing the form with inline validations in AngularJS that fall back to Rails standard validations when JS is unavailable.

If you want to see a working example of this tutorial, just download version 0.0.2 of the app ( assuming you have the capability to run Ruby on Rails on your local machine ) from the releases section of the Github repo.

We will need to add/remove fields on the client ( Browser & Angular ) and persist the form data on the server ( Rails ). So first, we will implement an active model based solution for the Rails backend.

A nested model

First, lets create our nested model for the one to many example:

rails g model article title:string description:string author:references

And add the has many reference and mass assignment to the author model in app/models/author.rb:

class Author < ActiveRecord::Base
  has_many :articles # add this line
  accepts_nested_attributes_for :articles # and this line
end

Now run the migrations to update our schema.

rake db:migrate

The next thing to do is set up the controller and form for our new model. Add a partial at app/views/authors/form/_articles.html.haml with the fields:

#articles{'ng-controller' => 'ArticlesController'}
  = f.fields_for :articles do |a|
    .div{'ng-repeat' => 'article in articles'}
      .field
        = a.label :title
        = a.text_field :title, 'ng-model' => 'title'
      .field
        = a.label :description
        = a.text_area :description, 'ng-model' => 'description'

And then include this partial in the form view.

= render :partial => 'authors/form/articles', locals:{f:f}

Now set up the respective controllers

In app/controllers/authors_controller.rb build an instance of article in the new action:

def new
  @author = Author.new
  @author.articles.build # add this line
end

And modify the angular code at app/assets/javascripts/authors.js.coffee to do the same.

...

  class ArticlesController
    constructor: (@$scope)->
      $scope.articles = [{}]
      $scope.addArticle = ->
        $scope.articles.push( {} )
      $scope.removeArticle = (index)->
        $scope.articles.splice(index, 1)

...
    .controller( 'ArticlesController', ['$scope', ArticlesController] )

A model form service

Now that we have added a new set of nested fields with a nested key author[article], the Angular code needs to be modified to handle the attribute keys in the same way that Rails would do. Otherwise we will end up with complex code or multiple end points doing virtually the same thing. To do this we wrap the form data model in a seperate service.

The service looks like this:

  class FormData
    constructor: (data)->
      formData = data or {articles:[{}]}
      return formData

  angular.module('authorsApp.services', [])
    .factory('FormData', [FormData])

But for the sake of simplicity look at the diff to get a better idea, and follow on from there.

Adding and removing articles.

Now that we have a form service handling our form submission, we can start to think about adding and removing authors and how to implement these in JS and POP ( Plain Old Posts ).

We’re going to start with the progressively enhanced version and the fallback to POP with the help of some directives. Directives are a feature of Angular that enable you to do stuff.

We open up our authors partial at app/views/authors/form/_articles.html.haml. And add the buttons that we are going to use, so that the template reads like this:

#articles{'ng-controller' => 'ArticlesController'}
  = f.fields_for :articles do |a|
    .div{'ng-repeat' => 'article in articles'}
      .field
        = a.label :title
        = a.text_field :title, 'ng-model' => 'article.title'
      .field
        = a.label :description
        = a.text_area :description, 'ng-model' => 'article.description'
      .actions
        = f.submit 'Remove Article', 'pe-remove-article' => true, 'ng-click' => 'removeArticle($index)'
  .actions
    = f.submit 'Add Article', 'pe-add-article' => true

Now we add the angular directive code to enable us to swap out the old submits that we added with the nice angular links.

    .directive('peAddArticle', (@$compile)->
      return {
        link: (scope, element, attrs)->
          @html = '<a ng-click="addArticle()">Add Article</a>'
          @e = $compile(@html)(scope)
          element.replaceWith(@e)
      }
    ).directive('peRemoveArticle', (@$compile)->
      return {
        scope: {
          eventHandler: '&ngClick'
        },
        link: (scope, element, attrs)->
          @html = '<a ng-click="eventHandler()">Remove Article</a>'
          @e = $compile(@html)(scope)
          element.replaceWith(@e)
      }
    )

Handling the form with Plain Old Posts.

This part pays homage to the concepts outlined in Hexagonal Rails.

In order to add and remove articles without the use of angular or ajax, we will need to use some plain old posting with form state handling added in to give us an almost up to par user experience.

The plot thickens

HTML forms only allow us to submit to one url ( without the use of javascript to modify the submission of course! ).

To notify the server that we are adding or removing articles we need to send back a parameter with the submission so that the server app can render the correct response. First, lets modify our enhanced submit buttons by adding an index and some name attributes that will get sent back to the server when the button is clicked.

#articles{'ng-controller' => 'ArticlesController'}
  - @index = 0
  = f.fields_for :articles do |a|
    .div{'ng-repeat' => 'article in articles'}
      .field
        = a.label :title
        = a.text_field :title, 'ng-model' => 'article.title'
      .field
        = a.label :description
        = a.text_area :description, 'ng-model' => 'article.description'
      .actions
        = f.submit 'Remove Article', 'pe-remove-article' => true, 'ng-click' => 'removeArticle($index)', name:"remove_article[#{@index}]"
        - @index += 1
  .actions
    = f.submit 'Add Article', 'pe-add-article' => true, name:'add_article'

The new view after we have made some more modifications.

Form handler

Now that we are telling the server what we pressed when we submit the form, we can write a form handler to process the submission accordingly. Lets add some hexagonal magic to facilitate this.

First, create the form handler in app/handlers/authors_handler.rb, something like this should suffice for now:

class AuthorsHandler < Struct.new(:listener)
  attr_accessor :state_change, :params, :author
  def perform params, author
    @author = author
    @params = params
    set_request_type
    if state_change?
      manage_articles
      listener.recycle_form
    else
      if author.save
        listener.author_create_succeeded
      else
        listener.author_create_failed
      end
    end
  end

  def set_request_type
    @state_change = false
    @state_change = true if params['add_article'] || params['remove_article']
  end

  def state_change?
    state_change == true
  end

  def manage_articles
    if params['add_article']
      author.articles.build
    end

    if params['remove_article']
      articles = author.articles.to_a
      articles.delete_at(remove_article_id)
      author.articles = articles
    end
  end

  def remove_article_id
    params['remove_article'].keys.first.to_i
  end
end

Will will use this handler in the create action of our authors controller, which is the action that our form posts back to every time we press a button in the form. Based on the parameters that are sent back to the server, we can deduce whether or not the request is a form “state_change” or an actual attempt to save the data. First we refactor the create action so that it reads as follows:

# POST /authors
# POST /authors.json
def create
  @author = Author.new(author_params)
  handler = AuthorsHandler.new(self) # pass the controller (self) in as the subscriber/listener.
  handler.perform(params, @author)
end

And now we add some callback methods to our controller subscriber clas, these will perform the correct response actions based on the outcome from the form handler.

def recycle_form
   render :new
end

def author_create_succeeded
  respond_to do |format|
    format.html { redirect_to @author, notice: 'Author was successfully created.' }
    format.json { render action: 'show', status: :created, location: @author }
  end
end

def author_create_failed
  respond_to do |format|
    format.html { render action: 'new' }
    format.json { render json: @author.errors, status: :unprocessable_entity }
  end
end

Also, don’t forget to add the articles attributes to the controller’s permit attributes, you’ll need to change the author_params method like so:

def author_params
   params.require(:author).permit(:name, :email)
   params.require(:author).permit(:name, :email, articles_attributes:[:title, :description])
 end

Now we have a working create form that will submit new data to the server both asyncronously ( the progressive bit ) and traditionally the Plain Old Post bit.

see the full source code changes here » If I have left anything out in this post, please leave a comment and I will update it accordingly.

In the next article, we will look at progressively adding validations.