UNPKG

webform-toolkit

Version:

Create a HTML form with field validation and custom errors.

536 lines (497 loc) 14.3 kB
/** * 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 */ 'use strict'; /** * @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