@zhunam/form-builder 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -71,6 +71,60 @@ the form submitted, validates, and emits `formSubmit` if valid. It's a
|
|
|
71
71
|
no-op while `loading` is `true`, the same guard that disables the
|
|
72
72
|
internal button.
|
|
73
73
|
|
|
74
|
+
### Switch and segmented appearance
|
|
75
|
+
|
|
76
|
+
`appearance` changes only how a specific `type` looks, never the control underneath it: a real `<input type="checkbox">`/`<input type="radio">` is always there, visually hidden but focusable, clickable via its label, and fully native for validation, keyboard operation, and screen readers.
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
fields: FieldConfig<Settings>[] = [
|
|
80
|
+
{ key: 'notifications', label: 'Email notifications', type: 'checkbox', appearance: 'switch' },
|
|
81
|
+
{
|
|
82
|
+
key: 'plan',
|
|
83
|
+
label: 'Plan',
|
|
84
|
+
type: 'radio',
|
|
85
|
+
appearance: 'segmented',
|
|
86
|
+
options: [
|
|
87
|
+
{ value: 'personal', label: 'Personal' },
|
|
88
|
+
{ value: 'business', label: 'Business' },
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`'switch'` only changes `type: 'checkbox'`; `'segmented'` only changes `type: 'radio'`. Setting `appearance` on any other `type` (or leaving it unset) has no effect at all, no error, no warning.
|
|
95
|
+
|
|
96
|
+
### Field hints and option descriptions
|
|
97
|
+
|
|
98
|
+
`hint` shows short helper text below a field, only while there's no active error: an error replaces it visually, and `aria-describedby` follows the same rule, always pointing at whichever one is currently shown, never both.
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
{ key: 'password', label: 'Password', type: 'password', hint: 'At least 8 characters', validators: { required: true, minLength: 8 } }
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`FieldOption.description` shows helper text below one specific `radio` option, independent of the field-level hint or error:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
{
|
|
108
|
+
key: 'plan',
|
|
109
|
+
label: 'Plan',
|
|
110
|
+
type: 'radio',
|
|
111
|
+
options: [
|
|
112
|
+
{ value: 'personal', label: 'Personal', description: 'For individual use' },
|
|
113
|
+
{ value: 'business', label: 'Business', description: 'For teams and invoicing' },
|
|
114
|
+
],
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Read-only fields
|
|
119
|
+
|
|
120
|
+
`readonly` keeps a field's value visible and its validation running exactly as before, only direct editing is blocked. Unlike `disabled`, the underlying `FormControl` stays enabled, so its value is never excluded from anything.
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
{ key: 'referenceCode', label: 'Reference code', type: 'text', readonly: true, defaultValue: 'REF-2026-001' }
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Only applies to `text`, `number`, `email`, `password`, `date`, and `textarea`: the HTML `readonly` attribute doesn't apply to `select`, `radio`, or `checkbox` natively, and this library doesn't simulate it for those, setting `readonly` on one of them is simply ignored.
|
|
127
|
+
|
|
74
128
|
## API
|
|
75
129
|
|
|
76
130
|
### `FormBuilder<T>`
|
|
@@ -104,6 +158,11 @@ internal button.
|
|
|
104
158
|
| `colSpan` | `1 \| 2` | `1` | How many grid columns this field spans, when the component's `columns` input is 2 or more. |
|
|
105
159
|
| `disabled` | `boolean` | `false` | Renders the control disabled from the start; still included in the value `formSubmit` emits. |
|
|
106
160
|
| `showPasswordToggle` | `boolean` | `true` | For a `type: 'password'` field, whether it renders a show/hide toggle button. No effect on other field types. |
|
|
161
|
+
| `appearance` | `FieldAppearance` (`'default' \| 'switch' \| 'segmented'`) | `'default'` | Purely visual variant. `'switch'` only affects `type: 'checkbox'`, `'segmented'` only affects `type: 'radio'`. Any other combination is ignored, same as `'default'`. See "Switch and segmented appearance" below. |
|
|
162
|
+
| `hint` | `string` | None | Short helper text shown below the field when there's no active error. See "Field hints and option descriptions" below. |
|
|
163
|
+
| `readonly` | `boolean` | `false` | Native HTML `readonly`, for `text`/`number`/`email`/`password`/`date`/`textarea` only. No effect on `select`/`radio`/`checkbox`. See "Read-only fields" below. |
|
|
164
|
+
|
|
165
|
+
`FieldOption` also gains an optional `description?: string`, shown below that specific option (for `radio`, see below); `select` has nowhere to render it, so it's ignored there.
|
|
107
166
|
|
|
108
167
|
### `FormBuilderMessages`
|
|
109
168
|
|
|
@@ -156,6 +215,7 @@ inherited CSS custom property.
|
|
|
156
215
|
| `--zhunam-transition-duration` | `150ms` | Duration of border/opacity transitions. |
|
|
157
216
|
| `--zhunam-form-control-padding-x` | `0.75rem` | Horizontal padding inside inputs/selects/textareas. |
|
|
158
217
|
| `--zhunam-form-control-padding-y` | `0.5rem` | Vertical padding inside inputs/selects/textareas. |
|
|
218
|
+
| `--zhunam-segmented-bg` | `#fff` | Inactive pill background for a `'segmented'` radio group. The active pill reuses `--zhunam-primary`/`--zhunam-primary-content` instead, same as the submit button. |
|
|
159
219
|
|
|
160
220
|
`--fb-columns` is an internal implementation detail (it carries the
|
|
161
221
|
`columns` input's value into the field grid's CSS), not a themeable
|
|
@@ -485,6 +485,57 @@ class FormBuilder {
|
|
|
485
485
|
legendId(field) {
|
|
486
486
|
return `${this.fieldId(field)}-legend`;
|
|
487
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
|
+
}
|
|
488
539
|
isSubmitDisabled() {
|
|
489
540
|
return this.loading() || this.formGroup().invalid || Object.keys(this.crossFieldErrors()).length > 0;
|
|
490
541
|
}
|
|
@@ -558,11 +609,11 @@ class FormBuilder {
|
|
|
558
609
|
return errors;
|
|
559
610
|
}
|
|
560
611
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: FormBuilder, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
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 });
|
|
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 });
|
|
562
613
|
}
|
|
563
614
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.17", ngImport: i0, type: FormBuilder, decorators: [{
|
|
564
615
|
type: Component,
|
|
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"] }]
|
|
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"] }]
|
|
566
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 }] }] } });
|
|
567
618
|
|
|
568
619
|
/**
|
|
@@ -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/models/form-builder-messages.ts","../../../../libs/form-builder/src/lib/tokens/form-builder-messages.token.ts","../../../../libs/form-builder/src/lib/form-builder/form-builder.ts","../../../../libs/form-builder/src/lib/form-builder/form-builder.html","../../../../libs/form-builder/src/lib/form-builder/form-builder.module.ts","../../../../libs/form-builder/src/zhunam-form-builder.ts"],"sourcesContent":["const MAX_PATTERN_LENGTH = 300;\r\n\r\n/** Matches a group that repeats another quantifier, e.g. `(x+)+`, `(x*)*`. */\r\nconst NESTED_QUANTIFIER = /\\([^()]*[+*][^()]*\\)[+*]/;\r\n\r\n/** Matches a repeated group so its alternated branches can be compared. */\r\nconst REPEATED_GROUP = /\\(([^()]*)\\)[+*]/g;\r\n\r\n/**\r\n * Validates that a regex `pattern` string is safe enough to compile into\r\n * a `RegExp` for form validation.\r\n *\r\n * This is a heuristic, not a mathematical guarantee of ReDoS-safety: it\r\n * rejects the pattern shapes most commonly responsible for catastrophic\r\n * backtracking (nested quantifiers, overlapping alternation inside a\r\n * repeated group) and enforces a length cap, but a maliciously crafted\r\n * pattern could still slip through undetected. Never accept `pattern`\r\n * values from a fully untrusted source without your own review.\r\n *\r\n * @throws {Error} If `pattern` is longer than 300 characters, or matches\r\n * a known catastrophic-backtracking shape.\r\n */\r\nexport function assertSafePattern(pattern: string): void {\r\n if (pattern.length > MAX_PATTERN_LENGTH) {\r\n throw new Error(\r\n `Unsafe regex pattern: exceeds the ${MAX_PATTERN_LENGTH}-character limit.`,\r\n );\r\n }\r\n\r\n if (NESTED_QUANTIFIER.test(pattern)) {\r\n throw new Error(\r\n 'Unsafe regex pattern: nested quantifiers detected (e.g. (x+)+), a known catastrophic-backtracking shape.',\r\n );\r\n }\r\n\r\n if (hasOverlappingAlternation(pattern)) {\r\n throw new Error(\r\n 'Unsafe regex pattern: overlapping alternation inside a repeated group detected (e.g. (a|a)+), a known catastrophic-backtracking shape.',\r\n );\r\n }\r\n}\r\n\r\nfunction hasOverlappingAlternation(pattern: string): boolean {\r\n const repeatedGroup = new RegExp(REPEATED_GROUP);\r\n let match: RegExpExecArray | null;\r\n\r\n while ((match = repeatedGroup.exec(pattern)) !== null) {\r\n const branches = match[1].split('|');\r\n for (let i = 0; i < branches.length; i++) {\r\n for (let j = i + 1; j < branches.length; j++) {\r\n if (branchesOverlap(branches[i], branches[j])) {\r\n return true;\r\n }\r\n }\r\n }\r\n }\r\n\r\n return false;\r\n}\r\n\r\nfunction branchesOverlap(a: string, b: string): boolean {\r\n if (a === b) return true;\r\n if (!a || !b) return false;\r\n return a.startsWith(b) || b.startsWith(a);\r\n}\r\n","/**\r\n * Translatable strings `FormBuilder` renders: validation messages and the\r\n * submit button label. Every member is a function, called fresh on every\r\n * render, so a message that reads a signal (e.g. a language switcher)\r\n * updates live without recreating the component.\r\n */\r\nexport interface FormBuilderMessages {\r\n /**\r\n * Shown when a required field is empty.\r\n */\r\n required(): string;\r\n\r\n /**\r\n * Shown when an `email` field doesn't hold a well-formed address.\r\n */\r\n email(): string;\r\n\r\n /**\r\n * Shown when a numeric field is below its configured `min`.\r\n * @param min The minimum allowed value, from Angular's own validation\r\n * error (`control.errors['min'].min`).\r\n */\r\n min(min: number): string;\r\n\r\n /**\r\n * Shown when a numeric field is above its configured `max`.\r\n * @param max The maximum allowed value, from Angular's own validation\r\n * error (`control.errors['max'].max`).\r\n */\r\n max(max: number): string;\r\n\r\n /**\r\n * Shown when a field's value is shorter than its configured `minLength`.\r\n * @param requiredLength The minimum required length, from Angular's own\r\n * validation error (`control.errors['minlength'].requiredLength`).\r\n */\r\n minLength(requiredLength: number): string;\r\n\r\n /**\r\n * Shown when a field's value is longer than its configured `maxLength`.\r\n * @param requiredLength The maximum required length, from Angular's own\r\n * validation error (`control.errors['maxlength'].requiredLength`).\r\n */\r\n maxLength(requiredLength: number): string;\r\n\r\n /**\r\n * Shown when a field's value doesn't match its configured `pattern`.\r\n */\r\n pattern(): string;\r\n\r\n /**\r\n * Label of the submit button.\r\n */\r\n submit(): string;\r\n\r\n /**\r\n * `aria-label` of the password visibility toggle when the field's\r\n * value is currently hidden (about to reveal it).\r\n */\r\n showPassword(): string;\r\n\r\n /**\r\n * `aria-label` of the password visibility toggle when the field's\r\n * value is currently shown (about to hide it).\r\n */\r\n hidePassword(): string;\r\n}\r\n\r\n/**\r\n * Default English messages. Used when no `FORM_BUILDER_MESSAGES` provider\r\n * is registered.\r\n */\r\nexport const FORM_BUILDER_MESSAGES_EN: FormBuilderMessages = {\r\n required: () => 'This field is required.',\r\n email: () => 'Enter a valid email address.',\r\n min: (min) => `The value must be at least ${min}.`,\r\n max: (max) => `The value must be at most ${max}.`,\r\n minLength: (requiredLength) => `Must be at least ${requiredLength} characters.`,\r\n maxLength: (requiredLength) => `Must be at most ${requiredLength} characters.`,\r\n pattern: () => 'The format is not valid.',\r\n submit: () => 'Submit',\r\n showPassword: () => 'Show password',\r\n hidePassword: () => 'Hide password',\r\n};\r\n\r\n/**\r\n * Spanish preset, ready to pass to `provideFormBuilderMessages()`.\r\n * @example\r\n * providers: [provideFormBuilderMessages(FORM_BUILDER_MESSAGES_ES)]\r\n */\r\nexport const FORM_BUILDER_MESSAGES_ES: FormBuilderMessages = {\r\n required: () => 'Este campo es obligatorio.',\r\n email: () => 'El email no es válido.',\r\n min: (min) => `El valor debe ser como mínimo ${min}.`,\r\n max: (max) => `El valor debe ser como máximo ${max}.`,\r\n minLength: (requiredLength) => `Debe tener al menos ${requiredLength} caracteres.`,\r\n maxLength: (requiredLength) => `Debe tener como máximo ${requiredLength} caracteres.`,\r\n pattern: () => 'El formato no es válido.',\r\n submit: () => 'Enviar',\r\n showPassword: () => 'Mostrar contraseña',\r\n hidePassword: () => 'Ocultar contraseña',\r\n};\r\n","import { InjectionToken, Provider } from '@angular/core';\r\nimport { FormBuilderMessages, FORM_BUILDER_MESSAGES_EN } from '../models/form-builder-messages';\r\n\r\n/**\r\n * DI token for the active `FormBuilderMessages` set. Defaults to\r\n * `FORM_BUILDER_MESSAGES_EN` when no provider is registered.\r\n *\r\n * A per-field `FieldValidatorConfig.errorMessages` override, when set,\r\n * still takes precedence over this token for that specific field: this\r\n * token supplies the message only when the field itself doesn't override\r\n * that validator's text.\r\n *\r\n * Prefer `provideFormBuilderMessages()` over providing this token\r\n * directly, it merges your overrides on top of the English defaults\r\n * instead of requiring every message to be re-specified.\r\n */\r\nexport const FORM_BUILDER_MESSAGES = new InjectionToken<FormBuilderMessages>('FORM_BUILDER_MESSAGES', {\r\n providedIn: 'root',\r\n factory: () => FORM_BUILDER_MESSAGES_EN,\r\n});\r\n\r\n/**\r\n * Registers a `FormBuilderMessages` override, merged on top of the\r\n * English defaults so a partial override (e.g. only `submit`) leaves the\r\n * rest in English instead of requiring the full set.\r\n *\r\n * @param overrides Messages to override. Any member left out falls back\r\n * to `FORM_BUILDER_MESSAGES_EN`.\r\n * @example\r\n * // Full preset:\r\n * providers: [provideFormBuilderMessages(FORM_BUILDER_MESSAGES_ES)]\r\n * @example\r\n * // Partial override, everything else stays in English:\r\n * providers: [provideFormBuilderMessages({ min: () => 'Must be greater than 0' })]\r\n */\r\nexport function provideFormBuilderMessages(overrides: Partial<FormBuilderMessages>): Provider {\r\n return {\r\n provide: FORM_BUILDER_MESSAGES,\r\n useValue: { ...FORM_BUILDER_MESSAGES_EN, ...overrides },\r\n };\r\n}\r\n","// NUNCA usar [innerHTML], bypassSecurityTrustHtml, ni ningún mecanismo de inserción de HTML crudo en el DOM del componente.\r\n// label, placeholder, errorMessages, y FieldOption.label se renderizan siempre con interpolación {{ }} normal de Angular, nunca vía binding de propiedad que inserte HTML.\r\n\r\nimport {\r\n ChangeDetectionStrategy,\r\n Component,\r\n effect,\r\n inject,\r\n input,\r\n linkedSignal,\r\n output,\r\n signal,\r\n untracked,\r\n ViewEncapsulation,\r\n} from '@angular/core';\r\nimport { FormControl, FormGroup, ReactiveFormsModule, ValidatorFn, Validators } from '@angular/forms';\r\nimport { CrossFieldValidator, FieldConfig, FieldOption } from '../models/field-config';\r\nimport { assertSafePattern } from '../utils/safe-pattern';\r\nimport { FORM_BUILDER_MESSAGES } from '../tokens/form-builder-messages.token';\r\n\r\ntype ErrorKey = 'required' | 'min' | 'max' | 'minLength' | 'maxLength' | 'pattern' | 'email';\r\n\r\n// Angular's built-in validators report `minlength`/`maxlength` (all lowercase)\r\n// on `control.errors`, not the camelCase keys `FieldValidatorConfig.errorMessages`\r\n// uses, this bridges the two so consumer-facing config can stay camelCase.\r\nconst RAW_ERROR_KEY_MAP: Record<string, ErrorKey> = {\r\n required: 'required',\r\n min: 'min',\r\n max: 'max',\r\n minlength: 'minLength',\r\n maxlength: 'maxLength',\r\n pattern: 'pattern',\r\n email: 'email',\r\n};\r\n\r\n@Component({\r\n selector: 'lib-form-builder',\r\n imports: [ReactiveFormsModule],\r\n templateUrl: './form-builder.html',\r\n styleUrl: './form-builder.scss',\r\n changeDetection: ChangeDetectionStrategy.OnPush,\r\n encapsulation: ViewEncapsulation.Emulated,\r\n})\r\nexport class FormBuilder<T> {\r\n /**\r\n * Declarative configuration of the fields to render, in display order.\r\n * @example\r\n * fields: FieldConfig<User>[] = [\r\n * { key: 'email', label: 'Email', type: 'email', validators: { required: true, email: true } },\r\n * ];\r\n */\r\n fields = input.required<FieldConfig<T>[]>();\r\n\r\n /**\r\n * Number of grid columns fields are laid out in on desktop. Always\r\n * collapses to a single column below a mobile breakpoint, regardless\r\n * of this value: mobile-first, like the rest of the library.\r\n * @default 1\r\n * @example\r\n * <lib-form-builder [fields]=\"fields\" [columns]=\"2\" />\r\n */\r\n columns = input<number>(1);\r\n\r\n /**\r\n * Form-level validators that check values across multiple fields (e.g.\r\n * confirming a password). Run separately from Angular's native\r\n * per-control validation, only when the native `FormGroup` is valid.\r\n * @default []\r\n * @example\r\n * crossFieldValidators: CrossFieldValidator<User>[] = [\r\n * {\r\n * validate: (value) =>\r\n * value.password !== value.confirmPassword\r\n * ? { confirmPassword: 'Passwords must match' }\r\n * : null,\r\n * },\r\n * ];\r\n */\r\n crossFieldValidators = input<CrossFieldValidator<T>[]>([]);\r\n\r\n /**\r\n * Emitted with the typed form values, only when both the native\r\n * `FormGroup` and every `crossFieldValidators` check pass.\r\n * @example\r\n * <lib-form-builder [fields]=\"fields\" (formSubmit)=\"onSubmit($event)\" />\r\n */\r\n formSubmit = output<T>();\r\n\r\n /**\r\n * Whether the form only reports its values on explicit submit, or\r\n * also continuously as the user edits it. `formSubmit` still fires\r\n * on submit in both modes; `valueChange` only fires when this is\r\n * `'live'`.\r\n * @default 'submit'\r\n * @example\r\n * <lib-form-builder [fields]=\"fields\" mode=\"live\" (valueChange)=\"onValueChange($event)\" />\r\n */\r\n mode = input<'submit' | 'live'>('submit');\r\n\r\n /**\r\n * Emitted with the typed, valid form values, only when `mode` is\r\n * `'live'` (never fires in the default `'submit'` mode). Fires once\r\n * immediately if the form starts valid with its default values (since\r\n * `FormGroup.valueChanges` never emits on its own at startup), then\r\n * again on every subsequent change, as long as both the native\r\n * `FormGroup` and every `crossFieldValidators` check keep passing —\r\n * the same validity criteria `formSubmit` already uses, just\r\n * continuous instead of submit-gated.\r\n * @example\r\n * <lib-form-builder [fields]=\"fields\" mode=\"live\" (valueChange)=\"onValueChange($event)\" />\r\n */\r\n valueChange = output<T>();\r\n\r\n /**\r\n * Errors returned by the backend after a submit attempt (e.g. \"email\r\n * already registered\"), keyed by field. Shown as soon as they're set,\r\n * regardless of whether the field has been touched or the form\r\n * submitted locally; receiving one implies a submit already\r\n * happened. Each message clears itself automatically the moment the\r\n * user edits that field again, so there's no need to clear this input\r\n * back to `{}` manually after showing it once.\r\n * @default {}\r\n * @example\r\n * // In the consumer's component:\r\n * protected readonly serverErrors = signal<Partial<Record<keyof User, string>>>({});\r\n *\r\n * onFormSubmit(value: User) {\r\n * this.api.register(value).subscribe({\r\n * error: () => this.serverErrors.set({ email: 'This email is already registered.' }),\r\n * });\r\n * }\r\n * // Template:\r\n * <lib-form-builder [fields]=\"fields\" [serverErrors]=\"serverErrors()\" (formSubmit)=\"onFormSubmit($event)\" />\r\n */\r\n serverErrors = input<Partial<Record<keyof T, string>>>({});\r\n\r\n /**\r\n * Custom label for the submit button, instead of the injected\r\n * FORM_BUILDER_MESSAGES `submit()` text. Takes precedence over both\r\n * the messages token and the default English label.\r\n * @example\r\n * <lib-form-builder [fields]=\"fields\" submitLabel=\"Create account\" />\r\n */\r\n submitLabel = input<string>();\r\n\r\n /**\r\n * Hides the internal submit button entirely, for a consumer that\r\n * triggers submission from its own UI instead, via the public\r\n * `submit()` method.\r\n * @default false\r\n * @example\r\n * <lib-form-builder #form [fields]=\"fields\" [hideSubmit]=\"true\" />\r\n * <button (click)=\"form.submit()\">Continue</button>\r\n */\r\n hideSubmit = input<boolean>(false);\r\n\r\n /**\r\n * Disables the internal submit button and makes the public `submit()`\r\n * method a no-op while `true`, e.g. during an in-flight request.\r\n * @default false\r\n * @example\r\n * <lib-form-builder [fields]=\"fields\" [loading]=\"saving()\" (formSubmit)=\"onSave($event)\" />\r\n */\r\n loading = input<boolean>(false);\r\n\r\n /**\r\n * External value applied to the form without recreating it. Every\r\n * time this receives a new, non-`undefined` value (including the\r\n * first one), it's patched into the current form\r\n * (`emitEvent: false`, so it never triggers `valueChange` in `'live'`\r\n * mode). Rebuilding `fields()` afterward keeps whatever was patched,\r\n * the same as any other value the user typed.\r\n * @example\r\n * <lib-form-builder [fields]=\"fields\" [value]=\"presetValue()\" />\r\n */\r\n value = input<T>();\r\n\r\n protected readonly messages = inject(FORM_BUILDER_MESSAGES);\r\n\r\n protected readonly submitted = signal(false);\r\n protected readonly crossFieldErrors = signal<Record<string, string>>({});\r\n\r\n // Keys of password fields currently showing plain text instead of\r\n // dots, toggled per-field by the user via the show/hide button.\r\n private readonly visiblePasswordFields = signal<ReadonlySet<string>>(new Set());\r\n\r\n // Snapshot of each server-error field's value at the moment that error\r\n // appeared. A server error is only ever about a rejected backend\r\n // observation of the exact value the user submitted — the instant the\r\n // control's live value no longer matches this snapshot, the message is\r\n // stale and `serverErrorFor` stops showing it, without the consumer\r\n // having to clear `serverErrors` themselves.\r\n private readonly serverErrorSnapshot = signal<Partial<Record<keyof T, unknown>>>({});\r\n\r\n // `fields()` can change at runtime (e.g. a wizard swapping steps), so the\r\n // FormGroup is derived rather than built once in the constructor: it's\r\n // rebuilt only when the `fields()` array reference changes. `linkedSignal`\r\n // (not `computed()`) so `buildFormGroup` can read the previous FormGroup\r\n // and carry forward the current value of every control whose key still\r\n // exists in the new `fields()`, instead of resetting it to `defaultValue`.\r\n protected readonly formGroup = linkedSignal<FieldConfig<T>[], FormGroup>({\r\n source: this.fields,\r\n computation: (fields, previous) => this.buildFormGroup(fields, previous?.value),\r\n });\r\n\r\n private static instanceCounter = 0;\r\n private readonly instanceId = ++FormBuilder.instanceCounter;\r\n\r\n constructor() {\r\n // Runs whenever a new `serverErrors()` value comes in and captures\r\n // each mentioned field's current value — the baseline `serverErrorFor`\r\n // compares against to decide whether that message is still relevant.\r\n effect(() => {\r\n const errors = this.serverErrors();\r\n const group = this.formGroup();\r\n const snapshot: Partial<Record<keyof T, unknown>> = {};\r\n for (const key of Object.keys(errors) as (keyof T)[]) {\r\n snapshot[key] = group.get(String(key))?.value;\r\n }\r\n this.serverErrorSnapshot.set(snapshot);\r\n });\r\n\r\n // Early return keeps this a no-op entirely (no subscription, no\r\n // extra work) for the default 'submit' mode every existing consumer\r\n // is already on. Re-subscribes whenever formGroup() itself changes\r\n // (fields() changed), since that's a brand new FormGroup instance.\r\n effect((onCleanup) => {\r\n if (this.mode() !== 'live') {\r\n return;\r\n }\r\n const group = this.formGroup();\r\n\r\n const emitIfValid = () => {\r\n if (!group.valid) {\r\n return;\r\n }\r\n const value = this.coerceNumberFields(group.getRawValue());\r\n if (Object.keys(this.runCrossFieldValidators(value)).length > 0) {\r\n return;\r\n }\r\n this.valueChange.emit(value);\r\n };\r\n\r\n // FormGroup.valueChanges only fires on an actual value change, never\r\n // on its own at startup — emit once here so a 'live' consumer sees\r\n // the form's initial state immediately, not just after the user's\r\n // first edit.\r\n emitIfValid();\r\n\r\n const subscription = group.valueChanges.subscribe(emitIfValid);\r\n onCleanup(() => subscription.unsubscribe());\r\n });\r\n\r\n // Applies an external `value()` to whichever FormGroup is current at\r\n // that moment, without recreating it. `formGroup()` is deliberately\r\n // read via `untracked()` so this effect only reacts to `value()`\r\n // itself changing, never to a `fields()`-triggered rebuild: `patchValue`\r\n // with `emitEvent: false` also means `valueChange` never fires from this.\r\n effect(() => {\r\n const value = this.value();\r\n if (value === undefined) {\r\n return;\r\n }\r\n const group = untracked(() => this.formGroup());\r\n group.patchValue(value as Record<string, unknown>, { emitEvent: false });\r\n });\r\n }\r\n\r\n private buildFormGroup(fields: FieldConfig<T>[], previous?: FormGroup): FormGroup {\r\n const controls: Record<string, FormControl<unknown>> = {};\r\n for (const field of fields) {\r\n const key = String(field.key);\r\n const previousControl = previous?.get(key);\r\n const initialValue = previousControl\r\n ? previousControl.value\r\n : (field.defaultValue ?? (field.type === 'checkbox' ? false : null));\r\n controls[key] = new FormControl<unknown>(\r\n { value: initialValue, disabled: !!field.disabled },\r\n this.buildValidators(field),\r\n );\r\n }\r\n return new FormGroup(controls);\r\n }\r\n\r\n private buildValidators(field: FieldConfig<T>): ValidatorFn[] {\r\n const config = field.validators;\r\n if (!config) {\r\n return [];\r\n }\r\n\r\n const validators: ValidatorFn[] = [];\r\n if (config.required) {\r\n // A checkbox's \"empty\" value is `false`, which Validators.required\r\n // doesn't treat as empty — requiredTrue is the correct check for it.\r\n validators.push(field.type === 'checkbox' ? Validators.requiredTrue : Validators.required);\r\n }\r\n if (config.min !== undefined) {\r\n validators.push(Validators.min(config.min));\r\n }\r\n if (config.max !== undefined) {\r\n validators.push(Validators.max(config.max));\r\n }\r\n if (config.minLength !== undefined) {\r\n validators.push(Validators.minLength(config.minLength));\r\n }\r\n if (config.maxLength !== undefined) {\r\n validators.push(Validators.maxLength(config.maxLength));\r\n }\r\n if (config.pattern !== undefined) {\r\n // An unsafe pattern is a bug in the consumer's config, not a runtime\r\n // condition to recover from — let it throw instead of swallowing it.\r\n assertSafePattern(config.pattern);\r\n validators.push(Validators.pattern(config.pattern));\r\n }\r\n if (config.email) {\r\n validators.push(Validators.email);\r\n }\r\n return validators;\r\n }\r\n\r\n protected controlFor(field: FieldConfig<T>): FormControl {\r\n return this.formGroup().get(String(field.key)) as FormControl;\r\n }\r\n\r\n protected optionsFor(field: FieldConfig<T>): FieldOption[] {\r\n return field.options ?? [];\r\n }\r\n\r\n protected showPasswordToggleFor(field: FieldConfig<T>): boolean {\r\n return field.showPasswordToggle ?? true;\r\n }\r\n\r\n protected isPasswordVisible(field: FieldConfig<T>): boolean {\r\n return this.visiblePasswordFields().has(String(field.key));\r\n }\r\n\r\n protected passwordInputType(field: FieldConfig<T>): 'text' | 'password' {\r\n return this.isPasswordVisible(field) ? 'text' : 'password';\r\n }\r\n\r\n protected togglePasswordVisibility(field: FieldConfig<T>): void {\r\n const key = String(field.key);\r\n const next = new Set(this.visiblePasswordFields());\r\n if (next.has(key)) {\r\n next.delete(key);\r\n } else {\r\n next.add(key);\r\n }\r\n this.visiblePasswordFields.set(next);\r\n }\r\n\r\n // Shared by the label's `for`, the control's `id`, and (for radio groups)\r\n // the `name` that keeps this field's radios from being grouped with\r\n // another field's — RadioControlValueAccessor matches radios by `name`\r\n // plus parent FormGroup, and every field here shares the same FormGroup.\r\n protected fieldId(field: FieldConfig<T>): string {\r\n return `fb-${this.instanceId}-${String(field.key)}`;\r\n }\r\n\r\n protected errorMessageFor(field: FieldConfig<T>): string | null {\r\n const control = this.controlFor(field);\r\n if (!control.errors || (!control.touched && !this.submitted())) {\r\n return null;\r\n }\r\n\r\n const rawKey = Object.keys(control.errors)[0];\r\n const errorKey = RAW_ERROR_KEY_MAP[rawKey];\r\n if (!errorKey) {\r\n return null;\r\n }\r\n\r\n // Precedence: a per-field override always wins, then the injected\r\n // FORM_BUILDER_MESSAGES token (English by default), evaluated fresh\r\n // here so a message reading a signal (e.g. a language switcher)\r\n // updates live.\r\n const fieldMessage = field.validators?.errorMessages?.[errorKey];\r\n if (fieldMessage) {\r\n return fieldMessage;\r\n }\r\n\r\n const errorDetail = control.errors[rawKey];\r\n switch (errorKey) {\r\n case 'required':\r\n return this.messages.required();\r\n case 'email':\r\n return this.messages.email();\r\n case 'min':\r\n return this.messages.min(errorDetail.min);\r\n case 'max':\r\n return this.messages.max(errorDetail.max);\r\n case 'minLength':\r\n return this.messages.minLength(errorDetail.requiredLength);\r\n case 'maxLength':\r\n return this.messages.maxLength(errorDetail.requiredLength);\r\n case 'pattern':\r\n return this.messages.pattern();\r\n }\r\n }\r\n\r\n protected serverErrorFor(field: FieldConfig<T>): string | null {\r\n const message = this.serverErrors()[field.key];\r\n if (!message) {\r\n return null;\r\n }\r\n\r\n const currentValue = this.controlFor(field).value;\r\n const snapshotValue = this.serverErrorSnapshot()[field.key];\r\n return currentValue === snapshotValue ? message : null;\r\n }\r\n\r\n protected crossFieldErrorFor(field: FieldConfig<T>): string | null {\r\n if (!this.submitted()) {\r\n return null;\r\n }\r\n return this.crossFieldErrors()[String(field.key)] ?? null;\r\n }\r\n\r\n // Single source of truth for \"is an error currently shown for this\r\n // field, and what does it say\": both the visible `.fb-error` message\r\n // and its ARIA wiring (aria-invalid, aria-describedby) read from this,\r\n // so the two can never drift out of sync with each other.\r\n protected activeErrorFor(field: FieldConfig<T>): string | null {\r\n return this.errorMessageFor(field) ?? this.serverErrorFor(field) ?? this.crossFieldErrorFor(field);\r\n }\r\n\r\n protected errorId(field: FieldConfig<T>): string {\r\n return `${this.fieldId(field)}-error`;\r\n }\r\n\r\n protected legendId(field: FieldConfig<T>): string {\r\n return `${this.fieldId(field)}-legend`;\r\n }\r\n\r\n protected isSubmitDisabled(): boolean {\r\n return this.loading() || this.formGroup().invalid || Object.keys(this.crossFieldErrors()).length > 0;\r\n }\r\n\r\n /**\r\n * Triggers the exact same submit logic the internal button triggers:\r\n * marks the form as submitted, runs validation, and emits `formSubmit`\r\n * if it passes. Works identically whether `hideSubmit` is `true` or\r\n * `false`, and is a no-op while `loading` is `true`.\r\n * @example\r\n * <lib-form-builder #form [fields]=\"fields\" [hideSubmit]=\"true\" />\r\n * <button (click)=\"form.submit()\">Continue</button>\r\n */\r\n submit(): void {\r\n this.performSubmit();\r\n }\r\n\r\n protected performSubmit(): void {\r\n if (this.loading()) {\r\n return;\r\n }\r\n\r\n this.submitted.set(true);\r\n const group = this.formGroup();\r\n\r\n if (group.invalid) {\r\n return;\r\n }\r\n\r\n // `.value` excludes disabled controls entirely, `getRawValue()` keeps\r\n // them, so a disabled field with a `defaultValue` still comes through\r\n // as a complete `T` instead of a hole in the emitted object.\r\n const value = this.coerceNumberFields(group.getRawValue());\r\n const errors = this.runCrossFieldValidators(value);\r\n this.crossFieldErrors.set(errors);\r\n\r\n if (Object.keys(errors).length > 0) {\r\n return;\r\n }\r\n\r\n this.formSubmit.emit(value);\r\n }\r\n\r\n // Native `<input type=\"number\">` always reports its value as a string\r\n // through the DOM. Angular's NumberValueAccessor would normally parse it\r\n // for us, but that accessor only activates for a *static* `type=\"number\"`\r\n // attribute in the template — this component binds `[type]=\"field.type\"`\r\n // dynamically, so it never matches, and DefaultValueAccessor (string in,\r\n // string out) handles the control instead. Fixed up here, at the point\r\n // the value leaves the component, rather than in how the FormControl\r\n // stores it: Validators.min/max already parseFloat internally either\r\n // way, so correcting the stored value would change nothing there but\r\n // would still need this same conversion for the emitted object.\r\n private coerceNumberFields(rawValue: unknown): T {\r\n const value = { ...(rawValue as Record<string, unknown>) };\r\n for (const field of this.fields()) {\r\n if (field.type !== 'number') {\r\n continue;\r\n }\r\n\r\n const key = String(field.key);\r\n const current = value[key];\r\n if (current === '' || current === null || current === undefined) {\r\n value[key] = null;\r\n } else if (typeof current === 'string') {\r\n value[key] = Number(current);\r\n }\r\n }\r\n return value as T;\r\n }\r\n\r\n private runCrossFieldValidators(value: T): Record<string, string> {\r\n const errors: Record<string, string> = {};\r\n for (const validator of this.crossFieldValidators()) {\r\n const result = validator.validate(value);\r\n if (result) {\r\n Object.assign(errors, result);\r\n }\r\n }\r\n return errors;\r\n }\r\n}\r\n","<form class=\"fb-form\" [formGroup]=\"formGroup()\" (ngSubmit)=\"performSubmit()\" novalidate>\r\n <div class=\"fb-fields-grid\" [style.--fb-columns]=\"columns()\">\r\n @for (field of fields(); track field.key) {\r\n <div class=\"fb-field\" [class.fb-field--span-2]=\"field.colSpan === 2\">\r\n @if (field.type !== 'checkbox' && field.type !== 'radio') {\r\n <label class=\"fb-label\" [attr.for]=\"fieldId(field)\">{{ field.label }}</label>\r\n }\r\n\r\n @switch (field.type) {\r\n @case ('checkbox') {\r\n <label class=\"fb-checkbox-option\">\r\n <input\r\n type=\"checkbox\"\r\n [formControl]=\"controlFor(field)\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n />\r\n {{ field.label }}\r\n </label>\r\n }\r\n @case ('radio') {\r\n <fieldset\r\n class=\"fb-radio-group\"\r\n role=\"radiogroup\"\r\n [attr.aria-labelledby]=\"legendId(field)\"\r\n >\r\n <legend class=\"fb-label\" [id]=\"legendId(field)\">{{ field.label }}</legend>\r\n @for (option of optionsFor(field); track option.value) {\r\n <label class=\"fb-radio-option\">\r\n <input\r\n type=\"radio\"\r\n [formControl]=\"controlFor(field)\"\r\n [name]=\"fieldId(field)\"\r\n [value]=\"option.value\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n />\r\n {{ option.label }}\r\n </label>\r\n }\r\n </fieldset>\r\n }\r\n @case ('password') {\r\n <div class=\"fb-password-wrapper\">\r\n <input\r\n class=\"fb-control\"\r\n [id]=\"fieldId(field)\"\r\n [type]=\"passwordInputType(field)\"\r\n [formControl]=\"controlFor(field)\"\r\n [placeholder]=\"field.placeholder ?? ''\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n />\r\n @if (showPasswordToggleFor(field)) {\r\n <button\r\n type=\"button\"\r\n class=\"fb-password-toggle\"\r\n [attr.aria-pressed]=\"isPasswordVisible(field)\"\r\n [attr.aria-label]=\"isPasswordVisible(field) ? messages.hidePassword() : messages.showPassword()\"\r\n (click)=\"togglePasswordVisibility(field)\"\r\n >\r\n @if (isPasswordVisible(field)) {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" aria-hidden=\"true\">\r\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3 3l18 18M10.58 10.58a2 2 0 002.83 2.83M9.88 4.24A9.9 9.9 0 0112 4c5 0 9 4 10 8-.32 1.12-.86 2.19-1.57 3.14M6.6 6.6C3.9 8.14 2.06 10.6 2 12c.66 2.34 2.19 4.34 4.19 5.68M14.12 14.12A3 3 0 019.88 9.88\" />\r\n </svg>\r\n } @else {\r\n <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" aria-hidden=\"true\">\r\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z\" />\r\n <circle cx=\"12\" cy=\"12\" r=\"3\" stroke-linecap=\"round\" stroke-linejoin=\"round\" />\r\n </svg>\r\n }\r\n </button>\r\n }\r\n </div>\r\n }\r\n @case ('textarea') {\r\n <textarea\r\n class=\"fb-control\"\r\n [id]=\"fieldId(field)\"\r\n [formControl]=\"controlFor(field)\"\r\n [placeholder]=\"field.placeholder ?? ''\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n ></textarea>\r\n }\r\n @case ('select') {\r\n <select\r\n class=\"fb-control\"\r\n [id]=\"fieldId(field)\"\r\n [formControl]=\"controlFor(field)\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n >\r\n @for (option of optionsFor(field); track option.value) {\r\n <option [ngValue]=\"option.value\">{{ option.label }}</option>\r\n }\r\n </select>\r\n }\r\n @default {\r\n <input\r\n class=\"fb-control\"\r\n [id]=\"fieldId(field)\"\r\n [type]=\"field.type\"\r\n [formControl]=\"controlFor(field)\"\r\n [placeholder]=\"field.placeholder ?? ''\"\r\n [disabled]=\"!!field.disabled\"\r\n [attr.aria-invalid]=\"activeErrorFor(field) ? true : null\"\r\n [attr.aria-describedby]=\"activeErrorFor(field) ? errorId(field) : null\"\r\n />\r\n }\r\n }\r\n\r\n @if (activeErrorFor(field); as message) {\r\n <p class=\"fb-error\" [id]=\"errorId(field)\">{{ message }}</p>\r\n }\r\n </div>\r\n }\r\n </div>\r\n\r\n @if (!hideSubmit()) {\r\n <button type=\"submit\" class=\"fb-submit\" [disabled]=\"isSubmitDisabled()\">{{ submitLabel() ?? messages.submit() }}</button>\r\n }\r\n</form>\r\n","import { NgModule } from '@angular/core';\r\nimport { FormBuilder } from './form-builder';\r\n\r\n/**\r\n * NgModule wrapper for `FormBuilder<T>`, for consumers still on a\r\n * classic NgModule architecture. `FormBuilder` is a standalone\r\n * component; importing it directly (`imports: [FormBuilder]`) is still\r\n * the recommended way to consume this library. Use this module only if\r\n * your app doesn't use standalone components yet.\r\n *\r\n * The generic type parameter `T` is inferred per usage from the bound\r\n * `[fields]` input, the same way it is with standalone consumption:\r\n * this module only changes how the component is imported, not how its\r\n * generic is resolved.\r\n *\r\n * @example\r\n * @NgModule({\r\n * imports: [FormBuilderModule],\r\n * })\r\n * export class SignupPageModule {}\r\n */\r\n@NgModule({\r\n imports: [FormBuilder],\r\n exports: [FormBuilder],\r\n})\r\nexport class FormBuilderModule {}\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;;;AAAA,MAAM,kBAAkB,GAAG,GAAG;AAE9B;AACA,MAAM,iBAAiB,GAAG,0BAA0B;AAEpD;AACA,MAAM,cAAc,GAAG,mBAAmB;AAE1C;;;;;;;;;;;;;AAaG;AACG,SAAU,iBAAiB,CAAC,OAAe,EAAA;AAC/C,IAAA,IAAI,OAAO,CAAC,MAAM,GAAG,kBAAkB,EAAE;AACvC,QAAA,MAAM,IAAI,KAAK,CACb,qCAAqC,kBAAkB,CAAA,iBAAA,CAAmB,CAC3E;IACH;AAEA,IAAA,IAAI,iBAAiB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE;AACnC,QAAA,MAAM,IAAI,KAAK,CACb,0GAA0G,CAC3G;IACH;AAEA,IAAA,IAAI,yBAAyB,CAAC,OAAO,CAAC,EAAE;AACtC,QAAA,MAAM,IAAI,KAAK,CACb,wIAAwI,CACzI;IACH;AACF;AAEA,SAAS,yBAAyB,CAAC,OAAe,EAAA;AAChD,IAAA,MAAM,aAAa,GAAG,IAAI,MAAM,CAAC,cAAc,CAAC;AAChD,IAAA,IAAI,KAA6B;AAEjC,IAAA,OAAO,CAAC,KAAK,GAAG,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE;QACrD,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC;AACpC,QAAA,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;AACxC,YAAA,KAAK,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;AAC5C,gBAAA,IAAI,eAAe,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE;AAC7C,oBAAA,OAAO,IAAI;gBACb;YACF;QACF;IACF;AAEA,IAAA,OAAO,KAAK;AACd;AAEA,SAAS,eAAe,CAAC,CAAS,EAAE,CAAS,EAAA;IAC3C,IAAI,CAAC,KAAK,CAAC;AAAE,QAAA,OAAO,IAAI;AACxB,IAAA,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC;AAAE,QAAA,OAAO,KAAK;AAC1B,IAAA,OAAO,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;AAC3C;;ACIA;;;AAGG;AACI,MAAM,wBAAwB,GAAwB;AAC3D,IAAA,QAAQ,EAAE,MAAM,yBAAyB;AACzC,IAAA,KAAK,EAAE,MAAM,8BAA8B;IAC3C,GAAG,EAAE,CAAC,GAAG,KAAK,CAAA,2BAAA,EAA8B,GAAG,CAAA,CAAA,CAAG;IAClD,GAAG,EAAE,CAAC,GAAG,KAAK,CAAA,0BAAA,EAA6B,GAAG,CAAA,CAAA,CAAG;IACjD,SAAS,EAAE,CAAC,cAAc,KAAK,CAAA,iBAAA,EAAoB,cAAc,CAAA,YAAA,CAAc;IAC/E,SAAS,EAAE,CAAC,cAAc,KAAK,CAAA,gBAAA,EAAmB,cAAc,CAAA,YAAA,CAAc;AAC9E,IAAA,OAAO,EAAE,MAAM,0BAA0B;AACzC,IAAA,MAAM,EAAE,MAAM,QAAQ;AACtB,IAAA,YAAY,EAAE,MAAM,eAAe;AACnC,IAAA,YAAY,EAAE,MAAM,eAAe;;AAGrC;;;;AAIG;AACI,MAAM,wBAAwB,GAAwB;AAC3D,IAAA,QAAQ,EAAE,MAAM,4BAA4B;AAC5C,IAAA,KAAK,EAAE,MAAM,wBAAwB;IACrC,GAAG,EAAE,CAAC,GAAG,KAAK,CAAA,8BAAA,EAAiC,GAAG,CAAA,CAAA,CAAG;IACrD,GAAG,EAAE,CAAC,GAAG,KAAK,CAAA,8BAAA,EAAiC,GAAG,CAAA,CAAA,CAAG;IACrD,SAAS,EAAE,CAAC,cAAc,KAAK,CAAA,oBAAA,EAAuB,cAAc,CAAA,YAAA,CAAc;IAClF,SAAS,EAAE,CAAC,cAAc,KAAK,CAAA,uBAAA,EAA0B,cAAc,CAAA,YAAA,CAAc;AACrF,IAAA,OAAO,EAAE,MAAM,0BAA0B;AACzC,IAAA,MAAM,EAAE,MAAM,QAAQ;AACtB,IAAA,YAAY,EAAE,MAAM,oBAAoB;AACxC,IAAA,YAAY,EAAE,MAAM,oBAAoB;;;ACjG1C;;;;;;;;;;;;AAYG;MACU,qBAAqB,GAAG,IAAI,cAAc,CAAsB,uBAAuB,EAAE;AACpG,IAAA,UAAU,EAAE,MAAM;AAClB,IAAA,OAAO,EAAE,MAAM,wBAAwB;AACxC,CAAA;AAED;;;;;;;;;;;;;AAaG;AACG,SAAU,0BAA0B,CAAC,SAAuC,EAAA;IAChF,OAAO;AACL,QAAA,OAAO,EAAE,qBAAqB;AAC9B,QAAA,QAAQ,EAAE,EAAE,GAAG,wBAAwB,EAAE,GAAG,SAAS,EAAE;KACxD;AACH;;ACxCA;AACA;AAqBA;AACA;AACA;AACA,MAAM,iBAAiB,GAA6B;AAClD,IAAA,QAAQ,EAAE,UAAU;AACpB,IAAA,GAAG,EAAE,KAAK;AACV,IAAA,GAAG,EAAE,KAAK;AACV,IAAA,SAAS,EAAE,WAAW;AACtB,IAAA,SAAS,EAAE,WAAW;AACtB,IAAA,OAAO,EAAE,SAAS;AAClB,IAAA,KAAK,EAAE,OAAO;CACf;MAUY,WAAW,CAAA;AACtB;;;;;;AAMG;AACH,IAAA,MAAM,GAAG,KAAK,CAAC,QAAQ,4EAAoB;AAE3C;;;;;;;AAOG;AACH,IAAA,OAAO,GAAG,KAAK,CAAS,CAAC,8EAAC;AAE1B;;;;;;;;;;;;;;AAcG;AACH,IAAA,oBAAoB,GAAG,KAAK,CAA2B,EAAE,2FAAC;AAE1D;;;;;AAKG;IACH,UAAU,GAAG,MAAM,EAAK;AAExB;;;;;;;;AAQG;AACH,IAAA,IAAI,GAAG,KAAK,CAAoB,QAAQ,2EAAC;AAEzC;;;;;;;;;;;AAWG;IACH,WAAW,GAAG,MAAM,EAAK;AAEzB;;;;;;;;;;;;;;;;;;;;AAoBG;AACH,IAAA,YAAY,GAAG,KAAK,CAAmC,EAAE,mFAAC;AAE1D;;;;;;AAMG;IACH,WAAW,GAAG,KAAK,CAAA,IAAA,SAAA,GAAA,CAAA,SAAA,EAAA,EAAA,SAAA,EAAA,aAAA,EAAA,CAAA,8BAAA,EAAA,CAAA,CAAU;AAE7B;;;;;;;;AAQG;AACH,IAAA,UAAU,GAAG,KAAK,CAAU,KAAK,iFAAC;AAElC;;;;;;AAMG;AACH,IAAA,OAAO,GAAG,KAAK,CAAU,KAAK,8EAAC;AAE/B;;;;;;;;;AASG;IACH,KAAK,GAAG,KAAK,CAAA,IAAA,SAAA,GAAA,CAAA,SAAA,EAAA,EAAA,SAAA,EAAA,OAAA,EAAA,CAAA,8BAAA,EAAA,CAAA,CAAK;AAEC,IAAA,QAAQ,GAAG,MAAM,CAAC,qBAAqB,CAAC;AAExC,IAAA,SAAS,GAAG,MAAM,CAAC,KAAK,gFAAC;AACzB,IAAA,gBAAgB,GAAG,MAAM,CAAyB,EAAE,uFAAC;;;AAIvD,IAAA,qBAAqB,GAAG,MAAM,CAAsB,IAAI,GAAG,EAAE,4FAAC;;;;;;;AAQ9D,IAAA,mBAAmB,GAAG,MAAM,CAAoC,EAAE,0FAAC;;;;;;;AAQjE,IAAA,SAAS,GAAG,YAAY,CAAA,EAAA,IAAA,SAAA,GAAA,EAAA,SAAA,EAAA,WAAA,EAAA,8BAAA,EAAA,CAAA,EACzC,MAAM,EAAE,IAAI,CAAC,MAAM;AACnB,QAAA,WAAW,EAAE,CAAC,MAAM,EAAE,QAAQ,KAAK,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,QAAQ,EAAE,KAAK,CAAC,GAC/E;AAEM,IAAA,OAAO,eAAe,GAAG,CAAC;AACjB,IAAA,UAAU,GAAG,EAAE,WAAW,CAAC,eAAe;AAE3D,IAAA,WAAA,GAAA;;;;QAIE,MAAM,CAAC,MAAK;AACV,YAAA,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,EAAE;AAClC,YAAA,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,EAAE;YAC9B,MAAM,QAAQ,GAAsC,EAAE;YACtD,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAgB,EAAE;AACpD,gBAAA,QAAQ,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK;YAC/C;AACA,YAAA,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,QAAQ,CAAC;AACxC,QAAA,CAAC,CAAC;;;;;AAMF,QAAA,MAAM,CAAC,CAAC,SAAS,KAAI;AACnB,YAAA,IAAI,IAAI,CAAC,IAAI,EAAE,KAAK,MAAM,EAAE;gBAC1B;YACF;AACA,YAAA,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,EAAE;YAE9B,MAAM,WAAW,GAAG,MAAK;AACvB,gBAAA,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE;oBAChB;gBACF;gBACA,MAAM,KAAK,GAAG,IAAI,CAAC,kBAAkB,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;AAC1D,gBAAA,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,uBAAuB,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE;oBAC/D;gBACF;AACA,gBAAA,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC;AAC9B,YAAA,CAAC;;;;;AAMD,YAAA,WAAW,EAAE;YAEb,MAAM,YAAY,GAAG,KAAK,CAAC,YAAY,CAAC,SAAS,CAAC,WAAW,CAAC;YAC9D,SAAS,CAAC,MAAM,YAAY,CAAC,WAAW,EAAE,CAAC;AAC7C,QAAA,CAAC,CAAC;;;;;;QAOF,MAAM,CAAC,MAAK;AACV,YAAA,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,EAAE;AAC1B,YAAA,IAAI,KAAK,KAAK,SAAS,EAAE;gBACvB;YACF;AACA,YAAA,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC;YAC/C,KAAK,CAAC,UAAU,CAAC,KAAgC,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;AAC1E,QAAA,CAAC,CAAC;IACJ;IAEQ,cAAc,CAAC,MAAwB,EAAE,QAAoB,EAAA;QACnE,MAAM,QAAQ,GAAyC,EAAE;AACzD,QAAA,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE;YAC1B,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC;YAC7B,MAAM,eAAe,GAAG,QAAQ,EAAE,GAAG,CAAC,GAAG,CAAC;YAC1C,MAAM,YAAY,GAAG;kBACjB,eAAe,CAAC;mBACf,KAAK,CAAC,YAAY,KAAK,KAAK,CAAC,IAAI,KAAK,UAAU,GAAG,KAAK,GAAG,IAAI,CAAC,CAAC;AACtE,YAAA,QAAQ,CAAC,GAAG,CAAC,GAAG,IAAI,WAAW,CAC7B,EAAE,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,QAAQ,EAAE,EACnD,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,CAC5B;QACH;AACA,QAAA,OAAO,IAAI,SAAS,CAAC,QAAQ,CAAC;IAChC;AAEQ,IAAA,eAAe,CAAC,KAAqB,EAAA;AAC3C,QAAA,MAAM,MAAM,GAAG,KAAK,CAAC,UAAU;QAC/B,IAAI,CAAC,MAAM,EAAE;AACX,YAAA,OAAO,EAAE;QACX;QAEA,MAAM,UAAU,GAAkB,EAAE;AACpC,QAAA,IAAI,MAAM,CAAC,QAAQ,EAAE;;;YAGnB,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,UAAU,GAAG,UAAU,CAAC,YAAY,GAAG,UAAU,CAAC,QAAQ,CAAC;QAC5F;AACA,QAAA,IAAI,MAAM,CAAC,GAAG,KAAK,SAAS,EAAE;AAC5B,YAAA,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC7C;AACA,QAAA,IAAI,MAAM,CAAC,GAAG,KAAK,SAAS,EAAE;AAC5B,YAAA,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC7C;AACA,QAAA,IAAI,MAAM,CAAC,SAAS,KAAK,SAAS,EAAE;AAClC,YAAA,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QACzD;AACA,QAAA,IAAI,MAAM,CAAC,SAAS,KAAK,SAAS,EAAE;AAClC,YAAA,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QACzD;AACA,QAAA,IAAI,MAAM,CAAC,OAAO,KAAK,SAAS,EAAE;;;AAGhC,YAAA,iBAAiB,CAAC,MAAM,CAAC,OAAO,CAAC;AACjC,YAAA,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACrD;AACA,QAAA,IAAI,MAAM,CAAC,KAAK,EAAE;AAChB,YAAA,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;QACnC;AACA,QAAA,OAAO,UAAU;IACnB;AAEU,IAAA,UAAU,CAAC,KAAqB,EAAA;AACxC,QAAA,OAAO,IAAI,CAAC,SAAS,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAgB;IAC/D;AAEU,IAAA,UAAU,CAAC,KAAqB,EAAA;AACxC,QAAA,OAAO,KAAK,CAAC,OAAO,IAAI,EAAE;IAC5B;AAEU,IAAA,qBAAqB,CAAC,KAAqB,EAAA;AACnD,QAAA,OAAO,KAAK,CAAC,kBAAkB,IAAI,IAAI;IACzC;AAEU,IAAA,iBAAiB,CAAC,KAAqB,EAAA;AAC/C,QAAA,OAAO,IAAI,CAAC,qBAAqB,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC5D;AAEU,IAAA,iBAAiB,CAAC,KAAqB,EAAA;AAC/C,QAAA,OAAO,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,GAAG,MAAM,GAAG,UAAU;IAC5D;AAEU,IAAA,wBAAwB,CAAC,KAAqB,EAAA;QACtD,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC;QAC7B,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,qBAAqB,EAAE,CAAC;AAClD,QAAA,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE;AACjB,YAAA,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC;QAClB;aAAO;AACL,YAAA,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;QACf;AACA,QAAA,IAAI,CAAC,qBAAqB,CAAC,GAAG,CAAC,IAAI,CAAC;IACtC;;;;;AAMU,IAAA,OAAO,CAAC,KAAqB,EAAA;AACrC,QAAA,OAAO,CAAA,GAAA,EAAM,IAAI,CAAC,UAAU,CAAA,CAAA,EAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA,CAAE;IACrD;AAEU,IAAA,eAAe,CAAC,KAAqB,EAAA;QAC7C,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;AACtC,QAAA,IAAI,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,OAAO,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,EAAE;AAC9D,YAAA,OAAO,IAAI;QACb;AAEA,QAAA,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;AAC7C,QAAA,MAAM,QAAQ,GAAG,iBAAiB,CAAC,MAAM,CAAC;QAC1C,IAAI,CAAC,QAAQ,EAAE;AACb,YAAA,OAAO,IAAI;QACb;;;;;QAMA,MAAM,YAAY,GAAG,KAAK,CAAC,UAAU,EAAE,aAAa,GAAG,QAAQ,CAAC;QAChE,IAAI,YAAY,EAAE;AAChB,YAAA,OAAO,YAAY;QACrB;QAEA,MAAM,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC;QAC1C,QAAQ,QAAQ;AACd,YAAA,KAAK,UAAU;AACb,gBAAA,OAAO,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE;AACjC,YAAA,KAAK,OAAO;AACV,gBAAA,OAAO,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE;AAC9B,YAAA,KAAK,KAAK;gBACR,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC;AAC3C,YAAA,KAAK,KAAK;gBACR,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC;AAC3C,YAAA,KAAK,WAAW;gBACd,OAAO,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,WAAW,CAAC,cAAc,CAAC;AAC5D,YAAA,KAAK,WAAW;gBACd,OAAO,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,WAAW,CAAC,cAAc,CAAC;AAC5D,YAAA,KAAK,SAAS;AACZ,gBAAA,OAAO,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE;;IAEpC;AAEU,IAAA,cAAc,CAAC,KAAqB,EAAA;QAC5C,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC;QAC9C,IAAI,CAAC,OAAO,EAAE;AACZ,YAAA,OAAO,IAAI;QACb;QAEA,MAAM,YAAY,GAAG,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,KAAK;QACjD,MAAM,aAAa,GAAG,IAAI,CAAC,mBAAmB,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC;QAC3D,OAAO,YAAY,KAAK,aAAa,GAAG,OAAO,GAAG,IAAI;IACxD;AAEU,IAAA,kBAAkB,CAAC,KAAqB,EAAA;AAChD,QAAA,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE;AACrB,YAAA,OAAO,IAAI;QACb;AACA,QAAA,OAAO,IAAI,CAAC,gBAAgB,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,IAAI;IAC3D;;;;;AAMU,IAAA,cAAc,CAAC,KAAqB,EAAA;QAC5C,OAAO,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC,kBAAkB,CAAC,KAAK,CAAC;IACpG;AAEU,IAAA,OAAO,CAAC,KAAqB,EAAA;QACrC,OAAO,CAAA,EAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ;IACvC;AAEU,IAAA,QAAQ,CAAC,KAAqB,EAAA;QACtC,OAAO,CAAA,EAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS;IACxC;IAEU,gBAAgB,GAAA;QACxB,OAAO,IAAI,CAAC,OAAO,EAAE,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC,OAAO,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC;IACtG;AAEA;;;;;;;;AAQG;IACH,MAAM,GAAA;QACJ,IAAI,CAAC,aAAa,EAAE;IACtB;IAEU,aAAa,GAAA;AACrB,QAAA,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE;YAClB;QACF;AAEA,QAAA,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC;AACxB,QAAA,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,EAAE;AAE9B,QAAA,IAAI,KAAK,CAAC,OAAO,EAAE;YACjB;QACF;;;;QAKA,MAAM,KAAK,GAAG,IAAI,CAAC,kBAAkB,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC;QAC1D,MAAM,MAAM,GAAG,IAAI,CAAC,uBAAuB,CAAC,KAAK,CAAC;AAClD,QAAA,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,MAAM,CAAC;QAEjC,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE;YAClC;QACF;AAEA,QAAA,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC;IAC7B;;;;;;;;;;;AAYQ,IAAA,kBAAkB,CAAC,QAAiB,EAAA;AAC1C,QAAA,MAAM,KAAK,GAAG,EAAE,GAAI,QAAoC,EAAE;QAC1D,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,EAAE,EAAE;AACjC,YAAA,IAAI,KAAK,CAAC,IAAI,KAAK,QAAQ,EAAE;gBAC3B;YACF;YAEA,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC;AAC7B,YAAA,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC;AAC1B,YAAA,IAAI,OAAO,KAAK,EAAE,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,SAAS,EAAE;AAC/D,gBAAA,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI;YACnB;AAAO,iBAAA,IAAI,OAAO,OAAO,KAAK,QAAQ,EAAE;gBACtC,KAAK,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC;YAC9B;QACF;AACA,QAAA,OAAO,KAAU;IACnB;AAEQ,IAAA,uBAAuB,CAAC,KAAQ,EAAA;QACtC,MAAM,MAAM,GAA2B,EAAE;QACzC,KAAK,MAAM,SAAS,IAAI,IAAI,CAAC,oBAAoB,EAAE,EAAE;YACnD,MAAM,MAAM,GAAG,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC;YACxC,IAAI,MAAM,EAAE;AACV,gBAAA,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC;YAC/B;QACF;AACA,QAAA,OAAO,MAAM;IACf;wGAtdW,WAAW,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;4FAAX,WAAW,EAAA,YAAA,EAAA,IAAA,EAAA,QAAA,EAAA,kBAAA,EAAA,MAAA,EAAA,EAAA,MAAA,EAAA,EAAA,iBAAA,EAAA,QAAA,EAAA,UAAA,EAAA,QAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,IAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,OAAA,EAAA,EAAA,iBAAA,EAAA,SAAA,EAAA,UAAA,EAAA,SAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,oBAAA,EAAA,EAAA,iBAAA,EAAA,sBAAA,EAAA,UAAA,EAAA,sBAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,IAAA,EAAA,EAAA,iBAAA,EAAA,MAAA,EAAA,UAAA,EAAA,MAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,YAAA,EAAA,EAAA,iBAAA,EAAA,cAAA,EAAA,UAAA,EAAA,cAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,WAAA,EAAA,EAAA,iBAAA,EAAA,aAAA,EAAA,UAAA,EAAA,aAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,UAAA,EAAA,EAAA,iBAAA,EAAA,YAAA,EAAA,UAAA,EAAA,YAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,OAAA,EAAA,EAAA,iBAAA,EAAA,SAAA,EAAA,UAAA,EAAA,SAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,KAAA,EAAA,EAAA,iBAAA,EAAA,OAAA,EAAA,UAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,KAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,EAAA,OAAA,EAAA,EAAA,UAAA,EAAA,YAAA,EAAA,WAAA,EAAA,aAAA,EAAA,EAAA,QAAA,EAAA,EAAA,EAAA,QAAA,EC3CxB,grMAgIA,EAAA,MAAA,EAAA,CAAA,4nGAAA,CAAA,EAAA,YAAA,EAAA,CAAA,EAAA,IAAA,EAAA,UAAA,EAAA,IAAA,ED3FY,mBAAmB,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAA,EAAA,CAAA,aAAA,EAAA,QAAA,EAAA,8CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAA,EAAA,CAAA,cAAA,EAAA,QAAA,EAAA,QAAA,EAAA,MAAA,EAAA,CAAA,SAAA,EAAA,OAAA,CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAA,EAAA,CAAA,uBAAA,EAAA,QAAA,EAAA,QAAA,EAAA,MAAA,EAAA,CAAA,SAAA,EAAA,OAAA,CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAA,EAAA,CAAA,oBAAA,EAAA,QAAA,EAAA,8MAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAA,EAAA,CAAA,4BAAA,EAAA,QAAA,EAAA,uGAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAA,EAAA,CAAA,0BAAA,EAAA,QAAA,EAAA,6GAAA,EAAA,MAAA,EAAA,CAAA,aAAA,CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAA,EAAA,CAAA,yBAAA,EAAA,QAAA,EAAA,8FAAA,EAAA,MAAA,EAAA,CAAA,MAAA,EAAA,iBAAA,EAAA,OAAA,CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAA,EAAA,CAAA,eAAA,EAAA,QAAA,EAAA,2CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAA,EAAA,CAAA,oBAAA,EAAA,QAAA,EAAA,sGAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAA,EAAA,CAAA,oBAAA,EAAA,QAAA,EAAA,eAAA,EAAA,MAAA,EAAA,CAAA,aAAA,EAAA,UAAA,EAAA,SAAA,CAAA,EAAA,OAAA,EAAA,CAAA,eAAA,CAAA,EAAA,QAAA,EAAA,CAAA,QAAA,CAAA,EAAA,EAAA,EAAA,IAAA,EAAA,WAAA,EAAA,IAAA,EAAA,EAAA,CAAA,kBAAA,EAAA,QAAA,EAAA,aAAA,EAAA,MAAA,EAAA,CAAA,WAAA,CAAA,EAAA,OAAA,EAAA,CAAA,UAAA,CAAA,EAAA,QAAA,EAAA,CAAA,QAAA,CAAA,EAAA,CAAA,EAAA,eAAA,EAAA,EAAA,CAAA,uBAAA,CAAA,MAAA,EAAA,CAAA;;4FAMlB,WAAW,EAAA,UAAA,EAAA,CAAA;kBARvB,SAAS;+BACE,kBAAkB,EAAA,OAAA,EACnB,CAAC,mBAAmB,CAAC,EAAA,eAAA,EAGb,uBAAuB,CAAC,MAAM,EAAA,aAAA,EAChC,iBAAiB,CAAC,QAAQ,EAAA,QAAA,EAAA,grMAAA,EAAA,MAAA,EAAA,CAAA,4nGAAA,CAAA,EAAA;;;AEtC3C;;;;;;;;;;;;;;;;;AAiBG;MAKU,iBAAiB,CAAA;wGAAjB,iBAAiB,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,QAAA,EAAA,CAAA;yGAAjB,iBAAiB,EAAA,OAAA,EAAA,CAHlB,WAAW,CAAA,EAAA,OAAA,EAAA,CACX,WAAW,CAAA,EAAA,CAAA;AAEV,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,mBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,SAAA,EAAA,QAAA,EAAA,EAAA,EAAA,IAAA,EAAA,iBAAiB,YAHlB,WAAW,CAAA,EAAA,CAAA;;4FAGV,iBAAiB,EAAA,UAAA,EAAA,CAAA;kBAJ7B,QAAQ;AAAC,YAAA,IAAA,EAAA,CAAA;oBACR,OAAO,EAAE,CAAC,WAAW,CAAC;oBACtB,OAAO,EAAE,CAAC,WAAW,CAAC;AACvB,iBAAA;;;ACxBD;;AAEG;;;;"}
|
|
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 hintId(field: FieldConfig<T>): string {\r\n return `${this.fieldId(field)}-hint`;\r\n }\r\n\r\n protected optionDescriptionId(field: FieldConfig<T>, option: FieldOption): string {\r\n return `${this.fieldId(field)}-option-${String(option.value)}-description`;\r\n }\r\n\r\n /**\r\n * `aria-describedby` for the field's own control: the error's id when\r\n * one is currently shown, otherwise the hint's id when `hint` is set,\r\n * otherwise `null`. Never both at once, mirroring `activeErrorFor`\r\n * being the single source of truth for whether an error is visible.\r\n */\r\n protected describedByFor(field: FieldConfig<T>): string | null {\r\n if (this.activeErrorFor(field)) {\r\n return this.errorId(field);\r\n }\r\n if (field.hint) {\r\n return this.hintId(field);\r\n }\r\n return null;\r\n }\r\n\r\n /**\r\n * `aria-describedby` for one `radio` option's own control: combines\r\n * the field-level id (`describedByFor`) with that option's own\r\n * description id when it has one, space-separated (the real HTML/ARIA\r\n * syntax for referencing more than one describing element), or just\r\n * whichever of the two is actually present, or `null` if neither is.\r\n */\r\n protected optionDescribedByFor(field: FieldConfig<T>, option: FieldOption): string | null {\r\n const ids = [\r\n this.describedByFor(field),\r\n option.description ? this.optionDescriptionId(field, option) : null,\r\n ].filter((id): id is string => id !== null);\r\n return ids.length > 0 ? ids.join(' ') : null;\r\n }\r\n\r\n /**\r\n * Whether `field` renders as a switch instead of a native checkbox\r\n * box. `false` for any `type` other than `'checkbox'`, regardless of\r\n * `appearance`.\r\n */\r\n protected isSwitch(field: FieldConfig<T>): boolean {\r\n return field.type === 'checkbox' && field.appearance === 'switch';\r\n }\r\n\r\n /**\r\n * Whether `field`'s options render as a row of pills instead of a\r\n * vertical list of native radios. `false` for any `type` other than\r\n * `'radio'`, regardless of `appearance`.\r\n */\r\n protected isSegmented(field: FieldConfig<T>): boolean {\r\n return field.type === 'radio' && field.appearance === 'segmented';\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\" [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","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
|
@@ -19,7 +19,37 @@ interface FieldOption {
|
|
|
19
19
|
* Text displayed to the user for this option.
|
|
20
20
|
*/
|
|
21
21
|
label: string;
|
|
22
|
+
/**
|
|
23
|
+
* Short helper text shown below this specific option, for a `radio`
|
|
24
|
+
* field (has no visible effect for `select`, native `<option>`
|
|
25
|
+
* elements have nowhere to render it). Shown whenever set, referenced
|
|
26
|
+
* by that option's own `aria-describedby`, independent of the
|
|
27
|
+
* field-level `hint`/error.
|
|
28
|
+
* @example
|
|
29
|
+
* options: [
|
|
30
|
+
* { value: 'personal', label: 'Personal', description: 'For individual use' },
|
|
31
|
+
* { value: 'business', label: 'Business', description: 'For teams and invoicing' },
|
|
32
|
+
* ]
|
|
33
|
+
*/
|
|
34
|
+
description?: string;
|
|
22
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Purely visual variant for a field's control. Only ever changes how
|
|
38
|
+
* one specific `type` renders; every other `type` ignores it entirely,
|
|
39
|
+
* same as `default` (the default) does:
|
|
40
|
+
* - `'switch'`: only affects `type: 'checkbox'`. Renders as a
|
|
41
|
+
* track-and-thumb toggle instead of a native checkbox box.
|
|
42
|
+
* - `'segmented'`: only affects `type: 'radio'`. Renders its options as
|
|
43
|
+
* a row of pill buttons instead of a vertical list.
|
|
44
|
+
*
|
|
45
|
+
* A real `<input type="checkbox">`/`<input type="radio">` still backs
|
|
46
|
+
* either variant, visually hidden but focusable, clickable via its
|
|
47
|
+
* `<label>`, and fully native for validation, keyboard, and screen
|
|
48
|
+
* readers, only its own visible box is replaced by a sibling element
|
|
49
|
+
* that reflects `:checked` through CSS, no state duplicated in
|
|
50
|
+
* TypeScript.
|
|
51
|
+
*/
|
|
52
|
+
type FieldAppearance = 'default' | 'switch' | 'segmented';
|
|
23
53
|
/**
|
|
24
54
|
* Native Angular validation rules applied to a single field.
|
|
25
55
|
*/
|
|
@@ -121,6 +151,47 @@ interface FieldConfig<T> {
|
|
|
121
151
|
* @default true
|
|
122
152
|
*/
|
|
123
153
|
showPasswordToggle?: boolean;
|
|
154
|
+
/**
|
|
155
|
+
* Purely visual variant for this field's control. A flat optional
|
|
156
|
+
* field rather than a discriminated union tying it to a specific
|
|
157
|
+
* `type`, the same "optional, only relevant for one type" pattern
|
|
158
|
+
* already used by `showPasswordToggle`: setting it on a `type` it
|
|
159
|
+
* doesn't apply to (e.g. `appearance: 'switch'` on a `type: 'text'`
|
|
160
|
+
* field) is simply ignored, not a compile error or a runtime warning.
|
|
161
|
+
* See `FieldAppearance` for which `type` each value affects.
|
|
162
|
+
* @default 'default'
|
|
163
|
+
* @example
|
|
164
|
+
* { key: 'subscribe', label: 'Subscribe', type: 'checkbox', appearance: 'switch' }
|
|
165
|
+
*/
|
|
166
|
+
appearance?: FieldAppearance;
|
|
167
|
+
/**
|
|
168
|
+
* Short helper text shown below the field, whenever there's no
|
|
169
|
+
* active error: an active error (`activeErrorFor(field)`) replaces
|
|
170
|
+
* it visually, and `aria-describedby` follows the same rule, always
|
|
171
|
+
* pointing at whichever one is currently shown, never both at once.
|
|
172
|
+
* @example
|
|
173
|
+
* { key: 'password', label: 'Password', type: 'password', hint: 'At least 8 characters' }
|
|
174
|
+
*/
|
|
175
|
+
hint?: string;
|
|
176
|
+
/**
|
|
177
|
+
* Applies the native HTML `readonly` attribute, for `type`
|
|
178
|
+
* `text`/`number`/`email`/`password`/`date`/`textarea` only: the
|
|
179
|
+
* value stays visible and the field keeps participating in
|
|
180
|
+
* validation exactly as before (a `required` field left empty is
|
|
181
|
+
* still invalid, even while `readonly`), only direct user editing is
|
|
182
|
+
* blocked. The underlying `FormControl` stays enabled, unlike
|
|
183
|
+
* `disabled`, `buildFormGroup()` needs no special case for it.
|
|
184
|
+
*
|
|
185
|
+
* Has no effect at all on `select`, `radio`, or `checkbox`: the
|
|
186
|
+
* `readonly` HTML attribute doesn't apply to those elements natively
|
|
187
|
+
* (confirmed against the HTML specification, not simulated with any
|
|
188
|
+
* workaround here). Setting it on one of those types is simply
|
|
189
|
+
* ignored.
|
|
190
|
+
* @default false
|
|
191
|
+
* @example
|
|
192
|
+
* { key: 'referenceCode', label: 'Reference code', type: 'text', readonly: true, defaultValue: 'REF-2026-001' }
|
|
193
|
+
*/
|
|
194
|
+
readonly?: boolean;
|
|
124
195
|
}
|
|
125
196
|
/**
|
|
126
197
|
* Declarative form-level validator that checks values across multiple
|
|
@@ -283,6 +354,35 @@ declare class FormBuilder<T> {
|
|
|
283
354
|
protected activeErrorFor(field: FieldConfig<T>): string | null;
|
|
284
355
|
protected errorId(field: FieldConfig<T>): string;
|
|
285
356
|
protected legendId(field: FieldConfig<T>): string;
|
|
357
|
+
protected hintId(field: FieldConfig<T>): string;
|
|
358
|
+
protected optionDescriptionId(field: FieldConfig<T>, option: FieldOption): string;
|
|
359
|
+
/**
|
|
360
|
+
* `aria-describedby` for the field's own control: the error's id when
|
|
361
|
+
* one is currently shown, otherwise the hint's id when `hint` is set,
|
|
362
|
+
* otherwise `null`. Never both at once, mirroring `activeErrorFor`
|
|
363
|
+
* being the single source of truth for whether an error is visible.
|
|
364
|
+
*/
|
|
365
|
+
protected describedByFor(field: FieldConfig<T>): string | null;
|
|
366
|
+
/**
|
|
367
|
+
* `aria-describedby` for one `radio` option's own control: combines
|
|
368
|
+
* the field-level id (`describedByFor`) with that option's own
|
|
369
|
+
* description id when it has one, space-separated (the real HTML/ARIA
|
|
370
|
+
* syntax for referencing more than one describing element), or just
|
|
371
|
+
* whichever of the two is actually present, or `null` if neither is.
|
|
372
|
+
*/
|
|
373
|
+
protected optionDescribedByFor(field: FieldConfig<T>, option: FieldOption): string | null;
|
|
374
|
+
/**
|
|
375
|
+
* Whether `field` renders as a switch instead of a native checkbox
|
|
376
|
+
* box. `false` for any `type` other than `'checkbox'`, regardless of
|
|
377
|
+
* `appearance`.
|
|
378
|
+
*/
|
|
379
|
+
protected isSwitch(field: FieldConfig<T>): boolean;
|
|
380
|
+
/**
|
|
381
|
+
* Whether `field`'s options render as a row of pills instead of a
|
|
382
|
+
* vertical list of native radios. `false` for any `type` other than
|
|
383
|
+
* `'radio'`, regardless of `appearance`.
|
|
384
|
+
*/
|
|
385
|
+
protected isSegmented(field: FieldConfig<T>): boolean;
|
|
286
386
|
protected isSubmitDisabled(): boolean;
|
|
287
387
|
/**
|
|
288
388
|
* Triggers the exact same submit logic the internal button triggers:
|
|
@@ -426,4 +526,4 @@ declare const FORM_BUILDER_MESSAGES: InjectionToken<FormBuilderMessages>;
|
|
|
426
526
|
declare function provideFormBuilderMessages(overrides: Partial<FormBuilderMessages>): Provider;
|
|
427
527
|
|
|
428
528
|
export { FORM_BUILDER_MESSAGES, FORM_BUILDER_MESSAGES_EN, FORM_BUILDER_MESSAGES_ES, FormBuilder, FormBuilderModule, provideFormBuilderMessages };
|
|
429
|
-
export type { CrossFieldValidator, FieldConfig, FieldOption, FieldType, FieldValidatorConfig, FormBuilderMessages };
|
|
529
|
+
export type { CrossFieldValidator, FieldAppearance, FieldConfig, FieldOption, FieldType, FieldValidatorConfig, FormBuilderMessages };
|