-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathbackbone.giraffe.contrib.coffee
418 lines (357 loc) · 14.4 KB
/
backbone.giraffe.contrib.coffee
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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
((root, factory) ->
if define?.amd
# Register Giraffe as a named AMD module.
define 'backbone.giraffe.contrib', ['jquery', 'underscore', 'backbone', 'backbone.giraffe'], ($, _, Backbone, Giraffe) ->
root.GiraffeContrib = factory(root, $, _, Backbone, Giraffe)
else if module?.exports
# Expose Giraffe to module loaders which implement the Node module pattern, including browserify.
$ = require('jquery')
_ = require('underscore')
Backbone = require('backbone')
Giraffe = require('./backbone.giraffe')
module.exports = factory(root, $, _, Backbone, Giraffe)
else
# Attach Giraffe.Contrib global to the root.
root.GiraffeContrib = factory(root, root.$, root._, root.Backbone, root.Giraffe)
)(@, (root, $, _, Backbone, Giraffe) ->
if not $ then throw new Error('Giraffe.Contrib cannot find jQuery')
if not _ then throw new Error('Giraffe.Contrib cannot find Underscore')
if not Backbone then throw new Error('Giraffe.Contrib cannot find Backbone')
if not Giraffe then throw new Error('Giraffe.Contrib cannot find Giraffe')
Contrib = Giraffe.Contrib =
version: '{{VERSION}}'
previousGiraffeContrib = root.GiraffeContrib
Contrib.noConflict = ->
root.GiraffeContrib = previousGiraffeContrib
@
###
* A __Controller__ is a simple evented class that can participate in appEvents.
* It demonstrates the usage of `Giraffe.configure` which extends any function
* instance with [features including lifecycle management, app events, and more.]
* (http://barc.github.io/backbone.giraffe/backbone.giraffe.html#configure)
*
* @param {Object} options
*
* - all options get merged into object like models and views.
*
* @example
*
* var SfxController = Giraffe.Contrib.Controller.extend({
* appEvents: {
* 'process:complete': 'playDingSound'
* },
*
* playDingSound: function() {
* // Code for playing sound...
* }
* });
*
* // Options get merged into object like views and models.
* var sfxController = new SfxController({
* basePath: 'media/audio',
* });
*
* @author darthapo <github.com/darthapo>
###
class Contrib.Controller
_.extend @::, Backbone.Events
constructor: (options) ->
Giraffe.configure @, options
###
* `Backbone.Giraffe.Contrib` is a collection of officially supported classes that are
* built on top of `Backbone.Giraffe`. These classes should be considered
* experimental as their APIs are subject to undocumented changes.
###
###
* A __CollectionView__ mirrors a `Collection`, rendering a view for each model.
*
* @param {Object} options
*
* - [collection] - {Collection} The collection instance for the `CollectionView`. Defaults to a new __Giraffe.Collection__.
* - [modelView] - {ViewClass} The view created per model in `collection.models`. Defaults to __Giraffe.View__.
* - [modelViewArgs] - {Array} The arguments passed to the `modelView` constructor. Can be a function returning an array.
* - [modelViewEl] - {Selector,Giraffe.View#ui} The container for the model views. Can be a function returning the same. Defaults to `collectionView.$el`.
*
* @example
*
* var FruitView = Giraffe.View.extend({});
*
* var FruitsView = Giraffe.Contrib.CollectionView.extend({
* modelView: FruitView
* });
*
* var view = new FruitsView({
* collection: [{name: 'apple'}],
* });
*
* view.children.length; // => 1
*
* view.collection.add({name: 'banana'});
*
* view.children.length; // => 2
###
class Contrib.CollectionView extends Giraffe.View
@getDefaults: (ctx) ->
collection: if ctx.collection then null else new Giraffe.Collection # lazy lood for efficiency
modelView: Giraffe.View
modelViewArgs: null # optional array of arguments passed to modelView constructor (or function returning the same)
modelViewEl: null # optional selector or Giraffe.View#ui name to contain the model views
renderOnChange: false
constructor: ->
super
_.defaults @, @constructor.getDefaults(@)
@collection = new Giraffe.Collection(@collection) if _.isArray(@collection) # accept a plain array as `collection`
#ifdef DEBUG
throw new Error('`modelView` is required') unless @modelView
#endif
@listenTo @collection, 'add', @addOne
@listenTo @collection, 'remove', @removeOne
@listenTo @collection, 'reset sort', @render
@listenTo @collection, 'change', @_onChangeModel if @renderOnChange
@modelViewEl = @ui?[@modelViewEl] or @modelViewEl if @modelViewEl # accept a Giraffe.View#ui name or a selector
_onChangeModel: (model) ->
view = @findByModel(model)
view.render()
findByModel: (model) ->
for view in @children
if view.model is model
return view
null
_calcAttachOptions: (model) ->
options =
el: null
method: 'prepend'
# Searches backwards for a modelView to insert after, falling back to prepend
index = @collection.indexOf(model)
i = 1
while prevModel = @collection.at(index - i)
prevView = @findByModel(prevModel)
if prevView?._isAttached # TODO a better way, perhaps add to Giraffe API?
options.method = 'after'
options.el = prevView.$el
break
i++
if !options.el and @modelViewEl
options.el = @$(@modelViewEl)
#ifdef DEBUG
throw new Error('`modelViewEl` not found in this view') if !options.el.length
#endif
options
# TODO fails if deep clone is needed
_cloneModelViewArgs: ->
args = @modelViewArgs or [{}]
args = args.call(@) if _.isFunction(args)
args = [args] if !_.isArray(args)
args = _.map(args, _.clone)
#ifdef DEBUG
throw new Error('`modelViewArgs` must be an array with an object as the first value') unless _.isArray(args) and _.isObject(args[0])
#endif
args
# TODO If there was a "rendered" event this wouldn't need to implement afterRender (requiring super calls)
afterRender: ->
@addOne model for model in @collection.models
@
removeOne: (model, options) ->
if @collection.contains(model)
@collection.remove model # falls through
else
modelView = _.findWhere(@children, {model})
modelView?.dispose()
@
addOne: (model) ->
if !@collection.contains(model)
@collection.add model # falls through
else if !@_renderedOnce # TODO a better way, perhaps add to Giraffe API?
@render() # falls through
else
attachOptions = @_calcAttachOptions(model)
modelViewArgs = @_cloneModelViewArgs()
modelViewArgs[0].model = model
modelView = new @modelView(modelViewArgs...)
@attach modelView, attachOptions
@
###
* A __FastCollectionView__ is a __CollectionView__ that _doesn't create a view
* per model_. Performance should generally be improved, especially when the
* entire collection must be rendered, as string concatenation is used to touch
* the DOM once. [Here's a jsPerf with more.](http://jsperf.com/collection-views-in-giraffe-and-marionette/5)
*
* The option `modelEl` can be used to specify where to insert the model html.
* It defaults to `view.$el` and cannot contain any DOM elemenets other
* than those automatically created per model by the `FastCollectionView`.
*
* The option `modelTemplate` is the only required one and it is used to create
* the html per model. ___`modelTemplate` must return a single top-level DOM node
* per call.___ The __FVC__ uses a similar templating system to the
* __Giraffe.View__, but instead of defining `template` and an optional
* `serialize` and templateStrategy`, __FVC__ takes `modelTemplate` and optional
* `modelSerialize` and `modelTemplateStrategy`. As in __Giraffe.View__,
* setting `modelTemplateStrategy` to a function bypasses Giraffe's usage
* of `modelTemplate` and `modelSerialize` to directly return a string of html.
*
* The __FVC__ reacts to the events `'add'`, `'remove'`, `'reset`', and `'sort'`.
* It should keep `modelEl` in sync wih the collection with a template per model.
* The __FVC__ API also exposes a shortcut to manipulating the collection with
* `addOne` and `removeOne`.
*
* @param {Object} options
*
* - [collection] - {Collection} The collection instance for the `FastCollectionView`. Defaults to a new __Giraffe.Collection__.
* - modelTemplate - {String,Function} Required. The template for each model. Must return exactly 1 top level DOM element per call. Is actually not required if `modelTemplateStrategy` is a function, signaling circumvention of Giraffe's templating help.
* - [modelTemplateStrategy] - {String} The template strategy used for the `modelTemplate`. Can be a function returning a string of HTML to override the need for `modelTemplate` and `modelSerialize`. Defaults to inheriting from the view.
* - [modelSerialize] - {Function} Used to get the data passed to `modelTemplate`. Returns the model by default. Customize by passing as an option or override globally at `Giraffe.Contrib.FastCollectionView.prototype.modelSerialize`.
* - [modelEl] - {Selector,Giraffe.View#ui} The selector or Giraffe.View#ui name for the model template container. Can be a function returning the same. Do not put html in here manually with the current design. Defaults to `view.$el`.
*
* @example
*
* var FruitsView = Giraffe.Contrib.CollectionView.extend({
* modelTemplate: 'my-fcv-template-id'
* });
*
* var view = new FruitsView({
* collection: [{name: 'apple'}],
* });
*
* view.render();
*
* view.$el.children().length; // => 1
*
* var banana = new Backbone.Model({name: 'banana'});
*
* view.collection.add(banana);
* // or
* // view.addOne(banana);
*
* view.$el.children().length; // => 2
*
* view.collection.remove(banana);
* // or
* // view.removeOne(banana);
*
* view.$el.children().length; // => 1
###
class Contrib.FastCollectionView extends Giraffe.View
@getDefaults: (ctx) ->
collection: if ctx.collection then null else new Giraffe.Collection # lazy lood for efficiency
modelTemplate: null # either this or a `modelTemplateStrategy` function is required
modelSerialize: if ctx.modelSerialize then null else -> @model # function returning the data passed to `modelTemplate`; called in the context of `modelTemplateCtx`
modelTemplateStrategy: ctx.templateStrategy # inherited by default, can be overridden to directly provide an html string without using `template` and `serialize`
modelEl: null # optional selector or Giraffe.View#ui name to contain the model html
renderOnChange: true
constructor: ->
super
#ifdef DEBUG
throw new Error('`modelTemplate` or a `modelTemplateStrategy` function is required') if !@modelTemplate? and !_.isFunction(@modelTemplateStrategy)
#endif
_.defaults @, @constructor.getDefaults(@)
@collection = new Giraffe.Collection(@collection) if _.isArray(@collection) # accept a plain array as `collection`
@listenTo @collection, 'add', @addOne
@listenTo @collection, 'remove', @removeOne
@listenTo @collection, 'reset sort', @render
@listenTo @collection, 'change', @addOne if @renderOnChange
@modelEl = @ui?[@modelEl] or @modelEl if @modelEl # accept a Giraffe.View#ui name or a selector
@modelTemplateCtx =
serialize: @modelSerialize
template: @modelTemplate
Giraffe.View.setTemplateStrategy @modelTemplateStrategy, @modelTemplateCtx
# TODO If there was a "rendered" event this wouldn't need to implement afterRender (requiring super calls)
afterRender: ->
@$modelEl = if @modelEl then @$(@modelEl) else @$el
#ifdef DEBUG
throw new Error('`$modelEl` not found after rendering') if !@$modelEl.length
#endif
@addAll()
@
###
* Removes `model` from the collection if present and removes its DOM elements.
###
removeOne: (model, collection, options) ->
if @collection.contains(model)
@collection.remove model # falls through
else
index = options?.index ? options
@removeByIndex index
@
###
* Adds `model` to the collection if not present and renders it to the DOM.
###
addOne: (model) ->
if !@collection.contains(model)
@collection.add model # falls through
else if !@_renderedOnce # TODO a better way, perhaps add to Giraffe API?
@render() # falls through
else
html = @_renderModel(model)
@_insertModelHTML html, model
@
###
* Adds all of the models to the DOM at once. Is destructive to `modelEl`.
###
addAll: ->
html = ''
for model in @collection.models
html += @_renderModel(model)
@$modelEl.empty()[0].innerHTML = html
@
###
* Removes children of `modelEl` by index.
*
* @param {Integer} index
###
removeByIndex: (index) ->
$el = @findElByIndex(index)
#ifdef DEBUG
throw new Error('Unable to find el with index ' + index) if !$el.length
#endif
$el.remove()
@
###
* Finds the element for `model`.
*
* @param {Model} model
###
findElByModel: (model) ->
@findElByIndex @collection.indexOf(model)
###
* Finds the element inside `modelEl` at `index`.
*
* @param {Integer} index
###
findElByIndex: (index) ->
$(@$modelEl.children()[index])
###
* Finds the corresponding model in the collection by a DOM element.
* Is especially useful in DOM handlers - pass `event.target` to get the model.
*
* @param {String/Element/$/Giraffe.View} el
###
findModelByEl: (el) ->
index = $(el).closest(@$modelEl.children()).index()
@collection.at index
###
* Generates a model's html string using `modelTemplateCtx` and its options.
###
_renderModel: (model) ->
@modelTemplateCtx.model = model
@modelTemplateCtx.templateStrategy()
###
* Inserts a model's html into the DOM.
###
_insertModelHTML: (html, model) ->
$children = @$modelEl.children()
numChildren = $children.length
index = @collection.indexOf(model)
if numChildren is @collection.length
$existingEl = $($children[index])
$existingEl.replaceWith html
else if index >= numChildren
@$modelEl.append html
else
$prevModel = $($children[index - 1])
if $prevModel.length
$prevModel.after html
else
@$modelEl.prepend html
@
return Contrib
)