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.
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.
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
# 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))
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'
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()
<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.
This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.