@ahrowe/ui 0.1.16 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +4 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +791 -748
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +1 -1
- package/dist/types/package/common/chip/chip.d.ts +1 -1
- package/dist/types/package/common/chip/chip.types.d.ts +8 -0
- package/dist/types/package/common/virtualList/useColumns.d.ts +7 -1
- package/dist/types/package/common/virtualList/useVirtualWindow.d.ts +5 -2
- package/dist/types/package/common/virtualList/virtualList.types.d.ts +6 -0
- package/docs/CLAUDE.md +19 -26
- package/docs/FormValidator.md +146 -0
- package/docs/FormValidatorGroup.md +154 -0
- package/package.json +1 -1
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# FormValidatorGroup
|
|
2
|
+
|
|
3
|
+
**When to use:** Manage a whole form — multiple fields, a single submit handler, validate-on-submit, dirty/touched tracking across the form, and mapping server-side errors back onto the right fields. Build it from a map of named `FormValidator` instances. For a single field, use `FormValidator` on its own.
|
|
4
|
+
|
|
5
|
+
**Import:** `import { useFormValidatorGroup, FormValidatorGroup, FormValidator, Validators } from '@ahrowe/ui'`
|
|
6
|
+
**Types:** `import type { FormValidatorGroupOptions, InferGroupValues, MergedValues } from '@ahrowe/ui'`
|
|
7
|
+
|
|
8
|
+
## Recommended: the `useFormValidatorGroup` hook
|
|
9
|
+
|
|
10
|
+
In function components, create the group with the hook. It instantiates the group once on mount, keeps `onSubmit`/`onValidate` fresh via refs, and re-renders the component whenever any field, the submit state, or validation state changes.
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { useFormValidatorGroup, FormValidator, Validators, Input, InputType, ActionButtons } from '@ahrowe/ui';
|
|
14
|
+
|
|
15
|
+
function ProfileForm({ user, onSaved }) {
|
|
16
|
+
const form = useFormValidatorGroup(
|
|
17
|
+
{
|
|
18
|
+
// Each FormValidator's first arg is the DEFAULT (empty) value, not the user's value
|
|
19
|
+
name: new FormValidator('', [Validators.required(), Validators.minLength(2)]),
|
|
20
|
+
email: new FormValidator('', [Validators.required(), Validators.email()]),
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
// initialValues OVERRIDES those defaults with the existing record you're editing
|
|
24
|
+
initialValues: { name: user.name, email: user.email },
|
|
25
|
+
onSubmit: async (values) => {
|
|
26
|
+
// `values` is fully typed: { name: string; email: string }
|
|
27
|
+
await api.updateProfile(values);
|
|
28
|
+
onSaved();
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// `form.Form` wires up onSubmit + noValidate for you
|
|
34
|
+
return (
|
|
35
|
+
<form.Form>
|
|
36
|
+
<Input label="Name" formValidator={form.group.name} />
|
|
37
|
+
<Input label="Email" type={InputType.Email} formValidator={form.group.email} />
|
|
38
|
+
<ActionButtons
|
|
39
|
+
submitLabel="Save"
|
|
40
|
+
onSubmit={form.submit}
|
|
41
|
+
isSubmitButtonLoading={form.isSubmitting}
|
|
42
|
+
isSubmitButtonDisabled={!form.isDirty}
|
|
43
|
+
/>
|
|
44
|
+
</form.Form>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
> The exported `FormValidatorGroup` is the form-aware subclass: every instance has a `Form` component (`form.Form`) that renders `<form onSubmit={form.submit} noValidate>`. You can also call `form.submit(event)` directly from a button.
|
|
50
|
+
|
|
51
|
+
> **Defaults vs. `initialValues` — the editing pattern.** Each `FormValidator`'s first argument is the field's _empty default_ (`''`, `null`, `false`, …). When you load an existing entity to **edit** it (e.g. an existing user), put those current values in `initialValues` — not in the validator constructors. The group seeds each field from `initialValues` on creation, and that same set becomes the baseline `reset()` restores to. Keep the constructors as empty defaults so a "create new" form (no `initialValues`) and an "edit" form share the exact same field definitions.
|
|
52
|
+
|
|
53
|
+
```tsx
|
|
54
|
+
// Create form — no initialValues, fields stay at their empty defaults
|
|
55
|
+
const createForm = useFormValidatorGroup(fields, { onSubmit: create });
|
|
56
|
+
|
|
57
|
+
// Edit form — same `fields`, current record passed as initialValues
|
|
58
|
+
const editForm = useFormValidatorGroup(fields, { initialValues: user, onSubmit: update });
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Submit flow
|
|
62
|
+
|
|
63
|
+
`submit(event?)` does the following:
|
|
64
|
+
|
|
65
|
+
1. `preventDefault` / `stopPropagation` on the event if present.
|
|
66
|
+
2. Sets `isSubmitting` true.
|
|
67
|
+
3. On the **first** submit, runs `validate()` (validates every field + marks them touched); on later submits, runs `onValidate` only.
|
|
68
|
+
4. If any field error or a custom validation error exists → calls `onSubmitError` with it, increments `submitCount`, and stops (does **not** call `onSubmit`).
|
|
69
|
+
5. Otherwise calls `onSubmit(values, group)`. If it throws, the error is passed to `parseError(err, true)` to surface field-level or generic errors.
|
|
70
|
+
6. Sets `isSubmitting` false.
|
|
71
|
+
|
|
72
|
+
By default `onSubmitError` shows the first error as an error **toast** (requires a `ToastProvider` in the tree — see `Toast`). Override it via `options.onSubmitError`.
|
|
73
|
+
|
|
74
|
+
## Options
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
interface FormValidatorGroupOptions {
|
|
78
|
+
initialValues?: Record<string, unknown>; // OVERRIDES the validators' empty defaults with an
|
|
79
|
+
// existing record's values (use when editing); also
|
|
80
|
+
// becomes the baseline reset() restores to
|
|
81
|
+
onSubmit?: (values, group) => void | Promise<void>;
|
|
82
|
+
onValidate?: (group) => ValidationError | null | void; // extra cross-field validation
|
|
83
|
+
errorParser?: (err: unknown) => ValidationError[]; // turn an API error into field errors
|
|
84
|
+
onSubmitError?: (errors: ValidationError[]) => void; // default: error toast of the first message
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`onValidate` runs as part of `submit` — return a `{ id, message }` to block submission with a form-level error (e.g. "passwords must match"):
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
useFormValidatorGroup(
|
|
92
|
+
{ password: new FormValidator(''), confirm: new FormValidator('') },
|
|
93
|
+
{
|
|
94
|
+
onValidate: (group) =>
|
|
95
|
+
group.group.password.value === group.group.confirm.value
|
|
96
|
+
? null
|
|
97
|
+
: { id: 'mismatch', message: 'Passwords do not match' },
|
|
98
|
+
onSubmit: async (values) => api.setPassword(values),
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Instance API
|
|
104
|
+
|
|
105
|
+
| Member | Signature | Description |
|
|
106
|
+
|--------|-----------|-------------|
|
|
107
|
+
| `group` | `Record<name, FormValidator>` | The field validators. Access a field via `form.group.<name>`. |
|
|
108
|
+
| `Form` | `React.FC<FormHTMLAttributes>` | `<form>` pre-wired with `onSubmit={submit}` + `noValidate`. |
|
|
109
|
+
| `submit` | `(event?) => Promise<void>` | Validate then run `onSubmit`. Safe to pass straight to a `<form onSubmit>` or button. |
|
|
110
|
+
| `validate` | `() => ValidationError \| null \| void` | Validate every field, mark all touched, run `onValidate`. |
|
|
111
|
+
| `getValues` | `() => MergedValues` | Typed snapshot: `initialValues` merged with every field's current value. |
|
|
112
|
+
| `set` | `(name, value) => this` | Set one field's value (validates it). Chainable. |
|
|
113
|
+
| `setValidator` | `(name, validator) => this` | Swap a field's `FormValidator` instance. |
|
|
114
|
+
| `reset` | `(overrides?) => void` | Restore fields to their post-creation values (or `overrides`), clear dirty/touched, fire `onReset`. |
|
|
115
|
+
| `resetDirtyAndTouched` | `() => void` | Clear dirty/touched on all fields without changing values. |
|
|
116
|
+
| `parseError` | `(err, isInternal?) => void` | Run `errorParser` and push field errors onto matching validators; otherwise a generic error. |
|
|
117
|
+
| `getFormError` | `() => ValidationError \| null` | First error across all fields, or `null`. |
|
|
118
|
+
| `isSubmitting` | `boolean` (get) | True while `submit` is running. |
|
|
119
|
+
| `isDirty` | `boolean` (get) | Any field changed since creation. |
|
|
120
|
+
| `wasTouched` | `boolean` (get) | Any field entered and left. |
|
|
121
|
+
| `hasErrors` | `boolean` (get) | Any field currently has an error. |
|
|
122
|
+
| `initialValues` | `object` (get) | Copy of the initial values. |
|
|
123
|
+
| `submitCount` | `number` | How many times `submit` has run (reset to 0 on any field change). |
|
|
124
|
+
| `onDebounce` | `(values) => void` | Assignable callback fired 500ms after the last field change. |
|
|
125
|
+
| `onReset` | `(group, valuesAfterCreation, initialValues) => void` | Assignable, fired by `reset()`. |
|
|
126
|
+
|
|
127
|
+
## Mapping API errors back to fields
|
|
128
|
+
|
|
129
|
+
If your API returns errors shaped like `{ errors: [{ <fieldName>: '<message>' }] }`, the default `errorParser` routes each message to the matching field. A thrown `onSubmit` error is parsed automatically; you can also call it manually:
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
try {
|
|
133
|
+
await api.save(form.getValues());
|
|
134
|
+
} catch (err) {
|
|
135
|
+
form.parseError(err); // pushes e.g. an "email already taken" error onto form.group.email
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Pass a custom `errorParser` in the options when your API uses a different shape.
|
|
140
|
+
|
|
141
|
+
## Class components
|
|
142
|
+
|
|
143
|
+
For legacy class components, instantiate directly and pass the component instance so the group can call `forceUpdate()`:
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
this.form = new FormValidatorGroup(
|
|
147
|
+
{ name: new FormValidator('', [Validators.required()]) },
|
|
148
|
+
this, // component instance — enables re-render via forceUpdate
|
|
149
|
+
{ onSubmit: (values) => this.save(values) },
|
|
150
|
+
);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`ValidatableComponent` (also exported) is the base class the built-in form components extend — subclass it when building your own validator-aware input.
|
|
154
|
+
</content>
|