source: Dev/branches/rest-dojo-ui/client/dojox/mvc/StatefulModel.js @ 256

Last change on this file since 256 was 256, checked in by hendrikvanantwerpen, 13 years ago

Reworked project structure based on REST interaction and Dojo library. As
soon as this is stable, the old jQueryUI branch can be removed (it's
kept for reference).

  • Property svn:executable set to *
File size: 15.3 KB
Line 
1define([
2        "dojo/_base/lang",
3        "dojo/_base/array",
4        "dojo/_base/declare",
5        "dojo/Stateful"
6], function(lang, array, declare, Stateful){
7        /*=====
8                declare = dojo.declare;
9                Stateful = dojo.Stateful;
10        =====*/
11
12        var StatefulModel = declare("dojox.mvc.StatefulModel", [Stateful], {
13                // summary:
14                //              The first-class native JavaScript data model based on dojo.Stateful
15                //              that wraps any data structure(s) that may be relevant for a view,
16                //              a view portion, a dijit or any custom view layer component.
17                //
18                //  description:
19                //              A data model is effectively instantiated with a plain JavaScript
20                //              object which specifies the initial data structure for the model.
21                //
22                //              |       var struct = {
23                //              |               order   : "abc123",
24                //              |               shipto  : {
25                //              |                       address : "123 Example St, New York, NY",
26                //              |                       phone   : "212-000-0000"
27                //              |               },
28                //              |               items : [
29                //              |                       { part : "x12345", num : 1 },
30                //              |                       { part : "n09876", num : 3 }
31                //              |               ]
32                //              |       };
33                //              |
34                //              |       var model = dojox.mvc.newStatefulModel({ data : struct });
35                //
36                //              The simple example above shows an inline plain JavaScript object
37                //              illustrating the data structure to prime the model with, however
38                //              the underlying data may be made available by other means, such as
39                //              from the results of a dojo.store or dojo.data query.
40                //
41                //              To deal with stores providing immediate values or Promises, a
42                //              factory method for model instantiation is provided. This method
43                //              will either return an immediate model or a model Promise depending
44                //              on the nature of the store.
45                //
46                //              |       var model = dojox.mvc.newStatefulModel({ store: someStore });
47                //
48                //              The created data model has the following properties:
49                //
50                //              - It enables dijits or custom components in the view to "bind" to
51                //                data within the model. A bind creates a bi-directional update
52                //                mechanism between the bound view and the underlying data:
53                //                      - The data model is "live" data i.e. it maintains any updates
54                //                        driven by the view on the underlying data.
55                //                      - The data model issues updates to portions of the view if the
56                //                        data they bind to is updated in the model. For example, if two
57                //                        dijits are bound to the same part of a data model, updating the
58                //                        value of one in the view will cause the data model to issue an
59                //                        update to the other containing the new value.
60                //
61                //              - The data model internally creates a tree of dojo.Stateful
62                //                objects that matches the input, which is effectively a plain
63                //                JavaScript object i.e. "pure data". This tree allows dijits or
64                //                other view components to bind to any node within the data model.
65                //                Typically, dijits with simple values bind to leaf nodes of the
66                //                datamodel, whereas containers bind to internal nodes of the
67                //                datamodel. For example, a datamodel created using the object below
68                //                will generate the dojo.Stateful tree as shown:
69                //
70                //              |       var model = dojox.mvc.newStatefulModel({ data : {
71                //              |               prop1   : "foo",
72                //              |               prop2   : {
73                //              |                       leaf1   : "bar",
74                //              |                       leaf2   : "baz"
75                //              |               }
76                //              |       }});
77                //              |
78                //              |       // The created dojo.Stateful tree is illustrated below (all nodes are dojo.Stateful objects)
79                //              |       //
80                //              |       //                      o  (root node)
81                //              |       //                     / \
82                //              |       //       (prop1 node) o   o (prop2 node)
83                //              |       //                       / \
84                //              |       //         (leaf1 node) o   o (leaf2 node)
85                //              |       //
86                //              |       // The root node is accessed using the expression "model" (the var name above). The prop1
87                //              |       // node is accessed using the expression "model.prop1", the leaf2 node is accessed using
88                //              |       // the expression "model.prop2.leaf2" and so on.
89                //
90                //              - Each of the dojo.Stateful nodes in the model may store data as well
91                //                as associated "meta-data", which includes things such as whether
92                //                the data is required or readOnly etc. This meta-data differs from
93                //                that maintained by, for example, an individual dijit in that this
94                //                is maintained by the datamodel and may therefore be affected by
95                //                datamodel-level constraints that span multiple dijits or even
96                //                additional criteria such as server-side computations.
97                //
98                //              - When the model is backed by a dojo.store or dojo.data query, the
99                //                client-side updates can be persisted once the client is ready to
100                //                "submit" the changes (which may include both value changes or
101                //                structural changes - adds/deletes). The datamodel allows control
102                //                over when the underlying data is persisted i.e. this can be more
103                //                incremental or batched per application needs.
104                //
105                //              There need not be a one-to-one association between a datamodel and
106                //              a view or portion thereof. For example, multiple datamodels may
107                //              back the dijits in a view. Indeed, this may be useful where the
108                //              binding data comes from a number of data sources or queries, for
109                //              example. Just as well, dijits from multiple portions of the view
110                //              may be bound to a single datamodel.
111                //
112                //              Finally, requiring this class also enables all dijits to become data
113                //              binding aware. The data binding is commonly specified declaratively
114                //              via the "ref" property in the "data-dojo-props" attribute value.
115                //
116                //              To illustrate, the following is the "Hello World" of such data-bound
117                //              widget examples:
118                //
119                //              |       <script>
120                //              |               dojo.require("dojox.mvc");
121                //              |               dojo.require("dojo.parser");
122                //              |               var model;
123                //              |               dojo.addOnLoad(function(){
124                //              |                       model = dojox.mvc.newStatefulModel({ data : {
125                //              |                               hello : "Hello World"
126                //              |                       }});
127                //              |                       dojo.parser.parse();
128                //              |               }
129                //              |       </script>
130                //              |
131                //              |       <input id="helloInput" dojoType="dijit.form.TextBox"
132                //              |               ref="model.hello">
133                //
134                //              or
135                //
136                //              |       <script>
137                //              |               var model;
138                //              |               require(["dojox/mvc", "dojo/parser"], function(dxmvc, parser){
139                //              |                       model = dojox.mvc.newStatefulModel({ data : {
140                //              |                               hello : "Hello World"
141                //              |                       }});
142                //              |                       parser.parse();
143                //              |               });
144                //              |       </script>
145                //              |
146                //              |       <input id="helloInput" data-dojo-type="dijit.form.TextBox"
147                //              |               data-dojo-props="ref: 'model.hello'">
148                //
149                //              Such data binding awareness for dijits is added by extending the
150                //              dijit._WidgetBase class to include data binding capabilities
151                //              provided by dojox.mvc._DataBindingMixin, and this class declares a
152                //              dependency on dojox.mvc._DataBindingMixin.
153                //
154                //              The presence of a data model and the data-binding capabilities
155                //              outlined above support the flexible development of a number of MVC
156                //              patterns on the client. As an example, CRUD operations can be
157                //              supported with minimal application code.
158       
159                // data: Object
160                //              The plain JavaScript object / data structure used to initialize
161                //              this model. At any point in time, it holds the lasted saved model
162                //              state.
163                //              Either data or store property must be provided.
164                data: null,
165
166                // store: dojo.store.DataStore
167                //              The data store from where to retrieve initial data for this model.
168                //              An optional query may also be provided along with this store.
169                //              Either data or store property must be provided.
170                store: null,
171       
172                // valid: boolean
173                //              Whether this model deems the associated data to be valid.
174                valid: true,
175
176                // value: Object
177                //              The associated value (if this is a leaf node). The value of
178                //              intermediate nodes in the model is not defined.
179                value: "",
180
181                //////////////////////// PUBLIC METHODS / API ////////////////////////
182
183                reset: function(){
184                        // summary:
185                        //              Resets this data model values to its original state.
186                        //              Structural changes to the data model (such as adds or removes)
187                        //              are not restored.
188                        if(lang.isObject(this.data) && !(this.data instanceof Date) && !(this.data instanceof RegExp)){
189                                for(var x in this){
190                                        if(this[x] && lang.isFunction(this[x].reset)){
191                                                this[x].reset();
192                                        }
193                                }
194                        }else{
195                                this.set("value", this.data);
196                        }
197                },
198
199                commit: function(/*"dojo.store.DataStore?"*/ store){
200                        // summary:
201                        //              Commits this data model:
202                        //              - Saves the current state such that a subsequent reset will not
203                        //                undo any prior changes.
204                        //              - Persists client-side changes to the data store, if a store
205                        //                has been supplied as a parameter or at instantiation.
206                        //      store:
207                        //              dojo.store.DataStore
208                        //              Optional dojo.store.DataStore to use for this commit, if none
209                        //              provided but one was provided at instantiation time, that store
210                        //              will be used instead.
211                        this._commit();
212                        var ds = store || this.store;
213                        if(ds){
214                                this._saveToStore(ds);
215                        }
216                },
217
218                toPlainObject: function(){
219                        // summary:
220                        //              Produces a plain JavaScript object representation of the data
221                        //              currently within this data model.
222                        // returns:
223                        //              Object
224                        //              The plain JavaScript object representation of the data in this
225                        //              model.
226                        var ret = {};
227                        var nested = false;
228                        for(var p in this){
229                                if(this[p] && lang.isFunction(this[p].toPlainObject)){
230                                        if(!nested && typeof this.get("length") === "number"){
231                                                ret = [];
232                                        }
233                                        nested = true;
234                                        ret[p] = this[p].toPlainObject();
235                                }
236                        }
237                        if(!nested){
238                                if(this.get("length") === 0){
239                                        ret = [];
240                                }else{                         
241                                        ret = this.value;
242                                }
243                        }
244                        return ret;
245                },
246
247                add: function(/*String*/ name, /*dojo.Stateful*/ stateful){
248                        // summary:
249                        //              Adds a dojo.Stateful tree represented by the given
250                        //              dojox.mvc.StatefulModel at the given property name.
251                        //      name:
252                        //              The property name to use whose value will become the given
253                        //              dijit.Stateful tree.
254                        //      stateful:
255                        //              The dojox.mvc.StatefulModel to insert.
256                        // description:
257                        //              In case of arrays, the property names are indices passed
258                        //              as Strings. An addition of such a dojo.Stateful node
259                        //              results in right-shifting any trailing sibling nodes.
260                        var n, n1, elem, elem1, save = new StatefulModel({ data : "" });
261                        if(typeof this.get("length") === "number" && /^[0-9]+$/.test(name.toString())){
262                                n = name;
263                                if(!this.get(n)){
264                                        if(this.get("length") == 0 && n == 0){ // handle the empty array case
265                                                this.set(n, stateful);
266                                        } else {
267                                                n1 = n-1;
268                                                if(!this.get(n1)){
269                                                        throw new Error("Out of bounds insert attempted, must be contiguous.");
270                                                }
271                                                this.set(n, stateful);
272                                        }
273                                }else{
274                                        n1 = n-0+1;
275                                        elem = stateful;
276                                        elem1 = this.get(n1);
277                                        if(!elem1){
278                                                this.set(n1, elem);
279                                        }else{
280                                                do{
281                                                        this._copyStatefulProperties(elem1, save);
282                                                        this._copyStatefulProperties(elem, elem1);
283                                                        this._copyStatefulProperties(save, elem);
284                                                        this.set(n1, elem1); // for watchers
285                                                        elem1 = this.get(++n1);
286                                                }while(elem1);
287                                                this.set(n1, elem);
288                                        }
289                                }
290                                this.set("length", this.get("length") + 1);
291                        }else{
292                                this.set(name, stateful);
293                        }
294                },
295
296                remove: function(/*String*/ name){
297                        // summary:
298                        //              Removes the dojo.Stateful tree at the given property name.
299                        //      name:
300                        //              The property name from where the tree will be removed.
301                        // description:
302                        //              In case of arrays, the property names are indices passed
303                        //              as Strings. A removal of such a dojo.Stateful node
304                        //              results in left-shifting any trailing sibling nodes.
305                        var n, elem, elem1;
306                        if(typeof this.get("length") === "number" && /^[0-9]+$/.test(name.toString())){
307                                n = name;
308                                elem = this.get(n);
309                                if(!elem){
310                                        throw new Error("Out of bounds delete attempted - no such index: " + n);
311                                }else{
312                                        this._removals = this._removals || [];
313                                        this._removals.push(elem.toPlainObject());
314                                        n1 = n-0+1;
315                                        elem1 = this.get(n1);
316                                        if(!elem1){
317                                                this.set(n, undefined);
318                                                delete this[n];
319                                        }else{
320                                                while(elem1){
321                                                        this._copyStatefulProperties(elem1, elem);
322                                                        elem = this.get(n1++);
323                                                        elem1 = this.get(n1);
324                                                }
325                                                this.set(n1-1, undefined);
326                                                delete this[n1-1];
327                                        }
328                                        this.set("length", this.get("length") - 1);
329                                }
330                        }else{
331                                elem = this.get(name);
332                                if(!elem){
333                                        throw new Error("Illegal delete attempted - no such property: " + name);
334                                }else{
335                                        this._removals = this._removals || [];
336                                        this._removals.push(elem.toPlainObject());
337                                        this.set(name, undefined);
338                                        delete this[name];
339                                }
340                        }
341                },
342
343                valueOf: function(){
344                        // summary:
345                        //              Returns the value representation of the data currently within this data model.
346                        // returns:
347                        //              Object
348                        //              The object representation of the data in this model.
349                        return this.toPlainObject();
350                },
351
352                toString: function(){
353                        // summary:
354                        //              Returns the string representation of the data currently within this data model.
355                        // returns:
356                        //              String
357                        //              The object representation of the data in this model.
358                        return this.value === "" && this.data ? this.data.toString() : this.value.toString();
359                },
360
361                //////////////////////// PRIVATE INITIALIZATION METHOD ////////////////////////
362
363                constructor: function(/*Object*/ args){
364                        // summary:
365                        //              Instantiates a new data model that view components may bind to.
366                        //              This is a private constructor, use the factory method
367                        //              instead: dojox.mvc.newStatefulModel(args)
368                        //      args:
369                        //              The mixin properties.
370                        // description:
371                        //              Creates a tree of dojo.Stateful objects matching the initial
372                        //              data structure passed as input. The mixin property "data" is
373                        //              used to provide a plain JavaScript object directly representing
374                        //              the data structure.
375                        // tags:
376                        //              private
377                        var data = (args && "data" in args) ? args.data : this.data;
378                        this._createModel(data);
379                },
380
381                //////////////////////// PRIVATE METHODS ////////////////////////
382
383                _createModel: function(/*Object*/ obj){
384                        // summary:
385                        //              Create this data model from provided input data.
386                        //      obj:
387                        //              The input for the model, as a plain JavaScript object.
388                        // tags:
389                        //              private
390                        if(lang.isObject(obj) && !(obj instanceof Date) && !(obj instanceof RegExp) && obj !== null){
391                                for(var x in obj){
392                                        var newProp = new StatefulModel({ data : obj[x] });
393                                        this.set(x, newProp);
394                                }
395                                if(lang.isArray(obj)){
396                                        this.set("length", obj.length);
397                                }
398                        }else{
399                                this.set("value", obj);
400                        }
401                },
402
403                _commit: function(){
404                        // summary:
405                        //              Commits this data model, saves the current state into data to become the saved state,
406                        //              so a reset will not undo any prior changes. 
407                        // tags:
408                        //              private
409                        for(var x in this){
410                                if(this[x] && lang.isFunction(this[x]._commit)){
411                                        this[x]._commit();
412                                }
413                        }
414                        this.data = this.toPlainObject();
415                },
416
417                _saveToStore: function(/*"dojo.store.DataStore"*/ store){
418                        // summary:
419                        //              Commit the current values to the data store:
420                        //              - remove() any deleted entries
421                        //              - put() any new or updated entries
422                        //      store:
423                        //              dojo.store.DataStore to use for this commit.
424                        // tags:
425                        //              private
426                        if(this._removals){
427                                array.forEach(this._removals, function(d){
428                                        store.remove(store.getIdentity(d));
429                                }, this);
430                                delete this._removals;
431                        }
432                        var dataToCommit = this.toPlainObject();
433                        if(lang.isArray(dataToCommit)){
434                                array.forEach(dataToCommit, function(d){
435                                        store.put(d);
436                                }, this);
437                        }else{
438                                store.put(dataToCommit);
439                        }
440                },
441
442                _copyStatefulProperties: function(/*dojo.Stateful*/ src, /*dojo.Stateful*/ dest){
443                        // summary:
444                        //              Copy only the dojo.Stateful properties from src to dest (uses
445                        //              duck typing).
446                        //      src:
447                        //              The source object for the copy.
448                        //      dest:
449                        //              The target object of the copy.
450                        // tags:
451                        //              private
452                        for(var x in src){
453                                var o = src.get(x);
454                                if(o && lang.isObject(o) && lang.isFunction(o.get)){
455                                        dest.set(x, o);
456                                }
457                        }
458                }
459        });
460
461        return StatefulModel;
462});
Note: See TracBrowser for help on using the repository browser.