/** * Provides input field management, validation, submission, and form loading services for the collection * of {@link Ext.form.field.Field Field} instances within a {@link Ext.container.Container}. It is recommended * that you use a {@link Ext.form.Panel} as the form container, as that has logic to automatically * hook up an instance of {@link Ext.form.Basic} (plus other conveniences related to field configuration.) * * ## Form Actions * * The Basic class delegates the handling of form loads and submits to instances of {@link Ext.form.action.Action}. * See the various Action implementations for specific details of each one's functionality, as well as the * documentation for {@link #doAction} which details the configuration options that can be specified in * each action call. * * The default submit Action is {@link Ext.form.action.Submit}, which uses an Ajax request to submit the * form's values to a configured URL. To enable normal browser submission of an Ext form, use the * {@link #standardSubmit} config option. * * ## File uploads * * File uploads are not performed using normal 'Ajax' techniques; see the description for * {@link #hasUpload} for details. If you're using file uploads you should read the method description. * * ## Example usage: * * @example * Ext.create('Ext.form.Panel', { * title: 'Basic Form', * renderTo: Ext.getBody(), * bodyPadding: 5, * width: 350, * * // Any configuration items here will be automatically passed along to * // the Ext.form.Basic instance when it gets created. * * // The form will submit an AJAX request to this URL when submitted * url: 'save-form.php', * * items: [{ * xtype: 'textfield', * fieldLabel: 'Field', * name: 'theField' * }], * * buttons: [{ * text: 'Submit', * handler: function() { * // The getForm() method returns the Ext.form.Basic instance: * var form = this.up('form').getForm(); * if (form.isValid()) { * // Submit the Ajax request and handle the response * form.submit({ * success: function(form, action) { * Ext.Msg.alert('Success', action.result.message); * }, * failure: function(form, action) { * Ext.Msg.alert('Failed', action.result ? action.result.message : 'No response'); * } * }); * } * } * }] * }); */ Ext.define('Ext.form.Basic', { extend: 'Ext.util.Observable', alternateClassName: 'Ext.form.BasicForm', requires: [ 'Ext.util.MixedCollection', 'Ext.form.action.Load', 'Ext.form.action.Submit', 'Ext.window.MessageBox', 'Ext.data.ErrorCollection', 'Ext.util.DelayedTask' ], // Not a public API config, this is useful when we're unit testing so we can // turn off the delayed tasks so they fire immediately. taskDelay: 10, /** * @event beforeaction * Fires before any action is performed. Return false to cancel the action. * @param {Ext.form.Basic} this * @param {Ext.form.action.Action} action The {@link Ext.form.action.Action} to be performed */ /** * @event actionfailed * Fires when an action fails. * @param {Ext.form.Basic} this * @param {Ext.form.action.Action} action The {@link Ext.form.action.Action} that failed */ /** * @event actioncomplete * Fires when an action is completed. * @param {Ext.form.Basic} this * @param {Ext.form.action.Action} action The {@link Ext.form.action.Action} that completed */ /** * @event validitychange * Fires when the validity of the entire form changes. * @param {Ext.form.Basic} this * @param {Boolean} valid `true` if the form is now valid, `false` if it is now invalid. */ /** * @event dirtychange * Fires when the dirty state of the entire form changes. * @param {Ext.form.Basic} this * @param {Boolean} dirty `true` if the form is now dirty, `false` if it is no longer dirty. */ /** * @event errorchange * Fires when the error of one (or more) of the fields in the form changes. * @param {Ext.form.Basic} this * * @private */ /** * Creates new form. * @param {Ext.container.Container} owner The component that is the container for the form, usually a {@link Ext.form.Panel} * @param {Object} config Configuration options. These are normally specified in the config to the * {@link Ext.form.Panel} constructor, which passes them along to the BasicForm automatically. */ constructor: function(owner, config) { var me = this, reader; /** * @property {Ext.container.Container} owner * The container component to which this BasicForm is attached. */ me.owner = owner; me.fieldMonitors = { validitychange: me.checkValidityDelay, enable: me.checkValidityDelay, disable: me.checkValidityDelay, dirtychange: me.checkDirtyDelay, errorchange: me.checkErrorDelay, scope: me }; me.checkValidityTask = new Ext.util.DelayedTask(me.checkValidity, me); me.checkDirtyTask = new Ext.util.DelayedTask(me.checkDirty, me); me.checkErrorTask = new Ext.util.DelayedTask(me.checkError, me); // We use the monitor here as opposed to event bubbling. The problem with bubbling is it doesn't // let us react to items being added/remove at different places in the hierarchy which may have an // impact on the dirty/valid state. me.monitor = new Ext.container.Monitor({ selector: '[isFormField]:not([excludeForm])', scope: me, addHandler: me.onFieldAdd, removeHandler: me.onFieldRemove, invalidateHandler: me.onMonitorInvalidate }); me.monitor.bind(owner); Ext.apply(me, config); // Normalize the paramOrder to an Array if (Ext.isString(me.paramOrder)) { me.paramOrder = me.paramOrder.split(/[\s,|]/); } reader = me.reader; if (reader && !reader.isReader) { if (typeof reader === 'string') { reader = { type: reader }; } me.reader = Ext.createByAlias('reader.' + reader.type, reader); } reader = me.errorReader; if (reader && !reader.isReader) { if (typeof reader === 'string') { reader = { type: reader }; } me.errorReader = Ext.createByAlias('reader.' + reader.type, reader); } me.callParent(); }, /** * Do any post layout initialization * @private */ initialize : function() { this.initialized = true; this.onValidityChange(!this.hasInvalidField()); }, /** * @cfg {String} method * The request method to use (GET or POST) for form actions if one isn't supplied in the action options. */ /** * @cfg {Object/Ext.data.reader.Reader} reader * An Ext.data.reader.Reader (e.g. {@link Ext.data.reader.Xml}) instance or * configuration to be used to read data when executing 'load' actions. This * is optional as there is built-in support for processing JSON responses. */ /** * @cfg {Object/Ext.data.reader.Reader} errorReader * An Ext.data.reader.Reader (e.g. {@link Ext.data.reader.Xml}) instance or * configuration to be used to read field error messages returned from 'submit' actions. * This is optional as there is built-in support for processing JSON responses. * * The Records which provide messages for the invalid Fields must use the * Field name (or id) as the Record ID, and must contain a field called 'msg' * which contains the error message. * * The errorReader does not have to be a full-blown implementation of a * Reader. It simply needs to implement a `read(xhr)` function * which returns an Array of Records in an object with the following * structure: * * { * records: recordArray * } */ /** * @cfg {String} url * The URL to use for form actions if one isn't supplied in the * {@link Ext.form.Basic#doAction doAction} options. */ /** * @cfg {Object} baseParams * Parameters to pass with all requests. e.g. baseParams: `{id: '123', foo: 'bar'}`. * * Parameters are encoded as standard HTTP parameters using {@link Ext.Object#toQueryString}. */ /** * @cfg {Number} timeout * Timeout for form actions in seconds. */ timeout: 30, /** * @cfg {Object} api * If specified, load and submit actions will be handled with {@link Ext.form.action.DirectLoad DirectLoad} * and {@link Ext.form.action.DirectSubmit DirectSubmit}. Methods which have been imported by * {@link Ext.direct.Manager} can be specified here to load and submit forms. API methods may also be * specified as strings. See {@link Ext.data.proxy.Direct#directFn}. Such as the following: * * api: { * load: App.ss.MyProfile.load, * submit: App.ss.MyProfile.submit * } * * Load actions can use {@link #paramOrder} or {@link #paramsAsHash} to customize how the load method * is invoked. Submit actions will always use a standard form submit. The `formHandler` configuration * (see Ext.direct.RemotingProvider#action) must be set on the associated server-side method which has * been imported by {@link Ext.direct.Manager}. */ /** * @cfg {String/String[]} paramOrder * A list of params to be executed server side. Only used for the {@link #api} `load` * configuration. * * Specify the params in the order in which they must be executed on the * server-side as either (1) an Array of String values, or (2) a String of params * delimited by either whitespace, comma, or pipe. For example, * any of the following would be acceptable: * * paramOrder: ['param1','param2','param3'] * paramOrder: 'param1 param2 param3' * paramOrder: 'param1,param2,param3' * paramOrder: 'param1|param2|param' */ /** * @cfg {Boolean} paramsAsHash * Only used for the {@link #api} `load` configuration. If true, parameters will be sent as a * single hash collection of named arguments. Providing a {@link #paramOrder} nullifies this * configuration. */ paramsAsHash: false, /** * @cfg {Object/Array} [metadata] * Optional metadata to pass with the actions when Ext.Direct {@link #api} is used. * See {@link Ext.direct.Manager} for more information. */ // /** * @cfg {String} waitTitle * The default title to show for the waiting message box */ waitTitle: 'Please Wait...', // /** * @cfg {Boolean} trackResetOnLoad * If set to true, {@link #method-reset}() resets to the last loaded or * {@link Ext.form.Basic#setValues}() data instead of when the form was first * created. */ trackResetOnLoad: false, /** * @cfg {Boolean} standardSubmit * If set to true, a standard HTML form submit is used instead of a XHR (Ajax) style form submission. * All of the field values, plus any additional params configured via {@link #baseParams} * and/or the `options` to {@link #submit}, will be included in the values submitted in the form. */ /** * @cfg {Boolean} jsonSubmit * If set to true, the field values are sent as JSON in the request body. * All of the field values, plus any additional params configured via {@link #baseParams} * and/or the `options` to {@link #submit}, will be included in the values POSTed in the body of the request. */ /** * @cfg {String/HTMLElement/Ext.dom.Element} waitMsgTarget * By default wait messages are displayed with Ext.MessageBox.wait. You can target a specific * element by passing it or its id or mask the form itself by passing in true. */ // Private wasDirty: false, /** * Destroys this object. */ destroy: function() { var me = this, mon = me.monitor; if (mon) { mon.unbind(); me.monitor = null; } me.clearListeners(); me.checkValidityTask.cancel(); me.checkDirtyTask.cancel(); me.checkErrorTask.cancel(); me.checkValidityTask = me.checkDirtyTask = me.checkErrorTask = null; me.isDestroyed = true; }, onFieldAdd: function(field){ field.on(this.fieldMonitors); this.onMonitorInvalidate(); }, onFieldRemove: function(field){ field.un(this.fieldMonitors); this.onMonitorInvalidate(); }, onMonitorInvalidate: function() { if (this.initialized) { this.checkValidityDelay(); } }, /** * Return all the {@link Ext.form.field.Field} components in the owner container. * @return {Ext.util.MixedCollection} Collection of the Field objects */ getFields: function() { return this.monitor.getItems(); }, /** * @private * Finds and returns the set of all items bound to fields inside this form * @return {Ext.util.MixedCollection} The set of all bound form field items */ getBoundItems: function() { var boundItems = this._boundItems; if (!boundItems || boundItems.getCount() === 0) { boundItems = this._boundItems = new Ext.util.MixedCollection(); boundItems.addAll(this.owner.query('[formBind]')); } return boundItems; }, /** * Returns true if the form contains any invalid fields. No fields will be marked as invalid * as a result of calling this; to trigger marking of fields use {@link #isValid} instead. */ hasInvalidField: function() { return !!this.getFields().findBy(function(field) { var preventMark = field.preventMark, isValid; field.preventMark = true; isValid = field.isValid(); field.preventMark = preventMark; return !isValid; }); }, /** * Returns true if client-side validation on the form is successful. Any invalid fields will be * marked as invalid. If you only want to determine overall form validity without marking anything, * use {@link #hasInvalidField} instead. * @return {Boolean} */ isValid: function() { var me = this, invalid; Ext.suspendLayouts(); invalid = me.getFields().filterBy(function(field) { return !field.validate(); }); Ext.resumeLayouts(true); return invalid.length < 1; }, /** * Check whether the validity of the entire form has changed since it was last checked, and * if so fire the {@link #validitychange validitychange} event. This is automatically invoked * when an individual field's validity changes. */ checkValidity: function() { var me = this, valid; if (me.isDestroyed) { return; } valid = !me.hasInvalidField(); if (valid !== me.wasValid) { me.onValidityChange(valid); me.fireEvent('validitychange', me, valid); me.wasValid = valid; } }, checkValidityDelay: function(){ var timer = this.taskDelay; if (timer) { this.checkValidityTask.delay(timer); } else { this.checkValidity(); } }, checkError: function() { // Currently this event is private, we don't really care // about the summation of the change, rather that something has // changed so we may need to recalculate. In the future if this // is made public, we would need to track the error on a per-field basis. this.fireEvent('errorchange', this); }, checkErrorDelay: function() { var timer = this.taskDelay; if (timer) { this.checkErrorTask.delay(timer); } else { this.checkError(); } }, /** * @private * Handle changes in the form's validity. If there are any sub components with * `formBind=true` then they are enabled/disabled based on the new validity. * @param {Boolean} valid */ onValidityChange: function(valid) { var boundItems = this.getBoundItems(), items, i, iLen, cmp; if (boundItems) { items = boundItems.items; iLen = items.length; for (i = 0; i < iLen; i++) { cmp = items[i]; if (cmp.disabled === valid) { cmp.setDisabled(!valid); } } } }, /** * Returns `true` if any fields in this form have changed from their original values. * * Note that if this BasicForm was configured with {@link Ext.form.Basic#trackResetOnLoad * trackResetOnLoad} then the Fields' *original values* are updated when the values are * loaded by {@link Ext.form.Basic#setValues setValues} or {@link #loadRecord}. This means * that: * * - {@link #trackResetOnLoad}: `false` -> Will return `true` after calling this method. * - {@link #trackResetOnLoad}: `true` -> Will return `false` after calling this method. * * @return {Boolean} */ isDirty: function() { return !!this.getFields().findBy(function(f) { return f.isDirty(); }); }, checkDirtyDelay: function(){ var timer = this.taskDelay; if (timer) { this.checkDirtyTask.delay(timer); } else { this.checkDirty(); } }, /** * Check whether the dirty state of the entire form has changed since it was last checked, and * if so fire the {@link #dirtychange dirtychange} event. This is automatically invoked * when an individual field's `dirty` state changes. */ checkDirty: function() { var me = this, dirty; if (me.isDestroyed) { return; } dirty = this.isDirty(); if (dirty !== this.wasDirty) { this.fireEvent('dirtychange', this, dirty); this.wasDirty = dirty; } }, /** * Returns `true` if the form contains a file upload field. This is used to determine the method for submitting the * form: File uploads are not performed using normal 'Ajax' techniques, that is they are **not** performed using * XMLHttpRequests. Instead a hidden `
` element containing all the fields is created temporarily and submitted * with its [target][1] set to refer to a dynamically generated, hidden `