(function($) {
var Alpaca = $.alpaca;
Alpaca.Form = Base.extend(
/**
* @lends Alpaca.Form.prototype
*/
{
/**
* @constructs
*
* @class This class is for managing HTML form control.
*
* @param {Object} container Field container.
* @param {Object} options Field options.
* @param {Object|String} view Field view.
* @param {Alpaca.Connector} connector Field connector.
* @param {Function} errorCallback Error callback.
*/
constructor: function(domEl, options, viewId, connector, errorCallback) {
// container
this.domEl = domEl;
// parent
this.parent = null;
this.connector = connector;
this.errorCallback = errorCallback;
// options
this.options = options;
if (this.options.attributes)
{
this.attributes = this.options.attributes;
}
else
{
this.attributes = {};
}
if (this.options.buttons)
{
if (this.options.buttons.submit)
{
if (!this.options.buttons.submit.type)
{
this.options.buttons.submit.type = 'submit';
}
if (!this.options.buttons.submit.name)
{
this.options.buttons.submit.name = 'submit';
}
if (!this.options.buttons.submit.value)
{
this.options.buttons.submit.value = 'Submit';
}
}
if (this.options.buttons.reset)
{
if (!this.options.buttons.reset.type)
{
this.options.buttons.reset.type = 'reset';
}
if (!this.options.buttons.reset.name)
{
this.options.buttons.reset.name = 'reset';
}
if (!this.options.buttons.reset.value)
{
this.options.buttons.reset.value = 'Reset';
}
}
// some general correction
for (var k in this.options.buttons)
{
if (this.options.buttons[k].label)
{
this.options.buttons[k].value = this.options.buttons[k].label;
}
if (this.options.buttons[k].title)
{
this.options.buttons[k].value = this.options.buttons[k].title;
}
if (!this.options.buttons[k].type)
{
this.options.buttons[k].type = "button";
}
}
}
if (this.attributes.id)
{
this.id = this.attributes.id;
}
else
{
this.id = Alpaca.generateId();
this.attributes.id = this.id;
}
// if we have a submit button specified, and toggleSubmitValidState isn't defined, set to true by default
// don't allow the form to submit unless valid
if (this.options.buttons && this.options.buttons.submit && Alpaca.isUndefined(this.options.toggleSubmitValidState))
{
this.options.toggleSubmitValidState = true;
}
this.viewType = options.viewType;
// set a runtime view
this.view = new Alpaca.RuntimeView(viewId, this);
// for each button, make sure that classes is set minimally to view.styles.button
for (var k in this.options.buttons)
{
if (!this.options.buttons[k].styles) {
this.options.buttons[k].styles = this.view.styles.button;
}
}
},
/**
* Renders this form into the container.
*
* @param {Function} callback
*/
render: function(callback)
{
var self = this;
// remove the previous form element if it exists
if (this.form)
{
this.form.remove();
}
// load the appropriate template and render it
this.processRender(this.domEl, function() {
// bind our field dom element into the container
self.form.appendTo(self.container);
// add default class
self.form.addClass("alpaca-form");
// CALLBACK: "form"
self.fireCallback("form");
// execute callback
callback(self);
});
},
afterInitialize: function()
{
var self = this;
if (self.options.toggleSubmitValidState) {
// adjust submit button state
self.adjustSubmitButtonState();
}
},
/**
* Determines whether the top control is entirely valid.
*
* @return {*}
*/
isFormValid: function()
{
// re-compute validation for the full control set
this.topControl.validate(true);
var valid = this.topControl.isValid(true);
//this.refreshValidationState(true);
return valid;
},
isValid: function()
{
return this.isFormValid();
},
validate: function(children)
{
return this.topControl.validate(children);
},
enableSubmitButton: function()
{
$(".alpaca-form-button-submit").attrProp("disabled", false);
if ($.mobile)
{
try { $(".alpaca-form-button-submit").button('refresh'); } catch (e) { }
}
},
disableSubmitButton: function()
{
$(".alpaca-form-button-submit").attrProp("disabled", true);
if ($.mobile)
{
try { $(".alpaca-form-button-submit").button('refresh'); } catch (e) { }
}
},
adjustSubmitButtonState: function()
{
this.disableSubmitButton();
if (this.isFormValid())
{
this.enableSubmitButton();
}
},
/**
* Responsible for fetching any templates needed so as to render the
* current mode for this field.
*
* Once completed, the onSuccess method is called.
*
* @param {Object} parentEl Field container.
* @param {Function} callback
*/
processRender: function(parentEl, callback)
{
var self = this;
// lookup the template we should use to render
this.formDescriptor = this.view.getTemplateDescriptor("form");
if (!this.formDescriptor)
{
return Alpaca.throwErrorWithCallback("Could not find template descriptor: form");
}
var renderedDomElement = Alpaca.tmpl(this.formDescriptor, {
id: this.getId(),
options: this.options,
view: this.view
});
renderedDomElement.appendTo(parentEl);
this.form = renderedDomElement;
// find our insertion point
// this is marked by the handlebars helper
this.formFieldsContainer = $(this.form).find("." + Alpaca.MARKER_CLASS_FORM_ITEMS_FIELD);
this.formFieldsContainer.removeClass(Alpaca.MARKER_CLASS_FORM_ITEMS_FIELD);
if (Alpaca.isEmpty(this.form.attr("id")))
{
this.form.attr("id", this.getId() + "-form-outer");
}
if (Alpaca.isEmpty(this.form.attr("data-alpaca-form-id")))
{
this.form.attr("data-alpaca-form-id", this.getId());
}
// the form field
$(parentEl).find("form").attr(this.attributes);
// populate the buttons as well
this.buttons = {};
$(parentEl).find(".alpaca-form-button").each(function() {
$(this).click(function(e) {
$(this).attr("button-pushed", true);
});
// custom click handler?
var key = $(this).attr("data-key");
if (key)
{
var buttonConfig = self.options.buttons[key];
if (buttonConfig)
{
if (buttonConfig.click)
{
$(this).click(function(form, handler) {
return function(e) {
e.preventDefault();
handler.call(form, e);
}
}(self, buttonConfig.click));
}
}
}
});
callback();
},
/**
* Returns the id of the form.
*
* @returns {String} Form id
*/
getId: function()
{
return this.id;
},
/**
* Returns form type.
*
* @returns {String} Form type.
*/
getType: function()
{
return this.type;
},
/**
* Returns this form's parent.
*
* @returns {Object} Form parent.
*/
getParent: function()
{
return this.parent;
},
/**
* Returns the value of the JSON rendered by this form.
*
* @returns {Any} Value of the JSON rendered by this form.
*/
getValue: function()
{
return this.topControl.getValue();
},
/**
* Sets the value of the JSON to be rendered by this form.
*
* @param {Any} value Value to be set.
*/
setValue: function(value)
{
this.topControl.setValue(value);
},
/**
* Initializes events handling (Form Submission) for this form.
*/
initEvents: function()
{
var _this = this;
var formTag = $(this.domEl).find("form");
var v = this.getValue();
$(formTag).submit(v, function(e) {
return _this.onSubmit(e, _this);
});
// listen for fieldupdates and determine whether the form is valid.
// if so, enable the submit button...
// otherwise, disable it
if (this.options.toggleSubmitValidState)
{
$(_this.topControl.getFieldEl()).bind("fieldupdate", function() {
_this.adjustSubmitButtonState();
});
$(_this.topControl.getFieldEl()).bind("fieldkeyup", function() {
_this.adjustSubmitButtonState();
});
$(_this.topControl.getFieldEl()).bind("fieldblur", function() {
_this.adjustSubmitButtonState();
});
}
},
getButtonEl: function(buttonId)
{
return $(this.domEl).find(".alpaca-form-button-" + buttonId);
},
/**
* Handles form submit events.
*
* @param {Object} e Submit event.
* @param {Object} form the form
*/
onSubmit: function(e, form)
{
if (!this.isFormValid())
{
e.stopPropagation();
this.refreshValidationState(true);
return false;
}
if (this.submitHandler)
{
e.stopPropagation();
var v = this.submitHandler(e, form);
if (Alpaca.isUndefined(v)) {
v = false;
}
return v;
}
},
/**
* Registers a custom submit handler.
*
* @param {Object} func Submit handler to be registered.
*/
registerSubmitHandler: function (func)
{
if (Alpaca.isFunction(func))
{
this.submitHandler = func;
}
},
/**
* Displays validation information of all fields of this form.
*
* @param {Boolean} children whether to render validation state for child fields
*
* @returns {Object} Form validation state.
*/
refreshValidationState: function(children, callback)
{
this.topControl.refreshValidationState(children, callback);
},
/**
* Disables this form.
*/
disable: function()
{
this.topControl.disable();
},
/**
* Enables this form.
*/
enable: function()
{
this.topControl.enable();
},
/**
* Focuses on this form.
*
* If a callback is provided, the callback receives the focused control.
*/
focus: function(onFocusCallback)
{
this.topControl.focus(function(controlWithFocus) {
if (onFocusCallback)
{
onFocusCallback(controlWithFocus);
}
});
},
/**
* Purge any event listeners and remove the form from the DOM.
*
* @param [Boolean] skipParent when true, the form cleans up without traversing through parent child controls
*/
destroy: function(skipParent)
{
this.getFormEl().remove();
// we allow form.destroy() which tells parent control to destroy
// if skipParent == true, then we do not call up (invoked from container)
if (!skipParent && this.parent)
{
this.parent.destroy();
}
},
/**
* Shows the form.
*/
show: function()
{
this.getFormEl().css({
"display": ""
});
},
/**
* Hides the form.
*/
hide: function()
{
this.getFormEl().css({
"display": "none"
});
},
/**
* Clears the form and resets values of its fields.
*
* @param stopUpdateTrigger If false, triggers the update event of this event.
*/
clear: function(stopUpdateTrigger)
{
this.topControl.clear(stopUpdateTrigger);
},
/**
* Checks if form is empty.
*
* @returns {Boolean} True if the form is empty, false otherwise.
*/
isEmpty: function()
{
return this.topControl.isEmpty();
},
/**
* Fires a view callback for the current form.
*
* @param id
* @param arg1
* @param arg2
* @param arg3
* @param arg4
* @param arg5
*/
fireCallback: function(id, arg1, arg2, arg3, arg4, arg5)
{
this.view.fireCallback(this, id, arg1, arg2, arg3, arg4, arg5);
},
/**
* Retrieves the form element.
*
* @returns {Object} The rendered DOM element.
*/
getFormEl: function() {
return this.form;
},
/**
* Performs a regular old submit.
*/
submit: function()
{
this.form.submit();
},
/**
* Fires the submit in the background and hands back the jQuery promise.
*
* An optional config can be passed in to control the underlying jQuery ajax XHR.
*
* @returns {*}
*/
ajaxSubmit: function(config)
{
var self = this;
if (!config) {
config = {};
}
config.url = self.options.attributes.action;
config.type = self.options.attributes.method;
if (!config.data) {
config.data = this.getValue();
}
if (!config.dataType) {
config.dataType = "json";
}
if (!config.headers) {
config.headers = {};
}
// support CSRF here
var csrfToken = self.determineCsrfToken();
if (csrfToken) {
config.headers[Alpaca.CSRF_HEADER_NAME] = csrfToken;
}
return $.ajax(config);
},
determineCsrfToken: function()
{
// is there a direct token specified?
var csrfToken = Alpaca.CSRF_TOKEN;
if (!csrfToken)
{
// is there a cookie that we can pull the value from?
for (var t = 0; t < Alpaca.CSRF_COOKIE_NAMES.length; t++)
{
var cookieName = Alpaca.CSRF_COOKIE_NAMES[t];
var cookieValue = Alpaca.readCookie(cookieName);
if (cookieValue)
{
csrfToken = cookieValue;
break;
}
}
}
return csrfToken;
}
});
})(jQuery);