_DataBindingMixin.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. define("dojox/mvc/_DataBindingMixin", [
  2. "dojo/_base/lang",
  3. "dojo/_base/array",
  4. "dojo/_base/declare",
  5. "dojo/Stateful",
  6. "dijit/registry"
  7. ], function(lang, array, declare, Stateful, registry){
  8. /*=====
  9. registry = dijit.registry;
  10. =====*/
  11. return declare("dojox.mvc._DataBindingMixin", null, {
  12. // summary:
  13. // Provides the ability for dijits or custom view components to become
  14. // data binding aware.
  15. //
  16. // description:
  17. // Data binding awareness enables dijits or other view layer
  18. // components to bind to locations within a client-side data model,
  19. // which is commonly an instance of the dojox.mvc.StatefulModel class. A
  20. // bind is a bi-directional update mechanism which is capable of
  21. // synchronizing value changes between the bound dijit or other view
  22. // component and the specified location within the data model, as well
  23. // as changes to other properties such as "valid", "required",
  24. // "readOnly" etc.
  25. //
  26. // The data binding is commonly specified declaratively via the "ref"
  27. // property in the "data-dojo-props" attribute value.
  28. //
  29. // Consider the following simple example:
  30. //
  31. // | <script>
  32. // | var model;
  33. // | require(["dijit/StatefulModel", "dojo/parser"], function(StatefulModel, parser){
  34. // | model = new StatefulModel({ data : {
  35. // | hello : "Hello World"
  36. // | }});
  37. // | parser.parse();
  38. // | });
  39. // | </script>
  40. // |
  41. // | <input id="hello1" data-dojo-type="dijit.form.TextBox"
  42. // | data-dojo-props="ref: model.hello"></input>
  43. // |
  44. // | <input id="hello2" data-dojo-type="dijit.form.TextBox"
  45. // | data-dojo-props="ref: model.hello"></input>
  46. //
  47. // In the above example, both dijit.form.TextBox instances (with IDs
  48. // "hello1" and "hello2" respectively) are bound to the same reference
  49. // location in the data model i.e. "hello" via the "ref" expression
  50. // "model.hello". Both will have an initial value of "Hello World".
  51. // Thereafter, a change in the value of either of the two textboxes
  52. // will cause an update of the value in the data model at location
  53. // "hello" which will in turn cause a matching update of the value in
  54. // the other textbox.
  55. // ref: String||dojox.mvc.StatefulModel
  56. // The value of the data binding expression passed declaratively by
  57. // the developer. This usually references a location within an
  58. // existing datamodel and may be a relative reference based on the
  59. // parent / container data binding (dot-separated string).
  60. ref: null,
  61. /*=====
  62. // binding: [readOnly] dojox.mvc.StatefulModel
  63. // The read only value of the resolved data binding for this widget.
  64. // This may be a result of resolving various relative refs along
  65. // the parent axis.
  66. binding: null,
  67. =====*/
  68. //////////////////////// PUBLIC METHODS ////////////////////////
  69. isValid: function(){
  70. // summary:
  71. // Returns the validity of the data binding.
  72. // returns:
  73. // Boolean
  74. // The validity associated with the data binding.
  75. // description:
  76. // This function is meant to provide an API bridge to the dijit API.
  77. // Validity of data-bound dijits is a function of multiple concerns:
  78. // - The validity of the value as ascertained by the data binding
  79. // and constraints specified in the data model (usually semantic).
  80. // - The validity of the value as ascertained by the widget itself
  81. // based on widget constraints (usually syntactic).
  82. // In order for dijits to function correctly in data-bound
  83. // environments, it is imperative that their isValid() functions
  84. // assess the model validity of the data binding via the
  85. // this.inherited(arguments) hierarchy and declare any values
  86. // failing the test as invalid.
  87. return this.get("binding") ? this.get("binding").get("valid") : true;
  88. },
  89. //////////////////////// LIFECYCLE METHODS ////////////////////////
  90. _dbstartup: function(){
  91. // summary:
  92. // Tie data binding initialization into the widget lifecycle, at
  93. // widget startup.
  94. // tags:
  95. // private
  96. if(this._databound){
  97. return;
  98. }
  99. this._unwatchArray(this._viewWatchHandles);
  100. // add 2 new view watches, active only after widget has started up
  101. this._viewWatchHandles = [
  102. // 1. data binding refs
  103. this.watch("ref", function(name, old, current){
  104. if(this._databound){
  105. this._setupBinding();
  106. }
  107. }),
  108. // 2. widget values
  109. this.watch("value", function(name, old, current){
  110. if(this._databound){
  111. var binding = this.get("binding");
  112. if(binding){
  113. // dont set value if the valueOf current and old match.
  114. if(!((current && old) && (old.valueOf() === current.valueOf()))){
  115. binding.set("value", current);
  116. }
  117. }
  118. }
  119. })
  120. ];
  121. this._beingBound = true;
  122. this._setupBinding();
  123. delete this._beingBound;
  124. this._databound = true;
  125. },
  126. //////////////////////// PRIVATE METHODS ////////////////////////
  127. _setupBinding: function(parentBinding){
  128. // summary:
  129. // Calculate and set the dojo.Stateful data binding for the
  130. // associated dijit or custom view component.
  131. // parentBinding:
  132. // The binding of this widget/view component's data-bound parent,
  133. // if available.
  134. // description:
  135. // The declarative data binding reference may be specified in two
  136. // ways via markup:
  137. // - For older style documents (non validating), controls may use
  138. // the "ref" attribute to specify the data binding reference
  139. // (String).
  140. // - For validating documents using the new Dojo parser, controls
  141. // may specify the data binding reference (String) as the "ref"
  142. // property specified in the data-dojo-props attribute.
  143. // Once the ref value is obtained using either of the above means,
  144. // the binding is set up for this control and its required, readOnly
  145. // etc. properties are refreshed.
  146. // The data binding may be specified as a direct reference to the
  147. // dojo.Stateful model node or as a string relative to its DOM
  148. // parent or another widget.
  149. // There are three ways in which the data binding node reference is
  150. // calculated when specified as a string:
  151. // - If an explicit parent widget is specified, the binding is
  152. // calculated relative to the parent widget's data binding.
  153. // - For any dijits that specify a data binding reference,
  154. // we walk up their DOM hierarchy to obtain the first container
  155. // dijit that has a data binding set up and use the reference String
  156. // as a property name relative to the parent's data binding context.
  157. // - If no such parent is found i.e. for the outermost container
  158. // dijits that specify a data binding reference, the binding is
  159. // calculated by treating the reference String as an expression and
  160. // evaluating it to obtain the dojo.Stateful node in the datamodel.
  161. // This method throws an Error in these two conditions:
  162. // - The ref is an expression i.e. outermost bound dijit, but the
  163. // expression evaluation fails.
  164. // - The calculated binding turns out to not be an instance of a
  165. // dojo.Stateful node.
  166. // tags:
  167. // private
  168. if(!this.ref){
  169. return; // nothing to do here
  170. }
  171. var ref = this.ref, pw, pb, binding;
  172. // Now compute the model node to bind to
  173. if(ref && lang.isFunction(ref.toPlainObject)){ // programmatic instantiation or direct ref
  174. binding = ref;
  175. }else if(/^\s*expr\s*:\s*/.test(ref)){ // declarative: refs as dot-separated expressions
  176. ref = ref.replace(/^\s*expr\s*:\s*/, "");
  177. binding = lang.getObject(ref);
  178. }else if(/^\s*rel\s*:\s*/.test(ref)){ // declarative: refs relative to parent binding, dot-separated
  179. ref = ref.replace(/^\s*rel\s*:\s*/, "");
  180. parentBinding = parentBinding || this._getParentBindingFromDOM();
  181. if(parentBinding){
  182. binding = lang.getObject("" + ref, false, parentBinding);
  183. }
  184. }else if(/^\s*widget\s*:\s*/.test(ref)){ // declarative: refs relative to another dijits binding, dot-separated
  185. ref = ref.replace(/^\s*widget\s*:\s*/, "");
  186. var tokens = ref.split(".");
  187. if(tokens.length == 1){
  188. binding = registry.byId(ref).get("binding");
  189. }else{
  190. pb = registry.byId(tokens.shift()).get("binding");
  191. binding = lang.getObject(tokens.join("."), false, pb);
  192. }
  193. }else{ // defaults: outermost refs are expressions, nested are relative to parents
  194. parentBinding = parentBinding || this._getParentBindingFromDOM();
  195. if(parentBinding){
  196. binding = lang.getObject("" + ref, false, parentBinding);
  197. }else{
  198. try{
  199. if(lang.getObject(ref) instanceof Stateful){
  200. binding = lang.getObject(ref);
  201. }
  202. }catch(err){
  203. if(ref.indexOf("${") == -1){ // Ignore templated refs such as in repeat body
  204. throw new Error("dojox.mvc._DataBindingMixin: '" + this.domNode +
  205. "' widget with illegal ref expression: '" + ref + "'");
  206. }
  207. }
  208. }
  209. }
  210. if(binding){
  211. if(lang.isFunction(binding.toPlainObject)){
  212. this.binding = binding;
  213. this._updateBinding("binding", null, binding);
  214. }else{
  215. throw new Error("dojox.mvc._DataBindingMixin: '" + this.domNode +
  216. "' widget with illegal ref not evaluating to a dojo.Stateful node: '" + ref + "'");
  217. }
  218. }
  219. },
  220. _isEqual: function(one, other){
  221. // test for equality
  222. return one === other ||
  223. // test for NaN === NaN
  224. isNaN(one) && typeof one === 'number' &&
  225. isNaN(other) && typeof other === 'number';
  226. },
  227. _updateBinding: function(name, old, current){
  228. // summary:
  229. // Set the data binding to the supplied value, which must be a
  230. // dojo.Stateful node of a data model.
  231. // name:
  232. // The name of the binding property (always "binding").
  233. // old:
  234. // The old dojo.Stateful binding node of the data model.
  235. // current:
  236. // The new dojo.Stateful binding node of the data model.
  237. // description:
  238. // Applies the specified data binding to the attached widget.
  239. // Loses any prior watch registrations on the previously active
  240. // bind, registers the new one, updates data binds of any contained
  241. // widgets and also refreshes all associated properties (valid,
  242. // required etc.)
  243. // tags:
  244. // private
  245. // remove all existing watches (if there are any, there will be 5)
  246. this._unwatchArray(this._modelWatchHandles);
  247. // add 5 new model watches
  248. var binding = this.get("binding");
  249. if(binding && lang.isFunction(binding.watch)){
  250. var pThis = this;
  251. this._modelWatchHandles = [
  252. // 1. value - no default
  253. binding.watch("value", function (name, old, current){
  254. if(pThis._isEqual(old, current)){return;}
  255. if(pThis._isEqual(pThis.get('value'), current)){return;}
  256. pThis.set("value", current);
  257. }),
  258. // 2. valid - default "true"
  259. binding.watch("valid", function (name, old, current){
  260. pThis._updateProperty(name, old, current, true);
  261. if(current !== pThis.get(name)){
  262. if(pThis.validate && lang.isFunction(pThis.validate)){
  263. pThis.validate();
  264. }
  265. }
  266. }),
  267. // 3. required - default "false"
  268. binding.watch("required", function (name, old, current){
  269. pThis._updateProperty(name, old, current, false, name, current);
  270. }),
  271. // 4. readOnly - default "false"
  272. binding.watch("readOnly", function (name, old, current){
  273. pThis._updateProperty(name, old, current, false, name, current);
  274. }),
  275. // 5. relevant - default "true"
  276. binding.watch("relevant", function (name, old, current){
  277. pThis._updateProperty(name, old, current, false, "disabled", !current);
  278. })
  279. ];
  280. var val = binding.get("value");
  281. if(val != null){
  282. this.set("value", val);
  283. }
  284. }
  285. this._updateChildBindings();
  286. },
  287. _updateProperty: function(name, old, current, defaultValue, setPropName, setPropValue){
  288. // summary:
  289. // Update a binding property of the bound widget.
  290. // name:
  291. // The binding property name.
  292. // old:
  293. // The old value of the binding property.
  294. // current:
  295. // The new or current value of the binding property.
  296. // defaultValue:
  297. // The optional value to be applied as the current value of the
  298. // binding property if the current value is null.
  299. // setPropName:
  300. // The optional name of a stateful property to set on the bound
  301. // widget.
  302. // setPropValue:
  303. // The value, if an optional name is provided, for the stateful
  304. // property of the bound widget.
  305. // tags:
  306. // private
  307. if(old === current){
  308. return;
  309. }
  310. if(current === null && defaultValue !== undefined){
  311. current = defaultValue;
  312. }
  313. if(current !== this.get("binding").get(name)){
  314. this.get("binding").set(name, current);
  315. }
  316. if(setPropName){
  317. this.set(setPropName, setPropValue);
  318. }
  319. },
  320. _updateChildBindings: function(parentBind){
  321. // summary:
  322. // Update this widget's value based on the current binding and
  323. // set up the bindings of all contained widgets so as to refresh
  324. // any relative binding references.
  325. // findWidgets does not return children of widgets so need to also
  326. // update children of widgets which are not bound but may hold widgets which are.
  327. // parentBind:
  328. // The binding on the parent of a widget whose children may have bindings
  329. // which need to be updated.
  330. // tags:
  331. // private
  332. var binding = this.get("binding") || parentBind;
  333. if(binding && !this._beingBound){
  334. array.forEach(registry.findWidgets(this.domNode), function(widget){
  335. if(widget.ref && widget._setupBinding){
  336. widget._setupBinding(binding);
  337. }else{
  338. widget._updateChildBindings(binding);
  339. }
  340. });
  341. }
  342. },
  343. _getParentBindingFromDOM: function(){
  344. // summary:
  345. // Get the parent binding by traversing the DOM ancestors to find
  346. // the first enclosing data-bound widget.
  347. // returns:
  348. // The parent binding, if one exists along the DOM parent axis.
  349. // tags:
  350. // private
  351. var pn = this.domNode.parentNode, pw, pb;
  352. while(pn){
  353. pw = registry.getEnclosingWidget(pn);
  354. if(pw){
  355. pb = pw.get("binding");
  356. if(pb && lang.isFunction(pb.toPlainObject)){
  357. break;
  358. }
  359. }
  360. pn = pw ? pw.domNode.parentNode : null;
  361. }
  362. return pb;
  363. },
  364. _unwatchArray: function(watchHandles){
  365. // summary:
  366. // Given an array of watch handles, unwatch all.
  367. // watchHandles:
  368. // The array of watch handles.
  369. // tags:
  370. // private
  371. array.forEach(watchHandles, function(h){ h.unwatch(); });
  372. }
  373. });
  374. });