UNPKG

validierung

Version:

Vue composition function for form validation

668 lines (653 loc) 21.7 kB
'use strict'; var internal_shared = require('./internal/shared.cjs'); var vueDemi = require('vue-demi'); const isField = (value) => internal_shared.isRecord(value) ? "$value" in value : false; const isTransformedField = (value) => internal_shared.isRecord(value) ? "$uid" in value && "$value" in value : false; function disposeForm(form, deletedFormData) { for (const value of Object.values(deletedFormData)) { if (isTransformedField(value)) { form.dispose(value.$uid); continue; } if (internal_shared.isObject(value)) { disposeForm(form, value); } } } function getResultFormDataImpl(formData, resultFormData, path, predicate) { let arrayIndexOffset = 0; for (const [key, value] of Object.entries(formData)) { const nextKey = internal_shared.isArray(formData) ? +key - arrayIndexOffset : key; const isTransformedFieldResult = isTransformedField(value); const unpackedValue = isTransformedFieldResult ? internal_shared.deepCopy(value.$value) : vueDemi.unref(value); if (predicate({ key, value: unpackedValue, path })) { resultFormData[nextKey] = unpackedValue; } else { ++arrayIndexOffset; continue; } if (isTransformedFieldResult) { continue; } if (internal_shared.isObject(value)) { resultFormData[nextKey] = internal_shared.isArray(value) ? [] : {}; getResultFormDataImpl( value, resultFormData[nextKey], [...path, key], predicate ); } } } function getResultFormData(transformedFormData, predicate = () => true) { const resultFormData = internal_shared.isArray(transformedFormData) ? [] : {}; getResultFormDataImpl(transformedFormData, resultFormData, [], predicate); return resultFormData; } function resetFields(form, data, transformedFormData) { Object.entries(data).forEach(([key, value]) => { const transformedValue = transformedFormData[key]; if (isTransformedField(transformedValue)) { const field = form.getField(transformedValue.$uid); field.reset(value); return; } if (internal_shared.isObject(value)) { resetFields(form, value, transformedFormData[key]); } }); } class ValidationConfig { constructor() { this.defaultValidationBehavior = null; this.vbfMap = /* @__PURE__ */ new Map(); } getDefaultVbf() { if (this.defaultValidationBehavior === null) { return () => true; } return this.vbfMap.get(this.defaultValidationBehavior); } } const validationConfig = new ValidationConfig(); function mapFieldRules(fieldRules) { const defaultVbf = validationConfig.getDefaultVbf(); return fieldRules.map((fieldRule) => { if (typeof fieldRule === "function") { return { vbf: defaultVbf, rule: fieldRule }; } if (!internal_shared.isArray(fieldRule)) { return { vbf: defaultVbf, rule: fieldRule }; } const [first, second, third] = fieldRule; if (typeof second === "number") { return { vbf: defaultVbf, rule: first, debounce: second }; } if (typeof first === "function") { return { vbf: first, rule: second, debounce: third }; } const vbf = validationConfig.vbfMap.get(first); if (vbf !== void 0) { return { vbf, rule: second, debounce: third }; } { throw new Error( `[validierung] Validation behavior with name '${first}' does not exist. Valid values are: "${[ ...validationConfig.vbfMap.keys() ].join(", ")}"` ); } }); } function registerField(form, name, field) { const { $value, $rules, ...fieldExtraProperties } = field; const rules = $rules ? mapFieldRules($rules) : []; const uid = internal_shared.uid(); const formField = form.registerField(uid, name, $value, rules); return { ...fieldExtraProperties, $uid: uid, $value: formField.modelValue, $errors: formField.errors, $hasError: formField.hasError, $validating: formField.validating, $dirty: formField.dirty, $touched: formField.touched, async $validate({ setTouched, force } = {}) { setTouched != null ? setTouched : setTouched = true; force != null ? force : force = true; if (setTouched) { formField.touched.value = true; } await form.validate(uid, force); } }; } function transformFormData(form, formData) { for (const [key, value] of Object.entries(formData)) { if (isField(value)) { const transformedField = registerField(form, key, value); formData[key] = transformedField; continue; } if (internal_shared.isObject(value)) { transformFormData(form, value); } } } const isSimpleRule = (rule) => typeof rule === "function"; const unpackRule = (rule) => isSimpleRule(rule) ? [rule, void 0] : [rule.rule, rule.key]; class FormField { constructor(form, uid, name, modelValue, ruleInfos) { this.touched = vueDemi.ref(false); this.dirty = vueDemi.ref(false); this.rulesValidating = vueDemi.ref(0); this.validating = vueDemi.computed( () => this.rulesValidating.value > 0 ); this.errors = vueDemi.computed( () => this.rawErrors.value.filter(internal_shared.isDefined) ); this.hasError = vueDemi.computed( () => this.hasErrors.value.some((e) => e) ); this.simpleValidators = []; this.keyedValidators = /* @__PURE__ */ new Map(); this.form = form; this.uid = uid; this.name = name; this.modelValue = vueDemi.ref(modelValue); this.rawErrors = vueDemi.ref(ruleInfos.map(() => null)); this.hasErrors = vueDemi.ref(ruleInfos.map(() => false)); this.initialModelValue = internal_shared.deepCopy(this.modelValue.value); this.ruleInfos = ruleInfos.map((info, ruleNumber) => { const [rule, key] = unpackRule(info.rule); if (!rule) { if (key && !this.keyedValidators.has(key)) { this.keyedValidators.set(key, []); } return; } let validator; let validatorNotDebounced; validator = validatorNotDebounced = (force, submit, modelValues = this.modelValue, skipShouldValidate = false) => { if (skipShouldValidate || this.shouldValidate(ruleNumber, force, submit)) { return this.validate(ruleNumber, modelValues); } }; let debouncedValidator = void 0; let debounceInvokedTimes = 0; let debounceResolve; if (info.debounce) { debouncedValidator = internal_shared.debounce( (modelValues) => { debounceResolve(this.validate(ruleNumber, modelValues)); this.rulesValidating.value -= debounceInvokedTimes; this.form.rulesValidating.value -= debounceInvokedTimes; debounceInvokedTimes = 0; }, { wait: info.debounce } ); validator = (force, submit, modelValues = this.modelValue, skipShouldValidate = false) => { if (skipShouldValidate || this.shouldValidate(ruleNumber, force, submit)) { ++debounceInvokedTimes; ++this.rulesValidating.value; ++this.form.rulesValidating.value; return new Promise((resolve) => { debounceResolve == null ? void 0 : debounceResolve(); debounceResolve = resolve; debouncedValidator(modelValues); }); } }; } const validatorTuple = { ruleNumber, validator, validatorNotDebounced }; if (key) { const keyedValidators = this.keyedValidators.get(key); if (keyedValidators === void 0) { this.keyedValidators.set(key, [validatorTuple]); } else { keyedValidators.push(validatorTuple); } } else { this.simpleValidators.push(validatorTuple); } return { buffer: new internal_shared.LinkedList(), rule, validator, validatorNotDebounced, vbf: info.vbf, cancelDebounce: !debouncedValidator ? () => { } : () => { debounceInvokedTimes = 0; debouncedValidator.cancel(); debounceResolve == null ? void 0 : debounceResolve(); } }; }); this.watchStopHandle = this.setupWatcher(); } async validate(ruleNumber, modelValues) { var _a; const { rule, buffer } = this.ruleInfos[ruleNumber]; let error; const ruleResult = vueDemi.isRef(modelValues) ? rule(modelValues.value) : rule(...modelValues.map((r) => r.value)); if ((_a = buffer.last) == null ? void 0 : _a.value) { buffer.last.value = false; this.rulesValidating.value--; this.form.rulesValidating.value--; } if (internal_shared.isPromise(ruleResult)) { const shouldSetError = buffer.addLast(true); this.rulesValidating.value++; this.form.rulesValidating.value++; try { error = await ruleResult; } catch (err) { error = err; } buffer.remove(shouldSetError); if (shouldSetError.value) { this.rulesValidating.value--; this.form.rulesValidating.value--; this.setError(ruleNumber, error); } else { if (typeof error === "string" || typeof error === "symbol") { throw error; } } } else { error = ruleResult; this.setError(ruleNumber, error); } } reset(resetValue = this.initialModelValue) { this.watchStopHandle(); this.touched.value = false; this.dirty.value = false; this.modelValue.value = internal_shared.deepCopy(resetValue); this.rulesValidating.value = 0; this.form.rulesValidating.value = 0; for (let i = 0; i < this.ruleInfos.length; ++i) { if (vueDemi.isVue3) { this.rawErrors.value[i] = null; this.hasErrors.value[i] = false; } else { vueDemi.set(this.rawErrors.value, i, null); vueDemi.set(this.hasErrors.value, i, false); } const ruleInfos = this.ruleInfos[i]; if (ruleInfos) { ruleInfos.cancelDebounce(); for (const shouldSetError of ruleInfos.buffer) { shouldSetError.value = false; } } } this.watchStopHandle = this.setupWatcher(); } dispose() { if (vueDemi.isVue3) { this.errors.effect.stop(); this.validating.effect.stop(); this.hasError.effect.stop(); } this.watchStopHandle(); } shouldValidateForKey(key, force, submit) { const keyedValidators = this.keyedValidators.get(key); for (let i = 0; i < keyedValidators.length; ++i) { const shouldValidateResult = this.shouldValidate( keyedValidators[i].ruleNumber, force, submit ); if (!shouldValidateResult) { return false; } } return true; } shouldValidate(ruleNumber, force, submit) { return this.ruleInfos[ruleNumber].vbf({ hasError: this.rawErrors.value[ruleNumber] !== null, touched: this.touched.value, dirty: this.dirty.value, force, submit, value: this.modelValue.value }); } setError(ruleNumber, error) { const isString = typeof error === "string"; const isSymbol = typeof error === "symbol"; if (vueDemi.isVue3) { if (isString) { this.rawErrors.value[ruleNumber] = error; this.hasErrors.value[ruleNumber] = true; throw error; } if (isSymbol) { this.hasErrors.value[ruleNumber] = true; throw error; } this.rawErrors.value[ruleNumber] = null; this.hasErrors.value[ruleNumber] = false; } else { if (isString) { vueDemi.set(this.rawErrors.value, ruleNumber, error); vueDemi.set(this.hasErrors.value, ruleNumber, true); throw error; } if (isSymbol) { vueDemi.set(this.hasErrors.value, ruleNumber, true); throw error; } vueDemi.set(this.rawErrors.value, ruleNumber, null); vueDemi.set(this.hasErrors.value, ruleNumber, false); } } setupWatcher() { return vueDemi.watch( this.modelValue, () => { this.dirty.value = true; this.form.validate(this.uid); }, { deep: true } ); } } class ValidationError extends Error { constructor() { super("One or more validation errors occurred"); } } class Form { constructor() { this.simpleMap = /* @__PURE__ */ new Map(); this.keyedMap = /* @__PURE__ */ new Map(); this.reactiveErrors = vueDemi.shallowReactive({}); this.rulesValidating = vueDemi.ref(0); this.submitting = vueDemi.ref(false); this.validating = vueDemi.computed(() => this.rulesValidating.value > 0); this.hasError = vueDemi.computed(() => this.errors.value.length > 0); this.errors = vueDemi.computed( () => Object.values(this.reactiveErrors).flatMap((e) => e.value) ); } registerField(uid, name, modelValue, ruleInfos) { const field = new FormField(this, uid, name, modelValue, ruleInfos); const simpleEntry = { rollbacks: [], field }; const keysSeen = /* @__PURE__ */ new Set(); for (let i = 0; i < ruleInfos.length; ++i) { const key = unpackRule(ruleInfos[i].rule)[1]; if (key === void 0) { continue; } let keyedEntry = this.keyedMap.get(key); if (keyedEntry === void 0) { this.keyedMap.set(key, { fields: [field], modelValues: [field.modelValue] }); } else if (!keysSeen.has(key)) { keyedEntry.fields.push(field); keyedEntry.modelValues.push(field.modelValue); } keyedEntry = this.keyedMap.get(key); keysSeen.add(key); const rollback = () => { for (let i2 = 0; i2 < keyedEntry.fields.length; ++i2) { if (keyedEntry.fields[i2] === field) { keyedEntry.fields.splice(i2, 1); keyedEntry.modelValues.splice(i2, 1); break; } } if (keyedEntry.fields.length === 0) { this.keyedMap.delete(key); } }; simpleEntry.rollbacks.push(rollback); } this.simpleMap.set(uid, simpleEntry); vueDemi.isVue3 ? this.reactiveErrors[uid] = field.errors : vueDemi.set(this.reactiveErrors, uid, field.errors); return field; } validate(uid, force = false) { const { field } = this.simpleMap.get(uid); return Promise.allSettled([ ...field.simpleValidators.map(({ validator }) => validator(force, false)), ...this.collectValidatorResultsForKeys(field, force, false) ]); } async validateAll(names) { const settledResults = await Promise.allSettled( this.collectValidatorResultsForNames(names) ); for (let i = 0; i < settledResults.length; ++i) { if (settledResults[i].status === "rejected") { throw new ValidationError(); } } } dispose(uid) { const simpleEntry = this.simpleMap.get(uid); if (simpleEntry) { simpleEntry.field.dispose(); simpleEntry.rollbacks.forEach((r) => r()); } this.simpleMap.delete(uid); vueDemi.isVue3 ? delete this.reactiveErrors[uid] : vueDemi.del(this.reactiveErrors, uid); } resetFields() { for (const { field } of this.simpleMap.values()) { field.reset(); } } getField(uid) { const simpleEntry = this.simpleMap.get(uid); return simpleEntry == null ? void 0 : simpleEntry.field; } *collectValidatorResultsForKeys(field, force, submit, keys = field.keyedValidators.keys()) { for (const key of keys) { const { fields, modelValues } = this.keyedMap.get(key); const isEveryOtherFieldTouchedAndShouldValidate = this.isEveryOtherFieldTouched(field, fields) && field.shouldValidateForKey(key, force, submit); if (!isEveryOtherFieldTouchedAndShouldValidate) { return; } for (let fieldIdx = 0; fieldIdx < fields.length; ++fieldIdx) { const keyedValidators = fields[fieldIdx].keyedValidators.get(key); for (let keyedValidatorIdx = 0; keyedValidatorIdx < keyedValidators.length; ++keyedValidatorIdx) { yield submit ? keyedValidators[keyedValidatorIdx].validatorNotDebounced( force, submit, modelValues, fields[keyedValidatorIdx] === field ) : keyedValidators[keyedValidatorIdx].validator( force, submit, modelValues, fields[keyedValidatorIdx] === field ); } } } } *collectValidatorResultsForNames(names) { if (names === void 0) { for (const { field } of this.simpleMap.values()) { field.touched.value = true; for (let i = 0; i < field.simpleValidators.length; ++i) { yield field.simpleValidators[i].validatorNotDebounced(false, true); } } for (const [key, { fields, modelValues }] of this.keyedMap.entries()) { for (let fieldIdx = 0; fieldIdx < fields.length; ++fieldIdx) { const keyedValidators = fields[fieldIdx].keyedValidators.get(key); for (let keyedValidatorIdx = 0; keyedValidatorIdx < keyedValidators.length; ++keyedValidatorIdx) { yield keyedValidators[keyedValidatorIdx].validatorNotDebounced( false, true, modelValues ); } } } return; } if (names.length === 0) { return; } const uniqueNames = new Set(names); const validatedKeys = /* @__PURE__ */ new Set(); for (const { field } of this.simpleMap.values()) { if (uniqueNames.has(field.name)) { field.touched.value = true; } } for (const { field } of this.simpleMap.values()) { if (!uniqueNames.has(field.name)) { continue; } for (let i = 0; i < field.simpleValidators.length; ++i) { yield field.simpleValidators[i].validatorNotDebounced(false, true); } for (const key of field.keyedValidators.keys()) { if (validatedKeys.has(key)) { continue; } validatedKeys.add(key); yield* this.collectValidatorResultsForKeys(field, false, true, [key]); } } } isEveryOtherFieldTouched(field, fields) { for (let i = 0; i < fields.length; ++i) { if (field !== fields[i] && !fields[i].touched.value) { return false; } } return true; } } function useValidation(formData) { const form = new Form(); const promiseCancel = new internal_shared.PromiseCancel(); transformFormData(form, formData); const transformedFormData = vueDemi.reactive(formData); return { form: transformedFormData, submitting: form.submitting, validating: form.validating, hasError: form.hasError, errors: form.errors, async validateFields({ names, predicate } = {}) { form.submitting.value = true; const resultFormData = getResultFormData( transformedFormData, predicate ); try { await promiseCancel.race(form.validateAll(names)); } finally { form.submitting.value = false; } return resultFormData; }, resetFields(formData2) { promiseCancel.cancelReject(new ValidationError()); if (formData2 === void 0) { form.resetFields(); } else { resetFields(form, formData2, transformedFormData); } }, add(path, value) { const lastKey = path[path.length - 1]; if (lastKey !== void 0) { const box = { [lastKey]: value }; transformFormData(form, box); let transformedValue = box[lastKey]; const valueAtPath = internal_shared.path(path, transformedFormData); if (vueDemi.isVue2) { transformedValue = vueDemi.reactive(transformedValue); } if (Array.isArray(valueAtPath)) { valueAtPath.push(transformedValue); } else { internal_shared.set(transformedFormData, path, transformedValue); } } }, remove(path) { const lastKey = path.pop(); if (lastKey === void 0) { return; } if (path.length === 0) { disposeForm(form, transformedFormData[lastKey]); vueDemi.isVue3 ? delete transformedFormData[lastKey] : vueDemi.del(transformedFormData, lastKey); } else { const valueAtPath = internal_shared.path(path, transformedFormData); if (Array.isArray(valueAtPath)) { const deletedFormData = valueAtPath.splice(+lastKey, 1); disposeForm(form, deletedFormData); } else { disposeForm(form, valueAtPath[lastKey]); vueDemi.isVue3 ? delete valueAtPath[lastKey] : vueDemi.del(valueAtPath, lastKey); } } } }; } function createValidation(configuration) { return { install() { var _a; for (const [key, validationBehavior] of Object.entries( (_a = configuration.validationBehavior) != null ? _a : {} )) { validationConfig.vbfMap.set(key, validationBehavior); } if (validationConfig.vbfMap.has(configuration.defaultValidationBehavior)) { validationConfig.defaultValidationBehavior = configuration.defaultValidationBehavior; } else { console.warn( `[validierung] Default validation behavior '${configuration.defaultValidationBehavior}' is not valid. Valid values are: "${[ ...validationConfig.vbfMap.keys() ].join(", ")}"` ); } } }; } exports.ValidationError = ValidationError; exports.createValidation = createValidation; exports.useValidation = useValidation;