Article summary
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:
createModel
: This function tells your view how to create a model instance after each successful save.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?