webform-toolkit
Version:
Create a HTML form with field validation and custom errors.
536 lines (497 loc) • 14.3 kB
JavaScript
/**
* webform-toolkit
* Create a HTML form with field validation and custom errors.
*
* Copyright 2012-2024, Marc S. Brooks (https://mbrooks.info)
* Licensed under the MIT license:
* http://www.opensource.org/licenses/mit-license.php
*/
;
/**
* @param {Element} container
* Containing HTML element.
*
* @param {Function} setttings
* Webform form settings.
*
* @param {Function} callback
* Returns defined webform values.
*/
function WebformToolkit(container, settings, callback) {
var self = this;
(function () {
var action = settings.action,
groups = settings.groups;
if (action && groups.length) {
renderWebform();
} else {
throw new Error('Failed to initialize (missing settings)');
}
})();
/**
* Create new instance of Webform-Toolkit
*/
function renderWebform() {
var form = createForm();
container.appendChild(form);
setButtonState(form);
}
/**
* Create form field elements.
*
* @return {Element}
*/
function createForm() {
var form = document.createElement('form');
form.classList.add('webform');
// Set POST action URI/URL
if (settings != null && settings.action) {
form.setAttribute('method', 'POST');
form.setAttribute('enctype', 'multipart/form-data');
form.setAttribute('action', settings.action);
}
// Create hidden elements, if POST parameters exist.
if (settings != null && settings.params) {
var params = settings.params.split('&');
for (var i = 0; i < params.length; i++) {
var param = params[i].split('=');
var elm = createInputElm({
type: 'hidden',
name: param[0],
value: param[1]
});
form.appendChild(elm);
}
}
// Create field elements.
for (var _i = 0; _i < settings.groups.length; _i++) {
var group = settings.groups[_i];
var fieldset = document.createElement('fieldset');
fieldset.classList.add('field-group' + _i);
// .. Legend, if exists.
if (group.legend) {
var legend = document.createElement('legend');
legend.textContent = group.legend;
fieldset.appendChild(legend);
}
for (var j = 0; j < group.fields.length; j++) {
var _elm = createField(form, group.fields[j]);
fieldset.appendChild(_elm);
}
form.appendChild(fieldset);
}
// Create submit button.
if ((settings == null ? void 0 : settings.submit) !== false) {
var div = document.createElement('div');
div.classList.add('form-submit');
var button = document.createElement('input');
button.setAttribute('type', 'submit');
button.setAttribute('value', 'Submit');
div.appendChild(button);
form.appendChild(div);
}
// Bind form submit event.
form.addEventListener('submit', function (event) {
event.preventDefault();
if (checkErrorsExist(form)) {
return;
}
// Return callback with form object response.
if (typeof callback === 'function') {
callback(Object.fromEntries(new FormData(form)));
}
// POST form values.
else {
self.submit();
}
});
return form;
}
/**
* Create field elements.
*
* @param {Element} form
* HTML form element.
*
* @param {Object} config
* Field configuration.
*
* @return {Element}
*/
function createField(form, config) {
var elm = null;
// Supported elements
switch (config.type) {
case 'color':
case 'date':
case 'email':
case 'hidden':
case 'number':
case 'password':
case 'quantity':
case 'range':
case 'submit':
case 'text':
case 'time':
elm = createInputElm(config);
break;
case 'file':
elm = createFileElm(config);
break;
case 'textarea':
elm = createTextAreaElm(config);
break;
case 'select':
elm = createMenuElm(config);
break;
case 'radio':
elm = createRadioElm(config);
break;
case 'checkbox':
elm = createCheckBoxElm(config);
break;
default:
throw new Error("Invalid field type: " + config.type);
}
(config == null ? void 0 : config.id) && elm.setAttribute('id', config.id);
if (config.type === 'hidden' || config.type === 'submit') {
return elm;
}
var div = document.createElement('div');
// .. Label, if exists.
if (config.type !== 'checkbox') {
var label = document.createElement('label');
label.setAttribute('for', config.id);
if (config.required) {
var span = document.createElement('span');
span.classList.add('required');
label.appendChild(span);
}
label.textContent = config.label;
div.appendChild(label);
}
// Filter with REGEX
if (config != null && config.filter) {
elm.regex = config.filter;
elm.message = config.error;
elm.error = false;
// Attach field events.
var handler = function handler() {
validateField(this), setButtonState(form);
};
if (config.type === 'select') {
// .. Select menu
elm.addEventListener('change', handler);
} else {
// .. Everything else.
elm.addEventListener('focusout', handler);
elm.addEventListener('keypress', handler);
elm.addEventListener('keyup', handler);
elm.addEventListener('mouseout', handler);
}
}
div.appendChild(elm);
// .. Description, if exists.
if (config != null && config.description) {
var block = document.createElement('p');
block.classList.add('description');
block.setAttribute('role', 'info');
block.textContent = config.description;
div.appendChild(block);
}
return div;
}
/**
* Create input element.
*
* @param {Object} config
* Field configuration.
*
* @return {Element}
*/
function createInputElm(config) {
var input = document.createElement('input');
// .. Field attributes
if (config.type) {
input.setAttribute('type', config.type);
}
if (config.name) {
input.setAttribute('name', config.name);
}
if (config.value) {
input.setAttribute('value', config.value);
}
if (config != null && config.maxlength && (config.type === 'password' || config.type === 'text')) {
input.setAttribute('maxlength', config.maxlength);
}
if (config != null && config.max || config != null && config.min || config != null && config.step && (config.type === 'number' || config.type === 'quantity')) {
input.setAttribute('max', config.max);
input.setAttribute('min', config.min);
if (config.type === 'step') {
input.setAttribute('step', config.step);
}
}
if (config != null && config.placeholder) {
input.setAttribute('placeholder', config.placeholder);
}
input.required = !!config.required;
return input;
}
/**
* Create file element.
*
* @param {Object} config
* Field configuration.
*
* @return {Elememt}
*/
function createFileElm(config) {
var input = document.createElement('input');
input.setAttribute('type', 'file');
// .. Field attributes
if (config.name) {
input.setAttribute('name', config.name);
}
return input;
}
/**
* Create select menu element.
*
* @param {Object} config
* Field configuration.
*
* @return {Element}
*/
function createMenuElm(config) {
var select = document.createElement('select');
select.classList.add('menu');
select.setAttribute('name', config.name);
var opts = config.filter.split('|');
var first = false;
// .. First option (custom)
if (config != null && config.value) {
opts.unshift(config.value);
first = true;
}
// .. Select options
for (var i = 0; i < opts.length; i++) {
var val = opts[i];
var option = document.createElement('option');
option.textContent = val;
if (!first) {
option.setAttribute('value', val);
} else {
first = false;
}
if (val == config.value) {
option.selected = true;
}
select.appendChild(option);
}
select.required = !!config.required;
return select;
}
/**
* Create radio button elements.
*
* @param {Object} config
* Field configuration.
*
* @return {Element}
*/
function createRadioElm(config) {
var div = document.createElement('div');
div.classList.add('radios');
var opts = config.filter.split('|');
for (var i = 0; i < opts.length; i++) {
var val = opts[i];
var input = document.createElement('input');
input.setAttribute('type', 'radio');
input.setAttribute('name', config.name);
input.setAttribute('value', val);
if (val == config.value) {
input.checked = true;
}
var span = document.createElement('span');
span.textContent = val;
div.appendChild(input);
div.appendChild(span);
}
return div;
}
/**
* Create checkbox element.
*
* @param {Object} config
* Field configuration.
*
* @return {Element}
*/
function createCheckBoxElm(config) {
var div = document.createElement('div');
div.classList.add('checkbox');
var label = document.createElement('span');
label.textContent = config.label;
var input = document.createElement('input');
input.setAttribute('type', 'checkbox');
input.setAttribute('name', config.name);
input.setAttribute('value', config.value);
if (config.value) {
input.checked = true;
}
input.required = !!config.required;
div.appendChild(input);
div.appendChild(label);
return div;
}
/**
* Create textarea element.
*
* @param {Object} config
* Field configuration.
*
* @return {Element}
*/
function createTextAreaElm(config) {
var textarea = document.createElement('textarea');
textarea.setAttribute('name', config.name);
if (config.placeholder) {
textarea.setAttribute('placeholder', config.placeholder);
}
textarea.required = !!config.required;
return textarea;
}
/**
* Validate form element value.
*
* @param {Element} elm
* HTML input element.
*
* @return {Boolean}
*/
function validateField(elm) {
var val = elm == null ? void 0 : elm.value;
if (!val) {
return;
}
var regex = elm.regex,
error = elm.error,
message = elm.message;
var search = new RegExp(regex, 'g');
var match = false;
// .. REGEX by type
switch (elm.tagName) {
case 'INPUT':
match = search.test(val);
break;
case 'SELECT':
match = search.test(val);
break;
case 'TEXTAREA':
match = search.test(val);
break;
}
var field = elm.parentNode;
var label = field.querySelector('label');
var errorId = "error-" + elm.id;
var block = document.getElementById(errorId) || document.createElement('p');
// Toggle error message visibility.
if (match === false && error === false) {
label.setAttribute('aria-invalid', 'true');
block.classList.add('error-message');
block.setAttribute('id', errorId);
block.setAttribute('aria-invalid', 'true');
block.textContent = message;
field.appendChild(block);
elm.classList.add('error-on');
elm.setAttribute('aria-describedBy', errorId);
elm.setAttribute('aria-invalid', 'true');
elm.error = true;
block.style.display = 'block';
block.style.opacity = 0;
// Show error message.
(function fadeIn() {
var val = parseFloat(block.style.opacity);
if ((val += 0.1) > 1 === false) {
block.style.opacity = val;
window.requestAnimationFrame(fadeIn);
}
})();
} else if (match === true && error === true) {
elm.error = false;
// Hide error message.
(function fadeOut() {
if ((block.style.opacity -= 0.1) < 0.1) {
label.removeAttribute('aria-invalid');
elm.classList.remove('error-on');
elm.removeAttribute('aria-describedBy');
elm.removeAttribute('aria-invalid');
block.style.display = 'none';
block.remove();
} else {
window.requestAnimationFrame(fadeOut);
}
})();
}
return true;
}
/**
* Enable/Disable submit button.
*
* @param {Element} form
* HTML form element.
*/
function setButtonState(form) {
var button = form.querySelector('input[type="submit"]');
if (button) {
button.disabled = checkErrorsExist(form);
} else {
throw new Error('Failed to change submit state (missing field)');
}
}
/**
* Return true, if form errors exist.
*
* @param {Element} form
* HTML form element.
*
* @return {Boolean}
*/
function checkErrorsExist(form) {
var fields = form.querySelectorAll('input, select, textarea');
for (var i = 0; i < fields.length; i++) {
var field = fields[i];
// Supported elements.
if (field.tagName == 'INPUT' && !/text|password|radio|checkbox/.test(field.type)) {
continue;
}
// Do errors exist?
if (field != null && field.required && (!field.value || (field == null ? void 0 : field.selectedIndex) <= 0) || field.error) {
return true;
}
}
}
/**
* Protected members.
*/
self.create = function (config, callback) {
var form = container.querySelector('form');
var elm = createField(form, config);
if (form && elm && typeof callback === 'function') {
callback(form, elm);
} else {
throw new Error("Failed to create field: " + elm.name + " (malformed config)");
}
};
return self;
}
/**
* Set global/exportable instance, where supported.
*/
window.webformToolkit = function (container, settings, options) {
return new WebformToolkit(container, settings, options);
};
if (typeof module !== 'undefined' && module.exports) {
module.exports = WebformToolkit;
}
//# sourceMappingURL=webform-toolkit.js.map