@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 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, computed, effect, ViewEncapsulation, ChangeDetectionStrategy, Component, NgModule } from '@angular/core';
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 — nunca vía binding de propiedad que inserte HTML.
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 — this bridges the two so consumer-facing config can stay camelCase.
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 with `computed()` rather than built once in the
179
- // constructor: it's rebuilt only when the `fields()` array reference
180
- // changes. `computed()` fits better than an `effect()` here because
181
- // building a FormGroup is a pure derivation with no teardown to manage,
182
- // and `computed()` already memoizes it — the same instance is returned
183
- // across change-detection cycles until `fields()` itself changes.
184
- formGroup = computed(() => this.buildFormGroup(this.fields()), ...(ngDevMode ? [{ debugName: "formGroup" }] : /* istanbul ignore next */ []));
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 initialValue = field.defaultValue ?? (field.type === 'checkbox' ? false : null);
232
- controls[String(field.key)] = new FormControl({ value: initialValue, disabled: !!field.disabled }, this.buildValidators(field));
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 errorKey = RAW_ERROR_KEY_MAP[Object.keys(control.errors)[0]];
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
- return field.validators?.errorMessages?.[errorKey] ?? DEFAULT_ERROR_MESSAGES[errorKey];
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" — both the visible `.fb-error` message
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
- onSubmit() {
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 — `getRawValue()` keeps
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 — Validators.min/max already parseFloat internally either
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)=\"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"] }]
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":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhunam/form-builder",
3
- "version": "1.2.0",
3
+ "version": "3.0.0",
4
4
  "license": "MIT",
5
5
  "peerDependencies": {
6
6
  "@angular/core": "^20.0.0 || ^21.0.0 || ^22.0.0",
@@ -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.Signal<FormGroup<any>>;
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
- protected onSubmit(): void;
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
- export { FormBuilder, FormBuilderModule };
268
- export type { CrossFieldValidator, FieldConfig, FieldOption, FieldType, FieldValidatorConfig };
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 };