UNPKG

pocket-hook-form

Version:

pocket-store base hook form

130 lines (116 loc) 3.83 kB
// src/utils/validate.ts import type {FieldRules, FieldError, FormValue} from '../models/type'; /** Fast emptiness check for common field types. */ function isEmptyValue(v: unknown): boolean { if (v == null) return true; // null | undefined if (typeof v === 'string') { // treat pure whitespace as empty // micro-opt: manual trim-like scan to avoid allocation for (let i = 0; i < v.length; i++) { const c = v.charCodeAt(i); if (c !== 32 && c !== 9 && c !== 10 && c !== 13) return false; // ' ', \t, \n, \r } return true; } if (Array.isArray(v)) return v.length === 0; return false; } /** * Run validators in order with minimal allocations.. * - Avoids creating an array unless needed. */ export function runValidators<V, T extends FormValue>( value: V, all: any, rules?: FieldRules<V, T>, criteria: 'firstError' | 'all' = 'firstError', ): undefined | FieldError | FieldError[] { if (!rules) return undefined; const wantAll = criteria === 'all'; const isStr = typeof value === 'string'; const empty = isEmptyValue(value); const hasRequired = !!rules.required; // We'll hold the first error separately to avoid array allocs on firstError path. let firstErr: FieldError | undefined; let allErrs: FieldError[] | undefined; const emit = (e: FieldError) => { if (wantAll) { if (!allErrs) allErrs = [e]; else allErrs.push(e); return false; // continue collecting } if (!firstErr) firstErr = e; return true; // stop (firstError) }; // 1) required if (hasRequired && empty) { const msg = typeof rules.required === 'string' ? rules.required : 'Required'; if (emit({type: 'required', message: msg})) return firstErr!; } // 2) If empty & NOT required → skip the rest if (empty && !hasRequired) { // nothing else to validate; return whatever we have (likely undefined) return wantAll ? allErrs : firstErr; } // 3) minLength/maxLength (strings only) if (isStr) { const len = (value as unknown as string).length; if (rules.minLength != null) { const min = typeof rules.minLength === 'number' ? rules.minLength : rules.minLength.value; if (len < min) { const msg = typeof rules.minLength === 'number' ? `Min length ${min}` : rules.minLength.message ?? `Min length ${min}`; if (emit({type: 'minLength', message: msg}) && !wantAll) { return firstErr!; } } } if (rules.maxLength != null) { const max = typeof rules.maxLength === 'number' ? rules.maxLength : rules.maxLength.value; if (len > max) { const msg = typeof rules.maxLength === 'number' ? `Max length ${max}` : rules.maxLength.message ?? `Max length ${max}`; if (emit({type: 'maxLength', message: msg}) && !wantAll) { return firstErr!; } } } if (rules.pattern) { const re = rules.pattern instanceof RegExp ? rules.pattern : rules.pattern.value; if (!re.test(value as unknown as string)) { const msg = rules.pattern instanceof RegExp ? 'Invalid format' : rules.pattern.message ?? 'Invalid format'; if (emit({type: 'pattern', message: msg}) && !wantAll) { return firstErr!; } } } } // 5) custom validate if (rules.validate) { const res = rules.validate(value as V, all); if (res !== true && res !== undefined) { const msg = typeof res === 'string' ? res : 'Invalid'; if (emit({type: 'validate', message: msg}) && !wantAll) { return firstErr!; } } } // Return according to criteria mode if (wantAll) return allErrs; return firstErr; }