/app/assets/javascripts/luca/core/collection.coffee
CoffeeScript | 409 lines | 207 code | 95 blank | 107 comment | 49 complexity | 8a27a09a8298b3be49cc0f2ca719e351 MD5 | raw file
- collection = Luca.define 'Luca.Collection'
- collection.extends 'Backbone.QueryCollection'
- collection.includes 'Luca.Events'
- collection.triggers "after:initialize",
- "before:fetch",
- "after:response"
- collection.defines
- model: Luca.Model
- # cachedMethods refers to a list of methods on the collection
- # whose value gets cached once it is ran. the collection then
- # binds to change, add, remove, and reset events and then expires
- # the cached value once these events are fired.
- # cachedMethods expects an array of strings representing the method name
- # or objects containing @method and @resetEvents properties. by default
- # @resetEvents are 'add','remove',reset' and 'change'.
- cachedMethods: []
- # if filtering a collection should handle via a call to a REST API
- # and return the filtered results that way, then set this to true
- remoteFilter: false
- initialize: (models=[], @options)->
- _.extend @, @options
- @_reset()
- # By specifying a @cache_key property or method, you can instruct
- # Luca.Collection instances where to pull an array of model attributes
- # usually done with the bootstrap functionality provided.
- # DEPRECATION NOTICE
- if @cached
- console.log 'The @cached property of Luca.Collection is being deprecated. Please change to cache_key'
- if @cache_key ||= @cached
- @bootstrap_cache_key = Luca.util.read(@cache_key)
- if @registerAs or @registerWith
- console.log "This configuration API is deprecated. use @name and @manager properties instead"
- # support the older configuration API
- @name ||= @registerAs
- @manager ||= @registerWith
- @manager = if _.isFunction(@manager) then @manager() else @manager
- # if they specify a
- if @name and not @manager
- @manager = Luca.CollectionManager.get()
- # If we are going to be registering this collection with the CollectionManager
- # class, then we need to specify a key to register ourselves under. @registerAs can be
- # as simple as something as "books", or if you are using collections which need
- # to be scoped with some sort of unique id, as say some sort of belongsTo relationship
- # then you can specify @registerAs as a method()
- if @manager
- @name ||= Luca.util.read(@cache_key)
- @name = Luca.util.read(@name)
- unless @private or @anonymous
- @bind "after:initialize", ()=>
- @register( @manager, @name, @)
- # by passing useLocalStorage = true to your collection definition
- # you will bypass the RESTful persistence layer and just persist everything
- # locally in localStorage
- if @useLocalStorage is true and window.localStorage?
- table = @bootstrap_cache_key || @name
- throw "Must specify a cache_key property or method to use localStorage"
- @localStorage = new Luca.LocalStore( table )
- # Populating a collection with local data
- #
- # by specifying a @data property which is an array
- # then you can set the collection to be a @memoryCollection
- # which never interacts with a persistence layer at all.
- #
- # this is mainly used by the Luca.fields.SelectField class for
- # generating simple select fields with static data
- if _.isArray(@data) and @data.length > 0
- @memoryCollection = true
- @__wrapUrl() unless @useNormalUrl is true
- Backbone.Collection::initialize.apply @, [models, @options]
- if models
- @reset models, silent: true, parse: options?.parse
- Luca.concern.setup.call(@)
- Luca.util.setupHooks.call(@, @hooks)
- @setupMethodCaching()
- @trigger "after:initialize"
- # Luca.Collections will append a query string to the URL
- # and will automatically do this for you without you having
- # to write a special url handler. If you want to use a normal
- # url without this feature, just set @useNormalUrl = true
- # TODO
- #
- # This has got to go. It is messing up URL for show actions
- # for models part of luca collections when there are base query params
- __wrapUrl: ()->
- if _.isFunction(@url)
- @url = _.wrap @url, (fn)=>
- val = fn.apply @
- parts = val.split('?')
- existing_params = _.last(parts) if parts.length > 1
- queryString = @queryString()
- if existing_params and val.match(existing_params)
- queryString = queryString.replace( existing_params, '')
- new_val = "#{ val }?#{ queryString }"
- new_val = new_val.replace(/\?$/,'') if new_val.match(/\?$/)
- new_val
- else
- url = @url
- params = @queryString()
- @url = _([url,params]).compact().join("?")
- queryString: ()->
- parts = _( @base_params ||= Luca.Collection.baseParams() ).inject (memo, value, key)=>
- str = "#{ key }=#{ value }"
- memo.push(str)
- memo
- , []
- _.uniq(parts).join("&")
- resetFilter: ()->
- @base_params = _( Luca.Collection.baseParams() ).clone()
- @
- applyFilter: (filter={}, options={})->
- options = _( options ).clone()
- if options.remote? is true or @remoteFilter is true
- @applyParams(filter)
- @fetch _.extend(options,refresh:true,remote:true)
- else
- @reset @query(filter, options)
- # You can apply params to a collection, so that any upcoming requests
- # made to the REST API are made with the key values specified
- applyParams: (params)->
- @base_params = _( Luca.Collection.baseParams() ).clone() || {}
- _.extend @base_params, params
- @
- register: (collectionManager, key="", collection)->
- collectionManager ||= Luca.CollectionManager.get()
- unless key.length >= 1
- throw "Attempt to register a collection without specifying a key."
- # by passing a string instead of a reference to an object, we can look up
- # that object only when necessary. this prevents us from having to create
- # the manager instance before we can define our collections
- if _.isString( collectionManager )
- collectionManager = Luca.util.resolve( collectionManager )
- unless collectionManager?
- throw "Attempt to register with a non existent collection manager."
- if _.isFunction( collectionManager.add )
- return collectionManager.add(key, collection)
- # If we don't want to use the CollectionManager class, and just want
- # to cache collection instances on an object, we can do that too.
- if _.isObject( collectionManager )
- collectionManager[ key ] = collection
- # A Luca.Collection will load models from the in memory model store
- # returned from Luca.Collection.cache, where the key returned from
- # the @cache_keyattribute or method matches the key of the model cache
- loadFromBootstrap: ()->
- return unless @bootstrap_cache_key
- @reset @cached_models()
- @trigger "bootstrapped", @
- # an alias for loadFromBootstrap which is a bit more descriptive
- bootstrap: ()->
- @loadFromBootstrap()
- # cached_models is a reference to the Luca.Collection.cache object
- # key'd on whatever this collection's bootstrap_cache_key is set to be
- # via the @cache_key() interface
- cached_models: ()->
- Luca.Collection.cache( @bootstrap_cache_key )
- # Luca.Collection overrides the default Backbone.Collection.fetch method
- # and triggers an event "before:fetch" which gives you additional control
- # over the process
- #
- # in addition, it loads models directly from the bootstrap cache instead
- # of going directly to the API
- fetch: (options={})->
- @trigger "before:fetch", @
- return @reset(@data) if @memoryCollection is true
- # fetch will try to pull from the bootstrap if it is setup to do so
- # you can actually make the roundtrip to the server anyway if you pass
- # refresh = true in the options hash
- return @bootstrap() if @cached_models().length and not (options.refresh is true or options.remote is true)
- url = if _.isFunction(@url) then @url() else @url
- return true unless ((url and url.length > 1) or @localStorage)
- @fetching = true
- try
- Backbone.Collection.prototype.fetch.apply @, arguments
- catch e
- console.log "Error in Collection.fetch", e
- throw e
- # onceLoaded is equivalent to binding to the
- # reset trigger with a function wrapped in _.once
- # so that it only gets run...ahem...once.
- #
- # it won't even bother fetching it it will just run
- # as if reset was already triggered
- onceLoaded: (fn, options={})->
- _.defaults(options, autoFetch: true)
- if @length > 0 and not @fetching
- fn.apply @, [@]
- return
- wrapped = ()=> fn.apply @,[@]
- @bind "reset", ()->
- wrapped()
- @unbind "reset", @
- unless @fetching or not !!options.autoFetch
- @fetch()
- # ifLoaded is equivalent to binding to the reset trigger with
- # a function, if the collection already has models it will just
- # run automatically. similar to onceLoaded except the binding
- # stays in place
- ifLoaded: (fn, options={scope:@,autoFetch:true})->
- scope = options.scope || @
- if @length > 0 and not @fetching
- fn.apply scope, [@]
- @bind "reset", (collection)=> fn.call(scope,collection)
- unless @fetching is true or !options.autoFetch or @length > 0
- @fetch()
- parse: (response)->
- @fetching = false
- @trigger "after:response", response
- models = if @root? then response[ @root ] else response
- if @bootstrap_cache_key
- Luca.Collection.cache( @bootstrap_cache_key, models)
- models
- # Method Caching
- #
- # Method Caching is a way of saving the output of a method on your collection.
- # And then expiring that value if any changes are detected to the models in
- # the collection
- restoreMethodCache: ()->
- for name, config of @_methodCache
- if config.original?
- config.args = undefined
- @[ name ] = config.original
- clearMethodCache: (method)->
- @_methodCache[method].value = undefined
- clearAllMethodsCache: ()->
- for name, config of @_methodCache
- @clearMethodCache(name)
- setupMethodCaching: ()->
- return unless @cachedMethods?.length > 0
- collection = @
- membershipEvents = ["reset","add","remove"]
- cache = @_methodCache = {}
- _( @cachedMethods ).each (method)->
- # store a reference to the unwrapped version of the method
- # and a placeholder for the cached value
- cache[ method ] =
- name: method
- original: collection[method]
- value: undefined
- # wrap the collection method with a basic memoize operation
- collection[ method ] = ()->
- cache[method].value ||= cache[method].original.apply(collection, arguments)
- # bind to events on the collection, which once triggered, will
- # invalidate the cached value. causing us to have to restore it
- for membershipEvent in membershipEvents
- collection.bind membershipEvent, ()->
- collection.clearAllMethodsCache()
- dependencies = method.split(':')[1]
- if dependencies
- watchForChangesOn = dependencies.split(",")
- _( watchForChangesOn ).each (dependency)->
- collection.bind "change:#{dependency}", ()->
- collection.clearMethodCache(method: method)
- # make sure the querying interface from backbone.query is present
- # in the case backbone-query isn't loaded. without it, it will
- # just return the models
- #
- # TODO:
- #
- # Currently the View mixins: Filterable, Sortable, and Paginatable
- # implement a lot of logic that belongs in this interface.
- query: (filter={},options={})->
- if Backbone.QueryCollection?
- if _.isFunction(prepare = options.prepare || @prepareQuery)
- filter = prepare(filter)
- return Backbone.QueryCollection::query.call(@, filter, options)
- else
- @models
- # Global Collection Observer
- _.extend Luca.Collection.prototype,
- trigger: ()->
- if Luca.enableGlobalObserver
- Luca.CollectionObserver ||= new Luca.Observer(type:"collection")
- Luca.CollectionObserver.relay(@, arguments)
- Backbone.View.prototype.trigger.apply @, arguments
- Luca.Collection._originalExtend = Backbone.Collection.extend
- Luca.Collection.extend = (definition={})->
- # for backward compatibility
- definition.concerns ||= definition.concerns if definition.concerns?
- componentClass = Luca.Collection._originalExtend.call(@, definition)
- if definition.concerns? and _.isArray( definition.concerns )
- for module in definition.concerns
- Luca.decorate( componentClass ).with( module )
- componentClass
- Luca.Collection.namespace = (namespace)->
- namespace = Luca.util.resolve(namespace) if _.isString(namespace)
- Luca.Collection.__defaultNamespace = namespace if namespace?
- Luca.Collection.__defaultNamespace ||= (window || global)
- Luca.util.read( Luca.Collection.__defaultNamespace )
-
- # Always include these parameters in every request to your REST API.
- #
- # either specify a function which returns a hash, or just a normal hash
- Luca.Collection.baseParams = (obj)->
- obj = Luca.util.resolve(obj) if _.isString(obj)
- Luca.Collection._baseParams = obj if obj
- Luca.util.read( Luca.Collection._baseParams )
- Luca.Collection.resetBaseParams = ()->
- Luca.Collection._baseParams = {}
-
- # In order to make our Backbone Apps super fast it is a good practice
- # to pre-populate your collections by what is referred to as bootstrapping
- #
- # Luca.Collections make it easier for you to do this cleanly and automatically
- #
- # by specifying a @cache_keyproperty or method in your collection definition
- # Luca.Collections will automatically look in this space to find models
- # and avoid a roundtrip to your API unless explicitly told to.
- Luca.Collection._bootstrapped_models = {}
- # In order to do this, just load an object whose keys
- Luca.Collection.bootstrap = (obj)->
- _.extend Luca.Collection._bootstrapped_models, obj
- # Lookup cached() or bootstrappable models. This is used by the
- # augmented version of Backbone.Collection.fetch() in order to avoid
- # roundtrips to the API
- Luca.Collection.cache = (key, models)->
- return Luca.Collection._bootstrapped_models[ key ] = models if models
- Luca.Collection._bootstrapped_models[ key ] || []