UNPKG

ella-sparse-array-controller

Version:

Sparse array controller for fetching large collections of records a few at a time.

731 lines (580 loc) 19.1 kB
`import Ember from 'ember'` get = Ember.get set = Ember.set ### @module emberella @submodule emberella-controllers ### ### `EmberellaSparseArray` is a variation on an `Ember.ArrayController` that allows content to be lazily loaded from the persistence layer. @class EmberellaSparseArray @extends Ember.ArrayProxy ### EmberellaSparseArray = Ember.ArrayProxy.extend Ember.ControllerMixin, ### @private Stash a reference to the original content object. @property _content @type {Mixed} @default null ### _content: null ### @private Stash the potential total number of items as reported by the persistence layer. @property _length @type {Integer} @default null ### _length: null ### @property isSelectable @type Boolean @default true @final ### isSparseArrayController: true #quack like a duck ### The number of items to fetch together in a single request. Essentially, the "page size" of each query. @property rangeSize @type {Integer} @default 1 ### rangeSize: 1 ### Flag to indicate if this controller should attempt to fetch data. @property shouldRequestObjects @type {Boolean} @default true ### shouldRequestObjects: true ### Alias to `content` property. Override to customize the behavior of content referencing. @property sparseContent ### sparseContent: Ember.computed.alias('content') ### The total number of potential items in the sparse array. If the length is unknown, requesting this property will cause the controller to try to fetch the total length from the persistence layer. @property length @type {Integer} @default 0 @readOnly ### length: Ember.computed -> ret = get(@, '_length') @requestLength() if Ember.isEmpty(ret) get(@, '_length') || 0 .property('_length').readOnly() ### True if this controller instance is attempting to fetch its length. @property isRequestingLength @type {Boolean} @default false ### isRequestingLength: null ### True if this controller instance is attempting to fetch its length. @property isUpdating @type {Boolean} @default false ### # TODO: expand to become true when any data being fetched. isUpdating: Ember.computed -> !!(get(@, 'isRequestingLength')) .property('isRequestingLength') init: -> @_TMP_OBJECT = isSparseArrayItem: true, isStale: true @_TMP_PROVIDE_ARRAY = [] @_TMP_PROVIDE_RANGE = length: 1 @_TMP_RANGE = {} @_super() ### Return the content in array format. @method toArray @return {Array} ### toArray: -> sparseContent = get(@, 'sparseContent') return Ember.A() unless sparseContent sparseContent.toArray() ### Check the content to see if a valid, non-stale object is available at the provided index. @method isObjectAt @param {Integer} idx The index to check for object existence @return {Boolean} ### isObjectAt: (idx) -> result = @objectAt(idx, true) !!(result and result.isStale isnt true) ### Get the data from the specified index. If an object is found at a given index, it will be returned immediately. Otherwise, a "stale" placeholder object will be returned and a new remote query to fetch the data for the given index will be created. @method objectAt @param {Integer} idx The index to obtain content for @param {Boolean} dontFetch Won't obtain remote data if `true` @return {Object} ### objectAt: (idx, dontFetch) -> idx = parseInt idx, 10 return undefined if (isNaN(idx) or (idx < 0) or (idx >= get(@, 'length'))) result = @_super(idx) ? @insertSparseArrayItem(idx) return result if (result and result.isStale isnt true) @requestObjectAt(idx, dontFetch) ### Fetches data at the specified index. If `rangeSize` is greater than 1, this method will also retrieve adjacent items to form a "page" of results. @method requestObjectAt @param {Integer} idx The index to fetch content for @param {Boolean} dontFetch Won't obtain remote data if `true` @return {Object|Null} A placeholder object or null if content is empty ### requestObjectAt: (idx, dontFetch = !get(@, 'shouldRequestObjects')) -> return (get(@, 'sparseContent')[idx] ? @insertSparseArrayItem(idx)) if dontFetch content = get(@, 'content') rangeSize = parseInt(get(@, 'rangeSize'), 10) || 1 return null unless content? start = Math.floor(idx / rangeSize) * rangeSize start = Math.max start, 0 placeholders = Math.min((start + rangeSize), get(@, 'length')) @insertSparseArrayItems([start...placeholders]) if @didRequestRange isnt Ember.K range = @_TMP_RANGE range.start = start range.length = rangeSize @_didRequestRange(range) else @_didRequestIndex(i) for i in [start...rangeSize] get(@, 'sparseContent')[idx] ### Fetches data regarding the total number of objects in the persistence layer. @method requestLength @return {Integer} The current known length ### requestLength: -> len = get(@, '_length') unless (@didRequestLength is Ember.K) or get(@, 'isRequestingLength') set @, 'isRequestingLength', true @_didRequestLength() return len get(@, '_content.length') ### Empty the sparse array. @method reset @chainable ### reset: -> @beginPropertyChanges() len = get(@, '_length') @_clearSparseContent() set(@, '_length', len) @endPropertyChanges() @ ### Uncache the item at the specified index. @method unset @param {Integer} idx The index to unset @chainable ### unset: (idx) -> return @ unless idx? sparseContent = get(@, 'sparseContent') sparseContent[idx] = undefined @ ### Remove the item at the specified index. @method removeObject @param {Mixed} obj The object to remove from the content @chainable ### removeObject: (obj) -> # Ember's standard `removeObject` method will try to fetch all available # data when attempting to remove an object. Disable data fetching to # prevent excessive (and slow) remote queries shouldRequestObjects = get @, 'shouldRequestObjects' @disableRequests() @_super obj @enableRequests() if shouldRequestObjects @ ### Enable data fetching. @method enableRequests @chainable ### enableRequests: -> set(@, 'shouldRequestObjects', true) @ ### Disable data fetching. @method disableRequests @chainable ### disableRequests: -> set(@, 'shouldRequestObjects', false) @ #INJECT PLACEHOLDER OBJECTS ### Insert a placeholder object at the specified index. @method insertSparseArrayItem @param {Integer} idx Where to inject a placeholder @param {Boolean} force If true, placeholder replaces existing content @return {Object} ### insertSparseArrayItem: (idx, force = false) -> sparseContent = get(@, 'sparseContent') proxy = Ember.copy(@_TMP_OBJECT) proxy.contentIndex = idx sparseContent[idx] = proxy if force or !sparseContent[idx]? sparseContent[idx] ### Insert placeholder objects at the specified indexes. @method insertSparseArrayItems @param {Integer|Array} idx Multiple indexes @chainable ### insertSparseArrayItems: (idx...) -> @insertSparseArrayItem(i) for i in [].concat.apply([], idx) @ # CALLBACK METHODS FOR LOADING FETCHED DATA ### Async callback to provide total number of objects available to this controller stored in the persistence layer. @method provideLength @param {Integer} length The total number of available objects @chainable ### provideLength: (length) -> set @, '_length', length set @, 'isRequestingLength', false @ ### Async callback to provide objects in a specific range. @method provideObjectsInRange @param {Object} [range] A range object @param {Integer} [range.start] The index at which objects should be inserted into the content array @param {Integer} [range.length] The number of items to replace with the updated data @param {Array} array The data to inject into the sparse array @chainable ### provideObjectsInRange: (range, array) -> sparseContent = get(@, 'sparseContent') sparseContent.replace(range.start, range.length, array) @ ### Async callback to provide an object at a specific index. Ultimately, this method calls `provideObjectsInRange`. Override `provideObjectsInRange` to inject custom behavior. @method provideObjectAtIndex @param {Integer} idx The index to insert data at @param {Object} obj The object to insert @chainable ### provideObjectAtIndex: (idx, obj) -> array = @_TMP_PROVIDE_ARRAY range = @_TMP_PROVIDE_RANGE array[0] = obj range.start = idx @provideObjectsInRange(range, array) # OVERRIDE ARRAY PROXY METHODS FOR CONTENT ### Hook for responding to impending updates to the content array. Override to add custom handling for array updates. @method contentArrayWillChange @param {Array} array The array instance being updated @param {Integer} idx The index where changes applied @param {Integer} removedCount @param {Integer} addedCount ### contentArrayWillChange: (array, idx, removedCount, addedCount) -> @ ### Hook for responding to updates to the content array. Override to add custom handling for array updates. @method contentArrayWillChange @param {Array} array The array instance being updated @param {Integer} idx The index where changes applied @param {Integer} removedCount @param {Integer} addedCount ### contentArrayDidChange: (array, idx, removedCount, addedCount) -> @ ### @private Override Ember's `_contentWillChange` to observe `_content`. @method _contentWillChange ### _contentWillChange: Ember.beforeObserver -> @_super() , '_content' ### @private Override Ember's `_contentDidChange` to observe `_content` and `content`. @method _contentDidChange ### _contentDidChange: Ember.observer -> @_super() , 'content', '_content' ### @private Move any array set to the `content` property to the `_content` property. This allows `content` to be used for referencing the sparse array while retaining a reference to the originally provided content object. @method _setupContent @return {Array} The sparse array ### _setupContent: -> controller = @ _content = get(controller, 'content') return if _content and _content.isSparseArray if _content _content.addArrayObserver(controller, willChange: "contentArrayWillChange" didChange: "contentArrayDidChange" ) sparseContent = Ember.A((_content and _content.slice()) ? []) sparseContent.isSparseArray = true set controller, '_content', _content set controller, 'sparseContent', sparseContent sparseContent ### @private Remove observers from `_content`. @method _teardownContent @return null ### _teardownContent: -> controller = @ _content = get(controller, '_content') if _content _content.removeArrayObserver(controller, willChange: "contentArrayWillChange" didChange: "contentArrayDidChange" ) null ### @private Set reported length to `content.total` if it changes. @method _contentTotalChanged @chainable ### _contentTotalChanged: Ember.observer -> set @, '_length', get(@, 'content.total') @ , 'content.total' # SPARSE CONTENT SETUP/EVENTS ### Hook for responding to the sparse array being replaced with a new array instance. Override to add custom handling. @method sparseContentWillChange @param {Object} self ### sparseContentWillChange: Ember.K ### Hook for responding to the sparse array being replaced with a new array instance. Override to add custom handling. @method sparseContentDidChange @param {Object} self ### sparseContentDidChange: Ember.K ### Hook for injecting custom behavior when an item in the sparse array gets replaced with new data. @method sparseContentDidChange @param {Object} item The previous value @param {Object} addedObject The new value ### didReplaceSparseArrayItem: Ember.K ### Hook for responding to impending updates to the sparse array. Extend to add custom handling for array updates. @method sparseContentArrayWillChange @param {Array} array The array instance being updated @param {Integer} idx The index where changes applied @param {Integer} removedCount @param {Integer} addedCount ### sparseContentArrayWillChange: (array, idx, removedCount, addedCount) -> @_PREVIOUS_SPARSE_CONTENT = array.slice(idx, idx + removedCount) @ ### Hook for responding to updates to the sparse array. Extend to add custom handling for array updates. @method sparseContentArrayDidChange @param {Array} array The array instance being updated @param {Integer} idx The index where changes applied @param {Integer} removedCount @param {Integer} addedCount ### sparseContentArrayDidChange: (array, idx, removedCount, addedCount) -> removedObjects = @_PREVIOUS_SPARSE_CONTENT ? Ember.A() addedObjects = array.slice(idx, idx + addedCount) # Calculate delta with length properties of actual arrays # More accurate than using addedCount and removedCount delta = (addedObjects?.length || 0) - (removedObjects?.length || 0) set(@, '_length', get(@, '_length') + delta) for item, i in removedObjects @didReplaceSparseArrayItem(item, addedObjects[i]) if item and item.isSparseArrayItem @_PREVIOUS_SPARSE_CONTENT = null @ ### @private Sparse array change handler. @method _sparseContentWillChange ### _sparseContentWillChange: Ember.beforeObserver -> sparseContent = get(@, 'sparseContent') len = if sparseContent then get(sparseContent, 'length') else 0 @sparseContentArrayWillChange @, 0, len, undefined @sparseContentWillChange @ @_teardownSparseContent sparseContent , 'sparseContent' ### @private Sparse array change handler. @method _sparseContentDidChange ### _sparseContentDidChange: Ember.observer -> sparseContent = get(@, 'sparseContent') len = if sparseContent then get(sparseContent, 'length') else 0 @_setupSparseContent sparseContent @sparseContentDidChange @ @sparseContentArrayDidChange @, 0, undefined, len , 'sparseContent' ### @private Remove change observing on sparse array. @method _teardownSparseContent ### _teardownSparseContent: -> @_clearSparseContent() sparseContent = get(@, 'sparseContent') if sparseContent sparseContent.removeArrayObserver @, willChange: 'sparseContentArrayWillChange', didChange: 'sparseContentArrayDidChange' ### @private Add change observing on sparse array. @method _setupSparseContent ### _setupSparseContent: -> sparseContent = get(@, 'sparseContent') if sparseContent sparseContent.addArrayObserver @, willChange: 'sparseContentArrayWillChange', didChange: 'sparseContentArrayDidChange' @_lengthDidChange() ### @private Set the sparse array's length to the controller's length. @method _lengthDidChange ### _lengthDidChange: Ember.observer -> length = get(@, 'length') ? 0 sparseContent = get(@, 'sparseContent') sparseContent.length = length if Ember.isArray(sparseContent) and sparseContent.isSparseArray and sparseContent.length isnt length , 'length' ### @private Empty the sparse array. @method _clearSparseContent ### _clearSparseContent: -> sparseContent = get(@, 'sparseContent') sparseContent.clear() if sparseContent and sparseContent.isSparseArray @ ### Called before controller destruction. @method willDestroy ### willDestroy: -> @_super() @_teardownSparseContent() # DATA FETCHING ### Hook for single object requests. Override this method to enable this controller to obtain a single persisted object. If the request is successful, insert the fetched object into the sparse array using the `provideObjectAtIndex` method. @method didRequestIndex @param {Integer} idx ### didRequestIndex: Ember.K ### Hook for range requests. Override this method to enable this controller to obtain a page of persisted data. If the request is successful, insert the fetched objects into the sparse array using the `provideObjectsInRange` method. @method didRequestRange @param {Object} [range] A range object @param {Integer} [range.start] The index to fetch @param {Integer} [range.length] The number of items to fetch ### didRequestRange: Ember.K ### Hook for initiating requests for the total number of objects available to this controller in the persistence layer. Override this method to enable this controller to obtain its length. If the request is successful, set the length of this sparse array controller using the `provideLength` method. @method didRequestLength ### didRequestLength: Ember.K ### @private Prevents the controller from continuously attempting to fetch data for objects that are already in the process of being fetched. @method _markSparseArrayItemInProgress @param {Integer} idx The index of the object to place into a loading state ### _markSparseArrayItemInProgress: (idx) -> sparseContent = get(@, 'sparseContent') return unless sparseContent and Ember.typeOf sparseContent is 'array' item = sparseContent[idx] set(item, 'isStale', false) if item and get(item, 'isStale') item ### @private Prepare to fetch a page of data from the persistence layer. @method _didRequestRange @param {Object} [range] A range object @param {Integer} [range.start] The index to fetch @param {Integer} [range.length] The number of items to fetch ### _didRequestRange: (range) -> @_markSparseArrayItemInProgress(idx) for idx in [range.start...(range.start + range.length)] @didRequestRange(range) ### @private Prepare to fetch a single object from the persistence layer. @method _didRequestIndex @param {Integer} idx ### _didRequestIndex: (idx) -> @_markSparseArrayItemInProgress(idx) @didRequestIndex(idx) ### @private Prepare to fetch the total number of available objects from the persistence layer. @method _didRequestLength ### _didRequestLength: -> @didRequestLength() `export default EmberellaSparseArray`