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.