PageRenderTime 3ms CodeModel.GetById 14ms app.highlight 33ms RepoModel.GetById 3ms app.codeStats 0ms

/app/assets/javascripts/luca/core/collection.coffee

https://github.com/moosehead/luca
CoffeeScript | 409 lines | 207 code | 95 blank | 107 comment | 49 complexity | 8a27a09a8298b3be49cc0f2ca719e351 MD5 | raw file
  1collection = Luca.define            'Luca.Collection'
  2collection.extends                  'Backbone.QueryCollection'
  3collection.includes                 'Luca.Events'
  4
  5collection.triggers                 "after:initialize",  
  6                                    "before:fetch",
  7                                    "after:response"
  8
  9collection.defines    
 10  model: Luca.Model
 11  # cachedMethods refers to a list of methods on the collection
 12  # whose value gets cached once it is ran.  the collection then
 13  # binds to change, add, remove, and reset events and then expires
 14  # the cached value once these events are fired.
 15
 16  # cachedMethods expects an array of strings representing the method name
 17  # or objects containing @method and @resetEvents properties.  by default
 18  # @resetEvents are 'add','remove',reset' and 'change'.
 19  cachedMethods: []
 20
 21  # if filtering a collection should handle via a call to a REST API
 22  # and return the filtered results that way, then set this to true
 23  remoteFilter: false
 24
 25  initialize: (models=[], @options)->
 26    _.extend @, @options
 27    @_reset()
 28
 29    # By specifying a @cache_key property or method, you can instruct
 30    # Luca.Collection instances where to pull an array of model attributes
 31    # usually done with the bootstrap functionality provided.
 32
 33    # DEPRECATION NOTICE
 34    if @cached
 35      console.log 'The @cached property of Luca.Collection is being deprecated.  Please change to cache_key'
 36
 37    if @cache_key ||= @cached
 38      @bootstrap_cache_key = Luca.util.read(@cache_key) 
 39
 40    if @registerAs or @registerWith
 41      console.log "This configuration API is deprecated.  use @name and @manager properties instead"
 42
 43    # support the older configuration API
 44    @name ||= @registerAs
 45    @manager ||= @registerWith
 46
 47    @manager = if _.isFunction(@manager) then @manager() else @manager
 48
 49    # if they specify a
 50    if @name and not @manager
 51      @manager = Luca.CollectionManager.get()
 52
 53    # If we are going to be registering this collection with the CollectionManager
 54    # class, then we need to specify a key to register ourselves under. @registerAs can be
 55    # as simple as something as "books", or if you are using collections which need
 56    # to be scoped with some sort of unique id, as say some sort of belongsTo relationship
 57    # then you can specify @registerAs as a method()
 58    if @manager
 59      @name ||= Luca.util.read(@cache_key) 
 60      @name = Luca.util.read(@name) 
 61
 62      unless @private or @anonymous
 63        @bind "after:initialize", ()=>
 64          @register( @manager, @name, @)
 65
 66    # by passing useLocalStorage = true to your collection definition
 67    # you will bypass the RESTful persistence layer and just persist everything
 68    # locally in localStorage
 69    if @useLocalStorage is true and window.localStorage?
 70      table = @bootstrap_cache_key || @name
 71      throw "Must specify a cache_key property or method to use localStorage"
 72      @localStorage = new Luca.LocalStore( table )
 73
 74    # Populating a collection with local data
 75    #
 76    # by specifying a @data property which is an array
 77    # then you can set the collection to be a @memoryCollection
 78    # which never interacts with a persistence layer at all.
 79    #
 80    # this is mainly used by the Luca.fields.SelectField class for
 81    # generating simple select fields with static data
 82    if _.isArray(@data) and @data.length > 0
 83      @memoryCollection = true
 84
 85    @__wrapUrl() unless @useNormalUrl is true
 86
 87    Backbone.Collection::initialize.apply @, [models, @options]
 88
 89    if models
 90      @reset models, silent: true, parse: options?.parse
 91
 92    Luca.concern.setup.call(@)
 93    Luca.util.setupHooks.call(@, @hooks)
 94
 95    @setupMethodCaching()
 96
 97    @trigger "after:initialize"
 98
 99  # Luca.Collections will append a query string to the URL
100  # and will automatically do this for you without you having
101  # to write a special url handler.  If you want to use a normal
102  # url without this feature, just set @useNormalUrl = true
103
104  # TODO
105  #
106  # This has got to go.  It is messing up URL for show actions
107  # for models part of luca collections when there are base query params
108  __wrapUrl: ()->
109    if _.isFunction(@url)
110      @url = _.wrap @url, (fn)=>
111        val = fn.apply @
112        parts = val.split('?')
113
114        existing_params = _.last(parts) if parts.length > 1
115
116        queryString = @queryString()
117
118        if existing_params and val.match(existing_params)
119          queryString = queryString.replace( existing_params, '')
120
121        new_val = "#{ val }?#{ queryString }"
122        new_val = new_val.replace(/\?$/,'') if new_val.match(/\?$/)
123
124        new_val
125    else
126      url = @url
127      params = @queryString()
128
129      @url = _([url,params]).compact().join("?")
130
131  queryString: ()->
132    parts = _( @base_params ||= Luca.Collection.baseParams() ).inject (memo, value, key)=>
133      str = "#{ key }=#{ value }"
134      memo.push(str)
135      memo
136    , []
137
138    _.uniq(parts).join("&")
139
140  resetFilter: ()->
141    @base_params = _( Luca.Collection.baseParams() ).clone()
142    @
143
144  applyFilter: (filter={}, options={})->
145    options = _( options ).clone()
146
147    if options.remote? is true or @remoteFilter is true
148      @applyParams(filter)
149      @fetch _.extend(options,refresh:true,remote:true)
150    else
151      @reset @query(filter, options)
152
153  # You can apply params to a collection, so that any upcoming requests
154  # made to the REST API are made with the key values specified
155  applyParams: (params)->
156    @base_params = _( Luca.Collection.baseParams() ).clone() || {}
157    _.extend @base_params, params
158
159    @
160
161  register: (collectionManager, key="", collection)->
162    collectionManager ||= Luca.CollectionManager.get()
163
164    unless key.length >= 1
165      throw "Attempt to register a collection without specifying a key." 
166
167
168    # by passing a string instead of a reference to an object, we can look up
169    # that object only when necessary.  this prevents us from having to create
170    # the manager instance before we can define our collections
171    if _.isString( collectionManager )
172      collectionManager = Luca.util.resolve( collectionManager )
173
174    unless collectionManager?
175      throw "Attempt to register with a non existent collection manager." 
176
177    if _.isFunction( collectionManager.add )
178      return collectionManager.add(key, collection)
179
180    # If we don't want to use the CollectionManager class, and just want
181    # to cache collection instances on an object, we can do that too.
182    if _.isObject( collectionManager )
183      collectionManager[ key ] = collection
184
185  # A Luca.Collection will load models from the in memory model store
186  # returned from Luca.Collection.cache, where the key returned from
187  # the @cache_keyattribute or method matches the key of the model cache
188  loadFromBootstrap: ()->
189    return unless @bootstrap_cache_key
190    @reset @cached_models()
191    @trigger "bootstrapped", @
192
193  # an alias for loadFromBootstrap which is a bit more descriptive
194  bootstrap: ()->
195    @loadFromBootstrap()
196
197  # cached_models is a reference to the Luca.Collection.cache object
198  # key'd on whatever this collection's bootstrap_cache_key is set to be
199  # via the @cache_key() interface
200  cached_models: ()->
201    Luca.Collection.cache( @bootstrap_cache_key )
202
203  # Luca.Collection overrides the default Backbone.Collection.fetch method
204  # and triggers an event "before:fetch" which gives you additional control
205  # over the process
206  #
207  # in addition, it loads models directly from the bootstrap cache instead
208  # of going directly to the API
209  fetch: (options={})->
210    @trigger "before:fetch", @
211
212    return @reset(@data) if @memoryCollection is true
213
214    # fetch will try to pull from the bootstrap if it is setup to do so
215    # you can actually make the roundtrip to the server anyway if you pass
216    # refresh = true in the options hash
217    return @bootstrap() if @cached_models().length and not (options.refresh is true or options.remote is true)
218
219    url = if _.isFunction(@url) then @url() else @url
220
221    return true unless ((url and url.length > 1) or @localStorage)
222
223    @fetching = true
224
225    try
226      Backbone.Collection.prototype.fetch.apply @, arguments
227    catch e
228      console.log "Error in Collection.fetch", e
229
230      throw e
231
232  # onceLoaded is equivalent to binding to the
233  # reset trigger with a function wrapped in _.once
234  # so that it only gets run...ahem...once.
235  #
236  # it won't even bother fetching it it will just run
237  # as if reset was already triggered
238  onceLoaded: (fn, options={})->
239    _.defaults(options, autoFetch: true)
240
241    if @length > 0 and not @fetching
242      fn.apply @, [@]
243      return
244
245    wrapped = ()=> fn.apply @,[@]
246
247    @bind "reset", ()->
248      wrapped()
249      @unbind "reset", @
250
251    unless @fetching or not !!options.autoFetch
252      @fetch()
253
254  # ifLoaded is equivalent to binding to the reset trigger with
255  # a function, if the collection already has models it will just
256  # run automatically.  similar to onceLoaded except the binding
257  # stays in place
258  ifLoaded: (fn, options={scope:@,autoFetch:true})->
259    scope = options.scope || @
260
261    if @length > 0 and not @fetching
262      fn.apply scope, [@]
263
264    @bind "reset", (collection)=> fn.call(scope,collection)
265
266    unless @fetching is true or !options.autoFetch or @length > 0
267      @fetch()
268
269  parse: (response)->
270    @fetching = false
271    @trigger "after:response", response
272    models = if @root? then response[ @root ] else response
273
274    if @bootstrap_cache_key
275      Luca.Collection.cache( @bootstrap_cache_key, models)
276
277    models
278
279  # Method Caching
280  #
281  # Method Caching is a way of saving the output of a method on your collection.
282  # And then expiring that value if any changes are detected to the models in
283  # the collection
284  restoreMethodCache: ()->
285    for name, config of @_methodCache
286      if config.original?
287        config.args = undefined
288        @[ name ] = config.original
289
290  clearMethodCache: (method)->
291    @_methodCache[method].value = undefined
292
293  clearAllMethodsCache: ()->
294    for name, config of @_methodCache
295      @clearMethodCache(name)
296
297  setupMethodCaching: ()->
298    return unless @cachedMethods?.length > 0 
299
300    collection = @
301    membershipEvents = ["reset","add","remove"]
302    cache = @_methodCache = {}
303
304    _( @cachedMethods ).each (method)->
305      # store a reference to the unwrapped version of the method
306      # and a placeholder for the cached value
307      cache[ method ] =
308        name: method
309        original: collection[method]
310        value: undefined
311
312      # wrap the collection method with a basic memoize operation
313      collection[ method ] = ()->
314        cache[method].value ||= cache[method].original.apply(collection, arguments)
315
316      # bind to events on the collection, which once triggered, will
317      # invalidate the cached value.  causing us to have to restore it
318      for membershipEvent in membershipEvents
319        collection.bind membershipEvent, ()->
320          collection.clearAllMethodsCache()
321
322      dependencies = method.split(':')[1]
323
324      if dependencies
325        watchForChangesOn = dependencies.split(",")
326
327        _( watchForChangesOn ).each (dependency)->
328          collection.bind "change:#{dependency}", ()->
329            collection.clearMethodCache(method: method)
330
331
332  # make sure the querying interface from backbone.query is present
333  # in the case backbone-query isn't loaded.  without it, it will
334  # just return the models
335  #
336  # TODO:
337  # 
338  # Currently the View mixins: Filterable, Sortable, and Paginatable
339  # implement a lot of logic that belongs in this interface.
340  query: (filter={},options={})->
341    if Backbone.QueryCollection?
342      if _.isFunction(prepare = options.prepare || @prepareQuery)
343        filter = prepare(filter) 
344
345      return Backbone.QueryCollection::query.call(@, filter, options)
346    else
347      @models
348
349# Global Collection Observer
350_.extend Luca.Collection.prototype,
351  trigger: ()->
352    if Luca.enableGlobalObserver
353      Luca.CollectionObserver ||= new Luca.Observer(type:"collection")
354      Luca.CollectionObserver.relay(@, arguments)
355
356    Backbone.View.prototype.trigger.apply @, arguments
357
358Luca.Collection._originalExtend = Backbone.Collection.extend
359
360Luca.Collection.extend = (definition={})->
361  # for backward compatibility
362  definition.concerns ||= definition.concerns if definition.concerns?
363
364  componentClass = Luca.Collection._originalExtend.call(@, definition)
365
366  if definition.concerns? and _.isArray( definition.concerns )
367    for module in definition.concerns
368      Luca.decorate( componentClass ).with( module )
369
370  componentClass
371
372Luca.Collection.namespace = (namespace)->
373  namespace = Luca.util.resolve(namespace) if _.isString(namespace)
374  Luca.Collection.__defaultNamespace = namespace if namespace?
375  Luca.Collection.__defaultNamespace ||= (window || global)
376  Luca.util.read( Luca.Collection.__defaultNamespace )
377  
378# Always include these parameters in every request to your REST API.
379#
380# either specify a function which returns a hash, or just a normal hash
381Luca.Collection.baseParams = (obj)->
382  obj = Luca.util.resolve(obj) if _.isString(obj)
383  Luca.Collection._baseParams = obj if obj
384
385  Luca.util.read( Luca.Collection._baseParams )
386
387Luca.Collection.resetBaseParams = ()->
388  Luca.Collection._baseParams = {}
389  
390# In order to make our Backbone Apps super fast it is a good practice
391# to pre-populate your collections by what is referred to as bootstrapping
392#
393# Luca.Collections make it easier for you to do this cleanly and automatically
394#
395# by specifying a @cache_keyproperty or method in your collection definition
396# Luca.Collections will automatically look in this space to find models
397# and avoid a roundtrip to your API unless explicitly told to.
398Luca.Collection._bootstrapped_models = {}
399
400# In order to do this, just load an object whose keys
401Luca.Collection.bootstrap = (obj)->
402  _.extend Luca.Collection._bootstrapped_models, obj
403
404# Lookup cached() or bootstrappable models.  This is used by the
405# augmented version of Backbone.Collection.fetch() in order to avoid
406# roundtrips to the API
407Luca.Collection.cache = (key, models)->
408  return Luca.Collection._bootstrapped_models[ key ] = models if models
409  Luca.Collection._bootstrapped_models[ key ] || []