123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462 |
- 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;
- });
|