@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.
@@ -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>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ahrowe/ui",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },