Last Updated: February 25, 2016
·
1.982K
· rossbrown

Form Errors in Chaplin JS views

When creating form views for models in a Chaplin JS app, it's a good idea to leverage the power of Chaplin's Mediator Pub/Sub pattern for your form submission to render validation and server errors in your form views.

Let's say we have a Party model:

class Party extends Model
  url: '/parties.json'
  defaults:
    ladies: 0
    fellas: 4
    celebrities: 0

  validate: (attrs, options) ->
    if attrs.ladies / attrs.fellas < 1
      "Sausage fest"

and its corresponding "edit" view:

class PartyEditView extends View
  templateName: 'parties/form'

  autoRender: true
  events:
    "submit form": 'saveParty'

  saveParty: (e) ->
    e.preventDefault()
    # Some method to get model attributes from view values goes here
    @model.save(@model.attributes)

So let's say our Party doesn't pass validation. We probably want to know about that, right? Right. Let's first make a simple Error model and a view for it:

class Error extends Model
  defaults:
    text: "Something went wrong"

class ErrorView extends View
  templateName: 'error'
  autoRender: true
  autoAttach: true

So our PartyEditView should do something with this error junk when something goes wrong:

class PartyEditView extends View
  templateName: 'parties/form'

  autoRender: true
  events:
    "submit form": 'saveParty'
  regions:
    '.errors': 'errors'

  initialize: ->
    super
    @error = false

    # When the Party is invalid, do something:
    @model.on "invalid", (model, error) =>
      @renderError(error)

  saveParty: (e) ->
    e.preventDefault()
    @error.dispose() if @error
    # Some method to get model attributes from view values goes here
    @model.save(@model.attributes)

  @renderError: (error) ->
    model = new Error({text: error})
    @error = new ErrorView model: model, region: 'errors'

So now when we try to save the model, it will trigger the invalid event. We've bound a listener to invalid to fire the renderError method, which creates a new Error model and a view for it. The view automatically renders and appends itself to the errors region, which we've defined in the view's regions object. Great.

But what if it passes our front-end validation and the server thinks it's still too lame of a party? We need a way to render errors that the server returns.

First of all, let's look at our PartyEditView. The saveParty method looks fishy. Why is a View concerned about model persistence? We should move @model.save elsewhere. The controller sounds like a good place for this sort of behavior.

class PartiesController extends Controller
  save: (model) ->  
    @model.save model.attributes

But how does the controller know when it should save the model? Chaplin's Pub/Sub service is extremely handy in cases like this. Let's have the view publish a "save_party" message that the controller will listen for.

class PartyEditView extends View
  saveParty: (e) ->
    e.preventDefault()
    @error.dispose() if @error

    # publish the save_party message and pass the model along
    @publishEvent 'save_party', @model
class PartiesController extends Controller
  initialize: ->
    super

    # Listen for word about any cool parties and fire the save method
    @subscribeEvent 'save_party', @save

  save: (model) ->  
    @model.save model.attributes,
      success: (model,response) =>
        alert 'Party was saved!'
        @redirectTo '/'
      error: (model, response) =>
        # Fire the view's renderError method and pass it the server response
        @view.renderError(response)

Typically the server response is going to be an object, so we'll need to modify our view's renderError method a bit to extract some helpful text to display. I have my Rails controller set up to deliver full_messages in the response:

render :json => @party.errors.full_messages, status: 422

So in order to show relevant text to the user, let's do something like this in our PartyEditView's renderError method:

renderError: (error) ->
  @error.dispose() if @error

  if typeof error is 'object'
    # Extract text from each error in server response
    for error in $.parseJSON(error.responseText)
      errorObj =
        text: error
      model = new Error(errorObj)
      @error = new ErrorView model: model, region: 'errors'
  else
    # Display validation error, which is just a string
    errorObj =
      text: error
    model = new Error(errorObj)
    @error = new ErrorView model: model, region: 'errors'

There you have it. Auto-rendering validation and server errors in your view. Ideally you'd take this funcionality and include it in a base view model that you can extend:

class ModelView extends View
  initialize: ->
    super
    @error = false
    @registerRegion '.form-errors', 'errors'
    @subscribeEvent 'renderError', @renderError
    @model.on "invalid", (model, error) =>
      @renderError(error)

  renderError: (error) ->
    @error.dispose() if @error
    if typeof error is 'object'
      for error in $.parseJSON(error.responseText)
        errorObj =
          text: error
        model = new Error(errorObj)
        @error = new ErrorView model: model, region: 'errors'
    else
      errorObj =
        text: error
      model = new Error(errorObj)
      @error = new ErrorView model: model, region: 'errors'

Then you can have error rendering on any view you want, using the @registerRegion method to target a container to render the views in:

class PartyEditView extends ModelView
  templateName: 'party/form'
  autoRender: true
  events:
    "submit form": 'saveParty'
  initialize: ->
    super
    @registerRegion '.party-errors', 'errors'
  saveParty: (e) ->
    e.preventDefault()
    @error.dispose() if @error
    @publishEvent 'save_party', @model

class NoiseViolationEditView extends ModelView
  templateName: 'violation/form'
  events:
    "submit form": 'saveViolation'
  initialize: ->
    super
    @registerRegion '.legal-errors', 'errors'
  saveViolation: (e) ->
    e.preventDefault()
    @error.dispose() if @error
    @publishEvent 'save_violation', @model