UNPKG

formiga

Version:

The simplest -yet effective- form validator for React

850 lines (787 loc) 26.2 kB
/** * formiga v1.9.0 * * https://www.afialapis.com/os/formiga * * Copyright (c) Donato Lorenzo <donato@afialapis.com> * * This source code is licensed under the MIT license found in the * LICENSE.md file in the root directory of this source tree. * * @license MIT */ /* eslint-disable */ import { useState, useCallback, useEffect, useRef } from 'react'; // try { // if ("production" !== "production") { // LOG_ENABLED= true // } // } catch(_){} var log = (w, s) => { { return; } }; var log_input = (inputNode, s) => { // const value = (inputNode?.value || inputNode?.checked)?.toString() || '' // const msg= `${inputNode.name} (t: ${inputNode.type}, #${inputNode?.id || ''}, v: ${value}) => ${s}` // log('input', msg) }; var attachFormValidationListener = (formNode, handler) => { var formValidityListener = event => { var _event$detail, _event$detail2, _event$detail3; log('form', "formiga-form-change on ".concat(event === null || event === void 0 || (_event$detail = event.detail) === null || _event$detail === void 0 ? void 0 : _event$detail.name, " (t: ").concat(event === null || event === void 0 || (_event$detail2 = event.detail) === null || _event$detail2 === void 0 ? void 0 : _event$detail2.type, ", v: ").concat(event === null || event === void 0 || (_event$detail3 = event.detail) === null || _event$detail3 === void 0 || (_event$detail3 = _event$detail3.value) === null || _event$detail3 === void 0 ? void 0 : _event$detail3.toString(), ")")); handler(event.detail.source); }; formNode.addEventListener('formiga-form-change', formValidityListener); // clean listeners function var removeAllChangeListeners = () => { if (formNode != undefined) { formNode.removeEventListener('formiga-form-change', formValidityListener); } }; return removeAllChangeListeners; }; //import {log} from '../helpers/log' var getInputValue = input => { if (!input) { return undefined; } var inputType = input.type.toLowerCase(); if (inputType == 'checkbox') { return input.checked; } if (inputType == 'select-multiple') { // const options= Array.prototype.slice.call(input.options) // const value = options // .filter((opt) => opt.selected) // .map((opt) => opt.value) var options = Array.from(input.options); var value = options.filter(opt => opt.selected).map(opt => opt.value); return value; } if (inputType == 'file') { try { return input === null || input === void 0 ? void 0 : input.files[0]; } catch (e) { console.error("Formiga: error on input ".concat(input.name, " of type file: ").concat(e.message)); console.error(e); return undefined; } } /* // TO CHECK: When do we need this? if (input.value==undefined) { return '' } */ return input.value; }; var parseForCompare = (inputType, value) => { if (value === undefined) { return undefined; } if (value === null) { return null; } inputType = inputType.toLowerCase(); if (inputType === 'text' || inputType === 'select' || inputType === 'select-one') { return value.toString(); } if (inputType === 'number') { if (value === '' || isNaN(value)) { return undefined; } return parseFloat(value); } if (inputType === 'checkbox') { if (value === true || value === 'true' || value === 1 || value === '1') { return true; } return false; } if (inputType === 'color') { if (value) { return value.toLowerCase(); } } /* if (inputType==='color') { if (value===undefined || value==='') { return undefined } return colorToHex(value) } if (inputType==='date') { if (value==='' || value === undefined) { return undefined } let vdate= undefined if (value instanceof Date) { vdate= value } if (typeof value === 'string') { vdate= new Date(value) } if (typeof value === 'number') { vdate= new Date(value * 1000) } try { vdate= new Date(value) } catch(e) { console.error(`Formiga: input of type date cannot convert value ${value} to Date`) return undefined } const tdate= `${vdate.getFullYear()}/${vdate.getMonth()+1}/${vdate.getDate()}` return tdate } */ if (inputType === 'select-multiple') { try { return value.sort().join(','); } catch (e) {} return ''; } return value.toString(); }; function getOriginalValueFromNode(node) { var originalValue = node.getAttribute('data-formiga-original-value'); if (originalValue == 'undefined') { originalValue = undefined; } else if (originalValue == 'null') { originalValue = null; } else if (originalValue == 'true') { originalValue = true; } else if (originalValue == 'false') { originalValue = false; } else if (node.type === 'select-multiple') { originalValue = originalValue.split(','); } return originalValue; } function compareOriginalValue(node) { var value = getInputValue(node); var originalValue = getOriginalValueFromNode(node); var valueCompare = node.type === 'file' ? node.files.length > 0 ? node.files[0].name : undefined : parseForCompare(node.type, value); var originalCompare = node.type === 'file' ? originalValue : parseForCompare(node.type, originalValue); return valueCompare !== originalCompare; } var getElementFromInput = node => { var name = node.name; var type = node.type || node.getAttribute('type'); // For some reason, node.validationMessage is not reliable when // accessing it from here. // const validationMessage= node.validationMessage var validationMessage = node.getAttribute('data-formiga-validity'); var value = getInputValue(node); var valid = validationMessage == ''; var originalValue = getOriginalValueFromNode(node); var hasChanged = compareOriginalValue(node); return { name, type, valid, validationMessage, value, originalValue, hasChanged }; }; var getFormElementsFromNode = node => { var formElements = node === null || node === void 0 ? void 0 : node.elements; if (!formElements) { return []; } var elements = []; for (var idx = 0; idx < formElements.length; idx++) { var el = formElements.item(idx); if (el.getAttribute('data-formiga-input') !== '1') { continue; } var element = getElementFromInput(el); elements.push(element); } elements.sort((a, b) => a.name - b.name); return elements; }; var useForm = () => { var [formNode, setFormNode] = useState(undefined); var [elements, setElements] = useState([]); // // Ref callback // Inits the elements array // var formRef = useCallback(node => { if (node != null) { try { node.noValidate = true; } catch (e) { console.error(e); } try { node.setAttribute('data-formiga-loaded', '1'); } catch (e) { console.error(e); } setFormNode(node); // Init elements var nElements = getFormElementsFromNode(node); setElements(nElements); } }, []); var updateForm = useCallback(source => { log('form', "updateForm callback. Changed input: ".concat(source === null || source === void 0 ? void 0 : source.name)); setElements(prevElements => { return prevElements.map(el => { if (el.name == (source === null || source === void 0 ? void 0 : source.name)) { return getElementFromInput(source); } return el; }); }); }, []); useEffect(() => { if (formNode == undefined) { return; } var removeAllChangeListeners = attachFormValidationListener(formNode, updateForm); return removeAllChangeListeners; }, [formNode, updateForm]); var valid = !elements.some(el => !el.valid); JSON.stringify(elements.filter(el => el.hasChanged === true)); var hasChanged = elements.some(el => el.hasChanged === true); return { ref: formRef, node: formNode, valid, hasChanged, elements }; }; var useCheckProps = (inputNode, _ref) => { var { doRepeat, doNotRepeat, inputFilter } = _ref; useEffect(() => { }, [inputNode, doRepeat, doNotRepeat, inputFilter]); }; /* * Predefined input filter by Formiga */ var FORMIGA_INPUT_FILTERS = { 'int': /^-?\d+$/, 'uint': /^\d+$/, 'float': /^-?\d*[.,]?\d*$/, //'float' : /[+-]?([0-9]*[.])?[0-9]+/, 'dollar': /^-?\d*[.,]?\d{0,2}$/, //'euro' : /^-?\d*[.,]?\d{0,2}$/, 'latin': /^[a-z ]*$/i, 'hexadecimal': /^[0-9a-f]*$/i }; var _fltBase = regex => { return v => { if (v === undefined || v === '') { return true; } return regex.test(v); }; }; var makeInputFilter = (inputFilter, inputName) => { if (inputFilter == undefined || inputFilter === '') { return undefined; } if (typeof inputFilter === 'string') { var regex = FORMIGA_INPUT_FILTERS[inputFilter]; if (regex === undefined) { console.error("Formiga: error on Input Element (".concat(inputName, "). (").concat(inputFilter, ") is not a valid inputFilter")); return undefined; } return _fltBase(regex); } if (inputFilter instanceof RegExp) { return _fltBase(inputFilter); } if (typeof inputFilter === "function") { return inputFilter; } console.error("Formiga: error on Input Element (".concat(inputName, "). (").concat(inputFilter, ") of type (").concat(typeof inputFilter, ") is not a valid inputFilter")); return undefined; }; // This event list would cover every need: // ['input', 'keydown', 'keyup', 'mousedown', 'mouseup', 'select', 'contextmenu', 'drop'], // But lets start simple and easy. var INPUT_FILTER_EVENT_TYPES = ['input', 'keydown', 'mousedown']; var useInputFilter = (inputRef, inputFilter) => { useEffect(() => { if (inputFilter == undefined) { return; } if (inputRef == undefined) { return; } var innerRef = (inputRef === null || inputRef === void 0 ? void 0 : inputRef.current) || inputRef; if (innerRef == undefined) { return; } if (innerRef.type.toLowerCase() != 'text') { return; } var allListeners = {}; // Input Filter listeners // Credits to: // https://stackoverflow.com/a/469362 // https://jsfiddle.net/emkey08/zgvtjc51 var theInputFilter = makeInputFilter(inputFilter, innerRef.name); // init auxiliar properties innerRef.oldValue = innerRef.value; var filterEventListener = function filterEventListener(event) { if (theInputFilter(event.target.value)) { event.target.oldValue = event.target.value; } else if (Object.hasOwnProperty.call(event.target, "oldValue")) { var selectionStart = event.target.selectionStart; var selectionEnd = event.target.selectionEnd; event.target.value = event.target.oldValue; try { event.target.setSelectionRange(selectionStart - 1, selectionEnd - 1); } catch (e) {} } else { event.target.value = ""; } }; INPUT_FILTER_EVENT_TYPES.forEach(function (eventType) { innerRef.addEventListener(eventType, filterEventListener); allListeners[eventType] = filterEventListener; }); // clean listeners function var removeAllChangeListeners = () => { if (innerRef != undefined) { Object.keys(allListeners).map(eventType => { innerRef.removeEventListener(eventType, allListeners[eventType]); }); } }; // return clean function return removeAllChangeListeners; }, [inputRef, inputFilter]); }; var useCheckboxEnsure = inputNode => { useEffect(() => { if (inputNode == undefined) { return; } var inputType = inputNode.type.toLowerCase(); // Ensure checkbox checked prop if (inputType === 'checkbox') { if (inputNode.value === 'true' || inputNode.value === true) { inputNode.setAttribute('checked', true); } } }, [inputNode]); }; var DEFAULT_MESSAGES = { badInput: 'Value is wrong', customError: 'Value does not match custom validity', patternMismatch: 'Value does not match expected pattern', rangeOverflow: 'Value is greater than expected', rangeUnderflow: 'Value is lesser than expected', stepMismatch: 'Value has an incorrect number of decimals', tooShort: 'Value is shorter than expected', tooLong: 'Value is longer than expected', typeMismatch: 'Value type is wrong', valueMissing: 'Value is required', valid: 'Value is not valid', // custom validations customAllowList: 'Value is not allowed', customDisallowList: 'Value is disallowed', customDoRepeat: 'Value must be repeated', customDoNotRepeat: 'Value cannot be repeated' }; var getDefaultMessage = n => DEFAULT_MESSAGES[n]; //import {log} from '../helpers/log' var countDecimals = f => { try { var s = parseFloat(f).toString(); if (s.indexOf('e-') > 0) { return parseInt(s.split('-')[1]); } return f.toString().split('.')[1].length; } catch (e) { return 0; } }; var _checkValidity = (input, transformValue, checkValue, allowedValues, disallowedValues, doRepeat, doNotRepeat, decimals) => { if (input == undefined) { return ''; } // NOTE Manage 'disable' prop? sure? if (input.disabled === true) { return ''; } input.name; var inputType = input.type.toLowerCase(); // Get input value var value = getInputValue(input); if (transformValue != undefined) { value = transformValue(value); } var isEmptyValue = value == undefined ? true : typeof value == 'string' ? value == '' : Array.isArray(value) ? value.length == 0 : false; //log('input', `${input.name} (${input.type}) #${input.id} checkValidity() checking...`) var vs = input.validity; if (vs != undefined) { if (vs.badInput) { return 'badInput'; } if (vs.patternMismatch) { return 'patternMismatch'; } if (vs.rangeOverflow) { return 'rangeOverflow'; } if (vs.rangeUnderflow) { return 'rangeUnderflow'; } if (vs.tooLong) { return 'tooLong'; } if (vs.tooShort) { return 'tooShort'; } if (vs.typeMismatch) { return 'typeMismatch'; } if (vs.valueMissing) { return 'valueMissing'; } if (decimals != undefined && !isNaN(decimals)) { // // For custom steppable inputs // if (decimals < countDecimals(value)) { return 'stepMismatch'; } } /*else if (input.step != undefined) { // // for non steppable inputs // if (vs.valid===false ) { return 'valid' } }*/else { if (vs.stepMismatch) { return 'stepMismatch'; } } } //log('input', `${input.name} (${input.type}) #${input.id} checkValidity() native validity is ok, doing custom checks...`) // When loading document, minlength/maxlength/step constraints are not checked // Check this pen: https://codepen.io/afialapis/pen/NWKJoPJ?editors=1111 // and /issues/validity_on_load if (input.maxLength && input.maxLength > 0 && value.length > input.maxLength) { return 'tooLong'; } if (input.minLength && input.minLength > 0 && value.length < input.minLength) { return 'tooShort'; } /*if (input.step!=undefined && input.step!=='' && input.step!=='any') { if (decimals==undefined || isNaN(decimals)) { if (countDecimals(input.step)!=countDecimals(value)) { return 'stepMismatch' } } }*/ // Some inputs like hidden and select, wont perform // the standard required validation if (input.required && isEmptyValue) { return 'valueMissing'; } // Custom validate function if (checkValue != undefined) { var result = checkValue(value); if (result == Promise.resolve(result)) { result.then(r => { if (!r) { return 'customError'; } }); } else { if (!result) { return 'customError'; } } } // Allowed values list if (allowedValues != undefined && !isEmptyValue) { var parsedAlloValues = allowedValues.map(v => parseForCompare(inputType, v)); var parsedValue = parseForCompare(inputType, value); var exists = parsedAlloValues.indexOf(parsedValue) >= 0; if (!exists) { return 'customAllowList'; } } // Disallowed values list if (disallowedValues != undefined) { var parsedDisaValues = disallowedValues.map(v => parseForCompare(inputType, v)); var _parsedValue = parseForCompare(inputType, value); var _exists = parsedDisaValues.indexOf(_parsedValue) >= 0; if (_exists) { return 'customDisallowList'; } } // Must repeat other's input value if (doRepeat != undefined && !isEmptyValue) { var otherInput = input.form.elements[doRepeat]; if (otherInput != undefined) { if (otherInput.value != value) { return 'customDoRepeat'; } } } // Do not repeat other's input value if (doNotRepeat != undefined && !isEmptyValue) { var _otherInput = input.form.elements[doNotRepeat]; if (_otherInput != undefined) { if (_otherInput.value == value) { return 'customDoNotRepeat'; } } } return ''; }; var checkValidity = (input, _ref) => { var { transformValue, checkValue, allowedValues, disallowedValues, doRepeat, doNotRepeat, decimals, validationMessage } = _ref; var chkValidity = _checkValidity(input, transformValue, checkValue, allowedValues, disallowedValues, doRepeat, doNotRepeat, decimals); var nValidity = chkValidity == '' ? '' : validationMessage != undefined && validationMessage != '' ? validationMessage : getDefaultMessage(chkValidity); return nValidity; }; //import getElementFromInput from '../../form/getElementFromInput.mjs' var setCustomValidationMessage = (node, validationMessage, transformValue) => { //if (node.form.getAttribute('data-formiga-loaded')!='1') { // log_input(node, `setCustomValidity( ${validity || 'ok'} )... skipping, form not ready yet`) // return //} // log_input(node, `setCustomValidity( ${validity || 'ok'} )`) var prevValidationMessage = node.getAttribute('data-formiga-validity'); var prevValue = node.getAttribute('data-formiga-value'); // Get input value var value = getInputValue(node); if (transformValue != undefined) { value = transformValue(value); } var valueStr = value != undefined ? value.toString() : ''; node.setCustomValidity(validationMessage); node.setAttribute('data-formiga-validity', validationMessage); node.setAttribute('data-formiga-value', valueStr); // Update form if (prevValue != valueStr || prevValidationMessage != validationMessage) { if (node.form != undefined) { if (node.form.getAttribute('data-formiga-loaded') == '1') { var event = new CustomEvent("formiga-form-change", { detail: { source: node } }); node.form.dispatchEvent(event); } } } }; var EVENT_TYPES = { 'checkbox': ['change'], // {change: 'click' , premature: []}, 'color': ['change', 'click'], // {change: 'change', premature: ['click']}, 'date': ['change', 'keyup', 'paste'], // {change: 'change', premature: ['keyup', 'paste']}, 'datetime-local': ['change', 'keyup', 'paste'], // {change: 'change', premature: ['keyup', 'paste']}, 'email': ['change', 'keyup', 'paste'], // {change: 'change', premature: ['keyup', 'paste']}, 'file': ['change'], // {change: 'change', premature: []}, 'hidden': ['change'], // {change: 'change', premature: []}, 'image': ['change'], // {change: 'change', premature: []}, 'month': ['change', 'keyup', 'paste'], // {change: 'change', premature: ['keyup', 'paste']}, 'number': ['change', 'keyup', 'paste'], // {change: 'change', premature: ['keyup', 'paste']}, 'password': ['change', 'keyup', 'paste'], // {change: 'change', premature: ['keyup', 'paste']}, 'radio': ['change', 'click'], // {change: 'change', premature: ['click']}, 'range': ['change', 'click'], // {change: 'change', premature: ['click']}, 'search': ['change', 'keyup', 'paste'], // {change: 'change', premature: ['keyup', 'paste']}, 'select-multiple': ['click'], // {change: 'click' , premature: []}, 'select-one': ['click'], // {change: 'click' , premature: []}, 'tel': ['change', 'keyup', 'paste'], // {change: 'change', premature: ['keyup', 'paste']}, 'text': ['change', 'keyup', 'paste'], // {change: 'change', premature: ['keyup', 'paste']}, 'textarea': ['change', 'keyup', 'paste'], // {change: 'change', premature: ['keyup', 'paste']}, 'time': ['change', 'keyup', 'paste'], // {change: 'change', premature: ['keyup', 'paste']}, 'url': ['change', 'keyup', 'paste'], // {change: 'change', premature: ['keyup', 'paste']}, 'week': ['change', 'keyup', 'paste'], // {change: 'change', premature: ['keyup', 'paste']}, // Obsolete 'datetime': ['change', 'keyup', 'paste'], // {change: 'change', premature: ['keyup', 'paste']}, // No handler for these 'button': [], // {change: '', premature: []}, 'reset': [], // {change: '', premature: []}, 'submit': [] // {change: '', premature: []} }; var getValidationEvents = inputType => { inputType = inputType.toLowerCase(); return EVENT_TYPES[inputType]; }; var getEventTarget = event => { if ((event === null || event === void 0 ? void 0 : event.target) == undefined) { return undefined; } if (event.target.tagName.toLowerCase() == 'option') { return event.target.closest('select'); } return event.target; }; var attachInputValidationListener = (node, validateInput) => { var validationHandler = event => { var theNode = getEventTarget(event); validateInput(theNode); }; var validationEvents = getValidationEvents(node.type) || []; validationEvents.map(eventType => { node.addEventListener(eventType, validationHandler); }); log_input(node, "attachInputValidationListener() attached: ".concat(validationEvents.join(', '))); // clean listeners function var removeAllChangeListeners = () => { if (node != undefined) { validationEvents.map(eventType => { node.removeEventListener(eventType, validationHandler); }); } }; // return clean function return removeAllChangeListeners; }; var useInput = props => { var [inputNode, setInputNode] = useState(undefined); var [validationMessage, setValidationMessage] = useState(''); var originalValue = useRef(); // // validate input callback // var validateInput = useCallback(function (node) { var markAdChanged = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; var nValidationMessage = checkValidity(node, props); setValidationMessage(nValidationMessage); setCustomValidationMessage(node, nValidationMessage, props.transformValue); if (markAdChanged) { node.setAttribute('data-formiga-changed', '1'); } return nValidationMessage; }, [props]); // // input Ref as callback // var inputRef = useCallback(node => { if (node != null) { node.setAttribute('data-formiga-input', '1'); validateInput(node, false); setInputNode(node); if (originalValue.current === undefined) { var defValue = (props === null || props === void 0 ? void 0 : props.originalValue) !== undefined ? props.originalValue : node.type == 'file' ? node.files.length > 0 ? node.files[0].name : undefined : getInputValue(node); originalValue.current = defValue; node.setAttribute('data-formiga-original-value', originalValue.current); } } }, [validateInput, props.originalValue]); // // attach listeners on node mount // useEffect(() => { if (inputNode != undefined) { var listeners = attachInputValidationListener(inputNode, validateInput); return listeners; } }, [inputNode, validateInput]); // // Specific effect to check props consistency. Just DEV time // useCheckProps(inputNode, props /*doRepeat, doNotRepeat, inputFilter*/); // // Attaches input filters when needed // useInputFilter(inputNode, props.inputFilter); // // Ensures checkboxes value // useCheckboxEnsure(inputNode); // // several callbacks to return // var validate = useCallback(() => { if (inputNode != undefined) { return validateInput(inputNode); } return undefined; }, [inputNode, validateInput]); var setValue = useCallback(v => { if (inputNode != undefined) { inputNode.value = v; } }, [inputNode]); var forceSetValidationMessage = useCallback(nValidationMessage => { if (inputNode != undefined) { setValidationMessage(nValidationMessage); setCustomValidationMessage(inputNode, nValidationMessage, props.transformValue); } }, [inputNode, props.transformValue]); var dispatchEvent = useCallback((type, ev_props) => { if (inputNode == undefined) { return; } var inputEvent = new Event(type, { bubbles: (ev_props === null || ev_props === void 0 ? void 0 : ev_props.bubbles) || true, cancelable: (ev_props === null || ev_props === void 0 ? void 0 : ev_props.cancelable) || true, view: (ev_props === null || ev_props === void 0 ? void 0 : ev_props.view) || window, detail: (ev_props === null || ev_props === void 0 ? void 0 : ev_props.detail) || (ev_props === null || ev_props === void 0 ? void 0 : ev_props.data) || {} }); inputNode.dispatchEvent(inputEvent); }, [inputNode]); var hasChangedOriginal = inputNode !== undefined && originalValue.current !== undefined ? inputNode.value !== originalValue.current : false; var hasChngedMark = inputNode !== undefined ? inputNode.getAttribute('data-formiga-changed') == '1' : false; return { ref: inputRef, node: inputNode, valid: validationMessage === '', validationMessage, validate, setValue, setValidationMessage: forceSetValidationMessage, dispatchEvent, originalValue: originalValue.current, hasChanged: hasChangedOriginal || hasChngedMark }; }; export { useForm, useInput, useInputFilter }; //# sourceMappingURL=formiga.mjs.map