Handling Forms with Marionette.js

Marionette.js is an extension library for Backbone.js that offers many improvements and conveniences to cover common use cases for Backbone. On a recent project, I helped build a large single page application using Marionette.

One thing that Marionette lacks out of the box is a convenient way to manage form lifecycles, including validating and submitting forms with minimal overhead. To address this, I created a generic FormView class that extends Marionette’s ItemView and works with the backbone-validation plugin.

Without further ado, here is the Marionette FormView class I created:

form_view.coffee

class FormView extends Backbone.Marionette.ItemView
 
    constructor: ->
      super
 
      @listenTo this, 'render', @hideActivityIndicator
      @listenTo this, 'render', @prepareModel
      @listenTo this, 'save:form:success', @success
      @listenTo this, 'save:form:failure', @failure
 
    delegateEvents: (events) ->
      @ui = _.extend @_baseUI(), _.result(this, 'ui')
      @events = _.extend @_baseEvents(), _.result(this, 'events')
      super events
 
    tagName: 'form'
 
    _baseUI: ->
      submit: 'input[type="submit"]'
      activityIndicator: '.spinner'
 
    _baseEvents: ->
      eventHash =
        'change [data-validation]': @validateField
        'blur [data-validation]':   @validateField
        'keyup [data-validation]':  @validateField
 
      eventHash["click #{@ui.submit}"] = @processForm
      eventHash
 
    createModel: ->
      throw new Error 'implement #createModel in your FormView subclass to return a Backbone model'
 
    prepareModel: ->
      @model = @createModel()
      @setupValidation()
 
    validateField: (e) ->
      validation = $(e.target).attr('data-validation')
      value = $(e.target).val()
      if @model.preValidate validation, value
        @invalid @, validation
      else
        @valid @, validation
 
    processForm: (e) ->
      e.preventDefault()
      @showActivityIndicator()
 
      @updateModel()
      @saveModel()
 
    updateModel: ->
      throw new Error 'implement #updateModel in your FormView subclass to update the attributes of @model'
 
    saveModel: ->
      callbacks =
        success: => @trigger 'save:form:success', @model
        error: => @trigger 'save:form:failure', @model
 
      @model.save {}, callbacks
 
    success: (model) ->
      @render()
      @onSuccess(model)
 
    onSuccess: (model) -> null
 
    failure: (model) ->
      @hideActivityIndicator()
      @onFailure(model)
 
    onFailure: (model) -> null
 
    showActivityIndicator: ->
      @ui.activityIndicator.show()
      @ui.submit.hide()
 
    hideActivityIndicator: ->
      @ui.activityIndicator.hide()
      @ui.submit.show()
 
    setupValidation: ->
      Backbone.Validation.unbind this
      Backbone.Validation.bind this,
        valid: @valid
        invalid: @invalid
 
    valid: (view, attr, selector) =>
      @$("[data-validation=#{attr}]")
        .removeClass('invalid')
        .addClass('valid')
 
    invalid: (view, attr, error, selector) =>
      @failure(@model)
      @$("[data-validation=#{attr}]")
        .removeClass('valid')
        .addClass('invalid')

Simplifying Form Handling

This class does a lot of the heavy lifting towards setting up your form for real-time validation. In order to implement a subclass of this, you only need to define 2 functions:

  1. createModel: This function tells your view how to create a model instance after each successful save.
  2. updateModel: This function tells your view how to read data out of your DOM and update the model.

Besides that, it’s just a simple matter of adding Backbone.validation specifiers to you model class and adding data-validation attributes to the correct HTML elements. I’ve included a simple example of a model, view, and template using the FormView below.

An Example

task_model.coffee

require.define 'task_model': (exports, require, module) ->
  module.exports = class TaskModel extends Backbone.Model
 
    urlRoot: '/tasks'
 
    validation:
      name:
        required: true
      priority:
        required: true
        oneOf: [1, 2, 3, '1', '2', '3']

create_task_view.coffee

require.define 'create_task_view': (exports, require, module) ->
  FormView = require 'form_view'
  TaskModel = require 'task_model'
 
  module.exports = class CreateTaskView extends FormView
 
    template: require 'templates/create_task_template'
 
    className: 'task-form'
 
    ui:
      name: '[name="name"]'
      priority: '[name="priority"]'
      activityIndicator: '.loading'
 
    createModel: -> new TaskModel
 
    updateModel: ->
      @model.set
        name: @ui.name.val()
        priority: parseInt @ui.priority.val()
 
    onSuccess: (model) ->
      Backbone.trigger 'task:create', model

create_task_template.coffee

require.define 'templates/create_task_template': (exports, require, module) ->
 
  template = """
  <h2>Create A Task:</h2>
  <label for="name">Task Name: </label>
  <input type="text" name="name" data-validation="name"/>
  <br/>
  <label for="priority">Priority: </label>
  <select name="priority" data-validation="priority">
    <option value="1">Low</option>
    <option value="2"selected="true">Normal</option>
    <option value="3">High</option>
  </select>
  <br/>
  <input type="submit" value="New Task">
  <p class="loading">Saving to server...</p>
  """
 
  module.exports = _.template template

While Backbone+Marionette.js is not as full featured as a more comprehensive framework like EmberJS or AngularJS, its simplicity and easiness to understand still makes it one of my favorite Javascript stacks. The code in this post is available on Github. There is also a live demo of the code here.

Are you using Marionette.JS? What have been your experiences?