webui-aria2/js/libs/dojox/mvc/StatefulModel.js.uncompressed.js
2012-05-01 19:52:07 +08:00

464 lines
15 KiB
JavaScript

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