backbone.listenablemodel
Version:
Composite model for Backbone.js
199 lines (153 loc) • 5.44 kB
text/coffeescript
# Backbone.ListenableModel.js 0.0.4
# (c) 2013 Di Wu MIT License
# https://github.com/diwu1989/Backbone.ListenableModel
# if executing in node
if module?.exports?
_ = require 'underscore'
Backbone = require 'backbone'
else
_ = window._
Backbone = window.Backbone
# save off the previous version of ListenableModel
previousListenableModel = Backbone.ListenableModel
# helper functions
class Helper
@getListenableCallback: (attributeName) ->
callback = (eventName) ->
args = [].slice.call arguments
# rename the change event to include the attribute name
args[0] = attributeName + '.' + args[0]
@trigger.apply @, args
if 'change' is eventName
args[0] = 'change'
@trigger.apply @, args
return callback
@isListenable: (value) ->
# not an object
unless _.isObject(value)
return false
# models are automatically allowed
if value instanceof Backbone.Model
return true
# duck typing
isListenable =
value.on and
value.off and
value.trigger and
value.listenTo and
value.stopListening
return isListenable
@listenableDifference: (source, target) ->
difference = {}
# iterate over the key and value from source
for key, value of source
if target[key] isnt value and Helper.isListenable value
# value is listenable and is different in the target
difference[key] = value
return difference
@getSubmodel: (model, key) ->
if undefined is key or null is key
return [false]
key = String(key)
index = key.indexOf '.'
if -1 is index
# no submodel specified
return [false]
# get the attribute name and retrieve the submodel
attributeName = key.substring 0, index
submodel = model.get attributeName
unless submodel
# submodel doesn't exist, do not forward
return [false]
unless 'function' is typeof submodel.set and 'function' is typeof submodel.get
# submodel doesn't duck type to a model, do not forward
return [false]
subkey = key.substring index + 1
return [submodel, subkey]
@forwardSetToSubmodel: (model, key, value, options) ->
[submodel, subkey] = Helper.getSubmodel model, key
unless submodel
return false
submodel.set.apply submodel, [subkey, value, options]
# set forwarded
return true
@forwardGetToSubmodel: (model, key) ->
[submodel, subkey] = Helper.getSubmodel model, key
unless submodel
return [false]
result = submodel.get.apply submodel, [subkey]
return [true, result]
class ListenableModel extends Backbone.Model
@VERSION: '0.0.4'
@noConflict: ->
Backbone.ListenableModel = previousListenableModel
return previousListenableModel
constructor: ->
# must set up the listenable callbacks hash before calling super constructor
@_listenableCallbacks = {}
# super constructor should eventually call set
super
get: (key) ->
[useSubmodel, result] = Helper.forwardGetToSubmodel @, key
if useSubmodel
return result
# did not forward to submodel
super
set: (key, val, options) ->
if 'object' is typeof key
for name, value of key
if Helper.forwardSetToSubmodel @, name, value, options
# already forwarded this attribute to the submodel
delete key[name]
else if Helper.forwardSetToSubmodel @, key, val, options
# event forwarded, return this
return @
previousAttributes = _.clone @attributes
# call Backbone.Model.set
result = super
unless result
# change failed validation
return result
# compute the listenable attributes and values that were dropped
droppedListenables = Helper.listenableDifference previousAttributes, @attributes
for attr, value of droppedListenables
# find the previously attached callback
callback = @_listenableCallbacks[attr]
# stop listening
@stopListening value, 'all', callback
# remove record
delete @_listenableCallbacks[attr]
# compute the listenable attributes and values that were added
addedListenables = Helper.listenableDifference @attributes, previousAttributes
for attr, value of addedListenables
# value is listenable, create a listener
callbackListener = Helper.getListenableCallback attr
# attach to value
@listenTo value, 'all', callbackListener
# keep track of the callback listener
@_listenableCallbacks[attr] = callbackListener
# return the result from super
return result
_validate: (attrs, options) ->
# call Backbone.Model._validate
result = super
# failed vanilla validation
unless result
return result
# ensure that attribute keys do not have spaces
for key in _.keys attrs
key = String(key)
if -1 isnt key.indexOf ' '
error = "attribute name '#{key}' should not have space"
@trigger 'invalid', @, error, _.extend(options || {}, {validationError: error})
return false
if -1 isnt key.indexOf '.'
error = "attribute name '#{key}' should not have period"
@trigger 'invalid', @, error, _.extend(options || {}, {validationError: error})
return false
return result
# export out ListenableModel
if module?.exports?
module.exports = ListenableModel
else
Backbone.ListenableModel = ListenableModel