_StoreMixin.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. define(["dojo/_base/kernel", "dojo/_base/declare", "dojo/_base/lang", "dojo/_base/Deferred", "dojo/on", "dojo/aspect", "put-selector/put"],
  2. function(kernel, declare, lang, Deferred, listen, aspect, put){
  3. // This module isolates the base logic required by store-aware list/grid
  4. // components, e.g. OnDemandList/Grid and the Pagination extension.
  5. // Noop function, needed for _trackError when callback due to a bug in 1.8
  6. // (see http://bugs.dojotoolkit.org/ticket/16667)
  7. function noop(value){ return value; }
  8. function emitError(err){
  9. // called by _trackError in context of list/grid, if an error is encountered
  10. if(typeof err !== "object"){
  11. // Ensure we actually have an error object, so we can attach a reference.
  12. err = new Error(err);
  13. }else if(err.dojoType === "cancel"){
  14. // Don't fire dgrid-error events for errors due to canceled requests
  15. // (unfortunately, the Deferred instrumentation will still log them)
  16. return;
  17. }
  18. // TODO: remove this @ 0.4 (prefer grid property directly on event object)
  19. err.grid = this;
  20. if(listen.emit(this.domNode, "dgrid-error", {
  21. grid: this,
  22. error: err,
  23. cancelable: true,
  24. bubbles: true })){
  25. console.error(err);
  26. }
  27. }
  28. return declare(null, {
  29. // store: Object
  30. // The object store (implementing the dojo/store API) from which data is
  31. // to be fetched.
  32. store: null,
  33. // query: Object
  34. // Specifies query parameter(s) to pass to store.query calls.
  35. query: null,
  36. // queryOptions: Object
  37. // Specifies additional query options to mix in when calling store.query;
  38. // sort, start, and count are already handled.
  39. queryOptions: null,
  40. // getBeforePut: boolean
  41. // If true, a get request will be performed to the store before each put
  42. // as a baseline when saving; otherwise, existing row data will be used.
  43. getBeforePut: true,
  44. // noDataMessage: String
  45. // Message to be displayed when no results exist for a query, whether at
  46. // the time of the initial query or upon subsequent observed changes.
  47. // Defined by _StoreMixin, but to be implemented by subclasses.
  48. noDataMessage: "",
  49. // loadingMessage: String
  50. // Message displayed when data is loading.
  51. // Defined by _StoreMixin, but to be implemented by subclasses.
  52. loadingMessage: "",
  53. constructor: function(){
  54. // Create empty objects on each instance, not the prototype
  55. this.query = {};
  56. this.queryOptions = {};
  57. this.dirty = {};
  58. this._updating = {}; // Tracks rows that are mid-update
  59. this._columnsWithSet = {};
  60. // Reset _columnsWithSet whenever column configuration is reset
  61. aspect.before(this, "configStructure", lang.hitch(this, function(){
  62. this._columnsWithSet = {};
  63. }));
  64. },
  65. postCreate: function(){
  66. this.inherited(arguments);
  67. if(this.store){
  68. this._updateNotifyHandle(this.store);
  69. }
  70. },
  71. destroy: function(){
  72. this.inherited(arguments);
  73. if(this._notifyHandle){
  74. this._notifyHandle.remove();
  75. }
  76. },
  77. _configColumn: function(column){
  78. // summary:
  79. // Implements extension point provided by Grid to store references to
  80. // any columns with `set` methods, for use during `save`.
  81. if (column.set){
  82. this._columnsWithSet[column.field] = column;
  83. }
  84. },
  85. _updateNotifyHandle: function(store){
  86. // summary:
  87. // Unhooks any previously-existing store.notify handle, and
  88. // hooks up a new one for the given store.
  89. if(this._notifyHandle){
  90. // Unhook notify handler from previous store
  91. this._notifyHandle.remove();
  92. delete this._notifyHandle;
  93. }
  94. if(store && typeof store.notify === "function"){
  95. this._notifyHandle = aspect.after(store, "notify",
  96. lang.hitch(this, "_onNotify"), true);
  97. }
  98. },
  99. _setStore: function(store, query, queryOptions){
  100. // summary:
  101. // Assigns a new store (and optionally query/queryOptions) to the list,
  102. // and tells it to refresh.
  103. this._updateNotifyHandle(store);
  104. this.store = store;
  105. this.dirty = {}; // discard dirty map, as it applied to a previous store
  106. this.set("query", query, queryOptions);
  107. },
  108. _setQuery: function(query, queryOptions){
  109. // summary:
  110. // Assigns a new query (and optionally queryOptions) to the list,
  111. // and tells it to refresh.
  112. var sort = queryOptions && queryOptions.sort;
  113. this.query = query !== undefined ? query : this.query;
  114. this.queryOptions = queryOptions || this.queryOptions;
  115. // If we have new sort criteria, pass them through sort
  116. // (which will update _sort and call refresh in itself).
  117. // Otherwise, just refresh.
  118. sort ? this.set("sort", sort) : this.refresh();
  119. },
  120. setStore: function(store, query, queryOptions){
  121. kernel.deprecated("setStore(...)", 'use set("store", ...) instead', "dgrid 0.4");
  122. this.set("store", store, query, queryOptions);
  123. },
  124. setQuery: function(query, queryOptions){
  125. kernel.deprecated("setQuery(...)", 'use set("query", ...) instead', "dgrid 0.4");
  126. this.set("query", query, queryOptions);
  127. },
  128. _getQueryOptions: function(){
  129. // summary:
  130. // Get a fresh queryOptions object, also including the current sort
  131. var options = lang.delegate(this.queryOptions, {});
  132. if(this._sort.length){
  133. // Prevents SimpleQueryEngine from doing unnecessary "null" sorts (which can
  134. // change the ordering in browsers that don't use a stable sort algorithm, eg Chrome)
  135. options.sort = this._sort;
  136. }
  137. return options;
  138. },
  139. _getQuery: function(){
  140. // summary:
  141. // Implemented consistent with _getQueryOptions so that if query is
  142. // an object, this returns a protected (delegated) object instead of
  143. // the original.
  144. var q = this.query;
  145. return typeof q == "object" && q != null ? lang.delegate(q, {}) : q;
  146. },
  147. _setSort: function(property, descending){
  148. // summary:
  149. // Sort the content
  150. // prevent default storeless sort logic as long as we have a store
  151. if(this.store){ this._lastCollection = null; }
  152. this.inherited(arguments);
  153. },
  154. _onNotify: function(object, existingId){
  155. // summary:
  156. // Method called when the store's notify method is called.
  157. // Call inherited in case anything was mixed in earlier
  158. this.inherited(arguments);
  159. // For adds/puts, check whether any observers are hooked up;
  160. // if not, force a refresh to properly hook one up now that there is data
  161. if(object && this._numObservers < 1){
  162. this.refresh({ keepScrollPosition: true });
  163. }
  164. },
  165. insertRow: function(object, parent, beforeNode, i, options){
  166. var store = this.store,
  167. dirty = this.dirty,
  168. id = store && store.getIdentity(object),
  169. dirtyObj;
  170. if(id in dirty && !(id in this._updating)){ dirtyObj = dirty[id]; }
  171. if(dirtyObj){
  172. // restore dirty object as delegate on top of original object,
  173. // to provide protection for subsequent changes as well
  174. object = lang.delegate(object, dirtyObj);
  175. }
  176. return this.inherited(arguments);
  177. },
  178. updateDirty: function(id, field, value){
  179. // summary:
  180. // Updates dirty data of a field for the item with the specified ID.
  181. var dirty = this.dirty,
  182. dirtyObj = dirty[id];
  183. if(!dirtyObj){
  184. dirtyObj = dirty[id] = {};
  185. }
  186. dirtyObj[field] = value;
  187. },
  188. setDirty: function(id, field, value){
  189. kernel.deprecated("setDirty(...)", "use updateDirty() instead", "dgrid 0.4");
  190. this.updateDirty(id, field, value);
  191. },
  192. save: function() {
  193. // Keep track of the store and puts
  194. var self = this,
  195. store = this.store,
  196. dirty = this.dirty,
  197. dfd = new Deferred(), promise = dfd.promise,
  198. getFunc = function(id){
  199. // returns a function to pass as a step in the promise chain,
  200. // with the id variable closured
  201. var data;
  202. return (self.getBeforePut || !(data = self.row(id).data)) ?
  203. function(){ return store.get(id); } :
  204. function(){ return data; };
  205. };
  206. // function called within loop to generate a function for putting an item
  207. function putter(id, dirtyObj) {
  208. // Return a function handler
  209. return function(object) {
  210. var colsWithSet = self._columnsWithSet,
  211. updating = self._updating,
  212. key, data;
  213. if (typeof object.set === "function") {
  214. object.set(dirtyObj);
  215. } else {
  216. // Copy dirty props to the original, applying setters if applicable
  217. for(key in dirtyObj){
  218. object[key] = dirtyObj[key];
  219. }
  220. }
  221. // Apply any set methods in column definitions.
  222. // Note that while in the most common cases column.set is intended
  223. // to return transformed data for the key in question, it is also
  224. // possible to directly modify the object to be saved.
  225. for(key in colsWithSet){
  226. data = colsWithSet[key].set(object);
  227. if(data !== undefined){ object[key] = data; }
  228. }
  229. updating[id] = true;
  230. // Put it in the store, returning the result/promise
  231. return Deferred.when(store.put(object), function() {
  232. // Clear the item now that it's been confirmed updated
  233. delete dirty[id];
  234. delete updating[id];
  235. });
  236. };
  237. }
  238. // For every dirty item, grab the ID
  239. for(var id in dirty) {
  240. // Create put function to handle the saving of the the item
  241. var put = putter(id, dirty[id]);
  242. // Add this item onto the promise chain,
  243. // getting the item from the store first if desired.
  244. promise = promise.then(getFunc(id)).then(put);
  245. }
  246. // Kick off and return the promise representing all applicable get/put ops.
  247. // If the success callback is fired, all operations succeeded; otherwise,
  248. // save will stop at the first error it encounters.
  249. dfd.resolve();
  250. return promise;
  251. },
  252. revert: function(){
  253. // summary:
  254. // Reverts any changes since the previous save.
  255. this.dirty = {};
  256. this.refresh();
  257. },
  258. _trackError: function(func){
  259. // summary:
  260. // Utility function to handle emitting of error events.
  261. // func: Function|String
  262. // A function which performs some store operation, or a String identifying
  263. // a function to be invoked (sans arguments) hitched against the instance.
  264. // If sync, it can return a value, but may throw an error on failure.
  265. // If async, it should return a promise, which would fire the error
  266. // callback on failure.
  267. // tags:
  268. // protected
  269. var result;
  270. if(typeof func == "string"){ func = lang.hitch(this, func); }
  271. try{
  272. result = func();
  273. }catch(err){
  274. // report sync error
  275. emitError.call(this, err);
  276. }
  277. // wrap in when call to handle reporting of potential async error
  278. return Deferred.when(result, noop, lang.hitch(this, emitError));
  279. },
  280. newRow: function(){
  281. // Override to remove no data message when a new row appears.
  282. // Run inherited logic first to prevent confusion due to noDataNode
  283. // no longer being present as a sibling.
  284. var row = this.inherited(arguments);
  285. if(this.noDataNode){
  286. put(this.noDataNode, "!");
  287. delete this.noDataNode;
  288. }
  289. return row;
  290. },
  291. removeRow: function(rowElement, justCleanup){
  292. var row = {element: rowElement};
  293. // Check to see if we are now empty...
  294. if(!justCleanup && this.noDataMessage &&
  295. (this.up(row).element === rowElement) &&
  296. (this.down(row).element === rowElement)){
  297. // ...we are empty, so show the no data message.
  298. this.noDataNode = put(this.contentNode, "div.dgrid-no-data");
  299. this.noDataNode.innerHTML = this.noDataMessage;
  300. }
  301. return this.inherited(arguments);
  302. }
  303. });
  304. });