source: Dev/trunk/src/client/dijit/form/_FormMixin.js

Last change on this file was 483, checked in by hendrikvanantwerpen, 11 years ago

Added Dojo 1.9.3 release.

File size: 14.3 KB
Line 
1define([
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});
Note: See TracBrowser for help on using the repository browser.