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
7 "dojo/window" // winUtils.scrollIntoView
8 ], function(array, declare, kernel, lang, on, winUtils){
11 // dijit/form/_FormMixin
13 return declare("dijit.form._FormMixin", null, {
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)
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
25 // Name/value hash for each child widget with a name and value.
26 // Child widgets without names are not part of the hash.
28 // If there are multiple child widgets w/the same name, value is an array,
29 // unless they are radio buttons in which case value is a scalar (since only
30 // one radio button can be checked at a time).
32 // If a child widget's name is a dot separated list (like a.b.c.d), it's a nested structure.
35 // | { name: "John Smith", interests: ["sports", "movies"] }
38 // state: [readonly] String
39 // Will be "Error" if one or more of the child widgets has an invalid value,
40 // "Incomplete" if not all of the required child widgets are filled in. Otherwise, "",
41 // which indicates that the form is ready to be submitted.
46 // * better handling for arrays. Often form elements have names with [] like
47 // * people[3].sex (for a list of people [{name: Bill, sex: M}, ...])
50 _getDescendantFormWidgets: function(/*dijit/_WidgetBase[]?*/ children){
52 // Returns all form widget descendants, searching through non-form child widgets like BorderContainer
54 array.forEach(children || this.getChildren(), function(child){
58 res = res.concat(this._getDescendantFormWidgets(child.getChildren()));
65 array.forEach(this._getDescendantFormWidgets(), function(widget){
74 // returns if the form is valid - same as isValid - but
75 // provides a few additional (ui-specific) features:
77 // 1. it will highlight any sub-widgets that are not valid
78 // 2. it will call focus() on the first invalid sub-widget
80 return array.every(array.map(this._getDescendantFormWidgets(), function(widget){
81 // Need to set this so that "required" widgets get their
83 widget._hasBeenBlurred = true;
84 var valid = widget.disabled || !widget.validate || widget.validate();
85 if(!valid && !didFocus){
86 // Set focus of the first non-valid widget
87 winUtils.scrollIntoView(widget.containerNode || widget.domNode);
92 }), function(item){ return item; });
95 setValues: function(val){
96 kernel.deprecated(this.declaredClass+"::setValues() is deprecated. Use set('value', val) instead.", "", "2.0");
97 return this.set('value', val);
99 _setValueAttr: function(/*Object*/ obj){
101 // Fill in form values from according to an Object (in the format returned by get('value'))
103 // generate map from name --> [list of widgets with that name]
105 array.forEach(this._getDescendantFormWidgets(), function(widget){
106 if(!widget.name){ return; }
107 var entry = map[widget.name] || (map[widget.name] = [] );
111 for(var name in map){
112 if(!map.hasOwnProperty(name)){
115 var widgets = map[name], // array of widgets w/this name
116 values = lang.getObject(name, false, obj); // list of values for those widgets
118 if(values === undefined){
121 if(!lang.isArray(values)){
124 if(typeof widgets[0].checked == 'boolean'){
125 // for checkbox/radio, values is a list of which widgets should be checked
126 array.forEach(widgets, function(w){
127 w.set('value', array.indexOf(values, w.value) != -1);
129 }else if(widgets[0].multiple){
130 // it takes an array (e.g. multi-select)
131 widgets[0].set('value', values);
133 // otherwise, values is a list of values to be assigned sequentially to each widget
134 array.forEach(widgets, function(w, i){
135 w.set('value', values[i]);
141 * TODO: code for plain input boxes (this shouldn't run for inputs that are part of widgets)
143 array.forEach(this.containerNode.elements, function(element){
144 if(element.name == ''){return}; // like "continue"
145 var namePath = element.name.split(".");
147 var name=namePath[namePath.length-1];
148 for(var j=1,len2=namePath.length;j<len2;++j){
149 var p=namePath[j - 1];
150 // repeater support block
151 var nameA=p.split("[");
152 if(nameA.length > 1){
153 if(typeof(myObj[nameA[0]]) == "undefined"){
157 nameIndex=parseInt(nameA[1]);
158 if(typeof(myObj[nameA[0]][nameIndex]) == "undefined"){
159 myObj[nameA[0]][nameIndex] = { };
161 myObj=myObj[nameA[0]][nameIndex];
163 } // repeater support ends
165 if(typeof(myObj[p]) == "undefined"){
172 if(typeof(myObj) == "undefined"){
173 return; // like "continue"
175 if(typeof(myObj[name]) == "undefined" && this.ignoreNullValues){
176 return; // like "continue"
179 // TODO: widget values (just call set('value', ...) on the widget)
181 // TODO: maybe should call dojo.getNodeProp() instead
182 switch(element.type){
184 element.checked = (name in myObj) &&
185 array.some(myObj[name], function(val){ return val == element.value; });
188 element.checked = (name in myObj) && myObj[name] == element.value;
190 case "select-multiple":
191 element.selectedIndex=-1;
192 array.forEach(element.options, function(option){
193 option.selected = array.some(myObj[name], function(val){ return option.value == val; });
197 element.selectedIndex="0";
198 array.forEach(element.options, function(option){
199 option.selected = option.value == myObj[name];
206 element.value = myObj[name] || "";
212 // Note: no need to call this._set("value", ...) as the child updates will trigger onChange events
213 // which I am monitoring.
216 getValues: function(){
217 kernel.deprecated(this.declaredClass+"::getValues() is deprecated. Use get('value') instead.", "", "2.0");
218 return this.get('value');
220 _getValueAttr: function(){
222 // Returns Object representing form values. See description of `value` for details.
225 // The value is updated into this.value every time a child has an onChange event,
226 // so in the common case this function could just return this.value. However,
227 // that wouldn't work when:
229 // 1. User presses return key to submit a form. That doesn't fire an onchange event,
230 // and even if it did it would come too late due to the defer(...) in _handleOnChange()
232 // 2. app for some reason calls this.get("value") while the user is typing into a
233 // form field. Not sure if that case needs to be supported or not.
237 array.forEach(this._getDescendantFormWidgets(), function(widget){
238 var name = widget.name;
239 if(!name || widget.disabled){ return; }
241 // Single value widget (checkbox, radio, or plain <input> type widget)
242 var value = widget.get('value');
244 // Store widget's value(s) as a scalar, except for checkboxes which are automatically arrays
245 if(typeof widget.checked == 'boolean'){
246 if(/Radio/.test(widget.declaredClass)){
249 lang.setObject(name, value, obj);
251 // give radio widgets a default of null
252 value = lang.getObject(name, false, obj);
253 if(value === undefined){
254 lang.setObject(name, null, obj);
258 // checkbox/toggle button
259 var ary=lang.getObject(name, false, obj);
262 lang.setObject(name, ary, obj);
269 var prev=lang.getObject(name, false, obj);
270 if(typeof prev != "undefined"){
271 if(lang.isArray(prev)){
274 lang.setObject(name, [prev, value], obj);
278 lang.setObject(name, value, obj);
284 * code for plain input boxes (see also domForm.formToObject, can we use that instead of this code?
285 * but it doesn't understand [] notation, presumably)
287 array.forEach(this.containerNode.elements, function(elm){
289 return; // like "continue"
291 var namePath = elm.name.split(".");
293 var name=namePath[namePath.length-1];
294 for(var j=1,len2=namePath.length;j<len2;++j){
295 var nameIndex = null;
296 var p=namePath[j - 1];
297 var nameA=p.split("[");
298 if(nameA.length > 1){
299 if(typeof(myObj[nameA[0]]) == "undefined"){
302 nameIndex=parseInt(nameA[1]);
303 if(typeof(myObj[nameA[0]][nameIndex]) == "undefined"){
304 myObj[nameA[0]][nameIndex] = { };
306 }else if(typeof(myObj[nameA[0]]) == "undefined"){
307 myObj[nameA[0]] = { }
310 if(nameA.length == 1){
311 myObj=myObj[nameA[0]];
313 myObj=myObj[nameA[0]][nameIndex];
317 if((elm.type != "select-multiple" && elm.type != "checkbox" && elm.type != "radio") || (elm.type == "radio" && elm.checked)){
318 if(name == name.split("[")[0]){
319 myObj[name]=elm.value;
321 // can not set value when there is no name
323 }else if(elm.type == "checkbox" && elm.checked){
324 if(typeof(myObj[name]) == 'undefined'){
327 myObj[name].push(elm.value);
328 }else if(elm.type == "select-multiple"){
329 if(typeof(myObj[name]) == 'undefined'){
332 for(var jdx=0,len3=elm.options.length; jdx<len3; ++jdx){
333 if(elm.options[jdx].selected){
334 myObj[name].push(elm.options[jdx].value);
346 // Returns true if all of the widgets are valid.
347 // Deprecated, will be removed in 2.0. Use get("state") instead.
349 return this.state == "";
352 onValidStateChange: function(/*Boolean*/ /*===== isValid =====*/){
354 // Stub function to connect to if you want to do something
355 // (like disable/enable a submit button) when the valid
356 // state changes on the form as a whole.
358 // Deprecated. Will be removed in 2.0. Use watch("state", ...) instead.
361 _getState: function(){
363 // Compute what this.state should be based on state of children
364 var states = array.map(this._descendants, function(w){
365 return w.get("state") || "";
368 return array.indexOf(states, "Error") >= 0 ? "Error" :
369 array.indexOf(states, "Incomplete") >= 0 ? "Incomplete" : "";
372 disconnectChildren: function(){
374 // Deprecated method. Applications no longer need to call this. Remove for 2.0.
377 connectChildren: function(/*Boolean*/ inStartup){
379 // You can call this function directly, ex. in the event that you
380 // programmatically add a widget to the form *after* the form has been
383 // TODO: rename for 2.0
385 this._descendants = this._getDescendantFormWidgets();
387 // To get notifications from children they need to be started. Children didn't used to need to be started,
388 // so for back-compat, start them here
389 array.forEach(this._descendants, function(child){
390 if(!child._started){ child.startup(); }
394 this._onChildChange();
398 _onChildChange: function(/*String*/ attr){
400 // Called when child's value or disabled state changes
402 // The unit tests expect state update to be synchronous, so update it immediately.
403 if(!attr || attr == "state" || attr == "disabled"){
404 this._set("state", this._getState());
407 // Use defer() to collapse value changes in multiple children into a single
408 // update to my value. Multiple updates will occur on:
411 // 3. user selecting a radio button (which will de-select another radio button,
412 // causing two onChange events)
413 if(!attr || attr == "value" || attr == "disabled" || attr == "checked"){
414 if(this._onChangeDelayTimer){
415 this._onChangeDelayTimer.remove();
417 this._onChangeDelayTimer = this.defer(function(){
418 delete this._onChangeDelayTimer;
419 this._set("value", this.get("value"));
425 this.inherited(arguments);
427 // Set initial this.value and this.state. Don't emit watch() notifications.
428 this._descendants = this._getDescendantFormWidgets();
429 this.value = this.get("value");
430 this.state = this._getState();
432 // Initialize value and valid/invalid state tracking.
437 "attrmodified-state, attrmodified-disabled, attrmodified-value, attrmodified-checked",
439 if(evt.target == self.domNode){
440 return; // ignore events that I fire on myself because my children changed
442 self._onChildChange(evt.type.replace("attrmodified-", ""));
447 // Make state change call onValidStateChange(), will be removed in 2.0
448 this.watch("state", function(attr, oldVal, newVal){ this.onValidStateChange(newVal == ""); });
452 this.inherited(arguments);