_FormMixin.js 15 KB

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