validierung
Version:
Vue composition function for form validation
668 lines (653 loc) • 21.7 kB
JavaScript
'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;