/** @module delite/Store */
define([
"dcl/dcl",
"requirejs-dplugins/has",
"decor/Invalidating",
"requirejs-dplugins/Promise!",
"decor/ObservableArray",
"decor/Observable",
"./ArrayToStoreAdapter",
"./DstoreToStoreAdapter"
], function (dcl, has, Invalidating, Promise, ObservableArray, Observable, ArrayToStoreAdapter, DstoreToStoreAdapter) {
/**
* Dispatched once the query has been executed and the `renderItems` array
* has been initialized with the list of initial render items.
* @example
* widget.on("query-success", function (evt) {
* console.log("query done, initial renderItems: " + evt.renderItems);
* });
* @event module:delite/Store#query-success
* @property {Object[]} renderItems - The array of initial render items.
* @property {boolean} cancelable - Indicates whether the event is cancelable or not.
* @property {boolean} bubbles - Indicates whether the given event bubbles up through the DOM or not.
*/
/**
* Mixin for store management that creates render items from store items after
* querying the store. The receiving class must extend decor/Evented or delite/Widget.
*
* Classes extending this mixin automatically create render items that are consumable
* from store items after querying the store. This happens each time the `store`, `query` or
* `queryOptions` properties are set. If that store is Trackable it will be observed and render items
* will be automatically updated, added or deleted based on store notifications.
*
* @mixin module:delite/Store
*/
return dcl(Invalidating, /** @lends module:delite/Store# */ {
/**
* The source that contains the items to display.
* @member {(dstore/Store|decor/ObservableArray|Array)}
* @default null
*/
source: null,
/**
* A query filter to apply to the store.
* @member {Object}
* @default {}
*/
query: {},
/**
* A function that processes the collection or the array returned by the source query and returns a new
* collection or a new array (to sort it, etc...). This processing is applied before potentially tracking
* the source for modifications (if Trackable or Observable).
* Be careful you can not use the same function for both arrays and collections.
* Changing this function on the instance will not automatically refresh the class.
* @default identity function
*/
processQueryResult: function (source) { return source; },
/**
* The render items corresponding to the store items for this widget. This is filled from the store and
* is not supposed to be modified directly. Initially null.
* @member {Object[]}
* @default null
*/
renderItems: null,
/**
* Creates a store item based from the widget internal item.
* @param {Object} renderItem - The render item.
* @returns {Object}
*/
renderItemToItem: function (renderItem) {
return renderItem;
},
/**
* Returns the widget internal item for a given store item. By default it returns the store
* item itself.
* @param {Object} item - The store item.
* @returns {Object}
* @protected
*/
itemToRenderItem: function (item) {
return item;
},
createdCallback: function () {
// Get the data from the textContent
if (this.textContent.trim()) {
var data = JSON.parse("[" + this.textContent + "]");
if (data.length) {
this.source = new ObservableArray();
for (var j = 0; j < data.length; j++) {
if (!data[j].id) {
data[j].id = Math.random();
}
this.source[j] = new Observable(data[j]);
}
}
this.textContent = "";
}
},
/**
* This method is called once the query has been executed to initialize the renderItems array
* with the list of initial render items.
*
* This method sets the renderItems property to the render items array passed as parameter. Once
* done, it fires a 'query-success' event.
* @param {Object[]} renderItems - The array of initial render items to be set in the renderItems property.
* @returns {Object[]} the renderItems array.
* @protected
* @fires module:delite/Store#query-success
*/
initItems: function (renderItems) {
this.renderItems = renderItems;
this.emit("query-success", { renderItems: renderItems, cancelable: false, bubbles: true });
return renderItems;
},
/**
* If the store parameters are invalidated, queries the store, creates the render items and calls initItems()
* when ready. If an error occurs a 'query-error' event will be fired.
* @param props
* @param isAfterCreation
* @protected
*/
computeProperties: function (props, isAfterCreation) {
// If this call is upon widget creation but `this.store` is not available, don't bother querying store
if (("source" in props || "query" in props) && (this.source || !isAfterCreation)) {
this.queryStoreAndInitItems(this.processQueryResult);
}
},
/**
* Queries the store, creates the render items and calls initItems() when ready. If an error occurs
* a 'query-error' event will be fired.
*
* This method is not supposed to be called by application developer.
* It will be called automatically when modifying the store related properties or by the subclass
* if needed.
* @param processQueryResult - A function that processes the collection returned by the store query
* and returns a new collection (to sort it, etc...)., applied before tracking.
* @returns {Promise} If store to be processed is not null a promise that will be resolved when the loading
* process will be finished.
* @protected
*/
queryStoreAndInitItems: function (processQueryResult) {
this._untrack();
if (this.source != null) {
if (!Array.isArray(this.source)) {
this._storeAdapter = new DstoreToStoreAdapter({source: this.source, query: this.query,
processQueryResult: processQueryResult});
} else {
this._storeAdapter = new ArrayToStoreAdapter({source: this.source, query: this.query,
processQueryResult: processQueryResult});
}
var collection = this._storeAdapter;
if (collection.track) {
this._addListener = collection.on("add", this._itemAdded.bind(this));
this._deleteListener = collection.on("delete", this._itemRemoved.bind(this));
this._updateListener = collection.on("update", this._itemUpdated.bind(this));
this._newQueryListener = collection.on("_new-query-asked", function (evt) {
this.emit("new-query-asked", evt);
}.bind(this));
}
return this.processCollection(collection);
} else {
this.initItems([]);
}
},
/**
* Synchronously deliver change records to all listeners registered via `observe()`.
*/
deliver: dcl.superCall(function (sup) {
return function () {
sup.call();
if (this._storeAdapter && typeof this._storeAdapter.deliver === "function") {
this._storeAdapter.deliver();
}
};
}),
/**
* Discard change records for all listeners registered via `observe()`.
*/
discardChanges: dcl.superCall(function (sup) {
return function () {
sup.call();
if (this._storeAdapter && typeof this._storeAdapter.discardChanges === "function") {
this._storeAdapter.discardChanges();
}
};
}),
/**
* Called to process the items returned after querying the store.
* @param {dstore/Collection} collection - Items to be displayed.
* @protected
*/
processCollection: function (collection) {
return this.fetch(collection).then(function (items) {
return this.initItems(items.map(this.itemToRenderItem.bind(this)));
}.bind(this), this._queryError.bind(this));
},
/**
* Called to perform the fetch operation on the collection.
* @param {dstore/Collection} collection - Items to be displayed.
* @protected
*/
fetch: function (collection) {
return collection.fetch();
},
_queryError: function (error) {
console.log(error);
this.emit("query-error", { error: error, cancelable: false, bubbles: true });
},
_untrack: function () {
if (this._storeAdapter) {
this._storeAdapter.untrack();
}
if (this._addListener) {
this._addListener.remove(this._addListener);
}
if (this._deleteListener) {
this._deleteListener.remove(this._deleteListener);
}
if (this._updateListener) {
this._updateListener.remove(this._updateListener);
}
if (this._newQueryListener) {
this._newQueryListener.remove(this._newQueryListener);
}
},
detachedCallback: function () {
this._untrack();
},
destroy: function () {
this._untrack();
},
/**
* This method is called when an item is removed from an observable store. The default
* implementation actually removes a renderItem from the renderItems array. This can be redefined but
* must not be called directly.
* @param {number} index - The index of the render item to remove.
* @param {Object[]} renderItems - The array of render items to remove the render item from.
* @protected
*/
itemRemoved: function (index, renderItems) {
renderItems.splice(index, 1);
},
/**
* This method is called when an item is added in an observable store. The default
* implementation actually adds the renderItem to the renderItems array. This can be redefined but
* must not be called directly.
* @param {number} index - The index where to add the render item.
* @param {Object} renderItem - The render item to be added.
* @param {Object[]} renderItems - The array of render items to add the render item to.
* @protected
*/
itemAdded: function (index, renderItem, renderItems) {
renderItems.splice(index, 0, renderItem);
},
/**
* This method is called when an item is updated in an observable store. The default
* implementation actually updates the renderItem in the renderItems array. This can be redefined but
* must not be called directly.
* @param {number} index - The index of the render item to update.
* @param {Object} renderItem - The render item data the render item must be updated with.
* @param {Object[]} renderItems - The array of render items to render item to be updated is part of.
* @protected
*/
itemUpdated: function (index, renderItem, renderItems) {
// we want to keep the same item object and mixin new values into old object
dcl.mix(renderItems[index], renderItem);
},
/**
* This method is called when an item is moved in an observable store. The default
* implementation actually moves the renderItem in the renderItems array. This can be redefined but
* must not be called directly.
* @param {number} previousIndex - The previous index of the render item.
* @param {number} newIndex - The new index of the render item.
* @param {Object} renderItem - The render item to be moved.
* @param {Object[]} renderItems - The array of render items to render item to be moved is part of.
* @protected
*/
itemMoved: function (previousIndex, newIndex, renderItem, renderItems) {
// we want to keep the same item object and mixin new values into old object
this.itemRemoved(previousIndex, renderItems);
this.itemAdded(newIndex, renderItem, renderItems);
},
_refreshHandler: function () {
this.queryStoreAndInitItems(this.processQueryResult);
},
/**
* When the store is observed and an item is removed in the store this method is called to remove the
* corresponding render item. This can be redefined but must not be called directly.
* @param {Event} event - The "remove" `dstore/Trackable` event.
* @private
*/
_itemRemoved: function (event) {
if (event.previousIndex !== undefined) {
this.itemRemoved(event.previousIndex, this.renderItems);
// the change of the value of the renderItems property (splice of the array)
// does not automatically trigger a notification. Hence:
this.notifyCurrentValue("renderItems");
}
// if no previousIndex the items is removed outside of the range we monitor so we don't care
},
/**
* When the store is observed and an item is updated in the store this method is called to update the
* corresponding render item. This can be redefined but must not be called directly.
* @param {Event} event - The "update" `dstore/Trackable` event.
* @private
*/
_itemUpdated: function (event) {
if (event.index === undefined) {
// this is actually a remove
this.itemRemoved(event.previousIndex, this.renderItems);
} else if (event.previousIndex === undefined) {
// this is actually a add
this.itemAdded(event.index, this.itemToRenderItem(event.target), this.renderItems);
} else if (event.index !== event.previousIndex) {
// this is a move
this.itemMoved(event.previousIndex, event.index, this.itemToRenderItem(event.target), this.renderItems);
} else {
// we want to keep the same item object and mixin new values into old object
this.itemUpdated(event.index, this.itemToRenderItem(event.target), this.renderItems);
}
// the change of the value of the renderItems property (splice of the array)
// does not automatically trigger a notification. Hence:
this.notifyCurrentValue("renderItems");
},
/**
* When the store is observed and an item is added in the store this method is called to add the
* corresponding render item. This can be redefined but must not be called directly.
* @param {Event} event - The "add" `dstore/Trackable` event.
* @private
*/
_itemAdded: function (event) {
if (event.index !== undefined) {
this.itemAdded(event.index, this.itemToRenderItem(event.target), this.renderItems);
// the change of the value of the renderItems property (splice of the array)
// does not automatically trigger a notification. Hence:
this.notifyCurrentValue("renderItems");
}
// if no index the item is added outside of the range we monitor so we don't care
},
/**
* Return the identity of an item.
* @param {Object} item - The item
* @returns {Object}
* @protected
*/
getIdentity: function (item) {
return this._storeAdapter.getIdentity(item);
}
});
});