-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathBackbone.DeepModelBinder.js.coffee
More file actions
160 lines (131 loc) · 8.17 KB
/
Copy pathBackbone.DeepModelBinder.js.coffee
File metadata and controls
160 lines (131 loc) · 8.17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
#TODO: add functionality to bind to collection.length property
class Backbone.DeepModelBinder
_.extend(@prototype, Backbone.Events)
@collectionRegexp = new RegExp("([^\\[]+)\\[(-?[0-9]+)\]$")
constructor: () ->
@topLevelModelBinder = new Backbone.ModelBinder()
@triggers = null
@bindingChains = {}
bind: (model, rootEl, bindings, options) =>
@model = model
@rootEl = rootEl
@bindings = bindings
@options = options
@_bind()
bindCustomTriggers: (model, rootEl, triggers, attributeBindings, modelSetOptions) =>
@triggers = triggers
@bind(model, rootEl, attributeBindings, modelSetOptions)
unbind: () =>
@topLevelModelBinder.unbind()
_.each(@bindingChains, (bindingChain, modelPath, ignored) =>
#remove any previous bindings/listenTo
bindingChain.modelBinder.unbind()
#_.each(bindingChain.chainSteps, (previouslyObservedModel, path, ignored) =>
# @stopListening(previouslyObservedModel)
#)
@stopListening()
bindingChain.chainSteps = {}
bindingChain.bindings = {}
bindingChain.model = null
)
@bindingChains = {}
_bind: () ->
@unbind()
proxiedBindings = {}
_.each(@bindings, (binding, attribute, ignored) =>
if attribute.indexOf('.') is -1
#nothing special about this binding, send it on to ModelBinder
proxiedBindings[attribute] = binding
else
#ok it's a deep binding, let's deal with it
@_bindDeep(attribute, binding)
)
#pass on our proxied bindings to the real ModelBinder
@_proxyBind(@topLevelModelBinder, @model, proxiedBindings)
#pass on bindings for deeply nested ModelBinders that were created from the @_bindDeep calls above
_.each(@bindingChains, (bindingChain, modelPath, ignored) =>
if bindingChain.model?
@_proxyBind(bindingChain.modelBinder, bindingChain.model, bindingChain.bindings)
)
#attribute = 'post.comments[1].author.name', binding = <typical ModelBinder binding>
_bindDeep: (attribute, binding) ->
#get the 'post.comments[1].author' part of 'post.comments[1].author.name'
nestedModelPath = attribute.substring(0, attribute.lastIndexOf('.'))
#construct our meta object to track model, ModelBinder, bindings and chainSteps
# model: the leaf model being monitored by ModelBinder (ex: 'post.comments[1].author' model)
# modelBinder: the actual ModelBinder that is monitoring the leaf model
# bindings: the bindings that are passed on to the ModelBinder for the leaf elements of this model
# (ex: bindings 'post.comments[1].author.name' and 'post.comments[1].author.email' would have 2 bindings, 1 for 'name' and 'email')
# chainSteps: an array of Backbone.Models representing each step in the chain that are monitored
# (ex: 'post.comments[1].author.name' would have 3 models in this array - one for 'post', 'comments[1]', and 'author')
@bindingChains[nestedModelPath] ?= {model: null, modelBinder: (new Backbone.ModelBinder()), bindings: {}, chainSteps: {}}
bindingChain = @bindingChains[nestedModelPath]
#allright, now let's loop through each part of the chain
attributeParts = attribute.split('.')
currentAttributePath = ''
currentBackboneObject = @model
_.each(attributeParts, (attributePart, index, ignored) =>
if currentBackboneObject? #don't do our complex logic if we encounter null/undefined in the chain (model/collection that hasn't been fetched yet, or could just be null/undefined)
@_validateBackboneModel(currentBackboneObject)
if (collectionMatch = @constructor.collectionRegexp.exec(attributePart))
#ok we're dealing with a collection index step (ex: 'post.comments[1]'), let's break it into its parts
attributeCollectionPart = collectionMatch[1] #ex: 'comments'
attributeCollectionIndex = Number(collectionMatch[2]) #ex: 1
currentAttributePath += '.' if currentAttributePath.length > 0
currentAttributePath += attributeCollectionPart
#now let's make sure to listen if this step (comments collection) in the chain changes (ie: replaced with a different collection)
# nestedModelPath = 'post.comments[1].author', currentAttributePath = 'post.comments', currentBackboneObject = <post model>, attributeCollectionPart = 'comments'
bindingChain.chainSteps[currentAttributePath] = currentBackboneObject
@listenTo(currentBackboneObject, "change:#{attributeCollectionPart}", @_bind)
currentBackboneObject = currentBackboneObject.get(attributeCollectionPart)
if currentBackboneObject?
@_validateBackboneCollection(currentBackboneObject)
currentAttributePath += "[#{attributeCollectionIndex}]"
#now let's make sure to listen if this step (comment within the collection) in the chain changes (ie: the index/order of comments changes)
# nestedModelPath = 'post.comments[1].author', currentAttributePath = 'post.comments[1]', currentBackboneObject = <comments collection>, attributeCollectionIndex = 1
bindingChain.chainSteps[currentAttributePath] = currentBackboneObject
@listenTo(currentBackboneObject, 'add remove reset sort', @_bind)
realIndex = (if attributeCollectionIndex < 0 then (currentBackboneObject.length + attributeCollectionIndex) else attributeCollectionIndex) #allow negative indexes
currentBackboneObject = currentBackboneObject.at(realIndex)
else
#we're dealing with just a model or leaf step (ex: 'post.comments[1].author' or 'post.comments[1].author.name')
if index is (attributeParts.length - 1)
#we're at the end/leaf (ex: 'post.comments[1].author.name'), let's bind this shit and be done with it
# nestedModelPath = 'post.comments[1].author', attributePart = 'name', currentBackboneObject = <author model>, binding = <what was passed in>
bindingChain.attributePathIsComplete = true #indicate that null/undefined wasn't encountered
bindingChain.model = currentBackboneObject
bindingChain.bindings[attributePart] = binding
else
#add to our currentAttributePath
currentAttributePath += '.' if currentAttributePath.length > 0
currentAttributePath += attributePart
#ok we're not at the end of the chain yet (ex: 'post.comments[1].author'), first things first let's
#listen for changes at this step in the chain
# nestedModelPath = 'post.comments[1].author', currentAttributePath = 'post.comments[1].author', currentBackboneObject = <comment model>, attributePart = 'author'
bindingChain.chainSteps[currentAttributePath] = currentBackboneObject
@listenTo(currentBackboneObject, "change:#{attributePart}", @_bind)
#now let's get the next step in the chain and see if we can continue binding to it
currentBackboneObject = currentBackboneObject.get(attributePart)
else
#TODO: if we encounter null/undefined in the chain, we still need to ultimately bind some model to the view. consider:
# m = new Backbone.Model({related: new Backbone.Model({prop: 'something'})})
# # bind some view to 'related.prop' on m
# m.unset('related')
# # the view that was bound to 'related.prop' will not get updated
if bindingChain.attributePathIsComplete isnt false
bindingChain.attributePathIsComplete = false #indicate that null/undefined was encountered
bindingChain.model = new Backbone.Model()
if index is (attributeParts.length - 1)
bindingChain.bindings[attributePart] = binding
)
_validateBackboneModel: (backboneObject) ->
unless (backboneObject instanceof Backbone.Model)
throw new Error('Deeply bound objects must inherit from Backbone.Model')
_validateBackboneCollection: (backboneObject) ->
unless (backboneObject instanceof Backbone.Collection)
throw new Error('Deeply bound objects must inherit from Backbone.Collection')
_proxyBind: (modelBinder, model, bindings) ->
if @triggers?
modelBinder.bindCustomTriggers(model, @rootEl, @triggers, bindings, @options)
else
modelBinder.bind(model, @rootEl, bindings, @options)