diff --git a/EditModelRefController.js b/EditModelRefController.js index e2a8aae..11fe6f4 100644 --- a/EditModelRefController.js +++ b/EditModelRefController.js @@ -1,30 +1,9 @@ define([ "dojo/_base/declare", - "dojo/_base/lang", - "./getPlainValue", - "./getStateful", - "./ModelRefController" -], function(declare, lang, getPlainValue, getStateful, ModelRefController){ - // module: - // dojox/mvc/EditModelRefController - - function setRefSourceModel(/*dojox/mvc/EditModelRefController*/ ctrl, /*Anything*/ old, /*Anything*/ current){ - // summary: - // A function called when this controller gets newer value as the data source. - // ctrl: dojox/mvc/EditModelRefController - // The controller. - // old: Anything - // The older value. - // current: Anything - // The newer value. - - if(old !== current){ - ctrl.set(ctrl._refOriginalModelProp, ctrl.holdModelUntilCommit ? current : ctrl.cloneModel(current)); - ctrl.set(ctrl._refEditModelProp, ctrl.holdModelUntilCommit ? ctrl.cloneModel(current) : current); - } - } - - return declare("dojox.mvc.EditModelRefController", ModelRefController, { + "./ModelRefController", + "./EditModelRefControllerMixin" +], function(declare, ModelRefController, EditModelRefControllerMixin){ + return declare("dojox.mvc.EditModelRefController", [ModelRefController, EditModelRefControllerMixin], { // summary: // A child class of dojox/mvc/ModelRefController. // Keeps a copy (originalModel) of given data model (sourceModel) so that it can manage the data model of before/after the edit. @@ -107,98 +86,5 @@ define([ // | data-dojo-props="checked: at('widget:ctrlEdit', 'value')"> // | // | - - // getStatefulOptions: dojox/mvc/getStatefulOptions - // The options to get stateful object from plain value. - getStatefulOptions: null, - - // getPlainValueOptions: dojox/mvc/getPlainValueOptions - // The options to get plain value from stateful object. - getPlainValueOptions: null, - - // holdModelUntilCommit: Boolean - // True not to send the change in model back to sourceModel until commit() is called. - holdModelUntilCommit: false, - - // originalModel: dojo/Stateful - // The data model, that serves as the original data. - originalModel: null, - - // originalModel: dojo/Stateful - // The data model, that serves as the data source. - sourceModel: null, - - // _refOriginalModelProp: String - // The property name for the data model, that serves as the original data. - _refOriginalModelProp: "originalModel", - - // _refSourceModelProp: String - // The property name for the data model, that serves as the data source. - _refSourceModelProp: "sourceModel", - - // _refEditModelProp: String - // The property name for the data model, that is being edited. - _refEditModelProp: "model", - - postscript: function(/*Object?*/ params, /*DomNode|String?*/ srcNodeRef){ - // summary: - // Sets certain properties before setting models. - - for(var s in {getStatefulOptions: 1, getPlainValueOptions: 1, holdModelUntilCommit: 1}){ - var value = (params || {})[s]; - if(typeof value != "undefined"){ - this[s] = value; - } - } - this.inherited(arguments); - }, - - set: function(/*String*/ name, /*Anything*/ value){ - // summary: - // Set a property to this. - // name: String - // The property to set. - // value: Anything - // The value to set in the property. - - if(name == this._refSourceModelProp){ - setRefSourceModel(this, this[this._refSourceModelProp], value); - } - this.inherited(arguments); - }, - - cloneModel: function(/*Anything*/ value){ - // summary: - // Create a clone object of the data source. - // Child classes of this controller can override it to achieve its specific needs. - // value: Anything - // The data serving as the data source. - - var plain = lang.isFunction((value || {}).set) && lang.isFunction((value || {}).watch) ? getPlainValue(value, this.getPlainValueOptions) : value; - return getStateful(plain, this.getStatefulOptions); - }, - - commit: function(){ - // summary: - // Send the change back to the data source. - - this.set(this.holdModelUntilCommit ? this._refSourceModelProp : this._refOriginalModelProp, this.cloneModel(this.get(this._refEditModelProp))); - }, - - reset: function(){ - // summary: - // Change the model back to its original state. - - this.set(this.holdModelUntilCommit ? this._refEditModelProp : this._refSourceModelProp, this.cloneModel(this.get(this._refOriginalModelProp))); - }, - - hasControllerProperty: function(/*String*/ name){ - // summary: - // Returns true if this controller itself owns the given property. - // name: String - // The property name. - - return this.inherited(arguments) || name == this._refOriginalModelProp || name == this._refSourceModelProp; - } }); -}); +}); \ No newline at end of file diff --git a/EditModelRefControllerMixin.js b/EditModelRefControllerMixin.js new file mode 100644 index 0000000..554061b --- /dev/null +++ b/EditModelRefControllerMixin.js @@ -0,0 +1,202 @@ +define([ + "dojo/_base/declare", + "dojo/_base/lang", + "./getPlainValue", + "./getStateful" +], function(declare, lang, getPlainValue, getStateful){ + // module: + // dojox/mvc/EditModelRefControllerMixin + + function setRefSourceModel(/*dojox/mvc/EditModelRefControllerMixin*/ ctrl, /*Anything*/ old, /*Anything*/ current){ + // summary: + // A function called when this controller gets newer value as the data source. + // ctrl: dojox/mvc/EditModelRefControllerMixin + // The controller. + // old: Anything + // The older value. + // current: Anything + // The newer value. + + if(old !== current){ + ctrl.set(ctrl._refOriginalModelProp, ctrl.holdModelUntilCommit ? current : ctrl.cloneModel(current)); + ctrl.set(ctrl._refEditModelProp, ctrl.holdModelUntilCommit ? ctrl.cloneModel(current) : current); + } + } + + return declare("dojox.mvc.EditModelRefControllerMixin", null, { + // summary: + // A mixin class to dojox/mvc/ModelRefController. + // Keeps a copy (originalModel) of given data model (sourceModel) so that it can manage the data model of before/after the edit. + // description: + // Has two modes: + // + // - Directly reflect the edits to sourceModel (holdModelUntilCommit=false) + // - Don't reflect the edits to sourceModel, until commit() is called (holdModelUntilCommit=true) + // + // For the 1st case, dojo/Stateful get()/set()/watch() interfaces will work with sourceModel. + // For the 2nd case, dojo/Stateful get()/set()/watch() interfaces will work with a copy of sourceModel, and sourceModel will be replaced with such copy when commit() is called. + // example: + // The check box refers to "value" property in the controller (with "ctrl" ID). + // The controller provides the "value" property on behalf of the model ("model" property in the controller, which comes from "sourceModel" property). + // Two seconds later, the check box changes from unchecked to checked, and the controller saves the state. + // Two seconds later then, the check box changes from checked to unchecked. + // Two seconds later then, the controller goes back to the last saved state, and the check box changes from unchecked to checked as the result. + // | + // | + // | + // | + // | + // | + // | + // | + // | + // | + // | + // example: + // The controller with "ctrlSource" ID specifies holding changes until commit() is called (by setting true to holdModelUntilCommit). + // As the change in the second check box is committed two seconds later from the change, the first check box is checked at then (when the change is committed). + // | + // | + // | + // | + // | + // | + // | + // | + // | + // | Source: + // | + // | Edit: + // | + // | + // | + + // getStatefulOptions: dojox/mvc/getStatefulOptions + // The options to get stateful object from plain value. + getStatefulOptions: null, + + // getPlainValueOptions: dojox/mvc/getPlainValueOptions + // The options to get plain value from stateful object. + getPlainValueOptions: null, + + // holdModelUntilCommit: Boolean + // True not to send the change in model back to sourceModel until commit() is called. + holdModelUntilCommit: false, + + // originalModel: dojo/Stateful + // The data model, that serves as the original data. + originalModel: null, + + // originalModel: dojo/Stateful + // The data model, that serves as the data source. + sourceModel: null, + + // _refOriginalModelProp: String + // The property name for the data model, that serves as the original data. + _refOriginalModelProp: "originalModel", + + // _refSourceModelProp: String + // The property name for the data model, that serves as the data source. + _refSourceModelProp: "sourceModel", + + // _refEditModelProp: String + // The property name for the data model, that is being edited. + _refEditModelProp: "model", + + postscript: function(/*Object?*/ params, /*DomNode|String?*/ srcNodeRef){ + // summary: + // Sets certain properties before setting models. + + for(var s in {getStatefulOptions: 1, getPlainValueOptions: 1, holdModelUntilCommit: 1}){ + var value = (params || {})[s]; + if(typeof value != "undefined"){ + this[s] = value; + } + } + this.inherited(arguments); + }, + + set: function(/*String*/ name, /*Anything*/ value){ + // summary: + // Set a property to this. + // name: String + // The property to set. + // value: Anything + // The value to set in the property. + + if(name == this._refSourceModelProp){ + setRefSourceModel(this, this[this._refSourceModelProp], value); + } + this.inherited(arguments); + }, + + cloneModel: function(/*Anything*/ value){ + // summary: + // Create a clone object of the data source. + // Child classes of this controller can override it to achieve its specific needs. + // value: Anything + // The data serving as the data source. + + var plain = lang.isFunction((value || {}).set) && lang.isFunction((value || {}).watch) ? getPlainValue(value, this.getPlainValueOptions) : value; + return getStateful(plain, this.getStatefulOptions); + }, + + commit: function(){ + // summary: + // Send the change back to the data source. + + this.set(this.holdModelUntilCommit ? this._refSourceModelProp : this._refOriginalModelProp, this.cloneModel(this.get(this._refEditModelProp))); + this.inherited(arguments); + }, + + reset: function(){ + // summary: + // Change the model back to its original state. + + this.set(this.holdModelUntilCommit ? this._refEditModelProp : this._refSourceModelProp, this.cloneModel(this.get(this._refOriginalModelProp))); + this.inherited(arguments); + }, + + hasControllerProperty: function(/*String*/ name){ + // summary: + // Returns true if this controller itself owns the given property. + // name: String + // The property name. + + return this.inherited(arguments) || name == this._refOriginalModelProp || name == this._refSourceModelProp; + } + }); +}); diff --git a/EditStoreRefController.js b/EditStoreRefController.js index c9e7b6a..63ca06b 100644 --- a/EditStoreRefController.js +++ b/EditStoreRefController.js @@ -1,12 +1,10 @@ define([ "dojo/_base/declare", - "dojo/_base/lang", - "dojo/when", - "./getPlainValue", - "./EditModelRefController", - "./StoreRefController" -], function(declare, lang, when, getPlainValue, EditModelRefController, StoreRefController){ - return declare("dojox.mvc.EditStoreRefController", [StoreRefController, EditModelRefController], { + "./ModelRefController", + "./EditModelRefControllerMixin", + "./StoreRefControllerMixin" +], function(declare, ModelRefController, EditModelRefControllerMixin, StoreRefControllerMixin){ + return declare("dojox.mvc.EditStoreRefController", [ModelRefController, EditModelRefControllerMixin, StoreRefControllerMixin], { // summary: // A child class of dojox/mvc/StoreRefController, managing edits. // description: @@ -46,97 +44,5 @@ define([ // | // | // | - - // getPlainValueOptions: dojox/mvc/getPlainValueOptions - // The options to get plain value from stateful object. - getPlainValueOptions: null, - - // _removals: Object[] - // The list of removed elements. - _removals: [], - - // _resultsWatchHandle: dojox/mvc/StatefulArray.watchElements.handle - // The watch handle for model array elements. - _resultsWatchHandle: null, - - // _refSourceModelProp: String - // The property name for the data model, that serves as the data source. - _refSourceModelProp: "sourceModel", - - queryStore: function(/*Object*/ query, /*dojo/store/api/Store.QueryOptions?*/ options){ - // summary: - // Queries the store for objects. - // query: Object - // The query to use for retrieving objects from the store. - // options: dojo/store/api/Store.QueryOptions? - // The optional arguments to apply to the resultset. - // returns: dojo/store/api/Store.QueryResults - // The results of the query, extended with iterative methods. - - if(!(this.store || {}).query){ return; } - if(this._resultsWatchHandle){ this._resultsWatchHandle.unwatch(); } - this._removals = []; - var _self = this; - return when(this.inherited(arguments), function(results){ - if(_self._beingDestroyed){ return; } - if(lang.isArray(results)){ - _self._resultsWatchHandle = results.watchElements(function(idx, removals, adds){ - [].push.apply(_self._removals, removals); - }); - } - return results; - }); - }, - - getStore: function(/*Number*/ id, /*Object*/ options){ - // summary: - // Retrieves an object by its identity. - // id: Number - // The identity to use to lookup the object. - // options: Object - // The options for dojo/store/*/get(). - // returns: Object - // The object in the store that matches the given id. - - if(this._resultsWatchHandle){ this._resultsWatchHandle.unwatch(); } - return this.inherited(arguments); - }, - - commit: function(){ - // summary: - // Send the change back to the data source. - - if(this._removals){ - for(var i = 0; i < this._removals.length; i++){ - this.store.remove(this.store.getIdentity(this._removals[i])); - } - this._removals = []; - } - var data = getPlainValue(this.get(this._refEditModelProp), this.getPlainValueOptions); - if(lang.isArray(data)){ - for(var i = 0; i < data.length; i++){ - this.store.put(data[i]); - } - }else{ - this.store.put(data); - } - this.inherited(arguments); - }, - - reset: function(){ - // summary: - // Change the model back to its original state. - - this.inherited(arguments); - this._removals = []; - }, - - destroy: function(){ - // summary: - // Clean up model watch handle as this object is destroyed. - - if(this._resultsWatchHandle){ this._resultsWatchHandle.unwatch(); } - this.inherited(arguments); - } }); }); diff --git a/EditStoreRefListController.js b/EditStoreRefListController.js index ae98083..1f6867f 100644 --- a/EditStoreRefListController.js +++ b/EditStoreRefListController.js @@ -1,11 +1,11 @@ define([ "dojo/_base/declare", - "dojo/_base/lang", - "./getPlainValue", - "./EditStoreRefController", - "./ListController" -], function(declare, lang, getPlainValue, EditStoreRefController, ListController){ - return declare("dojox.mvc.EditStoreRefListController", [EditStoreRefController, ListController], { + "./ModelRefController", + "./EditModelRefControllerMixin", + "./StoreRefControllerMixin", + "./ListControllerMixin" +], function(declare, ModelRefController, EditModelRefControllerMixin, StoreRefControllerMixin, ListControllerMixin){ + return declare("dojox.mvc.EditStoreRefListController", [ModelRefController, EditModelRefControllerMixin, StoreRefControllerMixin, ListControllerMixin], { // summary: // A child class of dojox/mvc/EditStoreRefController, mixed with ListController. // description: @@ -15,7 +15,7 @@ define([ // The check box refers to "value" property in the controller (with "ctrl" ID). // The controller provides the "value" property, from the data coming from data store ("store" property in the controller), using the first one in array. // Two seconds later, the check box changes from unchecked to checked. - // The change is committed to the data store, which is reflected to dojo/store/Observable callback. + // The change is committed to the data store, which is reflected to dojo/store/Observable callback. // | // | // | @@ -40,25 +40,10 @@ define([ // | // | // | - // | // | // | // | - - commitCurrent: function(){ - // summary: - // Send the change back to the data source for the current index. - - var id = this.cursor[this.idProperty]; - for(var i = 0; i < this.originalModel.length; i++){ - if(this.originalModel[i][this.idProperty] == id){ - this.originalModel.set(i, this.cloneModel(this.cursor)); - break; - } - } - this.store.put(this.cursor); - } - }); }); diff --git a/ListController.js b/ListController.js index 27edae1..704e32b 100644 --- a/ListController.js +++ b/ListController.js @@ -1,92 +1,9 @@ define([ - "dojo/_base/array", - "dojo/_base/lang", "dojo/_base/declare", - "./ModelRefController" -], function(array, lang, declare, ModelRefController){ - function unwatchHandles(/*dojox/mvc/ListController*/ c){ - // summary: - // Unwatch model watch handles. - - for(var s in {"_listModelWatchHandle": 1, "_tableModelWatchHandle": 1}){ - if(c[s]){ - c[s].unwatch(); - c[s] = null; - } - } - } - - function setRefInModel(/*dojox/mvc/ListController*/ ctrl, /*dojo/Stateful*/ old, /*dojo/Stateful*/ current){ - // summary: - // A function called when this controller gets newer value as the list data. - - unwatchHandles(ctrl); - if(current && old !== current){ - if(current.watchElements){ - ctrl._listModelWatchHandle = current.watchElements(function(idx, removals, adds){ - if(removals && adds){ - var curIdx = ctrl.get("cursorIndex"); - // If selected element is removed, make "no selection" state - if(removals && curIdx >= idx && curIdx < idx + removals.length){ - ctrl.set("cursorIndex", -1); - return; - } - // If selected element is equal to or larger than the removals/adds point, update the selected index - if((removals.length || adds.length) && curIdx >= idx){ - ctrl.set(ctrl._refCursorProp, ctrl.get("cursor")); - } - }else{ - // If there is a update to the whole array, update the selected index - ctrl.set(ctrl._refCursorProp, ctrl.get(ctrl._refCursorProp)); - } - }); - }else if(current.set && current.watch){ - if(ctrl.get("cursorIndex") < 0){ ctrl._set("cursorIndex", ""); } - ctrl._tableModelWatchHandle = current.watch(function(name, old, current){ - if(old !== current && name == ctrl.get("cursorIndex")){ - ctrl.set(ctrl._refCursorProp, current); - } - }); - } - } - ctrl._setCursorIndexAttr(ctrl.cursorIndex); - } - - function setRefCursor(/*dojox/mvc/ListController*/ ctrl, /*dojo/Stateful*/ old, /*dojo/Stateful*/ current){ - // summary: - // A function called when this controller gets newer value as the data of current selection. - // description: - // Finds the index associated with the given element, and updates cursorIndex property. - - var model = ctrl[ctrl._refInModelProp]; - if(!model){ return; } - if(old !== current){ - if(lang.isArray(model)){ - var foundIdx = array.indexOf(model, current); - if(foundIdx < 0){ - var targetIdx = ctrl.get("cursorIndex"); - if(targetIdx >= 0 && targetIdx < model.length){ - model.set(targetIdx, current); - } - }else{ - ctrl.set("cursorIndex", foundIdx); - } - }else{ - for(var s in model){ - if(model[s] == current){ - ctrl.set("cursorIndex", s); - return; - } - } - var targetIdx = ctrl.get("cursorIndex"); - if(targetIdx){ - model.set(targetIdx, current); - } - } - } - } - - return declare("dojox.mvc.ListController", ModelRefController, { + "./ModelRefController", + "./ListControllerMixin" +], function(declare, ModelRefController, ListControllerMixin){ + return declare("dojox.mvc.ListController", [ModelRefController, ListControllerMixin], { // summary: // A controller working with array model, managing its cursor. // NOTE - If this class is used with a widget by data-dojo-mixins, make sure putting the widget in data-dojo-type and putting this class to data-dojo-mixins. @@ -113,117 +30,5 @@ define([ // | // | // | - - // idProperty: String - // The property name in element in the model array, that works as its identifier. - idProperty: "uniqueId", - - // cursorId: String - // The ID of the selected element in the model array. - cursorId: null, - - // cursorIndex: Number|String - // The index of the selected element in the model. - cursorIndex: -1, - - // cursor: dojo/Stateful - // The selected element in the model array. - cursor: null, - - // model: dojox/mvc/StatefulArray - // The data model working as an array. - model: null, - - // _listModelWatchHandle: Object - // The watch handle of model, watching for array elements. - _listModelWatchHandle: null, - - // _tableModelWatchHandle: Object - // The watch handle of model. - _tableModelWatchHandle: null, - - // _refCursorProp: String - // The property name for the data model of the current selection. - _refCursorProp: "cursor", - - // _refModelProp: String - // The property name for the data model. - _refModelProp: "cursor", - - destroy: function(){ - unwatchHandles(this); - this.inherited(arguments); - }, - - set: function(/*String*/ name, /*Anything*/ value){ - // summary: - // Set a property to this. - // name: String - // The property to set. - // value: Anything - // The value to set in the property. - - var oldRefInCursor = this[this._refCursorProp]; - var oldRefInModel = this[this._refInModelProp]; - this.inherited(arguments); - if(name == this._refCursorProp){ - setRefCursor(this, oldRefInCursor, value); - } - if(name == this._refInModelProp){ - setRefInModel(this, oldRefInModel, value); - } - }, - - _setCursorIdAttr: function(/*String*/ value){ - // summary: - // Handler for calls to set("cursorId", val). - // description: - // Finds the index associated with the given cursor ID, and updates cursorIndex property. - - var old = this.cursorId; - this._set("cursorId", value); - var model = this[this._refInModelProp]; - if(!model){ return; } - if(old !== value){ - if(lang.isArray(model)){ - for(var i = 0; i < model.length; i++){ - if(model[i][this.idProperty] == value){ - this.set("cursorIndex", i); - return; - } - } - this._set("cursorIndex", -1); - }else{ - for(var s in model){ - if(model[s][this.idProperty] == value){ - this.set("cursorIndex", s); - return; - } - } - this._set("cursorIndex", ""); - } - } - }, - - _setCursorIndexAttr: function(/*Number*/ value){ - // summary: - // Handler for calls to set("cursorIndex", val). - // description: - // Updates cursor, cursorId, cursorIndex properties internally and call watch callbacks for them. - - this._set("cursorIndex", value); - if(!this[this._refInModelProp]){ return; } - this.set(this._refCursorProp, this[this._refInModelProp][value]); - this.set("cursorId", this[this._refInModelProp][value] && this[this._refInModelProp][value][this.idProperty]); - }, - - hasControllerProperty: function(/*String*/ name){ - // summary: - // Returns true if this controller itself owns the given property. - // name: String - // The property name. - - return this.inherited(arguments) || name == this._refCursorProp; - } }); }); diff --git a/ListControllerMixin.js b/ListControllerMixin.js new file mode 100644 index 0000000..30d104f --- /dev/null +++ b/ListControllerMixin.js @@ -0,0 +1,379 @@ +define([ + "dojo/_base/array", + "dojo/_base/lang", + "dojo/_base/declare" +], function(array, lang, declare){ + var undef; + + function findIndex(/*String*/ idProperty, /*Object*/ model, /*Object*/ cursor){ + if(lang.isArray(model)){ + for(var i = 0, l = model.length; i < l; ++i){ + if(model[i][idProperty] == cursor[idProperty]){ + return i; + } + } + }else{ + for(var s in model){ + if(model[s][idProperty] == cursor[idProperty]){ + return s; + } + } + } + } + + function unwatchHandles(/*dojox/mvc/ListControllerMixin*/ c){ + // summary: + // Unwatch model watch handles. + + for(var s in {"_listModelWatchHandle": 1, "_tableModelWatchHandle": 1}){ + if(c[s]){ + c[s].unwatch(); + c[s] = null; + } + } + } + + function setRefInModel(/*dojox/mvc/ListControllerMixin*/ ctrl, /*dojo/Stateful*/ old, /*dojo/Stateful*/ current){ + // summary: + // A function called when this controller gets newer value as the list data. + + unwatchHandles(ctrl); + if(current && old !== current){ + if(current.watchElements){ + ctrl._listModelWatchHandle = current.watchElements(function(idx, removals, adds){ + if(removals && adds){ + var curIdx = ctrl.get("cursorIndex"); + // If selected element is removed, make "no selection" state + if(removals && curIdx >= idx && curIdx < idx + removals.length){ + ctrl.set("cursorIndex", -1); + return; + } + // If selected element is equal to or larger than the removals/adds point, update the selected index + if((removals.length || adds.length) && curIdx >= idx){ + ctrl.set(ctrl._refCursorProp, ctrl.get("cursor")); + } + }else{ + // If there is a update to the whole array, update the selected index + ctrl.set(ctrl._refCursorProp, ctrl.get(ctrl._refCursorProp)); + } + }); + } + if(current.set && current.watch){ + if(ctrl.get("cursorIndex") < 0){ ctrl._set("cursorIndex", ""); } + ctrl._tableModelWatchHandle = current.watch(function(name, old, current){ + if(old !== current && name == ctrl.get("cursorIndex")){ + ctrl.set(ctrl._refCursorProp, current); + } + }); + } + } + ctrl._setCursorIndexAttr(ctrl.cursorIndex); + } + + function setRefSourceModel(/*dojox/mvc/EditModelRefListControllerMixin*/ ctrl, /*Anything*/ old, /*Anything*/ current){ + // summary: + // A function called when this controller gets newer value as the data source. + // ctrl: dojox/mvc/EditModelRefControllerMixin + // The controller. + // old: Anything + // The older value. + // current: Anything + // The newer value. + + if(old !== current){ + ctrl._handleRefSourceModel && ctrl._handleRefSourceModel.remove(); + current && ctrl.own(ctrl._handleRefSourceModel = current.watch(function(name, old, current){ + if(old !== current){ + var index; + index = findIndex(ctrl.idProperty, ctrl[ctrl._refOriginalModelProp], current); + if(index !== undef){ + ctrl[ctrl._refOriginalModelProp].set(index, ctrl.holdModelUntilCommit ? current : ctrl.cloneModel(current)); + } + index = findIndex(ctrl.idProperty, ctrl[ctrl._refEditModelProp], current); + if(index !== undef){ + ctrl[ctrl._refEditModelProp].set(index, ctrl.holdModelUntilCommit ? ctrl.cloneModel(current) : current); + } + } + })); + } + } + + function setRefCursor(/*dojox/mvc/ListControllerMixin*/ ctrl, /*dojo/Stateful*/ old, /*dojo/Stateful*/ current){ + // summary: + // A function called when this controller gets newer value as the data of current selection. + // description: + // Finds the index associated with the given element, and updates cursorIndex property. + + var model = ctrl[ctrl._refInModelProp]; + if(!model){ return; } + if(old !== current){ + if(lang.isArray(model)){ + var foundIdx = array.indexOf(model, current); + if(foundIdx < 0){ + var targetIdx = ctrl.get("cursorIndex"); + if(targetIdx >= 0 && targetIdx < model.length){ + model.set(targetIdx, current); + } + }else{ + ctrl.set("cursorIndex", foundIdx); + } + }else{ + for(var s in model){ + if(model[s] == current){ + ctrl.set("cursorIndex", s); + return; + } + } + var targetIdx = ctrl.get("cursorIndex"); + if(targetIdx){ + model.set(targetIdx, current); + } + } + } + } + + return declare("dojox.mvc.ListControllerMixin", null, { + // summary: + // A mixin to dojox/mvc/ModelRefController, working with array model, managing its cursor. + // example: + // The text box changes its value every two seconds. + // | + // | + // | + // | + // | + // | + // | + // | + // | + // | + // | + // example: + // The text box refers to "value" property in the controller (with "ctrl" ID). + // The controller provides the "value" property, from the data model, using the third one in array. + // Two seconds later, the text box changes from "Third" to "3rd", and it's saved for reversion. + // Two seconds later then, the text box changes from "3rd" to "THIRD", and two seconds later then, the text box changes back to "3rd". + // | + // | + // | + // | + // | + // | + // | + // | + // | + // | + // | + // example: + // The controller with "ctrlSource" ID specifies holding changes until commit() is called (by setting true to holdModelUntilCommit). + // As the change in the second text box is committed two seconds later from the change, the first text box is updated at then (when the change is committed). + // | + // | + // | + // | + // | + // | + // | + // | + // | + // | Source: + // | Edit: + // | + // | + + // idProperty: String + // The property name in element in the model array, that works as its identifier. + idProperty: "uniqueId", + + // cursorId: String + // The ID of the selected element in the model array. + cursorId: null, + + // cursorIndex: Number|String + // The index of the selected element in the model. + cursorIndex: -1, + + // cursor: dojo/Stateful + // The selected element in the model array. + cursor: null, + + // model: dojox/mvc/StatefulArray + // The data model working as an array. + model: null, + + // _listModelWatchHandle: Object + // The watch handle of model, watching for array elements. + _listModelWatchHandle: null, + + // _tableModelWatchHandle: Object + // The watch handle of model. + _tableModelWatchHandle: null, + + // _refCursorProp: String + // The property name for the data model of the current selection. + _refCursorProp: "cursor", + + // _refModelProp: String + // The property name for the data model. + _refModelProp: "cursor", + + destroy: function(){ + unwatchHandles(this); + this.inherited(arguments); + }, + + set: function(/*String*/ name, /*Anything*/ value){ + // summary: + // Set a property to this. + // name: String + // The property to set. + // value: Anything + // The value to set in the property. + + var oldRefInCursor = this[this._refCursorProp], + oldRefInModel = this[this._refInModelProp], + oldRefSourceModel = this[this._refSourceModelProp]; + this.inherited(arguments); + if(name == this._refCursorProp){ + setRefCursor(this, oldRefInCursor, value); + } + if(name == this._refInModelProp){ + setRefInModel(this, oldRefInModel, value); + } + if(name == this._refSourceModelProp){ + setRefSourceModel(this, oldRefSourceModel, value); + } + }, + + _setCursorIdAttr: function(/*String*/ value){ + // summary: + // Handler for calls to set("cursorId", val). + // description: + // Finds the index associated with the given cursor ID, and updates cursorIndex property. + + var old = this.cursorId; + this._set("cursorId", value); + var model = this[this._refInModelProp]; + if(!model){ return; } + if(old !== value){ + if(lang.isArray(model)){ + for(var i = 0; i < model.length; i++){ + if(model[i][this.idProperty] == value){ + this.set("cursorIndex", i); + return; + } + } + this._set("cursorIndex", -1); + }else{ + for(var s in model){ + if(model[s][this.idProperty] == value){ + this.set("cursorIndex", s); + return; + } + } + this._set("cursorIndex", ""); + } + } + }, + + _setCursorIndexAttr: function(/*Number*/ value){ + // summary: + // Handler for calls to set("cursorIndex", val). + // description: + // Updates cursor, cursorId, cursorIndex properties internally and call watch callbacks for them. + + this._set("cursorIndex", value); + if(!this[this._refInModelProp]){ return; } + this.set(this._refCursorProp, this[this._refInModelProp][value]); + this.set("cursorId", this[this._refInModelProp][value] && this[this._refInModelProp][value][this.idProperty]); + }, + + commitCurrent: function(){ + // summary: + // Send the change of currently selected list item back to the data source. + + var model = this[this.holdModelUntilCommit ? this._refSourceModelProp : this._refOriginalModelProp], + index = findIndex(this.idProperty, model, this.cursor); + if(index !== undef){ + model.set(index, this.cloneModel(this.cursor)); + } + this.inherited(arguments); + }, + + resetCurrent: function(){ + // summary: + // Change the currently selected list item back to its original state. + + var model = this[this.holdModelUntilCommit ? this._refEditModelProp : this._refSourceModelProp], + originalModel = this[this._refOriginalModelProp], + index = findIndex(this.idProperty, model, this.cursor), + originalModelIndex = findIndex(this.idProperty, originalModel, this.cursor); + if(index !== undef && originalModelIndex !== undef){ + model.set(index, this.cloneModel(originalModel[originalModelIndex])); + } + this.inherited(arguments); + }, + + hasControllerProperty: function(/*String*/ name){ + // summary: + // Returns true if this controller itself owns the given property. + // name: String + // The property name. + + return this.inherited(arguments) || name == this._refCursorProp; + } + }); +}); diff --git a/StoreRefController.js b/StoreRefController.js index ae4dc54..ad6a09d 100644 --- a/StoreRefController.js +++ b/StoreRefController.js @@ -1,11 +1,9 @@ define([ "dojo/_base/declare", - "dojo/_base/lang", - "dojo/when", - "./getStateful", - "./ModelRefController" -], function(declare, lang, when, getStateful, ModelRefController){ - return declare("dojox.mvc.StoreRefController", ModelRefController, { + "./ModelRefController", + "./StoreRefControllerMixin" +], function(declare, ModelRefController, StoreRefControllerMixin){ + return declare("dojox.mvc.StoreRefController", [ModelRefController, StoreRefControllerMixin], { // summary: // A child class of dojox.mvc.ModelRefController, which keeps a reference to Dojo Object Store (in store property). // description: @@ -45,109 +43,5 @@ define([ // | // | // | - - // store: dojo/store/* - // The Dojo Object Store in use. - store: null, - - // getStatefulOptions: dojox.mvc.getStatefulOptions - // The options to get stateful object from plain value. - getStatefulOptions: null, - - // _refSourceModelProp: String - // The property name for the data model, that serves as the data source. - _refSourceModelProp: "model", - - queryStore: function(/*Object*/ query, /*dojo/store/api/Store.QueryOptions?*/ options){ - // summary: - // Queries the store for objects. - // query: Object - // The query to use for retrieving objects from the store. - // options: dojo/store/api/Store.QueryOptions? - // The optional arguments to apply to the resultset. - // returns: dojo/store/api/Store.QueryResults - // The results of the query, extended with iterative methods. - - if(!(this.store || {}).query){ return; } - if(this._queryObserveHandle){ this._queryObserveHandle.cancel(); } - - var _self = this, - queryResult = this.store.query(query, options), - result = when(queryResult, function(results){ - if(_self._beingDestroyed){ return; } - results = getStateful(results, _self.getStatefulOptions); - _self.set(_self._refSourceModelProp, results); - return results; - }); - // For dojo/store/Observable, which adds a function to query result - for(var s in queryResult){ - if(isNaN(s) && queryResult.hasOwnProperty(s) && lang.isFunction(queryResult[s])){ - result[s] = queryResult[s]; - } - } - return result; - }, - - getStore: function(/*Number*/ id, /*Object*/ options){ - // summary: - // Retrieves an object by its identity. - // id: Number - // The identity to use to lookup the object. - // options: Object - // The options for dojo/store.*.get(). - // returns: Object - // The object in the store that matches the given id. - - if(!(this.store || {}).get){ return; } - if(this._queryObserveHandle){ this._queryObserveHandle.cancel(); } - var _self = this; - result = when(this.store.get(id, options), function(result){ - if(_self._beingDestroyed){ return; } - result = getStateful(result, _self.getStatefulOptions); - _self.set(_self._refSourceModelProp, result); - return result; - }); - return result; - }, - - putStore: function(/*Object*/ object, /*dojo/store/api/Store.PutDirectives?*/ options){ - // summary: - // Stores an object. - // object: Object - // The object to store. - // options: dojo/store/api/Store.PutDirectives? - // Additional metadata for storing the data. Includes an "id" property if a specific id is to be used. - // returns: Number - - if(!(this.store || {}).put){ return; } - return this.store.put(object, options); - }, - - addStore: function(object, options){ - // summary: - // Creates an object, throws an error if the object already exists. - // object: Object - // The object to store. - // options: dojo/store/api/Store.PutDirectives? - // Additional metadata for storing the data. Includes an "id" property if a specific id is to be used. - // returns: Number - - if(!(this.store || {}).add){ return; } - return this.store.add(object, options); - }, - - removeStore: function(/*Number*/ id, /*Object*/ options){ - // summary: - // Deletes an object by its identity - // id: Number - // The identity to use to delete the object - // options: Object - // The options for dojo/store/*.remove(). - // returns: Boolean - // Returns true if an object was removed, falsy (undefined) if no object matched the id. - - if(!(this.store || {}).remove){ return; } - return this.store.remove(id, options); - } }); -}); +}); \ No newline at end of file diff --git a/StoreRefControllerMixin.js b/StoreRefControllerMixin.js new file mode 100644 index 0000000..f93ef51 --- /dev/null +++ b/StoreRefControllerMixin.js @@ -0,0 +1,250 @@ +define([ + "dojo/_base/declare", + "dojo/_base/lang", + "dojo/when", + "./getStateful", + "./getPlainValue" +], function(declare, lang, when, getStateful, getPlainValue){ + return declare("dojox.mvc.StoreRefControllerMixin", null, { + // summary: + // A mixin class to dojox.mvc.ModelRefController, which keeps a reference to Dojo Object Store (in store property). + // description: + // Has several methods to work with the store: + // + // - queryStore(): Runs query() against the store, and creates a data model from retrieved data + // - getStore(): Runs get() against the store, and creates a data model from retrieved data + // - putStore(): Runs put() against the store + // - addStore(): Runs add() against the store + // - removeStore(): Runs remove() against the store + // + // dojo.Stateful get()/set()/watch() interfaces in dojox.mvc.StoreRefControllerMixin will work with the data model from queryStore() or getStore(). + // example: + // The text box refers to "value" property in the controller (with "ctrl" ID). + // The controller provides the "value" property, from the data coming from data store ("store" property in the controller). + // Two seconds later, the text box changes from "Foo" to "Bar" as the controller gets new data from data store. + // | + // | + // | + // | + // | + // | + // | + // | + // | + // | + // | + // example: + // The check box refers to "value" property in the controller (with "ctrl" ID). + // The controller provides the "value" property, from the data coming from data store ("store" property in the controller), using the first one in array. + // Two seconds later, the check box changes from unchecked to checked. + // The change is committed to the data store, which is reflected to dojo/store/Observable callback. + // | + // | + // | + // | + // | + // | + // | + // | + // | + // | + // | + + // store: dojo/store/* + // The Dojo Object Store in use. + store: null, + + // getStatefulOptions: dojox.mvc.getStatefulOptions + // The options to get stateful object from plain value. + getStatefulOptions: null, + + // getPlainValueOptions: dojox/mvc/getPlainValueOptions + // The options to get plain value from stateful object. + getPlainValueOptions: null, + + // _removals: Object[] + // The list of removed elements. + _removals: [], + + // _resultsWatchHandle: dojox/mvc/StatefulArray.watchElements.handle + // The watch handle for model array elements. + _resultsWatchHandle: null, + + constructor: function(){ + if(!this[this._refSourceModelProp]){ + this[this._refSourceModelProp] = "model"; + } + }, + + queryStore: function(/*Object*/ query, /*dojo/store/api/Store.QueryOptions?*/ options){ + // summary: + // Queries the store for objects. + // query: Object + // The query to use for retrieving objects from the store. + // options: dojo/store/api/Store.QueryOptions? + // The optional arguments to apply to the resultset. + // returns: dojo/store/api/Store.QueryResults + // The results of the query, extended with iterative methods. + + if(!(this.store || {}).query){ return; } + if(this._queryObserveHandle){ this._queryObserveHandle.cancel(); } + if(this._resultsWatchHandle){ this._resultsWatchHandle.unwatch(); } + this._removals = []; + + var _self = this, + queryResult = this.store.query(query, options), + result = when(queryResult, function(results){ + if(_self._beingDestroyed){ return; } + results = getStateful(results, _self.getStatefulOptions); + _self.set(_self._refSourceModelProp, results); + if(lang.isArray(results)){ + _self._resultsWatchHandle = results.watchElements(function(idx, removals, adds){ + [].push.apply(_self._removals, removals); + }); + } + return results; + }); + // For dojo/store/Observable, which adds a function to query result + for(var s in queryResult){ + if(isNaN(s) && queryResult.hasOwnProperty(s) && lang.isFunction(queryResult[s])){ + result[s] = queryResult[s]; + } + } + return result; + }, + + getStore: function(/*Number*/ id, /*Object*/ options){ + // summary: + // Retrieves an object by its identity. + // id: Number + // The identity to use to lookup the object. + // options: Object + // The options for dojo/store.*.get(). + // returns: Object + // The object in the store that matches the given id. + + if(!(this.store || {}).get){ return; } + if(this._queryObserveHandle){ this._queryObserveHandle.cancel(); } + if(this._resultsWatchHandle){ this._resultsWatchHandle.unwatch(); } + var _self = this; + return when(this.store.get(id, options), function(result){ + if(_self._beingDestroyed){ return; } + result = getStateful(result, _self.getStatefulOptions); + _self.set(_self._refSourceModelProp, result); + return result; + }); + }, + + putStore: function(/*Object*/ object, /*dojo/store/api/Store.PutDirectives?*/ options){ + // summary: + // Stores an object. + // object: Object + // The object to store. + // options: dojo/store/api/Store.PutDirectives? + // Additional metadata for storing the data. Includes an "id" property if a specific id is to be used. + // returns: Number + + if(!(this.store || {}).put){ return; } + return this.store.put(object, options); + }, + + addStore: function(object, options){ + // summary: + // Creates an object, throws an error if the object already exists. + // object: Object + // The object to store. + // options: dojo/store/api/Store.PutDirectives? + // Additional metadata for storing the data. Includes an "id" property if a specific id is to be used. + // returns: Number + + if(!(this.store || {}).add){ return; } + return this.store.add(object, options); + }, + + removeStore: function(/*Number*/ id, /*Object*/ options){ + // summary: + // Deletes an object by its identity + // id: Number + // The identity to use to delete the object + // options: Object + // The options for dojo/store/*.remove(). + // returns: Boolean + // Returns true if an object was removed, falsy (undefined) if no object matched the id. + + if(!(this.store || {}).remove){ return; } + return this.store.remove(id, options); + }, + + commit: function(){ + // summary: + // Send the change back to the data source. + + if(this._removals){ + for(var i = 0; i < this._removals.length; i++){ + this.store.remove(this.store.getIdentity(this._removals[i])); + } + this._removals = []; + } + var data = getPlainValue(this.get(this._refEditModelProp), this.getPlainValueOptions); + if(lang.isArray(data)){ + for(var i = 0; i < data.length; i++){ + this.store.put(data[i]); + } + }else{ + this.store.put(data); + } + this.inherited(arguments); + }, + + commitCurrent: function(){ + // summary: + // Send the change of currently selected list item back to the data source for the current index. + + this.inherited(arguments); + this.store.put(this.cursor); + }, + + reset: function(){ + // summary: + // Change the model back to its original state. + + this.inherited(arguments); + this._removals = []; + }, + + destroy: function(){ + // summary: + // Clean up model watch handle as this object is destroyed. + + if(this._resultsWatchHandle){ this._resultsWatchHandle.unwatch(); } + this.inherited(arguments); + } + }); +}); diff --git a/_Controller.js b/_Controller.js index 77693da..5693637 100644 --- a/_Controller.js +++ b/_Controller.js @@ -2,9 +2,10 @@ define([ "dojo/_base/declare", "dojo/_base/lang", "dojo/Stateful", + "dijit/Destroyable", "./_atBindingMixin" -], function(declare, lang, Stateful, _atBindingMixin){ - return declare("dojox.mvc._Controller", [Stateful, _atBindingMixin], { +], function(declare, lang, Stateful, Destroyable, _atBindingMixin){ + return declare("dojox.mvc._Controller", [Stateful, Destroyable, _atBindingMixin], { postscript: function(/*Object?*/ params, /*DomNode|String?*/ srcNodeRef){ // summary: // If this object is not called from Dojo parser, starts this up right away. diff --git a/tests/EditModelRefControllerMixin.js b/tests/EditModelRefControllerMixin.js new file mode 100644 index 0000000..0699bc5 --- /dev/null +++ b/tests/EditModelRefControllerMixin.js @@ -0,0 +1,78 @@ +define([ + "doh", + "dojo/_base/declare", + "dojo/Stateful", + "../ModelRefController", + "../EditModelRefControllerMixin", + "../ListControllerMixin", + "../at", + "../sync", + "../getStateful" +], function(doh, declare, Stateful, ModelRefController, EditModelRefControllerMixin, ListControllerMixin, at, sync, getStateful){ + var data = [ + {uniqueId: 0, value: "Index 0"}, + {uniqueId: 1, value: "Index 1"}, + {uniqueId: 2, value: "Index 2"}, + {uniqueId: 3, value: "Index 3"}, + {uniqueId: 4, value: "Index 4"} + ], clz = declare([ModelRefController, EditModelRefControllerMixin, ListControllerMixin], {}); + + doh.register("dojox.mvc.tests.EditModelRefControllerMixin", [ + function commit(){ + var ctrl = new clz({sourceModel: getStateful(data), cursorIndex: 2}); + ctrl.set("value", "3rd"); + doh.is("3rd", ctrl.sourceModel[2].value, "sourceModel should be updated as soon as there is a change in value"); + doh.is("Index 2", ctrl.originalModel[2].value, "originalModel shouldn't be updated until commit() is called"); + ctrl.commitCurrent(); + doh.is("3rd", ctrl.originalModel[2].value, "originalModel should be updated as commit() is called"); + }, + function commitHoldModel(){ + var ctrl = new clz({sourceModel: getStateful(data), cursorIndex: 2, holdModelUntilCommit: true}); + ctrl.set("value", "3rd"); + doh.is("Index 2", ctrl.sourceModel[2].value, "sourceModel shouldn't be updated until commit() is called"); + doh.is("Index 2", ctrl.originalModel[2].value, "originalModel shouldn't be updated until commit() is called"); + ctrl.commitCurrent(); + doh.is("3rd", ctrl.sourceModel[2].value, "sourceModel should be updated as commit() is called"); + doh.is("3rd", ctrl.originalModel[2].value, "originalModel should be updated as commit() is called"); + }, + function reset(){ + var ctrl = new clz({sourceModel: getStateful(data), cursorIndex: 2}); + ctrl.set("value", "3rd"); + ctrl.commitCurrent(); + ctrl.set("value", "Third"); + ctrl.resetCurrent(); + doh.is("3rd", ctrl.get("value"), "Model should be updated as reset() is called"); + doh.is("3rd", ctrl.model[2].value, "Model should be updated as reset() is called"); + doh.is("3rd", ctrl.sourceModel[2].value, "sourceModel should be updated as reset() is called"); + doh.is("3rd", ctrl.originalModel[2].value, "originalModel should be updated as reset() is called"); + }, + function resetHoldModel(){ + var ctrl = new clz({sourceModel: getStateful(data), cursorIndex: 2, holdModelUntilCommit: true}); + ctrl.set("value", "3rd"); + ctrl.commitCurrent(); + ctrl.set("value", "Third"); + ctrl.resetCurrent(); + doh.is("3rd", ctrl.get("value"), "Model should be updated as reset() is called"); + doh.is("3rd", ctrl.model[2].value, "Model should be updated as reset() is called"); + doh.is("3rd", ctrl.sourceModel[2].value, "sourceModel should be updated as reset() is called"); + doh.is("3rd", ctrl.originalModel[2].value, "originalModel should be updated as reset() is called"); + }, + { + name: "commitHoldModelToBindTarget", + runTest: function(){ + var ctrlSource = new (declare([ModelRefController, ListControllerMixin], {}))({model: getStateful(data), cursorIndex: 2}), + ctrlEdit = this.ctrlEdit = new clz({sourceModel: at(ctrlSource, "model"), cursorIndex: 2, holdModelUntilCommit: true}), + target = new Stateful(); + this.h = sync(ctrlSource, "value", target, "value"); + ctrlEdit.set("value", "3rd"); + doh.is("Index 2", target.value, "sourceModel shouldn't be updated until commit() is called"); + ctrlEdit.commitCurrent(); + doh.is("3rd", target.value, "sourceModel should be updated as commit() is called"); + }, + tearDown: function(){ + this.ctrlEdit && this.ctrlEdit.destroy(); + this.h && this.h.remove(); + } + } + ]); +}); \ No newline at end of file diff --git a/tests/module.js b/tests/module.js index 7d4bcca..462530e 100644 --- a/tests/module.js +++ b/tests/module.js @@ -6,6 +6,7 @@ define([ "./_Controller", "./ModelRefController", "./StoreRefControllerTest", + "./EditModelRefControllerMixin", "./WidgetList", "./StatefulArray", "./StatefulModelOptions" diff --git a/tests/moduleFullSet.js b/tests/moduleFullSet.js index 0597dd9..4e0830a 100644 --- a/tests/moduleFullSet.js +++ b/tests/moduleFullSet.js @@ -6,6 +6,7 @@ define([ "./_Controller", "./ModelRefController", "./StoreRefControllerTest", + "./EditModelRefControllerMixin", "./WidgetList", "./StatefulArray", "./StatefulModelOptions"