/**
* Object used to define actions FormHelper will take upon form submission and
* XHR lifecycle
*
* @typedef {Object} FormRule
* @namespace FormRule
*
* @property {String} form - Form CSS selector string
*
* @property {String} [action] - Where to post the XHR request. If ommitted,
* will use the form's 'action' attribute value
*
* @property {Object} [xhrOptions] - Custom XHR options to use. See [jQuery documentation]{@link http://api.jquery.com/jQuery.ajax/#jQuery-ajax-settings}
* for full list of options
*
* @property {Object} [data] - Additional post data. Useful for defaults or
* 'always args'<br><br>
*
* Note the order order in which data is merged (later items take
* precedence):
* <ol>
* <li>formHelper.FormHelperRequest.Defaults.xhrOptions.data
* <li>rule.xhrOptions.data
* <li>rule.data
* <li>serialized form data
* </ol>
*
* @property {Boolean} [isMultiForm] - Modify [$form]{@link formHelper.FormHelperRequest#$form}
* to be the set of all matched {@link FormRule.form} elements as opposed to the
* single submitted form, i.e. treat multiple forms as one single big form
*
* @property {Boolean} [disableControls] - Automatically disable form controls
* during submit
*
* @property {Boolean} [scrollIntoViewOnUIUpdate] - Automatically scroll the form
* into view (if it isn't already) if response caused us to modify the DOM
*
* @property {Boolean} [focusFirstErroredControl] - Automatically focuses
* the first errored control
*
* @property {Boolean} [blankAllPasswordsOnSubmit] - Automatically call
* [blankAllPasswordFields]{@link formHelper.FormHelperRequest#blankAllPasswordFields} on submit
*
* @property {Boolean} [releaseFormAndUpdateUIOnXHRSuccess] - Automatically call
* [releaseForm]{@link formHelper.FormHelperRequestController#releaseForm} and
* [updateUI]{@link formHelper.FormHelperRequestController#updateUI} after
* [xhrSuccess]{@link formHelper.FormHelperRequestController#xhrSuccess}
*
* @property {FormRule.xhrReady} [xhrReady] - Callback fired when [xhrOptions]{@link formHelper.FormHelperRequest#xhrOptions}
* has been created but before the actual jqXHR has been created. <b>The most
* relevant non-StatusHandler callback</b>
*
* @property {FormRule.xhrBeforeSend} [xhrBeforeSend] - Callback fired during
* the jqXHR beforeSend event:
* <blockquote>
* <b>[beforeSend]{@link http://api.jquery.com/Ajax_Events/}</b><br>
* This event, which is triggered before an Ajax request is started, allows you
* to modify the XMLHttpRequest object (setting additional headers, if need be.)
* </blockquote>
*
* @property {FormRule.xhrSuccess} [xhrSuccess] - Callback fired during the
* jqXHR success event:
* <blockquote>
* <b>[success]{@link http://api.jquery.com/Ajax_Events/}</b><br>
* This event is only called if the request was successful (no errors from the
* server, no errors with the data).
* </blockquote>
*
* @property {FormRule.xhrComplete} [xhrComplete] - Callback fired during the
* jqXHR complete event:
* <blockquote>
* <b>[complete]{@link http://api.jquery.com/Ajax_Events/}</b><br>
* This event is called regardless of if the request was successful, or not. You
* will always receive a complete callback, even for synchronous requests.
* </blockquote>
*
* @property {FormRule.xhrError} [xhrError] - Callback fired during the jqXHR
* error event:
* <blockquote>
* <b>[error]{@link http://api.jquery.com/Ajax_Events/}</b><br>
* This event is only called if an error occurred with the request (you can
* never have both an error and a success callback with a request).
* </blockquote>
*
* @property {Function} [requestController] - Class to instantiate to handle
* FormRule execution. Defaults to [FormHelperRequest]{@link formHelper.FormHelperRequest}
*
* @property {Function} [customSubmitHandler] - Prevents FormHelper from creating
* a [FormHelperRequest]{@link formHelper.FormHelperRequest} on submit and calls
* this function instead.
*
* @property {Function} [onComplete] - Callback fired after when FormHelperRequest
* is complete.
*
* @property {Object.<String, FormRule.StatusHandler>} [status]
* Object with keys matching {@link FormHelperResponse} status codes and their
* accompanying StatusHandlers. Only the StatusHandler registered for the
* returned status code will be invoked
*
* @example
* {
* form: '#form-login',
* action: '/h/module/loginForm',
*
* xhrOptions: {
* type: 'POST', // Default value
* data: {
* foo: 'bar'
* }
* },
*
* data: {
* foo: 'bar',
* baz: 'qux'
* },
*
* xhrReady: function(xhrOptions) {
* // Everything is prepared, about to create jqXHR
* },
* xhrBeforeSend: function(jqXHR, settings) {
* // jqXHR instantiated, about to make the request
* },
* xhrSuccess: function(data, textStatus, jqXHR) {
* // Response received, all good.
* },
* xhrError: function(jqXHR, textStatus, errorThrown) {
* // Something went wrong...
* },
* xhrComplete: function(jqXHR, textStatus) {
* // jqXHR complete
* },
*
* status: {
* SUCCESS: '/account', // Login successful, redirect user
* ERROR: function() {
* // Fired on a response status code of 'error'
* this.find('#fancy-error-message').show();
* }
* }
* }
*
* @example
* {
* form: '#form-sign-up',
* action: '/h/module/signup',
*
* xhrReady: function(xhrOptions) {
* if (xhrOptions.data.accepts !== '1') {
* this.cancel();
* this.find('#message-accept-terms').show();
* }
* },
*
* status: {
* SUCCESS: function() {
* this.$form.html(this.data);
* }
* }
* }
*/
/**
* @typedef {(fn|String)} StatusHandler
* @memberOf FormRule
* @description
* Handler for a given {@link FormHelperResponse} status. Can either be a
* function or a string. If it's a function, it will be called in the context of
* the [FormHelperRequest]{@link formHelper.FormHelperRequest} instance. If it's
* a string, the browser will be forwarded to the given URL/path.
* @example
* // Update UI appropriately for this status code
* function() {
* this.find("#update-success").show();
* this.scrollToTopOfForm();
* }
* @example
* // Forward the browser to '/account/main'
* '/account/main'
*/
/**
* @callback xhrReady
* @memberOf FormRule
* @param {Object} xhrOptions - Complete XHR options object
*/
/**
* @callback xhrBeforeSend
* @memberOf FormRule
* @param {jqXHR} jqXHR - Prepared [jQuery XMLHttpRequest object]{@link http://api.jquery.com/jQuery.ajax/#jqXHR}
* @param {object} settings - jqXHR settings object
*/
/**
* @callback xhrComplete
* @memberOf FormRule
* @param {jqXHR} jqXHR - [jQuery XMLHttpRequest object]{@link http://api.jquery.com/jQuery.ajax/#jqXHR}
* @param {String} textStatus jQuery request status
*/
/**
* @callback xhrSuccess
* @memberOf FormRule
* @param {object} data Response payload
* @param {String} textStatus jQuery request status
* @param {jqXHR} jqXHR - [jQuery XMLHttpRequest object]{@link http://api.jquery.com/jQuery.ajax/#jqXHR}
*/
/**
* @callback xhrError
* @memberOf FormRule
* @param {jqXHR} jqXHR - [jQuery XMLHttpRequest object]{@link http://api.jquery.com/jQuery.ajax/#jqXHR}
* @param {String} textStatus jQuery request status
* @param {String} errorThrown HTTP status – 'Not Found', 'Internal Server
* Error', etc.
*/
/**
* This describes the expected format of a FormHelperRequest JSON response
*
* @typedef {Object} FormHelperResponse
* @namespace FormHelperResponse
*
* @property {String} status Custom status code for the request. Form Helper
* will try to make visible any elements within the form having a class matching
* the pattern <code>js-fh-status-{status}</code>. Optionally maps to a
* {@link FormRule.StatusHandler} for additional logic
*
* @property [data] Arbitrary data. Typically HTML or additional JSON data
*
* @property {Array.<FormHelperResponse.RequestError>} [errors] Any errors and
* metadata for the request
*
* @example
* {
* "status": "ERROR",
* "errors": [
* {
* "code": "pw-too-short",
* "message": "password must be at least 8 characters",
* "params": [
* "password",
* "password-confirm"
* ]
* },
* {
* "code": "incomplete-address",
* "message": "missing address fields",
* "params": [
* "city"
* ]
* }
* ]
* }
*
* @example
* {
* "status": "SUCCESS",
* "data": "<div>Thank you!</div>"
* }
*/
/**
* @typedef {Object} RequestError
* @memberOf FormHelperResponse
*
* @property {String} code User defined error code. Form Helper will try to make
* visible any elements within the form having a class matching the pattern
* <code>js-fh-error-{code}</code>
*
* @property {String} [message] Developer-only details and/or description of the
* error. Not used by Form Helper
*
* @property {Array.String} [params] Array of request parameters, i.e. form
* inputs, that are associated with the RequestError
*
* @example
* {
* "code": "login-failure",
* "message": "Email address and/or password invalid",
* "params": [
* "email-address",
* "password",
* "password-confirm"
* ]
* }
*/
/**
* @namespace formHelper
*/
(function(formHelper, $, undefined) /** @lends formHelper **/ {
var rules = [];
/**
* API method for registering a FormRule. The associated form element not need
* to exist in the DOM at this point
* @param {FormRule} formRule [FormRule]{@link formHelper.FormRule} object
*/
formHelper.addRule = function(formRule) {
rules.push(formRule);
};
/**
* Fires when ANY form submits. Checks to see if the submitting form matches
* any given {@link FormRule} selectors. If a match is found, [preventDefault]{@link https://developer.mozilla.org/en-US/docs/Web/API/event.preventDefault}
* and instantiate a new {@link formHelper.FormHelperRequest}
* @param {Event} event submit event
*
* @memberof formHelper
* @inner
*/
function onFormSubmit(event) {
var matchedRule = null;
var $form = $(this);
// Check if this form matches any of our rules
$(rules).each(function(index, rule) {
if (!matchedRule) {
if ($form.is(rule.form)) {
matchedRule = rule;
}
}
});
if (matchedRule) {
event.preventDefault();
// If there is an alternate onFormSubmit behaviour specified
// in the rule, do that instead
if(matchedRule.customSubmitHandler){
matchedRule.customSubmitHandler();
return;
}
if (matchedRule.isMultiForm) {
$form = $(matchedRule.form);
}
var RequestController = matchedRule.requestController || formHelper.FormHelperRequest;
new RequestController($form, matchedRule, event);
}
}
$(function() {
$(document).on('submit', 'form', onFormSubmit);
});
/**
* FormHelperRequestController's methods get mixed in to FormHelperRequest.
* FormHelperRequestController should never be instantiated - it's simply a
* way to separate the internal FormHelper system logic from functionality
* safe for use in [FormRule.StatusHandler]{@link FormRule.StatusHandler}
*
* @class
* @memberOf formHelper
* @private
*/
function FormHelperRequestController() { throw new Error('FormHelperRequestController should not be instantiated'); }
$.extend(FormHelperRequestController.prototype, /** @lends formHelper.FormHelperRequestController.prototype */ {
startXHR: function() {
if (this.cancelled) return;
if (this.rule.blankAllPasswordsOnSubmit) {
this.blankAllPasswordFields();
}
this.jqXHR = $.ajax(this.xhrOptions);
},
/**
* Invoke form rule's xhrBeforeSend callback. If it returns false or [cancels]{@link formHelper.FormHelperRequest#cancel},
* request and don't do anything with the UI. Hold off on modifying the
* form's state until we know we're in the clear.
*
* If not cancelled, reset UI and proceed with the request.
*
* @param {jqXHR} jqXHR - Prepared [jQuery XMLHttpRequest object]{@link http://api.jquery.com/jQuery.ajax/#jqXHR}
* @param {object} settings - jqXHR settings object
*/
xhrBeforeSend: function(jqXHR, settings) {
var cancelledViaReturnFalse = this.invokeStatusHandler(this.rule.xhrBeforeSend, [jqXHR, settings]) === false;
if (!cancelledViaReturnFalse) {
cancelledViaReturnFalse = this.invokeStatusHandler(formHelper.always.xhrBeforeSend, [jqXHR, settings]) === false;
}
if (!cancelledViaReturnFalse || !this.cancelled) {
this.checkoutForm();
this.hideAllStatusMessages();
this.hideAllErrorMessages();
this.hideAllParamMessages();
this.clearAllErroredFields();
this.disableControls();
}
},
/**
* Sets [status]{@link formHelper.FormHelperRequest#status}, [data]{@link formHelper.FormHelperRequest#data},
* and [errors]{@link formHelper.FormHelperRequest#errors} properties.
* Invokes [xhrSuccess StatusHandler]{@link FormRule.StatusHandler}. If not
* [cancelled]{@link formHelper.FormHelperRequest#cancel}, [enable controls]{@link formHelper.FormHelperRequestController#enableControls}
* and [update UI]{@link formHelper.FormHelperRequestController#updateUI}
*
* @param {Object} data Response payload, i.e. {@link FormHelperResponse}
* @param {String} textStatus jQuery request status
* @param {jqXHR} jqXHR - [jQuery XMLHttpRequest object]{@link http://api.jquery.com/jQuery.ajax/#jqXHR}
*/
xhrSuccess: function(data, textStatus, jqXHR) {
this.status = data.status;
this.data = data.data;
this.errors = data.errors;
this.invokeStatusHandler(this.rule.xhrSuccess, [data, textStatus, jqXHR]);
this.invokeStatusHandler(formHelper.always.xhrSuccess, [data, textStatus, jqXHR]);
if (this.status) {
if (this.rule.status) {
this.invokeStatusHandler(this.rule.status[this.status]);
}
if (formHelper.always.status) {
this.invokeStatusHandler(formHelper.always.status[this.status]);
}
}
if (this.cancelled) return;
if (this.rule.releaseFormAndUpdateUIOnXHRSuccess) {
this.releaseForm();
this.updateUI();
}
this.invokeStatusHandler(this.rule.onComplete);
this.invokeStatusHandler(formHelper.always.onComplete);
},
/**
* [Releases form]{@link formHelper.FormHelperRequestController#releaseForm}
* and invoke [xhrError StatusHandler]{@link FormRule.StatusHandler}
* @param {jqXHR} jqXHR - [jQuery XMLHttpRequest object]{@link http://api.jquery.com/jQuery.ajax/#jqXHR}
* @param {String} textStatus jQuery request status
* @param {String} errorThrown HTTP status – 'Not Found', 'Internal Server Error', etc.
*/
xhrError: function(jqXHR, textStatus, errorThrown) {
this.releaseForm();
this.invokeStatusHandler(this.rule.xhrError, [jqXHR, textStatus, errorThrown]);
this.invokeStatusHandler(formHelper.always.xhrError, [jqXHR, textStatus, errorThrown]);
},
/**
* Invoke [xhrComplete StatusHandler]{@link FormRule.StatusHandler}
*
* @param {jqXHR} jqXHR - [jQuery XMLHttpRequest object]{@link http://api.jquery.com/jQuery.ajax/#jqXHR}
* @param {String} textStatus jQuery request status
*/
xhrComplete: function(jqXHR, textStatus) {
this.invokeStatusHandler(this.rule.xhrComplete, [jqXHR, textStatus]);
this.invokeStatusHandler(formHelper.always.xhrComplete, [jqXHR, textStatus]);
},
/**
* Invoke given StatusHandler (if any) with supplied args
*
* @param {FormRule.StatusHandler} [statusHandler] StatusHandler to invoke
* @param {array} [args] The callback's arguments
*/
invokeStatusHandler: function(statusHandler, args) {
if (this.cancelled) return;
if (statusHandler) {
switch (typeof statusHandler) {
case 'string':
this.redirect(statusHandler);
break;
case 'function':
return statusHandler.apply(this, args || []);
default:
break;
}
}
},
/**
* Marks the form element as being 'checked out' by applying
* [FH_FORM_SUBMITTING]{@link formHelper.Classes.FH_FORM_SUBMITTING} class.
* A checked out form will cancel any additional FormHelperRequests on the
* form until [released]{@link formHelper.FormHelperRequestController#releaseForm}
*/
checkoutForm: function() {
this.$form.addClass(formHelper.Classes.FH_FORM_SUBMITTING);
},
/**
* Removes [FH_FORM_SUBMITTING]{@link formHelper.Classes.FH_FORM_SUBMITTING}
* class from the form and [enables controls]{@link formHelper.FormHelperRequestController#enableControls}
*/
releaseForm: function() {
this.enableControls();
this.$form.removeClass(formHelper.Classes.FH_FORM_SUBMITTING);
},
/**
* Determine if the form is 'checked out' by a FormHelperRequest instance by
* testing for the presence of [FH_FORM_SUBMITTING]{@link formHelper.Classes.FH_FORM_SUBMITTING}
* class on the form element
* @return {Boolean}
*/
isFormCheckedOut: function() {
return this.$form.hasClass(formHelper.Classes.FH_FORM_SUBMITTING);
},
/**
* Calls [getErrorMessages]{@link formHelper.FormHelperRequestController#getErrorMessages},
* [getParamMessages]{@link formHelper.FormHelperRequestController#getParamMessages},
* [getStatusMessage]{@link formHelper.FormHelperRequestController#getStatusMessage},
* and [getErroredControls]{@link formHelper.FormHelperRequestController#getErroredControls}.
* Uses the return values to determine if any UI has been updated, and if
* so, scrolls the form into view
*/
updateUI: function() {
var errors = this.errors;
var classes = formHelper.Classes;
var ctx = this;
var $win;
var winScrollTop;
var viewportHeight;
var formOffset;
var formHeight;
var scrollTopTarget;
var errorMessages = this.getErrorMessages();
var paramMessages = this.getParamMessages();
var statusMessage = this.getStatusMessage();
var erroredFields = this.getErroredControls();
if (this.rule.scrollIntoViewOnUIUpdate) {
if (errorMessages.length || paramMessages.length || statusMessage.length || erroredFields.length) {
// UI has been updated
$win = $(window);
formOffset = this.$form.offset();
winScrollTop = $win.scrollTop();
viewportHeight = $win.innerHeight();
formHeight = this.$form.outerHeight();
// If the form is NOT completely in view
if (!(formOffset.top > winScrollTop && formOffset.top + formHeight < winScrollTop + viewportHeight)) {
if (formOffset.top < winScrollTop) {
scrollTopTarget = formOffset.top;
} else {
if (formHeight > viewportHeight) {
scrollTopTarget = formOffset.top;
} else {
scrollTopTarget = formOffset.top + formHeight - viewportHeight;
}
}
}
if (scrollTopTarget !== undefined) {
$('html, body').animate({scrollTop: scrollTopTarget + 'px'});
}
}
}
},
/**
* Given any [RequestError]{@link FormHelperResponse.RequestError} codes,
* find, show, and mark [FH_MARKED_ERROR_MESSAGE]{@link formHelper.Classes.FH_MARKED_ERROR_MESSAGE}
* all associated error message elements
*
* @return {jQuery} All shown error messages
*/
getErrorMessages: function() {
var Classes = formHelper.Classes;
var ErrorCodes = formHelper.ErrorCodes;
var errors = this.errors;
var selectors = [];
var objects = $();
if (errors && errors.length) {
selectors.push('.' + Classes.ERROR_MESSAGE_PREFIX + ErrorCodes.ANY_ERROR);
$(errors).each(function(index, error) {
if (error.code) {
selectors.push('.' + Classes.ERROR_MESSAGE_PREFIX + error.code);
}
});
}
if (selectors.length) {
objects = objects.add( this.find(selectors.join(',')).addClass(Classes.FH_MARKED_ERROR_MESSAGE).show() );
}
return objects;
},
/**
* Given any [RequestError]{@link FormHelperResponse.RequestError} params,
* find and show all associated param messages
*
* @return {jQuery} All modified param messages
*/
getParamMessages: function() {
var Classes = formHelper.Classes;
var errors = this.errors;
var selectors = [];
var objects = $();
if (errors && errors.length) {
$(errors).each(function(index, error) {
if (error.params) {
$(error.params).each(function(index, param) {
selectors.push('.' + Classes.PARAM_MESSAGE_PREFIX + param);
});
}
});
}
if (selectors.length) {
objects = objects.add( this.find(selectors.join(',')).addClass(Classes.FH_MARKED_PARAM_MESSAGE).show() );
}
return objects;
},
/**
* Find and show the status message element, if it exists, for the given
* [RequestError]{@link FormHelperResponse.RequestError} status
*
* @return {jQuery} Shown status message
*/
getStatusMessage: function() {
var Classes = formHelper.Classes;
if (this.status) {
return this.find('.' + Classes.STATUS_MESSAGE_PREFIX + this.status.toLowerCase()).addClass(Classes.FH_MARKED_STATUS_MESSAGE).show();
} else {
return $();
}
},
/**
* Given any [RequestError]{@link FormHelperResponse.RequestError} params,
* find all associated control and apply [CONTROL_GROUP_ERROR]{@link formHelper.Classes.CONTROL_GROUP_ERROR}
* to their control group
*
* @return {jQuery} All controls for error params
*/
getErroredControls: function() {
var Classes = formHelper.Classes;
var errors = this.errors;
var controls = $();
var ctx = this;
var control;
if (errors && errors.length) {
$(errors).each(function(index, error) {
if (error.params) {
$(error.params).each(function(index, param) {
control = ctx.control(param);
if (control.length) {
controls = controls.add(control);
ctx.controlGroup(control).addClass(Classes.CONTROL_GROUP_ERROR + ' ' + Classes.FH_MARKED_CONTROL_GROUP);
}
});
}
});
}
if (this.rule.focusFirstErroredControl) {
if (controls.length) {
control = controls[0];
// IE8 jank - can't focus an input that was just enabled.
window.setTimeout(function() {
control.focus();
if (control.select) {
control.select();
}
}, 0);
}
}
return controls;
},
/**
* Removes all occurrances of [CONTROL_GROUP_ERROR]{@link formHelper.Classes.CONTROL_GROUP_ERROR}
* from [FH_MARKED_CONTROL_GROUP]{@link formHelper.Classes.FH_MARKED_CONTROL_GROUP}
* elements
*/
clearAllErroredFields: function() {
var Classes = formHelper.Classes;
this.find('.' + Classes.FH_MARKED_CONTROL_GROUP).removeClass(Classes.CONTROL_GROUP_ERROR + ' ' + Classes.FH_MARKED_CONTROL_GROUP);
},
/**
* Hides any elements marked [FH_MARKED_ERROR_MESSAGE]{@link formHelper.Classes.FH_MARKED_ERROR_MESSAGE}
* and removes marker
*/
hideAllErrorMessages: function() {
var Classes = formHelper.Classes;
this.find('.' + Classes.FH_MARKED_ERROR_MESSAGE).hide().removeClass(Classes.FH_MARKED_ERROR_MESSAGE);
},
/**
* Hides any elements marked [FH_MARKED_PARAM_MESSAGE]{@link formHelper.Classes.FH_MARKED_PARAM_MESSAGE}
* and removes marker
*/
hideAllParamMessages: function() {
var Classes = formHelper.Classes;
this.find('.' + Classes.FH_MARKED_PARAM_MESSAGE).hide().removeClass(Classes.FH_MARKED_PARAM_MESSAGE);
},
/**
* Hides any elements marked [FH_MARKED_STATUS_MESSAGE]{@link formHelper.Classes.FH_MARKED_STATUS_MESSAGE}
* and removes marker
*/
hideAllStatusMessages: function() {
var Classes = formHelper.Classes;
this.find('.' + Classes.FH_MARKED_STATUS_MESSAGE).hide().removeClass(Classes.FH_MARKED_STATUS_MESSAGE);
},
/**
* Sets 'disabled' attribute and applies [FH_DISABLED_CONTROL]{@link formHelper.Classes.FH_DISABLED_CONTROL}
* class to all controls within the form
*
* @todo 99.9% sure this doesn't work in all browsers. Will probably need to
* beef this up by adding disabled classes and registering a handler on form
* change that simply prevents default until enabled.
*/
disableControls: function() {
if (this.rule.disableControls) {
this.find('input, select, textarea, button').not(':disabled').attr('disabled', 'disabled').addClass(formHelper.Classes.FH_DISABLED_CONTROL);
}
},
/**
* Removes 'disabled' attribute and [FH_DISABLED_CONTROL]{@link formHelper.Classes.FH_DISABLED_CONTROL}
* class from all inputs within the form
*/
enableControls: function() {
var Classes = formHelper.Classes;
if (this.rule.disableControls) {
this.find('.' + Classes.FH_DISABLED_CONTROL).removeAttr('disabled').removeClass(Classes.FH_DISABLED_CONTROL);
}
}
});
/**
* FormHelperRequest instances are created automatically by FormHelper when
* form matching any [registered FormRule]{@link formHelper.addRule}
* submits. FormHelperRequest is not typically instantiated directly by the
* user.<br><br>
*
* The FormRule's callbacks are executed in the context of the
* FormHelperRequest instance, so members listed below are available during
* any XHR callbacks and {@link FormRule.StatusHandler}<br>
*
* @class
* @param {HTMLFormElement} formEl Form element to be handled
* @param {FormRule} rule The form's [FormRule]{@link formHelper.FormRule}
* @param {Event} submitEvent The form's original submit event
* @mixes formHelper.FormHelperRequestController
* @memberOf formHelper
*/
function FormHelperRequest(formEl, rule, submitEvent) {
if (!formEl) return;
this.initialize(formEl, rule, submitEvent);
}
FormHelperRequest.prototype = FormHelperRequestController.prototype;
formHelper.FormHelperRequest = FormHelperRequest;
$.extend(formHelper.FormHelperRequest.prototype, /** @lends formHelper.FormHelperRequest.prototype */ {
initialize: function(formEl, rule, submitEvent) {
/**
* jQuery-wrapped form element for the request
* @type {jQuery}
*/
this.$form = $(formEl);
if (this.isFormCheckedOut()) return;
/**
* The FormRule for this request. Your custom rules merged with the defaults
* @type {FormRule}
*/
this.rule = $.extend(true, {}, formHelper.FormHelperRequest.Defaults.rule, rule);
/**
* The form's submit event
* @type {Event}
*/
this.submitEvent = submitEvent;
this.cancelled = false;
/**
* {@link FormHelperResponse} status code. Only available after [xhrSuccess]{@link formHelper.FormHelperRequestController#xhrSuccess}
* @type {String}
*/
this.status = null;
/**
* {@link FormHelperResponse} data, if any was supplied. Only available after [xhrSuccess]{@link formHelper.FormHelperRequestController#xhrSuccess}
*/
this.data = null;
/**
* Array of {@link FormHelperResponse.RequestError} objects, if any. Only available after [xhrSuccess]{@link formHelper.FormHelperRequestController#xhrSuccess}
* @type {Array}
*/
this.errors = null;
// Deep copy so rule.xhrOptions.data will be sent along.
var xhrOptions = $.extend(true, {}, formHelper.FormHelperRequest.Defaults.xhrOptions, rule.xhrOptions || {}, {
context: this,
url: this.rule.action || this.$form.attr('action'),
data: $.extend({}, rule.data || {}, this.$form.serializeJSON()),
beforeSend: this.xhrBeforeSend,
success: this.xhrSuccess,
complete: this.xhrComplete,
error: this.xhrError
});
/**
* Object used to create [jQuery Ajax request]{@link http://api.jquery.com/jQuery.ajax/#jQuery-ajax-settings}.
* Hang on to this mostly for its 'data' property, which is an object with
* key value pairs of the form input names and their values. After this
* property is set, it's no longer necessary to query the DOM to read user
* input values
*
* @example
* {
* url: "/h/module/loginForm",
* type: "POST",
* dataType: "json",
* cache: false,
* data: {
* // user input values
* firstname: "Billy",
* lastname: "White",
* email: "billy@lynch2.com"
* },
* ...
* }
*
* @type {object}
*/
this.xhrOptions = xhrOptions;
this.invokeStatusHandler(rule.xhrReady, [xhrOptions]);
this.invokeStatusHandler(formHelper.always.xhrReady, [xhrOptions]);
if (this.cancelled) return;
this.startXHR();
},
/**
* Cancels any further processing of the FormHelperRequest. Can be called
* during a rule's [xhrReady]{@link FormRule.xhrReady} or [xhrBeforeSend]{@link formHelper.FormHelperRequestController#xhrBeforeSend}
* callbacks to prevent FormHelper UI updates and XHR
*/
cancel: function() {
this.cancelled = true;
},
/**
* [Cancels]{@link formHelper.FormHelperRequest#cancel} the request and forwards the browser to the given location
* @param {String} location - URL/path to go to
*/
redirect: function(location) {
this.cancel();
window.location.href = location;
},
/**
* Scroll the page so the top of the form is at the top of the window
*/
scrollToTopOfForm: function() {
$('html, body').animate({scrollTop: this.$form.offset().top + 'px'});
},
/**
* Clear the value of every password input field in the form
*/
blankAllPasswordFields: function() {
this.find('input[type=password]').each(function(index, input) {
$(input).val('');
});
},
/**
* Shortcut for finding elements scoped within the form
* @param {String} selector CSS Selector String
* @return {jQuery}
*/
find: function(selector) {
return this.$form.find(selector);
},
/**
* Find the control element for given name or id
* @param {String} control - Control name or id
* @return {jQuery}
*/
control: function(control) {
var $el = this.find('[name=' + control + ']');
if (!$el.length) {
$el = this.find('#' + control);
}
return $el.first();
},
/**
* Find and return a jQuery-wrapped control group for a given control
* @param {String|jQuery} control - Control name or id string, or
* jQuery-wrapped control element
* @return {jQuery}
*/
controlGroup: function(control) {
return ((typeof control === 'string') ? this.control(control) : control).closest('.' + formHelper.Classes.CONTROL_GROUP);
}
});
/** @namespace */
formHelper.Classes = {
/**
* Class denoting a 'control group', i.e. the container for an input and
* label pair
* @default
*/
CONTROL_GROUP : 'form-group',
/**
* Class applied to CONTROL_GROUP in the event of an error
* @default
*/
CONTROL_GROUP_ERROR : 'has-error',
/**
* Class prefix denoting a pre-loaded hidden error message
* @example
* <div class="js-fh-error-bad-password" style="display: none;">Invalid password. Please try again.</div>
* @default
*/
ERROR_MESSAGE_PREFIX : 'js-fh-error-',
/**
* Class prefix denoting a pre-loaded hidden message for returned params
* @example
* <div class="js-fh-param-error-first-name" style="display: none;">First Name</div>
* @default
*/
PARAM_MESSAGE_PREFIX : 'js-fh-param-error-',
/**
* Class prefix denoting a pre-loaded hidden message for returned status
* code
* @example
* <div class="js-fh-status-success" style="display: none;">Thank You!</div>
* @default
*/
STATUS_MESSAGE_PREFIX : 'js-fh-status-',
/**
* Class applied to [$form]{@link formHelper.FormHelperRequest#$form} while
* submitting
* @default
*/
FH_FORM_SUBMITTING : 'js-fh-submitting',
/**
* Class applied to made-visible error messages
* @default
*/
FH_MARKED_ERROR_MESSAGE : 'js-fh-marked-error-msg',
/**
* Class applied to made-visible param error messages
* @default
*/
FH_MARKED_PARAM_MESSAGE : 'js-fh-marked-param-msg',
/**
* Class applied to made-visible status messages
* @default
*/
FH_MARKED_STATUS_MESSAGE : 'js-fh-marked-status-msg',
/**
* Class applied to error-marked control groups
* @default
*/
FH_MARKED_CONTROL_GROUP : 'js-fh-marked-control-group',
/**
* Class applied to made-disabled control groups while submitting
* @default
*/
FH_DISABLED_CONTROL : 'js-fh-disabled-control'
};
/** @namespace */
formHelper.ErrorCodes = {
/**
* Special error code that will be made visible if there is at least on
* error in the response
* @example
* <div class="js-fh-error-any-error" style="display: none;">Oops, something went wrong!</div>
* @default
*/
ANY_ERROR : 'any-error'
};
/**
* <p>A place to declare site-wide XHR callbacks and StatusHandlers. These
* callbacks, if present, are invoked immediately after (and independant of) a
* FormRule's local counterpart.</p>
* <p>Note that even though these are registered
* 'globally', they are still executed within the context of the working
* FormHelperRequest.</p>
*
* @example
* formHelper.always.xhrSuccess = function() {
* if (this.status !== 'SUCCESS') {
* // A form submission was not successful!
* // Track an event perhaps?
* }
* };
* @example
* formHelper.always = {
* xhrError: function() {
* // XHR error - what to do?
* },
* status: {
* WEBSITE_BROKEN: '/' // Redirect home
* }
* };
* @namespace
* @property {FormRule.xhrReady} [xhrReady]
* @property {FormRule.xhrBeforeSend} [xhrBeforeSend]
* @property {FormRule.xhrSuccess} [xhrSuccess]
* @property {FormRule.xhrError} [xhrError]
* @property {FormRule.xhrComplete} [xhrComplete]
* @property {FormRule.onComplete} [onComplete]
* @property {Object.<String, FormRule.StatusHandler>} [status]
*/
formHelper.always = {
xhrReady: null,
xhrBeforeSend: null,
xhrSuccess: null,
xhrError: null,
xhrComplete: null,
onComplete: null,
status: {}
};
/**
* @namespace
* @memberOf formHelper.FormHelperRequest
*/
formHelper.FormHelperRequest.Defaults = {
/**
* @namespace
* @description Either change these globaly for a site by modifying these
* properties or on a form-by-form basis by specifying alternate values in
* a form rule.
*/
rule: {
/** @default */
disableControls: true,
/** @default */
scrollIntoViewOnUIUpdate: true,
/** @default */
focusFirstErroredControl: true,
/** @default */
releaseFormAndUpdateUIOnXHRSuccess: true
},
/**
* @namespace
* @description Yeah, don't change these...
*/
xhrOptions: {
/** @default */
cache: false,
/** @default */
dataType: 'json',
/** @default */
type: 'POST'
}
};
})(window.formHelper = window.formHelper || {}, jQuery);