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