Skip to content
This repository has been archived by the owner on Jun 15, 2023. It is now read-only.

Cookbook

Michael Obrocnik edited this page Jan 5, 2016 · 16 revisions

The following are a collection of proven recipes that are too small to warrant an extension or a plugin but are not complete enough or too far out-of-scope to warrant inclusion in the core of Chaplin.

Simple Data Binding

Problem

Traditionally binding via a micro-template can get messy; especially if the model has not finished syncing with the server when the view is rendered. Re-rendering the view when the model updates is more of an anti-pattern as there could be a lot of structure that is needlessly re-created.

Solution

Add the following method to your base class that extends Chaplin.View and is extended by your Views.

pass: (selector, attribute) ->
  return unless @model

  # Initially bind the current value of the attribute to 
  # the template (if we are rendered).
  $el = @$ selector
  $el.text @model.get attribute if $el

  # Then register a listener to respond to changes in the model
  # to keep the view in sync with the model.
  @listenTo @model, "change:#{attribute}", (model, value) =>
    @$(selector).text value

And call this method as follows in a view that derives from that base view.

initialize: ->
  @pass '.name', 'name'
  @pass '.phone', 'phone'

Which will set up 1-way data bindings for the model attributes name and phone to the DOM elements in the view with the classes .name and .phone respectively.

Limitations

This is limited to one-way binding and there isn't much allowance for complicated logic to generate the value, etc. For a more complete solution, refer to Backbone.Stickit.

Securing Your Application

By lukateake

Problem

You've gone ahead and built a big application. Congratulations! But now you've got a million bookmarkable states that the user wants to use in order to resume their work. Alas, we don't want to keep filtering them through the front door but we do need to make sure they're logged in. Here's what I did and, mind you, I don't like hash signs in my URLs as I feel their atrocious aesthetically so I used pushState which requires some server magic of its own.

(Note: my implementation certainly isn't perfect so if you improve upon it, please do let me know.)

First thing you want to do is make an additional Controller superclass, I called mine AuthController and cloned it from the out-of-the-box one that's provided by Chaplin. Then I added this sweet little method to it:

AuthController.coffee

'use strict'

Chaplin = require 'chaplin'
mediator = require 'mediator'

module.exports = class AuthController extends Chaplin.Controller

  beforeAction:
    '.*': ->
      console.debug 'Controllers.Base.AuthController#beforeAction()'
      console.debug 'path', window.location.pathname
      if Chaplin.mediator.user == null
        mediator.redirectUrl = window.location.pathname
        @redirectToRoute 'auth_login' 

This does a couple of things, the biggest thing it does is intercept every action '.*' of every controller that will inherit/extend from AuthController. Secondly, I'm using the existence of mediator.user to check login state. (Hint: your 'logged in successfully' process would need to set this, of course.) Lastly, if mediator.user nulls out, two additional things happen: a) store the location of where I'm trying to get to, and b) redirect to the named route 'auth_login' as defined by routes.coffee thusly:

routes.coffee

  # login/logout
  match 'login',     controller: 'auth/login',    action: 'login',    name: 'auth_login'
  match 'logout',    controller: 'auth/login',    action: 'logout',   name: 'auth_logout'

Next, have every portion of your application that you want secured have its controller extend from AuthController. However, do not have the LoginController inherit/extend it though. Instead for it, use the vanilla one that doesn't have the beforeAction check. And speaking of LoginController, let's go ahead and create that now.

It's pretty straight forward from here on out: have your LoginController and its related View implement some sort of authentication mechanism. (Security processes are touchy point among developers and/or application requirements vary considerably, thus, I effectively 'punt the ball' and leave implementation details as an exercise to the reader.) From the routes.coffee file above, you can see mine is in auth/login.

When the user is successfully authenticated, do this:

login-view.coffee

loginSuccess: (user) =>
  mediator.user = user
  if mediator.redirectUrl == null
    @publishEvent '!router:routeByName', 'site_home'
  else
    @publishEvent '!router:route', mediator.redirectUrl
  @publishEvent 'loginStatus', true

I hope this helps someone.

Atomic addition to collection

Adds a collection atomically, i.e. throws no event until all members have been added.

addAtomic: (models, options = {}) ->
  return if models.length is 0
  options.silent = true
  direction = if typeof options.at is 'number' then 'pop' else 'shift'
  clone = _.clone models
  while model = clone[direction]()
    @add model, options
  @trigger 'reset'

Calculated properties on a Model

Problem

Property methods added on to a model cannot be referenced on a template.

Solution

Add the following property and method to your Model class extending Chaplin.Model.

# lib/models/model.coffee
Chaplin = require 'chaplin'
module.exports = class Model extends Chaplin.Model
  accessors: null
  getAttributes: ->
    data = Chaplin.utils.beget super
    data[k] = @[k].bind(this) for k in fns if fns = @accessors
    data
Usage
# models/user.coffee
Model = require 'lib/models/model'
module.exports = class User extends Model
  accessors: ['fullName']
  fullName: -> "#{ @get 'firstName' } #{ @get 'lastName' }"
Limitations

This only allows for read-only accessors and the property methods are not cached. There are a number of improvements that can be made such as caching properties and updating them on change events and allowing for mutators as well. See https://github.com/asciidisco/Backbone.Mutators for a more complete alternative.

List View and Detail View Interaction

Problem

How to present a DetailView from CollectionView without asking the server for the item

Solution

Load the collection in the controller beforeAction as a compose item. This way it persists when the action change from list to show

# was
list: ->
  @collection = new Collection
  @collection.fetch()
show: (params) ->
  @model = new Model id:params.id
  @view = new DetailsView model: @model 
  @model.fetch()

# now, moved @collection as a compose 
beforeAction: ->
  @compose 'collection', ->
    reuse: ->
      @item = new Collection
      @item.fetch()

list: ->
  @view = new CollectionView collection: @compose('collection')

show: (params) ->
  @view = new DetailsView model: @compose('collection').get(params.id)

Compositions for Performance and Profit

How to use Chaplin.Composer to achieve memory-safe reuse of synchronized collections and models within a Chaplin.Controller.

Use case

Provide a two-column layout backed by data from the same asynchronous collection, one column showing the data from the entire collection and the other a detail view of a single model in the collection.

Problem statement

In theory, this sounds pretty simple. Use the promise pattern or wait for a sync event on the collection, and then hydrate each of the views - one with the collection data and the other with a model from the collection. But if not done carefully the above can quickly lead to memory leakage, layout thrashing and redundant calls to the server.

The solution

Chaplin.Composer to the rescue! Let the example below serve as a guide for creating layouts of the kind indicated without worry of layout thrashing or memory leaks or unexpected object disposal.

module.exports = class ShopController extends Controller
 
  # Compose the reusable item in the controller `beforeAction`
  beforeAction: (params, route) ->
    super
    @compose 'departments',
      compose: ->
        @item = new Departments # note use of the identifier name `item`
        @item.fetch()

  departments: (params) ->
    # Compose the two-column layout, and attach it to the `main` region
    @compose 'layout', SidebarLayoutView, {region: 'main'}

    # Compose the sidebar so it does not get reloaded unexpectedly
    @compose 'sidebar',
      reuse: =>
        collection = @compose 'departments' # call controller's compose method; keep var local
        new SidebarView {collection, region: 'sidebar'} # instantiate and hydrate sidebar view

    name = if params.id then Chaplin.utils.upcase params.id else 'Accessories'
    model = @compose('departments').findWhere {name} # note the local var assignment

    @view = new DepartmentsPageView {model, region: 'page'}
    @adjustTitle "Shop #{model?.get('name') || 'Departments'}"