-
Notifications
You must be signed in to change notification settings - Fork 232
Cookbook
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
- Securing Your Application
- Atomic addition to collection
- Calculated properties in a Model
- List View and Detail View Interaction
- Compositions for Performance and Profit
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.
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.
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.
By lukateake
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.
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'
Property methods added on to a model cannot be referenced on a template.
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
# models/user.coffee
Model = require 'lib/models/model'
module.exports = class User extends Model
accessors: ['fullName']
fullName: -> "#{ @get 'firstName' } #{ @get 'lastName' }"
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.
How to present a DetailView from CollectionView without asking the server for the item
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)
How to use Chaplin.Composer
to achieve memory-safe reuse of synchronized collections and models within a Chaplin.Controller
.
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.
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.
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'}"