@zhunam/form-builder 2.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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) {
@@ -319,16 +485,82 @@ class FormBuilder {
319
485
  legendId(field) {
320
486
  return `${this.fieldId(field)}-legend`;
321
487
  }
488
+ hintId(field) {
489
+ return `${this.fieldId(field)}-hint`;
490
+ }
491
+ optionDescriptionId(field, option) {
492
+ return `${this.fieldId(field)}-option-${String(option.value)}-description`;
493
+ }
494
+ /**
495
+ * `aria-describedby` for the field's own control: the error's id when
496
+ * one is currently shown, otherwise the hint's id when `hint` is set,
497
+ * otherwise `null`. Never both at once, mirroring `activeErrorFor`
498
+ * being the single source of truth for whether an error is visible.
499
+ */
500
+ describedByFor(field) {
501
+ if (this.activeErrorFor(field)) {
502
+ return this.errorId(field);
503
+ }
504
+ if (field.hint) {
505
+ return this.hintId(field);
506
+ }
507
+ return null;
508
+ }
509
+ /**
510
+ * `aria-describedby` for one `radio` option's own control: combines
511
+ * the field-level id (`describedByFor`) with that option's own
512
+ * description id when it has one, space-separated (the real HTML/ARIA
513
+ * syntax for referencing more than one describing element), or just
514
+ * whichever of the two is actually present, or `null` if neither is.
515
+ */
516
+ optionDescribedByFor(field, option) {
517
+ const ids = [
518
+ this.describedByFor(field),
519
+ option.description ? this.optionDescriptionId(field, option) : null,
520
+ ].filter((id) => id !== null);
521
+ return ids.length > 0 ? ids.join(' ') : null;
522
+ }
523
+ /**
524
+ * Whether `field` renders as a switch instead of a native checkbox
525
+ * box. `false` for any `type` other than `'checkbox'`, regardless of
526
+ * `appearance`.
527
+ */
528
+ isSwitch(field) {
529
+ return field.type === 'checkbox' && field.appearance === 'switch';
530
+ }
531
+ /**
532
+ * Whether `field`'s options render as a row of pills instead of a
533
+ * vertical list of native radios. `false` for any `type` other than
534
+ * `'radio'`, regardless of `appearance`.
535
+ */
536
+ isSegmented(field) {
537
+ return field.type === 'radio' && field.appearance === 'segmented';
538
+ }
322
539
  isSubmitDisabled() {
323
- return this.formGroup().invalid || Object.keys(this.crossFieldErrors()).length > 0;
540
+ return this.loading() || this.formGroup().invalid || Object.keys(this.crossFieldErrors()).length > 0;
541
+ }
542
+ /**
543
+ * Triggers the exact same submit logic the internal button triggers:
544
+ * marks the form as submitted, runs validation, and emits `formSubmit`
545
+ * if it passes. Works identically whether `hideSubmit` is `true` or
546
+ * `false`, and is a no-op while `loading` is `true`.
547
+ * @example
548
+ * <lib-form-builder #form [fields]="fields" [hideSubmit]="true" />
549
+ * <button (click)="form.submit()">Continue</button>
550
+ */
551
+ submit() {
552
+ this.performSubmit();
324
553
  }
325
- onSubmit() {
554
+ performSubmit() {
555
+ if (this.loading()) {
556
+ return;
557
+ }
326
558
  this.submitted.set(true);
327
559
  const group = this.formGroup();
328
560
  if (group.invalid) {
329
561
  return;
330
562
  }
331
- // `.value` excludes disabled controls entirely — `getRawValue()` keeps
563
+ // `.value` excludes disabled controls entirely, `getRawValue()` keeps
332
564
  // them, so a disabled field with a `defaultValue` still comes through
333
565
  // as a complete `T` instead of a hole in the emitted object.
334
566
  const value = this.coerceNumberFields(group.getRawValue());
@@ -346,7 +578,7 @@ class FormBuilder {
346
578
  // dynamically, so it never matches, and DefaultValueAccessor (string in,
347
579
  // string out) handles the control instead. Fixed up here, at the point
348
580
  // the value leaves the component, rather than in how the FormControl
349
- // stores it — Validators.min/max already parseFloat internally either
581
+ // stores it: Validators.min/max already parseFloat internally either
350
582
  // way, so correcting the stored value would change nothing there but
351
583
  // would still need this same conversion for the emitted object.
352
584
  coerceNumberFields(rawValue) {
@@ -377,12 +609,12 @@ class FormBuilder {
377
609
  return errors;
378
610
  }
379
611
  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{--zhunam-font-family: system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;--zhunam-font-size: .9375rem;--zhunam-text: #1f2937;--zhunam-text-secondary: #374151;--zhunam-border: #d1d5db;--zhunam-error: #dc2626;--zhunam-primary: #3b82f6;--zhunam-primary-content: #fff;--zhunam-focus: var(--zhunam-primary, #3b82f6);--zhunam-radius: .5rem;--zhunam-spacing: 1rem;--zhunam-form-control-padding-y: .5rem;--zhunam-form-control-padding-x: .75rem;--zhunam-transition-duration: .15s;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-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-error{margin:0;font-size:.8125rem;color:var(--zhunam-error, #dc2626)}.fb-submit{appearance:none;align-self:flex-start;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 });
612
+ 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\" [class.fb-switch-option]=\"isSwitch(field)\">\r\n <input\r\n type=\"checkbox\"\r\n class=\"fb-checkbox-input\"\r\n [class.sr-only]=\"isSwitch(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]=\"describedByFor(field)\"\r\n />\r\n @if (isSwitch(field)) {\r\n <span class=\"fb-switch-track\" aria-hidden=\"true\">\r\n <span class=\"fb-switch-thumb\"></span>\r\n </span>\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 [class.fb-segmented-group]=\"isSegmented(field)\"\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\" [class.fb-segmented-option]=\"isSegmented(field)\">\r\n <input\r\n type=\"radio\"\r\n class=\"fb-radio-input\"\r\n [class.sr-only]=\"isSegmented(field)\"\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]=\"optionDescribedByFor(field, option)\"\r\n />\r\n @if (isSegmented(field)) {\r\n <span class=\"fb-segmented-pill\">{{ option.label }}</span>\r\n } @else {\r\n {{ option.label }}\r\n }\r\n @if (option.description; as description) {\r\n <span class=\"fb-option-description\" [id]=\"optionDescriptionId(field, option)\">{{ description }}</span>\r\n }\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 [readOnly]=\"!!field.readonly\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"describedByFor(field)\"\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 [readOnly]=\"!!field.readonly\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"describedByFor(field)\"\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]=\"describedByFor(field)\"\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 [readOnly]=\"!!field.readonly\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"describedByFor(field)\"\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 } @else if (field.hint) {\r\n <p class=\"fb-hint\" [id]=\"hintId(field)\">{{ field.hint }}</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;flex-wrap:wrap;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}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.fb-switch-track{position:relative;display:inline-block;flex-shrink:0;width:2.25rem;height:1.25rem;border-radius:9999px;background:var(--zhunam-border, #d1d5db);transition:background-color var(--zhunam-transition-duration, .15s) ease}.fb-switch-thumb{position:absolute;top:.125rem;left:.125rem;width:1rem;height:1rem;border-radius:50%;background:#fff;transition:transform var(--zhunam-transition-duration, .15s) ease}.fb-checkbox-input:checked+.fb-switch-track{background:var(--zhunam-accent, var(--zhunam-primary, #3b82f6))}.fb-checkbox-input:checked+.fb-switch-track .fb-switch-thumb{transform:translate(1rem)}.fb-checkbox-input:focus-visible+.fb-switch-track{outline:2px solid var(--zhunam-focus, var(--zhunam-primary, #3b82f6));outline-offset:2px}.fb-segmented-group{flex-direction:row;flex-wrap:wrap;gap:.5rem}.fb-segmented-pill{display:inline-flex;align-items:center;padding:.375rem .875rem;border-radius:9999px;background:var(--zhunam-segmented-bg, #fff);color:var(--zhunam-text-secondary, #374151);font-weight:600;box-shadow:0 2px 10px #0f252a14;transition:background-color .3s ease,color .3s ease}.fb-radio-input:checked+.fb-segmented-pill{background:var(--zhunam-primary, #3b82f6);color:var(--zhunam-primary-content, #fff)}.fb-radio-input:focus-visible+.fb-segmented-pill{outline:2px solid var(--zhunam-focus, var(--zhunam-primary, #3b82f6));outline-offset:2px}.fb-hint{margin:0;font-size:.8125rem;color:var(--zhunam-text-secondary, #374151)}.fb-option-description{flex-basis:100%;font-size:.8125rem;color:var(--zhunam-text-secondary, #374151)}@media(prefers-reduced-motion:reduce){.fb-switch-track,.fb-switch-thumb,.fb-segmented-pill{transition-duration:.01ms}}.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
613
  }
382
614
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: FormBuilder, decorators: [{
383
615
  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{--zhunam-font-family: system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;--zhunam-font-size: .9375rem;--zhunam-text: #1f2937;--zhunam-text-secondary: #374151;--zhunam-border: #d1d5db;--zhunam-error: #dc2626;--zhunam-primary: #3b82f6;--zhunam-primary-content: #fff;--zhunam-focus: var(--zhunam-primary, #3b82f6);--zhunam-radius: .5rem;--zhunam-spacing: 1rem;--zhunam-form-control-padding-y: .5rem;--zhunam-form-control-padding-x: .75rem;--zhunam-transition-duration: .15s;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-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-error{margin:0;font-size:.8125rem;color:var(--zhunam-error, #dc2626)}.fb-submit{appearance:none;align-self:flex-start;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"] }]
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 }] }] } });
616
+ 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\" [class.fb-switch-option]=\"isSwitch(field)\">\r\n <input\r\n type=\"checkbox\"\r\n class=\"fb-checkbox-input\"\r\n [class.sr-only]=\"isSwitch(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]=\"describedByFor(field)\"\r\n />\r\n @if (isSwitch(field)) {\r\n <span class=\"fb-switch-track\" aria-hidden=\"true\">\r\n <span class=\"fb-switch-thumb\"></span>\r\n </span>\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 [class.fb-segmented-group]=\"isSegmented(field)\"\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\" [class.fb-segmented-option]=\"isSegmented(field)\">\r\n <input\r\n type=\"radio\"\r\n class=\"fb-radio-input\"\r\n [class.sr-only]=\"isSegmented(field)\"\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]=\"optionDescribedByFor(field, option)\"\r\n />\r\n @if (isSegmented(field)) {\r\n <span class=\"fb-segmented-pill\">{{ option.label }}</span>\r\n } @else {\r\n {{ option.label }}\r\n }\r\n @if (option.description; as description) {\r\n <span class=\"fb-option-description\" [id]=\"optionDescriptionId(field, option)\">{{ description }}</span>\r\n }\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 [readOnly]=\"!!field.readonly\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"describedByFor(field)\"\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 [readOnly]=\"!!field.readonly\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"describedByFor(field)\"\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]=\"describedByFor(field)\"\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 [readOnly]=\"!!field.readonly\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"describedByFor(field)\"\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 } @else if (field.hint) {\r\n <p class=\"fb-hint\" [id]=\"hintId(field)\">{{ field.hint }}</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;flex-wrap:wrap;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}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.fb-switch-track{position:relative;display:inline-block;flex-shrink:0;width:2.25rem;height:1.25rem;border-radius:9999px;background:var(--zhunam-border, #d1d5db);transition:background-color var(--zhunam-transition-duration, .15s) ease}.fb-switch-thumb{position:absolute;top:.125rem;left:.125rem;width:1rem;height:1rem;border-radius:50%;background:#fff;transition:transform var(--zhunam-transition-duration, .15s) ease}.fb-checkbox-input:checked+.fb-switch-track{background:var(--zhunam-accent, var(--zhunam-primary, #3b82f6))}.fb-checkbox-input:checked+.fb-switch-track .fb-switch-thumb{transform:translate(1rem)}.fb-checkbox-input:focus-visible+.fb-switch-track{outline:2px solid var(--zhunam-focus, var(--zhunam-primary, #3b82f6));outline-offset:2px}.fb-segmented-group{flex-direction:row;flex-wrap:wrap;gap:.5rem}.fb-segmented-pill{display:inline-flex;align-items:center;padding:.375rem .875rem;border-radius:9999px;background:var(--zhunam-segmented-bg, #fff);color:var(--zhunam-text-secondary, #374151);font-weight:600;box-shadow:0 2px 10px #0f252a14;transition:background-color .3s ease,color .3s ease}.fb-radio-input:checked+.fb-segmented-pill{background:var(--zhunam-primary, #3b82f6);color:var(--zhunam-primary-content, #fff)}.fb-radio-input:focus-visible+.fb-segmented-pill{outline:2px solid var(--zhunam-focus, var(--zhunam-primary, #3b82f6));outline-offset:2px}.fb-hint{margin:0;font-size:.8125rem;color:var(--zhunam-text-secondary, #374151)}.fb-option-description{flex-basis:100%;font-size:.8125rem;color:var(--zhunam-text-secondary, #374151)}@media(prefers-reduced-motion:reduce){.fb-switch-track,.fb-switch-thumb,.fb-segmented-pill{transition-duration:.01ms}}.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"] }]
617
+ }], 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
618
 
387
619
  /**
388
620
  * NgModule wrapper for `FormBuilder<T>`, for consumers still on a
@@ -419,5 +651,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImpo
419
651
  * Generated bundle index. Do not edit.
420
652
  */
421
653
 
422
- export { FormBuilder, FormBuilderModule };
654
+ export { FORM_BUILDER_MESSAGES, FORM_BUILDER_MESSAGES_EN, FORM_BUILDER_MESSAGES_ES, FormBuilder, FormBuilderModule, provideFormBuilderMessages };
423
655
  //# sourceMappingURL=zhunam-form-builder.mjs.map