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