@zhunam/form-builder 1.2.0 → 3.0.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 +133 -0
- package/fesm2022/zhunam-form-builder.mjs +214 -33
- package/fesm2022/zhunam-form-builder.mjs.map +1 -1
- package/package.json +1 -1
- package/types/zhunam-form-builder.d.ts +166 -5
package/README.md
CHANGED
|
@@ -36,6 +36,41 @@ 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
|
+
|
|
39
74
|
## API
|
|
40
75
|
|
|
41
76
|
### `FormBuilder<T>`
|
|
@@ -49,6 +84,11 @@ For a calculator-style form (no submit action, values used as you type):
|
|
|
49
84
|
| `formSubmit` | `output<T>` | N/A | Emitted with the typed form values, only when the native form and every `crossFieldValidators` check pass. |
|
|
50
85
|
| `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
86
|
| `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. |
|
|
87
|
+
| `submitLabel` | `input<string>` | None | Custom label for the submit button, instead of `messages.submit()`. |
|
|
88
|
+
| `hideSubmit` | `input<boolean>` | `false` | Hides the internal submit button; use the public `submit()` method to trigger submission from your own UI. |
|
|
89
|
+
| `loading` | `input<boolean>` | `false` | Disables the internal button and makes `submit()` a no-op while `true`. |
|
|
90
|
+
| `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. |
|
|
91
|
+
| `submit()` | `(): void` | N/A | Public method: runs the same submit logic as the internal button, works whether `hideSubmit` is `true` or `false`. |
|
|
52
92
|
|
|
53
93
|
### `FieldConfig<T>`
|
|
54
94
|
|
|
@@ -63,6 +103,99 @@ For a calculator-style form (no submit action, values used as you type):
|
|
|
63
103
|
| `defaultValue`| `T[keyof T]` | None | Initial value assigned to the field before user interaction. |
|
|
64
104
|
| `colSpan` | `1 \| 2` | `1` | How many grid columns this field spans, when the component's `columns` input is 2 or more. |
|
|
65
105
|
| `disabled` | `boolean` | `false` | Renders the control disabled from the start; still included in the value `formSubmit` emits. |
|
|
106
|
+
| `showPasswordToggle` | `boolean` | `true` | For a `type: 'password'` field, whether it renders a show/hide toggle button. No effect on other field types. |
|
|
107
|
+
|
|
108
|
+
### `FormBuilderMessages`
|
|
109
|
+
|
|
110
|
+
| Member | Type | Description |
|
|
111
|
+
| ------ | ---- | ------------ |
|
|
112
|
+
| `required()` | `() => string` | Shown when a required field is empty. |
|
|
113
|
+
| `email()` | `() => string` | Shown when an `email` field isn't well-formed. |
|
|
114
|
+
| `min(min)` | `(min: number) => string` | Shown when a numeric field is below `min`. |
|
|
115
|
+
| `max(max)` | `(max: number) => string` | Shown when a numeric field is above `max`. |
|
|
116
|
+
| `minLength(requiredLength)` | `(requiredLength: number) => string` | Shown when a field's value is shorter than `minLength`. |
|
|
117
|
+
| `maxLength(requiredLength)` | `(requiredLength: number) => string` | Shown when a field's value is longer than `maxLength`. |
|
|
118
|
+
| `pattern()` | `() => string` | Shown when a field's value doesn't match `pattern`. |
|
|
119
|
+
| `submit()` | `() => string` | Label of the submit button. |
|
|
120
|
+
| `showPassword()` | `() => string` | `aria-label` of the password toggle when the value is currently hidden. |
|
|
121
|
+
| `hidePassword()` | `() => string` | `aria-label` of the password toggle when the value is currently shown. |
|
|
122
|
+
|
|
123
|
+
| Export | Type | Description |
|
|
124
|
+
| ------ | ---- | ------------ |
|
|
125
|
+
| `FORM_BUILDER_MESSAGES` | `InjectionToken<FormBuilderMessages>` | Defaults to `FORM_BUILDER_MESSAGES_EN`. Prefer `provideFormBuilderMessages()` over providing this directly. |
|
|
126
|
+
| `provideFormBuilderMessages(overrides)` | `(overrides: Partial<FormBuilderMessages>) => Provider` | Registers a message override, merged on top of the English defaults. |
|
|
127
|
+
| `FORM_BUILDER_MESSAGES_EN` | `FormBuilderMessages` | English preset (the default). |
|
|
128
|
+
| `FORM_BUILDER_MESSAGES_ES` | `FormBuilderMessages` | Spanish preset. |
|
|
129
|
+
|
|
130
|
+
### Theming
|
|
131
|
+
|
|
132
|
+
`FormBuilder` sizes and colors itself via CSS custom properties, part of
|
|
133
|
+
the shared `--zhunam-*` namespace used across every `@zhunam/*` library.
|
|
134
|
+
None of them are declared on the component's own `:host`, so you can set
|
|
135
|
+
any of them from `:root`, from a wrapping element, or scoped directly to
|
|
136
|
+
`lib-form-builder`, whichever is more convenient for your app; the
|
|
137
|
+
closest ancestor that sets a given property wins, same as any other
|
|
138
|
+
inherited CSS custom property.
|
|
139
|
+
|
|
140
|
+
| Custom property | Default | Description |
|
|
141
|
+
| ------------------------------- | -------- | -------------------------------------------------- |
|
|
142
|
+
| `--zhunam-primary` | `#3b82f6` | Submit button background, radio/checkbox accent color fallback. |
|
|
143
|
+
| `--zhunam-accent` | `var(--zhunam-primary)` | Radio/checkbox accent color specifically, independent from `--zhunam-primary`. Falls back to it when unset. |
|
|
144
|
+
| `--zhunam-submit-width` | `auto` | Submit button width. |
|
|
145
|
+
| `--zhunam-primary-content` | `#fff` | Submit button text/icon color, always painted on top of `--zhunam-primary`. |
|
|
146
|
+
| `--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. |
|
|
147
|
+
| `--zhunam-text` | `#1f2937` | Base text color, radio/checkbox option labels. |
|
|
148
|
+
| `--zhunam-text-secondary` | `#374151` | Field label text. |
|
|
149
|
+
| `--zhunam-border` | `#d1d5db` | Control borders. |
|
|
150
|
+
| `--zhunam-error` | `#dc2626` | Validation/server error message background/border, where used. See `--zhunam-error-text` for the message's own text color. |
|
|
151
|
+
| `--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. |
|
|
152
|
+
| `--zhunam-radius` | `0.5rem` | Control and submit button corner radius. |
|
|
153
|
+
| `--zhunam-font-family` | `system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif` | Font for the whole component. |
|
|
154
|
+
| `--zhunam-font-size` | `0.9375rem` | Base font size. |
|
|
155
|
+
| `--zhunam-spacing` | `1rem` | Vertical rhythm between fields, and the grid gap on desktop. |
|
|
156
|
+
| `--zhunam-transition-duration` | `150ms` | Duration of border/opacity transitions. |
|
|
157
|
+
| `--zhunam-form-control-padding-x` | `0.75rem` | Horizontal padding inside inputs/selects/textareas. |
|
|
158
|
+
| `--zhunam-form-control-padding-y` | `0.5rem` | Vertical padding inside inputs/selects/textareas. |
|
|
159
|
+
|
|
160
|
+
`--fb-columns` is an internal implementation detail (it carries the
|
|
161
|
+
`columns` input's value into the field grid's CSS), not a themeable
|
|
162
|
+
custom property; setting it manually has no supported effect.
|
|
163
|
+
|
|
164
|
+
### Internationalization
|
|
165
|
+
|
|
166
|
+
Every message `FormBuilder` renders on its own (validation text, the
|
|
167
|
+
submit button label) comes from `FORM_BUILDER_MESSAGES`, an injectable
|
|
168
|
+
token that defaults to English. A per-field `FieldValidatorConfig.errorMessages`
|
|
169
|
+
override always takes precedence over this token for that specific field.
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
import { provideFormBuilderMessages, FORM_BUILDER_MESSAGES_ES } from '@zhunam/form-builder';
|
|
173
|
+
|
|
174
|
+
// app.config.ts, or any component's own `providers`:
|
|
175
|
+
providers: [provideFormBuilderMessages(FORM_BUILDER_MESSAGES_ES)]
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
A partial override only replaces the messages you specify, the rest stay
|
|
179
|
+
in English:
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
providers: [provideFormBuilderMessages({ submit: () => 'Send' })]
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Every message is a function, called on every render rather than once at
|
|
186
|
+
startup, so one that reads a signal updates live:
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
const language = signal<'en' | 'es'>('en');
|
|
190
|
+
|
|
191
|
+
providers: [
|
|
192
|
+
provideFormBuilderMessages({
|
|
193
|
+
required: () => (language() === 'en' ? 'This field is required.' : 'Este campo es obligatorio.'),
|
|
194
|
+
min: (min) =>
|
|
195
|
+
language() === 'en' ? `The value must be at least ${min}.` : `El valor debe ser como mínimo ${min}.`,
|
|
196
|
+
}),
|
|
197
|
+
]
|
|
198
|
+
```
|
|
66
199
|
|
|
67
200
|
## Compatibility
|
|
68
201
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { input, output, signal,
|
|
2
|
+
import { InjectionToken, input, output, inject, signal, linkedSignal, effect, untracked, ViewEncapsulation, ChangeDetectionStrategy, Component, NgModule } from '@angular/core';
|
|
3
3
|
import * as i1 from '@angular/forms';
|
|
4
4
|
import { FormControl, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
|
|
5
5
|
|
|
@@ -56,11 +56,83 @@ function branchesOverlap(a, b) {
|
|
|
56
56
|
return a.startsWith(b) || b.startsWith(a);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Default English messages. Used when no `FORM_BUILDER_MESSAGES` provider
|
|
61
|
+
* is registered.
|
|
62
|
+
*/
|
|
63
|
+
const FORM_BUILDER_MESSAGES_EN = {
|
|
64
|
+
required: () => 'This field is required.',
|
|
65
|
+
email: () => 'Enter a valid email address.',
|
|
66
|
+
min: (min) => `The value must be at least ${min}.`,
|
|
67
|
+
max: (max) => `The value must be at most ${max}.`,
|
|
68
|
+
minLength: (requiredLength) => `Must be at least ${requiredLength} characters.`,
|
|
69
|
+
maxLength: (requiredLength) => `Must be at most ${requiredLength} characters.`,
|
|
70
|
+
pattern: () => 'The format is not valid.',
|
|
71
|
+
submit: () => 'Submit',
|
|
72
|
+
showPassword: () => 'Show password',
|
|
73
|
+
hidePassword: () => 'Hide password',
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Spanish preset, ready to pass to `provideFormBuilderMessages()`.
|
|
77
|
+
* @example
|
|
78
|
+
* providers: [provideFormBuilderMessages(FORM_BUILDER_MESSAGES_ES)]
|
|
79
|
+
*/
|
|
80
|
+
const FORM_BUILDER_MESSAGES_ES = {
|
|
81
|
+
required: () => 'Este campo es obligatorio.',
|
|
82
|
+
email: () => 'El email no es válido.',
|
|
83
|
+
min: (min) => `El valor debe ser como mínimo ${min}.`,
|
|
84
|
+
max: (max) => `El valor debe ser como máximo ${max}.`,
|
|
85
|
+
minLength: (requiredLength) => `Debe tener al menos ${requiredLength} caracteres.`,
|
|
86
|
+
maxLength: (requiredLength) => `Debe tener como máximo ${requiredLength} caracteres.`,
|
|
87
|
+
pattern: () => 'El formato no es válido.',
|
|
88
|
+
submit: () => 'Enviar',
|
|
89
|
+
showPassword: () => 'Mostrar contraseña',
|
|
90
|
+
hidePassword: () => 'Ocultar contraseña',
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* DI token for the active `FormBuilderMessages` set. Defaults to
|
|
95
|
+
* `FORM_BUILDER_MESSAGES_EN` when no provider is registered.
|
|
96
|
+
*
|
|
97
|
+
* A per-field `FieldValidatorConfig.errorMessages` override, when set,
|
|
98
|
+
* still takes precedence over this token for that specific field: this
|
|
99
|
+
* token supplies the message only when the field itself doesn't override
|
|
100
|
+
* that validator's text.
|
|
101
|
+
*
|
|
102
|
+
* Prefer `provideFormBuilderMessages()` over providing this token
|
|
103
|
+
* directly, it merges your overrides on top of the English defaults
|
|
104
|
+
* instead of requiring every message to be re-specified.
|
|
105
|
+
*/
|
|
106
|
+
const FORM_BUILDER_MESSAGES = new InjectionToken('FORM_BUILDER_MESSAGES', {
|
|
107
|
+
providedIn: 'root',
|
|
108
|
+
factory: () => FORM_BUILDER_MESSAGES_EN,
|
|
109
|
+
});
|
|
110
|
+
/**
|
|
111
|
+
* Registers a `FormBuilderMessages` override, merged on top of the
|
|
112
|
+
* English defaults so a partial override (e.g. only `submit`) leaves the
|
|
113
|
+
* rest in English instead of requiring the full set.
|
|
114
|
+
*
|
|
115
|
+
* @param overrides Messages to override. Any member left out falls back
|
|
116
|
+
* to `FORM_BUILDER_MESSAGES_EN`.
|
|
117
|
+
* @example
|
|
118
|
+
* // Full preset:
|
|
119
|
+
* providers: [provideFormBuilderMessages(FORM_BUILDER_MESSAGES_ES)]
|
|
120
|
+
* @example
|
|
121
|
+
* // Partial override, everything else stays in English:
|
|
122
|
+
* providers: [provideFormBuilderMessages({ min: () => 'Must be greater than 0' })]
|
|
123
|
+
*/
|
|
124
|
+
function provideFormBuilderMessages(overrides) {
|
|
125
|
+
return {
|
|
126
|
+
provide: FORM_BUILDER_MESSAGES,
|
|
127
|
+
useValue: { ...FORM_BUILDER_MESSAGES_EN, ...overrides },
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
59
131
|
// NUNCA usar [innerHTML], bypassSecurityTrustHtml, ni ningún mecanismo de inserción de HTML crudo en el DOM del componente.
|
|
60
|
-
// label, placeholder, errorMessages, y FieldOption.label se renderizan siempre con interpolación {{ }} normal de Angular
|
|
132
|
+
// label, placeholder, errorMessages, y FieldOption.label se renderizan siempre con interpolación {{ }} normal de Angular, nunca vía binding de propiedad que inserte HTML.
|
|
61
133
|
// Angular's built-in validators report `minlength`/`maxlength` (all lowercase)
|
|
62
134
|
// on `control.errors`, not the camelCase keys `FieldValidatorConfig.errorMessages`
|
|
63
|
-
// uses
|
|
135
|
+
// uses, this bridges the two so consumer-facing config can stay camelCase.
|
|
64
136
|
const RAW_ERROR_KEY_MAP = {
|
|
65
137
|
required: 'required',
|
|
66
138
|
min: 'min',
|
|
@@ -70,15 +142,6 @@ const RAW_ERROR_KEY_MAP = {
|
|
|
70
142
|
pattern: 'pattern',
|
|
71
143
|
email: 'email',
|
|
72
144
|
};
|
|
73
|
-
const DEFAULT_ERROR_MESSAGES = {
|
|
74
|
-
required: 'Este campo es obligatorio.',
|
|
75
|
-
min: 'El valor es menor al mínimo permitido.',
|
|
76
|
-
max: 'El valor supera el máximo permitido.',
|
|
77
|
-
minLength: 'El valor es demasiado corto.',
|
|
78
|
-
maxLength: 'El valor es demasiado largo.',
|
|
79
|
-
pattern: 'El formato no es válido.',
|
|
80
|
-
email: 'Ingresá un email válido.',
|
|
81
|
-
};
|
|
82
145
|
class FormBuilder {
|
|
83
146
|
/**
|
|
84
147
|
* Declarative configuration of the fields to render, in display order.
|
|
@@ -165,8 +228,49 @@ class FormBuilder {
|
|
|
165
228
|
* <lib-form-builder [fields]="fields" [serverErrors]="serverErrors()" (formSubmit)="onFormSubmit($event)" />
|
|
166
229
|
*/
|
|
167
230
|
serverErrors = input({}, ...(ngDevMode ? [{ debugName: "serverErrors" }] : /* istanbul ignore next */ []));
|
|
231
|
+
/**
|
|
232
|
+
* Custom label for the submit button, instead of the injected
|
|
233
|
+
* FORM_BUILDER_MESSAGES `submit()` text. Takes precedence over both
|
|
234
|
+
* the messages token and the default English label.
|
|
235
|
+
* @example
|
|
236
|
+
* <lib-form-builder [fields]="fields" submitLabel="Create account" />
|
|
237
|
+
*/
|
|
238
|
+
submitLabel = input(...(ngDevMode ? [undefined, { debugName: "submitLabel" }] : /* istanbul ignore next */ []));
|
|
239
|
+
/**
|
|
240
|
+
* Hides the internal submit button entirely, for a consumer that
|
|
241
|
+
* triggers submission from its own UI instead, via the public
|
|
242
|
+
* `submit()` method.
|
|
243
|
+
* @default false
|
|
244
|
+
* @example
|
|
245
|
+
* <lib-form-builder #form [fields]="fields" [hideSubmit]="true" />
|
|
246
|
+
* <button (click)="form.submit()">Continue</button>
|
|
247
|
+
*/
|
|
248
|
+
hideSubmit = input(false, ...(ngDevMode ? [{ debugName: "hideSubmit" }] : /* istanbul ignore next */ []));
|
|
249
|
+
/**
|
|
250
|
+
* Disables the internal submit button and makes the public `submit()`
|
|
251
|
+
* method a no-op while `true`, e.g. during an in-flight request.
|
|
252
|
+
* @default false
|
|
253
|
+
* @example
|
|
254
|
+
* <lib-form-builder [fields]="fields" [loading]="saving()" (formSubmit)="onSave($event)" />
|
|
255
|
+
*/
|
|
256
|
+
loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
|
|
257
|
+
/**
|
|
258
|
+
* External value applied to the form without recreating it. Every
|
|
259
|
+
* time this receives a new, non-`undefined` value (including the
|
|
260
|
+
* first one), it's patched into the current form
|
|
261
|
+
* (`emitEvent: false`, so it never triggers `valueChange` in `'live'`
|
|
262
|
+
* mode). Rebuilding `fields()` afterward keeps whatever was patched,
|
|
263
|
+
* the same as any other value the user typed.
|
|
264
|
+
* @example
|
|
265
|
+
* <lib-form-builder [fields]="fields" [value]="presetValue()" />
|
|
266
|
+
*/
|
|
267
|
+
value = input(...(ngDevMode ? [undefined, { debugName: "value" }] : /* istanbul ignore next */ []));
|
|
268
|
+
messages = inject(FORM_BUILDER_MESSAGES);
|
|
168
269
|
submitted = signal(false, ...(ngDevMode ? [{ debugName: "submitted" }] : /* istanbul ignore next */ []));
|
|
169
270
|
crossFieldErrors = signal({}, ...(ngDevMode ? [{ debugName: "crossFieldErrors" }] : /* istanbul ignore next */ []));
|
|
271
|
+
// Keys of password fields currently showing plain text instead of
|
|
272
|
+
// dots, toggled per-field by the user via the show/hide button.
|
|
273
|
+
visiblePasswordFields = signal(new Set(), ...(ngDevMode ? [{ debugName: "visiblePasswordFields" }] : /* istanbul ignore next */ []));
|
|
170
274
|
// Snapshot of each server-error field's value at the moment that error
|
|
171
275
|
// appeared. A server error is only ever about a rejected backend
|
|
172
276
|
// observation of the exact value the user submitted — the instant the
|
|
@@ -175,13 +279,13 @@ class FormBuilder {
|
|
|
175
279
|
// having to clear `serverErrors` themselves.
|
|
176
280
|
serverErrorSnapshot = signal({}, ...(ngDevMode ? [{ debugName: "serverErrorSnapshot" }] : /* istanbul ignore next */ []));
|
|
177
281
|
// `fields()` can change at runtime (e.g. a wizard swapping steps), so the
|
|
178
|
-
// FormGroup is derived
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
282
|
+
// FormGroup is derived rather than built once in the constructor: it's
|
|
283
|
+
// rebuilt only when the `fields()` array reference changes. `linkedSignal`
|
|
284
|
+
// (not `computed()`) so `buildFormGroup` can read the previous FormGroup
|
|
285
|
+
// and carry forward the current value of every control whose key still
|
|
286
|
+
// exists in the new `fields()`, instead of resetting it to `defaultValue`.
|
|
287
|
+
formGroup = linkedSignal({ ...(ngDevMode ? { debugName: "formGroup" } : /* istanbul ignore next */ {}), source: this.fields,
|
|
288
|
+
computation: (fields, previous) => this.buildFormGroup(fields, previous?.value) });
|
|
185
289
|
static instanceCounter = 0;
|
|
186
290
|
instanceId = ++FormBuilder.instanceCounter;
|
|
187
291
|
constructor() {
|
|
@@ -224,12 +328,29 @@ class FormBuilder {
|
|
|
224
328
|
const subscription = group.valueChanges.subscribe(emitIfValid);
|
|
225
329
|
onCleanup(() => subscription.unsubscribe());
|
|
226
330
|
});
|
|
331
|
+
// Applies an external `value()` to whichever FormGroup is current at
|
|
332
|
+
// that moment, without recreating it. `formGroup()` is deliberately
|
|
333
|
+
// read via `untracked()` so this effect only reacts to `value()`
|
|
334
|
+
// itself changing, never to a `fields()`-triggered rebuild: `patchValue`
|
|
335
|
+
// with `emitEvent: false` also means `valueChange` never fires from this.
|
|
336
|
+
effect(() => {
|
|
337
|
+
const value = this.value();
|
|
338
|
+
if (value === undefined) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const group = untracked(() => this.formGroup());
|
|
342
|
+
group.patchValue(value, { emitEvent: false });
|
|
343
|
+
});
|
|
227
344
|
}
|
|
228
|
-
buildFormGroup(fields) {
|
|
345
|
+
buildFormGroup(fields, previous) {
|
|
229
346
|
const controls = {};
|
|
230
347
|
for (const field of fields) {
|
|
231
|
-
const
|
|
232
|
-
|
|
348
|
+
const key = String(field.key);
|
|
349
|
+
const previousControl = previous?.get(key);
|
|
350
|
+
const initialValue = previousControl
|
|
351
|
+
? previousControl.value
|
|
352
|
+
: (field.defaultValue ?? (field.type === 'checkbox' ? false : null));
|
|
353
|
+
controls[key] = new FormControl({ value: initialValue, disabled: !!field.disabled }, this.buildValidators(field));
|
|
233
354
|
}
|
|
234
355
|
return new FormGroup(controls);
|
|
235
356
|
}
|
|
@@ -273,6 +394,26 @@ class FormBuilder {
|
|
|
273
394
|
optionsFor(field) {
|
|
274
395
|
return field.options ?? [];
|
|
275
396
|
}
|
|
397
|
+
showPasswordToggleFor(field) {
|
|
398
|
+
return field.showPasswordToggle ?? true;
|
|
399
|
+
}
|
|
400
|
+
isPasswordVisible(field) {
|
|
401
|
+
return this.visiblePasswordFields().has(String(field.key));
|
|
402
|
+
}
|
|
403
|
+
passwordInputType(field) {
|
|
404
|
+
return this.isPasswordVisible(field) ? 'text' : 'password';
|
|
405
|
+
}
|
|
406
|
+
togglePasswordVisibility(field) {
|
|
407
|
+
const key = String(field.key);
|
|
408
|
+
const next = new Set(this.visiblePasswordFields());
|
|
409
|
+
if (next.has(key)) {
|
|
410
|
+
next.delete(key);
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
next.add(key);
|
|
414
|
+
}
|
|
415
|
+
this.visiblePasswordFields.set(next);
|
|
416
|
+
}
|
|
276
417
|
// Shared by the label's `for`, the control's `id`, and (for radio groups)
|
|
277
418
|
// the `name` that keeps this field's radios from being grouped with
|
|
278
419
|
// another field's — RadioControlValueAccessor matches radios by `name`
|
|
@@ -285,11 +426,36 @@ class FormBuilder {
|
|
|
285
426
|
if (!control.errors || (!control.touched && !this.submitted())) {
|
|
286
427
|
return null;
|
|
287
428
|
}
|
|
288
|
-
const
|
|
429
|
+
const rawKey = Object.keys(control.errors)[0];
|
|
430
|
+
const errorKey = RAW_ERROR_KEY_MAP[rawKey];
|
|
289
431
|
if (!errorKey) {
|
|
290
432
|
return null;
|
|
291
433
|
}
|
|
292
|
-
|
|
434
|
+
// Precedence: a per-field override always wins, then the injected
|
|
435
|
+
// FORM_BUILDER_MESSAGES token (English by default), evaluated fresh
|
|
436
|
+
// here so a message reading a signal (e.g. a language switcher)
|
|
437
|
+
// updates live.
|
|
438
|
+
const fieldMessage = field.validators?.errorMessages?.[errorKey];
|
|
439
|
+
if (fieldMessage) {
|
|
440
|
+
return fieldMessage;
|
|
441
|
+
}
|
|
442
|
+
const errorDetail = control.errors[rawKey];
|
|
443
|
+
switch (errorKey) {
|
|
444
|
+
case 'required':
|
|
445
|
+
return this.messages.required();
|
|
446
|
+
case 'email':
|
|
447
|
+
return this.messages.email();
|
|
448
|
+
case 'min':
|
|
449
|
+
return this.messages.min(errorDetail.min);
|
|
450
|
+
case 'max':
|
|
451
|
+
return this.messages.max(errorDetail.max);
|
|
452
|
+
case 'minLength':
|
|
453
|
+
return this.messages.minLength(errorDetail.requiredLength);
|
|
454
|
+
case 'maxLength':
|
|
455
|
+
return this.messages.maxLength(errorDetail.requiredLength);
|
|
456
|
+
case 'pattern':
|
|
457
|
+
return this.messages.pattern();
|
|
458
|
+
}
|
|
293
459
|
}
|
|
294
460
|
serverErrorFor(field) {
|
|
295
461
|
const message = this.serverErrors()[field.key];
|
|
@@ -307,7 +473,7 @@ class FormBuilder {
|
|
|
307
473
|
return this.crossFieldErrors()[String(field.key)] ?? null;
|
|
308
474
|
}
|
|
309
475
|
// Single source of truth for "is an error currently shown for this
|
|
310
|
-
// field, and what does it say"
|
|
476
|
+
// field, and what does it say": both the visible `.fb-error` message
|
|
311
477
|
// and its ARIA wiring (aria-invalid, aria-describedby) read from this,
|
|
312
478
|
// so the two can never drift out of sync with each other.
|
|
313
479
|
activeErrorFor(field) {
|
|
@@ -320,15 +486,30 @@ class FormBuilder {
|
|
|
320
486
|
return `${this.fieldId(field)}-legend`;
|
|
321
487
|
}
|
|
322
488
|
isSubmitDisabled() {
|
|
323
|
-
return this.formGroup().invalid || Object.keys(this.crossFieldErrors()).length > 0;
|
|
489
|
+
return this.loading() || this.formGroup().invalid || Object.keys(this.crossFieldErrors()).length > 0;
|
|
324
490
|
}
|
|
325
|
-
|
|
491
|
+
/**
|
|
492
|
+
* Triggers the exact same submit logic the internal button triggers:
|
|
493
|
+
* marks the form as submitted, runs validation, and emits `formSubmit`
|
|
494
|
+
* if it passes. Works identically whether `hideSubmit` is `true` or
|
|
495
|
+
* `false`, and is a no-op while `loading` is `true`.
|
|
496
|
+
* @example
|
|
497
|
+
* <lib-form-builder #form [fields]="fields" [hideSubmit]="true" />
|
|
498
|
+
* <button (click)="form.submit()">Continue</button>
|
|
499
|
+
*/
|
|
500
|
+
submit() {
|
|
501
|
+
this.performSubmit();
|
|
502
|
+
}
|
|
503
|
+
performSubmit() {
|
|
504
|
+
if (this.loading()) {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
326
507
|
this.submitted.set(true);
|
|
327
508
|
const group = this.formGroup();
|
|
328
509
|
if (group.invalid) {
|
|
329
510
|
return;
|
|
330
511
|
}
|
|
331
|
-
// `.value` excludes disabled controls entirely
|
|
512
|
+
// `.value` excludes disabled controls entirely, `getRawValue()` keeps
|
|
332
513
|
// them, so a disabled field with a `defaultValue` still comes through
|
|
333
514
|
// as a complete `T` instead of a hole in the emitted object.
|
|
334
515
|
const value = this.coerceNumberFields(group.getRawValue());
|
|
@@ -346,7 +527,7 @@ class FormBuilder {
|
|
|
346
527
|
// dynamically, so it never matches, and DefaultValueAccessor (string in,
|
|
347
528
|
// string out) handles the control instead. Fixed up here, at the point
|
|
348
529
|
// the value leaves the component, rather than in how the FormControl
|
|
349
|
-
// stores it
|
|
530
|
+
// stores it: Validators.min/max already parseFloat internally either
|
|
350
531
|
// way, so correcting the stored value would change nothing there but
|
|
351
532
|
// would still need this same conversion for the emitted object.
|
|
352
533
|
coerceNumberFields(rawValue) {
|
|
@@ -377,12 +558,12 @@ class FormBuilder {
|
|
|
377
558
|
return errors;
|
|
378
559
|
}
|
|
379
560
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: FormBuilder, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
380
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: FormBuilder, isStandalone: true, selector: "lib-form-builder", inputs: { fields: { classPropertyName: "fields", publicName: "fields", isSignal: true, isRequired: true, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, crossFieldValidators: { classPropertyName: "crossFieldValidators", publicName: "crossFieldValidators", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, serverErrors: { classPropertyName: "serverErrors", publicName: "serverErrors", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { formSubmit: "formSubmit", valueChange: "valueChange" }, ngImport: i0, template: "<form class=\"fb-form\" [formGroup]=\"formGroup()\" (ngSubmit)=\"onSubmit()\" novalidate>\r\n <div class=\"fb-fields-grid\" [style.--fb-columns]=\"columns()\">\r\n @for (field of fields(); track field.key) {\r\n <div class=\"fb-field\" [class.fb-field--span-2]=\"field.colSpan === 2\">\r\n @if (field.type !== 'checkbox' && field.type !== 'radio') {\r\n <label class=\"fb-label\" [attr.for]=\"fieldId(field)\">{{ field.label }}</label>\r\n }\r\n\r\n @switch (field.type) {\r\n @case ('checkbox') {\r\n <label class=\"fb-checkbox-option\">\r\n <input\r\n type=\"checkbox\"\r\n [formControl]=\"controlFor(field)\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n />\r\n {{ field.label }}\r\n </label>\r\n }\r\n @case ('radio') {\r\n <fieldset\r\n class=\"fb-radio-group\"\r\n role=\"radiogroup\"\r\n [attr.aria-labelledby]=\"legendId(field)\"\r\n >\r\n <legend class=\"fb-label\" [id]=\"legendId(field)\">{{ field.label }}</legend>\r\n @for (option of optionsFor(field); track option.value) {\r\n <label class=\"fb-radio-option\">\r\n <input\r\n type=\"radio\"\r\n [formControl]=\"controlFor(field)\"\r\n [name]=\"fieldId(field)\"\r\n [value]=\"option.value\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n />\r\n {{ option.label }}\r\n </label>\r\n }\r\n </fieldset>\r\n }\r\n @case ('textarea') {\r\n <textarea\r\n class=\"fb-control\"\r\n [id]=\"fieldId(field)\"\r\n [formControl]=\"controlFor(field)\"\r\n [placeholder]=\"field.placeholder ?? ''\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n ></textarea>\r\n }\r\n @case ('select') {\r\n <select\r\n class=\"fb-control\"\r\n [id]=\"fieldId(field)\"\r\n [formControl]=\"controlFor(field)\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n >\r\n @for (option of optionsFor(field); track option.value) {\r\n <option [ngValue]=\"option.value\">{{ option.label }}</option>\r\n }\r\n </select>\r\n }\r\n @default {\r\n <input\r\n class=\"fb-control\"\r\n [id]=\"fieldId(field)\"\r\n [type]=\"field.type\"\r\n [formControl]=\"controlFor(field)\"\r\n [placeholder]=\"field.placeholder ?? ''\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n />\r\n }\r\n }\r\n\r\n @if (activeErrorFor(field); as message) {\r\n <p class=\"fb-error\" [id]=\"errorId(field)\">{{ message }}</p>\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n <button type=\"submit\" class=\"fb-submit\" [disabled]=\"isSubmitDisabled()\">Submit</button>\r\n</form>\r\n", styles: [":host{--fb-font-family: system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;--fb-font-size: .9375rem;--fb-text-color: #1f2937;--fb-label-color: #374151;--fb-border-color: #d1d5db;--fb-error-color: #dc2626;--fb-primary-color: #3b82f6;--fb-focus-color: var(--fb-primary-color);--fb-border-radius: .5rem;--fb-spacing: 1rem;--fb-control-padding-y: .5rem;--fb-control-padding-x: .75rem;--fb-transition-duration: .15s;display:block;font-family:var(--fb-font-family);font-size:var(--fb-font-size);color:var(--fb-text-color)}.fb-form{display:flex;flex-direction:column;gap:var(--fb-spacing);width:100%}.fb-fields-grid{display:grid;grid-template-columns:repeat(var(--fb-columns, 1),1fr);gap:var(--fb-spacing)}.fb-field{display:flex;flex-direction:column;gap:calc(var(--fb-spacing) / 2.5);min-width:0}.fb-field--span-2{grid-column:span 2}@media(max-width:640px){.fb-fields-grid{grid-template-columns:1fr}.fb-field--span-2{grid-column:span 1}}.fb-label{font-weight:600;color:var(--fb-label-color);padding:0;border:none}.fb-control{box-sizing:border-box;width:100%;font:inherit;color:inherit;padding:var(--fb-control-padding-y) var(--fb-control-padding-x);border:1px solid var(--fb-border-color);border-radius:var(--fb-border-radius);background:transparent;transition:border-color var(--fb-transition-duration) ease}.fb-control:focus-visible{outline:2px solid var(--fb-focus-color);outline-offset:-1px;border-color:var(--fb-focus-color)}textarea.fb-control{min-height:5rem;resize:vertical}select.fb-control{cursor:pointer}.fb-radio-group{display:flex;flex-direction:column;gap:calc(var(--fb-spacing) / 2.5)}.fb-radio-option,.fb-checkbox-option{display:inline-flex;align-items:center;gap:.5rem;font-weight:400;color:var(--fb-text-color);cursor:pointer}.fb-radio-option input,.fb-checkbox-option input{accent-color:var(--fb-primary-color)}.fb-radio-option input:focus-visible,.fb-checkbox-option input:focus-visible{outline:2px solid var(--fb-focus-color);outline-offset:2px}.fb-error{margin:0;font-size:.8125rem;color:var(--fb-error-color)}.fb-submit{appearance:none;align-self:flex-start;border:none;border-radius:var(--fb-border-radius);background:var(--fb-primary-color);color:#fff;padding:var(--fb-control-padding-y) 1.25rem;font:inherit;font-weight:600;cursor:pointer;transition:opacity var(--fb-transition-duration) ease}.fb-submit:focus-visible{outline:2px solid var(--fb-focus-color);outline-offset:2px}.fb-submit:disabled{opacity:.45;cursor:not-allowed}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
561
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.17", type: FormBuilder, isStandalone: true, selector: "lib-form-builder", inputs: { fields: { classPropertyName: "fields", publicName: "fields", isSignal: true, isRequired: true, transformFunction: null }, columns: { classPropertyName: "columns", publicName: "columns", isSignal: true, isRequired: false, transformFunction: null }, crossFieldValidators: { classPropertyName: "crossFieldValidators", publicName: "crossFieldValidators", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, serverErrors: { classPropertyName: "serverErrors", publicName: "serverErrors", isSignal: true, isRequired: false, transformFunction: null }, submitLabel: { classPropertyName: "submitLabel", publicName: "submitLabel", isSignal: true, isRequired: false, transformFunction: null }, hideSubmit: { classPropertyName: "hideSubmit", publicName: "hideSubmit", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { formSubmit: "formSubmit", valueChange: "valueChange" }, ngImport: i0, template: "<form class=\"fb-form\" [formGroup]=\"formGroup()\" (ngSubmit)=\"performSubmit()\" novalidate>\r\n <div class=\"fb-fields-grid\" [style.--fb-columns]=\"columns()\">\r\n @for (field of fields(); track field.key) {\r\n <div class=\"fb-field\" [class.fb-field--span-2]=\"field.colSpan === 2\">\r\n @if (field.type !== 'checkbox' && field.type !== 'radio') {\r\n <label class=\"fb-label\" [attr.for]=\"fieldId(field)\">{{ field.label }}</label>\r\n }\r\n\r\n @switch (field.type) {\r\n @case ('checkbox') {\r\n <label class=\"fb-checkbox-option\">\r\n <input\r\n type=\"checkbox\"\r\n [formControl]=\"controlFor(field)\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n />\r\n {{ field.label }}\r\n </label>\r\n }\r\n @case ('radio') {\r\n <fieldset\r\n class=\"fb-radio-group\"\r\n role=\"radiogroup\"\r\n [attr.aria-labelledby]=\"legendId(field)\"\r\n >\r\n <legend class=\"fb-label\" [id]=\"legendId(field)\">{{ field.label }}</legend>\r\n @for (option of optionsFor(field); track option.value) {\r\n <label class=\"fb-radio-option\">\r\n <input\r\n type=\"radio\"\r\n [formControl]=\"controlFor(field)\"\r\n [name]=\"fieldId(field)\"\r\n [value]=\"option.value\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n />\r\n {{ option.label }}\r\n </label>\r\n }\r\n </fieldset>\r\n }\r\n @case ('password') {\r\n <div class=\"fb-password-wrapper\">\r\n <input\r\n class=\"fb-control\"\r\n [id]=\"fieldId(field)\"\r\n [type]=\"passwordInputType(field)\"\r\n [formControl]=\"controlFor(field)\"\r\n [placeholder]=\"field.placeholder ?? ''\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n />\r\n @if (showPasswordToggleFor(field)) {\r\n <button\r\n type=\"button\"\r\n class=\"fb-password-toggle\"\r\n [attr.aria-pressed]=\"isPasswordVisible(field)\"\r\n [attr.aria-label]=\"isPasswordVisible(field) ? messages.hidePassword() : messages.showPassword()\"\r\n (click)=\"togglePasswordVisibility(field)\"\r\n >\r\n @if (isPasswordVisible(field)) {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" aria-hidden=\"true\">\r\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3 3l18 18M10.58 10.58a2 2 0 002.83 2.83M9.88 4.24A9.9 9.9 0 0112 4c5 0 9 4 10 8-.32 1.12-.86 2.19-1.57 3.14M6.6 6.6C3.9 8.14 2.06 10.6 2 12c.66 2.34 2.19 4.34 4.19 5.68M14.12 14.12A3 3 0 019.88 9.88\" />\r\n </svg>\r\n } @else {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" aria-hidden=\"true\">\r\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z\" />\r\n <circle cx=\"12\" cy=\"12\" r=\"3\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\r\n </svg>\r\n }\r\n </button>\r\n }\r\n </div>\r\n }\r\n @case ('textarea') {\r\n <textarea\r\n class=\"fb-control\"\r\n [id]=\"fieldId(field)\"\r\n [formControl]=\"controlFor(field)\"\r\n [placeholder]=\"field.placeholder ?? ''\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n ></textarea>\r\n }\r\n @case ('select') {\r\n <select\r\n class=\"fb-control\"\r\n [id]=\"fieldId(field)\"\r\n [formControl]=\"controlFor(field)\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n >\r\n @for (option of optionsFor(field); track option.value) {\r\n <option [ngValue]=\"option.value\">{{ option.label }}</option>\r\n }\r\n </select>\r\n }\r\n @default {\r\n <input\r\n class=\"fb-control\"\r\n [id]=\"fieldId(field)\"\r\n [type]=\"field.type\"\r\n [formControl]=\"controlFor(field)\"\r\n [placeholder]=\"field.placeholder ?? ''\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n />\r\n }\r\n }\r\n\r\n @if (activeErrorFor(field); as message) {\r\n <p class=\"fb-error\" [id]=\"errorId(field)\">{{ message }}</p>\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n @if (!hideSubmit()) {\r\n <button type=\"submit\" class=\"fb-submit\" [disabled]=\"isSubmitDisabled()\">{{ submitLabel() ?? messages.submit() }}</button>\r\n }\r\n</form>\r\n", styles: [":host{display:block;font-family:var(--zhunam-font-family, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif);font-size:var(--zhunam-font-size, .9375rem);color:var(--zhunam-text, #1f2937)}.fb-form{display:flex;flex-direction:column;gap:var(--zhunam-spacing, 1rem);width:100%}.fb-fields-grid{display:grid;grid-template-columns:repeat(var(--fb-columns, 1),1fr);gap:var(--zhunam-spacing, 1rem)}.fb-field{display:flex;flex-direction:column;gap:calc(var(--zhunam-spacing, 1rem) / 2.5);min-width:0}.fb-field--span-2{grid-column:span 2}@media(max-width:640px){.fb-fields-grid{grid-template-columns:1fr}.fb-field--span-2{grid-column:span 1}}.fb-label{font-weight:600;color:var(--zhunam-text-secondary, #374151);padding:0;border:none}.fb-control{box-sizing:border-box;width:100%;font:inherit;color:inherit;padding:var(--zhunam-form-control-padding-y, .5rem) var(--zhunam-form-control-padding-x, .75rem);border:1px solid var(--zhunam-border, #d1d5db);border-radius:var(--zhunam-radius, .5rem);background:transparent;transition:border-color var(--zhunam-transition-duration, .15s) ease}.fb-control:focus-visible{outline:2px solid var(--zhunam-focus, var(--zhunam-primary, #3b82f6));outline-offset:-1px;border-color:var(--zhunam-focus, var(--zhunam-primary, #3b82f6))}textarea.fb-control{min-height:5rem;resize:vertical}select.fb-control{cursor:pointer}.fb-radio-group{display:flex;flex-direction:column;gap:calc(var(--zhunam-spacing, 1rem) / 2.5)}.fb-radio-option,.fb-checkbox-option{display:inline-flex;align-items:center;gap:.5rem;font-weight:400;color:var(--zhunam-text, #1f2937);cursor:pointer}.fb-radio-option input,.fb-checkbox-option input{accent-color:var(--zhunam-accent, var(--zhunam-primary, #3b82f6))}.fb-radio-option input:focus-visible,.fb-checkbox-option input:focus-visible{outline:2px solid var(--zhunam-focus, var(--zhunam-primary, #3b82f6));outline-offset:2px}.fb-password-wrapper{position:relative;display:flex}.fb-password-wrapper .fb-control{padding-right:calc(var(--zhunam-form-control-padding-x, .75rem) + 1.75rem)}.fb-password-toggle{appearance:none;position:absolute;top:50%;right:var(--zhunam-form-control-padding-x, .75rem);transform:translateY(-50%);display:inline-flex;align-items:center;justify-content:center;border:none;background:transparent;color:var(--zhunam-text-secondary, #374151);padding:0;cursor:pointer;line-height:0}.fb-password-toggle svg{width:1.125rem;height:1.125rem}.fb-password-toggle:focus-visible{outline:2px solid var(--zhunam-focus, var(--zhunam-primary, #3b82f6));outline-offset:2px}.fb-error{margin:0;font-size:.8125rem;color:var(--zhunam-error-text, var(--zhunam-error, #dc2626))}.fb-submit{appearance:none;align-self:flex-start;width:var(--zhunam-submit-width, auto);border:none;border-radius:var(--zhunam-radius, .5rem);background:var(--zhunam-primary, #3b82f6);color:var(--zhunam-primary-content, #fff);padding:var(--zhunam-form-control-padding-y, .5rem) 1.25rem;font:inherit;font-weight:600;cursor:pointer;transition:opacity var(--zhunam-transition-duration, .15s) ease}.fb-submit:focus-visible{outline:2px solid var(--zhunam-focus, var(--zhunam-primary, #3b82f6));outline-offset:2px}.fb-submit:disabled{opacity:.45;cursor:not-allowed}\n"], dependencies: [{ kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i1.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1.RadioControlValueAccessor, selector: "input[type=radio][formControlName],input[type=radio][formControl],input[type=radio][ngModel]", inputs: ["name", "formControlName", "value"] }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
381
562
|
}
|
|
382
563
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: FormBuilder, decorators: [{
|
|
383
564
|
type: Component,
|
|
384
|
-
args: [{ selector: 'lib-form-builder', imports: [ReactiveFormsModule], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, template: "<form class=\"fb-form\" [formGroup]=\"formGroup()\" (ngSubmit)=\"
|
|
385
|
-
}], ctorParameters: () => [], propDecorators: { fields: [{ type: i0.Input, args: [{ isSignal: true, alias: "fields", required: true }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], crossFieldValidators: [{ type: i0.Input, args: [{ isSignal: true, alias: "crossFieldValidators", required: false }] }], formSubmit: [{ type: i0.Output, args: ["formSubmit"] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }], serverErrors: [{ type: i0.Input, args: [{ isSignal: true, alias: "serverErrors", required: false }] }] } });
|
|
565
|
+
args: [{ selector: 'lib-form-builder', imports: [ReactiveFormsModule], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, template: "<form class=\"fb-form\" [formGroup]=\"formGroup()\" (ngSubmit)=\"performSubmit()\" novalidate>\r\n <div class=\"fb-fields-grid\" [style.--fb-columns]=\"columns()\">\r\n @for (field of fields(); track field.key) {\r\n <div class=\"fb-field\" [class.fb-field--span-2]=\"field.colSpan === 2\">\r\n @if (field.type !== 'checkbox' && field.type !== 'radio') {\r\n <label class=\"fb-label\" [attr.for]=\"fieldId(field)\">{{ field.label }}</label>\r\n }\r\n\r\n @switch (field.type) {\r\n @case ('checkbox') {\r\n <label class=\"fb-checkbox-option\">\r\n <input\r\n type=\"checkbox\"\r\n [formControl]=\"controlFor(field)\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n />\r\n {{ field.label }}\r\n </label>\r\n }\r\n @case ('radio') {\r\n <fieldset\r\n class=\"fb-radio-group\"\r\n role=\"radiogroup\"\r\n [attr.aria-labelledby]=\"legendId(field)\"\r\n >\r\n <legend class=\"fb-label\" [id]=\"legendId(field)\">{{ field.label }}</legend>\r\n @for (option of optionsFor(field); track option.value) {\r\n <label class=\"fb-radio-option\">\r\n <input\r\n type=\"radio\"\r\n [formControl]=\"controlFor(field)\"\r\n [name]=\"fieldId(field)\"\r\n [value]=\"option.value\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n />\r\n {{ option.label }}\r\n </label>\r\n }\r\n </fieldset>\r\n }\r\n @case ('password') {\r\n <div class=\"fb-password-wrapper\">\r\n <input\r\n class=\"fb-control\"\r\n [id]=\"fieldId(field)\"\r\n [type]=\"passwordInputType(field)\"\r\n [formControl]=\"controlFor(field)\"\r\n [placeholder]=\"field.placeholder ?? ''\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n />\r\n @if (showPasswordToggleFor(field)) {\r\n <button\r\n type=\"button\"\r\n class=\"fb-password-toggle\"\r\n [attr.aria-pressed]=\"isPasswordVisible(field)\"\r\n [attr.aria-label]=\"isPasswordVisible(field) ? messages.hidePassword() : messages.showPassword()\"\r\n (click)=\"togglePasswordVisibility(field)\"\r\n >\r\n @if (isPasswordVisible(field)) {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" aria-hidden=\"true\">\r\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3 3l18 18M10.58 10.58a2 2 0 002.83 2.83M9.88 4.24A9.9 9.9 0 0112 4c5 0 9 4 10 8-.32 1.12-.86 2.19-1.57 3.14M6.6 6.6C3.9 8.14 2.06 10.6 2 12c.66 2.34 2.19 4.34 4.19 5.68M14.12 14.12A3 3 0 019.88 9.88\" />\r\n </svg>\r\n } @else {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" aria-hidden=\"true\">\r\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z\" />\r\n <circle cx=\"12\" cy=\"12\" r=\"3\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\r\n </svg>\r\n }\r\n </button>\r\n }\r\n </div>\r\n }\r\n @case ('textarea') {\r\n <textarea\r\n class=\"fb-control\"\r\n [id]=\"fieldId(field)\"\r\n [formControl]=\"controlFor(field)\"\r\n [placeholder]=\"field.placeholder ?? ''\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n ></textarea>\r\n }\r\n @case ('select') {\r\n <select\r\n class=\"fb-control\"\r\n [id]=\"fieldId(field)\"\r\n [formControl]=\"controlFor(field)\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n >\r\n @for (option of optionsFor(field); track option.value) {\r\n <option [ngValue]=\"option.value\">{{ option.label }}</option>\r\n }\r\n </select>\r\n }\r\n @default {\r\n <input\r\n class=\"fb-control\"\r\n [id]=\"fieldId(field)\"\r\n [type]=\"field.type\"\r\n [formControl]=\"controlFor(field)\"\r\n [placeholder]=\"field.placeholder ?? ''\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n />\r\n }\r\n }\r\n\r\n @if (activeErrorFor(field); as message) {\r\n <p class=\"fb-error\" [id]=\"errorId(field)\">{{ message }}</p>\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n @if (!hideSubmit()) {\r\n <button type=\"submit\" class=\"fb-submit\" [disabled]=\"isSubmitDisabled()\">{{ submitLabel() ?? messages.submit() }}</button>\r\n }\r\n</form>\r\n", styles: [":host{display:block;font-family:var(--zhunam-font-family, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif);font-size:var(--zhunam-font-size, .9375rem);color:var(--zhunam-text, #1f2937)}.fb-form{display:flex;flex-direction:column;gap:var(--zhunam-spacing, 1rem);width:100%}.fb-fields-grid{display:grid;grid-template-columns:repeat(var(--fb-columns, 1),1fr);gap:var(--zhunam-spacing, 1rem)}.fb-field{display:flex;flex-direction:column;gap:calc(var(--zhunam-spacing, 1rem) / 2.5);min-width:0}.fb-field--span-2{grid-column:span 2}@media(max-width:640px){.fb-fields-grid{grid-template-columns:1fr}.fb-field--span-2{grid-column:span 1}}.fb-label{font-weight:600;color:var(--zhunam-text-secondary, #374151);padding:0;border:none}.fb-control{box-sizing:border-box;width:100%;font:inherit;color:inherit;padding:var(--zhunam-form-control-padding-y, .5rem) var(--zhunam-form-control-padding-x, .75rem);border:1px solid var(--zhunam-border, #d1d5db);border-radius:var(--zhunam-radius, .5rem);background:transparent;transition:border-color var(--zhunam-transition-duration, .15s) ease}.fb-control:focus-visible{outline:2px solid var(--zhunam-focus, var(--zhunam-primary, #3b82f6));outline-offset:-1px;border-color:var(--zhunam-focus, var(--zhunam-primary, #3b82f6))}textarea.fb-control{min-height:5rem;resize:vertical}select.fb-control{cursor:pointer}.fb-radio-group{display:flex;flex-direction:column;gap:calc(var(--zhunam-spacing, 1rem) / 2.5)}.fb-radio-option,.fb-checkbox-option{display:inline-flex;align-items:center;gap:.5rem;font-weight:400;color:var(--zhunam-text, #1f2937);cursor:pointer}.fb-radio-option input,.fb-checkbox-option input{accent-color:var(--zhunam-accent, var(--zhunam-primary, #3b82f6))}.fb-radio-option input:focus-visible,.fb-checkbox-option input:focus-visible{outline:2px solid var(--zhunam-focus, var(--zhunam-primary, #3b82f6));outline-offset:2px}.fb-password-wrapper{position:relative;display:flex}.fb-password-wrapper .fb-control{padding-right:calc(var(--zhunam-form-control-padding-x, .75rem) + 1.75rem)}.fb-password-toggle{appearance:none;position:absolute;top:50%;right:var(--zhunam-form-control-padding-x, .75rem);transform:translateY(-50%);display:inline-flex;align-items:center;justify-content:center;border:none;background:transparent;color:var(--zhunam-text-secondary, #374151);padding:0;cursor:pointer;line-height:0}.fb-password-toggle svg{width:1.125rem;height:1.125rem}.fb-password-toggle:focus-visible{outline:2px solid var(--zhunam-focus, var(--zhunam-primary, #3b82f6));outline-offset:2px}.fb-error{margin:0;font-size:.8125rem;color:var(--zhunam-error-text, var(--zhunam-error, #dc2626))}.fb-submit{appearance:none;align-self:flex-start;width:var(--zhunam-submit-width, auto);border:none;border-radius:var(--zhunam-radius, .5rem);background:var(--zhunam-primary, #3b82f6);color:var(--zhunam-primary-content, #fff);padding:var(--zhunam-form-control-padding-y, .5rem) 1.25rem;font:inherit;font-weight:600;cursor:pointer;transition:opacity var(--zhunam-transition-duration, .15s) ease}.fb-submit:focus-visible{outline:2px solid var(--zhunam-focus, var(--zhunam-primary, #3b82f6));outline-offset:2px}.fb-submit:disabled{opacity:.45;cursor:not-allowed}\n"] }]
|
|
566
|
+
}], ctorParameters: () => [], propDecorators: { fields: [{ type: i0.Input, args: [{ isSignal: true, alias: "fields", required: true }] }], columns: [{ type: i0.Input, args: [{ isSignal: true, alias: "columns", required: false }] }], crossFieldValidators: [{ type: i0.Input, args: [{ isSignal: true, alias: "crossFieldValidators", required: false }] }], formSubmit: [{ type: i0.Output, args: ["formSubmit"] }], mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], valueChange: [{ type: i0.Output, args: ["valueChange"] }], serverErrors: [{ type: i0.Input, args: [{ isSignal: true, alias: "serverErrors", required: false }] }], submitLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "submitLabel", required: false }] }], hideSubmit: [{ type: i0.Input, args: [{ isSignal: true, alias: "hideSubmit", required: false }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }] } });
|
|
386
567
|
|
|
387
568
|
/**
|
|
388
569
|
* NgModule wrapper for `FormBuilder<T>`, for consumers still on a
|
|
@@ -419,5 +600,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
|
|
|
419
600
|
* Generated bundle index. Do not edit.
|
|
420
601
|
*/
|
|
421
602
|
|
|
422
|
-
export { FormBuilder, FormBuilderModule };
|
|
603
|
+
export { FORM_BUILDER_MESSAGES, FORM_BUILDER_MESSAGES_EN, FORM_BUILDER_MESSAGES_ES, FormBuilder, FormBuilderModule, provideFormBuilderMessages };
|
|
423
604
|
//# sourceMappingURL=zhunam-form-builder.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"zhunam-form-builder.mjs","sources":["../../../../libs/form-builder/src/lib/utils/safe-pattern.ts","../../../../libs/form-builder/src/lib/form-builder/form-builder.ts","../../../../libs/form-builder/src/lib/form-builder/form-builder.html","../../../../libs/form-builder/src/lib/form-builder/form-builder.module.ts","../../../../libs/form-builder/src/zhunam-form-builder.ts"],"sourcesContent":["const MAX_PATTERN_LENGTH = 300;\r\n\r\n/** Matches a group that repeats another quantifier, e.g. `(x+)+`, `(x*)*`. */\r\nconst NESTED_QUANTIFIER = /\\([^()]*[+*][^()]*\\)[+*]/;\r\n\r\n/** Matches a repeated group so its alternated branches can be compared. */\r\nconst REPEATED_GROUP = /\\(([^()]*)\\)[+*]/g;\r\n\r\n/**\r\n * Validates that a regex `pattern` string is safe enough to compile into\r\n * a `RegExp` for form validation.\r\n *\r\n * This is a heuristic, not a mathematical guarantee of ReDoS-safety: it\r\n * rejects the pattern shapes most commonly responsible for catastrophic\r\n * backtracking (nested quantifiers, overlapping alternation inside a\r\n * repeated group) and enforces a length cap, but a maliciously crafted\r\n * pattern could still slip through undetected. Never accept `pattern`\r\n * values from a fully untrusted source without your own review.\r\n *\r\n * @throws {Error} If `pattern` is longer than 300 characters, or matches\r\n * a known catastrophic-backtracking shape.\r\n */\r\nexport function assertSafePattern(pattern: string): void {\r\n if (pattern.length > MAX_PATTERN_LENGTH) {\r\n throw new Error(\r\n `Unsafe regex pattern: exceeds the ${MAX_PATTERN_LENGTH}-character limit.`,\r\n );\r\n }\r\n\r\n if (NESTED_QUANTIFIER.test(pattern)) {\r\n throw new Error(\r\n 'Unsafe regex pattern: nested quantifiers detected (e.g. (x+)+), a known catastrophic-backtracking shape.',\r\n );\r\n }\r\n\r\n if (hasOverlappingAlternation(pattern)) {\r\n throw new Error(\r\n 'Unsafe regex pattern: overlapping alternation inside a repeated group detected (e.g. (a|a)+), a known catastrophic-backtracking shape.',\r\n );\r\n }\r\n}\r\n\r\nfunction hasOverlappingAlternation(pattern: string): boolean {\r\n const repeatedGroup = new RegExp(REPEATED_GROUP);\r\n let match: RegExpExecArray | null;\r\n\r\n while ((match = repeatedGroup.exec(pattern)) !== null) {\r\n const branches = match[1].split('|');\r\n for (let i = 0; i < branches.length; i++) {\r\n for (let j = i + 1; j < branches.length; j++) {\r\n if (branchesOverlap(branches[i], branches[j])) {\r\n return true;\r\n }\r\n }\r\n }\r\n }\r\n\r\n return false;\r\n}\r\n\r\nfunction branchesOverlap(a: string, b: string): boolean {\r\n if (a === b) return true;\r\n if (!a || !b) return false;\r\n return a.startsWith(b) || b.startsWith(a);\r\n}\r\n","// NUNCA usar [innerHTML], bypassSecurityTrustHtml, ni ningún mecanismo de inserción de HTML crudo en el DOM del componente.\r\n// label, placeholder, errorMessages, y FieldOption.label se renderizan siempre con interpolación {{ }} normal de Angular — nunca vía binding de propiedad que inserte HTML.\r\n\r\nimport {\r\n ChangeDetectionStrategy,\r\n Component,\r\n computed,\r\n effect,\r\n input,\r\n output,\r\n signal,\r\n ViewEncapsulation,\r\n} from '@angular/core';\r\nimport { FormControl, FormGroup, ReactiveFormsModule, ValidatorFn, Validators } from '@angular/forms';\r\nimport { CrossFieldValidator, FieldConfig, FieldOption } from '../models/field-config';\r\nimport { assertSafePattern } from '../utils/safe-pattern';\r\n\r\ntype ErrorKey = 'required' | 'min' | 'max' | 'minLength' | 'maxLength' | 'pattern' | 'email';\r\n\r\n// Angular's built-in validators report `minlength`/`maxlength` (all lowercase)\r\n// on `control.errors`, not the camelCase keys `FieldValidatorConfig.errorMessages`\r\n// uses — this bridges the two so consumer-facing config can stay camelCase.\r\nconst RAW_ERROR_KEY_MAP: Record<string, ErrorKey> = {\r\n required: 'required',\r\n min: 'min',\r\n max: 'max',\r\n minlength: 'minLength',\r\n maxlength: 'maxLength',\r\n pattern: 'pattern',\r\n email: 'email',\r\n};\r\n\r\nconst DEFAULT_ERROR_MESSAGES: Record<ErrorKey, string> = {\r\n required: 'Este campo es obligatorio.',\r\n min: 'El valor es menor al mínimo permitido.',\r\n max: 'El valor supera el máximo permitido.',\r\n minLength: 'El valor es demasiado corto.',\r\n maxLength: 'El valor es demasiado largo.',\r\n pattern: 'El formato no es válido.',\r\n email: 'Ingresá un email válido.',\r\n};\r\n\r\n@Component({\r\n selector: 'lib-form-builder',\r\n imports: [ReactiveFormsModule],\r\n templateUrl: './form-builder.html',\r\n styleUrl: './form-builder.scss',\r\n changeDetection: ChangeDetectionStrategy.OnPush,\r\n encapsulation: ViewEncapsulation.Emulated,\r\n})\r\nexport class FormBuilder<T> {\r\n /**\r\n * Declarative configuration of the fields to render, in display order.\r\n * @example\r\n * fields: FieldConfig<User>[] = [\r\n * { key: 'email', label: 'Email', type: 'email', validators: { required: true, email: true } },\r\n * ];\r\n */\r\n fields = input.required<FieldConfig<T>[]>();\r\n\r\n /**\r\n * Number of grid columns fields are laid out in on desktop. Always\r\n * collapses to a single column below a mobile breakpoint, regardless\r\n * of this value: mobile-first, like the rest of the library.\r\n * @default 1\r\n * @example\r\n * <lib-form-builder [fields]=\"fields\" [columns]=\"2\" />\r\n */\r\n columns = input<number>(1);\r\n\r\n /**\r\n * Form-level validators that check values across multiple fields (e.g.\r\n * confirming a password). Run separately from Angular's native\r\n * per-control validation, only when the native `FormGroup` is valid.\r\n * @default []\r\n * @example\r\n * crossFieldValidators: CrossFieldValidator<User>[] = [\r\n * {\r\n * validate: (value) =>\r\n * value.password !== value.confirmPassword\r\n * ? { confirmPassword: 'Passwords must match' }\r\n * : null,\r\n * },\r\n * ];\r\n */\r\n crossFieldValidators = input<CrossFieldValidator<T>[]>([]);\r\n\r\n /**\r\n * Emitted with the typed form values, only when both the native\r\n * `FormGroup` and every `crossFieldValidators` check pass.\r\n * @example\r\n * <lib-form-builder [fields]=\"fields\" (formSubmit)=\"onSubmit($event)\" />\r\n */\r\n formSubmit = output<T>();\r\n\r\n /**\r\n * Whether the form only reports its values on explicit submit, or\r\n * also continuously as the user edits it. `formSubmit` still fires\r\n * on submit in both modes; `valueChange` only fires when this is\r\n * `'live'`.\r\n * @default 'submit'\r\n * @example\r\n * <lib-form-builder [fields]=\"fields\" mode=\"live\" (valueChange)=\"onValueChange($event)\" />\r\n */\r\n mode = input<'submit' | 'live'>('submit');\r\n\r\n /**\r\n * Emitted with the typed, valid form values, only when `mode` is\r\n * `'live'` (never fires in the default `'submit'` mode). Fires once\r\n * immediately if the form starts valid with its default values (since\r\n * `FormGroup.valueChanges` never emits on its own at startup), then\r\n * again on every subsequent change, as long as both the native\r\n * `FormGroup` and every `crossFieldValidators` check keep passing —\r\n * the same validity criteria `formSubmit` already uses, just\r\n * continuous instead of submit-gated.\r\n * @example\r\n * <lib-form-builder [fields]=\"fields\" mode=\"live\" (valueChange)=\"onValueChange($event)\" />\r\n */\r\n valueChange = output<T>();\r\n\r\n /**\r\n * Errors returned by the backend after a submit attempt (e.g. \"email\r\n * already registered\"), keyed by field. Shown as soon as they're set,\r\n * regardless of whether the field has been touched or the form\r\n * submitted locally; receiving one implies a submit already\r\n * happened. Each message clears itself automatically the moment the\r\n * user edits that field again, so there's no need to clear this input\r\n * back to `{}` manually after showing it once.\r\n * @default {}\r\n * @example\r\n * // In the consumer's component:\r\n * protected readonly serverErrors = signal<Partial<Record<keyof User, string>>>({});\r\n *\r\n * onFormSubmit(value: User) {\r\n * this.api.register(value).subscribe({\r\n * error: () => this.serverErrors.set({ email: 'This email is already registered.' }),\r\n * });\r\n * }\r\n * // Template:\r\n * <lib-form-builder [fields]=\"fields\" [serverErrors]=\"serverErrors()\" (formSubmit)=\"onFormSubmit($event)\" />\r\n */\r\n serverErrors = input<Partial<Record<keyof T, string>>>({});\r\n\r\n protected readonly submitted = signal(false);\r\n protected readonly crossFieldErrors = signal<Record<string, string>>({});\r\n\r\n // Snapshot of each server-error field's value at the moment that error\r\n // appeared. A server error is only ever about a rejected backend\r\n // observation of the exact value the user submitted — the instant the\r\n // control's live value no longer matches this snapshot, the message is\r\n // stale and `serverErrorFor` stops showing it, without the consumer\r\n // having to clear `serverErrors` themselves.\r\n private readonly serverErrorSnapshot = signal<Partial<Record<keyof T, unknown>>>({});\r\n\r\n // `fields()` can change at runtime (e.g. a wizard swapping steps), so the\r\n // FormGroup is derived with `computed()` rather than built once in the\r\n // constructor: it's rebuilt only when the `fields()` array reference\r\n // changes. `computed()` fits better than an `effect()` here because\r\n // building a FormGroup is a pure derivation with no teardown to manage,\r\n // and `computed()` already memoizes it — the same instance is returned\r\n // across change-detection cycles until `fields()` itself changes.\r\n protected readonly formGroup = computed(() => this.buildFormGroup(this.fields()));\r\n\r\n private static instanceCounter = 0;\r\n private readonly instanceId = ++FormBuilder.instanceCounter;\r\n\r\n constructor() {\r\n // Runs whenever a new `serverErrors()` value comes in and captures\r\n // each mentioned field's current value — the baseline `serverErrorFor`\r\n // compares against to decide whether that message is still relevant.\r\n effect(() => {\r\n const errors = this.serverErrors();\r\n const group = this.formGroup();\r\n const snapshot: Partial<Record<keyof T, unknown>> = {};\r\n for (const key of Object.keys(errors) as (keyof T)[]) {\r\n snapshot[key] = group.get(String(key))?.value;\r\n }\r\n this.serverErrorSnapshot.set(snapshot);\r\n });\r\n\r\n // Early return keeps this a no-op entirely (no subscription, no\r\n // extra work) for the default 'submit' mode every existing consumer\r\n // is already on. Re-subscribes whenever formGroup() itself changes\r\n // (fields() changed), since that's a brand new FormGroup instance.\r\n effect((onCleanup) => {\r\n if (this.mode() !== 'live') {\r\n return;\r\n }\r\n const group = this.formGroup();\r\n\r\n const emitIfValid = () => {\r\n if (!group.valid) {\r\n return;\r\n }\r\n const value = this.coerceNumberFields(group.getRawValue());\r\n if (Object.keys(this.runCrossFieldValidators(value)).length > 0) {\r\n return;\r\n }\r\n this.valueChange.emit(value);\r\n };\r\n\r\n // FormGroup.valueChanges only fires on an actual value change, never\r\n // on its own at startup — emit once here so a 'live' consumer sees\r\n // the form's initial state immediately, not just after the user's\r\n // first edit.\r\n emitIfValid();\r\n\r\n const subscription = group.valueChanges.subscribe(emitIfValid);\r\n onCleanup(() => subscription.unsubscribe());\r\n });\r\n }\r\n\r\n private buildFormGroup(fields: FieldConfig<T>[]): FormGroup {\r\n const controls: Record<string, FormControl<unknown>> = {};\r\n for (const field of fields) {\r\n const initialValue = field.defaultValue ?? (field.type === 'checkbox' ? false : null);\r\n controls[String(field.key)] = new FormControl<unknown>(\r\n { value: initialValue, disabled: !!field.disabled },\r\n this.buildValidators(field),\r\n );\r\n }\r\n return new FormGroup(controls);\r\n }\r\n\r\n private buildValidators(field: FieldConfig<T>): ValidatorFn[] {\r\n const config = field.validators;\r\n if (!config) {\r\n return [];\r\n }\r\n\r\n const validators: ValidatorFn[] = [];\r\n if (config.required) {\r\n // A checkbox's \"empty\" value is `false`, which Validators.required\r\n // doesn't treat as empty — requiredTrue is the correct check for it.\r\n validators.push(field.type === 'checkbox' ? Validators.requiredTrue : Validators.required);\r\n }\r\n if (config.min !== undefined) {\r\n validators.push(Validators.min(config.min));\r\n }\r\n if (config.max !== undefined) {\r\n validators.push(Validators.max(config.max));\r\n }\r\n if (config.minLength !== undefined) {\r\n validators.push(Validators.minLength(config.minLength));\r\n }\r\n if (config.maxLength !== undefined) {\r\n validators.push(Validators.maxLength(config.maxLength));\r\n }\r\n if (config.pattern !== undefined) {\r\n // An unsafe pattern is a bug in the consumer's config, not a runtime\r\n // condition to recover from — let it throw instead of swallowing it.\r\n assertSafePattern(config.pattern);\r\n validators.push(Validators.pattern(config.pattern));\r\n }\r\n if (config.email) {\r\n validators.push(Validators.email);\r\n }\r\n return validators;\r\n }\r\n\r\n protected controlFor(field: FieldConfig<T>): FormControl {\r\n return this.formGroup().get(String(field.key)) as FormControl;\r\n }\r\n\r\n protected optionsFor(field: FieldConfig<T>): FieldOption[] {\r\n return field.options ?? [];\r\n }\r\n\r\n // Shared by the label's `for`, the control's `id`, and (for radio groups)\r\n // the `name` that keeps this field's radios from being grouped with\r\n // another field's — RadioControlValueAccessor matches radios by `name`\r\n // plus parent FormGroup, and every field here shares the same FormGroup.\r\n protected fieldId(field: FieldConfig<T>): string {\r\n return `fb-${this.instanceId}-${String(field.key)}`;\r\n }\r\n\r\n protected errorMessageFor(field: FieldConfig<T>): string | null {\r\n const control = this.controlFor(field);\r\n if (!control.errors || (!control.touched && !this.submitted())) {\r\n return null;\r\n }\r\n\r\n const errorKey = RAW_ERROR_KEY_MAP[Object.keys(control.errors)[0]];\r\n if (!errorKey) {\r\n return null;\r\n }\r\n\r\n return field.validators?.errorMessages?.[errorKey] ?? DEFAULT_ERROR_MESSAGES[errorKey];\r\n }\r\n\r\n protected serverErrorFor(field: FieldConfig<T>): string | null {\r\n const message = this.serverErrors()[field.key];\r\n if (!message) {\r\n return null;\r\n }\r\n\r\n const currentValue = this.controlFor(field).value;\r\n const snapshotValue = this.serverErrorSnapshot()[field.key];\r\n return currentValue === snapshotValue ? message : null;\r\n }\r\n\r\n protected crossFieldErrorFor(field: FieldConfig<T>): string | null {\r\n if (!this.submitted()) {\r\n return null;\r\n }\r\n return this.crossFieldErrors()[String(field.key)] ?? null;\r\n }\r\n\r\n // Single source of truth for \"is an error currently shown for this\r\n // field, and what does it say\" — both the visible `.fb-error` message\r\n // and its ARIA wiring (aria-invalid, aria-describedby) read from this,\r\n // so the two can never drift out of sync with each other.\r\n protected activeErrorFor(field: FieldConfig<T>): string | null {\r\n return this.errorMessageFor(field) ?? this.serverErrorFor(field) ?? this.crossFieldErrorFor(field);\r\n }\r\n\r\n protected errorId(field: FieldConfig<T>): string {\r\n return `${this.fieldId(field)}-error`;\r\n }\r\n\r\n protected legendId(field: FieldConfig<T>): string {\r\n return `${this.fieldId(field)}-legend`;\r\n }\r\n\r\n protected isSubmitDisabled(): boolean {\r\n return this.formGroup().invalid || Object.keys(this.crossFieldErrors()).length > 0;\r\n }\r\n\r\n protected onSubmit(): void {\r\n this.submitted.set(true);\r\n const group = this.formGroup();\r\n\r\n if (group.invalid) {\r\n return;\r\n }\r\n\r\n // `.value` excludes disabled controls entirely — `getRawValue()` keeps\r\n // them, so a disabled field with a `defaultValue` still comes through\r\n // as a complete `T` instead of a hole in the emitted object.\r\n const value = this.coerceNumberFields(group.getRawValue());\r\n const errors = this.runCrossFieldValidators(value);\r\n this.crossFieldErrors.set(errors);\r\n\r\n if (Object.keys(errors).length > 0) {\r\n return;\r\n }\r\n\r\n this.formSubmit.emit(value);\r\n }\r\n\r\n // Native `<input type=\"number\">` always reports its value as a string\r\n // through the DOM. Angular's NumberValueAccessor would normally parse it\r\n // for us, but that accessor only activates for a *static* `type=\"number\"`\r\n // attribute in the template — this component binds `[type]=\"field.type\"`\r\n // dynamically, so it never matches, and DefaultValueAccessor (string in,\r\n // string out) handles the control instead. Fixed up here, at the point\r\n // the value leaves the component, rather than in how the FormControl\r\n // stores it — Validators.min/max already parseFloat internally either\r\n // way, so correcting the stored value would change nothing there but\r\n // would still need this same conversion for the emitted object.\r\n private coerceNumberFields(rawValue: unknown): T {\r\n const value = { ...(rawValue as Record<string, unknown>) };\r\n for (const field of this.fields()) {\r\n if (field.type !== 'number') {\r\n continue;\r\n }\r\n\r\n const key = String(field.key);\r\n const current = value[key];\r\n if (current === '' || current === null || current === undefined) {\r\n value[key] = null;\r\n } else if (typeof current === 'string') {\r\n value[key] = Number(current);\r\n }\r\n }\r\n return value as T;\r\n }\r\n\r\n private runCrossFieldValidators(value: T): Record<string, string> {\r\n const errors: Record<string, string> = {};\r\n for (const validator of this.crossFieldValidators()) {\r\n const result = validator.validate(value);\r\n if (result) {\r\n Object.assign(errors, result);\r\n }\r\n }\r\n return errors;\r\n }\r\n}\r\n","<form class=\"fb-form\" [formGroup]=\"formGroup()\" (ngSubmit)=\"onSubmit()\" novalidate>\r\n <div class=\"fb-fields-grid\" [style.--fb-columns]=\"columns()\">\r\n @for (field of fields(); track field.key) {\r\n <div class=\"fb-field\" [class.fb-field--span-2]=\"field.colSpan === 2\">\r\n @if (field.type !== 'checkbox' && field.type !== 'radio') {\r\n <label class=\"fb-label\" [attr.for]=\"fieldId(field)\">{{ field.label }}</label>\r\n }\r\n\r\n @switch (field.type) {\r\n @case ('checkbox') {\r\n <label class=\"fb-checkbox-option\">\r\n <input\r\n type=\"checkbox\"\r\n [formControl]=\"controlFor(field)\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n />\r\n {{ field.label }}\r\n </label>\r\n }\r\n @case ('radio') {\r\n <fieldset\r\n class=\"fb-radio-group\"\r\n role=\"radiogroup\"\r\n [attr.aria-labelledby]=\"legendId(field)\"\r\n >\r\n <legend class=\"fb-label\" [id]=\"legendId(field)\">{{ field.label }}</legend>\r\n @for (option of optionsFor(field); track option.value) {\r\n <label class=\"fb-radio-option\">\r\n <input\r\n type=\"radio\"\r\n [formControl]=\"controlFor(field)\"\r\n [name]=\"fieldId(field)\"\r\n [value]=\"option.value\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n />\r\n {{ option.label }}\r\n </label>\r\n }\r\n </fieldset>\r\n }\r\n @case ('textarea') {\r\n <textarea\r\n class=\"fb-control\"\r\n [id]=\"fieldId(field)\"\r\n [formControl]=\"controlFor(field)\"\r\n [placeholder]=\"field.placeholder ?? ''\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n ></textarea>\r\n }\r\n @case ('select') {\r\n <select\r\n class=\"fb-control\"\r\n [id]=\"fieldId(field)\"\r\n [formControl]=\"controlFor(field)\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n >\r\n @for (option of optionsFor(field); track option.value) {\r\n <option [ngValue]=\"option.value\">{{ option.label }}</option>\r\n }\r\n </select>\r\n }\r\n @default {\r\n <input\r\n class=\"fb-control\"\r\n [id]=\"fieldId(field)\"\r\n [type]=\"field.type\"\r\n [formControl]=\"controlFor(field)\"\r\n [placeholder]=\"field.placeholder ?? ''\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n />\r\n }\r\n }\r\n\r\n @if (activeErrorFor(field); as message) {\r\n <p class=\"fb-error\" [id]=\"errorId(field)\">{{ message }}</p>\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n <button type=\"submit\" class=\"fb-submit\" [disabled]=\"isSubmitDisabled()\">Submit</button>\r\n</form>\r\n","import { NgModule } from '@angular/core';\r\nimport { FormBuilder } from './form-builder';\r\n\r\n/**\r\n * NgModule wrapper for `FormBuilder<T>`, for consumers still on a\r\n * classic NgModule architecture. `FormBuilder` is a standalone\r\n * component; importing it directly (`imports: [FormBuilder]`) is still\r\n * the recommended way to consume this library. Use this module only if\r\n * your app doesn't use standalone components yet.\r\n *\r\n * The generic type parameter `T` is inferred per usage from the bound\r\n * `[fields]` input, the same way it is with standalone consumption:\r\n * this module only changes how the component is imported, not how its\r\n * generic is resolved.\r\n *\r\n * @example\r\n * @NgModule({\r\n * imports: [FormBuilderModule],\r\n * })\r\n * export class SignupPageModule {}\r\n */\r\n@NgModule({\r\n imports: [FormBuilder],\r\n exports: [FormBuilder],\r\n})\r\nexport class FormBuilderModule {}\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"zhunam-form-builder.mjs","sources":["../../../../libs/form-builder/src/lib/utils/safe-pattern.ts","../../../../libs/form-builder/src/lib/models/form-builder-messages.ts","../../../../libs/form-builder/src/lib/tokens/form-builder-messages.token.ts","../../../../libs/form-builder/src/lib/form-builder/form-builder.ts","../../../../libs/form-builder/src/lib/form-builder/form-builder.html","../../../../libs/form-builder/src/lib/form-builder/form-builder.module.ts","../../../../libs/form-builder/src/zhunam-form-builder.ts"],"sourcesContent":["const MAX_PATTERN_LENGTH = 300;\r\n\r\n/** Matches a group that repeats another quantifier, e.g. `(x+)+`, `(x*)*`. */\r\nconst NESTED_QUANTIFIER = /\\([^()]*[+*][^()]*\\)[+*]/;\r\n\r\n/** Matches a repeated group so its alternated branches can be compared. */\r\nconst REPEATED_GROUP = /\\(([^()]*)\\)[+*]/g;\r\n\r\n/**\r\n * Validates that a regex `pattern` string is safe enough to compile into\r\n * a `RegExp` for form validation.\r\n *\r\n * This is a heuristic, not a mathematical guarantee of ReDoS-safety: it\r\n * rejects the pattern shapes most commonly responsible for catastrophic\r\n * backtracking (nested quantifiers, overlapping alternation inside a\r\n * repeated group) and enforces a length cap, but a maliciously crafted\r\n * pattern could still slip through undetected. Never accept `pattern`\r\n * values from a fully untrusted source without your own review.\r\n *\r\n * @throws {Error} If `pattern` is longer than 300 characters, or matches\r\n * a known catastrophic-backtracking shape.\r\n */\r\nexport function assertSafePattern(pattern: string): void {\r\n if (pattern.length > MAX_PATTERN_LENGTH) {\r\n throw new Error(\r\n `Unsafe regex pattern: exceeds the ${MAX_PATTERN_LENGTH}-character limit.`,\r\n );\r\n }\r\n\r\n if (NESTED_QUANTIFIER.test(pattern)) {\r\n throw new Error(\r\n 'Unsafe regex pattern: nested quantifiers detected (e.g. (x+)+), a known catastrophic-backtracking shape.',\r\n );\r\n }\r\n\r\n if (hasOverlappingAlternation(pattern)) {\r\n throw new Error(\r\n 'Unsafe regex pattern: overlapping alternation inside a repeated group detected (e.g. (a|a)+), a known catastrophic-backtracking shape.',\r\n );\r\n }\r\n}\r\n\r\nfunction hasOverlappingAlternation(pattern: string): boolean {\r\n const repeatedGroup = new RegExp(REPEATED_GROUP);\r\n let match: RegExpExecArray | null;\r\n\r\n while ((match = repeatedGroup.exec(pattern)) !== null) {\r\n const branches = match[1].split('|');\r\n for (let i = 0; i < branches.length; i++) {\r\n for (let j = i + 1; j < branches.length; j++) {\r\n if (branchesOverlap(branches[i], branches[j])) {\r\n return true;\r\n }\r\n }\r\n }\r\n }\r\n\r\n return false;\r\n}\r\n\r\nfunction branchesOverlap(a: string, b: string): boolean {\r\n if (a === b) return true;\r\n if (!a || !b) return false;\r\n return a.startsWith(b) || b.startsWith(a);\r\n}\r\n","/**\r\n * Translatable strings `FormBuilder` renders: validation messages and the\r\n * submit button label. Every member is a function, called fresh on every\r\n * render, so a message that reads a signal (e.g. a language switcher)\r\n * updates live without recreating the component.\r\n */\r\nexport interface FormBuilderMessages {\r\n /**\r\n * Shown when a required field is empty.\r\n */\r\n required(): string;\r\n\r\n /**\r\n * Shown when an `email` field doesn't hold a well-formed address.\r\n */\r\n email(): string;\r\n\r\n /**\r\n * Shown when a numeric field is below its configured `min`.\r\n * @param min The minimum allowed value, from Angular's own validation\r\n * error (`control.errors['min'].min`).\r\n */\r\n min(min: number): string;\r\n\r\n /**\r\n * Shown when a numeric field is above its configured `max`.\r\n * @param max The maximum allowed value, from Angular's own validation\r\n * error (`control.errors['max'].max`).\r\n */\r\n max(max: number): string;\r\n\r\n /**\r\n * Shown when a field's value is shorter than its configured `minLength`.\r\n * @param requiredLength The minimum required length, from Angular's own\r\n * validation error (`control.errors['minlength'].requiredLength`).\r\n */\r\n minLength(requiredLength: number): string;\r\n\r\n /**\r\n * Shown when a field's value is longer than its configured `maxLength`.\r\n * @param requiredLength The maximum required length, from Angular's own\r\n * validation error (`control.errors['maxlength'].requiredLength`).\r\n */\r\n maxLength(requiredLength: number): string;\r\n\r\n /**\r\n * Shown when a field's value doesn't match its configured `pattern`.\r\n */\r\n pattern(): string;\r\n\r\n /**\r\n * Label of the submit button.\r\n */\r\n submit(): string;\r\n\r\n /**\r\n * `aria-label` of the password visibility toggle when the field's\r\n * value is currently hidden (about to reveal it).\r\n */\r\n showPassword(): string;\r\n\r\n /**\r\n * `aria-label` of the password visibility toggle when the field's\r\n * value is currently shown (about to hide it).\r\n */\r\n hidePassword(): string;\r\n}\r\n\r\n/**\r\n * Default English messages. Used when no `FORM_BUILDER_MESSAGES` provider\r\n * is registered.\r\n */\r\nexport const FORM_BUILDER_MESSAGES_EN: FormBuilderMessages = {\r\n required: () => 'This field is required.',\r\n email: () => 'Enter a valid email address.',\r\n min: (min) => `The value must be at least ${min}.`,\r\n max: (max) => `The value must be at most ${max}.`,\r\n minLength: (requiredLength) => `Must be at least ${requiredLength} characters.`,\r\n maxLength: (requiredLength) => `Must be at most ${requiredLength} characters.`,\r\n pattern: () => 'The format is not valid.',\r\n submit: () => 'Submit',\r\n showPassword: () => 'Show password',\r\n hidePassword: () => 'Hide password',\r\n};\r\n\r\n/**\r\n * Spanish preset, ready to pass to `provideFormBuilderMessages()`.\r\n * @example\r\n * providers: [provideFormBuilderMessages(FORM_BUILDER_MESSAGES_ES)]\r\n */\r\nexport const FORM_BUILDER_MESSAGES_ES: FormBuilderMessages = {\r\n required: () => 'Este campo es obligatorio.',\r\n email: () => 'El email no es válido.',\r\n min: (min) => `El valor debe ser como mínimo ${min}.`,\r\n max: (max) => `El valor debe ser como máximo ${max}.`,\r\n minLength: (requiredLength) => `Debe tener al menos ${requiredLength} caracteres.`,\r\n maxLength: (requiredLength) => `Debe tener como máximo ${requiredLength} caracteres.`,\r\n pattern: () => 'El formato no es válido.',\r\n submit: () => 'Enviar',\r\n showPassword: () => 'Mostrar contraseña',\r\n hidePassword: () => 'Ocultar contraseña',\r\n};\r\n","import { InjectionToken, Provider } from '@angular/core';\r\nimport { FormBuilderMessages, FORM_BUILDER_MESSAGES_EN } from '../models/form-builder-messages';\r\n\r\n/**\r\n * DI token for the active `FormBuilderMessages` set. Defaults to\r\n * `FORM_BUILDER_MESSAGES_EN` when no provider is registered.\r\n *\r\n * A per-field `FieldValidatorConfig.errorMessages` override, when set,\r\n * still takes precedence over this token for that specific field: this\r\n * token supplies the message only when the field itself doesn't override\r\n * that validator's text.\r\n *\r\n * Prefer `provideFormBuilderMessages()` over providing this token\r\n * directly, it merges your overrides on top of the English defaults\r\n * instead of requiring every message to be re-specified.\r\n */\r\nexport const FORM_BUILDER_MESSAGES = new InjectionToken<FormBuilderMessages>('FORM_BUILDER_MESSAGES', {\r\n providedIn: 'root',\r\n factory: () => FORM_BUILDER_MESSAGES_EN,\r\n});\r\n\r\n/**\r\n * Registers a `FormBuilderMessages` override, merged on top of the\r\n * English defaults so a partial override (e.g. only `submit`) leaves the\r\n * rest in English instead of requiring the full set.\r\n *\r\n * @param overrides Messages to override. Any member left out falls back\r\n * to `FORM_BUILDER_MESSAGES_EN`.\r\n * @example\r\n * // Full preset:\r\n * providers: [provideFormBuilderMessages(FORM_BUILDER_MESSAGES_ES)]\r\n * @example\r\n * // Partial override, everything else stays in English:\r\n * providers: [provideFormBuilderMessages({ min: () => 'Must be greater than 0' })]\r\n */\r\nexport function provideFormBuilderMessages(overrides: Partial<FormBuilderMessages>): Provider {\r\n return {\r\n provide: FORM_BUILDER_MESSAGES,\r\n useValue: { ...FORM_BUILDER_MESSAGES_EN, ...overrides },\r\n };\r\n}\r\n","// NUNCA usar [innerHTML], bypassSecurityTrustHtml, ni ningún mecanismo de inserción de HTML crudo en el DOM del componente.\r\n// label, placeholder, errorMessages, y FieldOption.label se renderizan siempre con interpolación {{ }} normal de Angular, nunca vía binding de propiedad que inserte HTML.\r\n\r\nimport {\r\n ChangeDetectionStrategy,\r\n Component,\r\n effect,\r\n inject,\r\n input,\r\n linkedSignal,\r\n output,\r\n signal,\r\n untracked,\r\n ViewEncapsulation,\r\n} from '@angular/core';\r\nimport { FormControl, FormGroup, ReactiveFormsModule, ValidatorFn, Validators } from '@angular/forms';\r\nimport { CrossFieldValidator, FieldConfig, FieldOption } from '../models/field-config';\r\nimport { assertSafePattern } from '../utils/safe-pattern';\r\nimport { FORM_BUILDER_MESSAGES } from '../tokens/form-builder-messages.token';\r\n\r\ntype ErrorKey = 'required' | 'min' | 'max' | 'minLength' | 'maxLength' | 'pattern' | 'email';\r\n\r\n// Angular's built-in validators report `minlength`/`maxlength` (all lowercase)\r\n// on `control.errors`, not the camelCase keys `FieldValidatorConfig.errorMessages`\r\n// uses, this bridges the two so consumer-facing config can stay camelCase.\r\nconst RAW_ERROR_KEY_MAP: Record<string, ErrorKey> = {\r\n required: 'required',\r\n min: 'min',\r\n max: 'max',\r\n minlength: 'minLength',\r\n maxlength: 'maxLength',\r\n pattern: 'pattern',\r\n email: 'email',\r\n};\r\n\r\n@Component({\r\n selector: 'lib-form-builder',\r\n imports: [ReactiveFormsModule],\r\n templateUrl: './form-builder.html',\r\n styleUrl: './form-builder.scss',\r\n changeDetection: ChangeDetectionStrategy.OnPush,\r\n encapsulation: ViewEncapsulation.Emulated,\r\n})\r\nexport class FormBuilder<T> {\r\n /**\r\n * Declarative configuration of the fields to render, in display order.\r\n * @example\r\n * fields: FieldConfig<User>[] = [\r\n * { key: 'email', label: 'Email', type: 'email', validators: { required: true, email: true } },\r\n * ];\r\n */\r\n fields = input.required<FieldConfig<T>[]>();\r\n\r\n /**\r\n * Number of grid columns fields are laid out in on desktop. Always\r\n * collapses to a single column below a mobile breakpoint, regardless\r\n * of this value: mobile-first, like the rest of the library.\r\n * @default 1\r\n * @example\r\n * <lib-form-builder [fields]=\"fields\" [columns]=\"2\" />\r\n */\r\n columns = input<number>(1);\r\n\r\n /**\r\n * Form-level validators that check values across multiple fields (e.g.\r\n * confirming a password). Run separately from Angular's native\r\n * per-control validation, only when the native `FormGroup` is valid.\r\n * @default []\r\n * @example\r\n * crossFieldValidators: CrossFieldValidator<User>[] = [\r\n * {\r\n * validate: (value) =>\r\n * value.password !== value.confirmPassword\r\n * ? { confirmPassword: 'Passwords must match' }\r\n * : null,\r\n * },\r\n * ];\r\n */\r\n crossFieldValidators = input<CrossFieldValidator<T>[]>([]);\r\n\r\n /**\r\n * Emitted with the typed form values, only when both the native\r\n * `FormGroup` and every `crossFieldValidators` check pass.\r\n * @example\r\n * <lib-form-builder [fields]=\"fields\" (formSubmit)=\"onSubmit($event)\" />\r\n */\r\n formSubmit = output<T>();\r\n\r\n /**\r\n * Whether the form only reports its values on explicit submit, or\r\n * also continuously as the user edits it. `formSubmit` still fires\r\n * on submit in both modes; `valueChange` only fires when this is\r\n * `'live'`.\r\n * @default 'submit'\r\n * @example\r\n * <lib-form-builder [fields]=\"fields\" mode=\"live\" (valueChange)=\"onValueChange($event)\" />\r\n */\r\n mode = input<'submit' | 'live'>('submit');\r\n\r\n /**\r\n * Emitted with the typed, valid form values, only when `mode` is\r\n * `'live'` (never fires in the default `'submit'` mode). Fires once\r\n * immediately if the form starts valid with its default values (since\r\n * `FormGroup.valueChanges` never emits on its own at startup), then\r\n * again on every subsequent change, as long as both the native\r\n * `FormGroup` and every `crossFieldValidators` check keep passing —\r\n * the same validity criteria `formSubmit` already uses, just\r\n * continuous instead of submit-gated.\r\n * @example\r\n * <lib-form-builder [fields]=\"fields\" mode=\"live\" (valueChange)=\"onValueChange($event)\" />\r\n */\r\n valueChange = output<T>();\r\n\r\n /**\r\n * Errors returned by the backend after a submit attempt (e.g. \"email\r\n * already registered\"), keyed by field. Shown as soon as they're set,\r\n * regardless of whether the field has been touched or the form\r\n * submitted locally; receiving one implies a submit already\r\n * happened. Each message clears itself automatically the moment the\r\n * user edits that field again, so there's no need to clear this input\r\n * back to `{}` manually after showing it once.\r\n * @default {}\r\n * @example\r\n * // In the consumer's component:\r\n * protected readonly serverErrors = signal<Partial<Record<keyof User, string>>>({});\r\n *\r\n * onFormSubmit(value: User) {\r\n * this.api.register(value).subscribe({\r\n * error: () => this.serverErrors.set({ email: 'This email is already registered.' }),\r\n * });\r\n * }\r\n * // Template:\r\n * <lib-form-builder [fields]=\"fields\" [serverErrors]=\"serverErrors()\" (formSubmit)=\"onFormSubmit($event)\" />\r\n */\r\n serverErrors = input<Partial<Record<keyof T, string>>>({});\r\n\r\n /**\r\n * Custom label for the submit button, instead of the injected\r\n * FORM_BUILDER_MESSAGES `submit()` text. Takes precedence over both\r\n * the messages token and the default English label.\r\n * @example\r\n * <lib-form-builder [fields]=\"fields\" submitLabel=\"Create account\" />\r\n */\r\n submitLabel = input<string>();\r\n\r\n /**\r\n * Hides the internal submit button entirely, for a consumer that\r\n * triggers submission from its own UI instead, via the public\r\n * `submit()` method.\r\n * @default false\r\n * @example\r\n * <lib-form-builder #form [fields]=\"fields\" [hideSubmit]=\"true\" />\r\n * <button (click)=\"form.submit()\">Continue</button>\r\n */\r\n hideSubmit = input<boolean>(false);\r\n\r\n /**\r\n * Disables the internal submit button and makes the public `submit()`\r\n * method a no-op while `true`, e.g. during an in-flight request.\r\n * @default false\r\n * @example\r\n * <lib-form-builder [fields]=\"fields\" [loading]=\"saving()\" (formSubmit)=\"onSave($event)\" />\r\n */\r\n loading = input<boolean>(false);\r\n\r\n /**\r\n * External value applied to the form without recreating it. Every\r\n * time this receives a new, non-`undefined` value (including the\r\n * first one), it's patched into the current form\r\n * (`emitEvent: false`, so it never triggers `valueChange` in `'live'`\r\n * mode). Rebuilding `fields()` afterward keeps whatever was patched,\r\n * the same as any other value the user typed.\r\n * @example\r\n * <lib-form-builder [fields]=\"fields\" [value]=\"presetValue()\" />\r\n */\r\n value = input<T>();\r\n\r\n protected readonly messages = inject(FORM_BUILDER_MESSAGES);\r\n\r\n protected readonly submitted = signal(false);\r\n protected readonly crossFieldErrors = signal<Record<string, string>>({});\r\n\r\n // Keys of password fields currently showing plain text instead of\r\n // dots, toggled per-field by the user via the show/hide button.\r\n private readonly visiblePasswordFields = signal<ReadonlySet<string>>(new Set());\r\n\r\n // Snapshot of each server-error field's value at the moment that error\r\n // appeared. A server error is only ever about a rejected backend\r\n // observation of the exact value the user submitted — the instant the\r\n // control's live value no longer matches this snapshot, the message is\r\n // stale and `serverErrorFor` stops showing it, without the consumer\r\n // having to clear `serverErrors` themselves.\r\n private readonly serverErrorSnapshot = signal<Partial<Record<keyof T, unknown>>>({});\r\n\r\n // `fields()` can change at runtime (e.g. a wizard swapping steps), so the\r\n // FormGroup is derived rather than built once in the constructor: it's\r\n // rebuilt only when the `fields()` array reference changes. `linkedSignal`\r\n // (not `computed()`) so `buildFormGroup` can read the previous FormGroup\r\n // and carry forward the current value of every control whose key still\r\n // exists in the new `fields()`, instead of resetting it to `defaultValue`.\r\n protected readonly formGroup = linkedSignal<FieldConfig<T>[], FormGroup>({\r\n source: this.fields,\r\n computation: (fields, previous) => this.buildFormGroup(fields, previous?.value),\r\n });\r\n\r\n private static instanceCounter = 0;\r\n private readonly instanceId = ++FormBuilder.instanceCounter;\r\n\r\n constructor() {\r\n // Runs whenever a new `serverErrors()` value comes in and captures\r\n // each mentioned field's current value — the baseline `serverErrorFor`\r\n // compares against to decide whether that message is still relevant.\r\n effect(() => {\r\n const errors = this.serverErrors();\r\n const group = this.formGroup();\r\n const snapshot: Partial<Record<keyof T, unknown>> = {};\r\n for (const key of Object.keys(errors) as (keyof T)[]) {\r\n snapshot[key] = group.get(String(key))?.value;\r\n }\r\n this.serverErrorSnapshot.set(snapshot);\r\n });\r\n\r\n // Early return keeps this a no-op entirely (no subscription, no\r\n // extra work) for the default 'submit' mode every existing consumer\r\n // is already on. Re-subscribes whenever formGroup() itself changes\r\n // (fields() changed), since that's a brand new FormGroup instance.\r\n effect((onCleanup) => {\r\n if (this.mode() !== 'live') {\r\n return;\r\n }\r\n const group = this.formGroup();\r\n\r\n const emitIfValid = () => {\r\n if (!group.valid) {\r\n return;\r\n }\r\n const value = this.coerceNumberFields(group.getRawValue());\r\n if (Object.keys(this.runCrossFieldValidators(value)).length > 0) {\r\n return;\r\n }\r\n this.valueChange.emit(value);\r\n };\r\n\r\n // FormGroup.valueChanges only fires on an actual value change, never\r\n // on its own at startup — emit once here so a 'live' consumer sees\r\n // the form's initial state immediately, not just after the user's\r\n // first edit.\r\n emitIfValid();\r\n\r\n const subscription = group.valueChanges.subscribe(emitIfValid);\r\n onCleanup(() => subscription.unsubscribe());\r\n });\r\n\r\n // Applies an external `value()` to whichever FormGroup is current at\r\n // that moment, without recreating it. `formGroup()` is deliberately\r\n // read via `untracked()` so this effect only reacts to `value()`\r\n // itself changing, never to a `fields()`-triggered rebuild: `patchValue`\r\n // with `emitEvent: false` also means `valueChange` never fires from this.\r\n effect(() => {\r\n const value = this.value();\r\n if (value === undefined) {\r\n return;\r\n }\r\n const group = untracked(() => this.formGroup());\r\n group.patchValue(value as Record<string, unknown>, { emitEvent: false });\r\n });\r\n }\r\n\r\n private buildFormGroup(fields: FieldConfig<T>[], previous?: FormGroup): FormGroup {\r\n const controls: Record<string, FormControl<unknown>> = {};\r\n for (const field of fields) {\r\n const key = String(field.key);\r\n const previousControl = previous?.get(key);\r\n const initialValue = previousControl\r\n ? previousControl.value\r\n : (field.defaultValue ?? (field.type === 'checkbox' ? false : null));\r\n controls[key] = new FormControl<unknown>(\r\n { value: initialValue, disabled: !!field.disabled },\r\n this.buildValidators(field),\r\n );\r\n }\r\n return new FormGroup(controls);\r\n }\r\n\r\n private buildValidators(field: FieldConfig<T>): ValidatorFn[] {\r\n const config = field.validators;\r\n if (!config) {\r\n return [];\r\n }\r\n\r\n const validators: ValidatorFn[] = [];\r\n if (config.required) {\r\n // A checkbox's \"empty\" value is `false`, which Validators.required\r\n // doesn't treat as empty — requiredTrue is the correct check for it.\r\n validators.push(field.type === 'checkbox' ? Validators.requiredTrue : Validators.required);\r\n }\r\n if (config.min !== undefined) {\r\n validators.push(Validators.min(config.min));\r\n }\r\n if (config.max !== undefined) {\r\n validators.push(Validators.max(config.max));\r\n }\r\n if (config.minLength !== undefined) {\r\n validators.push(Validators.minLength(config.minLength));\r\n }\r\n if (config.maxLength !== undefined) {\r\n validators.push(Validators.maxLength(config.maxLength));\r\n }\r\n if (config.pattern !== undefined) {\r\n // An unsafe pattern is a bug in the consumer's config, not a runtime\r\n // condition to recover from — let it throw instead of swallowing it.\r\n assertSafePattern(config.pattern);\r\n validators.push(Validators.pattern(config.pattern));\r\n }\r\n if (config.email) {\r\n validators.push(Validators.email);\r\n }\r\n return validators;\r\n }\r\n\r\n protected controlFor(field: FieldConfig<T>): FormControl {\r\n return this.formGroup().get(String(field.key)) as FormControl;\r\n }\r\n\r\n protected optionsFor(field: FieldConfig<T>): FieldOption[] {\r\n return field.options ?? [];\r\n }\r\n\r\n protected showPasswordToggleFor(field: FieldConfig<T>): boolean {\r\n return field.showPasswordToggle ?? true;\r\n }\r\n\r\n protected isPasswordVisible(field: FieldConfig<T>): boolean {\r\n return this.visiblePasswordFields().has(String(field.key));\r\n }\r\n\r\n protected passwordInputType(field: FieldConfig<T>): 'text' | 'password' {\r\n return this.isPasswordVisible(field) ? 'text' : 'password';\r\n }\r\n\r\n protected togglePasswordVisibility(field: FieldConfig<T>): void {\r\n const key = String(field.key);\r\n const next = new Set(this.visiblePasswordFields());\r\n if (next.has(key)) {\r\n next.delete(key);\r\n } else {\r\n next.add(key);\r\n }\r\n this.visiblePasswordFields.set(next);\r\n }\r\n\r\n // Shared by the label's `for`, the control's `id`, and (for radio groups)\r\n // the `name` that keeps this field's radios from being grouped with\r\n // another field's — RadioControlValueAccessor matches radios by `name`\r\n // plus parent FormGroup, and every field here shares the same FormGroup.\r\n protected fieldId(field: FieldConfig<T>): string {\r\n return `fb-${this.instanceId}-${String(field.key)}`;\r\n }\r\n\r\n protected errorMessageFor(field: FieldConfig<T>): string | null {\r\n const control = this.controlFor(field);\r\n if (!control.errors || (!control.touched && !this.submitted())) {\r\n return null;\r\n }\r\n\r\n const rawKey = Object.keys(control.errors)[0];\r\n const errorKey = RAW_ERROR_KEY_MAP[rawKey];\r\n if (!errorKey) {\r\n return null;\r\n }\r\n\r\n // Precedence: a per-field override always wins, then the injected\r\n // FORM_BUILDER_MESSAGES token (English by default), evaluated fresh\r\n // here so a message reading a signal (e.g. a language switcher)\r\n // updates live.\r\n const fieldMessage = field.validators?.errorMessages?.[errorKey];\r\n if (fieldMessage) {\r\n return fieldMessage;\r\n }\r\n\r\n const errorDetail = control.errors[rawKey];\r\n switch (errorKey) {\r\n case 'required':\r\n return this.messages.required();\r\n case 'email':\r\n return this.messages.email();\r\n case 'min':\r\n return this.messages.min(errorDetail.min);\r\n case 'max':\r\n return this.messages.max(errorDetail.max);\r\n case 'minLength':\r\n return this.messages.minLength(errorDetail.requiredLength);\r\n case 'maxLength':\r\n return this.messages.maxLength(errorDetail.requiredLength);\r\n case 'pattern':\r\n return this.messages.pattern();\r\n }\r\n }\r\n\r\n protected serverErrorFor(field: FieldConfig<T>): string | null {\r\n const message = this.serverErrors()[field.key];\r\n if (!message) {\r\n return null;\r\n }\r\n\r\n const currentValue = this.controlFor(field).value;\r\n const snapshotValue = this.serverErrorSnapshot()[field.key];\r\n return currentValue === snapshotValue ? message : null;\r\n }\r\n\r\n protected crossFieldErrorFor(field: FieldConfig<T>): string | null {\r\n if (!this.submitted()) {\r\n return null;\r\n }\r\n return this.crossFieldErrors()[String(field.key)] ?? null;\r\n }\r\n\r\n // Single source of truth for \"is an error currently shown for this\r\n // field, and what does it say\": both the visible `.fb-error` message\r\n // and its ARIA wiring (aria-invalid, aria-describedby) read from this,\r\n // so the two can never drift out of sync with each other.\r\n protected activeErrorFor(field: FieldConfig<T>): string | null {\r\n return this.errorMessageFor(field) ?? this.serverErrorFor(field) ?? this.crossFieldErrorFor(field);\r\n }\r\n\r\n protected errorId(field: FieldConfig<T>): string {\r\n return `${this.fieldId(field)}-error`;\r\n }\r\n\r\n protected legendId(field: FieldConfig<T>): string {\r\n return `${this.fieldId(field)}-legend`;\r\n }\r\n\r\n protected isSubmitDisabled(): boolean {\r\n return this.loading() || this.formGroup().invalid || Object.keys(this.crossFieldErrors()).length > 0;\r\n }\r\n\r\n /**\r\n * Triggers the exact same submit logic the internal button triggers:\r\n * marks the form as submitted, runs validation, and emits `formSubmit`\r\n * if it passes. Works identically whether `hideSubmit` is `true` or\r\n * `false`, and is a no-op while `loading` is `true`.\r\n * @example\r\n * <lib-form-builder #form [fields]=\"fields\" [hideSubmit]=\"true\" />\r\n * <button (click)=\"form.submit()\">Continue</button>\r\n */\r\n submit(): void {\r\n this.performSubmit();\r\n }\r\n\r\n protected performSubmit(): void {\r\n if (this.loading()) {\r\n return;\r\n }\r\n\r\n this.submitted.set(true);\r\n const group = this.formGroup();\r\n\r\n if (group.invalid) {\r\n return;\r\n }\r\n\r\n // `.value` excludes disabled controls entirely, `getRawValue()` keeps\r\n // them, so a disabled field with a `defaultValue` still comes through\r\n // as a complete `T` instead of a hole in the emitted object.\r\n const value = this.coerceNumberFields(group.getRawValue());\r\n const errors = this.runCrossFieldValidators(value);\r\n this.crossFieldErrors.set(errors);\r\n\r\n if (Object.keys(errors).length > 0) {\r\n return;\r\n }\r\n\r\n this.formSubmit.emit(value);\r\n }\r\n\r\n // Native `<input type=\"number\">` always reports its value as a string\r\n // through the DOM. Angular's NumberValueAccessor would normally parse it\r\n // for us, but that accessor only activates for a *static* `type=\"number\"`\r\n // attribute in the template — this component binds `[type]=\"field.type\"`\r\n // dynamically, so it never matches, and DefaultValueAccessor (string in,\r\n // string out) handles the control instead. Fixed up here, at the point\r\n // the value leaves the component, rather than in how the FormControl\r\n // stores it: Validators.min/max already parseFloat internally either\r\n // way, so correcting the stored value would change nothing there but\r\n // would still need this same conversion for the emitted object.\r\n private coerceNumberFields(rawValue: unknown): T {\r\n const value = { ...(rawValue as Record<string, unknown>) };\r\n for (const field of this.fields()) {\r\n if (field.type !== 'number') {\r\n continue;\r\n }\r\n\r\n const key = String(field.key);\r\n const current = value[key];\r\n if (current === '' || current === null || current === undefined) {\r\n value[key] = null;\r\n } else if (typeof current === 'string') {\r\n value[key] = Number(current);\r\n }\r\n }\r\n return value as T;\r\n }\r\n\r\n private runCrossFieldValidators(value: T): Record<string, string> {\r\n const errors: Record<string, string> = {};\r\n for (const validator of this.crossFieldValidators()) {\r\n const result = validator.validate(value);\r\n if (result) {\r\n Object.assign(errors, result);\r\n }\r\n }\r\n return errors;\r\n }\r\n}\r\n","<form class=\"fb-form\" [formGroup]=\"formGroup()\" (ngSubmit)=\"performSubmit()\" novalidate>\r\n <div class=\"fb-fields-grid\" [style.--fb-columns]=\"columns()\">\r\n @for (field of fields(); track field.key) {\r\n <div class=\"fb-field\" [class.fb-field--span-2]=\"field.colSpan === 2\">\r\n @if (field.type !== 'checkbox' && field.type !== 'radio') {\r\n <label class=\"fb-label\" [attr.for]=\"fieldId(field)\">{{ field.label }}</label>\r\n }\r\n\r\n @switch (field.type) {\r\n @case ('checkbox') {\r\n <label class=\"fb-checkbox-option\">\r\n <input\r\n type=\"checkbox\"\r\n [formControl]=\"controlFor(field)\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n />\r\n {{ field.label }}\r\n </label>\r\n }\r\n @case ('radio') {\r\n <fieldset\r\n class=\"fb-radio-group\"\r\n role=\"radiogroup\"\r\n [attr.aria-labelledby]=\"legendId(field)\"\r\n >\r\n <legend class=\"fb-label\" [id]=\"legendId(field)\">{{ field.label }}</legend>\r\n @for (option of optionsFor(field); track option.value) {\r\n <label class=\"fb-radio-option\">\r\n <input\r\n type=\"radio\"\r\n [formControl]=\"controlFor(field)\"\r\n [name]=\"fieldId(field)\"\r\n [value]=\"option.value\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n />\r\n {{ option.label }}\r\n </label>\r\n }\r\n </fieldset>\r\n }\r\n @case ('password') {\r\n <div class=\"fb-password-wrapper\">\r\n <input\r\n class=\"fb-control\"\r\n [id]=\"fieldId(field)\"\r\n [type]=\"passwordInputType(field)\"\r\n [formControl]=\"controlFor(field)\"\r\n [placeholder]=\"field.placeholder ?? ''\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n />\r\n @if (showPasswordToggleFor(field)) {\r\n <button\r\n type=\"button\"\r\n class=\"fb-password-toggle\"\r\n [attr.aria-pressed]=\"isPasswordVisible(field)\"\r\n [attr.aria-label]=\"isPasswordVisible(field) ? messages.hidePassword() : messages.showPassword()\"\r\n (click)=\"togglePasswordVisibility(field)\"\r\n >\r\n @if (isPasswordVisible(field)) {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" aria-hidden=\"true\">\r\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3 3l18 18M10.58 10.58a2 2 0 002.83 2.83M9.88 4.24A9.9 9.9 0 0112 4c5 0 9 4 10 8-.32 1.12-.86 2.19-1.57 3.14M6.6 6.6C3.9 8.14 2.06 10.6 2 12c.66 2.34 2.19 4.34 4.19 5.68M14.12 14.12A3 3 0 019.88 9.88\" />\r\n </svg>\r\n } @else {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" aria-hidden=\"true\">\r\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z\" />\r\n <circle cx=\"12\" cy=\"12\" r=\"3\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\r\n </svg>\r\n }\r\n </button>\r\n }\r\n </div>\r\n }\r\n @case ('textarea') {\r\n <textarea\r\n class=\"fb-control\"\r\n [id]=\"fieldId(field)\"\r\n [formControl]=\"controlFor(field)\"\r\n [placeholder]=\"field.placeholder ?? ''\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n ></textarea>\r\n }\r\n @case ('select') {\r\n <select\r\n class=\"fb-control\"\r\n [id]=\"fieldId(field)\"\r\n [formControl]=\"controlFor(field)\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n >\r\n @for (option of optionsFor(field); track option.value) {\r\n <option [ngValue]=\"option.value\">{{ option.label }}</option>\r\n }\r\n </select>\r\n }\r\n @default {\r\n <input\r\n class=\"fb-control\"\r\n [id]=\"fieldId(field)\"\r\n [type]=\"field.type\"\r\n [formControl]=\"controlFor(field)\"\r\n [placeholder]=\"field.placeholder ?? ''\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n />\r\n }\r\n }\r\n\r\n @if (activeErrorFor(field); as message) {\r\n <p class=\"fb-error\" [id]=\"errorId(field)\">{{ message }}</p>\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n @if (!hideSubmit()) {\r\n <button type=\"submit\" class=\"fb-submit\" [disabled]=\"isSubmitDisabled()\">{{ submitLabel() ?? messages.submit() }}</button>\r\n }\r\n</form>\r\n","import { NgModule } from '@angular/core';\r\nimport { FormBuilder } from './form-builder';\r\n\r\n/**\r\n * NgModule wrapper for `FormBuilder<T>`, for consumers still on a\r\n * classic NgModule architecture. `FormBuilder` is a standalone\r\n * component; importing it directly (`imports: [FormBuilder]`) is still\r\n * the recommended way to consume this library. Use this module only if\r\n * your app doesn't use standalone components yet.\r\n *\r\n * The generic type parameter `T` is inferred per usage from the bound\r\n * `[fields]` input, the same way it is with standalone consumption:\r\n * this module only changes how the component is imported, not how its\r\n * generic is resolved.\r\n *\r\n * @example\r\n * @NgModule({\r\n * imports: [FormBuilderModule],\r\n * })\r\n * export class SignupPageModule {}\r\n */\r\n@NgModule({\r\n imports: [FormBuilder],\r\n exports: [FormBuilder],\r\n})\r\nexport class FormBuilderModule {}\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;;AAAA,MAAM,kBAAkB,GAAG,GAAG;AAE9B;AACA,MAAM,iBAAiB,GAAG,0BAA0B;AAEpD;AACA,MAAM,cAAc,GAAG,mBAAmB;AAE1C;;;;;;;;;;;;;AAaG;AACG,SAAU,iBAAiB,CAAC,OAAe,EAAA;AAC/C,IAAA,IAAI,OAAO,CAAC,MAAM,GAAG,kBAAkB,EAAE;AACvC,QAAA,MAAM,IAAI,KAAK,CACb,qCAAqC,kBAAkB,CAAA,iBAAA,CAAmB,CAC3E;IACH;AAEA,IAAA,IAAI,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE;AACnC,QAAA,MAAM,IAAI,KAAK,CACb,0GAA0G,CAC3G;IACH;AAEA,IAAA,IAAI,yBAAyB,CAAC,OAAO,CAAC,EAAE;AACtC,QAAA,MAAM,IAAI,KAAK,CACb,wIAAwI,CACzI;IACH;AACF;AAEA,SAAS,yBAAyB,CAAC,OAAe,EAAA;AAChD,IAAA,MAAM,aAAa,GAAG,IAAI,MAAM,CAAC,cAAc,CAAC;AAChD,IAAA,IAAI,KAA6B;AAEjC,IAAA,OAAO,CAAC,KAAK,GAAG,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE;QACrD,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC;AACpC,QAAA,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;AACxC,YAAA,KAAK,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;AAC5C,gBAAA,IAAI,eAAe,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE;AAC7C,oBAAA,OAAO,IAAI;gBACb;YACF;QACF;IACF;AAEA,IAAA,OAAO,KAAK;AACd;AAEA,SAAS,eAAe,CAAC,CAAS,EAAE,CAAS,EAAA;IAC3C,IAAI,CAAC,KAAK,CAAC;AAAE,QAAA,OAAO,IAAI;AACxB,IAAA,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;AAAE,QAAA,OAAO,KAAK;AAC1B,IAAA,OAAO,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;AAC3C;;ACIA;;;AAGG;AACI,MAAM,wBAAwB,GAAwB;AAC3D,IAAA,QAAQ,EAAE,MAAM,yBAAyB;AACzC,IAAA,KAAK,EAAE,MAAM,8BAA8B;IAC3C,GAAG,EAAE,CAAC,GAAG,KAAK,CAAA,2BAAA,EAA8B,GAAG,CAAA,CAAA,CAAG;IAClD,GAAG,EAAE,CAAC,GAAG,KAAK,CAAA,0BAAA,EAA6B,GAAG,CAAA,CAAA,CAAG;IACjD,SAAS,EAAE,CAAC,cAAc,KAAK,CAAA,iBAAA,EAAoB,cAAc,CAAA,YAAA,CAAc;IAC/E,SAAS,EAAE,CAAC,cAAc,KAAK,CAAA,gBAAA,EAAmB,cAAc,CAAA,YAAA,CAAc;AAC9E,IAAA,OAAO,EAAE,MAAM,0BAA0B;AACzC,IAAA,MAAM,EAAE,MAAM,QAAQ;AACtB,IAAA,YAAY,EAAE,MAAM,eAAe;AACnC,IAAA,YAAY,EAAE,MAAM,eAAe;;AAGrC;;;;AAIG;AACI,MAAM,wBAAwB,GAAwB;AAC3D,IAAA,QAAQ,EAAE,MAAM,4BAA4B;AAC5C,IAAA,KAAK,EAAE,MAAM,wBAAwB;IACrC,GAAG,EAAE,CAAC,GAAG,KAAK,CAAA,8BAAA,EAAiC,GAAG,CAAA,CAAA,CAAG;IACrD,GAAG,EAAE,CAAC,GAAG,KAAK,CAAA,8BAAA,EAAiC,GAAG,CAAA,CAAA,CAAG;IACrD,SAAS,EAAE,CAAC,cAAc,KAAK,CAAA,oBAAA,EAAuB,cAAc,CAAA,YAAA,CAAc;IAClF,SAAS,EAAE,CAAC,cAAc,KAAK,CAAA,uBAAA,EAA0B,cAAc,CAAA,YAAA,CAAc;AACrF,IAAA,OAAO,EAAE,MAAM,0BAA0B;AACzC,IAAA,MAAM,EAAE,MAAM,QAAQ;AACtB,IAAA,YAAY,EAAE,MAAM,oBAAoB;AACxC,IAAA,YAAY,EAAE,MAAM,oBAAoB;;;ACjG1C;;;;;;;;;;;;AAYG;MACU,qBAAqB,GAAG,IAAI,cAAc,CAAsB,uBAAuB,EAAE;AACpG,IAAA,UAAU,EAAE,MAAM;AAClB,IAAA,OAAO,EAAE,MAAM,wBAAwB;AACxC,CAAA;AAED;;;;;;;;;;;;;AAaG;AACG,SAAU,0BAA0B,CAAC,SAAuC,EAAA;IAChF,OAAO;AACL,QAAA,OAAO,EAAE,qBAAqB;AAC9B,QAAA,QAAQ,EAAE,EAAE,GAAG,wBAAwB,EAAE,GAAG,SAAS,EAAE;KACxD;AACH;;ACxCA;AACA;AAqBA;AACA;AACA;AACA,MAAM,iBAAiB,GAA6B;AAClD,IAAA,QAAQ,EAAE,UAAU;AACpB,IAAA,GAAG,EAAE,KAAK;AACV,IAAA,GAAG,EAAE,KAAK;AACV,IAAA,SAAS,EAAE,WAAW;AACtB,IAAA,SAAS,EAAE,WAAW;AACtB,IAAA,OAAO,EAAE,SAAS;AAClB,IAAA,KAAK,EAAE,OAAO;CACf;MAUY,WAAW,CAAA;AACtB;;;;;;AAMG;AACH,IAAA,MAAM,GAAG,KAAK,CAAC,QAAQ,4EAAoB;AAE3C;;;;;;;AAOG;AACH,IAAA,OAAO,GAAG,KAAK,CAAS,CAAC,8EAAC;AAE1B;;;;;;;;;;;;;;AAcG;AACH,IAAA,oBAAoB,GAAG,KAAK,CAA2B,EAAE,2FAAC;AAE1D;;;;;AAKG;IACH,UAAU,GAAG,MAAM,EAAK;AAExB;;;;;;;;AAQG;AACH,IAAA,IAAI,GAAG,KAAK,CAAoB,QAAQ,2EAAC;AAEzC;;;;;;;;;;;AAWG;IACH,WAAW,GAAG,MAAM,EAAK;AAEzB;;;;;;;;;;;;;;;;;;;;AAoBG;AACH,IAAA,YAAY,GAAG,KAAK,CAAmC,EAAE,mFAAC;AAE1D;;;;;;AAMG;IACH,WAAW,GAAG,KAAK,CAAA,IAAA,SAAA,GAAA,CAAA,SAAA,EAAA,EAAA,SAAA,EAAA,aAAA,EAAA,CAAA,8BAAA,EAAA,CAAA,CAAU;AAE7B;;;;;;;;AAQG;AACH,IAAA,UAAU,GAAG,KAAK,CAAU,KAAK,iFAAC;AAElC;;;;;;AAMG;AACH,IAAA,OAAO,GAAG,KAAK,CAAU,KAAK,8EAAC;AAE/B;;;;;;;;;AASG;IACH,KAAK,GAAG,KAAK,CAAA,IAAA,SAAA,GAAA,CAAA,SAAA,EAAA,EAAA,SAAA,EAAA,OAAA,EAAA,CAAA,8BAAA,EAAA,CAAA,CAAK;AAEC,IAAA,QAAQ,GAAG,MAAM,CAAC,qBAAqB,CAAC;AAExC,IAAA,SAAS,GAAG,MAAM,CAAC,KAAK,gFAAC;AACzB,IAAA,gBAAgB,GAAG,MAAM,CAAyB,EAAE,uFAAC;;;AAIvD,IAAA,qBAAqB,GAAG,MAAM,CAAsB,IAAI,GAAG,EAAE,4FAAC;;;;;;;AAQ9D,IAAA,mBAAmB,GAAG,MAAM,CAAoC,EAAE,0FAAC;;;;;;;AAQjE,IAAA,SAAS,GAAG,YAAY,CAAA,EAAA,IAAA,SAAA,GAAA,EAAA,SAAA,EAAA,WAAA,EAAA,8BAAA,EAAA,CAAA,EACzC,MAAM,EAAE,IAAI,CAAC,MAAM;AACnB,QAAA,WAAW,EAAE,CAAC,MAAM,EAAE,QAAQ,KAAK,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,QAAQ,EAAE,KAAK,CAAC,GAC/E;AAEM,IAAA,OAAO,eAAe,GAAG,CAAC;AACjB,IAAA,UAAU,GAAG,EAAE,WAAW,CAAC,eAAe;AAE3D,IAAA,WAAA,GAAA;;;;QAIE,MAAM,CAAC,MAAK;AACV,YAAA,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,EAAE;AAClC,YAAA,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,EAAE;YAC9B,MAAM,QAAQ,GAAsC,EAAE;YACtD,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAgB,EAAE;AACpD,gBAAA,QAAQ,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK;YAC/C;AACA,YAAA,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,QAAQ,CAAC;AACxC,QAAA,CAAC,CAAC;;;;;AAMF,QAAA,MAAM,CAAC,CAAC,SAAS,KAAI;AACnB,YAAA,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,MAAM,EAAE;gBAC1B;YACF;AACA,YAAA,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,EAAE;YAE9B,MAAM,WAAW,GAAG,MAAK;AACvB,gBAAA,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE;oBAChB;gBACF;gBACA,MAAM,KAAK,GAAG,IAAI,CAAC,kBAAkB,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;AAC1D,gBAAA,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,uBAAuB,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE;oBAC/D;gBACF;AACA,gBAAA,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC;AAC9B,YAAA,CAAC;;;;;AAMD,YAAA,WAAW,EAAE;YAEb,MAAM,YAAY,GAAG,KAAK,CAAC,YAAY,CAAC,SAAS,CAAC,WAAW,CAAC;YAC9D,SAAS,CAAC,MAAM,YAAY,CAAC,WAAW,EAAE,CAAC;AAC7C,QAAA,CAAC,CAAC;;;;;;QAOF,MAAM,CAAC,MAAK;AACV,YAAA,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,EAAE;AAC1B,YAAA,IAAI,KAAK,KAAK,SAAS,EAAE;gBACvB;YACF;AACA,YAAA,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC;YAC/C,KAAK,CAAC,UAAU,CAAC,KAAgC,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;AAC1E,QAAA,CAAC,CAAC;IACJ;IAEQ,cAAc,CAAC,MAAwB,EAAE,QAAoB,EAAA;QACnE,MAAM,QAAQ,GAAyC,EAAE;AACzD,QAAA,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE;YAC1B,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC;YAC7B,MAAM,eAAe,GAAG,QAAQ,EAAE,GAAG,CAAC,GAAG,CAAC;YAC1C,MAAM,YAAY,GAAG;kBACjB,eAAe,CAAC;mBACf,KAAK,CAAC,YAAY,KAAK,KAAK,CAAC,IAAI,KAAK,UAAU,GAAG,KAAK,GAAG,IAAI,CAAC,CAAC;AACtE,YAAA,QAAQ,CAAC,GAAG,CAAC,GAAG,IAAI,WAAW,CAC7B,EAAE,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,QAAQ,EAAE,EACnD,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,CAC5B;QACH;AACA,QAAA,OAAO,IAAI,SAAS,CAAC,QAAQ,CAAC;IAChC;AAEQ,IAAA,eAAe,CAAC,KAAqB,EAAA;AAC3C,QAAA,MAAM,MAAM,GAAG,KAAK,CAAC,UAAU;QAC/B,IAAI,CAAC,MAAM,EAAE;AACX,YAAA,OAAO,EAAE;QACX;QAEA,MAAM,UAAU,GAAkB,EAAE;AACpC,QAAA,IAAI,MAAM,CAAC,QAAQ,EAAE;;;YAGnB,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,UAAU,GAAG,UAAU,CAAC,YAAY,GAAG,UAAU,CAAC,QAAQ,CAAC;QAC5F;AACA,QAAA,IAAI,MAAM,CAAC,GAAG,KAAK,SAAS,EAAE;AAC5B,YAAA,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC7C;AACA,QAAA,IAAI,MAAM,CAAC,GAAG,KAAK,SAAS,EAAE;AAC5B,YAAA,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC7C;AACA,QAAA,IAAI,MAAM,CAAC,SAAS,KAAK,SAAS,EAAE;AAClC,YAAA,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QACzD;AACA,QAAA,IAAI,MAAM,CAAC,SAAS,KAAK,SAAS,EAAE;AAClC,YAAA,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QACzD;AACA,QAAA,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS,EAAE;;;AAGhC,YAAA,iBAAiB,CAAC,MAAM,CAAC,OAAO,CAAC;AACjC,YAAA,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACrD;AACA,QAAA,IAAI,MAAM,CAAC,KAAK,EAAE;AAChB,YAAA,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;QACnC;AACA,QAAA,OAAO,UAAU;IACnB;AAEU,IAAA,UAAU,CAAC,KAAqB,EAAA;AACxC,QAAA,OAAO,IAAI,CAAC,SAAS,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAgB;IAC/D;AAEU,IAAA,UAAU,CAAC,KAAqB,EAAA;AACxC,QAAA,OAAO,KAAK,CAAC,OAAO,IAAI,EAAE;IAC5B;AAEU,IAAA,qBAAqB,CAAC,KAAqB,EAAA;AACnD,QAAA,OAAO,KAAK,CAAC,kBAAkB,IAAI,IAAI;IACzC;AAEU,IAAA,iBAAiB,CAAC,KAAqB,EAAA;AAC/C,QAAA,OAAO,IAAI,CAAC,qBAAqB,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC5D;AAEU,IAAA,iBAAiB,CAAC,KAAqB,EAAA;AAC/C,QAAA,OAAO,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,GAAG,MAAM,GAAG,UAAU;IAC5D;AAEU,IAAA,wBAAwB,CAAC,KAAqB,EAAA;QACtD,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC;QAC7B,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,qBAAqB,EAAE,CAAC;AAClD,QAAA,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE;AACjB,YAAA,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC;QAClB;aAAO;AACL,YAAA,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;QACf;AACA,QAAA,IAAI,CAAC,qBAAqB,CAAC,GAAG,CAAC,IAAI,CAAC;IACtC;;;;;AAMU,IAAA,OAAO,CAAC,KAAqB,EAAA;AACrC,QAAA,OAAO,CAAA,GAAA,EAAM,IAAI,CAAC,UAAU,CAAA,CAAA,EAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA,CAAE;IACrD;AAEU,IAAA,eAAe,CAAC,KAAqB,EAAA;QAC7C,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;AACtC,QAAA,IAAI,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,OAAO,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,EAAE;AAC9D,YAAA,OAAO,IAAI;QACb;AAEA,QAAA,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AAC7C,QAAA,MAAM,QAAQ,GAAG,iBAAiB,CAAC,MAAM,CAAC;QAC1C,IAAI,CAAC,QAAQ,EAAE;AACb,YAAA,OAAO,IAAI;QACb;;;;;QAMA,MAAM,YAAY,GAAG,KAAK,CAAC,UAAU,EAAE,aAAa,GAAG,QAAQ,CAAC;QAChE,IAAI,YAAY,EAAE;AAChB,YAAA,OAAO,YAAY;QACrB;QAEA,MAAM,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC;QAC1C,QAAQ,QAAQ;AACd,YAAA,KAAK,UAAU;AACb,gBAAA,OAAO,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE;AACjC,YAAA,KAAK,OAAO;AACV,gBAAA,OAAO,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE;AAC9B,YAAA,KAAK,KAAK;gBACR,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC;AAC3C,YAAA,KAAK,KAAK;gBACR,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC;AAC3C,YAAA,KAAK,WAAW;gBACd,OAAO,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,WAAW,CAAC,cAAc,CAAC;AAC5D,YAAA,KAAK,WAAW;gBACd,OAAO,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,WAAW,CAAC,cAAc,CAAC;AAC5D,YAAA,KAAK,SAAS;AACZ,gBAAA,OAAO,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE;;IAEpC;AAEU,IAAA,cAAc,CAAC,KAAqB,EAAA;QAC5C,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC;QAC9C,IAAI,CAAC,OAAO,EAAE;AACZ,YAAA,OAAO,IAAI;QACb;QAEA,MAAM,YAAY,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,KAAK;QACjD,MAAM,aAAa,GAAG,IAAI,CAAC,mBAAmB,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC;QAC3D,OAAO,YAAY,KAAK,aAAa,GAAG,OAAO,GAAG,IAAI;IACxD;AAEU,IAAA,kBAAkB,CAAC,KAAqB,EAAA;AAChD,QAAA,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE;AACrB,YAAA,OAAO,IAAI;QACb;AACA,QAAA,OAAO,IAAI,CAAC,gBAAgB,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,IAAI;IAC3D;;;;;AAMU,IAAA,cAAc,CAAC,KAAqB,EAAA;QAC5C,OAAO,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,kBAAkB,CAAC,KAAK,CAAC;IACpG;AAEU,IAAA,OAAO,CAAC,KAAqB,EAAA;QACrC,OAAO,CAAA,EAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ;IACvC;AAEU,IAAA,QAAQ,CAAC,KAAqB,EAAA;QACtC,OAAO,CAAA,EAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS;IACxC;IAEU,gBAAgB,GAAA;QACxB,OAAO,IAAI,CAAC,OAAO,EAAE,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC;IACtG;AAEA;;;;;;;;AAQG;IACH,MAAM,GAAA;QACJ,IAAI,CAAC,aAAa,EAAE;IACtB;IAEU,aAAa,GAAA;AACrB,QAAA,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE;YAClB;QACF;AAEA,QAAA,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC;AACxB,QAAA,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,EAAE;AAE9B,QAAA,IAAI,KAAK,CAAC,OAAO,EAAE;YACjB;QACF;;;;QAKA,MAAM,KAAK,GAAG,IAAI,CAAC,kBAAkB,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;QAC1D,MAAM,MAAM,GAAG,IAAI,CAAC,uBAAuB,CAAC,KAAK,CAAC;AAClD,QAAA,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC;QAEjC,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE;YAClC;QACF;AAEA,QAAA,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC;IAC7B;;;;;;;;;;;AAYQ,IAAA,kBAAkB,CAAC,QAAiB,EAAA;AAC1C,QAAA,MAAM,KAAK,GAAG,EAAE,GAAI,QAAoC,EAAE;QAC1D,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE,EAAE;AACjC,YAAA,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,EAAE;gBAC3B;YACF;YAEA,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC;AAC7B,YAAA,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC;AAC1B,YAAA,IAAI,OAAO,KAAK,EAAE,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,SAAS,EAAE;AAC/D,gBAAA,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI;YACnB;AAAO,iBAAA,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE;gBACtC,KAAK,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC;YAC9B;QACF;AACA,QAAA,OAAO,KAAU;IACnB;AAEQ,IAAA,uBAAuB,CAAC,KAAQ,EAAA;QACtC,MAAM,MAAM,GAA2B,EAAE;QACzC,KAAK,MAAM,SAAS,IAAI,IAAI,CAAC,oBAAoB,EAAE,EAAE;YACnD,MAAM,MAAM,GAAG,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC;YACxC,IAAI,MAAM,EAAE;AACV,gBAAA,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;YAC/B;QACF;AACA,QAAA,OAAO,MAAM;IACf;wGAtdW,WAAW,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;4FAAX,WAAW,EAAA,YAAA,EAAA,IAAA,EAAA,QAAA,EAAA,kBAAA,EAAA,MAAA,EAAA,EAAA,MAAA,EAAA,EAAA,iBAAA,EAAA,QAAA,EAAA,UAAA,EAAA,QAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,IAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,OAAA,EAAA,EAAA,iBAAA,EAAA,SAAA,EAAA,UAAA,EAAA,SAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,oBAAA,EAAA,EAAA,iBAAA,EAAA,sBAAA,EAAA,UAAA,EAAA,sBAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,IAAA,EAAA,EAAA,iBAAA,EAAA,MAAA,EAAA,UAAA,EAAA,MAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,YAAA,EAAA,EAAA,iBAAA,EAAA,cAAA,EAAA,UAAA,EAAA,cAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,WAAA,EAAA,EAAA,iBAAA,EAAA,aAAA,EAAA,UAAA,EAAA,aAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,UAAA,EAAA,EAAA,iBAAA,EAAA,YAAA,EAAA,UAAA,EAAA,YAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,OAAA,EAAA,EAAA,iBAAA,EAAA,SAAA,EAAA,UAAA,EAAA,SAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,KAAA,EAAA,EAAA,iBAAA,EAAA,OAAA,EAAA,UAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,EAAA,OAAA,EAAA,EAAA,UAAA,EAAA,YAAA,EAAA,WAAA,EAAA,aAAA,EAAA,EAAA,QAAA,EAAA,EAAA,EAAA,QAAA,EC3CxB,grMAgIA,EAAA,MAAA,EAAA,CAAA,4nGAAA,CAAA,EAAA,YAAA,EAAA,CAAA,EAAA,IAAA,EAAA,UAAA,EAAA,IAAA,ED3FY,mBAAmB,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAA,EAAA,CAAA,aAAA,EAAA,QAAA,EAAA,8CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAA,EAAA,CAAA,cAAA,EAAA,QAAA,EAAA,QAAA,EAAA,MAAA,EAAA,CAAA,SAAA,EAAA,OAAA,CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAA,EAAA,CAAA,uBAAA,EAAA,QAAA,EAAA,QAAA,EAAA,MAAA,EAAA,CAAA,SAAA,EAAA,OAAA,CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAA,EAAA,CAAA,oBAAA,EAAA,QAAA,EAAA,8MAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAA,EAAA,CAAA,4BAAA,EAAA,QAAA,EAAA,uGAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAA,EAAA,CAAA,0BAAA,EAAA,QAAA,EAAA,6GAAA,EAAA,MAAA,EAAA,CAAA,aAAA,CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAA,EAAA,CAAA,yBAAA,EAAA,QAAA,EAAA,8FAAA,EAAA,MAAA,EAAA,CAAA,MAAA,EAAA,iBAAA,EAAA,OAAA,CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAA,EAAA,CAAA,eAAA,EAAA,QAAA,EAAA,2CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAA,EAAA,CAAA,oBAAA,EAAA,QAAA,EAAA,sGAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAA,EAAA,CAAA,oBAAA,EAAA,QAAA,EAAA,eAAA,EAAA,MAAA,EAAA,CAAA,aAAA,EAAA,UAAA,EAAA,SAAA,CAAA,EAAA,OAAA,EAAA,CAAA,eAAA,CAAA,EAAA,QAAA,EAAA,CAAA,QAAA,CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAA,EAAA,CAAA,kBAAA,EAAA,QAAA,EAAA,aAAA,EAAA,MAAA,EAAA,CAAA,WAAA,CAAA,EAAA,OAAA,EAAA,CAAA,UAAA,CAAA,EAAA,QAAA,EAAA,CAAA,QAAA,CAAA,EAAA,CAAA,EAAA,eAAA,EAAA,EAAA,CAAA,uBAAA,CAAA,MAAA,EAAA,CAAA;;4FAMlB,WAAW,EAAA,UAAA,EAAA,CAAA;kBARvB,SAAS;+BACE,kBAAkB,EAAA,OAAA,EACnB,CAAC,mBAAmB,CAAC,EAAA,eAAA,EAGb,uBAAuB,CAAC,MAAM,EAAA,aAAA,EAChC,iBAAiB,CAAC,QAAQ,EAAA,QAAA,EAAA,grMAAA,EAAA,MAAA,EAAA,CAAA,4nGAAA,CAAA,EAAA;;;AEtC3C;;;;;;;;;;;;;;;;;AAiBG;MAKU,iBAAiB,CAAA;wGAAjB,iBAAiB,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,QAAA,EAAA,CAAA;yGAAjB,iBAAiB,EAAA,OAAA,EAAA,CAHlB,WAAW,CAAA,EAAA,OAAA,EAAA,CACX,WAAW,CAAA,EAAA,CAAA;AAEV,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,mBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,SAAA,EAAA,QAAA,EAAA,EAAA,EAAA,IAAA,EAAA,iBAAiB,YAHlB,WAAW,CAAA,EAAA,CAAA;;4FAGV,iBAAiB,EAAA,UAAA,EAAA,CAAA;kBAJ7B,QAAQ;AAAC,YAAA,IAAA,EAAA,CAAA;oBACR,OAAO,EAAE,CAAC,WAAW,CAAC;oBACtB,OAAO,EAAE,CAAC,WAAW,CAAC;AACvB,iBAAA;;;ACxBD;;AAEG;;;;"}
|
package/package.json
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import * as _zhunam_form_builder from '@zhunam/form-builder';
|
|
1
2
|
import * as _angular_core from '@angular/core';
|
|
3
|
+
import { InjectionToken, Provider } from '@angular/core';
|
|
2
4
|
import { FormGroup, FormControl } from '@angular/forms';
|
|
3
5
|
|
|
4
6
|
/**
|
|
@@ -113,6 +115,12 @@ interface FieldConfig<T> {
|
|
|
113
115
|
* @default false
|
|
114
116
|
*/
|
|
115
117
|
disabled?: boolean;
|
|
118
|
+
/**
|
|
119
|
+
* Whether a `type: 'password'` field renders a show/hide toggle
|
|
120
|
+
* button next to its control. Has no effect on any other field type.
|
|
121
|
+
* @default true
|
|
122
|
+
*/
|
|
123
|
+
showPasswordToggle?: boolean;
|
|
116
124
|
}
|
|
117
125
|
/**
|
|
118
126
|
* Declarative form-level validator that checks values across multiple
|
|
@@ -214,10 +222,49 @@ declare class FormBuilder<T> {
|
|
|
214
222
|
* <lib-form-builder [fields]="fields" [serverErrors]="serverErrors()" (formSubmit)="onFormSubmit($event)" />
|
|
215
223
|
*/
|
|
216
224
|
serverErrors: _angular_core.InputSignal<Partial<Record<keyof T, string>>>;
|
|
225
|
+
/**
|
|
226
|
+
* Custom label for the submit button, instead of the injected
|
|
227
|
+
* FORM_BUILDER_MESSAGES `submit()` text. Takes precedence over both
|
|
228
|
+
* the messages token and the default English label.
|
|
229
|
+
* @example
|
|
230
|
+
* <lib-form-builder [fields]="fields" submitLabel="Create account" />
|
|
231
|
+
*/
|
|
232
|
+
submitLabel: _angular_core.InputSignal<string | undefined>;
|
|
233
|
+
/**
|
|
234
|
+
* Hides the internal submit button entirely, for a consumer that
|
|
235
|
+
* triggers submission from its own UI instead, via the public
|
|
236
|
+
* `submit()` method.
|
|
237
|
+
* @default false
|
|
238
|
+
* @example
|
|
239
|
+
* <lib-form-builder #form [fields]="fields" [hideSubmit]="true" />
|
|
240
|
+
* <button (click)="form.submit()">Continue</button>
|
|
241
|
+
*/
|
|
242
|
+
hideSubmit: _angular_core.InputSignal<boolean>;
|
|
243
|
+
/**
|
|
244
|
+
* Disables the internal submit button and makes the public `submit()`
|
|
245
|
+
* method a no-op while `true`, e.g. during an in-flight request.
|
|
246
|
+
* @default false
|
|
247
|
+
* @example
|
|
248
|
+
* <lib-form-builder [fields]="fields" [loading]="saving()" (formSubmit)="onSave($event)" />
|
|
249
|
+
*/
|
|
250
|
+
loading: _angular_core.InputSignal<boolean>;
|
|
251
|
+
/**
|
|
252
|
+
* External value applied to the form without recreating it. Every
|
|
253
|
+
* time this receives a new, non-`undefined` value (including the
|
|
254
|
+
* first one), it's patched into the current form
|
|
255
|
+
* (`emitEvent: false`, so it never triggers `valueChange` in `'live'`
|
|
256
|
+
* mode). Rebuilding `fields()` afterward keeps whatever was patched,
|
|
257
|
+
* the same as any other value the user typed.
|
|
258
|
+
* @example
|
|
259
|
+
* <lib-form-builder [fields]="fields" [value]="presetValue()" />
|
|
260
|
+
*/
|
|
261
|
+
value: _angular_core.InputSignal<T | undefined>;
|
|
262
|
+
protected readonly messages: _zhunam_form_builder.FormBuilderMessages;
|
|
217
263
|
protected readonly submitted: _angular_core.WritableSignal<boolean>;
|
|
218
264
|
protected readonly crossFieldErrors: _angular_core.WritableSignal<Record<string, string>>;
|
|
265
|
+
private readonly visiblePasswordFields;
|
|
219
266
|
private readonly serverErrorSnapshot;
|
|
220
|
-
protected readonly formGroup: _angular_core.
|
|
267
|
+
protected readonly formGroup: _angular_core.WritableSignal<FormGroup<any>>;
|
|
221
268
|
private static instanceCounter;
|
|
222
269
|
private readonly instanceId;
|
|
223
270
|
constructor();
|
|
@@ -225,6 +272,10 @@ declare class FormBuilder<T> {
|
|
|
225
272
|
private buildValidators;
|
|
226
273
|
protected controlFor(field: FieldConfig<T>): FormControl;
|
|
227
274
|
protected optionsFor(field: FieldConfig<T>): FieldOption[];
|
|
275
|
+
protected showPasswordToggleFor(field: FieldConfig<T>): boolean;
|
|
276
|
+
protected isPasswordVisible(field: FieldConfig<T>): boolean;
|
|
277
|
+
protected passwordInputType(field: FieldConfig<T>): 'text' | 'password';
|
|
278
|
+
protected togglePasswordVisibility(field: FieldConfig<T>): void;
|
|
228
279
|
protected fieldId(field: FieldConfig<T>): string;
|
|
229
280
|
protected errorMessageFor(field: FieldConfig<T>): string | null;
|
|
230
281
|
protected serverErrorFor(field: FieldConfig<T>): string | null;
|
|
@@ -233,11 +284,21 @@ declare class FormBuilder<T> {
|
|
|
233
284
|
protected errorId(field: FieldConfig<T>): string;
|
|
234
285
|
protected legendId(field: FieldConfig<T>): string;
|
|
235
286
|
protected isSubmitDisabled(): boolean;
|
|
236
|
-
|
|
287
|
+
/**
|
|
288
|
+
* Triggers the exact same submit logic the internal button triggers:
|
|
289
|
+
* marks the form as submitted, runs validation, and emits `formSubmit`
|
|
290
|
+
* if it passes. Works identically whether `hideSubmit` is `true` or
|
|
291
|
+
* `false`, and is a no-op while `loading` is `true`.
|
|
292
|
+
* @example
|
|
293
|
+
* <lib-form-builder #form [fields]="fields" [hideSubmit]="true" />
|
|
294
|
+
* <button (click)="form.submit()">Continue</button>
|
|
295
|
+
*/
|
|
296
|
+
submit(): void;
|
|
297
|
+
protected performSubmit(): void;
|
|
237
298
|
private coerceNumberFields;
|
|
238
299
|
private runCrossFieldValidators;
|
|
239
300
|
static ɵfac: _angular_core.ɵɵFactoryDeclaration<FormBuilder<any>, never>;
|
|
240
|
-
static ɵcmp: _angular_core.ɵɵComponentDeclaration<FormBuilder<any>, "lib-form-builder", never, { "fields": { "alias": "fields"; "required": true; "isSignal": true; }; "columns": { "alias": "columns"; "required": false; "isSignal": true; }; "crossFieldValidators": { "alias": "crossFieldValidators"; "required": false; "isSignal": true; }; "mode": { "alias": "mode"; "required": false; "isSignal": true; }; "serverErrors": { "alias": "serverErrors"; "required": false; "isSignal": true; }; }, { "formSubmit": "formSubmit"; "valueChange": "valueChange"; }, never, never, true, never>;
|
|
301
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<FormBuilder<any>, "lib-form-builder", never, { "fields": { "alias": "fields"; "required": true; "isSignal": true; }; "columns": { "alias": "columns"; "required": false; "isSignal": true; }; "crossFieldValidators": { "alias": "crossFieldValidators"; "required": false; "isSignal": true; }; "mode": { "alias": "mode"; "required": false; "isSignal": true; }; "serverErrors": { "alias": "serverErrors"; "required": false; "isSignal": true; }; "submitLabel": { "alias": "submitLabel"; "required": false; "isSignal": true; }; "hideSubmit": { "alias": "hideSubmit"; "required": false; "isSignal": true; }; "loading": { "alias": "loading"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; }, { "formSubmit": "formSubmit"; "valueChange": "valueChange"; }, never, never, true, never>;
|
|
241
302
|
}
|
|
242
303
|
|
|
243
304
|
/**
|
|
@@ -264,5 +325,105 @@ declare class FormBuilderModule {
|
|
|
264
325
|
static ɵinj: _angular_core.ɵɵInjectorDeclaration<FormBuilderModule>;
|
|
265
326
|
}
|
|
266
327
|
|
|
267
|
-
|
|
268
|
-
|
|
328
|
+
/**
|
|
329
|
+
* Translatable strings `FormBuilder` renders: validation messages and the
|
|
330
|
+
* submit button label. Every member is a function, called fresh on every
|
|
331
|
+
* render, so a message that reads a signal (e.g. a language switcher)
|
|
332
|
+
* updates live without recreating the component.
|
|
333
|
+
*/
|
|
334
|
+
interface FormBuilderMessages {
|
|
335
|
+
/**
|
|
336
|
+
* Shown when a required field is empty.
|
|
337
|
+
*/
|
|
338
|
+
required(): string;
|
|
339
|
+
/**
|
|
340
|
+
* Shown when an `email` field doesn't hold a well-formed address.
|
|
341
|
+
*/
|
|
342
|
+
email(): string;
|
|
343
|
+
/**
|
|
344
|
+
* Shown when a numeric field is below its configured `min`.
|
|
345
|
+
* @param min The minimum allowed value, from Angular's own validation
|
|
346
|
+
* error (`control.errors['min'].min`).
|
|
347
|
+
*/
|
|
348
|
+
min(min: number): string;
|
|
349
|
+
/**
|
|
350
|
+
* Shown when a numeric field is above its configured `max`.
|
|
351
|
+
* @param max The maximum allowed value, from Angular's own validation
|
|
352
|
+
* error (`control.errors['max'].max`).
|
|
353
|
+
*/
|
|
354
|
+
max(max: number): string;
|
|
355
|
+
/**
|
|
356
|
+
* Shown when a field's value is shorter than its configured `minLength`.
|
|
357
|
+
* @param requiredLength The minimum required length, from Angular's own
|
|
358
|
+
* validation error (`control.errors['minlength'].requiredLength`).
|
|
359
|
+
*/
|
|
360
|
+
minLength(requiredLength: number): string;
|
|
361
|
+
/**
|
|
362
|
+
* Shown when a field's value is longer than its configured `maxLength`.
|
|
363
|
+
* @param requiredLength The maximum required length, from Angular's own
|
|
364
|
+
* validation error (`control.errors['maxlength'].requiredLength`).
|
|
365
|
+
*/
|
|
366
|
+
maxLength(requiredLength: number): string;
|
|
367
|
+
/**
|
|
368
|
+
* Shown when a field's value doesn't match its configured `pattern`.
|
|
369
|
+
*/
|
|
370
|
+
pattern(): string;
|
|
371
|
+
/**
|
|
372
|
+
* Label of the submit button.
|
|
373
|
+
*/
|
|
374
|
+
submit(): string;
|
|
375
|
+
/**
|
|
376
|
+
* `aria-label` of the password visibility toggle when the field's
|
|
377
|
+
* value is currently hidden (about to reveal it).
|
|
378
|
+
*/
|
|
379
|
+
showPassword(): string;
|
|
380
|
+
/**
|
|
381
|
+
* `aria-label` of the password visibility toggle when the field's
|
|
382
|
+
* value is currently shown (about to hide it).
|
|
383
|
+
*/
|
|
384
|
+
hidePassword(): string;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Default English messages. Used when no `FORM_BUILDER_MESSAGES` provider
|
|
388
|
+
* is registered.
|
|
389
|
+
*/
|
|
390
|
+
declare const FORM_BUILDER_MESSAGES_EN: FormBuilderMessages;
|
|
391
|
+
/**
|
|
392
|
+
* Spanish preset, ready to pass to `provideFormBuilderMessages()`.
|
|
393
|
+
* @example
|
|
394
|
+
* providers: [provideFormBuilderMessages(FORM_BUILDER_MESSAGES_ES)]
|
|
395
|
+
*/
|
|
396
|
+
declare const FORM_BUILDER_MESSAGES_ES: FormBuilderMessages;
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* DI token for the active `FormBuilderMessages` set. Defaults to
|
|
400
|
+
* `FORM_BUILDER_MESSAGES_EN` when no provider is registered.
|
|
401
|
+
*
|
|
402
|
+
* A per-field `FieldValidatorConfig.errorMessages` override, when set,
|
|
403
|
+
* still takes precedence over this token for that specific field: this
|
|
404
|
+
* token supplies the message only when the field itself doesn't override
|
|
405
|
+
* that validator's text.
|
|
406
|
+
*
|
|
407
|
+
* Prefer `provideFormBuilderMessages()` over providing this token
|
|
408
|
+
* directly, it merges your overrides on top of the English defaults
|
|
409
|
+
* instead of requiring every message to be re-specified.
|
|
410
|
+
*/
|
|
411
|
+
declare const FORM_BUILDER_MESSAGES: InjectionToken<FormBuilderMessages>;
|
|
412
|
+
/**
|
|
413
|
+
* Registers a `FormBuilderMessages` override, merged on top of the
|
|
414
|
+
* English defaults so a partial override (e.g. only `submit`) leaves the
|
|
415
|
+
* rest in English instead of requiring the full set.
|
|
416
|
+
*
|
|
417
|
+
* @param overrides Messages to override. Any member left out falls back
|
|
418
|
+
* to `FORM_BUILDER_MESSAGES_EN`.
|
|
419
|
+
* @example
|
|
420
|
+
* // Full preset:
|
|
421
|
+
* providers: [provideFormBuilderMessages(FORM_BUILDER_MESSAGES_ES)]
|
|
422
|
+
* @example
|
|
423
|
+
* // Partial override, everything else stays in English:
|
|
424
|
+
* providers: [provideFormBuilderMessages({ min: () => 'Must be greater than 0' })]
|
|
425
|
+
*/
|
|
426
|
+
declare function provideFormBuilderMessages(overrides: Partial<FormBuilderMessages>): Provider;
|
|
427
|
+
|
|
428
|
+
export { FORM_BUILDER_MESSAGES, FORM_BUILDER_MESSAGES_EN, FORM_BUILDER_MESSAGES_ES, FormBuilder, FormBuilderModule, provideFormBuilderMessages };
|
|
429
|
+
export type { CrossFieldValidator, FieldConfig, FieldOption, FieldType, FieldValidatorConfig, FormBuilderMessages };
|