define("dijit/form/_FormMixin", [ "dojo/_base/array", // array.every array.filter array.forEach array.indexOf array.map "dojo/_base/declare", // declare "dojo/_base/kernel", // kernel.deprecated "dojo/_base/lang", // lang.hitch lang.isArray "dojo/window" // winUtils.scrollIntoView ], function(array, declare, kernel, lang, winUtils){ // module: // dijit/form/_FormMixin // summary: // Mixin for containers of form widgets (i.e. widgets that represent a single value // and can be children of a
node or dijit.form.Form widget) return declare("dijit.form._FormMixin", null, { // summary: // Mixin for containers of form widgets (i.e. widgets that represent a single value // and can be children of a node or dijit.form.Form widget) // description: // Can extract all the form widgets // values and combine them into a single javascript object, or alternately // take such an object and set the values for all the contained // form widgets /*===== // value: Object // Name/value hash for each child widget with a name and value. // Child widgets without names are not part of the hash. // // If there are multiple child widgets w/the same name, value is an array, // unless they are radio buttons in which case value is a scalar (since only // one radio button can be checked at a time). // // If a child widget's name is a dot separated list (like a.b.c.d), it's a nested structure. // // Example: // | { name: "John Smith", interests: ["sports", "movies"] } =====*/ // state: [readonly] String // Will be "Error" if one or more of the child widgets has an invalid value, // "Incomplete" if not all of the required child widgets are filled in. Otherwise, "", // which indicates that the form is ready to be submitted. state: "", // TODO: // * Repeater // * better handling for arrays. Often form elements have names with [] like // * people[3].sex (for a list of people [{name: Bill, sex: M}, ...]) // // _getDescendantFormWidgets: function(/*dijit._WidgetBase[]?*/ children){ // summary: // Returns all form widget descendants, searching through non-form child widgets like BorderContainer var res = []; array.forEach(children || this.getChildren(), function(child){ if("value" in child){ res.push(child); }else{ res = res.concat(this._getDescendantFormWidgets(child.getChildren())); } }, this); return res; }, reset: function(){ array.forEach(this._getDescendantFormWidgets(), function(widget){ if(widget.reset){ widget.reset(); } }); }, validate: function(){ // summary: // returns if the form is valid - same as isValid - but // provides a few additional (ui-specific) features. // 1 - it will highlight any sub-widgets that are not // valid // 2 - it will call focus() on the first invalid // sub-widget var didFocus = false; return array.every(array.map(this._getDescendantFormWidgets(), function(widget){ // Need to set this so that "required" widgets get their // state set. widget._hasBeenBlurred = true; var valid = widget.disabled || !widget.validate || widget.validate(); if(!valid && !didFocus){ // Set focus of the first non-valid widget winUtils.scrollIntoView(widget.containerNode || widget.domNode); widget.focus(); didFocus = true; } return valid; }), function(item){ return item; }); }, setValues: function(val){ kernel.deprecated(this.declaredClass+"::setValues() is deprecated. Use set('value', val) instead.", "", "2.0"); return this.set('value', val); }, _setValueAttr: function(/*Object*/ obj){ // summary: // Fill in form values from according to an Object (in the format returned by get('value')) // generate map from name --> [list of widgets with that name] var map = { }; array.forEach(this._getDescendantFormWidgets(), function(widget){ if(!widget.name){ return; } var entry = map[widget.name] || (map[widget.name] = [] ); entry.push(widget); }); for(var name in map){ if(!map.hasOwnProperty(name)){ continue; } var widgets = map[name], // array of widgets w/this name values = lang.getObject(name, false, obj); // list of values for those widgets if(values === undefined){ continue; } if(!lang.isArray(values)){ values = [ values ]; } if(typeof widgets[0].checked == 'boolean'){ // for checkbox/radio, values is a list of which widgets should be checked array.forEach(widgets, function(w){ w.set('value', array.indexOf(values, w.value) != -1); }); }else if(widgets[0].multiple){ // it takes an array (e.g. multi-select) widgets[0].set('value', values); }else{ // otherwise, values is a list of values to be assigned sequentially to each widget array.forEach(widgets, function(w, i){ w.set('value', values[i]); }); } } /*** * TODO: code for plain input boxes (this shouldn't run for inputs that are part of widgets) array.forEach(this.containerNode.elements, function(element){ if(element.name == ''){return}; // like "continue" var namePath = element.name.split("."); var myObj=obj; var name=namePath[namePath.length-1]; for(var j=1,len2=namePath.length;j 1){ if(typeof(myObj[nameA[0]]) == "undefined"){ myObj[nameA[0]]=[ ]; } // if nameIndex=parseInt(nameA[1]); if(typeof(myObj[nameA[0]][nameIndex]) == "undefined"){ myObj[nameA[0]][nameIndex] = { }; } myObj=myObj[nameA[0]][nameIndex]; continue; } // repeater support ends if(typeof(myObj[p]) == "undefined"){ myObj=undefined; break; }; myObj=myObj[p]; } if(typeof(myObj) == "undefined"){ return; // like "continue" } if(typeof(myObj[name]) == "undefined" && this.ignoreNullValues){ return; // like "continue" } // TODO: widget values (just call set('value', ...) on the widget) // TODO: maybe should call dojo.getNodeProp() instead switch(element.type){ case "checkbox": element.checked = (name in myObj) && array.some(myObj[name], function(val){ return val == element.value; }); break; case "radio": element.checked = (name in myObj) && myObj[name] == element.value; break; case "select-multiple": element.selectedIndex=-1; array.forEach(element.options, function(option){ option.selected = array.some(myObj[name], function(val){ return option.value == val; }); }); break; case "select-one": element.selectedIndex="0"; array.forEach(element.options, function(option){ option.selected = option.value == myObj[name]; }); break; case "hidden": case "text": case "textarea": case "password": element.value = myObj[name] || ""; break; } }); */ // Note: no need to call this._set("value", ...) as the child updates will trigger onChange events // which I am monitoring. }, getValues: function(){ kernel.deprecated(this.declaredClass+"::getValues() is deprecated. Use get('value') instead.", "", "2.0"); return this.get('value'); }, _getValueAttr: function(){ // summary: // Returns Object representing form values. See description of `value` for details. // description: // The value is updated into this.value every time a child has an onChange event, // so in the common case this function could just return this.value. However, // that wouldn't work when: // // 1. User presses return key to submit a form. That doesn't fire an onchange event, // and even if it did it would come too late due to the setTimeout(..., 0) in _handleOnChange() // // 2. app for some reason calls this.get("value") while the user is typing into a // form field. Not sure if that case needs to be supported or not. // get widget values var obj = { }; array.forEach(this._getDescendantFormWidgets(), function(widget){ var name = widget.name; if(!name || widget.disabled){ return; } // Single value widget (checkbox, radio, or plain type widget) var value = widget.get('value'); // Store widget's value(s) as a scalar, except for checkboxes which are automatically arrays if(typeof widget.checked == 'boolean'){ if(/Radio/.test(widget.declaredClass)){ // radio button if(value !== false){ lang.setObject(name, value, obj); }else{ // give radio widgets a default of null value = lang.getObject(name, false, obj); if(value === undefined){ lang.setObject(name, null, obj); } } }else{ // checkbox/toggle button var ary=lang.getObject(name, false, obj); if(!ary){ ary=[]; lang.setObject(name, ary, obj); } if(value !== false){ ary.push(value); } } }else{ var prev=lang.getObject(name, false, obj); if(typeof prev != "undefined"){ if(lang.isArray(prev)){ prev.push(value); }else{ lang.setObject(name, [prev, value], obj); } }else{ // unique name lang.setObject(name, value, obj); } } }); /*** * code for plain input boxes (see also domForm.formToObject, can we use that instead of this code? * but it doesn't understand [] notation, presumably) var obj = { }; array.forEach(this.containerNode.elements, function(elm){ if(!elm.name) { return; // like "continue" } var namePath = elm.name.split("."); var myObj=obj; var name=namePath[namePath.length-1]; for(var j=1,len2=namePath.length;j 1){ if(typeof(myObj[nameA[0]]) == "undefined"){ myObj[nameA[0]]=[ ]; } // if nameIndex=parseInt(nameA[1]); if(typeof(myObj[nameA[0]][nameIndex]) == "undefined"){ myObj[nameA[0]][nameIndex] = { }; } }else if(typeof(myObj[nameA[0]]) == "undefined"){ myObj[nameA[0]] = { } } // if if(nameA.length == 1){ myObj=myObj[nameA[0]]; }else{ myObj=myObj[nameA[0]][nameIndex]; } // if } // for if((elm.type != "select-multiple" && elm.type != "checkbox" && elm.type != "radio") || (elm.type == "radio" && elm.checked)){ if(name == name.split("[")[0]){ myObj[name]=elm.value; }else{ // can not set value when there is no name } }else if(elm.type == "checkbox" && elm.checked){ if(typeof(myObj[name]) == 'undefined'){ myObj[name]=[ ]; } myObj[name].push(elm.value); }else if(elm.type == "select-multiple"){ if(typeof(myObj[name]) == 'undefined'){ myObj[name]=[ ]; } for(var jdx=0,len3=elm.options.length; jdx= 0 ? "Error" : array.indexOf(states, "Incomplete") >= 0 ? "Incomplete" : ""; }, disconnectChildren: function(){ // summary: // Remove connections to monitor changes to children's value, error state, and disabled state, // in order to update Form.value and Form.state. array.forEach(this._childConnections || [], lang.hitch(this, "disconnect")); array.forEach(this._childWatches || [], function(w){ w.unwatch(); }); }, connectChildren: function(/*Boolean*/ inStartup){ // summary: // Setup connections to monitor changes to children's value, error state, and disabled state, // in order to update Form.value and Form.state. // // You can call this function directly, ex. in the event that you // programmatically add a widget to the form *after* the form has been // initialized. var _this = this; // Remove old connections, if any this.disconnectChildren(); this._descendants = this._getDescendantFormWidgets(); // (Re)set this.value and this.state. Send watch() notifications but not on startup. var set = inStartup ? function(name, val){ _this[name] = val; } : lang.hitch(this, "_set"); set("value", this.get("value")); set("state", this._getState()); // Monitor changes to error state and disabled state in order to update // Form.state var conns = (this._childConnections = []), watches = (this._childWatches = []); array.forEach(array.filter(this._descendants, function(item){ return item.validate; } ), function(widget){ // We are interested in whenever the widget changes validity state - or // whenever the disabled attribute on that widget is changed. array.forEach(["state", "disabled"], function(attr){ watches.push(widget.watch(attr, function(){ _this.set("state", _this._getState()); })); }); }); // And monitor calls to child.onChange so we can update this.value var onChange = function(){ // summary: // Called when child's value or disabled state changes // Use setTimeout() to collapse value changes in multiple children into a single // update to my value. Multiple updates will occur on: // 1. Form.set() // 2. Form.reset() // 3. user selecting a radio button (which will de-select another radio button, // causing two onChange events) if(_this._onChangeDelayTimer){ clearTimeout(_this._onChangeDelayTimer); } _this._onChangeDelayTimer = setTimeout(function(){ delete _this._onChangeDelayTimer; _this._set("value", _this.get("value")); }, 10); }; array.forEach( array.filter(this._descendants, function(item){ return item.onChange; } ), function(widget){ // When a child widget's value changes, // the efficient thing to do is to just update that one attribute in this.value, // but that gets a little complicated when a checkbox is checked/unchecked // since this.value["checkboxName"] contains an array of all the checkboxes w/the same name. // Doing simple thing for now. conns.push(_this.connect(widget, "onChange", onChange)); // Disabling/enabling a child widget should remove it's value from this.value. // Again, this code could be more efficient, doing simple thing for now. watches.push(widget.watch("disabled", onChange)); } ); }, startup: function(){ this.inherited(arguments); // Initialize value and valid/invalid state tracking. Needs to be done in startup() // so that children are initialized. this.connectChildren(true); // Make state change call onValidStateChange(), will be removed in 2.0 this.watch("state", function(attr, oldVal, newVal){ this.onValidStateChange(newVal == ""); }); }, destroy: function(){ this.disconnectChildren(); this.inherited(arguments); } }); });