blog

Representing Marionette.js Views with State

by

You want to create a simple login page for your new website, all written to be dynamic using Backbone.js and Marionette.js. To accomplish this, you listen for the login button to be pressed and then use jQuery to disable the form controls and display a loading message. An invalid login message comes back, so you reenable the form, highlight the incorrect fields, display a message, etc. The user presses login again… uh oh, you forgot to add code that removes the highlighted fields and hides the error message div.

Login dialog with username and password inputted. An error message is displayed informing the user that the username and/or password are incorrect. The password field has been focused and highlighted for the user.

Your view logic is starting to become tedious and you never know if you’ve covered all the possible paths through your user interface. However, there may be a solution to make your view more organized and predictable: states.

The State Machine

The first step to implementing this is coming up with the states that you’re going to need. After starting with some obvious state and thinking about each situation, I came up with the following state machine.

Initial state is Not Authenticated. When form is submitted, we enter the Pending Authentication state. If the server responds with HTTP 200, enter Authentication Success state. If the server responds with HTTP 404, enter the Authentication Failure state. If the server responds with any other HTTP status code, enter the Authentication Unknown state. The Authentication Success state is the final state. The Authentication Failure and Unknown states both lead to the Pending Authentication state when the form is submitted again.

This assumes that you have a backend API for authenticating users that returns HTTP 200 when the username and password are found to lead to a valid user, and HTTP 404 when the combination of username and password are not found in the users database.

Marionette.js Frontend Code

Overview

To implement this, you will need to create a module that acts as the controller, a model for the form data and state information, a Marionette.js ItemView that renders that model, and an Underscore.js template that is populated by the ItemView. I coded up an example implementation below with some extra narration in the form of code comments. I prefer to write Backbone.js code in CoffeeScript, but it will work just as well in JavaScript of course.

Module (Controller)

loginmodel.coffee

# Initializes Marionette.js global application object.
@gApp = new Backbone.Marionette.Application()
# Prepares the DOM by assigning regions to various elements.
@gApp.addInitializer (options) ->
   @addRegions(content: 'body')
# Login page controller.
@gApp.module 'LoginPage', (module, app, backbone, marionette, $, _) ->
   module.addInitializer (options) ->
      module.loginModel = new ALoginModel()
      module.loginView = new ALoginView(model: module.loginModel)
      app.content.show(module.loginView)
   # Called when the async request to the server returns a successful status.
   module.loginSuccess = (data) ->
      module.loginModel.set('state', module.loginModel.authSuccessState)
   # Called when the async request to the server returns an unsuccessful status.
   module.loginFail = (response) ->
      # HTTP 404 means that the user + password combo was not found.
      # This is the "incorrect username/password" error.
      # Any other status code indicates something unexpected happened on the
      # server such as HTTP 418: I'm a Teapot.
      if 404 == response.status
         module.loginModel.set('state', module.loginModel.authFailState)
      else
         module.loginModel.set('state', module.loginModel.authUnknownState)
         module.loginModel.set('stateDetails', 'Unexpected Server Response: ' +
          response.status + ' ' + response.statusText)
   # The view fires off a global event when the form is submitted so that this
   # controller can catch it and handle the server communication logic.
   app.vent.on 'login:submit', (loginModel) =>
      loginModel.set('state', loginModel.pendingAuthState)
      $.post('/api/auth', loginModel.toJSON())
         .done((data) => module.loginSuccess(data))
         .fail((response) => module.loginFail(response))

Model

class @ALoginModel extends Backbone.Model
   defaults: ->
      username:     ''
      password:     ''
      state:        @notAuthState # This is where you set the initial state.
      stateDetails: ''
   # Define constants to represent the various states and give them descriptive
   # values to help with debugging.
   notAuthState:     'Not Authenticated'
   pendingAuthState: 'Pending Authentication'
   authSuccessState: 'Authentication Success'
   authFailState:    'Authentication Failure'
   authUnknownState: 'Authentication Unknown'

View

loginview.coffee

class @ALoginView extends Backbone.Marionette.ItemView
   # Specifies the Underscore.js template to use.
   template:  '#login-template'
   # Properties of the DOM element that will be created/inserted by this view.
   tagName:   'div'
   className: 'login-area'
   # Shortcut references to components within the UI.
   ui:
      loginForm:           'form'
      usernameField:       'input[name=username]'
      passwordField:       'input[name=password]'
      successMessage:      '.msg-success'
      authErrorMessage:    '.error-bad-auth'
      generalErrorMessage: '.error-unknown'
   # Allows us to capture when the user submits the form either via selecting
   # the login button or typing enter in one of the input fields.
   events:
      'submit form': 'formSubmitted'
   # Specify the model properties that we should rerender the view on.
   modelEvents:
      'change:state':        'render'
      'change:stateDetails': 'render'
   formSubmitted: (event) ->
      # Stop the form from actually submitting to the server.
      event.stopPropagation();
      event.preventDefault();
      @model.set
       'username': @ui.usernameField.val()
       'password': @ui.passwordField.val()
      # Fire off the global event for the controller so that it handles the
      # server communication.
      window.gApp.vent.trigger('login:submit', @model)
   onRender: ->
      # This is where most of the state-dependent logic goes that used to be
      # written as random jQuery calls. Now, since the view is rerendered on
      # each state change, you just have to modify the DOM relative to the
      # initial content specified in the Underscore template.
      switch @model.get('state')
         when @model.notAuthState
            # Focus the username field for the user's convenience.
            @ui.usernameField.focus()
         when @model.pendingAuthState
            # Disable all the form controls and change the button text to show
            # the user that a request is pending.
            @ui.loginForm.find('input, select, textarea').prop('disabled', true)
            @ui.loginForm.find('input[type=submit]').val('Logging In…')
         when @model.authFailState
            # When the user submits invalid credentials, show them an
            # appropriate error message and focus the password field for their
            # convenience.
            @ui.authErrorMessage.show()
            @ui.passwordField.focus()
         when @model.authUnknownState
            @ui.generalErrorMessage.show()
         when @model.authSuccessState
            @ui.successMessage.show()
            @ui.loginForm.hide()
            # Insert more success logic here.
            # For example, you could reload the page, redirect the user to a
            # different page, or you could fire off a global event that causes
            # the page view to switch to a different one.
   onShow: ->
      # The browser can't focus on a field that's not displayed on the screen
      # yet. This happens when the view is first shown on the screen.
      if @model.notAuthState == @model.get('state')
         @ui.usernameField.focus()

Underscore.js Template

<script id="login-template" type="text/template">
   <div class="msg-success hide">You are now logged in.</div>
   <div class="error-bad-auth hide">Username and/or password incorrect.</div>
   <div class="error-unknown hide"><%- stateDetails %></div>
   <form action="#" method="post">
      <h1>Login</h1>
      <input name="username" type="text" value="<%- username %>">
      <input name="password" type="password" value="<%- password %>">
      <input type="submit" value="Log In">
   </form>
</script>

Isn’t That Better?

Instead of having to code up a bunch of messy if/else if statements to transition your UI between the states and handle reverting elements back to their defaults when needed, you just have a single switch statement that takes your default UI and modifies it appropriately for the state that your model is in. This will make your UI logic much more readable, predictable, and extensible.

+ more