StatefulModel.js 15 KB

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