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