vue3-form-validation
Advanced tools
Comparing version 2.1.2 to 3.0.0
@@ -1,2 +0,2 @@ | ||
import { Ref, UnwrapRef } from 'vue'; | ||
import { Ref, ComputedRef, UnwrapRef } from 'vue'; | ||
import Form from '../Form'; | ||
@@ -9,5 +9,5 @@ export declare type SimpleRule<T = any> = (value: T) => Promise<unknown> | unknown; | ||
export declare type Rule<T = any> = SimpleRule<T> | KeyedRule<T>; | ||
export declare type Field<T> = { | ||
$value: Ref<T> | T; | ||
$rules?: Rule<T>[]; | ||
export declare type Field<TValue> = { | ||
$value: TValue extends Ref ? TValue | UnwrapRef<TValue> : TValue extends Record<string, unknown> | any[] ? RefUnref<TValue> : Ref<TValue> | TValue; | ||
$rules?: Rule<UnwrapRef<TValue>>[]; | ||
}; | ||
@@ -21,39 +21,58 @@ export declare type TransformedField<T> = { | ||
}; | ||
declare type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>; | ||
declare type ValidateFormData<T> = { | ||
export declare type RefUnref<T extends Record<string, unknown> | any[]> = { | ||
[K in keyof T]: T[K] extends Ref ? T[K] | UnwrapRef<T[K]> : T[K] extends Array<infer TArray> ? RefUnref<TArray[]> : T[K] extends Record<string, unknown> ? RefUnref<T[K]> : Ref<T[K]> | T[K]; | ||
}; | ||
export declare type ValidateInput<T extends object | any[]> = { | ||
[K in keyof T]: T[K] extends { | ||
$value: infer TValue; | ||
} ? TValue extends Ref<infer TRef> ? Field<TRef> : Field<TValue> : T[K] extends Array<infer TArray> ? ValidateFormData<TArray>[] : T[K] extends Record<string, unknown> ? ValidateFormData<T[K]> : unknown; | ||
} ? Field<TValue> : T[K] extends Array<infer TArray> ? ValidateInput<TArray[]> : T[K] extends Record<string, unknown> ? ValidateInput<T[K]> : unknown; | ||
}; | ||
declare type TransformedFormData<T> = T extends any ? { | ||
[K in keyof T]: T[K] extends Field<infer TValue> ? TransformedField<TValue> : T[K] extends Array<infer TArray> ? TransformedFormData<TArray>[] : T[K] extends Record<string, unknown> ? TransformedFormData<T[K]> : T[K]; | ||
export declare type TransformedFormData<T extends object> = T extends any ? { | ||
[K in keyof T]: T[K] extends { | ||
$value: infer TValue; | ||
} ? TransformedField<UnwrapRef<TValue>> : T[K] extends Array<infer TArray> ? TransformedFormData<TArray[]> : T[K] extends Record<string, unknown> ? TransformedFormData<T[K]> : T[K]; | ||
} : never; | ||
declare type FormData<T> = T extends any ? { | ||
[K in keyof T as T[K] extends any[] ? K : T[K] extends Record<string, unknown> ? K : never]: T[K] extends Field<infer TValue> ? UnwrapNestedRefs<TValue> : T[K] extends Array<infer TArray> ? FormData<TArray>[] : T[K] extends Record<string, unknown> ? FormData<T[K]> : never; | ||
export declare type FormData<T extends object> = T extends any ? { | ||
[K in keyof T]: T[K] extends { | ||
$value: infer TValue; | ||
} ? UnwrapRef<TValue> : T[K] extends Array<infer TArray> ? FormData<TArray[]> : T[K] extends Record<string, unknown> ? FormData<T[K]> : T[K]; | ||
} : never; | ||
declare type Keys = readonly (string | number)[]; | ||
declare type DeepIndex<T, Ks extends Keys> = Ks extends [infer First, ...infer Rest] ? First extends keyof T ? Rest extends Keys ? DeepIndex<T[First], Rest> : undefined : undefined : T; | ||
/** | ||
* | ||
* @param form - Form object with methods like `registerField` and `validate`. | ||
* @param formData - The form data to transform. | ||
* @description In place transformation of a given form data object. | ||
* Recursively add's some metadata to every form field. | ||
*/ | ||
export declare type Keys = readonly (string | number)[]; | ||
export declare type DeepIndex<T, Ks extends Keys> = Ks extends [ | ||
infer First, | ||
...infer Rest | ||
] ? First extends keyof T ? Rest extends Keys ? DeepIndex<T[First], Rest> : undefined : undefined : T; | ||
export declare function transformFormData(form: Form, formData: any): void; | ||
export declare function cleanupForm(form: Form, formData: any): void; | ||
export declare function getResultFormData(formData: any, resultFormData: any): void; | ||
declare type UseValidation<T extends object> = { | ||
form: TransformedFormData<T>; | ||
submitting: Ref<boolean>; | ||
errors: ComputedRef<string[]>; | ||
validateFields(): Promise<FormData<T>>; | ||
resetFields(): void; | ||
add<Ks extends Keys>(pathToArray: readonly [...Ks], value: DeepIndex<ValidateInput<T>, Ks> extends Array<infer TArray> ? TArray : never): void; | ||
remove<Ks extends Keys>(pathToArray: readonly [...Ks], index: DeepIndex<ValidateInput<T>, Ks> extends any[] ? number : never): void; | ||
}; | ||
/** | ||
* | ||
* @param formData - The structure of your Form Data | ||
* @hint | ||
* To get the best IntelliSense when using TypeScript, consider defining the structure | ||
* of your `formData` upfront and pass it as the generic parameter `T`. | ||
* @param formData The structure of your Form Data. | ||
* @description | ||
* Vue composition function for Form Validation. | ||
* @docs | ||
* https://github.com/JensDll/vue3-form-validation | ||
* @typescript | ||
* For best type inference, consider defining the structure | ||
* of your `formData` upfront and pass it as the generic parameter `T`. For example: | ||
* ``` | ||
* type FormData = { | ||
* name: Field<string>, | ||
* email: Field<string>, | ||
* password: Field<string> | ||
* } | ||
* | ||
* const { ... } = useValidation<FormData>({ ... }) | ||
* ``` | ||
*/ | ||
export declare function useValidation<T>(formData: T & ValidateFormData<T>): { | ||
form: TransformedFormData<T>; | ||
onSubmit(success: (formData: FormData<T>) => void, error?: (() => void) | undefined): void; | ||
add<Ks extends Keys>(pathToArray: readonly [...Ks], value: DeepIndex<ValidateFormData<T>, Ks> extends (infer TArray)[] ? TArray : never): void; | ||
remove<Ks_1 extends Keys>(pathToArray: readonly [...Ks_1], index: DeepIndex<ValidateFormData<T>, Ks_1> extends any[] ? number : never): void; | ||
}; | ||
export declare function useValidation<T extends object>(formData: T & ValidateInput<T>): UseValidation<T>; | ||
export {}; |
@@ -1,2 +0,2 @@ | ||
import { isReactive, reactive, watch } from 'vue'; | ||
import { reactive, watch, ref } from 'vue'; | ||
import useUid from './useUid'; | ||
@@ -12,9 +12,2 @@ import Form from '../Form'; | ||
: false; | ||
/** | ||
* | ||
* @param form - Form object with methods like `registerField` and `validate`. | ||
* @param formData - The form data to transform. | ||
* @description In place transformation of a given form data object. | ||
* Recursively add's some metadata to every form field. | ||
*/ | ||
export function transformFormData(form, formData) { | ||
@@ -25,6 +18,6 @@ Object.entries(formData).forEach(([key, value]) => { | ||
const uid = useUid(); | ||
const formField = form.registerField(uid, (_a = value.$rules) !== null && _a !== void 0 ? _a : []); | ||
const formField = form.registerField(uid, (_a = value.$rules) !== null && _a !== void 0 ? _a : [], value.$value); | ||
formData[key] = reactive({ | ||
$uid: uid, | ||
$value: value.$value, | ||
$value: formField.modelValue, | ||
$errors: formField.getErrors(), | ||
@@ -39,15 +32,7 @@ $validating: formField.validating(), | ||
}); | ||
formField.modelValue = formData[key].$value; | ||
const watchHandler = async (modelValue) => { | ||
formField.modelValue = modelValue; | ||
watch(formField.modelValue, () => { | ||
if (formField.touched) { | ||
await form.validate(uid); | ||
form.validate(uid); | ||
} | ||
}; | ||
if (isReactive(formData[key].$value)) { | ||
watch(formData[key].$value, watchHandler); | ||
} | ||
else { | ||
watch(() => formData[key].$value, watchHandler); | ||
} | ||
}); | ||
return; | ||
@@ -77,12 +62,13 @@ } | ||
} | ||
resultFormData[key] = {}; | ||
if (typeof value == 'object') { | ||
resultFormData[key] = {}; | ||
} | ||
else { | ||
resultFormData[key] = value; | ||
return; | ||
} | ||
if (Array.isArray(value)) { | ||
resultFormData[key] = []; | ||
} | ||
if (typeof value === 'object') { | ||
getResultFormData(value, resultFormData[key]); | ||
} | ||
else { | ||
delete resultFormData[key]; | ||
} | ||
getResultFormData(value, resultFormData[key]); | ||
}); | ||
@@ -92,27 +78,46 @@ } | ||
* | ||
* @param formData - The structure of your Form Data | ||
* @hint | ||
* To get the best IntelliSense when using TypeScript, consider defining the structure | ||
* of your `formData` upfront and pass it as the generic parameter `T`. | ||
* @param formData The structure of your Form Data. | ||
* @description | ||
* Vue composition function for Form Validation. | ||
* @docs | ||
* https://github.com/JensDll/vue3-form-validation | ||
* @typescript | ||
* For best type inference, consider defining the structure | ||
* of your `formData` upfront and pass it as the generic parameter `T`. For example: | ||
* ``` | ||
* type FormData = { | ||
* name: Field<string>, | ||
* email: Field<string>, | ||
* password: Field<string> | ||
* } | ||
* | ||
* const { ... } = useValidation<FormData>({ ... }) | ||
* ``` | ||
*/ | ||
export function useValidation(formData) { | ||
const form = new Form(); | ||
const submitting = ref(false); | ||
transformFormData(form, formData); | ||
const reactiveFormData = reactive(formData); | ||
const transformedFormData = reactive(formData); | ||
return { | ||
form: reactiveFormData, | ||
onSubmit(success, error) { | ||
form.validateAll().then(hasError => { | ||
if (hasError) { | ||
error === null || error === void 0 ? void 0 : error(); | ||
} | ||
else { | ||
const resultFormData = {}; | ||
getResultFormData(reactiveFormData, resultFormData); | ||
success(resultFormData); | ||
} | ||
}); | ||
form: transformedFormData, | ||
submitting, | ||
errors: form.getErrors(), | ||
async validateFields() { | ||
submitting.value = true; | ||
const hasError = await form.validateAll(); | ||
if (hasError) { | ||
submitting.value = false; | ||
throw undefined; | ||
} | ||
const resultFormData = {}; | ||
getResultFormData(transformedFormData, resultFormData); | ||
submitting.value = false; | ||
return resultFormData; | ||
}, | ||
resetFields() { | ||
form.resetFields(); | ||
}, | ||
add(pathToArray, value) { | ||
const xs = path(pathToArray, reactiveFormData); | ||
const xs = path(pathToArray, transformedFormData); | ||
if (Array.isArray(xs)) { | ||
@@ -124,3 +129,3 @@ transformFormData(form, value); | ||
remove(pathToArray, index) { | ||
const xs = path(pathToArray, reactiveFormData); | ||
const xs = path(pathToArray, transformedFormData); | ||
if (Array.isArray(xs)) { | ||
@@ -127,0 +132,0 @@ const deleted = xs.splice(index, 1); |
import { Rule } from './composable/useValidation'; | ||
import FormField from './FormField'; | ||
declare type ValidateResult = void | string; | ||
export default class Form { | ||
private simpleValidators; | ||
private keyedValidators; | ||
registerField(uid: number, rules: Rule[]): FormField; | ||
private reactiveFormFields; | ||
private trySetKeyed; | ||
private tryGetSimple; | ||
private tryGetKeyed; | ||
registerField(uid: number, rules: Rule[], modelValue?: unknown): FormField; | ||
getErrors(): import("vue").ComputedRef<string[]>; | ||
resetFields(): void; | ||
validateAll(): Promise<boolean>; | ||
validate(uid: number): Promise<PromiseSettledResult<string | void>[]>; | ||
validate(uid: number): Promise<PromiseSettledResult<ValidateResult>[]>; | ||
onDelete(uid: number): void; | ||
private getValidatorsFor; | ||
private getValidateResultsFor; | ||
private isEveryFormFieldTouchedWith; | ||
private makeValidate; | ||
} | ||
export {}; |
@@ -0,1 +1,2 @@ | ||
import { computed, reactive, unref } from 'vue'; | ||
import FormField from './FormField'; | ||
@@ -8,23 +9,25 @@ import { tryGet, trySet } from './utils'; | ||
this.keyedValidators = new Map(); | ||
this.reactiveFormFields = reactive(new Map()); | ||
this.trySetKeyed = trySet(this.keyedValidators); | ||
this.tryGetSimple = tryGet(this.simpleValidators); | ||
this.tryGetKeyed = tryGet(this.keyedValidators); | ||
} | ||
registerField(uid, rules) { | ||
const formField = new FormField(rules); | ||
const simple = rules.reduce((acc, rule, index) => { | ||
registerField(uid, rules, modelValue) { | ||
const formField = new FormField(rules, modelValue); | ||
const simple = rules.reduce((simple, rule, index) => { | ||
const validate = this.makeValidate(formField, rule, index); | ||
if (isSimpleRule(rule)) { | ||
acc.validators.push(validate); | ||
simple.validators.push(validate); | ||
} | ||
else { | ||
const entry = { formField, validator: validate }; | ||
acc.keys.push(rule.key); | ||
trySet(this.keyedValidators)({ | ||
failure: validators => { | ||
validators.add(entry); | ||
} | ||
simple.keys.push(rule.key); | ||
this.trySetKeyed({ | ||
failure: keyed => keyed.add(entry) | ||
})(rule.key, new Set([entry])); | ||
acc.rollback.push(() => { | ||
tryGet(this.keyedValidators)({ | ||
success: validators => { | ||
validators.delete(entry); | ||
if (!validators.size) { | ||
simple.rollback.push(() => { | ||
this.tryGetKeyed({ | ||
success: keyed => { | ||
keyed.delete(entry); | ||
if (!keyed.size) { | ||
this.keyedValidators.delete(rule.key); | ||
@@ -36,3 +39,3 @@ } | ||
} | ||
return acc; | ||
return simple; | ||
}, { | ||
@@ -45,4 +48,19 @@ formField, | ||
this.simpleValidators.set(uid, simple); | ||
this.reactiveFormFields.set(uid, formField); | ||
return formField; | ||
} | ||
getErrors() { | ||
return computed(() => { | ||
const errors = []; | ||
for (const formField of this.reactiveFormFields.values()) { | ||
errors.push(...formField.getErrors().value); | ||
} | ||
return errors; | ||
}); | ||
} | ||
resetFields() { | ||
for (const { formField } of this.simpleValidators.values()) { | ||
formField.reset(); | ||
} | ||
} | ||
async validateAll() { | ||
@@ -56,6 +74,6 @@ for (const { formField } of this.simpleValidators.values()) { | ||
}); | ||
const errors = await Promise.all(promises); | ||
for (const promiseResults of errors) { | ||
for (const result of promiseResults) { | ||
if (result.status === 'rejected') { | ||
const allSettledResults = await Promise.all(promises); | ||
for (const settledResults of allSettledResults) { | ||
for (const settledResult of settledResults) { | ||
if (settledResult.status === 'rejected') { | ||
return true; | ||
@@ -69,3 +87,3 @@ } | ||
let promise = Promise.allSettled([]); | ||
tryGet(this.simpleValidators)({ | ||
this.tryGetSimple({ | ||
success: ({ formField, keys, validators }) => { | ||
@@ -75,3 +93,3 @@ if (formField.touched) { | ||
...validators.map(v => v()), | ||
...this.getValidatorsFor(keys).map(v => v()) | ||
...this.getValidateResultsFor(keys) | ||
]); | ||
@@ -84,3 +102,3 @@ } | ||
onDelete(uid) { | ||
tryGet(this.simpleValidators)({ | ||
this.tryGetSimple({ | ||
success({ rollback }) { | ||
@@ -91,15 +109,10 @@ rollback.forEach(r => r()); | ||
this.simpleValidators.delete(uid); | ||
this.reactiveFormFields.delete(uid); | ||
} | ||
getValidatorsFor(keys) { | ||
getValidateResultsFor(keys) { | ||
return keys.reduce((promises, key) => { | ||
tryGet(this.keyedValidators)({ | ||
success(validators) { | ||
let everyFormFieldIsTouched = true; | ||
validators.forEach(({ formField }) => { | ||
if (!formField.touched) { | ||
everyFormFieldIsTouched = false; | ||
} | ||
}); | ||
if (everyFormFieldIsTouched) { | ||
promises.push(...[...validators.values()].map(({ validator }) => validator)); | ||
this.tryGetKeyed({ | ||
success: keyed => { | ||
if (this.isEveryFormFieldTouchedWith(key)) { | ||
promises.push(...[...keyed.values()].map(({ validator }) => validator())); | ||
} | ||
@@ -111,2 +124,16 @@ } | ||
} | ||
isEveryFormFieldTouchedWith(key) { | ||
let everyFormFieldIsTouched = true; | ||
this.tryGetKeyed({ | ||
success: keyed => { | ||
for (const { formField } of keyed) { | ||
if (!formField.touched) { | ||
everyFormFieldIsTouched = false; | ||
break; | ||
} | ||
} | ||
} | ||
})(key); | ||
return everyFormFieldIsTouched; | ||
} | ||
makeValidate(formField, rule, index) { | ||
@@ -117,3 +144,3 @@ const validator = formField => (index, rule) => async () => { | ||
formField.incrementWaiting(index); | ||
error = await rule(formField.modelValue); | ||
error = await rule(unref(formField.modelValue)); | ||
} | ||
@@ -126,3 +153,3 @@ catch (err) { | ||
} | ||
if (formField.nooneIsWaiting(index)) { | ||
if (formField.nooneIsWaiting(index) && formField.touched) { | ||
if (typeof error === 'string') { | ||
@@ -129,0 +156,0 @@ formField.setError(index, error); |
@@ -0,1 +1,2 @@ | ||
import { reactive, ref } from 'vue'; | ||
import { Rule } from './composable/useValidation'; | ||
@@ -7,5 +8,6 @@ export default class FormField { | ||
private errorCount; | ||
modelValue: unknown; | ||
modelValue: ReturnType<typeof ref> | ReturnType<typeof reactive>; | ||
private initialModelValue; | ||
touched: boolean; | ||
constructor(rules: Rule[]); | ||
constructor(rules: Rule[], modelValue: unknown); | ||
setError(index: number, error: string | null): void; | ||
@@ -18,2 +20,3 @@ incrementWaiting(index: number): void; | ||
validating(): import("vue").ComputedRef<boolean>; | ||
reset(): void; | ||
} |
@@ -1,13 +0,25 @@ | ||
import { computed, ref } from 'vue'; | ||
import { computed, isRef, reactive, ref } from 'vue'; | ||
const notNull = (value) => value !== null; | ||
export default class FormField { | ||
constructor(rules) { | ||
constructor(rules, modelValue) { | ||
this.totalWaiting = ref(0); | ||
this.errorCount = 0; | ||
this.touched = false; | ||
this.errors = ref(rules.map(() => null)); | ||
this.errors = reactive(rules.map(() => null)); | ||
this.waiting = rules.map(() => 0); | ||
if (isRef(modelValue)) { | ||
this.modelValue = modelValue; | ||
this.initialModelValue = modelValue.value; | ||
} | ||
else if (typeof modelValue === 'object' && modelValue !== null) { | ||
this.modelValue = reactive(modelValue); | ||
this.initialModelValue = JSON.parse(JSON.stringify(this.modelValue)); | ||
} | ||
else { | ||
this.modelValue = ref(modelValue); | ||
this.initialModelValue = modelValue; | ||
} | ||
} | ||
setError(index, error) { | ||
const willBeSet = this.errors.value[index]; | ||
const willBeSet = this.errors[index]; | ||
if (willBeSet === null && typeof error === 'string') { | ||
@@ -19,3 +31,3 @@ this.errorCount++; | ||
} | ||
this.errors.value[index] = error; | ||
this.errors[index] = error; | ||
} | ||
@@ -37,3 +49,3 @@ incrementWaiting(index) { | ||
getErrors() { | ||
return computed(() => this.errors.value.filter(notNull)); | ||
return computed(() => this.errors.filter(notNull)); | ||
} | ||
@@ -43,2 +55,14 @@ validating() { | ||
} | ||
reset() { | ||
this.touched = false; | ||
if (isRef(this.modelValue)) { | ||
this.modelValue.value = this.initialModelValue; | ||
} | ||
else { | ||
Object.assign(this.modelValue, this.initialModelValue); | ||
this.initialModelValue = JSON.parse(JSON.stringify(this.initialModelValue)); | ||
} | ||
Object.assign(this.errors, this.errors.map(() => null)); | ||
this.errorCount = 0; | ||
} | ||
} |
{ | ||
"name": "vue3-form-validation", | ||
"version": "2.1.2", | ||
"description": "Form validation for Vue 3", | ||
"version": "3.0.0", | ||
"description": "Vue composition function for Form Validation", | ||
"author": { | ||
@@ -11,3 +11,3 @@ "name": "jens", | ||
"type": "git", | ||
"url": "https://github.com/JensD98/vue3-form-validation.git" | ||
"url": "https://github.com/JensDll/vue3-form-validation#readme" | ||
}, | ||
@@ -25,29 +25,42 @@ "main": "dist/index.js", | ||
"form", | ||
"validation" | ||
"validation", | ||
"utils", | ||
"vue-use" | ||
], | ||
"scripts": { | ||
"build": "npx tsc -p tsconfig.main.json", | ||
"build": "npx eslint --rule=\"no-console:error\" --fix-dry-run main && npx tsc --project ./main", | ||
"lint": "npx prettier --write .", | ||
"test": "jest", | ||
"dev": "vite" | ||
"test": "jest --config ./main/jest.config.ts", | ||
"test-dts": "npx tsd", | ||
"test-all": "npm run build && npm run test && npm run test-dts", | ||
"dev": "vite", | ||
"_postinstall": "husky install", | ||
"prepublishOnly": "pinst --disable", | ||
"postpublish": "pinst --enable" | ||
}, | ||
"devDependencies": { | ||
"@types/jest": "^26.0.15", | ||
"@types/node": "^14.14.10", | ||
"@typescript-eslint/eslint-plugin": "^4.8.2", | ||
"@typescript-eslint/parser": "^4.8.2", | ||
"@vue/compiler-sfc": "^3.0.3", | ||
"eslint": "^7.14.0", | ||
"eslint-config-prettier": "^6.15.0", | ||
"eslint-plugin-vue": "^7.1.0", | ||
"husky": "^4.3.0", | ||
"@types/jest": "^26.0.20", | ||
"@types/node": "^14.14.27", | ||
"@typescript-eslint/eslint-plugin": "^4.15.0", | ||
"@typescript-eslint/parser": "^4.15.0", | ||
"@vitejs/plugin-vue": "^1.0.6", | ||
"@vue/compiler-sfc": "^3.0.5", | ||
"autoprefixer": "^10.2.4", | ||
"eslint": "^7.18.0", | ||
"eslint-config-prettier": "^7.2.0", | ||
"eslint-plugin-vue": "^7.4.1", | ||
"husky": "^5.0.9", | ||
"jest": "^26.6.3", | ||
"lint-staged": "^10.5.2", | ||
"lint-staged": "^10.5.3", | ||
"pinst": "^2.1.4", | ||
"postcss": "^8.2.6", | ||
"prettier": "^2.2.1", | ||
"tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.0.1", | ||
"ts-jest": "^26.4.4", | ||
"typescript": "^4.1.2", | ||
"vite": "^1.0.0-rc.13", | ||
"vue": "^3.0.3", | ||
"vue-router": "^4.0.0-rc.5" | ||
"tailwindcss": "^2.0.3", | ||
"ts-jest": "^26.5.1", | ||
"ts-node": "^9.1.1", | ||
"tsd": "^0.14.0", | ||
"typescript": "^4.1.5", | ||
"vite": "^2.0.0-beta.69", | ||
"vue": "^3.0.5", | ||
"vue-router": "^4.0.3" | ||
}, | ||
@@ -60,7 +73,5 @@ "prettier": { | ||
}, | ||
"husky": { | ||
"hooks": { | ||
"pre-commit": "npx eslint --rule=\"no-console:error\" --fix-dry-run main" | ||
} | ||
"tsd": { | ||
"directory": "test-dts" | ||
} | ||
} |
156
README.md
@@ -1,31 +0,44 @@ | ||
# Form validation for Vue 3 | ||
Easy to use opinionated Form validation for Vue 3. | ||
# Form Validation for Vue 3 | ||
* :milky_way: **Written in TypeScript** | ||
* :ocean: **Dynamic Form support** | ||
* :fallen_leaf: **Light weight** | ||
[![npm](https://img.shields.io/npm/v/vue3-form-validation)](https://www.npmjs.com/package/vue3-form-validation) | ||
Opinionated Vue composition function for Form Validation. | ||
- :milky_way: **Written in TypeScript** | ||
- :ocean: **Dynamic Form support** | ||
- :fallen_leaf: **Light weight** | ||
```bash | ||
npm i vue3-form-validation | ||
npm install vue3-form-validation | ||
``` | ||
Validation is async and is utilising `Promise.allSettled`, [which](https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled) has not yet reached cross-browser stability. Example usage can be found in this [Code Sandbox](https://codesandbox.io/s/vue-3-form-validation-demo-busd9?file=/src/LoginForm.vue). | ||
Validation is async and is utilising `Promise.allSettled`, [which](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled) has not yet reached cross-browser stability. Example usage can be found in this [Code Sandbox](https://codesandbox.io/s/vue-3-form-validation-demo-busd9?file=/src/LoginForm.vue). | ||
## API | ||
This package exports one function `useValidation`, plus some type definitions for when using TypeScript. | ||
### useValidation | ||
```ts | ||
const { form, add, remove, onSubmit } = useValidation<T>(formData) | ||
const { | ||
form, | ||
errors, | ||
submitting, | ||
validateFields, | ||
resetFields, | ||
add, | ||
remove | ||
} = useValidation<T>(formData); | ||
``` | ||
* `useValidation` takes the following parameters: | ||
### `useValidation` takes the following parameters: | ||
Parameters | Type | Required | Description | ||
---|:-:|:-:|--- | ||
formData | `object` | `true` | The structure of your Form data. | ||
- `formData` | ||
- **Type** - `object` | ||
- **Required** - `true` | ||
- **Description** - The structure of your `formData`. | ||
The `formData` object has a structure that is similar to any other object you would write for `v-model` data binding. The only difference being that together with every value you can provide rules to display validation errors. | ||
Let's look at an example how the structure of some `formData` object, can be converted to an object with the addition of rules: | ||
Let's look at an example how the structure of some `formData` object can be converted to an object with the addition of rules: | ||
```ts | ||
@@ -36,3 +49,3 @@ const formData = { | ||
password: '' | ||
} | ||
}; | ||
@@ -51,8 +64,10 @@ // The above can be converted to the following | ||
$value: '', | ||
$rules: [pw => pw.length > 7 || 'Password has to be longer than 7 characters'] | ||
$rules: [ | ||
pw => pw.length > 7 || 'Password has to be longer than 7 characters' | ||
] | ||
} | ||
} | ||
}; | ||
``` | ||
The `formData` object can contain arrays and can be deeply nested. At the leaf level, the object should contain Form Fields whose type definition looks like the following: | ||
The `formData` object can contain arrays and can be deeply nested. At the leaf level, the object should contain Form Fields whose simplified type definition looks like the following: | ||
@@ -66,3 +81,3 @@ ```ts | ||
To get the best IntelliSense while writing the `useValidation` function, it's recommended to define the structure of your `formData` upfront and pass it as the generic parameter `T`. The type for the example above is pretty straightforward: | ||
To get better type inference while writing the `useValidation` function, it's recommended to define the structure of your `formData` upfront and pass it as the generic parameter `T`. The type for the example above is pretty straightforward: | ||
@@ -74,15 +89,19 @@ ```ts | ||
password: Field<string>; | ||
} | ||
}; | ||
``` | ||
* `useValidation` exposes the following state: | ||
### `useValidation` exposes the following state: | ||
State | Type | Description | ||
---|:-:|--- | ||
form | `object` | Transformed `formData` object with added metadata to every Form Field. | ||
- `form` | ||
- **Type** - `object` | ||
- **Description** - Transformed `formData` object. | ||
- `submitting` | ||
- **Type** - `Ref<boolean>` | ||
- **Description** - `True` during validation after calling `validateFields`. | ||
- `errors` | ||
- **Type** - `ComputedRef<string[]>` | ||
- **Description** - Array of all current validation error messages. | ||
`Form` is a reactive object with identical structure as the `formData` input, but with added metadata to every Form Field. | ||
**Typing:** | ||
```ts | ||
@@ -102,27 +121,43 @@ type TransformedField<T> = { | ||
password: TransformedField<string>; | ||
} | ||
}; | ||
``` | ||
As you may have noticed, all of the properties are prefixed with the `$` symbol, which is to distinguish them from other properties but also to avoid naming conflicts. | ||
Key | Value | Description | ||
---|:-:|--- | ||
uid | `number` | Unique identifier of the Form Field. For dynamic Forms this can be used as the `key` attribute in `v-for`. | ||
value | `T` | The `modelValue` of the Form Field which is meant to be used together with `v-model`. | ||
errors | `string[]` | Array of validation error messages. | ||
validating | `boolean` | `True` while atleast one rule is validating. | ||
onBlur | `function` | Function which will mark this Form Field as touched. When a Form Field has been touched it will validate all it's rules after every input. Before it will not do any validation. | ||
* `useValidation` exposes the following methods: | ||
- `$uid` | ||
- **Type** - `number` | ||
- **Description** - Unique identifier of the Form Field. For dynamic Forms this can be used as the `key` attribute in `v-for`. | ||
- `$value` | ||
- **Type** - `T` | ||
- **Description** - The `modelValue` of the Form Field which is meant to be used together with `v-model`. | ||
- `$errors` | ||
- **Type** - `string[]` | ||
- **Description** - Array of validation error messages. | ||
- `$validating` | ||
- **Type** - `boolean` | ||
- **Description** - `True` while at least one rule is validating. | ||
- `$onBlur` | ||
- **Type** - `function` | ||
- **Description** - Function which will mark this Form Field as touched. When a Form Field has been touched it will validate all it's rules after every input. Before it will not do any validation. | ||
Signature | Parameters | Description | ||
--- | :-: | --- | ||
`onSubmit(success, error?)` | | Function that will validate all Form Fields. It takes two parameters, a `success` callback and an optional `error` callback. | ||
|| `success` | Will be called if there are no validation errors. Receives the `formData` as it's first argument. | ||
|| `error?` | Will be called if there are validation errors. Receives no arguments. | ||
`add(pathToArray, value)` || Utility function for writing dynamic Forms. It takes two parameters, a `pathToArray` of type `(string \| number)[]` and a `value`. | ||
|| `pathToArray` | Tupel of `string` and `numbers` representing the path to an array in the `formData`. | ||
|| `value` | The `value` that will be pushed to the array at the given path. | ||
`remove(pathToArray, index)` || Identical to `add` but instead of providing a `value` you provide an `index` that will be removed. | ||
### `useValidation` exposes the following methods: | ||
- `validateFields() -> Promise` | ||
- **Description** - Validate all Form Fields. | ||
- **Returns** - A `Promise` which will reject if there are validation errors, and resolve with the `formData` otherwise. | ||
- `resetFields() -> void` | ||
- **Description** - Reset all Form Fields to their original values. | ||
- `add(pathToArray: (string | number)[], value: any) -> void` | ||
- **Description** - Utility function for writing dynamic Forms. | ||
- **Parameters** | ||
- `pathToArray` - Tuple representing the path to an array in the `formData`. | ||
- `value` - The value that will be pushed to the array at the given path. | ||
- `remove(pathToArray: (string | number)[], index: number) -> void` | ||
- **Description** - Utility function for writing dynamic Forms. | ||
- **Parameters** | ||
- `pathToArray` - Tuple representing the path to an array in the `formData`. | ||
- `index` - Array index that will be remove. | ||
## Writing Rules | ||
Rules are functions that should return a `string` when the validation fails. They can be written purely as a function or together with a `key` property in an object. | ||
@@ -132,2 +167,3 @@ They can also alternatively return a `Promise` when you have a rule that requires asynchronous code. | ||
**Typing:** | ||
```ts | ||
@@ -139,27 +175,17 @@ type SimpleRule<T = any> = (value: T) => Promise<unknown> | unknown; | ||
Keyed rules that share the same `key` will be executed together, this can be useful in a situation where rules are dependent on another. For example the `Password` and `Repeat password` fields in a Login Form. | ||
Keyed rules that share the same `key` will be executed together, this can be useful in a situation where rules are dependent on another. For example the `Password` and `Repeat Password` fields in a Login Form. | ||
Rules will always be called with the latest `modelValue`, to determine if a call should result in an error, it will check if the rule's return value is of type `string`. | ||
This allows you to write many rules in one line: | ||
`main/Form.ts` | ||
```ts | ||
// Somewhere at the bottom of the file | ||
let error: unknown; | ||
// ... | ||
error = await rule(formField.modelValue); | ||
// ... | ||
if (typeof error === 'string') { | ||
// report validation error | ||
} | ||
// ... | ||
const required = value => !value && 'This field is required'; | ||
const min = value => | ||
value.length > 3 || 'This field has to be longer than 3 characters'; | ||
const max = value => | ||
value.length < 7 || 'This field is too long (maximum is 6 characters)'; | ||
``` | ||
This allows you to write many rules in one line: | ||
Async rules allow you to perform network requests, for example checking if a username exists in the database: | ||
```ts | ||
const required = value => !value && 'This field is required'; | ||
const min = value => value.length > 3 || 'This field has to be longer than 3 characters'; | ||
const max = value => value.length < 7 || 'This field is too long (maximum is 6 characters)'; | ||
``` | ||
Async rules allow you to perform network requests, for example checking if a username already exists in the database: | ||
```ts | ||
const isNameTaken = name => | ||
@@ -174,9 +200,11 @@ new Promise(resolve => { | ||
}, 2000); | ||
}) | ||
}); | ||
``` | ||
## Contributing | ||
[Contributing](https://github.com/JensD98/vue3-form-validation/blob/master/.github/contributing.md) | ||
## License | ||
[MIT](https://github.com/JensD98/vue3-form-validation/blob/master/LICENSE) |
Sorry, the diff of this file is not supported yet
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
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
29239
523
203
24