ssd-form
Version:
jQuery plugin to handle form validation and submission
945 lines (558 loc) • 25.4 kB
JavaScript
/*
* ssdForm jQuery plugin
* Examples and documentation at: https://github.com/sebastiansulinski/ssd-form
* Copyright (c) 2017 Sebastian Sulinski
* Version: 1.5.5 (27-DEC-2017)
* Licensed under the MIT.
* Requires: jQuery v1.9 or later
*/
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module unless amdModuleId is set
define(['jquery'], function (a0) {
return (factory(a0));
});
} else if (typeof exports === 'object') {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like environments that support module.exports,
// like Node.
module.exports = factory(require('jquery'));
} else {
factory(root["jQuery"]);
}
}(this, function ($) {
(function() {
"use strict";
$.fn.ssdForm = function (options) {
"use strict";
var settings = $.extend({
dataFormWrapper: 'data-form-wrapper',
dataConfirmation: 'data-confirmation',
dataValidationSegment: 'data-validation',
dataValidationCase: 'data-case',
dataSubmitTrigger: 'data-submit-trigger',
dataSubmitPending: 'data-submit-pending',
classCkEditor: 'editor',
classHide: 'hide',
classShow: 'show',
extendBehaviours: {},
extendValidationRules: {},
postAjaxSuccess: function(form, form_model, data) {
form.successBehaviour(form_model, data);
},
postAjaxFailure: function(form, form_model, jqXHR, textStatus, errorThrown) {
form.endRequestDisplayErrors(form_model, jqXHR.responseJSON);
},
ignoreElements: '.button, [disabled]',
serializeAttribute: 'name',
actionMethod: function(form, form_model, success, error) {
$.ajax({
method: form_model.method(),
url: form_model.action(),
data: form_model.data(),
dataType: 'json',
cache: false,
success: success,
error: error
});
}
}, options),
formWrapper = '[' + settings.dataFormWrapper + ']',
formConfirmation = '[' + settings.dataConfirmation + ']',
formValidationSegment = '[' + settings.dataValidationSegment + ']',
formValidationCase = '[' + settings.dataValidationCase + ']',
formSubmitTrigger = '[' + settings.dataSubmitTrigger + ']',
formSubmitPending = '[' + settings.dataSubmitPending + ']';
var ErrorCollection = function() {
"use strict";
var errors = {};
this.add = function(key, index) {
"use strict";
if (errors.hasOwnProperty(key)) {
return false;
}
errors[key] = index;
};
this.all = function() {
"use strict";
return errors;
};
this.empty = function() {
return Object.keys(errors).length === 0;
};
};
var Document = {
replace: function(data) {
"use strict";
if (data.replace === undefined) {
return;
}
$.each(data.replace, function(key, value) {
$('[data-' + key + ']').html(value);
});
},
remove: function(data) {
"use strict";
if (data.remove === undefined) {
return;
}
$.each(data.remove, function(key, value) {
$('[data-' + value + ']').remove();
});
},
reload: function() {
"use strict";
window.location.reload(true);
},
redirect: function(url) {
"use strict";
window.location.href = url;
}
};
var ValidatorRule = $.extend({
elements: [],
required: function(element) {
"use strict";
return ( ! (
element.value === '' ||
element.value === null ||
element.value === undefined
));
},
checked: function(element) {
"use strict";
return element.isChecked;
},
radio: function(element) {
"use strict";
return this.checked(element);
},
value_is: function(element) {
"use strict";
return element.value === element.rules_collection[0];
},
email: function(element) {
"use strict";
var pattern = /^[a-zA-Z0-9._\-]+@[a-zA-Z0-9]+([.\-]?[a-zA-Z0-9]+)?([\.]{1}[a-zA-Z]{2,4}){1,4}$/;
return pattern.test(element.value);
},
password: function(element) {
"use strict";
var pattern = /(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{6,}/;
return pattern.test(element.value);
},
min: function(element) {
"use strict";
return element.value.length >= element.rules_collection[0];
},
max: function(element) {
"use strict";
return element.value.length <= element.rules_collection[0];
},
confirmed: function(element) {
"use strict";
var confirmation_field = element.name + '_confirmation',
confirmation = this.elements.filter(function (el) {
return el.name === confirmation_field;
})[0];
if (!confirmation) {
throw new Error(element.name + '_confirmation' + ' field is missing');
}
return confirmation.value === element.value;
},
regex: function(element) {
"use strict";
var regex = new RegExp(element.regex);
return regex.test(element.value);
},
test: function(elements, element) {
"use strict";
var $this = this,
deferred = $.Deferred();
$this.elements = elements;
$.each(element.rules, function(index, rules) {
var split = rules.split(':'),
rule = split.shift();
element.rules_collection = split;
if ( ! $this[rule](element)) {
deferred.reject(rule);
}
if ( (index + 1) === element.rules.length ) {
deferred.resolve();
}
});
return deferred.promise();
}
}, settings.extendValidationRules);
var Validator = function(form_model, error) {
"use strict";
if ( ! (form_model instanceof FormModel) ) {
throw new Error('Invalid argument form_model.');
}
if ( ! (error instanceof ErrorCollection) ) {
throw new Error('Invalid argument error.');
}
function shouldInclude(element) {
"use strict";
return (
element.isVisible ||
element.isEditor
);
}
function validate(elements, element) {
"use strict";
if ( ( $.inArray('required', element.rules) === -1 ) && element.value === '') {
return true;
}
return ValidatorRule.test(elements, element);
}
function end(i, length, deferred) {
"use strict";
if ( (i + 1) === length ) {
if (error.empty()) {
deferred.resolve();
} else {
deferred.reject(error.all());
}
}
}
this.run = function() {
"use strict";
var elements = form_model.inputs(),
length = elements.length,
deferred = $.Deferred();
$.each(elements, function(index, element) {
if ( ! shouldInclude(element)) {
end(index, length, deferred);
return true;
}
if ( element.rules === undefined || element.rules.length === 0 ) {
end(index, length, deferred);
return true;
}
element.rules = element.rules.split('|');
$.when(validate(elements, element))
.done(function() {
end(index, length, deferred);
})
.fail(function(rule) {
error.add(element.name, rule);
end(index, length, deferred);
});
});
return deferred.promise();
}
};
var FormBehaviour = $.extend({
redirect: function(form_model, data) {
"use strict";
if ( ! data.redirect ) {
throw new Error('Redirect entry missing.');
}
Document.redirect(data.redirect);
},
reload: function() {
"use strict";
Document.reload();
},
fadeOutShowMessage: function(form_model, data) {
"use strict";
var wrapper = form_model.instance().closest(formWrapper),
messageWrapper = wrapper.find(formConfirmation);
form_model.instance().fadeOut(200, function() {
messageWrapper.html(data.message).fadeIn(200);
});
},
fadeOutShowMessageRedirect: function(form_model, data) {
"use strict";
var wrapper = form_model.instance().closest(formWrapper),
messageWrapper = wrapper.find(formConfirmation);
form_model.instance().fadeOut(200, function() {
messageWrapper.html(data.message).fadeIn(200, function() {
setTimeout(function() {
window.location.href = data.redirect;
}, 3000);
});
});
},
fadeOutShowMessageReload: function(form_model, data) {
"use strict";
var wrapper = form_model.instance().closest(formWrapper),
messageWrapper = wrapper.find(formConfirmation);
form_model.instance().fadeOut(200, function() {
messageWrapper.html(data.message).fadeIn(200, function() {
setTimeout(function() {
window.location.reload(true);
}, 3000);
});
});
},
fadeOutShowMessageResetFadeIn: function(form_model, data) {
"use strict";
var wrapper = form_model.instance().closest(formWrapper),
messageWrapper = wrapper.find(formConfirmation);
form_model.instance().fadeOut(200, function() {
Form.endRequest(form_model);
messageWrapper.html(data.message).fadeIn(200);
setTimeout(function() {
messageWrapper.fadeOut(200, function() {
form_model.instance()[0].reset();
form_model.instance().fadeIn(200);
});
}, 3000);
});
},
callReplaceRemove: function(form_model, data) {
"use strict";
Document.replace(data);
Document.remove(data);
},
ask: function(form_model, data) {
"use strict";
if ( ! this.hasOwnProperty(data.behaviour)) {
throw new Error('Behaviour not specified.');
}
this[data.behaviour](form_model, data);
},
run: function(form_model, data) {
"use strict";
var behaviour = form_model.successBehaviour();
if ( ! this.hasOwnProperty(behaviour)) {
throw new Error('Behaviour does not exist.');
}
this[behaviour](form_model, data);
}
}, settings.extendBehaviours);
var FormModel = function(form) {
"use strict";
var data = form.serializeArray(),
inputs = serialize(form.find(':input').not(settings.ignoreElements)),
method = form.prop('method'),
action = form.prop('action'),
successBehaviour = form.data('success-behaviour');
function serialize(elements) {
"use strict";
var serializedArray = [],
arrays = [],
radios = [];
elements.each(function(index, element) {
var $this = $(element),
nameAttribute = $this.attr(settings.serializeAttribute);
if (!nameAttribute) {
return;
}
var fullName = nameAttribute.split('['),
id = $this.attr('id'),
params = {
name: fullName[0],
type: element.type,
rules: $this.data('validate'),
isArray: fullName.length > 1,
isChecked: $this.is(':checked'),
isVisible: $this.is(':visible'),
isEditor: $this.hasClass('editor'),
regex: $this.data('regex')
};
if ($this.hasClass(settings.classCkEditor)) {
params.value = window.CKEDITOR.instances[id].getData();
replaceCkEditor(id, params.value);
} else {
params.value = $this.val();
}
if (params.type === 'radio') {
if (radios[params.name]) {
if (!params.isChecked) {
return;
}
params.rules = serializedArray[radios[params.name]].rules;
serializedArray.splice(radios[params.name], 1, params);
return;
}
radios[params.name] = serializedArray.length;
}
if (params.isArray) {
if (arrays[params.name]) {
if (params.type === 'checkbox' && !params.isChecked) {
return;
}
serializedArray[arrays[params.name]].value.push(params.value);
return;
} else {
params.value = [params.value];
if (params.type === 'checkbox' && !params.isChecked) {
params.value = [];
}
arrays[params.name] = serializedArray.length;
}
}
serializedArray.push(params);
});
return serializedArray;
}
function replaceCkEditor(name, value) {
$.each(data, function(index, item) {
if (item.name === name) {
data[index].value = value;
}
});
}
function validateOptions() {
"use strict";
if (method === undefined) {
method = 'post';
}
if (action === undefined) {
throw new Error('No action defined.');
}
}
validateOptions();
this.instance = function() {
"use strict";
return form;
};
this.method = function() {
"use strict";
return method;
};
this.action = function() {
"use strict";
return action;
};
this.successBehaviour = function() {
"use strict";
return successBehaviour;
};
this.inputs = function() {
"use strict";
return inputs;
};
this.data = function() {
"use strict";
return data;
};
this.validate = function() {
"use strict";
return (new Validator(this, new ErrorCollection)).run();
};
this.reset = function() {
"use strict";
form.trigger('reset');
}
};
var Form = {
clearErrors: function(form_model) {
"use strict";
form_model.instance().find(
formValidationSegment + ' ' + formValidationCase
).removeClass(settings.classShow);
},
displayAlert: function(content) {
"use strict";
// TODO - to implement
},
displayErrors: function(form_model, errors) {
"use strict";
var index;
for (var key in errors) {
if (errors.hasOwnProperty(key)) {
if (key === 'alert') {
this.displayAlert(errors[key]);
continue;
}
index = $.isArray(errors[key]) ? errors[key][0] : errors[key];
form_model
.instance()
.find(
'[' + settings.dataValidationSegment + '="' + key + '"] ' +
'[' + settings.dataValidationCase + '="' + index + '"]'
)
.addClass(settings.classShow);
}
}
},
beginRequest: function(form_model) {
"use strict";
if (form_model.instance().data('quiet')) {
return true;
}
form_model.instance().find(formSubmitTrigger).addClass(settings.classHide);
form_model.instance().find(formSubmitPending).removeClass(settings.classHide);
},
endRequest: function(form_model) {
"use strict";
if (form_model.instance().data('quiet')) {
return true;
}
form_model.instance().find(formSubmitTrigger).removeClass(settings.classHide);
form_model.instance().find(formSubmitPending).addClass(settings.classHide);
},
successBehaviour: function(form_model, data) {
"use strict";
FormBehaviour.run(form_model, data);
},
disableForm: function(form_model) {
"use strict";
this.submitted = form_model;
},
enableForm: function() {
"use strict";
this.submitted = null;
},
endRequestDisplayErrors: function(form_model, errors) {
"use strict";
this.endRequest(form_model);
this.displayErrors(form_model, errors);
},
endRequestDisplayErrorsReset: function(form_model, errors) {
"use strict";
this.endRequestDisplayErrors(form_model, errors);
form_model.reset();
},
formAction: function(form_model, success, error, fail) {
"use strict";
var $this = this;
$.when(form_model.validate())
.done(function(data, textStatus, jqXHR) {
$this.beginRequest(form_model);
settings.actionMethod($this, form_model, success, error);
})
.fail(fail);
},
submit: function(instance) {
"use strict";
var $this = this;
$(instance).on('submit', function(event) {
event.preventDefault();
var form_model = new FormModel($(this));
$this.clearErrors(form_model);
$this.formAction(
form_model,
function(data) {
settings.postAjaxSuccess(
$this, form_model, data
);
},
function(jqXHR, textStatus, errorThrown) {
settings.postAjaxFailure(
$this,
form_model,
jqXHR,
textStatus,
errorThrown
)
},
function(errors) {
$this.endRequestDisplayErrors(form_model, errors);
}
);
});
},
init: function(instance) {
"use strict";
this.submit(instance);
}
};
return Form.init(this);
};
})();
}));