@zhunam/form-builder 2.0.0 → 3.1.0

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/README.md CHANGED
@@ -36,6 +36,95 @@ For a calculator-style form (no submit action, values used as you type):
36
36
 
37
37
  `formSubmit` stays available in `'live'` mode too; most `'live'` consumers just won't use it.
38
38
 
39
+ ### Presetting or replacing the form's value
40
+
41
+ The `value` input patches the form in place, without rebuilding it, so
42
+ whatever the user already typed in fields that stay unaffected is never
43
+ lost. Useful for a "swap" action or restoring a draft:
44
+
45
+ ```typescript
46
+ swapValue = signal<Partial<User> | undefined>(undefined);
47
+
48
+ onSwap(): void {
49
+ this.swapValue.set({ name: this.currentLastValue.name /* ... */ });
50
+ }
51
+ ```
52
+
53
+ ```html
54
+ <lib-form-builder [fields]="fields" [value]="swapValue()" mode="live" (valueChange)="onValueChange($event)" />
55
+ ```
56
+
57
+ Rebuilding `fields()` itself (e.g. a wizard changing steps) also keeps
58
+ the current value of every control whose key still exists in the new
59
+ array; only a control whose key disappears is dropped, and only a
60
+ brand-new key falls back to its own `defaultValue`.
61
+
62
+ ### Submitting from outside the component
63
+
64
+ ```html
65
+ <lib-form-builder #form [fields]="fields" [hideSubmit]="true" [loading]="saving()" (formSubmit)="onSave($event)" />
66
+ <button (click)="form.submit()">Continue</button>
67
+ ```
68
+
69
+ `submit()` runs the exact same logic the internal button would: it marks
70
+ the form submitted, validates, and emits `formSubmit` if valid. It's a
71
+ no-op while `loading` is `true`, the same guard that disables the
72
+ internal button.
73
+
74
+ ### Switch and segmented appearance
75
+
76
+ `appearance` changes only how a specific `type` looks, never the control underneath it: a real `<input type="checkbox">`/`<input type="radio">` is always there, visually hidden but focusable, clickable via its label, and fully native for validation, keyboard operation, and screen readers.
77
+
78
+ ```typescript
79
+ fields: FieldConfig<Settings>[] = [
80
+ { key: 'notifications', label: 'Email notifications', type: 'checkbox', appearance: 'switch' },
81
+ {
82
+ key: 'plan',
83
+ label: 'Plan',
84
+ type: 'radio',
85
+ appearance: 'segmented',
86
+ options: [
87
+ { value: 'personal', label: 'Personal' },
88
+ { value: 'business', label: 'Business' },
89
+ ],
90
+ },
91
+ ];
92
+ ```
93
+
94
+ `'switch'` only changes `type: 'checkbox'`; `'segmented'` only changes `type: 'radio'`. Setting `appearance` on any other `type` (or leaving it unset) has no effect at all, no error, no warning.
95
+
96
+ ### Field hints and option descriptions
97
+
98
+ `hint` shows short helper text below a field, only while there's no active error: an error replaces it visually, and `aria-describedby` follows the same rule, always pointing at whichever one is currently shown, never both.
99
+
100
+ ```typescript
101
+ { key: 'password', label: 'Password', type: 'password', hint: 'At least 8 characters', validators: { required: true, minLength: 8 } }
102
+ ```
103
+
104
+ `FieldOption.description` shows helper text below one specific `radio` option, independent of the field-level hint or error:
105
+
106
+ ```typescript
107
+ {
108
+ key: 'plan',
109
+ label: 'Plan',
110
+ type: 'radio',
111
+ options: [
112
+ { value: 'personal', label: 'Personal', description: 'For individual use' },
113
+ { value: 'business', label: 'Business', description: 'For teams and invoicing' },
114
+ ],
115
+ }
116
+ ```
117
+
118
+ ### Read-only fields
119
+
120
+ `readonly` keeps a field's value visible and its validation running exactly as before, only direct editing is blocked. Unlike `disabled`, the underlying `FormControl` stays enabled, so its value is never excluded from anything.
121
+
122
+ ```typescript
123
+ { key: 'referenceCode', label: 'Reference code', type: 'text', readonly: true, defaultValue: 'REF-2026-001' }
124
+ ```
125
+
126
+ Only applies to `text`, `number`, `email`, `password`, `date`, and `textarea`: the HTML `readonly` attribute doesn't apply to `select`, `radio`, or `checkbox` natively, and this library doesn't simulate it for those, setting `readonly` on one of them is simply ignored.
127
+
39
128
  ## API
40
129
 
41
130
  ### `FormBuilder<T>`
@@ -49,6 +138,11 @@ For a calculator-style form (no submit action, values used as you type):
49
138
  | `formSubmit` | `output<T>` | N/A | Emitted with the typed form values, only when the native form and every `crossFieldValidators` check pass. |
50
139
  | `mode` | `input<'submit' \| 'live'>` | `'submit'` | `'submit'`: only `formSubmit` fires, on submit. `'live'`: `valueChange` also fires continuously as the user edits, in addition to `formSubmit` staying available. |
51
140
  | `valueChange` | `output<T>` | N/A | Emitted with the typed, valid values on every change, only when `mode` is `'live'`. Fires once immediately if the form starts valid with its defaults. |
141
+ | `submitLabel` | `input<string>` | None | Custom label for the submit button, instead of `messages.submit()`. |
142
+ | `hideSubmit` | `input<boolean>` | `false` | Hides the internal submit button; use the public `submit()` method to trigger submission from your own UI. |
143
+ | `loading` | `input<boolean>` | `false` | Disables the internal button and makes `submit()` a no-op while `true`. |
144
+ | `value` | `input<T>` | None | External value patched into the current form (no rebuild, no `valueChange` in `'live'` mode) every time it receives a new, non-`undefined` value. |
145
+ | `submit()` | `(): void` | N/A | Public method: runs the same submit logic as the internal button, works whether `hideSubmit` is `true` or `false`. |
52
146
 
53
147
  ### `FieldConfig<T>`
54
148
 
@@ -63,23 +157,57 @@ For a calculator-style form (no submit action, values used as you type):
63
157
  | `defaultValue`| `T[keyof T]` | None | Initial value assigned to the field before user interaction. |
64
158
  | `colSpan` | `1 \| 2` | `1` | How many grid columns this field spans, when the component's `columns` input is 2 or more. |
65
159
  | `disabled` | `boolean` | `false` | Renders the control disabled from the start; still included in the value `formSubmit` emits. |
160
+ | `showPasswordToggle` | `boolean` | `true` | For a `type: 'password'` field, whether it renders a show/hide toggle button. No effect on other field types. |
161
+ | `appearance` | `FieldAppearance` (`'default' \| 'switch' \| 'segmented'`) | `'default'` | Purely visual variant. `'switch'` only affects `type: 'checkbox'`, `'segmented'` only affects `type: 'radio'`. Any other combination is ignored, same as `'default'`. See "Switch and segmented appearance" below. |
162
+ | `hint` | `string` | None | Short helper text shown below the field when there's no active error. See "Field hints and option descriptions" below. |
163
+ | `readonly` | `boolean` | `false` | Native HTML `readonly`, for `text`/`number`/`email`/`password`/`date`/`textarea` only. No effect on `select`/`radio`/`checkbox`. See "Read-only fields" below. |
164
+
165
+ `FieldOption` also gains an optional `description?: string`, shown below that specific option (for `radio`, see below); `select` has nowhere to render it, so it's ignored there.
166
+
167
+ ### `FormBuilderMessages`
168
+
169
+ | Member | Type | Description |
170
+ | ------ | ---- | ------------ |
171
+ | `required()` | `() => string` | Shown when a required field is empty. |
172
+ | `email()` | `() => string` | Shown when an `email` field isn't well-formed. |
173
+ | `min(min)` | `(min: number) => string` | Shown when a numeric field is below `min`. |
174
+ | `max(max)` | `(max: number) => string` | Shown when a numeric field is above `max`. |
175
+ | `minLength(requiredLength)` | `(requiredLength: number) => string` | Shown when a field's value is shorter than `minLength`. |
176
+ | `maxLength(requiredLength)` | `(requiredLength: number) => string` | Shown when a field's value is longer than `maxLength`. |
177
+ | `pattern()` | `() => string` | Shown when a field's value doesn't match `pattern`. |
178
+ | `submit()` | `() => string` | Label of the submit button. |
179
+ | `showPassword()` | `() => string` | `aria-label` of the password toggle when the value is currently hidden. |
180
+ | `hidePassword()` | `() => string` | `aria-label` of the password toggle when the value is currently shown. |
181
+
182
+ | Export | Type | Description |
183
+ | ------ | ---- | ------------ |
184
+ | `FORM_BUILDER_MESSAGES` | `InjectionToken<FormBuilderMessages>` | Defaults to `FORM_BUILDER_MESSAGES_EN`. Prefer `provideFormBuilderMessages()` over providing this directly. |
185
+ | `provideFormBuilderMessages(overrides)` | `(overrides: Partial<FormBuilderMessages>) => Provider` | Registers a message override, merged on top of the English defaults. |
186
+ | `FORM_BUILDER_MESSAGES_EN` | `FormBuilderMessages` | English preset (the default). |
187
+ | `FORM_BUILDER_MESSAGES_ES` | `FormBuilderMessages` | Spanish preset. |
66
188
 
67
189
  ### Theming
68
190
 
69
191
  `FormBuilder` sizes and colors itself via CSS custom properties, part of
70
192
  the shared `--zhunam-*` namespace used across every `@zhunam/*` library.
71
- Set any of them from the consuming app's own stylesheet, scoped to
72
- `lib-form-builder` or wider:
193
+ None of them are declared on the component's own `:host`, so you can set
194
+ any of them from `:root`, from a wrapping element, or scoped directly to
195
+ `lib-form-builder`, whichever is more convenient for your app; the
196
+ closest ancestor that sets a given property wins, same as any other
197
+ inherited CSS custom property.
73
198
 
74
199
  | Custom property | Default | Description |
75
200
  | ------------------------------- | -------- | -------------------------------------------------- |
76
- | `--zhunam-primary` | `#3b82f6` | Submit button background, radio/checkbox accent color. |
201
+ | `--zhunam-primary` | `#3b82f6` | Submit button background, radio/checkbox accent color fallback. |
202
+ | `--zhunam-accent` | `var(--zhunam-primary)` | Radio/checkbox accent color specifically, independent from `--zhunam-primary`. Falls back to it when unset. |
203
+ | `--zhunam-submit-width` | `auto` | Submit button width. |
77
204
  | `--zhunam-primary-content` | `#fff` | Submit button text/icon color, always painted on top of `--zhunam-primary`. |
78
205
  | `--zhunam-focus` | `var(--zhunam-primary)` | Focus outline and border on controls, independent from `--zhunam-primary` so a high-contrast focus ring doesn't require changing your brand color. |
79
206
  | `--zhunam-text` | `#1f2937` | Base text color, radio/checkbox option labels. |
80
207
  | `--zhunam-text-secondary` | `#374151` | Field label text. |
81
208
  | `--zhunam-border` | `#d1d5db` | Control borders. |
82
- | `--zhunam-error` | `#dc2626` | Validation/server error message text. |
209
+ | `--zhunam-error` | `#dc2626` | Validation/server error message background/border, where used. See `--zhunam-error-text` for the message's own text color. |
210
+ | `--zhunam-error-text` | `var(--zhunam-error)` | Validation/server error message text color specifically. Falls back to `--zhunam-error` when unset, so most apps only ever need to set one of the two. |
83
211
  | `--zhunam-radius` | `0.5rem` | Control and submit button corner radius. |
84
212
  | `--zhunam-font-family` | `system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif` | Font for the whole component. |
85
213
  | `--zhunam-font-size` | `0.9375rem` | Base font size. |
@@ -87,11 +215,48 @@ Set any of them from the consuming app's own stylesheet, scoped to
87
215
  | `--zhunam-transition-duration` | `150ms` | Duration of border/opacity transitions. |
88
216
  | `--zhunam-form-control-padding-x` | `0.75rem` | Horizontal padding inside inputs/selects/textareas. |
89
217
  | `--zhunam-form-control-padding-y` | `0.5rem` | Vertical padding inside inputs/selects/textareas. |
218
+ | `--zhunam-segmented-bg` | `#fff` | Inactive pill background for a `'segmented'` radio group. The active pill reuses `--zhunam-primary`/`--zhunam-primary-content` instead, same as the submit button. |
90
219
 
91
220
  `--fb-columns` is an internal implementation detail (it carries the
92
221
  `columns` input's value into the field grid's CSS), not a themeable
93
222
  custom property; setting it manually has no supported effect.
94
223
 
224
+ ### Internationalization
225
+
226
+ Every message `FormBuilder` renders on its own (validation text, the
227
+ submit button label) comes from `FORM_BUILDER_MESSAGES`, an injectable
228
+ token that defaults to English. A per-field `FieldValidatorConfig.errorMessages`
229
+ override always takes precedence over this token for that specific field.
230
+
231
+ ```typescript
232
+ import { provideFormBuilderMessages, FORM_BUILDER_MESSAGES_ES } from '@zhunam/form-builder';
233
+
234
+ // app.config.ts, or any component's own `providers`:
235
+ providers: [provideFormBuilderMessages(FORM_BUILDER_MESSAGES_ES)]
236
+ ```
237
+
238
+ A partial override only replaces the messages you specify, the rest stay
239
+ in English:
240
+
241
+ ```typescript
242
+ providers: [provideFormBuilderMessages({ submit: () => 'Send' })]
243
+ ```
244
+
245
+ Every message is a function, called on every render rather than once at
246
+ startup, so one that reads a signal updates live:
247
+
248
+ ```typescript
249
+ const language = signal<'en' | 'es'>('en');
250
+
251
+ providers: [
252
+ provideFormBuilderMessages({
253
+ required: () => (language() === 'en' ? 'This field is required.' : 'Este campo es obligatorio.'),
254
+ min: (min) =>
255
+ language() === 'en' ? `The value must be at least ${min}.` : `El valor debe ser como mínimo ${min}.`,
256
+ }),
257
+ ]
258
+ ```
259
+
95
260
  ## Compatibility
96
261
 
97
262
  `@angular/core` and `@angular/forms` `^20.0.0 || ^21.0.0 || ^22.0.0`.