gib-validate
Version:
gib-validate is a lightweight validation helper for Vue 3 (Composition API). It provides a set of common validation rules and a `useValidation` hook to validate reactive form state.
270 lines (196 loc) • 9.2 kB
Markdown
# gib-validate
gib-validate is a lightweight validation helper for Vue 3 (Composition API). It provides a set of common validation rules and a `useValidation` hook to validate reactive form state.
### Quick summary
- Peer dependency: Vue 3
- Main exports: `useValidation`, a collection of rules (for example `required`, `minLength`, `isEmail`) and types (`ValidationState`, `ValidationRule`, `ValidationRules`, `Errors`).
### Installation
```bash
npm install gib-validate
```
### Basic usage (Vue 3, Composition API)
```ts
import { reactive } from 'vue';
import { useValidation, required, minLength, isEmail } from 'gib-validate';
const form = reactive({ email: '', password: '' });
const rules = {
email: [required('Email is required'), isEmail('Invalid email')],
password: [required(), minLength(8, 'Minimum 8 characters')]
};
// useValidation returns ComputedRef<ValidationState<T>>
const v = useValidation(form, rules);
// In script: access via v.value (v is a ComputedRef)
// v.value.$errors, v.value.$touch(), v.value.$reset(), etc.
```
```html
<template>
<input v-model="form.email" />
<div v-if="v.$errors.email">{{ v.$errors.email[0] }}</div>
<input v-model="form.password" type="password" />
<div v-if="v.$errors.password">{{ v.$errors.password[0] }}</div>
<button @click="v.$touch()">Submit</button>
</template>
```
### API overview
- useValidation:
- Returns a computed validation state exposing:
- `$touch()` — mark form as touched (marks all fields and child validators)
- `$reset()` — reset dirty/touched state
- `$touchField(field | fields)` — mark one or more fields as touched
- `$dirty: boolean` — whether the form was attempted/submitted
- `$errors: Errors<T>` — visible errors (only for touched fields)
- `$silentErrors: Errors<T>` — all validation errors regardless of touched state
- `$children` — support for nested validators (via provide/inject)
### Dynamic rules
The `useValidation` function now supports dynamic rules through computed properties. This allows you to change validation rules based on reactive conditions:
```ts
import { reactive, ref, computed } from 'vue';
import { useValidation, required, minLength } from 'gib-validate';
const form = reactive({ password: '' });
const requirePassword = ref(false);
// Dynamic rules based on reactive conditions
const rules = computed(() => ({
password: requirePassword.value
? [required('Password is required'), minLength(8, 'Minimum 8 characters')]
: [minLength(8, 'Minimum 8 characters')]
}));
const v = useValidation(form, rules);
// Later, when you change requirePassword, the validation rules will automatically update
requirePassword.value = true;
```
- useNamedValidation:
- Returns a reactive ref to a named validation:
- Allows reactive access to named validations
- Updates automatically when the named validation changes
- Returns undefined if no validation is registered with that name
- Validation rules: a `ValidationRule<T>` returns `true` (success) or a string (error message). Rules can be synchronous or asynchronous (return Promise<string|boolean>).
Types (short):
- `ValidationRule<T>` = (value: T) => string | boolean | Promise<string | boolean>
- `ValidationRules<T>` = { [K in keyof T]?: ValidationRule<T[K]>[] }
- `Errors<T>` = Partial<Record<keyof T, string[]>>
### Built-in rules
The package exports a set of common rules from `src/rules`:
- `required(error?)`
- `requiredIf(...)`
- `oneOf(...)`
- `isEmail(...)`
- `isUrl(...)`
- `isPhone(...)`
- `isNumeric(...)`
- `isInteger(...)`
- `isDate(...)`
- `isPastDate(...)`, `isFutureDate(...)`
- `isPassword(...)`
- `fileType(...)`, `fileSizeLessThan(...)`
- `equalTo(...)`
- `contains(...)`
- `between(...)`
- `minLength(...)`, `maxLength(...)`
Each rule typically accepts parameters (for example, `minLength(8, 'Too short')`) and returns a function that will be invoked with the value to validate.
Example custom rules:
```ts
// sync rule
const startsWithA = (v: string) => (v.startsWith('A') ? true : 'Must start with A');
// async rule
const uniqueEmail = async (email: string) => {
const ok = await checkEmailOnServer(email); // your API call
return ok ? true : 'Email already taken';
};
```
### Behavior and notes
- The hook watches the provided `state` and runs validation rules automatically, updating `$silentErrors`.
- `$errors` contains only errors for fields that are touched (or after calling `$touch()`).
- Rules return `true` on success or a string with an error message on failure. Async rules are supported.
- Nested validators are supported: child validators register with a parent via provide/inject.
### Nested validation
gib-validate supports nested validators across component boundaries. When a child component calls `useValidation`, it will automatically register its validation state with a parent validator (if a parent is present) using provide/inject. This allows the parent to trigger child validation (`$touch`) and for child validators to be reset together with the parent.
Key points:
- Child validators register automatically; you don't need to pass the parent explicitly.
- Parent's `$touch()` will call `$touch()` on registered children. Parent's `$reset()` will reset children as well.
- Registered children appear under the parent's `$children` ref (keys are assigned internally, in insertion order).
Simple example (Parent + Child component):
Parent component (script setup):
```ts
import { reactive } from 'vue';
import { useValidation, required } from 'gib-validate';
import AddressField from './AddressField.vue';
const form = reactive({ name: '', address: { street: '', city: '' } });
const rules = {
name: [required('Name required')]
};
const v = useValidation(form, rules);
// trigger parent + child validation before submit
function onSubmit() {
v.value.$touch(); // will also touch children
// check parent errors and optionally inspect children via v.value.$children
}
```
Child component `AddressField.vue` (script setup):
```ts
import { toRef } from 'vue';
import { useValidation, required, minLength } from 'gib-validate';
// props: { address }
const street = toRef(props.address, 'street');
const city = toRef(props.address, 'city');
const rules = {
street: [required('Street is required')],
city: [required('City is required'), minLength(2, 'Too short')]
};
// useValidation in the child registers itself with the parent validator automatically
const v = useValidation(props.address, rules);
```
How to inspect child errors from parent:
```ts
// v is parent's computed validation state
// parent silent errors
console.log(v.value.$silentErrors);
// iterate registered children
for (const [key, childState] of Object.entries(v.value.$children.value)) {
console.log('child errors', key, childState.$errors);
}
```
Note: child validators are stored under `$children` as a `Ref<Record<string, ValidationState>>;` the keys are created internally and may change when children mount/unmount. Use `$children` for programmatic inspection only — prefer using high-level flows like `v.$touch()` on the parent which will propagate to children.
### Named validations
gib-validate also supports naming your validations to make them easier to access and manage. You can name both top-level validations and nested children validations.
#### Named top-level validations
```ts
import { reactive } from 'vue';
import { useValidation, required, useNamedValidation } from 'gib-validate';
const form = reactive({ name: '' });
const rules = {
name: [required('Name is required')]
};
// Register validation with a name
const v = useValidation(form, rules, 'userForm');
// Later, retrieve the validation by name from anywhere in your app
// useNamedValidation returns a reactive ref that updates when the validation changes
const userFormValidation = useNamedValidation('userForm');
userFormValidation.value?.$touch(); // Trigger validation
```
#### Named nested children validations
When creating nested validations, you can provide names for children to make them easier to identify:
```ts
// Parent component
import { reactive } from 'vue';
import { useValidation, required } from 'gib-validate';
const form = reactive({ name: '', address: { street: '', city: '' } });
const rules = {
name: [required('Name required')]
};
// Parent validation (without name)
const parentValidation = useValidation(form, rules);
// Child component
import { toRef } from 'vue';
import { useValidation, required, minLength } from 'gib-validate';
const address = toRef(props, 'address');
const rules = {
street: [required('Street is required')],
city: [required('City is required'), minLength(2, 'Too short')]
};
// Child validation with name - this will appear in parent.$children under the key 'addressForm'
const addressValidation = useValidation(address, rules, 'addressForm');
// Now you can access the child by name:
// parentValidation.value.$children.value['addressForm']
```
This makes it easier to work with nested validations as you can directly access children by meaningful names rather than numeric indices.
</file_content>
</file_content>