vue3-form-validation
Advanced tools
Comparing version 4.1.1 to 5.0.0-beta.1
@@ -9,4 +9,20 @@ 'use strict'; | ||
constructor(value) { | ||
this.next = null; | ||
this.prev = null; | ||
Object.defineProperty(this, "value", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: void 0 | ||
}); | ||
Object.defineProperty(this, "next", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: null | ||
}); | ||
Object.defineProperty(this, "prev", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: null | ||
}); | ||
this.value = value; | ||
@@ -17,5 +33,20 @@ } | ||
constructor() { | ||
this.head = null; | ||
this.tail = null; | ||
this.count = 0; | ||
Object.defineProperty(this, "head", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: null | ||
}); | ||
Object.defineProperty(this, "tail", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: null | ||
}); | ||
Object.defineProperty(this, "count", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: 0 | ||
}); | ||
} | ||
@@ -106,3 +137,3 @@ get first() { | ||
} | ||
valuesForwards() { | ||
nodesForwards() { | ||
let node = this.head; | ||
@@ -112,3 +143,3 @@ return { | ||
for (; node !== null; node = node.next) { | ||
yield node.value; | ||
yield node; | ||
} | ||
@@ -118,3 +149,3 @@ } | ||
} | ||
valuesBackwards() { | ||
nodesBackwards() { | ||
let node = this.tail; | ||
@@ -124,3 +155,3 @@ return { | ||
for (; node !== null; node = node.prev) { | ||
yield node.value; | ||
yield node; | ||
} | ||
@@ -132,13 +163,20 @@ } | ||
const isDefined = (x) => x !== null && typeof x !== 'undefined'; | ||
const isRecord = (x) => typeof x === 'object' && x !== null && !Array.isArray(x); | ||
const isArray = (x) => Array.isArray(x); | ||
const isObject = (x) => typeof x === 'object' && x !== null; | ||
const isField = (x) => isRecord(x) ? '$value' in x : false; | ||
const isTransformedField = (x) => isRecord(x) | ||
? '$uid' in x && '$value' in x && '$errors' in x && '$validating' in x | ||
: false; | ||
const isSimpleRule = (rule) => typeof rule === 'function'; | ||
function* deepIterator(obj, predicate = () => false) { | ||
const stack = new LinkedList(); | ||
stack.addLast([obj, null, '', []]); | ||
stack.addLast({ current: obj, parent: null, parentKey: '', path: [] }); | ||
while (stack.count > 0) { | ||
const [current, parent, parentKey, path] = stack.last.value; | ||
const { current, parent, parentKey, path } = stack.last.value; | ||
stack.removeLast(); | ||
let pushedItemsOnStack = false; | ||
if (typeof current === 'object' && | ||
current !== null && | ||
!vue.isRef(current) && | ||
!predicate(current)) { | ||
if (isObject(current) && !vue.isRef(current) && !predicate(current)) { | ||
const entries = Object.entries(current); | ||
@@ -148,7 +186,18 @@ pushedItemsOnStack = entries.length > 0; | ||
const [key, value] = entries[i]; | ||
stack.addLast([value, current, key, [...path, key]]); | ||
stack.addLast({ | ||
current: value, | ||
parent: current, | ||
parentKey: key, | ||
path: [...path, key] | ||
}); | ||
} | ||
} | ||
if (typeof parent === 'object' && parent !== null) { | ||
yield [parentKey, parent[parentKey], parent, path, !pushedItemsOnStack]; | ||
if (isObject(parent)) { | ||
yield { | ||
key: parentKey, | ||
value: parent[parentKey], | ||
parent, | ||
path, | ||
isLeaf: !pushedItemsOnStack | ||
}; | ||
} | ||
@@ -158,22 +207,2 @@ } | ||
const isDefined = (x) => x !== null && typeof x !== 'undefined'; | ||
const isObject = (x) => typeof x === 'object' && x !== null && !Array.isArray(x); | ||
const isArray = (x) => Array.isArray(x); | ||
const isField = (x) => isObject(x) ? '$value' in x : false; | ||
const isTransformedField = (x) => isObject(x) | ||
? '$uid' in x && '$value' in x && '$errors' in x && '$validating' in x | ||
: false; | ||
function cleanupForm(form, deletedData) { | ||
if (isTransformedField(deletedData)) { | ||
form.onDelete(deletedData.$uid); | ||
return; | ||
} | ||
for (const [, value] of deepIterator(deletedData, isTransformedField)) { | ||
if (isTransformedField(value)) { | ||
form.onDelete(value.$uid); | ||
} | ||
} | ||
} | ||
function set(obj, keys, value) { | ||
@@ -202,19 +231,113 @@ if (keys.length === 0) { | ||
function deepCopy(toClone) { | ||
if (typeof toClone !== 'object') { | ||
return toClone; | ||
if (isObject(toClone)) { | ||
const copy = isArray(toClone) ? [] : {}; | ||
for (const { value, path, isLeaf } of deepIterator(toClone)) { | ||
if (isLeaf) { | ||
set(copy, path, value); | ||
} | ||
} | ||
return copy; | ||
} | ||
const copy = isArray(toClone) ? [] : {}; | ||
for (const [, value, , path, isLeaf] of deepIterator(toClone)) { | ||
if (isLeaf) { | ||
set(copy, path, value); | ||
return toClone; | ||
} | ||
const trySet = (map) => ({ success, failure }) => (key, value) => { | ||
const _value = map.get(key); | ||
if (_value) { | ||
failure === null || failure === void 0 ? void 0 : failure(_value); | ||
} | ||
else { | ||
map.set(key, value); | ||
success === null || success === void 0 ? void 0 : success(value); | ||
} | ||
}; | ||
const tryGet = (map) => ({ success, failure }) => (key) => { | ||
const value = map.get(key); | ||
if (value) { | ||
success(value); | ||
} | ||
else { | ||
failure === null || failure === void 0 ? void 0 : failure(); | ||
} | ||
}; | ||
function path(path, obj) { | ||
let value = obj[path[0]]; | ||
for (let i = 0; i < path.length; i++) { | ||
const key = path[i]; | ||
if (value === null || value === undefined) { | ||
return undefined; | ||
} | ||
if (i > 0) { | ||
value = value[key]; | ||
} | ||
} | ||
return copy; | ||
return value; | ||
} | ||
let id = 1; | ||
function uid() { | ||
return id++; | ||
} | ||
class PromiseCancel { | ||
constructor() { | ||
Object.defineProperty(this, "promise", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: void 0 | ||
}); | ||
Object.defineProperty(this, "resolve", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: void 0 | ||
}); | ||
Object.defineProperty(this, "reject", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: void 0 | ||
}); | ||
this.assign(); | ||
} | ||
cancelResolve(value) { | ||
this.resolve(value); | ||
this.assign(); | ||
} | ||
cancelReject(reason) { | ||
this.reject(reason); | ||
this.assign(); | ||
} | ||
race(...promises) { | ||
return Promise.race([this.promise, ...promises]); | ||
} | ||
assign() { | ||
this.promise = new Promise((resolve, reject) => { | ||
this.resolve = resolve; | ||
this.reject = reject; | ||
}); | ||
} | ||
} | ||
function cleanupForm(form, deletedFormData) { | ||
if (isTransformedField(deletedFormData)) { | ||
form.onDelete(deletedFormData.$uid); | ||
return; | ||
} | ||
for (const { value } of deepIterator(deletedFormData, isTransformedField)) { | ||
if (isTransformedField(value)) { | ||
form.onDelete(value.$uid); | ||
} | ||
} | ||
} | ||
function getResultFormData(transformedFormData) { | ||
const result = {}; | ||
for (const [, value, , path, isLeaf] of deepIterator(transformedFormData, isTransformedField)) { | ||
for (const { value, path, isLeaf } of deepIterator(transformedFormData, isTransformedField)) { | ||
if (isTransformedField(value)) { | ||
if (vue.isReactive(value.$value)) { | ||
// Value is reactive -> value is an object or array | ||
// Make sure to do a deep clone to loose the reactive reference | ||
set(result, path, deepCopy(value.$value)); | ||
@@ -228,2 +351,3 @@ } | ||
if (vue.isReactive(value)) { | ||
// Same as above | ||
set(result, path, deepCopy(value)); | ||
@@ -246,3 +370,3 @@ } | ||
} | ||
else if (isObject(transformedValue.$value)) { | ||
else if (isRecord(transformedValue.$value)) { | ||
const copy = deepCopy(value); | ||
@@ -256,3 +380,3 @@ Object.assign(transformedValue.$value, copy); | ||
} | ||
if (typeof value === 'object') { | ||
if (isObject(value)) { | ||
resetFields(value, transformedFormData[key]); | ||
@@ -263,18 +387,11 @@ } | ||
let uid = 1; | ||
function useUid() { | ||
return uid++; | ||
} | ||
function registerField(form, name, field) { | ||
var _a; | ||
const uid = useUid(); | ||
const formField = form.registerField(uid, name, field.$value, (_a = field.$rules) !== null && _a !== void 0 ? _a : []); | ||
const uid$1 = uid(); | ||
const formField = form.registerField(uid$1, name, field.$value, field.$validationBehavior || 'lazier', (_a = field.$rules) !== null && _a !== void 0 ? _a : []); | ||
vue.watch(formField.modelValue, () => { | ||
if (formField.touched) { | ||
form.validate(uid); | ||
} | ||
form.validate(uid$1); | ||
}, { deep: true }); | ||
return { | ||
$uid: uid, | ||
$uid: uid$1, | ||
$value: formField.modelValue, | ||
@@ -284,12 +401,10 @@ $errors: formField.errors, | ||
$validating: formField.validating, | ||
async $onBlur() { | ||
if (!formField.touched) { | ||
formField.touched = true; | ||
await form.validate(uid); | ||
} | ||
async $setTouched() { | ||
formField.touched = true; | ||
await form.validate(uid$1, true); | ||
} | ||
}; | ||
} | ||
function transformFormData(form, data) { | ||
for (const [key, value, parent] of deepIterator(data)) { | ||
function transformFormData(form, formData) { | ||
for (const { key, value, parent } of deepIterator(formData)) { | ||
if (isField(value)) { | ||
@@ -302,85 +417,169 @@ const transformedField = registerField(form, key, value); | ||
const tryGet = (map) => ({ success, failure }) => (key) => { | ||
const value = map.get(key); | ||
if (value) { | ||
success(value); | ||
} | ||
else { | ||
failure === null || failure === void 0 ? void 0 : failure(); | ||
} | ||
const VALIDATION_BEHAVIOR_RESTRICTIVENESS = { | ||
aggresive: 0, | ||
lazy: 1, | ||
lazier: 2 | ||
}; | ||
const trySet = (map) => ({ success, failure }) => (key, value) => { | ||
const _value = map.get(key); | ||
if (_value) { | ||
failure === null || failure === void 0 ? void 0 : failure(_value); | ||
const compareValidationBehavior = (a, b) => VALIDATION_BEHAVIOR_RESTRICTIVENESS[a] - | ||
VALIDATION_BEHAVIOR_RESTRICTIVENESS[b]; | ||
const getMostRestrictiveValidationBehavior = (form, keyedValidators) => { | ||
let mostRestrictiveValidationBehavior = 'aggresive'; | ||
for (const { meta } of keyedValidators.values()) { | ||
const validationBehavior = meta.formField.getValidationBehavior(form); | ||
if (validationBehavior && | ||
compareValidationBehavior(validationBehavior, mostRestrictiveValidationBehavior) > 0) { | ||
mostRestrictiveValidationBehavior = validationBehavior; | ||
} | ||
} | ||
else { | ||
map.set(key, value); | ||
success === null || success === void 0 ? void 0 : success(value); | ||
} | ||
return mostRestrictiveValidationBehavior; | ||
}; | ||
function path(path, o) { | ||
let value = o[path[0]]; | ||
for (let i = 0; i < path.length; i++) { | ||
const key = path[i]; | ||
if (value === null || value === undefined) { | ||
return undefined; | ||
} | ||
if (i > 0) { | ||
value = value[key]; | ||
} | ||
} | ||
return value; | ||
} | ||
class PromiseCancel { | ||
constructor() { | ||
this.assign(); | ||
} | ||
cancelResolve(value) { | ||
this.resolve(value); | ||
this.assign(); | ||
} | ||
cancelReject(reason) { | ||
this.reject(reason); | ||
this.assign(); | ||
} | ||
race(...promises) { | ||
return Promise.race([this.promise, ...promises]); | ||
} | ||
assign() { | ||
this.promise = new Promise((resolve, reject) => { | ||
this.resolve = resolve; | ||
this.reject = reject; | ||
class FormField { | ||
constructor(name, modelValue, validationBehavior, rules) { | ||
Object.defineProperty(this, "_rules", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: void 0 | ||
}); | ||
} | ||
} | ||
class FormField { | ||
constructor(name, modelValue, rules) { | ||
this.rulesValidating = vue.ref(0); | ||
this.touched = false; | ||
this.errors = vue.computed(() => this._errors.filter(isDefined)); | ||
this.validating = vue.computed(() => this.rulesValidating.value > 0); | ||
this.hasError = vue.computed(() => this.errors.value.length > 0); | ||
Object.defineProperty(this, "_buffers", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: void 0 | ||
}); | ||
Object.defineProperty(this, "_errors", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: void 0 | ||
}); | ||
Object.defineProperty(this, "_validationBehavior", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: void 0 | ||
}); | ||
Object.defineProperty(this, "_rulesValidating", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: vue.ref(0) | ||
}); | ||
Object.defineProperty(this, "initialModelValue", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: void 0 | ||
}); | ||
Object.defineProperty(this, "name", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: void 0 | ||
}); | ||
Object.defineProperty(this, "touched", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: false | ||
}); | ||
Object.defineProperty(this, "modelValue", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: void 0 | ||
}); | ||
Object.defineProperty(this, "errors", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: vue.computed(() => this._errors.filter(isDefined)) | ||
}); | ||
Object.defineProperty(this, "validating", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: vue.computed(() => this._rulesValidating.value > 0) | ||
}); | ||
Object.defineProperty(this, "hasError", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: vue.computed(() => this.errors.value.length > 0) | ||
}); | ||
this._rules = rules.map(rule => isSimpleRule(rule) ? rule : rule.rule); | ||
this._buffers = rules.map(() => new LinkedList()); | ||
this._errors = vue.reactive(rules.map(() => null)); | ||
this._validationBehavior = validationBehavior; | ||
this.name = name; | ||
this._errors = vue.reactive(rules.map(() => null)); | ||
if (vue.isRef(modelValue) || vue.isReactive(modelValue)) { | ||
this.modelValue = modelValue; | ||
this._initialModelValue = deepCopy(vue.unref(modelValue)); | ||
this.initialModelValue = deepCopy(vue.unref(modelValue)); | ||
} | ||
else if (isObject(modelValue)) { | ||
else if (isRecord(modelValue)) { | ||
this.modelValue = vue.reactive(modelValue); | ||
this._initialModelValue = deepCopy(this.modelValue); | ||
this.initialModelValue = deepCopy(this.modelValue); | ||
} | ||
else { | ||
this.modelValue = vue.ref(modelValue); | ||
this._initialModelValue = deepCopy(vue.unref(modelValue)); | ||
this.initialModelValue = deepCopy(vue.unref(modelValue)); | ||
} | ||
} | ||
setError(ruleNumber, error) { | ||
this._errors[ruleNumber] = error; | ||
async validate(ruleNumber, modelValues) { | ||
const rule = this._rules[ruleNumber]; | ||
const buffer = this._buffers[ruleNumber]; | ||
let error; | ||
if (!rule) { | ||
return; | ||
} | ||
const ruleResult = rule(...modelValues.map(vue.unref)); | ||
if (typeof (ruleResult === null || ruleResult === void 0 ? void 0 : ruleResult.then) === 'function') { | ||
this._rulesValidating.value++; | ||
const shouldSetError = buffer.addLast(true); | ||
if (shouldSetError.prev) { | ||
shouldSetError.prev.value = false; | ||
} | ||
try { | ||
error = await ruleResult; | ||
} | ||
catch (err) { | ||
error = err; | ||
} | ||
buffer.remove(shouldSetError); | ||
this._rulesValidating.value--; | ||
if (shouldSetError.value) { | ||
this._setError(ruleNumber, error); | ||
} | ||
} | ||
else { | ||
error = ruleResult; | ||
this._setError(ruleNumber, error); | ||
} | ||
} | ||
getValidationBehavior(form) { | ||
return typeof this._validationBehavior === 'function' | ||
? this._validationBehavior({ | ||
submitCount: form.submitCount, | ||
errorMessages: this.errors.value | ||
}) | ||
: this._validationBehavior; | ||
} | ||
shouldValidate(form, validationBehavior) { | ||
switch (validationBehavior || this.getValidationBehavior(form)) { | ||
case 'aggresive': | ||
return true; | ||
case 'lazy': | ||
if (this.touched) { | ||
return true; | ||
} | ||
break; | ||
case 'lazier': | ||
if (this.touched && this.hasError.value) { | ||
return true; | ||
} | ||
break; | ||
} | ||
return false; | ||
} | ||
reset(toDefaultValues) { | ||
@@ -391,15 +590,29 @@ this.touched = false; | ||
if (isArray(this.modelValue.value)) { | ||
this.modelValue.value = deepCopy(this._initialModelValue); | ||
this.modelValue.value = deepCopy(this.initialModelValue); | ||
} | ||
else { | ||
this.modelValue.value = this._initialModelValue; | ||
this.modelValue.value = this.initialModelValue; | ||
} | ||
} | ||
else { | ||
const copy = deepCopy(this._initialModelValue); | ||
const copy = deepCopy(this.initialModelValue); | ||
Object.assign(this.modelValue, copy); | ||
} | ||
} | ||
for (const buffer of this._buffers) { | ||
for (const shouldSetError of buffer.nodesForwards()) { | ||
shouldSetError.value = false; | ||
} | ||
} | ||
Object.assign(this._errors, this._errors.map(() => null)); | ||
} | ||
_setError(ruleNumber, error) { | ||
if (typeof error === 'string') { | ||
this._errors[ruleNumber] = error; | ||
throw error; | ||
} | ||
else { | ||
this._errors[ruleNumber] = null; | ||
} | ||
} | ||
} | ||
@@ -413,48 +626,94 @@ | ||
const isSimpleRule = (rule) => typeof rule === 'function'; | ||
class Form { | ||
constructor() { | ||
this.simpleMap = new Map(); | ||
this.keyedSetMap = new Map(); | ||
this.reactiveFormFieldMap = vue.shallowReactive(new Map()); | ||
this.trySetKeyedSet = trySet(this.keyedSetMap); | ||
this.tryGetKeyedSet = tryGet(this.keyedSetMap); | ||
this.tryGetSimple = tryGet(this.simpleMap); | ||
this.formFields = vue.ref(new Set()); | ||
this.submitting = vue.ref(false); | ||
this.errors = vue.computed(() => { | ||
const errors = []; | ||
for (const formField of this.reactiveFormFieldMap.values()) { | ||
errors.push(...formField.errors.value); | ||
} | ||
return errors; | ||
Object.defineProperty(this, "_simpleValidators", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: new Map() | ||
}); | ||
Object.defineProperty(this, "_keyedValidators", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: new Map() | ||
}); | ||
Object.defineProperty(this, "_reactiveFormFieldMap", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: vue.shallowReactive(new Map()) | ||
}); | ||
Object.defineProperty(this, "tryGetSimpleValidators", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: tryGet(this._simpleValidators) | ||
}); | ||
Object.defineProperty(this, "trySetKeyedValidators", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: trySet(this._keyedValidators) | ||
}); | ||
Object.defineProperty(this, "tryGetKeyedValidators", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: tryGet(this._keyedValidators) | ||
}); | ||
Object.defineProperty(this, "submitCount", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: 0 | ||
}); | ||
Object.defineProperty(this, "submitting", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: vue.ref(false) | ||
}); | ||
Object.defineProperty(this, "errors", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: vue.computed(() => { | ||
const errors = []; | ||
for (const formField of this._reactiveFormFieldMap.values()) { | ||
errors.push(...formField.errors.value); | ||
} | ||
return errors; | ||
}) | ||
}); | ||
} | ||
registerField(uid, name, modelValue, rules) { | ||
const formField = new FormField(name, modelValue, rules); | ||
const simple = { | ||
formField, | ||
keys: [], | ||
vs: [], | ||
rollbacks: [] | ||
registerField(uid, name, modelValue, validationBehavior, rules) { | ||
const formField = new FormField(name, modelValue, validationBehavior, rules); | ||
const simpleValidators = { | ||
validators: [], | ||
meta: { | ||
formField, | ||
keys: [], | ||
rollbacks: [] | ||
} | ||
}; | ||
rules.forEach((rule, ruleNumber) => { | ||
const validate = Form.validateFactory(formField, rule, ruleNumber); | ||
const validator = Form._validatorFactory(formField, ruleNumber); | ||
if (isSimpleRule(rule)) { | ||
if (typeof validate !== 'undefined') { | ||
simple.vs.push(validate); | ||
} | ||
simpleValidators.validators.push(validator); | ||
} | ||
else { | ||
const { key } = rule; | ||
const keyed = { | ||
formField, | ||
v: validate | ||
const keyedValidator = { | ||
validator, | ||
meta: { | ||
formField | ||
} | ||
}; | ||
const rollback = () => { | ||
this.tryGetKeyedSet({ | ||
success: keyedSet => { | ||
keyedSet.delete(keyed); | ||
if (keyedSet.size === 0) { | ||
this.keyedSetMap.delete(key); | ||
this.tryGetKeyedValidators({ | ||
success: keyedValidators => { | ||
keyedValidators.delete(keyedValidator); | ||
if (keyedValidators.size === 0) { | ||
this._keyedValidators.delete(key); | ||
} | ||
@@ -464,25 +723,35 @@ } | ||
}; | ||
simple.keys.push(key); | ||
simple.rollbacks.push(rollback); | ||
this.trySetKeyedSet({ | ||
failure: keyedSet => keyedSet.add(keyed) | ||
})(key, new Set([keyed])); | ||
simpleValidators.meta.keys.push(key); | ||
simpleValidators.meta.rollbacks.push(rollback); | ||
this.trySetKeyedValidators({ | ||
failure: keyedValidators => keyedValidators.add(keyedValidator) | ||
})(key, new Set([keyedValidator])); | ||
} | ||
}); | ||
this.simpleMap.set(uid, simple); | ||
this.reactiveFormFieldMap.set(uid, formField); | ||
this.formFields.value.add(formField); | ||
this._simpleValidators.set(uid, simpleValidators); | ||
this._reactiveFormFieldMap.set(uid, formField); | ||
return formField; | ||
} | ||
validate(uid) { | ||
const simple = this.simpleMap.get(uid); | ||
if (simple && simple.formField.touched) { | ||
return Promise.allSettled([ | ||
...simple.vs.map(v => v([simple.formField.modelValue])), | ||
...this.getPromisesForKeys(simple.keys) | ||
]); | ||
validate(uid, force = false) { | ||
const simpleValidators = this._simpleValidators.get(uid); | ||
if (simpleValidators) { | ||
const { validators, meta } = simpleValidators; | ||
const shouldValidate = meta.formField.shouldValidate(this); | ||
if (force) { | ||
return Promise.allSettled([ | ||
...validators.map(v => v([meta.formField.modelValue])), | ||
...this._getValidationResultsForKeys(meta.keys) | ||
]); | ||
} | ||
if (shouldValidate) { | ||
return Promise.allSettled([ | ||
...validators.map(v => v([meta.formField.modelValue])), | ||
...this._getValidationResultsForKeys(meta.keys) | ||
]); | ||
} | ||
} | ||
} | ||
async validateAll(names) { | ||
const settledResults = await Promise.allSettled(this.getPromisesForNames(names)); | ||
this.submitCount++; | ||
const settledResults = await Promise.allSettled(this._getValidationResultsForNames(names)); | ||
for (const result of settledResults) { | ||
@@ -495,29 +764,24 @@ if (result.status === 'rejected') { | ||
onDelete(uid) { | ||
this.tryGetSimple({ | ||
success: ({ rollbacks, formField }) => { | ||
this.formFields.value.delete(formField); | ||
rollbacks.forEach(r => r()); | ||
this.tryGetSimpleValidators({ | ||
success: ({ meta }) => { | ||
meta.rollbacks.forEach(r => r()); | ||
} | ||
})(uid); | ||
this.simpleMap.delete(uid); | ||
this.reactiveFormFieldMap.delete(uid); | ||
this._simpleValidators.delete(uid); | ||
this._reactiveFormFieldMap.delete(uid); | ||
} | ||
resetFields(toDefaultValues = true) { | ||
for (const { formField } of this.simpleMap.values()) { | ||
formField.reset(toDefaultValues); | ||
resetFields(toDefaultValues) { | ||
for (const { meta } of this._simpleValidators.values()) { | ||
meta.formField.reset(toDefaultValues); | ||
} | ||
} | ||
getPromisesForKeys(keys) { | ||
const promises = []; | ||
_getValidationResultsForKeys(keys) { | ||
const validationResults = []; | ||
for (const key of keys) { | ||
this.tryGetKeyedSet({ | ||
success: keyedSet => { | ||
if (this.isEveryFormFieldTouchedWith(key)) { | ||
const values = [...keyedSet.values()]; | ||
const modelValues = values.map(({ formField }) => formField.modelValue); | ||
const vs = values | ||
.map(({ v }) => v) | ||
.filter(isDefined) | ||
.map(v => v(modelValues)); | ||
promises.push(...vs); | ||
this.tryGetKeyedValidators({ | ||
success: keyedValidators => { | ||
if (this._shouldEveryFieldValidate(keyedValidators)) { | ||
const values = [...keyedValidators.values()]; | ||
const modelValues = values.map(({ meta }) => meta.formField.modelValue); | ||
validationResults.push(...values.map(({ validator }) => validator(modelValues))); | ||
} | ||
@@ -527,82 +791,40 @@ } | ||
} | ||
return promises; | ||
return validationResults; | ||
} | ||
getPromisesForNames(names) { | ||
const promises = []; | ||
if (typeof names === 'undefined') { | ||
for (const { formField, vs } of this.simpleMap.values()) { | ||
formField.touched = true; | ||
promises.push(...vs.map(v => v([formField.modelValue]))); | ||
_getValidationResultsForNames(names) { | ||
const validationResults = []; | ||
if (names === undefined) { | ||
for (const { validators, meta } of this._simpleValidators.values()) { | ||
meta.formField.touched = true; | ||
validationResults.push(...validators.map(v => v([meta.formField.modelValue]))); | ||
} | ||
promises.push(...this.getPromisesForKeys(this.keyedSetMap.keys())); | ||
validationResults.push(...this._getValidationResultsForKeys(this._keyedValidators.keys())); | ||
} | ||
else if (names.length > 0) { | ||
const nameSet = new Set(names); | ||
for (const { formField, keys, vs } of this.simpleMap.values()) { | ||
if (nameSet.has(formField.name)) { | ||
formField.touched = true; | ||
promises.push(...vs.map(v => v([formField.modelValue]))); | ||
promises.push(...this.getPromisesForKeys(keys)); | ||
const uniqueNames = new Set(names); | ||
for (const { validators, meta } of this._simpleValidators.values()) { | ||
meta.formField.touched = true; | ||
if (uniqueNames.has(meta.formField.name)) { | ||
validationResults.push(...validators.map(v => v([meta.formField.modelValue]))); | ||
validationResults.push(...this._getValidationResultsForKeys(meta.keys)); | ||
} | ||
} | ||
} | ||
return promises; | ||
return validationResults; | ||
} | ||
isEveryFormFieldTouchedWith(key) { | ||
let everyFormFieldIsTouched = true; | ||
this.tryGetKeyedSet({ | ||
success: keyedSet => { | ||
for (const { formField } of keyedSet) { | ||
if (!formField.touched) { | ||
everyFormFieldIsTouched = false; | ||
break; | ||
} | ||
} | ||
_shouldEveryFieldValidate(keyedValidators) { | ||
let shouldEveryFieldValidate = true; | ||
const mostRestrictiveValidationBehavior = getMostRestrictiveValidationBehavior(this, keyedValidators); | ||
for (const { meta } of keyedValidators) { | ||
if (!meta.formField.shouldValidate(this, mostRestrictiveValidationBehavior)) { | ||
shouldEveryFieldValidate = false; | ||
} | ||
})(key); | ||
return everyFormFieldIsTouched; | ||
} | ||
return shouldEveryFieldValidate; | ||
} | ||
static validateFactory(formField, rule, ruleNumber) { | ||
const buffer = new LinkedList(); | ||
const setError = (formField, ruleNumber, error) => { | ||
if (typeof error === 'string' && formField.touched) { | ||
formField.setError(ruleNumber, error); | ||
throw error; | ||
} | ||
else { | ||
formField.setError(ruleNumber, null); | ||
} | ||
static _validatorFactory(formField, ruleNumber) { | ||
const curriedValidator = (formField, ruleNumber) => modelValues => { | ||
return formField.validate(ruleNumber, modelValues); | ||
}; | ||
const validator = (formField, rule, ruleNumber) => async (modelValues) => { | ||
let error; | ||
const ruleResult = rule(...modelValues.map(vue.unref)); | ||
if (typeof (ruleResult === null || ruleResult === void 0 ? void 0 : ruleResult.then) === 'function') { | ||
formField.rulesValidating.value++; | ||
const node = buffer.addLast(false); | ||
if (node.prev) { | ||
node.prev.value = true; | ||
} | ||
try { | ||
error = await ruleResult; | ||
} | ||
catch (err) { | ||
error = err; | ||
} | ||
buffer.remove(node); | ||
formField.rulesValidating.value--; | ||
if (!node.value) { | ||
setError(formField, ruleNumber, error); | ||
} | ||
} | ||
else { | ||
error = ruleResult; | ||
setError(formField, ruleNumber, error); | ||
} | ||
}; | ||
if (isSimpleRule(rule)) { | ||
return validator(formField, rule, ruleNumber); | ||
} | ||
else if (rule.rule) { | ||
return validator(formField, rule.rule, ruleNumber); | ||
} | ||
return curriedValidator(formField, ruleNumber); | ||
} | ||
@@ -613,3 +835,3 @@ } | ||
* | ||
* @param formData The structure of your Form Data. | ||
* @param formData - The structure of your Form Data. | ||
* @description | ||
@@ -621,3 +843,3 @@ * Vue composition function for Form Validation. | ||
* For better type inference, consider defining the structure | ||
* of your `formData` upfront and pass it as the generic parameter `T`: | ||
* of your `formData` upfront and pass it as the generic parameter `FormData`: | ||
* ``` | ||
@@ -640,3 +862,2 @@ * type FormData = { | ||
form: transformedFormData, | ||
formFields: form.formFields, | ||
submitting: form.submitting, | ||
@@ -659,21 +880,22 @@ errors: form.errors, | ||
} | ||
if (formData) { | ||
if (formData === undefined) { | ||
form.resetFields(true); | ||
} | ||
else { | ||
form.resetFields(false); | ||
resetFields(formData, transformedFormData); | ||
} | ||
else { | ||
form.resetFields(); | ||
} | ||
}, | ||
add(path$1, value) { | ||
const lastKey = path$1[path$1.length - 1]; | ||
if (typeof lastKey !== 'undefined') { | ||
if (lastKey !== undefined) { | ||
const box = { [lastKey]: value }; | ||
transformFormData(form, box); | ||
const x = path(path$1, transformedFormData); | ||
if (Array.isArray(x)) { | ||
x.push(box[lastKey]); | ||
const transformedField = box[lastKey]; | ||
const valueAtPath = path(path$1, transformedFormData); | ||
if (Array.isArray(valueAtPath)) { | ||
valueAtPath.push(transformedField); | ||
} | ||
else { | ||
set(transformedFormData, path$1, box[lastKey]); | ||
set(transformedFormData, path$1, transformedField); | ||
} | ||
@@ -684,15 +906,17 @@ } | ||
const lastKey = path$1.pop(); | ||
if (typeof lastKey !== 'undefined' && path$1.length === 0) { | ||
cleanupForm(form, transformedFormData[lastKey]); | ||
delete transformedFormData[lastKey]; | ||
} | ||
else if (typeof lastKey !== 'undefined') { | ||
const value = path(path$1, transformedFormData); | ||
if (Array.isArray(value)) { | ||
const deleted = value.splice(+lastKey, 1); | ||
cleanupForm(form, deleted); | ||
if (lastKey !== undefined) { | ||
if (path$1.length === 0) { | ||
cleanupForm(form, transformedFormData[lastKey]); | ||
delete transformedFormData[lastKey]; | ||
} | ||
else { | ||
cleanupForm(form, value[lastKey]); | ||
delete value[lastKey]; | ||
const valueAtPath = path(path$1, transformedFormData); | ||
if (Array.isArray(valueAtPath)) { | ||
const deletedFormData = valueAtPath.splice(+lastKey, 1); | ||
cleanupForm(form, deletedFormData); | ||
} | ||
else { | ||
cleanupForm(form, valueAtPath[lastKey]); | ||
delete valueAtPath[lastKey]; | ||
} | ||
} | ||
@@ -699,0 +923,0 @@ } |
@@ -1,25 +0,117 @@ | ||
import * as vue from 'vue'; | ||
import { ref, reactive, Ref, UnwrapRef, ComputedRef } from 'vue'; | ||
import { ComputedRef } from 'vue'; | ||
import { reactive } from 'vue'; | ||
import { Ref } from 'vue'; | ||
import { ref } from 'vue'; | ||
import { UnwrapRef } from 'vue'; | ||
declare function cleanupForm(form: Form, deletedFormData: any): void; | ||
declare type CurriedValidator = (formField: FormField, ruleNumber: number) => (modelValues: unknown[]) => ValidationResult; | ||
declare function deepCopy<T>(toClone: T): T; | ||
declare type DeepIndex<T, Ks extends readonly Key[], R = unknown> = Ks extends [ | ||
infer First, | ||
...infer Rest | ||
] ? First extends keyof T ? Rest extends readonly Key[] ? DeepIndex<T[First], Rest> : R : R : T; | ||
declare function deepIterator(obj: object, predicate?: (value: object) => boolean): Generator<DeepIteratorResult, void>; | ||
declare type DeepIteratorResult = { | ||
key: string; | ||
value: any; | ||
parent: any; | ||
path: string[]; | ||
isLeaf: boolean; | ||
}; | ||
declare type DeepMaybeRef<T> = T extends Ref ? T | UnwrapRef<T> : T extends Record<string, unknown> ? DeepMaybeRefObject<T> : MaybeRef<T>; | ||
declare type DeepMaybeRefObject<T extends Record<string, unknown>> = { | ||
[K in keyof T]: T[K] extends Ref ? T[K] | UnwrapRef<T[K]> : T[K] extends any[] ? MaybeRef<T[K]> : T[K] extends Record<string, unknown> ? DeepMaybeRefObject<T[K]> : MaybeRef<T[K]>; | ||
}; | ||
export declare type Field<TValue> = { | ||
$value: n_domain.DeepMaybeRef<TValue>; | ||
$rules?: n_domain.Rule<TValue extends any[] ? TValue : UnwrapRef<TValue>>[]; | ||
$validationBehavior?: n_form.ValidationBehavior; | ||
}; | ||
declare type FieldNames<T> = T extends (infer TArray)[] ? FieldNames<TArray> : { | ||
[K in keyof T]-?: T[K] extends { | ||
$value: any; | ||
} | undefined ? K : FieldNames<T[K]>; | ||
}[keyof T]; | ||
declare class Form { | ||
private _simpleValidators; | ||
private _keyedValidators; | ||
private _reactiveFormFieldMap; | ||
tryGetSimpleValidators: ({ success, failure }: { | ||
success(value: SimpleValidators): void; | ||
failure?(): void; | ||
}) => (key: number) => void; | ||
trySetKeyedValidators: ({ success, failure }: { | ||
success?(value: KeyedValidators): void; | ||
failure?(value: KeyedValidators): void; | ||
}) => (key: string, value: KeyedValidators) => void; | ||
tryGetKeyedValidators: ({ success, failure }: { | ||
success(value: KeyedValidators): void; | ||
failure?(): void; | ||
}) => (key: string) => void; | ||
submitCount: number; | ||
submitting: Ref<boolean>; | ||
errors: ComputedRef<string[]>; | ||
registerField(uid: number, name: string, modelValue: unknown, validationBehavior: ValidationBehavior, rules: n_domain.Rule[]): FormField; | ||
validate(uid: number, force?: boolean): Promise<PromiseSettledResult<string | void>[]> | undefined; | ||
validateAll(names?: readonly n_domain.Key[]): Promise<void>; | ||
onDelete(uid: number): void; | ||
resetFields(toDefaultValues: boolean): void; | ||
private _getValidationResultsForKeys; | ||
private _getValidationResultsForNames; | ||
private _shouldEveryFieldValidate; | ||
private static _validatorFactory; | ||
} | ||
declare class FormField { | ||
private _rules; | ||
private _buffers; | ||
private _errors; | ||
private _initialModelValue; | ||
private _validationBehavior; | ||
private _rulesValidating; | ||
initialModelValue: unknown; | ||
name: string; | ||
rulesValidating: vue.Ref<number>; | ||
touched: boolean; | ||
modelValue: ReturnType<typeof ref> | ReturnType<typeof reactive>; | ||
touched: boolean; | ||
errors: vue.ComputedRef<string[]>; | ||
validating: vue.ComputedRef<boolean>; | ||
hasError: vue.ComputedRef<boolean>; | ||
constructor(name: string, modelValue: any, rules: Rule[]); | ||
setError(ruleNumber: number, error: string | null): void; | ||
errors: ComputedRef<string[]>; | ||
validating: ComputedRef<boolean>; | ||
hasError: ComputedRef<boolean>; | ||
constructor(name: string, modelValue: any, validationBehavior: ValidationBehavior, rules: n_domain.Rule[]); | ||
validate(ruleNumber: number, modelValues: unknown[]): Promise<void>; | ||
getValidationBehavior(form: Form): void | ValidationBehaviorString; | ||
shouldValidate(form: Form, validationBehavior?: ValidationBehaviorString): boolean; | ||
reset(toDefaultValues: boolean): void; | ||
private _setError; | ||
} | ||
declare type RefUnrefObject<T extends Record<string, unknown>> = { | ||
[K in keyof T]: T[K] extends Ref ? T[K] | UnwrapRef<T[K]> : T[K] extends any[] ? Ref<T[K]> | T[K] : T[K] extends Record<string, unknown> ? RefUnref<T[K]> : Ref<T[K]> | T[K]; | ||
}; | ||
declare type RefUnref<T> = T extends Ref ? T | UnwrapRef<T> : T extends Record<string, unknown> ? RefUnrefObject<T> : Ref<T> | T; | ||
declare const getMostRestrictiveValidationBehavior: (form: Form, keyedValidators: KeyedValidators) => ValidationBehaviorString; | ||
declare type SimpleRule<T = any> = (value: T) => any; | ||
declare function getResultFormData(transformedFormData: any): any; | ||
declare const isArray: (x: unknown) => x is any[]; | ||
declare const isDefined: <T>(x: T | null | undefined) => x is T; | ||
declare const isField: <T>(x: unknown) => x is Field<T>; | ||
declare const isObject: (x: unknown) => x is Record<Key, any>; | ||
declare const isRecord: (x: unknown) => x is Record<Key, any>; | ||
declare const isSimpleRule: (rule: Rule) => rule is SimpleRule<any>; | ||
declare const isTransformedField: <T>(x: unknown) => x is TransformedField<T>; | ||
declare type Key = string | number; | ||
declare type KeyedRule = { | ||
@@ -29,7 +121,121 @@ key: string; | ||
}; | ||
declare type KeyedValidator = { | ||
validator: Validator; | ||
meta: { | ||
formField: FormField; | ||
}; | ||
}; | ||
declare type KeyedValidators = Set<KeyedValidator>; | ||
declare class LinkedList<T> { | ||
private head; | ||
private tail; | ||
count: number; | ||
get first(): LinkedListNode<T> | null; | ||
get last(): LinkedListNode<T> | null; | ||
addFirst(value: T): LinkedListNode<T>; | ||
addLast(value: T): LinkedListNode<T>; | ||
remove(node: LinkedListNode<T>): void; | ||
removeFirst(): void; | ||
removeLast(): void; | ||
nodesForwards(): { | ||
[Symbol.iterator](): Generator<LinkedListNode<T>, void, unknown>; | ||
}; | ||
nodesBackwards(): { | ||
[Symbol.iterator](): Generator<LinkedListNode<T>, void, unknown>; | ||
}; | ||
} | ||
declare class LinkedListNode<T> { | ||
value: T; | ||
next: LinkedListNode<T> | null; | ||
prev: LinkedListNode<T> | null; | ||
constructor(value: T); | ||
} | ||
declare type MaybeRef<T> = Ref<T> | T; | ||
declare namespace n_domain { | ||
export { | ||
deepCopy, | ||
deepIterator, | ||
tryGet, | ||
trySet, | ||
path, | ||
set, | ||
uid, | ||
isArray, | ||
isTransformedField, | ||
isDefined, | ||
isField, | ||
isObject, | ||
isRecord, | ||
isSimpleRule, | ||
Rule, | ||
SimpleRule, | ||
KeyedRule, | ||
LinkedList, | ||
PromiseCancel, | ||
Key, | ||
DeepMaybeRef, | ||
MaybeRef, | ||
DeepIndex, | ||
Tuple | ||
} | ||
} | ||
declare namespace n_form { | ||
export { | ||
cleanupForm, | ||
getResultFormData, | ||
resetFields, | ||
transformFormData, | ||
ValidationBehavior, | ||
ValidationBehaviorInfo, | ||
ValidationBehaviorString, | ||
getMostRestrictiveValidationBehavior, | ||
Form, | ||
FormField, | ||
ValidationError | ||
} | ||
} | ||
declare function path(path: readonly Key[], obj: Record<Key, unknown>): any; | ||
declare class PromiseCancel<T = unknown> { | ||
private promise; | ||
private resolve; | ||
private reject; | ||
constructor(); | ||
cancelResolve(value: T | PromiseLike<T>): void; | ||
cancelReject(reason?: any): void; | ||
race<Ps extends readonly Promise<any>[]>(...promises: [...Ps]): Promise<T | ([...Ps][number] extends PromiseLike<infer U> ? U : [...Ps][number])>; | ||
private assign; | ||
} | ||
declare function resetFields(formData: any, transformedFormData: any): void; | ||
declare type ResultFormData<FormData extends object> = FormData extends any ? { | ||
[K in keyof FormData]: FormData[K] extends { | ||
$value: infer TValue; | ||
} | undefined ? UnwrapRef<TValue> : FormData[K] extends object ? ResultFormData<FormData[K]> : FormData[K]; | ||
} : never; | ||
declare type Rule<T = any> = SimpleRule<T> | KeyedRule; | ||
declare type Field<TValue> = { | ||
$value: RefUnref<TValue>; | ||
$rules?: Rule<TValue extends any[] ? TValue : UnwrapRef<TValue>>[]; | ||
declare function set(obj: any, keys: readonly Key[], value: any): void; | ||
declare type SimpleRule<T = any> = (value: T) => any; | ||
declare type SimpleValidators = { | ||
validators: Validator[]; | ||
meta: { | ||
formField: FormField; | ||
keys: string[]; | ||
rollbacks: (() => void)[]; | ||
}; | ||
}; | ||
declare type TransformedField<T> = { | ||
@@ -41,31 +247,42 @@ $uid: number; | ||
$validating: boolean; | ||
$onBlur(): void; | ||
$setTouched(): Promise<void>; | ||
}; | ||
declare type TransformedFormData<T extends object> = T extends any ? { | ||
[K in keyof T]: T[K] extends Field<infer TValue> | undefined ? T[K] extends undefined ? undefined : TransformedField<UnwrapRef<TValue>> : T[K] extends object ? TransformedFormData<T[K]> : T[K]; | ||
declare type TransformedFormData<FormData extends object> = FormData extends any ? { | ||
[K in keyof FormData]: FormData[K] extends { | ||
$value: infer TValue; | ||
} | undefined ? FormData[K] extends undefined ? undefined : TransformedField<UnwrapRef<TValue>> : FormData[K] extends object ? TransformedFormData<FormData[K]> : FormData[K]; | ||
} : never; | ||
declare type FormData<T extends object> = T extends any ? { | ||
[K in keyof T]: T[K] extends Field<infer TValue> | undefined ? UnwrapRef<TValue> : T[K] extends object ? FormData<T[K]> : T[K]; | ||
} : never; | ||
declare type FieldNames<T> = T extends (infer TArray)[] ? FieldNames<TArray> : { | ||
[K in keyof T]-?: T[K] extends Field<any> | undefined ? K : FieldNames<T[K]>; | ||
}[keyof T]; | ||
declare type Keys = readonly (string | number)[]; | ||
declare type DeepIndex<T, Ks extends Keys, R = unknown> = Ks extends [ | ||
infer First, | ||
...infer Rest | ||
] ? First extends keyof T ? Rest extends Keys ? DeepIndex<T[First], Rest> : R : R : T; | ||
declare type UseValidation<T extends object> = { | ||
form: TransformedFormData<T>; | ||
formFields: Ref<Set<FormField>>; | ||
declare function transformFormData(form: Form, formData: object): void; | ||
declare const tryGet: <K, V>(map: Map<K, V>) => ({ success, failure }: { | ||
success(value: V): void; | ||
failure?(): void; | ||
}) => (key: K) => void; | ||
declare const trySet: <K, V>(map: Map<K, V>) => ({ success, failure }: { | ||
success?(value: V): void; | ||
failure?(value: V): void; | ||
}) => (key: K, value: V) => void; | ||
declare type Tuple<T, N extends number> = number extends N ? T[] : _Tuple<T, N, []>; | ||
declare type _Tuple<T, N extends number, R extends unknown[]> = R['length'] extends N ? R : _Tuple<T, N, [T, ...R]>; | ||
declare function uid(): number; | ||
declare type UseValidation<FormData extends object> = { | ||
form: TransformedFormData<FormData>; | ||
submitting: Ref<boolean>; | ||
errors: ComputedRef<string[]>; | ||
validateFields(names?: FieldNames<T>[] | string[]): Promise<FormData<T>>; | ||
resetFields(formData?: Partial<FormData<T>>): void; | ||
add<Ks extends Keys>(path: readonly [...Ks], value: DeepIndex<T, Ks> extends Array<infer TArray> ? TArray : DeepIndex<T, Ks>): void; | ||
remove(path: (string | number)[]): void; | ||
validateFields(names?: FieldNames<FormData>[] | string[]): Promise<ResultFormData<FormData>>; | ||
resetFields(formData?: Partial<ResultFormData<FormData>>): void; | ||
add<Ks extends readonly n_domain.Key[]>(path: readonly [...Ks], value: n_domain.DeepIndex<FormData, Ks> extends (infer TArray)[] ? TArray : n_domain.DeepIndex<FormData, Ks>): void; | ||
remove(path: n_domain.Key[]): void; | ||
}; | ||
/** | ||
* | ||
* @param formData The structure of your Form Data. | ||
* @param formData - The structure of your Form Data. | ||
* @description | ||
@@ -77,3 +294,3 @@ * Vue composition function for Form Validation. | ||
* For better type inference, consider defining the structure | ||
* of your `formData` upfront and pass it as the generic parameter `T`: | ||
* of your `formData` upfront and pass it as the generic parameter `FormData`: | ||
* ``` | ||
@@ -89,8 +306,21 @@ * type FormData = { | ||
*/ | ||
declare function useValidation<T extends object>(formData: T): UseValidation<T>; | ||
export declare function useValidation<FormData extends object>(formData: FormData): UseValidation<FormData>; | ||
declare class ValidationError extends Error { | ||
declare type ValidationBehavior = ValidationBehaviorString | ((info: ValidationBehaviorInfo) => ValidationBehaviorString | void); | ||
declare type ValidationBehaviorInfo = { | ||
submitCount: number; | ||
errorMessages: string[]; | ||
}; | ||
declare type ValidationBehaviorString = 'aggresive' | 'lazy' | 'lazier'; | ||
export declare class ValidationError extends Error { | ||
constructor(); | ||
} | ||
export { Field, ValidationError, useValidation }; | ||
declare type ValidationResult = Promise<void | string>; | ||
declare type Validator = ReturnType<CurriedValidator>; | ||
export { } |
@@ -5,4 +5,20 @@ import { isRef, isReactive, watch, ref, computed, reactive, unref, shallowReactive } from 'vue'; | ||
constructor(value) { | ||
this.next = null; | ||
this.prev = null; | ||
Object.defineProperty(this, "value", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: void 0 | ||
}); | ||
Object.defineProperty(this, "next", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: null | ||
}); | ||
Object.defineProperty(this, "prev", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: null | ||
}); | ||
this.value = value; | ||
@@ -13,5 +29,20 @@ } | ||
constructor() { | ||
this.head = null; | ||
this.tail = null; | ||
this.count = 0; | ||
Object.defineProperty(this, "head", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: null | ||
}); | ||
Object.defineProperty(this, "tail", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: null | ||
}); | ||
Object.defineProperty(this, "count", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: 0 | ||
}); | ||
} | ||
@@ -102,3 +133,3 @@ get first() { | ||
} | ||
valuesForwards() { | ||
nodesForwards() { | ||
let node = this.head; | ||
@@ -108,3 +139,3 @@ return { | ||
for (; node !== null; node = node.next) { | ||
yield node.value; | ||
yield node; | ||
} | ||
@@ -114,3 +145,3 @@ } | ||
} | ||
valuesBackwards() { | ||
nodesBackwards() { | ||
let node = this.tail; | ||
@@ -120,3 +151,3 @@ return { | ||
for (; node !== null; node = node.prev) { | ||
yield node.value; | ||
yield node; | ||
} | ||
@@ -128,13 +159,20 @@ } | ||
const isDefined = (x) => x !== null && typeof x !== 'undefined'; | ||
const isRecord = (x) => typeof x === 'object' && x !== null && !Array.isArray(x); | ||
const isArray = (x) => Array.isArray(x); | ||
const isObject = (x) => typeof x === 'object' && x !== null; | ||
const isField = (x) => isRecord(x) ? '$value' in x : false; | ||
const isTransformedField = (x) => isRecord(x) | ||
? '$uid' in x && '$value' in x && '$errors' in x && '$validating' in x | ||
: false; | ||
const isSimpleRule = (rule) => typeof rule === 'function'; | ||
function* deepIterator(obj, predicate = () => false) { | ||
const stack = new LinkedList(); | ||
stack.addLast([obj, null, '', []]); | ||
stack.addLast({ current: obj, parent: null, parentKey: '', path: [] }); | ||
while (stack.count > 0) { | ||
const [current, parent, parentKey, path] = stack.last.value; | ||
const { current, parent, parentKey, path } = stack.last.value; | ||
stack.removeLast(); | ||
let pushedItemsOnStack = false; | ||
if (typeof current === 'object' && | ||
current !== null && | ||
!isRef(current) && | ||
!predicate(current)) { | ||
if (isObject(current) && !isRef(current) && !predicate(current)) { | ||
const entries = Object.entries(current); | ||
@@ -144,7 +182,18 @@ pushedItemsOnStack = entries.length > 0; | ||
const [key, value] = entries[i]; | ||
stack.addLast([value, current, key, [...path, key]]); | ||
stack.addLast({ | ||
current: value, | ||
parent: current, | ||
parentKey: key, | ||
path: [...path, key] | ||
}); | ||
} | ||
} | ||
if (typeof parent === 'object' && parent !== null) { | ||
yield [parentKey, parent[parentKey], parent, path, !pushedItemsOnStack]; | ||
if (isObject(parent)) { | ||
yield { | ||
key: parentKey, | ||
value: parent[parentKey], | ||
parent, | ||
path, | ||
isLeaf: !pushedItemsOnStack | ||
}; | ||
} | ||
@@ -154,22 +203,2 @@ } | ||
const isDefined = (x) => x !== null && typeof x !== 'undefined'; | ||
const isObject = (x) => typeof x === 'object' && x !== null && !Array.isArray(x); | ||
const isArray = (x) => Array.isArray(x); | ||
const isField = (x) => isObject(x) ? '$value' in x : false; | ||
const isTransformedField = (x) => isObject(x) | ||
? '$uid' in x && '$value' in x && '$errors' in x && '$validating' in x | ||
: false; | ||
function cleanupForm(form, deletedData) { | ||
if (isTransformedField(deletedData)) { | ||
form.onDelete(deletedData.$uid); | ||
return; | ||
} | ||
for (const [, value] of deepIterator(deletedData, isTransformedField)) { | ||
if (isTransformedField(value)) { | ||
form.onDelete(value.$uid); | ||
} | ||
} | ||
} | ||
function set(obj, keys, value) { | ||
@@ -198,19 +227,113 @@ if (keys.length === 0) { | ||
function deepCopy(toClone) { | ||
if (typeof toClone !== 'object') { | ||
return toClone; | ||
if (isObject(toClone)) { | ||
const copy = isArray(toClone) ? [] : {}; | ||
for (const { value, path, isLeaf } of deepIterator(toClone)) { | ||
if (isLeaf) { | ||
set(copy, path, value); | ||
} | ||
} | ||
return copy; | ||
} | ||
const copy = isArray(toClone) ? [] : {}; | ||
for (const [, value, , path, isLeaf] of deepIterator(toClone)) { | ||
if (isLeaf) { | ||
set(copy, path, value); | ||
return toClone; | ||
} | ||
const trySet = (map) => ({ success, failure }) => (key, value) => { | ||
const _value = map.get(key); | ||
if (_value) { | ||
failure === null || failure === void 0 ? void 0 : failure(_value); | ||
} | ||
else { | ||
map.set(key, value); | ||
success === null || success === void 0 ? void 0 : success(value); | ||
} | ||
}; | ||
const tryGet = (map) => ({ success, failure }) => (key) => { | ||
const value = map.get(key); | ||
if (value) { | ||
success(value); | ||
} | ||
else { | ||
failure === null || failure === void 0 ? void 0 : failure(); | ||
} | ||
}; | ||
function path(path, obj) { | ||
let value = obj[path[0]]; | ||
for (let i = 0; i < path.length; i++) { | ||
const key = path[i]; | ||
if (value === null || value === undefined) { | ||
return undefined; | ||
} | ||
if (i > 0) { | ||
value = value[key]; | ||
} | ||
} | ||
return copy; | ||
return value; | ||
} | ||
let id = 1; | ||
function uid() { | ||
return id++; | ||
} | ||
class PromiseCancel { | ||
constructor() { | ||
Object.defineProperty(this, "promise", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: void 0 | ||
}); | ||
Object.defineProperty(this, "resolve", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: void 0 | ||
}); | ||
Object.defineProperty(this, "reject", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: void 0 | ||
}); | ||
this.assign(); | ||
} | ||
cancelResolve(value) { | ||
this.resolve(value); | ||
this.assign(); | ||
} | ||
cancelReject(reason) { | ||
this.reject(reason); | ||
this.assign(); | ||
} | ||
race(...promises) { | ||
return Promise.race([this.promise, ...promises]); | ||
} | ||
assign() { | ||
this.promise = new Promise((resolve, reject) => { | ||
this.resolve = resolve; | ||
this.reject = reject; | ||
}); | ||
} | ||
} | ||
function cleanupForm(form, deletedFormData) { | ||
if (isTransformedField(deletedFormData)) { | ||
form.onDelete(deletedFormData.$uid); | ||
return; | ||
} | ||
for (const { value } of deepIterator(deletedFormData, isTransformedField)) { | ||
if (isTransformedField(value)) { | ||
form.onDelete(value.$uid); | ||
} | ||
} | ||
} | ||
function getResultFormData(transformedFormData) { | ||
const result = {}; | ||
for (const [, value, , path, isLeaf] of deepIterator(transformedFormData, isTransformedField)) { | ||
for (const { value, path, isLeaf } of deepIterator(transformedFormData, isTransformedField)) { | ||
if (isTransformedField(value)) { | ||
if (isReactive(value.$value)) { | ||
// Value is reactive -> value is an object or array | ||
// Make sure to do a deep clone to loose the reactive reference | ||
set(result, path, deepCopy(value.$value)); | ||
@@ -224,2 +347,3 @@ } | ||
if (isReactive(value)) { | ||
// Same as above | ||
set(result, path, deepCopy(value)); | ||
@@ -242,3 +366,3 @@ } | ||
} | ||
else if (isObject(transformedValue.$value)) { | ||
else if (isRecord(transformedValue.$value)) { | ||
const copy = deepCopy(value); | ||
@@ -252,3 +376,3 @@ Object.assign(transformedValue.$value, copy); | ||
} | ||
if (typeof value === 'object') { | ||
if (isObject(value)) { | ||
resetFields(value, transformedFormData[key]); | ||
@@ -259,18 +383,11 @@ } | ||
let uid = 1; | ||
function useUid() { | ||
return uid++; | ||
} | ||
function registerField(form, name, field) { | ||
var _a; | ||
const uid = useUid(); | ||
const formField = form.registerField(uid, name, field.$value, (_a = field.$rules) !== null && _a !== void 0 ? _a : []); | ||
const uid$1 = uid(); | ||
const formField = form.registerField(uid$1, name, field.$value, field.$validationBehavior || 'lazier', (_a = field.$rules) !== null && _a !== void 0 ? _a : []); | ||
watch(formField.modelValue, () => { | ||
if (formField.touched) { | ||
form.validate(uid); | ||
} | ||
form.validate(uid$1); | ||
}, { deep: true }); | ||
return { | ||
$uid: uid, | ||
$uid: uid$1, | ||
$value: formField.modelValue, | ||
@@ -280,12 +397,10 @@ $errors: formField.errors, | ||
$validating: formField.validating, | ||
async $onBlur() { | ||
if (!formField.touched) { | ||
formField.touched = true; | ||
await form.validate(uid); | ||
} | ||
async $setTouched() { | ||
formField.touched = true; | ||
await form.validate(uid$1, true); | ||
} | ||
}; | ||
} | ||
function transformFormData(form, data) { | ||
for (const [key, value, parent] of deepIterator(data)) { | ||
function transformFormData(form, formData) { | ||
for (const { key, value, parent } of deepIterator(formData)) { | ||
if (isField(value)) { | ||
@@ -298,85 +413,169 @@ const transformedField = registerField(form, key, value); | ||
const tryGet = (map) => ({ success, failure }) => (key) => { | ||
const value = map.get(key); | ||
if (value) { | ||
success(value); | ||
} | ||
else { | ||
failure === null || failure === void 0 ? void 0 : failure(); | ||
} | ||
const VALIDATION_BEHAVIOR_RESTRICTIVENESS = { | ||
aggresive: 0, | ||
lazy: 1, | ||
lazier: 2 | ||
}; | ||
const trySet = (map) => ({ success, failure }) => (key, value) => { | ||
const _value = map.get(key); | ||
if (_value) { | ||
failure === null || failure === void 0 ? void 0 : failure(_value); | ||
const compareValidationBehavior = (a, b) => VALIDATION_BEHAVIOR_RESTRICTIVENESS[a] - | ||
VALIDATION_BEHAVIOR_RESTRICTIVENESS[b]; | ||
const getMostRestrictiveValidationBehavior = (form, keyedValidators) => { | ||
let mostRestrictiveValidationBehavior = 'aggresive'; | ||
for (const { meta } of keyedValidators.values()) { | ||
const validationBehavior = meta.formField.getValidationBehavior(form); | ||
if (validationBehavior && | ||
compareValidationBehavior(validationBehavior, mostRestrictiveValidationBehavior) > 0) { | ||
mostRestrictiveValidationBehavior = validationBehavior; | ||
} | ||
} | ||
else { | ||
map.set(key, value); | ||
success === null || success === void 0 ? void 0 : success(value); | ||
} | ||
return mostRestrictiveValidationBehavior; | ||
}; | ||
function path(path, o) { | ||
let value = o[path[0]]; | ||
for (let i = 0; i < path.length; i++) { | ||
const key = path[i]; | ||
if (value === null || value === undefined) { | ||
return undefined; | ||
} | ||
if (i > 0) { | ||
value = value[key]; | ||
} | ||
} | ||
return value; | ||
} | ||
class PromiseCancel { | ||
constructor() { | ||
this.assign(); | ||
} | ||
cancelResolve(value) { | ||
this.resolve(value); | ||
this.assign(); | ||
} | ||
cancelReject(reason) { | ||
this.reject(reason); | ||
this.assign(); | ||
} | ||
race(...promises) { | ||
return Promise.race([this.promise, ...promises]); | ||
} | ||
assign() { | ||
this.promise = new Promise((resolve, reject) => { | ||
this.resolve = resolve; | ||
this.reject = reject; | ||
class FormField { | ||
constructor(name, modelValue, validationBehavior, rules) { | ||
Object.defineProperty(this, "_rules", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: void 0 | ||
}); | ||
} | ||
} | ||
class FormField { | ||
constructor(name, modelValue, rules) { | ||
this.rulesValidating = ref(0); | ||
this.touched = false; | ||
this.errors = computed(() => this._errors.filter(isDefined)); | ||
this.validating = computed(() => this.rulesValidating.value > 0); | ||
this.hasError = computed(() => this.errors.value.length > 0); | ||
Object.defineProperty(this, "_buffers", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: void 0 | ||
}); | ||
Object.defineProperty(this, "_errors", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: void 0 | ||
}); | ||
Object.defineProperty(this, "_validationBehavior", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: void 0 | ||
}); | ||
Object.defineProperty(this, "_rulesValidating", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: ref(0) | ||
}); | ||
Object.defineProperty(this, "initialModelValue", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: void 0 | ||
}); | ||
Object.defineProperty(this, "name", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: void 0 | ||
}); | ||
Object.defineProperty(this, "touched", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: false | ||
}); | ||
Object.defineProperty(this, "modelValue", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: void 0 | ||
}); | ||
Object.defineProperty(this, "errors", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: computed(() => this._errors.filter(isDefined)) | ||
}); | ||
Object.defineProperty(this, "validating", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: computed(() => this._rulesValidating.value > 0) | ||
}); | ||
Object.defineProperty(this, "hasError", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: computed(() => this.errors.value.length > 0) | ||
}); | ||
this._rules = rules.map(rule => isSimpleRule(rule) ? rule : rule.rule); | ||
this._buffers = rules.map(() => new LinkedList()); | ||
this._errors = reactive(rules.map(() => null)); | ||
this._validationBehavior = validationBehavior; | ||
this.name = name; | ||
this._errors = reactive(rules.map(() => null)); | ||
if (isRef(modelValue) || isReactive(modelValue)) { | ||
this.modelValue = modelValue; | ||
this._initialModelValue = deepCopy(unref(modelValue)); | ||
this.initialModelValue = deepCopy(unref(modelValue)); | ||
} | ||
else if (isObject(modelValue)) { | ||
else if (isRecord(modelValue)) { | ||
this.modelValue = reactive(modelValue); | ||
this._initialModelValue = deepCopy(this.modelValue); | ||
this.initialModelValue = deepCopy(this.modelValue); | ||
} | ||
else { | ||
this.modelValue = ref(modelValue); | ||
this._initialModelValue = deepCopy(unref(modelValue)); | ||
this.initialModelValue = deepCopy(unref(modelValue)); | ||
} | ||
} | ||
setError(ruleNumber, error) { | ||
this._errors[ruleNumber] = error; | ||
async validate(ruleNumber, modelValues) { | ||
const rule = this._rules[ruleNumber]; | ||
const buffer = this._buffers[ruleNumber]; | ||
let error; | ||
if (!rule) { | ||
return; | ||
} | ||
const ruleResult = rule(...modelValues.map(unref)); | ||
if (typeof (ruleResult === null || ruleResult === void 0 ? void 0 : ruleResult.then) === 'function') { | ||
this._rulesValidating.value++; | ||
const shouldSetError = buffer.addLast(true); | ||
if (shouldSetError.prev) { | ||
shouldSetError.prev.value = false; | ||
} | ||
try { | ||
error = await ruleResult; | ||
} | ||
catch (err) { | ||
error = err; | ||
} | ||
buffer.remove(shouldSetError); | ||
this._rulesValidating.value--; | ||
if (shouldSetError.value) { | ||
this._setError(ruleNumber, error); | ||
} | ||
} | ||
else { | ||
error = ruleResult; | ||
this._setError(ruleNumber, error); | ||
} | ||
} | ||
getValidationBehavior(form) { | ||
return typeof this._validationBehavior === 'function' | ||
? this._validationBehavior({ | ||
submitCount: form.submitCount, | ||
errorMessages: this.errors.value | ||
}) | ||
: this._validationBehavior; | ||
} | ||
shouldValidate(form, validationBehavior) { | ||
switch (validationBehavior || this.getValidationBehavior(form)) { | ||
case 'aggresive': | ||
return true; | ||
case 'lazy': | ||
if (this.touched) { | ||
return true; | ||
} | ||
break; | ||
case 'lazier': | ||
if (this.touched && this.hasError.value) { | ||
return true; | ||
} | ||
break; | ||
} | ||
return false; | ||
} | ||
reset(toDefaultValues) { | ||
@@ -387,15 +586,29 @@ this.touched = false; | ||
if (isArray(this.modelValue.value)) { | ||
this.modelValue.value = deepCopy(this._initialModelValue); | ||
this.modelValue.value = deepCopy(this.initialModelValue); | ||
} | ||
else { | ||
this.modelValue.value = this._initialModelValue; | ||
this.modelValue.value = this.initialModelValue; | ||
} | ||
} | ||
else { | ||
const copy = deepCopy(this._initialModelValue); | ||
const copy = deepCopy(this.initialModelValue); | ||
Object.assign(this.modelValue, copy); | ||
} | ||
} | ||
for (const buffer of this._buffers) { | ||
for (const shouldSetError of buffer.nodesForwards()) { | ||
shouldSetError.value = false; | ||
} | ||
} | ||
Object.assign(this._errors, this._errors.map(() => null)); | ||
} | ||
_setError(ruleNumber, error) { | ||
if (typeof error === 'string') { | ||
this._errors[ruleNumber] = error; | ||
throw error; | ||
} | ||
else { | ||
this._errors[ruleNumber] = null; | ||
} | ||
} | ||
} | ||
@@ -409,48 +622,94 @@ | ||
const isSimpleRule = (rule) => typeof rule === 'function'; | ||
class Form { | ||
constructor() { | ||
this.simpleMap = new Map(); | ||
this.keyedSetMap = new Map(); | ||
this.reactiveFormFieldMap = shallowReactive(new Map()); | ||
this.trySetKeyedSet = trySet(this.keyedSetMap); | ||
this.tryGetKeyedSet = tryGet(this.keyedSetMap); | ||
this.tryGetSimple = tryGet(this.simpleMap); | ||
this.formFields = ref(new Set()); | ||
this.submitting = ref(false); | ||
this.errors = computed(() => { | ||
const errors = []; | ||
for (const formField of this.reactiveFormFieldMap.values()) { | ||
errors.push(...formField.errors.value); | ||
} | ||
return errors; | ||
Object.defineProperty(this, "_simpleValidators", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: new Map() | ||
}); | ||
Object.defineProperty(this, "_keyedValidators", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: new Map() | ||
}); | ||
Object.defineProperty(this, "_reactiveFormFieldMap", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: shallowReactive(new Map()) | ||
}); | ||
Object.defineProperty(this, "tryGetSimpleValidators", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: tryGet(this._simpleValidators) | ||
}); | ||
Object.defineProperty(this, "trySetKeyedValidators", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: trySet(this._keyedValidators) | ||
}); | ||
Object.defineProperty(this, "tryGetKeyedValidators", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: tryGet(this._keyedValidators) | ||
}); | ||
Object.defineProperty(this, "submitCount", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: 0 | ||
}); | ||
Object.defineProperty(this, "submitting", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: ref(false) | ||
}); | ||
Object.defineProperty(this, "errors", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: computed(() => { | ||
const errors = []; | ||
for (const formField of this._reactiveFormFieldMap.values()) { | ||
errors.push(...formField.errors.value); | ||
} | ||
return errors; | ||
}) | ||
}); | ||
} | ||
registerField(uid, name, modelValue, rules) { | ||
const formField = new FormField(name, modelValue, rules); | ||
const simple = { | ||
formField, | ||
keys: [], | ||
vs: [], | ||
rollbacks: [] | ||
registerField(uid, name, modelValue, validationBehavior, rules) { | ||
const formField = new FormField(name, modelValue, validationBehavior, rules); | ||
const simpleValidators = { | ||
validators: [], | ||
meta: { | ||
formField, | ||
keys: [], | ||
rollbacks: [] | ||
} | ||
}; | ||
rules.forEach((rule, ruleNumber) => { | ||
const validate = Form.validateFactory(formField, rule, ruleNumber); | ||
const validator = Form._validatorFactory(formField, ruleNumber); | ||
if (isSimpleRule(rule)) { | ||
if (typeof validate !== 'undefined') { | ||
simple.vs.push(validate); | ||
} | ||
simpleValidators.validators.push(validator); | ||
} | ||
else { | ||
const { key } = rule; | ||
const keyed = { | ||
formField, | ||
v: validate | ||
const keyedValidator = { | ||
validator, | ||
meta: { | ||
formField | ||
} | ||
}; | ||
const rollback = () => { | ||
this.tryGetKeyedSet({ | ||
success: keyedSet => { | ||
keyedSet.delete(keyed); | ||
if (keyedSet.size === 0) { | ||
this.keyedSetMap.delete(key); | ||
this.tryGetKeyedValidators({ | ||
success: keyedValidators => { | ||
keyedValidators.delete(keyedValidator); | ||
if (keyedValidators.size === 0) { | ||
this._keyedValidators.delete(key); | ||
} | ||
@@ -460,25 +719,35 @@ } | ||
}; | ||
simple.keys.push(key); | ||
simple.rollbacks.push(rollback); | ||
this.trySetKeyedSet({ | ||
failure: keyedSet => keyedSet.add(keyed) | ||
})(key, new Set([keyed])); | ||
simpleValidators.meta.keys.push(key); | ||
simpleValidators.meta.rollbacks.push(rollback); | ||
this.trySetKeyedValidators({ | ||
failure: keyedValidators => keyedValidators.add(keyedValidator) | ||
})(key, new Set([keyedValidator])); | ||
} | ||
}); | ||
this.simpleMap.set(uid, simple); | ||
this.reactiveFormFieldMap.set(uid, formField); | ||
this.formFields.value.add(formField); | ||
this._simpleValidators.set(uid, simpleValidators); | ||
this._reactiveFormFieldMap.set(uid, formField); | ||
return formField; | ||
} | ||
validate(uid) { | ||
const simple = this.simpleMap.get(uid); | ||
if (simple && simple.formField.touched) { | ||
return Promise.allSettled([ | ||
...simple.vs.map(v => v([simple.formField.modelValue])), | ||
...this.getPromisesForKeys(simple.keys) | ||
]); | ||
validate(uid, force = false) { | ||
const simpleValidators = this._simpleValidators.get(uid); | ||
if (simpleValidators) { | ||
const { validators, meta } = simpleValidators; | ||
const shouldValidate = meta.formField.shouldValidate(this); | ||
if (force) { | ||
return Promise.allSettled([ | ||
...validators.map(v => v([meta.formField.modelValue])), | ||
...this._getValidationResultsForKeys(meta.keys) | ||
]); | ||
} | ||
if (shouldValidate) { | ||
return Promise.allSettled([ | ||
...validators.map(v => v([meta.formField.modelValue])), | ||
...this._getValidationResultsForKeys(meta.keys) | ||
]); | ||
} | ||
} | ||
} | ||
async validateAll(names) { | ||
const settledResults = await Promise.allSettled(this.getPromisesForNames(names)); | ||
this.submitCount++; | ||
const settledResults = await Promise.allSettled(this._getValidationResultsForNames(names)); | ||
for (const result of settledResults) { | ||
@@ -491,29 +760,24 @@ if (result.status === 'rejected') { | ||
onDelete(uid) { | ||
this.tryGetSimple({ | ||
success: ({ rollbacks, formField }) => { | ||
this.formFields.value.delete(formField); | ||
rollbacks.forEach(r => r()); | ||
this.tryGetSimpleValidators({ | ||
success: ({ meta }) => { | ||
meta.rollbacks.forEach(r => r()); | ||
} | ||
})(uid); | ||
this.simpleMap.delete(uid); | ||
this.reactiveFormFieldMap.delete(uid); | ||
this._simpleValidators.delete(uid); | ||
this._reactiveFormFieldMap.delete(uid); | ||
} | ||
resetFields(toDefaultValues = true) { | ||
for (const { formField } of this.simpleMap.values()) { | ||
formField.reset(toDefaultValues); | ||
resetFields(toDefaultValues) { | ||
for (const { meta } of this._simpleValidators.values()) { | ||
meta.formField.reset(toDefaultValues); | ||
} | ||
} | ||
getPromisesForKeys(keys) { | ||
const promises = []; | ||
_getValidationResultsForKeys(keys) { | ||
const validationResults = []; | ||
for (const key of keys) { | ||
this.tryGetKeyedSet({ | ||
success: keyedSet => { | ||
if (this.isEveryFormFieldTouchedWith(key)) { | ||
const values = [...keyedSet.values()]; | ||
const modelValues = values.map(({ formField }) => formField.modelValue); | ||
const vs = values | ||
.map(({ v }) => v) | ||
.filter(isDefined) | ||
.map(v => v(modelValues)); | ||
promises.push(...vs); | ||
this.tryGetKeyedValidators({ | ||
success: keyedValidators => { | ||
if (this._shouldEveryFieldValidate(keyedValidators)) { | ||
const values = [...keyedValidators.values()]; | ||
const modelValues = values.map(({ meta }) => meta.formField.modelValue); | ||
validationResults.push(...values.map(({ validator }) => validator(modelValues))); | ||
} | ||
@@ -523,82 +787,40 @@ } | ||
} | ||
return promises; | ||
return validationResults; | ||
} | ||
getPromisesForNames(names) { | ||
const promises = []; | ||
if (typeof names === 'undefined') { | ||
for (const { formField, vs } of this.simpleMap.values()) { | ||
formField.touched = true; | ||
promises.push(...vs.map(v => v([formField.modelValue]))); | ||
_getValidationResultsForNames(names) { | ||
const validationResults = []; | ||
if (names === undefined) { | ||
for (const { validators, meta } of this._simpleValidators.values()) { | ||
meta.formField.touched = true; | ||
validationResults.push(...validators.map(v => v([meta.formField.modelValue]))); | ||
} | ||
promises.push(...this.getPromisesForKeys(this.keyedSetMap.keys())); | ||
validationResults.push(...this._getValidationResultsForKeys(this._keyedValidators.keys())); | ||
} | ||
else if (names.length > 0) { | ||
const nameSet = new Set(names); | ||
for (const { formField, keys, vs } of this.simpleMap.values()) { | ||
if (nameSet.has(formField.name)) { | ||
formField.touched = true; | ||
promises.push(...vs.map(v => v([formField.modelValue]))); | ||
promises.push(...this.getPromisesForKeys(keys)); | ||
const uniqueNames = new Set(names); | ||
for (const { validators, meta } of this._simpleValidators.values()) { | ||
meta.formField.touched = true; | ||
if (uniqueNames.has(meta.formField.name)) { | ||
validationResults.push(...validators.map(v => v([meta.formField.modelValue]))); | ||
validationResults.push(...this._getValidationResultsForKeys(meta.keys)); | ||
} | ||
} | ||
} | ||
return promises; | ||
return validationResults; | ||
} | ||
isEveryFormFieldTouchedWith(key) { | ||
let everyFormFieldIsTouched = true; | ||
this.tryGetKeyedSet({ | ||
success: keyedSet => { | ||
for (const { formField } of keyedSet) { | ||
if (!formField.touched) { | ||
everyFormFieldIsTouched = false; | ||
break; | ||
} | ||
} | ||
_shouldEveryFieldValidate(keyedValidators) { | ||
let shouldEveryFieldValidate = true; | ||
const mostRestrictiveValidationBehavior = getMostRestrictiveValidationBehavior(this, keyedValidators); | ||
for (const { meta } of keyedValidators) { | ||
if (!meta.formField.shouldValidate(this, mostRestrictiveValidationBehavior)) { | ||
shouldEveryFieldValidate = false; | ||
} | ||
})(key); | ||
return everyFormFieldIsTouched; | ||
} | ||
return shouldEveryFieldValidate; | ||
} | ||
static validateFactory(formField, rule, ruleNumber) { | ||
const buffer = new LinkedList(); | ||
const setError = (formField, ruleNumber, error) => { | ||
if (typeof error === 'string' && formField.touched) { | ||
formField.setError(ruleNumber, error); | ||
throw error; | ||
} | ||
else { | ||
formField.setError(ruleNumber, null); | ||
} | ||
static _validatorFactory(formField, ruleNumber) { | ||
const curriedValidator = (formField, ruleNumber) => modelValues => { | ||
return formField.validate(ruleNumber, modelValues); | ||
}; | ||
const validator = (formField, rule, ruleNumber) => async (modelValues) => { | ||
let error; | ||
const ruleResult = rule(...modelValues.map(unref)); | ||
if (typeof (ruleResult === null || ruleResult === void 0 ? void 0 : ruleResult.then) === 'function') { | ||
formField.rulesValidating.value++; | ||
const node = buffer.addLast(false); | ||
if (node.prev) { | ||
node.prev.value = true; | ||
} | ||
try { | ||
error = await ruleResult; | ||
} | ||
catch (err) { | ||
error = err; | ||
} | ||
buffer.remove(node); | ||
formField.rulesValidating.value--; | ||
if (!node.value) { | ||
setError(formField, ruleNumber, error); | ||
} | ||
} | ||
else { | ||
error = ruleResult; | ||
setError(formField, ruleNumber, error); | ||
} | ||
}; | ||
if (isSimpleRule(rule)) { | ||
return validator(formField, rule, ruleNumber); | ||
} | ||
else if (rule.rule) { | ||
return validator(formField, rule.rule, ruleNumber); | ||
} | ||
return curriedValidator(formField, ruleNumber); | ||
} | ||
@@ -609,3 +831,3 @@ } | ||
* | ||
* @param formData The structure of your Form Data. | ||
* @param formData - The structure of your Form Data. | ||
* @description | ||
@@ -617,3 +839,3 @@ * Vue composition function for Form Validation. | ||
* For better type inference, consider defining the structure | ||
* of your `formData` upfront and pass it as the generic parameter `T`: | ||
* of your `formData` upfront and pass it as the generic parameter `FormData`: | ||
* ``` | ||
@@ -636,3 +858,2 @@ * type FormData = { | ||
form: transformedFormData, | ||
formFields: form.formFields, | ||
submitting: form.submitting, | ||
@@ -655,21 +876,22 @@ errors: form.errors, | ||
} | ||
if (formData) { | ||
if (formData === undefined) { | ||
form.resetFields(true); | ||
} | ||
else { | ||
form.resetFields(false); | ||
resetFields(formData, transformedFormData); | ||
} | ||
else { | ||
form.resetFields(); | ||
} | ||
}, | ||
add(path$1, value) { | ||
const lastKey = path$1[path$1.length - 1]; | ||
if (typeof lastKey !== 'undefined') { | ||
if (lastKey !== undefined) { | ||
const box = { [lastKey]: value }; | ||
transformFormData(form, box); | ||
const x = path(path$1, transformedFormData); | ||
if (Array.isArray(x)) { | ||
x.push(box[lastKey]); | ||
const transformedField = box[lastKey]; | ||
const valueAtPath = path(path$1, transformedFormData); | ||
if (Array.isArray(valueAtPath)) { | ||
valueAtPath.push(transformedField); | ||
} | ||
else { | ||
set(transformedFormData, path$1, box[lastKey]); | ||
set(transformedFormData, path$1, transformedField); | ||
} | ||
@@ -680,15 +902,17 @@ } | ||
const lastKey = path$1.pop(); | ||
if (typeof lastKey !== 'undefined' && path$1.length === 0) { | ||
cleanupForm(form, transformedFormData[lastKey]); | ||
delete transformedFormData[lastKey]; | ||
} | ||
else if (typeof lastKey !== 'undefined') { | ||
const value = path(path$1, transformedFormData); | ||
if (Array.isArray(value)) { | ||
const deleted = value.splice(+lastKey, 1); | ||
cleanupForm(form, deleted); | ||
if (lastKey !== undefined) { | ||
if (path$1.length === 0) { | ||
cleanupForm(form, transformedFormData[lastKey]); | ||
delete transformedFormData[lastKey]; | ||
} | ||
else { | ||
cleanupForm(form, value[lastKey]); | ||
delete value[lastKey]; | ||
const valueAtPath = path(path$1, transformedFormData); | ||
if (Array.isArray(valueAtPath)) { | ||
const deletedFormData = valueAtPath.splice(+lastKey, 1); | ||
cleanupForm(form, deletedFormData); | ||
} | ||
else { | ||
cleanupForm(form, valueAtPath[lastKey]); | ||
delete valueAtPath[lastKey]; | ||
} | ||
} | ||
@@ -695,0 +919,0 @@ } |
{ | ||
"name": "vue3-form-validation", | ||
"version": "4.1.1", | ||
"version": "5.0.0-beta.1", | ||
"description": "Vue composition function for Form Validation", | ||
@@ -5,0 +5,0 @@ "author": { |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
81438
2028
2