_FormMixin.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. define("dijit/form/_FormMixin", [
  2. "dojo/_base/array", // array.every array.filter array.forEach array.indexOf array.map
  3. "dojo/_base/declare", // declare
  4. "dojo/_base/kernel", // kernel.deprecated
  5. "dojo/_base/lang", // lang.hitch lang.isArray
  6. "dojo/window" // winUtils.scrollIntoView
  7. ], function(array, declare, kernel, lang, winUtils){
  8. // module:
  9. // dijit/form/_FormMixin
  10. // summary:
  11. // Mixin for containers of form widgets (i.e. widgets that represent a single value
  12. // and can be children of a <form> node or dijit.form.Form widget)
  13. return declare("dijit.form._FormMixin", null, {
  14. // summary:
  15. // Mixin for containers of form widgets (i.e. widgets that represent a single value
  16. // and can be children of a <form> node or dijit.form.Form widget)
  17. // description:
  18. // Can extract all the form widgets
  19. // values and combine them into a single javascript object, or alternately
  20. // take such an object and set the values for all the contained
  21. // form widgets
  22. /*=====
  23. // value: Object
  24. // Name/value hash for each child widget with a name and value.
  25. // Child widgets without names are not part of the hash.
  26. //
  27. // If there are multiple child widgets w/the same name, value is an array,
  28. // unless they are radio buttons in which case value is a scalar (since only
  29. // one radio button can be checked at a time).
  30. //
  31. // If a child widget's name is a dot separated list (like a.b.c.d), it's a nested structure.
  32. //
  33. // Example:
  34. // | { name: "John Smith", interests: ["sports", "movies"] }
  35. =====*/
  36. // state: [readonly] String
  37. // Will be "Error" if one or more of the child widgets has an invalid value,
  38. // "Incomplete" if not all of the required child widgets are filled in. Otherwise, "",
  39. // which indicates that the form is ready to be submitted.
  40. state: "",
  41. // TODO:
  42. // * Repeater
  43. // * better handling for arrays. Often form elements have names with [] like
  44. // * people[3].sex (for a list of people [{name: Bill, sex: M}, ...])
  45. //
  46. //
  47. _getDescendantFormWidgets: function(/*dijit._WidgetBase[]?*/ children){
  48. // summary:
  49. // Returns all form widget descendants, searching through non-form child widgets like BorderContainer
  50. var res = [];
  51. array.forEach(children || this.getChildren(), function(child){
  52. if("value" in child){
  53. res.push(child);
  54. }else{
  55. res = res.concat(this._getDescendantFormWidgets(child.getChildren()));
  56. }
  57. }, this);
  58. return res;
  59. },
  60. reset: function(){
  61. array.forEach(this._getDescendantFormWidgets(), function(widget){
  62. if(widget.reset){
  63. widget.reset();
  64. }
  65. });
  66. },
  67. validate: function(){
  68. // summary:
  69. // returns if the form is valid - same as isValid - but
  70. // provides a few additional (ui-specific) features.
  71. // 1 - it will highlight any sub-widgets that are not
  72. // valid
  73. // 2 - it will call focus() on the first invalid
  74. // sub-widget
  75. var didFocus = false;
  76. return array.every(array.map(this._getDescendantFormWidgets(), function(widget){
  77. // Need to set this so that "required" widgets get their
  78. // state set.
  79. widget._hasBeenBlurred = true;
  80. var valid = widget.disabled || !widget.validate || widget.validate();
  81. if(!valid && !didFocus){
  82. // Set focus of the first non-valid widget
  83. winUtils.scrollIntoView(widget.containerNode || widget.domNode);
  84. widget.focus();
  85. didFocus = true;
  86. }
  87. return valid;
  88. }), function(item){ return item; });
  89. },
  90. setValues: function(val){
  91. kernel.deprecated(this.declaredClass+"::setValues() is deprecated. Use set('value', val) instead.", "", "2.0");
  92. return this.set('value', val);
  93. },
  94. _setValueAttr: function(/*Object*/ obj){
  95. // summary:
  96. // Fill in form values from according to an Object (in the format returned by get('value'))
  97. // generate map from name --> [list of widgets with that name]
  98. var map = { };
  99. array.forEach(this._getDescendantFormWidgets(), function(widget){
  100. if(!widget.name){ return; }
  101. var entry = map[widget.name] || (map[widget.name] = [] );
  102. entry.push(widget);
  103. });
  104. for(var name in map){
  105. if(!map.hasOwnProperty(name)){
  106. continue;
  107. }
  108. var widgets = map[name], // array of widgets w/this name
  109. values = lang.getObject(name, false, obj); // list of values for those widgets
  110. if(values === undefined){
  111. continue;
  112. }
  113. if(!lang.isArray(values)){
  114. values = [ values ];
  115. }
  116. if(typeof widgets[0].checked == 'boolean'){
  117. // for checkbox/radio, values is a list of which widgets should be checked
  118. array.forEach(widgets, function(w){
  119. w.set('value', array.indexOf(values, w.value) != -1);
  120. });
  121. }else if(widgets[0].multiple){
  122. // it takes an array (e.g. multi-select)
  123. widgets[0].set('value', values);
  124. }else{
  125. // otherwise, values is a list of values to be assigned sequentially to each widget
  126. array.forEach(widgets, function(w, i){
  127. w.set('value', values[i]);
  128. });
  129. }
  130. }
  131. /***
  132. * TODO: code for plain input boxes (this shouldn't run for inputs that are part of widgets)
  133. array.forEach(this.containerNode.elements, function(element){
  134. if(element.name == ''){return}; // like "continue"
  135. var namePath = element.name.split(".");
  136. var myObj=obj;
  137. var name=namePath[namePath.length-1];
  138. for(var j=1,len2=namePath.length;j<len2;++j){
  139. var p=namePath[j - 1];
  140. // repeater support block
  141. var nameA=p.split("[");
  142. if(nameA.length > 1){
  143. if(typeof(myObj[nameA[0]]) == "undefined"){
  144. myObj[nameA[0]]=[ ];
  145. } // if
  146. nameIndex=parseInt(nameA[1]);
  147. if(typeof(myObj[nameA[0]][nameIndex]) == "undefined"){
  148. myObj[nameA[0]][nameIndex] = { };
  149. }
  150. myObj=myObj[nameA[0]][nameIndex];
  151. continue;
  152. } // repeater support ends
  153. if(typeof(myObj[p]) == "undefined"){
  154. myObj=undefined;
  155. break;
  156. };
  157. myObj=myObj[p];
  158. }
  159. if(typeof(myObj) == "undefined"){
  160. return; // like "continue"
  161. }
  162. if(typeof(myObj[name]) == "undefined" && this.ignoreNullValues){
  163. return; // like "continue"
  164. }
  165. // TODO: widget values (just call set('value', ...) on the widget)
  166. // TODO: maybe should call dojo.getNodeProp() instead
  167. switch(element.type){
  168. case "checkbox":
  169. element.checked = (name in myObj) &&
  170. array.some(myObj[name], function(val){ return val == element.value; });
  171. break;
  172. case "radio":
  173. element.checked = (name in myObj) && myObj[name] == element.value;
  174. break;
  175. case "select-multiple":
  176. element.selectedIndex=-1;
  177. array.forEach(element.options, function(option){
  178. option.selected = array.some(myObj[name], function(val){ return option.value == val; });
  179. });
  180. break;
  181. case "select-one":
  182. element.selectedIndex="0";
  183. array.forEach(element.options, function(option){
  184. option.selected = option.value == myObj[name];
  185. });
  186. break;
  187. case "hidden":
  188. case "text":
  189. case "textarea":
  190. case "password":
  191. element.value = myObj[name] || "";
  192. break;
  193. }
  194. });
  195. */
  196. // Note: no need to call this._set("value", ...) as the child updates will trigger onChange events
  197. // which I am monitoring.
  198. },
  199. getValues: function(){
  200. kernel.deprecated(this.declaredClass+"::getValues() is deprecated. Use get('value') instead.", "", "2.0");
  201. return this.get('value');
  202. },
  203. _getValueAttr: function(){
  204. // summary:
  205. // Returns Object representing form values. See description of `value` for details.
  206. // description:
  207. // The value is updated into this.value every time a child has an onChange event,
  208. // so in the common case this function could just return this.value. However,
  209. // that wouldn't work when:
  210. //
  211. // 1. User presses return key to submit a form. That doesn't fire an onchange event,
  212. // and even if it did it would come too late due to the setTimeout(..., 0) in _handleOnChange()
  213. //
  214. // 2. app for some reason calls this.get("value") while the user is typing into a
  215. // form field. Not sure if that case needs to be supported or not.
  216. // get widget values
  217. var obj = { };
  218. array.forEach(this._getDescendantFormWidgets(), function(widget){
  219. var name = widget.name;
  220. if(!name || widget.disabled){ return; }
  221. // Single value widget (checkbox, radio, or plain <input> type widget)
  222. var value = widget.get('value');
  223. // Store widget's value(s) as a scalar, except for checkboxes which are automatically arrays
  224. if(typeof widget.checked == 'boolean'){
  225. if(/Radio/.test(widget.declaredClass)){
  226. // radio button
  227. if(value !== false){
  228. lang.setObject(name, value, obj);
  229. }else{
  230. // give radio widgets a default of null
  231. value = lang.getObject(name, false, obj);
  232. if(value === undefined){
  233. lang.setObject(name, null, obj);
  234. }
  235. }
  236. }else{
  237. // checkbox/toggle button
  238. var ary=lang.getObject(name, false, obj);
  239. if(!ary){
  240. ary=[];
  241. lang.setObject(name, ary, obj);
  242. }
  243. if(value !== false){
  244. ary.push(value);
  245. }
  246. }
  247. }else{
  248. var prev=lang.getObject(name, false, obj);
  249. if(typeof prev != "undefined"){
  250. if(lang.isArray(prev)){
  251. prev.push(value);
  252. }else{
  253. lang.setObject(name, [prev, value], obj);
  254. }
  255. }else{
  256. // unique name
  257. lang.setObject(name, value, obj);
  258. }
  259. }
  260. });
  261. /***
  262. * code for plain input boxes (see also domForm.formToObject, can we use that instead of this code?
  263. * but it doesn't understand [] notation, presumably)
  264. var obj = { };
  265. array.forEach(this.containerNode.elements, function(elm){
  266. if(!elm.name) {
  267. return; // like "continue"
  268. }
  269. var namePath = elm.name.split(".");
  270. var myObj=obj;
  271. var name=namePath[namePath.length-1];
  272. for(var j=1,len2=namePath.length;j<len2;++j){
  273. var nameIndex = null;
  274. var p=namePath[j - 1];
  275. var nameA=p.split("[");
  276. if(nameA.length > 1){
  277. if(typeof(myObj[nameA[0]]) == "undefined"){
  278. myObj[nameA[0]]=[ ];
  279. } // if
  280. nameIndex=parseInt(nameA[1]);
  281. if(typeof(myObj[nameA[0]][nameIndex]) == "undefined"){
  282. myObj[nameA[0]][nameIndex] = { };
  283. }
  284. }else if(typeof(myObj[nameA[0]]) == "undefined"){
  285. myObj[nameA[0]] = { }
  286. } // if
  287. if(nameA.length == 1){
  288. myObj=myObj[nameA[0]];
  289. }else{
  290. myObj=myObj[nameA[0]][nameIndex];
  291. } // if
  292. } // for
  293. if((elm.type != "select-multiple" && elm.type != "checkbox" && elm.type != "radio") || (elm.type == "radio" && elm.checked)){
  294. if(name == name.split("[")[0]){
  295. myObj[name]=elm.value;
  296. }else{
  297. // can not set value when there is no name
  298. }
  299. }else if(elm.type == "checkbox" && elm.checked){
  300. if(typeof(myObj[name]) == 'undefined'){
  301. myObj[name]=[ ];
  302. }
  303. myObj[name].push(elm.value);
  304. }else if(elm.type == "select-multiple"){
  305. if(typeof(myObj[name]) == 'undefined'){
  306. myObj[name]=[ ];
  307. }
  308. for(var jdx=0,len3=elm.options.length; jdx<len3; ++jdx){
  309. if(elm.options[jdx].selected){
  310. myObj[name].push(elm.options[jdx].value);
  311. }
  312. }
  313. } // if
  314. name=undefined;
  315. }); // forEach
  316. ***/
  317. return obj;
  318. },
  319. isValid: function(){
  320. // summary:
  321. // Returns true if all of the widgets are valid.
  322. // Deprecated, will be removed in 2.0. Use get("state") instead.
  323. return this.state == "";
  324. },
  325. onValidStateChange: function(/*Boolean*/ /*===== isValid =====*/){
  326. // summary:
  327. // Stub function to connect to if you want to do something
  328. // (like disable/enable a submit button) when the valid
  329. // state changes on the form as a whole.
  330. //
  331. // Deprecated. Will be removed in 2.0. Use watch("state", ...) instead.
  332. },
  333. _getState: function(){
  334. // summary:
  335. // Compute what this.state should be based on state of children
  336. var states = array.map(this._descendants, function(w){
  337. return w.get("state") || "";
  338. });
  339. return array.indexOf(states, "Error") >= 0 ? "Error" :
  340. array.indexOf(states, "Incomplete") >= 0 ? "Incomplete" : "";
  341. },
  342. disconnectChildren: function(){
  343. // summary:
  344. // Remove connections to monitor changes to children's value, error state, and disabled state,
  345. // in order to update Form.value and Form.state.
  346. array.forEach(this._childConnections || [], lang.hitch(this, "disconnect"));
  347. array.forEach(this._childWatches || [], function(w){ w.unwatch(); });
  348. },
  349. connectChildren: function(/*Boolean*/ inStartup){
  350. // summary:
  351. // Setup connections to monitor changes to children's value, error state, and disabled state,
  352. // in order to update Form.value and Form.state.
  353. //
  354. // You can call this function directly, ex. in the event that you
  355. // programmatically add a widget to the form *after* the form has been
  356. // initialized.
  357. var _this = this;
  358. // Remove old connections, if any
  359. this.disconnectChildren();
  360. this._descendants = this._getDescendantFormWidgets();
  361. // (Re)set this.value and this.state. Send watch() notifications but not on startup.
  362. var set = inStartup ? function(name, val){ _this[name] = val; } : lang.hitch(this, "_set");
  363. set("value", this.get("value"));
  364. set("state", this._getState());
  365. // Monitor changes to error state and disabled state in order to update
  366. // Form.state
  367. var conns = (this._childConnections = []),
  368. watches = (this._childWatches = []);
  369. array.forEach(array.filter(this._descendants,
  370. function(item){ return item.validate; }
  371. ),
  372. function(widget){
  373. // We are interested in whenever the widget changes validity state - or
  374. // whenever the disabled attribute on that widget is changed.
  375. array.forEach(["state", "disabled"], function(attr){
  376. watches.push(widget.watch(attr, function(){
  377. _this.set("state", _this._getState());
  378. }));
  379. });
  380. });
  381. // And monitor calls to child.onChange so we can update this.value
  382. var onChange = function(){
  383. // summary:
  384. // Called when child's value or disabled state changes
  385. // Use setTimeout() to collapse value changes in multiple children into a single
  386. // update to my value. Multiple updates will occur on:
  387. // 1. Form.set()
  388. // 2. Form.reset()
  389. // 3. user selecting a radio button (which will de-select another radio button,
  390. // causing two onChange events)
  391. if(_this._onChangeDelayTimer){
  392. clearTimeout(_this._onChangeDelayTimer);
  393. }
  394. _this._onChangeDelayTimer = setTimeout(function(){
  395. delete _this._onChangeDelayTimer;
  396. _this._set("value", _this.get("value"));
  397. }, 10);
  398. };
  399. array.forEach(
  400. array.filter(this._descendants, function(item){ return item.onChange; } ),
  401. function(widget){
  402. // When a child widget's value changes,
  403. // the efficient thing to do is to just update that one attribute in this.value,
  404. // but that gets a little complicated when a checkbox is checked/unchecked
  405. // since this.value["checkboxName"] contains an array of all the checkboxes w/the same name.
  406. // Doing simple thing for now.
  407. conns.push(_this.connect(widget, "onChange", onChange));
  408. // Disabling/enabling a child widget should remove it's value from this.value.
  409. // Again, this code could be more efficient, doing simple thing for now.
  410. watches.push(widget.watch("disabled", onChange));
  411. }
  412. );
  413. },
  414. startup: function(){
  415. this.inherited(arguments);
  416. // Initialize value and valid/invalid state tracking. Needs to be done in startup()
  417. // so that children are initialized.
  418. this.connectChildren(true);
  419. // Make state change call onValidStateChange(), will be removed in 2.0
  420. this.watch("state", function(attr, oldVal, newVal){ this.onValidStateChange(newVal == ""); });
  421. },
  422. destroy: function(){
  423. this.disconnectChildren();
  424. this.inherited(arguments);
  425. }
  426. });
  427. });