diff options
Diffstat (limited to 'js/dojo/dojox/mvc/StatefulModel.js')
| -rw-r--r-- | js/dojo/dojox/mvc/StatefulModel.js | 463 |
1 files changed, 463 insertions, 0 deletions
diff --git a/js/dojo/dojox/mvc/StatefulModel.js b/js/dojo/dojox/mvc/StatefulModel.js new file mode 100644 index 0000000..0f4b41f --- /dev/null +++ b/js/dojo/dojox/mvc/StatefulModel.js @@ -0,0 +1,463 @@ +//>>built +define("dojox/mvc/StatefulModel", [ + "dojo/_base/lang", + "dojo/_base/array", + "dojo/_base/declare", + "dojo/Stateful" +], function(lang, array, declare, Stateful){ + /*===== + declare = dojo.declare; + Stateful = dojo.Stateful; + =====*/ + + var StatefulModel = declare("dojox.mvc.StatefulModel", [Stateful], { + // summary: + // The first-class native JavaScript data model based on dojo.Stateful + // that wraps any data structure(s) that may be relevant for a view, + // a view portion, a dijit or any custom view layer component. + // + // description: + // A data model is effectively instantiated with a plain JavaScript + // object which specifies the initial data structure for the model. + // + // | var struct = { + // | order : "abc123", + // | shipto : { + // | address : "123 Example St, New York, NY", + // | phone : "212-000-0000" + // | }, + // | items : [ + // | { part : "x12345", num : 1 }, + // | { part : "n09876", num : 3 } + // | ] + // | }; + // | + // | var model = dojox.mvc.newStatefulModel({ data : struct }); + // + // The simple example above shows an inline plain JavaScript object + // illustrating the data structure to prime the model with, however + // the underlying data may be made available by other means, such as + // from the results of a dojo.store or dojo.data query. + // + // To deal with stores providing immediate values or Promises, a + // factory method for model instantiation is provided. This method + // will either return an immediate model or a model Promise depending + // on the nature of the store. + // + // | var model = dojox.mvc.newStatefulModel({ store: someStore }); + // + // The created data model has the following properties: + // + // - It enables dijits or custom components in the view to "bind" to + // data within the model. A bind creates a bi-directional update + // mechanism between the bound view and the underlying data: + // - The data model is "live" data i.e. it maintains any updates + // driven by the view on the underlying data. + // - The data model issues updates to portions of the view if the + // data they bind to is updated in the model. For example, if two + // dijits are bound to the same part of a data model, updating the + // value of one in the view will cause the data model to issue an + // update to the other containing the new value. + // + // - The data model internally creates a tree of dojo.Stateful + // objects that matches the input, which is effectively a plain + // JavaScript object i.e. "pure data". This tree allows dijits or + // other view components to bind to any node within the data model. + // Typically, dijits with simple values bind to leaf nodes of the + // datamodel, whereas containers bind to internal nodes of the + // datamodel. For example, a datamodel created using the object below + // will generate the dojo.Stateful tree as shown: + // + // | var model = dojox.mvc.newStatefulModel({ data : { + // | prop1 : "foo", + // | prop2 : { + // | leaf1 : "bar", + // | leaf2 : "baz" + // | } + // | }}); + // | + // | // The created dojo.Stateful tree is illustrated below (all nodes are dojo.Stateful objects) + // | // + // | // o (root node) + // | // / \ + // | // (prop1 node) o o (prop2 node) + // | // / \ + // | // (leaf1 node) o o (leaf2 node) + // | // + // | // The root node is accessed using the expression "model" (the var name above). The prop1 + // | // node is accessed using the expression "model.prop1", the leaf2 node is accessed using + // | // the expression "model.prop2.leaf2" and so on. + // + // - Each of the dojo.Stateful nodes in the model may store data as well + // as associated "meta-data", which includes things such as whether + // the data is required or readOnly etc. This meta-data differs from + // that maintained by, for example, an individual dijit in that this + // is maintained by the datamodel and may therefore be affected by + // datamodel-level constraints that span multiple dijits or even + // additional criteria such as server-side computations. + // + // - When the model is backed by a dojo.store or dojo.data query, the + // client-side updates can be persisted once the client is ready to + // "submit" the changes (which may include both value changes or + // structural changes - adds/deletes). The datamodel allows control + // over when the underlying data is persisted i.e. this can be more + // incremental or batched per application needs. + // + // There need not be a one-to-one association between a datamodel and + // a view or portion thereof. For example, multiple datamodels may + // back the dijits in a view. Indeed, this may be useful where the + // binding data comes from a number of data sources or queries, for + // example. Just as well, dijits from multiple portions of the view + // may be bound to a single datamodel. + // + // Finally, requiring this class also enables all dijits to become data + // binding aware. The data binding is commonly specified declaratively + // via the "ref" property in the "data-dojo-props" attribute value. + // + // To illustrate, the following is the "Hello World" of such data-bound + // widget examples: + // + // | <script> + // | dojo.require("dojox.mvc"); + // | dojo.require("dojo.parser"); + // | var model; + // | dojo.addOnLoad(function(){ + // | model = dojox.mvc.newStatefulModel({ data : { + // | hello : "Hello World" + // | }}); + // | dojo.parser.parse(); + // | } + // | </script> + // | + // | <input id="helloInput" dojoType="dijit.form.TextBox" + // | ref="model.hello"> + // + // or + // + // | <script> + // | var model; + // | require(["dojox/mvc", "dojo/parser"], function(dxmvc, parser){ + // | model = dojox.mvc.newStatefulModel({ data : { + // | hello : "Hello World" + // | }}); + // | parser.parse(); + // | }); + // | </script> + // | + // | <input id="helloInput" data-dojo-type="dijit.form.TextBox" + // | data-dojo-props="ref: 'model.hello'"> + // + // Such data binding awareness for dijits is added by extending the + // dijit._WidgetBase class to include data binding capabilities + // provided by dojox.mvc._DataBindingMixin, and this class declares a + // dependency on dojox.mvc._DataBindingMixin. + // + // The presence of a data model and the data-binding capabilities + // outlined above support the flexible development of a number of MVC + // patterns on the client. As an example, CRUD operations can be + // supported with minimal application code. + + // data: Object + // The plain JavaScript object / data structure used to initialize + // this model. At any point in time, it holds the lasted saved model + // state. + // Either data or store property must be provided. + data: null, + + // store: dojo.store.DataStore + // The data store from where to retrieve initial data for this model. + // An optional query may also be provided along with this store. + // Either data or store property must be provided. + store: null, + + // valid: boolean + // Whether this model deems the associated data to be valid. + valid: true, + + // value: Object + // The associated value (if this is a leaf node). The value of + // intermediate nodes in the model is not defined. + value: "", + + //////////////////////// PUBLIC METHODS / API //////////////////////// + + reset: function(){ + // summary: + // Resets this data model values to its original state. + // Structural changes to the data model (such as adds or removes) + // are not restored. + if(lang.isObject(this.data) && !(this.data instanceof Date) && !(this.data instanceof RegExp)){ + for(var x in this){ + if(this[x] && lang.isFunction(this[x].reset)){ + this[x].reset(); + } + } + }else{ + this.set("value", this.data); + } + }, + + commit: function(/*"dojo.store.DataStore?"*/ store){ + // summary: + // Commits this data model: + // - Saves the current state such that a subsequent reset will not + // undo any prior changes. + // - Persists client-side changes to the data store, if a store + // has been supplied as a parameter or at instantiation. + // store: + // dojo.store.DataStore + // Optional dojo.store.DataStore to use for this commit, if none + // provided but one was provided at instantiation time, that store + // will be used instead. + this._commit(); + var ds = store || this.store; + if(ds){ + this._saveToStore(ds); + } + }, + + toPlainObject: function(){ + // summary: + // Produces a plain JavaScript object representation of the data + // currently within this data model. + // returns: + // Object + // The plain JavaScript object representation of the data in this + // model. + var ret = {}; + var nested = false; + for(var p in this){ + if(this[p] && lang.isFunction(this[p].toPlainObject)){ + if(!nested && typeof this.get("length") === "number"){ + ret = []; + } + nested = true; + ret[p] = this[p].toPlainObject(); + } + } + if(!nested){ + if(this.get("length") === 0){ + ret = []; + }else{ + ret = this.value; + } + } + return ret; + }, + + add: function(/*String*/ name, /*dojo.Stateful*/ stateful){ + // summary: + // Adds a dojo.Stateful tree represented by the given + // dojox.mvc.StatefulModel at the given property name. + // name: + // The property name to use whose value will become the given + // dijit.Stateful tree. + // stateful: + // The dojox.mvc.StatefulModel to insert. + // description: + // In case of arrays, the property names are indices passed + // as Strings. An addition of such a dojo.Stateful node + // results in right-shifting any trailing sibling nodes. + var n, n1, elem, elem1, save = new StatefulModel({ data : "" }); + if(typeof this.get("length") === "number" && /^[0-9]+$/.test(name.toString())){ + n = name; + if(!this.get(n)){ + if(this.get("length") == 0 && n == 0){ // handle the empty array case + this.set(n, stateful); + } else { + n1 = n-1; + if(!this.get(n1)){ + throw new Error("Out of bounds insert attempted, must be contiguous."); + } + this.set(n, stateful); + } + }else{ + n1 = n-0+1; + elem = stateful; + elem1 = this.get(n1); + if(!elem1){ + this.set(n1, elem); + }else{ + do{ + this._copyStatefulProperties(elem1, save); + this._copyStatefulProperties(elem, elem1); + this._copyStatefulProperties(save, elem); + this.set(n1, elem1); // for watchers + elem1 = this.get(++n1); + }while(elem1); + this.set(n1, elem); + } + } + this.set("length", this.get("length") + 1); + }else{ + this.set(name, stateful); + } + }, + + remove: function(/*String*/ name){ + // summary: + // Removes the dojo.Stateful tree at the given property name. + // name: + // The property name from where the tree will be removed. + // description: + // In case of arrays, the property names are indices passed + // as Strings. A removal of such a dojo.Stateful node + // results in left-shifting any trailing sibling nodes. + var n, elem, elem1; + if(typeof this.get("length") === "number" && /^[0-9]+$/.test(name.toString())){ + n = name; + elem = this.get(n); + if(!elem){ + throw new Error("Out of bounds delete attempted - no such index: " + n); + }else{ + this._removals = this._removals || []; + this._removals.push(elem.toPlainObject()); + n1 = n-0+1; + elem1 = this.get(n1); + if(!elem1){ + this.set(n, undefined); + delete this[n]; + }else{ + while(elem1){ + this._copyStatefulProperties(elem1, elem); + elem = this.get(n1++); + elem1 = this.get(n1); + } + this.set(n1-1, undefined); + delete this[n1-1]; + } + this.set("length", this.get("length") - 1); + } + }else{ + elem = this.get(name); + if(!elem){ + throw new Error("Illegal delete attempted - no such property: " + name); + }else{ + this._removals = this._removals || []; + this._removals.push(elem.toPlainObject()); + this.set(name, undefined); + delete this[name]; + } + } + }, + + valueOf: function(){ + // summary: + // Returns the value representation of the data currently within this data model. + // returns: + // Object + // The object representation of the data in this model. + return this.toPlainObject(); + }, + + toString: function(){ + // summary: + // Returns the string representation of the data currently within this data model. + // returns: + // String + // The object representation of the data in this model. + return this.value === "" && this.data ? this.data.toString() : this.value.toString(); + }, + + //////////////////////// PRIVATE INITIALIZATION METHOD //////////////////////// + + constructor: function(/*Object*/ args){ + // summary: + // Instantiates a new data model that view components may bind to. + // This is a private constructor, use the factory method + // instead: dojox.mvc.newStatefulModel(args) + // args: + // The mixin properties. + // description: + // Creates a tree of dojo.Stateful objects matching the initial + // data structure passed as input. The mixin property "data" is + // used to provide a plain JavaScript object directly representing + // the data structure. + // tags: + // private + var data = (args && "data" in args) ? args.data : this.data; + this._createModel(data); + }, + + //////////////////////// PRIVATE METHODS //////////////////////// + + _createModel: function(/*Object*/ obj){ + // summary: + // Create this data model from provided input data. + // obj: + // The input for the model, as a plain JavaScript object. + // tags: + // private + if(lang.isObject(obj) && !(obj instanceof Date) && !(obj instanceof RegExp) && obj !== null){ + for(var x in obj){ + var newProp = new StatefulModel({ data : obj[x] }); + this.set(x, newProp); + } + if(lang.isArray(obj)){ + this.set("length", obj.length); + } + }else{ + this.set("value", obj); + } + }, + + _commit: function(){ + // summary: + // Commits this data model, saves the current state into data to become the saved state, + // so a reset will not undo any prior changes. + // tags: + // private + for(var x in this){ + if(this[x] && lang.isFunction(this[x]._commit)){ + this[x]._commit(); + } + } + this.data = this.toPlainObject(); + }, + + _saveToStore: function(/*"dojo.store.DataStore"*/ store){ + // summary: + // Commit the current values to the data store: + // - remove() any deleted entries + // - put() any new or updated entries + // store: + // dojo.store.DataStore to use for this commit. + // tags: + // private + if(this._removals){ + array.forEach(this._removals, function(d){ + store.remove(store.getIdentity(d)); + }, this); + delete this._removals; + } + var dataToCommit = this.toPlainObject(); + if(lang.isArray(dataToCommit)){ + array.forEach(dataToCommit, function(d){ + store.put(d); + }, this); + }else{ + store.put(dataToCommit); + } + }, + + _copyStatefulProperties: function(/*dojo.Stateful*/ src, /*dojo.Stateful*/ dest){ + // summary: + // Copy only the dojo.Stateful properties from src to dest (uses + // duck typing). + // src: + // The source object for the copy. + // dest: + // The target object of the copy. + // tags: + // private + for(var x in src){ + var o = src.get(x); + if(o && lang.isObject(o) && lang.isFunction(o.get)){ + dest.set(x, o); + } + } + } + }); + + return StatefulModel; +}); |
