define([
"dojo/_base/kernel",
"dojo/_base/lang",
"dojo/_base/array",
"dojo/_base/declare",
"dojo/Stateful",
"./getStateful",
"./getPlainValue",
"./StatefulArray"
], function(kernel, lang, array, declare, Stateful, getStateful, getPlainValue, StatefulArray){
kernel.deprecated("dojox/mvc/StatefulModel", "Use dojox/mvc/getStateful, dojox/mvc/getPlainValue, dojox/mvc/StatefulArray or one of the dojox/mvc/*RefControllers instead");
var StatefulModel = declare("dojox.mvc.StatefulModel", [Stateful], {
// summary:
// Deprecated. Use dojox/mvc/getStateful, dojox/mvc/getPlainValue, dojox/mvc/StatefulArray or one of the dojox/mvc/*RefControllers instead.
// 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 = 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:
//
// a) The data model is "live" data i.e. it maintains any updates
// driven by the view on the underlying data.
//
// b) 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:
//
//
// |
// |
// |
//
// 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.
//
// tags:
// deprecated
// 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.
return getPlainValue(this, StatefulModel.getPlainValueOptions);
},
splice: function(/*Number*/ idx, /*Number*/ n){
// summary:
// Removes and then adds some elements to this array.
// Updates the removed/added elements, as well as the length, as stateful.
// idx: Number
// The index where removal/addition should be done.
// n: Number
// How many elements to be removed at idx.
// varargs: Anything[]
// The elements to be added to idx.
// returns: dojox/mvc/StatefulArray
// The removed elements.
var a = (new StatefulArray([])).splice.apply(this, lang._toArray(arguments));
for(var i = 0; i < a.length; i++){
(this._removals = this._removals || []).push(a[i].toPlainObject());
}
return a;
},
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.
if(typeof this.get("length") === "number" && /^[0-9]+$/.test(name.toString())){
if(this.get("length") < (name - 0)){
throw new Error("Out of bounds insert attempted, must be contiguous.");
}
this.splice(name - 0, 0, stateful);
}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.
if(typeof this.get("length") === "number" && /^[0-9]+$/.test(name.toString())){
if(!this.get(name)){
throw new Error("Out of bounds delete attempted - no such index: " + n);
}else{
this.splice(name - 0, 1);
}
}else{
var 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*/ data){
// summary:
// Create this data model from provided input data.
// obj:
// The input for the model, as a plain JavaScript object.
// tags:
// private
if(data != null){
data = getStateful(data, StatefulModel.getStatefulOptions);
if(lang.isArray(data)){
// Some consumers of dojox/mvc/StatefulModel inherits it via dojo/declare(), where we cannot use array inheritance technique
// (dojo/declare() does not support return value in constructor)
this.length = 0;
[].splice.apply(this, data);
}else if(lang.isObject(data)){
for(var s in data){
if(data.hasOwnProperty(s)){
this[s] = data[s];
}
}
}else{
this.set("value", data);
}
}
},
_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);
}
}
});
lang.mixin(StatefulModel, {
getStatefulOptions: {
// summary:
// An object that defines how model object should be created from plain object hierarchy.
getType: function(/*Anything*/ v){
// summary:
// Returns the type of the given value.
// v: Anything
// The value.
return lang.isArray(v) ? "array" : v != null && {}.toString.call(v) == "[object Object]" ? "object" : "value"; // String
},
getStatefulArray: function(/*Anything[]*/ a){
// summary:
// Create a stateful array from a plain array.
// a: Anything[]
// The plain array.
var _self = this, statefularray = lang.mixin(new StatefulArray(array.map(a, function(item){ return getStateful(item, _self); })));
for(var s in StatefulModel.prototype){
if(s != "set"){ statefularray[s] = StatefulModel.prototype[s]; }
}
statefularray.data = a;
return statefularray;
},
getStatefulObject: function(/*Object*/ o){
// summary:
// Create a stateful object from a plain object.
// o: Object
// The plain object.
var object = new StatefulModel();
object.data = o;
for(var s in o){
object.set(s, getStateful(o[s], this));
}
return object; // dojox/mvc/StatefulModel
},
getStatefulValue: function(/*Anything*/ v){
// summary:
// Create a stateful value from a plain value.
// v: Anything
// The plain value.
var value = new StatefulModel();
value.data = v;
value.set("value", v);
return value;
}
},
getPlainValueOptions: {
// summary:
// An object that defines how plain value should be created from model object.
getType: function(/*Anything*/ v){
// summary:
// Returns the type of the given value.
// v: Anything
// The value.
if(lang.isArray(v)){ return "array"; }
if(lang.isObject(v)){ // Primitive values may have their own properties
for(var s in v){
if(v.hasOwnProperty(s) && s != "value" && (v[s] || {}).get && (v[s] || {}).watch){
return "object";
}
}
}
return "value";
},
getPlainArray: function(/*dojox/mvc/StatefulArray*/ a){
return array.map(a, function(item){ return getPlainValue(item, this); }, this);
},
getPlainObject: function(/*dojox/mvc/StatefulModel*/ o){
var plain = {};
for(var s in o){
if(s == "_watchCallbacks" || (s in StatefulModel.prototype)){ continue; }
plain[s] = getPlainValue(o[s], this);
}
return plain;
},
getPlainValue: function(/*Anything*/ v){
return (v || {}).set && (v || {}).watch ? getPlainValue(v.value, this) : v;
}
}
});
return StatefulModel;
});