summaryrefslogtreecommitdiff
path: root/js/dojo/dojox/mvc/StatefulModel.js
diff options
context:
space:
mode:
Diffstat (limited to 'js/dojo/dojox/mvc/StatefulModel.js')
-rw-r--r--js/dojo/dojox/mvc/StatefulModel.js463
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;
+});