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 = """
  

Create A Task:



Saving to server...

""" 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?