formiga
Version:
The simplest -yet effective- form validator for React
850 lines (787 loc) • 26.2 kB
JavaScript
/**
* 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