@ng-forge/dynamic-forms-bootstrap 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  import { AsyncPipe } from '@angular/common';
2
2
  import * as i0 from '@angular/core';
3
3
  import { inject, input, computed, ChangeDetectionStrategy, Component, ElementRef, viewChild, effect, Directive, InjectionToken, afterRenderEffect, linkedSignal, model, isSignal } from '@angular/core';
4
- import { EventBus, ARRAY_CONTEXT, resolveTokens, DynamicTextPipe, buildBaseInputs, FIELD_SIGNAL_CONTEXT, resolveSubmitButtonDisabled, resolveNextButtonDisabled, NextPageEvent, PreviousPageEvent, DynamicFormLogger, AddArrayItemEvent, RemoveArrayItemEvent } from '@ng-forge/dynamic-forms';
4
+ import { EventBus, ARRAY_CONTEXT, resolveTokens, DynamicTextPipe, DEFAULT_PROPS, buildBaseInputs, RootFormRegistryService, FORM_OPTIONS, resolveSubmitButtonDisabled, FIELD_SIGNAL_CONTEXT, resolveNextButtonDisabled, NextPageEvent, PreviousPageEvent, DynamicFormLogger, AddArrayItemEvent, RemoveArrayItemEvent } from '@ng-forge/dynamic-forms';
5
5
  import { FormField } from '@angular/forms/signals';
6
6
  import { createResolvedErrorsSignal, shouldShowErrors, setupMetaTracking, isEqual, valueFieldMapper, optionsFieldMapper, checkboxFieldMapper, datepickerFieldMapper } from '@ng-forge/dynamic-forms/integration';
7
7
  import { explicitEffect } from 'ngxtension/explicit-effect';
@@ -78,8 +78,8 @@ class BsButtonFieldComponent {
78
78
  this.eventBus.dispatch(event);
79
79
  }
80
80
  }
81
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsButtonFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
82
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.0.8", type: BsButtonFieldComponent, isStandalone: true, selector: "df-bs-button", inputs: { key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, event: { classPropertyName: "event", publicName: "event", isSignal: true, isRequired: false, transformFunction: null }, eventArgs: { classPropertyName: "eventArgs", publicName: "eventArgs", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, eventContext: { classPropertyName: "eventContext", publicName: "eventContext", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()", "class": "className()", "hidden": "hidden()" } }, ngImport: i0, template: `
81
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsButtonFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
82
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.1.0", type: BsButtonFieldComponent, isStandalone: true, selector: "df-bs-button", inputs: { key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, hidden: { classPropertyName: "hidden", publicName: "hidden", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, event: { classPropertyName: "event", publicName: "event", isSignal: true, isRequired: false, transformFunction: null }, eventArgs: { classPropertyName: "eventArgs", publicName: "eventArgs", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, eventContext: { classPropertyName: "eventContext", publicName: "eventContext", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()", "class": "className()", "attr.hidden": "hidden() || null" } }, ngImport: i0, template: `
83
83
  @let buttonId = key() + '-button';
84
84
  <button
85
85
  [id]="buttonId"
@@ -94,13 +94,13 @@ class BsButtonFieldComponent {
94
94
  </button>
95
95
  `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--bs-secondary-color, #6c757d);--df-hint-font-size: .875rem;--df-error-color: var(--bs-danger, #dc3545);--df-error-font-size: .875rem}.df-bs-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%;margin-bottom:1rem}.df-bs-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color);margin-bottom:0}.df-bs-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size);margin-top:.25rem}.df-bs-field:has(.df-bs-hint){margin-bottom:.5rem}:host([hidden]){display:none!important}\n"], dependencies: [{ kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
96
96
  }
97
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsButtonFieldComponent, decorators: [{
97
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsButtonFieldComponent, decorators: [{
98
98
  type: Component,
99
99
  args: [{ selector: 'df-bs-button', imports: [DynamicTextPipe, AsyncPipe], host: {
100
100
  '[id]': '`${key()}`',
101
101
  '[attr.data-testid]': 'key()',
102
102
  '[class]': 'className()',
103
- '[hidden]': 'hidden()',
103
+ '[attr.hidden]': 'hidden() || null',
104
104
  }, template: `
105
105
  @let buttonId = key() + '-button';
106
106
  <button
@@ -124,6 +124,29 @@ var bsButton_component = /*#__PURE__*/Object.freeze({
124
124
 
125
125
  // Public API - component
126
126
 
127
+ /**
128
+ * Creates a signal that computes the aria-describedby value based on errors and hint state.
129
+ * Errors take precedence over hints - when errors are displayed, the hint is hidden.
130
+ *
131
+ * @param errorsToDisplay Signal containing the array of errors currently being displayed
132
+ * @param errorId Signal containing the base ID for error elements
133
+ * @param hintId Signal containing the ID for the hint element
134
+ * @param hasHint Function that returns true if a hint is configured
135
+ * @returns Signal containing the aria-describedby value (space-separated IDs) or null
136
+ */
137
+ function createAriaDescribedBySignal(errorsToDisplay, errorId, hintId, hasHint) {
138
+ return computed(() => {
139
+ const errors = errorsToDisplay();
140
+ if (errors.length > 0) {
141
+ return errors.map((_, i) => `${errorId()}-${i}`).join(' ');
142
+ }
143
+ if (hasHint()) {
144
+ return hintId();
145
+ }
146
+ return null;
147
+ });
148
+ }
149
+
127
150
  class BsCheckboxFieldComponent {
128
151
  elementRef = inject((ElementRef));
129
152
  field = input.required(...(ngDevMode ? [{ debugName: "field" }] : []));
@@ -154,7 +177,7 @@ class BsCheckboxFieldComponent {
154
177
  // ─────────────────────────────────────────────────────────────────────────────
155
178
  // Accessibility
156
179
  // ─────────────────────────────────────────────────────────────────────────────
157
- helpTextId = computed(() => `${this.key()}-help`, ...(ngDevMode ? [{ debugName: "helpTextId" }] : []));
180
+ hintId = computed(() => `${this.key()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : []));
158
181
  errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : []));
159
182
  ariaInvalid = computed(() => {
160
183
  const fieldState = this.field()();
@@ -163,21 +186,10 @@ class BsCheckboxFieldComponent {
163
186
  ariaRequired = computed(() => {
164
187
  return this.field()().required?.() === true ? true : null;
165
188
  }, ...(ngDevMode ? [{ debugName: "ariaRequired" }] : []));
166
- ariaDescribedBy = computed(() => {
167
- const ids = [];
168
- if (this.props()?.helpText) {
169
- ids.push(this.helpTextId());
170
- }
171
- const errors = this.errorsToDisplay();
172
- errors.forEach((_, i) => {
173
- ids.push(`${this.errorId()}-${i}`);
174
- });
175
- return ids.length > 0 ? ids.join(' ') : null;
176
- }, ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : []));
177
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsCheckboxFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
178
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: BsCheckboxFieldComponent, isStandalone: true, selector: "df-bs-checkbox", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className()", "id": "`${key()}`", "attr.data-testid": "key()", "attr.hidden": "field()().hidden() || null" } }, viewQueries: [{ propertyName: "checkboxInput", first: true, predicate: ["checkboxInput"], descendants: true, isSignal: true }], ngImport: i0, template: `
179
- @let f = field(); @let checkboxId = key() + '-checkbox'; @let ariaInvalid = this.ariaInvalid(); @let ariaRequired = this.ariaRequired();
180
- @let ariaDescribedBy = this.ariaDescribedBy();
189
+ ariaDescribedBy = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () => !!this.props()?.hint);
190
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsCheckboxFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
191
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: BsCheckboxFieldComponent, isStandalone: true, selector: "df-bs-checkbox", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className()", "id": "`${key()}`", "attr.data-testid": "key()", "attr.hidden": "field()().hidden() || null" } }, viewQueries: [{ propertyName: "checkboxInput", first: true, predicate: ["checkboxInput"], descendants: true, isSignal: true }], ngImport: i0, template: `
192
+ @let f = field(); @let checkboxId = key() + '-checkbox';
181
193
 
182
194
  <div
183
195
  class="form-check"
@@ -194,30 +206,28 @@ class BsCheckboxFieldComponent {
194
206
  class="form-check-input"
195
207
  [class.is-invalid]="f().invalid() && f().touched()"
196
208
  [attr.tabindex]="tabIndex()"
197
- [attr.aria-invalid]="ariaInvalid"
198
- [attr.aria-required]="ariaRequired"
199
- [attr.aria-describedby]="ariaDescribedBy"
209
+ [attr.aria-invalid]="ariaInvalid()"
210
+ [attr.aria-required]="ariaRequired()"
211
+ [attr.aria-describedby]="ariaDescribedBy()"
200
212
  />
201
213
  <label [for]="checkboxId" class="form-check-label">
202
214
  {{ label() | dynamicText | async }}
203
215
  </label>
204
216
  </div>
205
217
 
206
- @if (props()?.helpText; as helpText) {
207
- <div class="form-text" [id]="helpTextId()" [attr.hidden]="f().hidden() || null">
208
- {{ helpText | dynamicText | async }}
209
- </div>
210
- }
211
218
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
212
219
  <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
220
+ } @empty {
221
+ @if (props()?.hint; as hint) {
222
+ <div class="form-text" [id]="hintId()" [attr.hidden]="f().hidden() || null">{{ hint | dynamicText | async }}</div>
223
+ }
213
224
  }
214
225
  `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--bs-secondary-color, #6c757d);--df-hint-font-size: .875rem;--df-error-color: var(--bs-danger, #dc3545);--df-error-font-size: .875rem}.df-bs-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%;margin-bottom:1rem}.df-bs-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color);margin-bottom:0}.df-bs-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size);margin-top:.25rem}.df-bs-field:has(.df-bs-hint){margin-bottom:.5rem}:host([hidden]){display:none!important}\n", ":host{display:block}:host([hidden]){display:none!important}\n"], dependencies: [{ kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
215
226
  }
216
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsCheckboxFieldComponent, decorators: [{
227
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsCheckboxFieldComponent, decorators: [{
217
228
  type: Component,
218
229
  args: [{ selector: 'df-bs-checkbox', imports: [FormField, DynamicTextPipe, AsyncPipe], template: `
219
- @let f = field(); @let checkboxId = key() + '-checkbox'; @let ariaInvalid = this.ariaInvalid(); @let ariaRequired = this.ariaRequired();
220
- @let ariaDescribedBy = this.ariaDescribedBy();
230
+ @let f = field(); @let checkboxId = key() + '-checkbox';
221
231
 
222
232
  <div
223
233
  class="form-check"
@@ -234,22 +244,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
234
244
  class="form-check-input"
235
245
  [class.is-invalid]="f().invalid() && f().touched()"
236
246
  [attr.tabindex]="tabIndex()"
237
- [attr.aria-invalid]="ariaInvalid"
238
- [attr.aria-required]="ariaRequired"
239
- [attr.aria-describedby]="ariaDescribedBy"
247
+ [attr.aria-invalid]="ariaInvalid()"
248
+ [attr.aria-required]="ariaRequired()"
249
+ [attr.aria-describedby]="ariaDescribedBy()"
240
250
  />
241
251
  <label [for]="checkboxId" class="form-check-label">
242
252
  {{ label() | dynamicText | async }}
243
253
  </label>
244
254
  </div>
245
255
 
246
- @if (props()?.helpText; as helpText) {
247
- <div class="form-text" [id]="helpTextId()" [attr.hidden]="f().hidden() || null">
248
- {{ helpText | dynamicText | async }}
249
- </div>
250
- }
251
256
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
252
257
  <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
258
+ } @empty {
259
+ @if (props()?.hint; as hint) {
260
+ <div class="form-text" [id]="hintId()" [attr.hidden]="f().hidden() || null">{{ hint | dynamicText | async }}</div>
261
+ }
253
262
  }
254
263
  `, host: {
255
264
  '[class]': 'className()',
@@ -299,10 +308,10 @@ class InputConstraintsDirective {
299
308
  nativeElement.removeAttribute('step');
300
309
  }
301
310
  });
302
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: InputConstraintsDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
303
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.8", type: InputConstraintsDirective, isStandalone: true, selector: "[dfBsInputConstraints]", inputs: { dfMin: { classPropertyName: "dfMin", publicName: "dfMin", isSignal: true, isRequired: false, transformFunction: null }, dfMax: { classPropertyName: "dfMax", publicName: "dfMax", isSignal: true, isRequired: false, transformFunction: null }, dfStep: { classPropertyName: "dfStep", publicName: "dfStep", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
311
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: InputConstraintsDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
312
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.0", type: InputConstraintsDirective, isStandalone: true, selector: "[dfBsInputConstraints]", inputs: { dfMin: { classPropertyName: "dfMin", publicName: "dfMin", isSignal: true, isRequired: false, transformFunction: null }, dfMax: { classPropertyName: "dfMax", publicName: "dfMax", isSignal: true, isRequired: false, transformFunction: null }, dfStep: { classPropertyName: "dfStep", publicName: "dfStep", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
304
313
  }
305
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: InputConstraintsDirective, decorators: [{
314
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: InputConstraintsDirective, decorators: [{
306
315
  type: Directive,
307
316
  args: [{
308
317
  selector: '[dfBsInputConstraints]',
@@ -342,7 +351,7 @@ class BsDatepickerFieldComponent {
342
351
  // ─────────────────────────────────────────────────────────────────────────────
343
352
  // Accessibility
344
353
  // ─────────────────────────────────────────────────────────────────────────────
345
- helpTextId = computed(() => `${this.key()}-help`, ...(ngDevMode ? [{ debugName: "helpTextId" }] : []));
354
+ hintId = computed(() => `${this.key()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : []));
346
355
  errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : []));
347
356
  ariaInvalid = computed(() => {
348
357
  const fieldState = this.field()();
@@ -351,21 +360,10 @@ class BsDatepickerFieldComponent {
351
360
  ariaRequired = computed(() => {
352
361
  return this.field()().required?.() === true ? true : null;
353
362
  }, ...(ngDevMode ? [{ debugName: "ariaRequired" }] : []));
354
- ariaDescribedBy = computed(() => {
355
- const ids = [];
356
- if (this.props()?.helpText) {
357
- ids.push(this.helpTextId());
358
- }
359
- const errors = this.errorsToDisplay();
360
- errors.forEach((_, i) => {
361
- ids.push(`${this.errorId()}-${i}`);
362
- });
363
- return ids.length > 0 ? ids.join(' ') : null;
364
- }, ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : []));
365
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsDatepickerFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
366
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: BsDatepickerFieldComponent, isStandalone: true, selector: "df-bs-datepicker", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, startAt: { classPropertyName: "startAt", publicName: "startAt", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()", "class": "className()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
367
- @let f = field(); @let p = props(); @let ariaInvalid = this.ariaInvalid(); @let ariaRequired = this.ariaRequired();
368
- @let ariaDescribedBy = this.ariaDescribedBy(); @let inputId = key() + '-input';
363
+ ariaDescribedBy = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () => !!this.props()?.hint);
364
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsDatepickerFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
365
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: BsDatepickerFieldComponent, isStandalone: true, selector: "df-bs-datepicker", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, minDate: { classPropertyName: "minDate", publicName: "minDate", isSignal: true, isRequired: false, transformFunction: null }, maxDate: { classPropertyName: "maxDate", publicName: "maxDate", isSignal: true, isRequired: false, transformFunction: null }, startAt: { classPropertyName: "startAt", publicName: "startAt", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()", "class": "className()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
366
+ @let f = field(); @let p = props(); @let inputId = key() + '-input';
369
367
  @if (p?.floatingLabel) {
370
368
  <!-- Floating label variant -->
371
369
  <div class="form-floating mb-3">
@@ -378,9 +376,9 @@ class BsDatepickerFieldComponent {
378
376
  [dfMin]="minAsString()"
379
377
  [dfMax]="maxAsString()"
380
378
  [attr.tabindex]="tabIndex()"
381
- [attr.aria-invalid]="ariaInvalid"
382
- [attr.aria-required]="ariaRequired"
383
- [attr.aria-describedby]="ariaDescribedBy"
379
+ [attr.aria-invalid]="ariaInvalid()"
380
+ [attr.aria-required]="ariaRequired()"
381
+ [attr.aria-describedby]="ariaDescribedBy()"
384
382
  class="form-control"
385
383
  [class.form-control-sm]="p?.size === 'sm'"
386
384
  [class.form-control-lg]="p?.size === 'lg'"
@@ -415,9 +413,9 @@ class BsDatepickerFieldComponent {
415
413
  [dfMin]="minAsString()"
416
414
  [dfMax]="maxAsString()"
417
415
  [attr.tabindex]="tabIndex()"
418
- [attr.aria-invalid]="ariaInvalid"
419
- [attr.aria-required]="ariaRequired"
420
- [attr.aria-describedby]="ariaDescribedBy"
416
+ [attr.aria-invalid]="ariaInvalid()"
417
+ [attr.aria-required]="ariaRequired()"
418
+ [attr.aria-describedby]="ariaDescribedBy()"
421
419
  class="form-control"
422
420
  [class.form-control-sm]="p?.size === 'sm'"
423
421
  [class.form-control-lg]="p?.size === 'lg'"
@@ -425,11 +423,6 @@ class BsDatepickerFieldComponent {
425
423
  [class.is-valid]="f().valid() && f().touched() && p?.validFeedback"
426
424
  />
427
425
 
428
- @if (p?.helpText) {
429
- <div class="form-text" [id]="helpTextId()">
430
- {{ p?.helpText | dynamicText | async }}
431
- </div>
432
- }
433
426
  @if (p?.validFeedback && f().valid() && f().touched()) {
434
427
  <div class="valid-feedback d-block">
435
428
  {{ p?.validFeedback | dynamicText | async }}
@@ -437,16 +430,19 @@ class BsDatepickerFieldComponent {
437
430
  }
438
431
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
439
432
  <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
433
+ } @empty {
434
+ @if (p?.hint) {
435
+ <div class="form-text" [id]="hintId()">{{ p?.hint | dynamicText | async }}</div>
436
+ }
440
437
  }
441
438
  </div>
442
439
  }
443
440
  `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--bs-secondary-color, #6c757d);--df-hint-font-size: .875rem;--df-error-color: var(--bs-danger, #dc3545);--df-error-font-size: .875rem}.df-bs-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%;margin-bottom:1rem}.df-bs-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color);margin-bottom:0}.df-bs-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size);margin-top:.25rem}.df-bs-field:has(.df-bs-hint){margin-bottom:.5rem}:host([hidden]){display:none!important}\n", ":host([hidden]){display:none!important}\n"], dependencies: [{ kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"] }, { kind: "directive", type: InputConstraintsDirective, selector: "[dfBsInputConstraints]", inputs: ["dfMin", "dfMax", "dfStep"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
444
441
  }
445
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsDatepickerFieldComponent, decorators: [{
442
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsDatepickerFieldComponent, decorators: [{
446
443
  type: Component,
447
444
  args: [{ selector: 'df-bs-datepicker', imports: [FormField, DynamicTextPipe, AsyncPipe, InputConstraintsDirective], template: `
448
- @let f = field(); @let p = props(); @let ariaInvalid = this.ariaInvalid(); @let ariaRequired = this.ariaRequired();
449
- @let ariaDescribedBy = this.ariaDescribedBy(); @let inputId = key() + '-input';
445
+ @let f = field(); @let p = props(); @let inputId = key() + '-input';
450
446
  @if (p?.floatingLabel) {
451
447
  <!-- Floating label variant -->
452
448
  <div class="form-floating mb-3">
@@ -459,9 +455,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
459
455
  [dfMin]="minAsString()"
460
456
  [dfMax]="maxAsString()"
461
457
  [attr.tabindex]="tabIndex()"
462
- [attr.aria-invalid]="ariaInvalid"
463
- [attr.aria-required]="ariaRequired"
464
- [attr.aria-describedby]="ariaDescribedBy"
458
+ [attr.aria-invalid]="ariaInvalid()"
459
+ [attr.aria-required]="ariaRequired()"
460
+ [attr.aria-describedby]="ariaDescribedBy()"
465
461
  class="form-control"
466
462
  [class.form-control-sm]="p?.size === 'sm'"
467
463
  [class.form-control-lg]="p?.size === 'lg'"
@@ -496,9 +492,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
496
492
  [dfMin]="minAsString()"
497
493
  [dfMax]="maxAsString()"
498
494
  [attr.tabindex]="tabIndex()"
499
- [attr.aria-invalid]="ariaInvalid"
500
- [attr.aria-required]="ariaRequired"
501
- [attr.aria-describedby]="ariaDescribedBy"
495
+ [attr.aria-invalid]="ariaInvalid()"
496
+ [attr.aria-required]="ariaRequired()"
497
+ [attr.aria-describedby]="ariaDescribedBy()"
502
498
  class="form-control"
503
499
  [class.form-control-sm]="p?.size === 'sm'"
504
500
  [class.form-control-lg]="p?.size === 'lg'"
@@ -506,11 +502,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
506
502
  [class.is-valid]="f().valid() && f().touched() && p?.validFeedback"
507
503
  />
508
504
 
509
- @if (p?.helpText) {
510
- <div class="form-text" [id]="helpTextId()">
511
- {{ p?.helpText | dynamicText | async }}
512
- </div>
513
- }
514
505
  @if (p?.validFeedback && f().valid() && f().touched()) {
515
506
  <div class="valid-feedback d-block">
516
507
  {{ p?.validFeedback | dynamicText | async }}
@@ -518,6 +509,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
518
509
  }
519
510
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
520
511
  <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
512
+ } @empty {
513
+ @if (p?.hint) {
514
+ <div class="form-text" [id]="hintId()">{{ p?.hint | dynamicText | async }}</div>
515
+ }
521
516
  }
522
517
  </div>
523
518
  }
@@ -613,8 +608,8 @@ class BsInputFieldComponent {
613
608
  // ─────────────────────────────────────────────────────────────────────────────
614
609
  // Accessibility
615
610
  // ─────────────────────────────────────────────────────────────────────────────
616
- /** Unique ID for the help text element, used for aria-describedby */
617
- helpTextId = computed(() => `${this.key()}-help`, ...(ngDevMode ? [{ debugName: "helpTextId" }] : []));
611
+ /** Unique ID for the hint element, used for aria-describedby */
612
+ hintId = computed(() => `${this.key()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : []));
618
613
  /** Base ID for error elements, used for aria-describedby */
619
614
  errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : []));
620
615
  /** aria-invalid: true when field is invalid AND touched, false otherwise */
@@ -626,26 +621,12 @@ class BsInputFieldComponent {
626
621
  ariaRequired = computed(() => {
627
622
  return this.field()().required?.() === true ? true : null;
628
623
  }, ...(ngDevMode ? [{ debugName: "ariaRequired" }] : []));
629
- /** aria-describedby: links to help text and error messages for screen readers */
630
- ariaDescribedBy = computed(() => {
631
- const ids = [];
632
- if (this.props()?.helpText) {
633
- ids.push(this.helpTextId());
634
- }
635
- const errors = this.errorsToDisplay();
636
- errors.forEach((_, i) => {
637
- ids.push(`${this.errorId()}-${i}`);
638
- });
639
- return ids.length > 0 ? ids.join(' ') : null;
640
- }, ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : []));
641
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsInputFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
642
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: BsInputFieldComponent, isStandalone: true, selector: "df-bs-input", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()", "class": "className()", "attr.hidden": "field()().hidden() || null" } }, viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["inputRef"], descendants: true, isSignal: true }], ngImport: i0, template: `
643
- @let f = field(); @let p = props(); @let effectiveSize = this.effectiveSize();
644
- @let effectiveFloatingLabel = this.effectiveFloatingLabel();
645
- @let inputId = key() + '-input';
646
- @let ariaInvalid = this.ariaInvalid(); @let ariaRequired = this.ariaRequired();
647
- @let ariaDescribedBy = this.ariaDescribedBy();
648
- @if (effectiveFloatingLabel) {
624
+ /** aria-describedby: links to hint and error messages for screen readers */
625
+ ariaDescribedBy = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () => !!this.props()?.hint);
626
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsInputFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
627
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: BsInputFieldComponent, isStandalone: true, selector: "df-bs-input", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()", "class": "className()", "attr.hidden": "field()().hidden() || null" } }, viewQueries: [{ propertyName: "inputRef", first: true, predicate: ["inputRef"], descendants: true, isSignal: true }], ngImport: i0, template: `
628
+ @let f = field(); @let p = props(); @let inputId = key() + '-input';
629
+ @if (effectiveFloatingLabel()) {
649
630
  <!-- Floating label variant -->
650
631
  <div class="form-floating mb-3">
651
632
  <input
@@ -655,12 +636,12 @@ class BsInputFieldComponent {
655
636
  [type]="p?.type ?? 'text'"
656
637
  [placeholder]="(placeholder() | dynamicText | async) ?? ''"
657
638
  [attr.tabindex]="tabIndex()"
658
- [attr.aria-invalid]="ariaInvalid"
659
- [attr.aria-required]="ariaRequired"
660
- [attr.aria-describedby]="ariaDescribedBy"
639
+ [attr.aria-invalid]="ariaInvalid()"
640
+ [attr.aria-required]="ariaRequired()"
641
+ [attr.aria-describedby]="ariaDescribedBy()"
661
642
  class="form-control"
662
- [class.form-control-sm]="effectiveSize === 'sm'"
663
- [class.form-control-lg]="effectiveSize === 'lg'"
643
+ [class.form-control-sm]="effectiveSize() === 'sm'"
644
+ [class.form-control-lg]="effectiveSize() === 'lg'"
664
645
  [class.form-control-plaintext]="p?.plaintext"
665
646
  [class.is-invalid]="f().invalid() && f().touched()"
666
647
  [class.is-valid]="f().valid() && f().touched() && p?.validFeedback"
@@ -690,21 +671,16 @@ class BsInputFieldComponent {
690
671
  [type]="p?.type ?? 'text'"
691
672
  [placeholder]="(placeholder() | dynamicText | async) ?? ''"
692
673
  [attr.tabindex]="tabIndex()"
693
- [attr.aria-invalid]="ariaInvalid"
694
- [attr.aria-required]="ariaRequired"
695
- [attr.aria-describedby]="ariaDescribedBy"
674
+ [attr.aria-invalid]="ariaInvalid()"
675
+ [attr.aria-required]="ariaRequired()"
676
+ [attr.aria-describedby]="ariaDescribedBy()"
696
677
  class="form-control"
697
- [class.form-control-sm]="effectiveSize === 'sm'"
698
- [class.form-control-lg]="effectiveSize === 'lg'"
678
+ [class.form-control-sm]="effectiveSize() === 'sm'"
679
+ [class.form-control-lg]="effectiveSize() === 'lg'"
699
680
  [class.form-control-plaintext]="p?.plaintext"
700
681
  [class.is-invalid]="f().invalid() && f().touched()"
701
682
  [class.is-valid]="f().valid() && f().touched() && p?.validFeedback"
702
683
  />
703
- @if (p?.helpText) {
704
- <div class="form-text" [id]="helpTextId()">
705
- {{ p?.helpText | dynamicText | async }}
706
- </div>
707
- }
708
684
  @if (p?.validFeedback && f().valid() && f().touched()) {
709
685
  <div class="valid-feedback d-block">
710
686
  {{ p?.validFeedback | dynamicText | async }}
@@ -712,20 +688,20 @@ class BsInputFieldComponent {
712
688
  }
713
689
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
714
690
  <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
691
+ } @empty {
692
+ @if (p?.hint) {
693
+ <div class="form-text" [id]="hintId()">{{ p?.hint | dynamicText | async }}</div>
694
+ }
715
695
  }
716
696
  </div>
717
697
  }
718
698
  `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--bs-secondary-color, #6c757d);--df-hint-font-size: .875rem;--df-error-color: var(--bs-danger, #dc3545);--df-error-font-size: .875rem}.df-bs-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%;margin-bottom:1rem}.df-bs-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color);margin-bottom:0}.df-bs-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size);margin-top:.25rem}.df-bs-field:has(.df-bs-hint){margin-bottom:.5rem}:host([hidden]){display:none!important}\n", ":host([hidden]){display:none!important}\n"], dependencies: [{ kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
719
699
  }
720
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsInputFieldComponent, decorators: [{
700
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsInputFieldComponent, decorators: [{
721
701
  type: Component,
722
702
  args: [{ selector: 'df-bs-input', imports: [FormField, DynamicTextPipe, AsyncPipe], template: `
723
- @let f = field(); @let p = props(); @let effectiveSize = this.effectiveSize();
724
- @let effectiveFloatingLabel = this.effectiveFloatingLabel();
725
- @let inputId = key() + '-input';
726
- @let ariaInvalid = this.ariaInvalid(); @let ariaRequired = this.ariaRequired();
727
- @let ariaDescribedBy = this.ariaDescribedBy();
728
- @if (effectiveFloatingLabel) {
703
+ @let f = field(); @let p = props(); @let inputId = key() + '-input';
704
+ @if (effectiveFloatingLabel()) {
729
705
  <!-- Floating label variant -->
730
706
  <div class="form-floating mb-3">
731
707
  <input
@@ -735,12 +711,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
735
711
  [type]="p?.type ?? 'text'"
736
712
  [placeholder]="(placeholder() | dynamicText | async) ?? ''"
737
713
  [attr.tabindex]="tabIndex()"
738
- [attr.aria-invalid]="ariaInvalid"
739
- [attr.aria-required]="ariaRequired"
740
- [attr.aria-describedby]="ariaDescribedBy"
714
+ [attr.aria-invalid]="ariaInvalid()"
715
+ [attr.aria-required]="ariaRequired()"
716
+ [attr.aria-describedby]="ariaDescribedBy()"
741
717
  class="form-control"
742
- [class.form-control-sm]="effectiveSize === 'sm'"
743
- [class.form-control-lg]="effectiveSize === 'lg'"
718
+ [class.form-control-sm]="effectiveSize() === 'sm'"
719
+ [class.form-control-lg]="effectiveSize() === 'lg'"
744
720
  [class.form-control-plaintext]="p?.plaintext"
745
721
  [class.is-invalid]="f().invalid() && f().touched()"
746
722
  [class.is-valid]="f().valid() && f().touched() && p?.validFeedback"
@@ -770,21 +746,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
770
746
  [type]="p?.type ?? 'text'"
771
747
  [placeholder]="(placeholder() | dynamicText | async) ?? ''"
772
748
  [attr.tabindex]="tabIndex()"
773
- [attr.aria-invalid]="ariaInvalid"
774
- [attr.aria-required]="ariaRequired"
775
- [attr.aria-describedby]="ariaDescribedBy"
749
+ [attr.aria-invalid]="ariaInvalid()"
750
+ [attr.aria-required]="ariaRequired()"
751
+ [attr.aria-describedby]="ariaDescribedBy()"
776
752
  class="form-control"
777
- [class.form-control-sm]="effectiveSize === 'sm'"
778
- [class.form-control-lg]="effectiveSize === 'lg'"
753
+ [class.form-control-sm]="effectiveSize() === 'sm'"
754
+ [class.form-control-lg]="effectiveSize() === 'lg'"
779
755
  [class.form-control-plaintext]="p?.plaintext"
780
756
  [class.is-invalid]="f().invalid() && f().touched()"
781
757
  [class.is-valid]="f().valid() && f().touched() && p?.validFeedback"
782
758
  />
783
- @if (p?.helpText) {
784
- <div class="form-text" [id]="helpTextId()">
785
- {{ p?.helpText | dynamicText | async }}
786
- </div>
787
- }
788
759
  @if (p?.validFeedback && f().valid() && f().touched()) {
789
760
  <div class="valid-feedback d-block">
790
761
  {{ p?.validFeedback | dynamicText | async }}
@@ -792,6 +763,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
792
763
  }
793
764
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
794
765
  <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
766
+ } @empty {
767
+ @if (p?.hint) {
768
+ <div class="form-text" [id]="hintId()">{{ p?.hint | dynamicText | async }}</div>
769
+ }
795
770
  }
796
771
  </div>
797
772
  }
@@ -824,6 +799,14 @@ class BsMultiCheckboxFieldComponent {
824
799
  resolvedErrors = createResolvedErrorsSignal(this.field, this.validationMessages, this.defaultValidationMessages);
825
800
  showErrors = shouldShowErrors(this.field);
826
801
  errorsToDisplay = computed(() => (this.showErrors() ? this.resolvedErrors() : []), ...(ngDevMode ? [{ debugName: "errorsToDisplay" }] : []));
802
+ /** Computed map of checked option values for O(1) lookup in template */
803
+ checkedValuesMap = computed(() => {
804
+ const map = {};
805
+ for (const opt of this.valueViewModel()) {
806
+ map[String(opt.value)] = true;
807
+ }
808
+ return map;
809
+ }, ...(ngDevMode ? [{ debugName: "checkedValuesMap" }] : []));
827
810
  valueViewModel = linkedSignal(() => {
828
811
  const currentValues = this.field()().value();
829
812
  return this.options().filter((option) => currentValues.includes(option.value));
@@ -845,23 +828,23 @@ class BsMultiCheckboxFieldComponent {
845
828
  }
846
829
  });
847
830
  }
848
- onCheckboxChange(option, checked) {
831
+ onCheckboxChange(option, event) {
832
+ const checked = event.target.checked;
849
833
  this.valueViewModel.update((currentOptions) => {
850
834
  if (checked) {
851
- return currentOptions.some((opt) => opt.value === option.value) ? currentOptions : [...currentOptions, option];
835
+ return currentOptions.some((opt) => opt.value === option.value)
836
+ ? currentOptions
837
+ : [...currentOptions, option];
852
838
  }
853
839
  else {
854
840
  return currentOptions.filter((opt) => opt.value !== option.value);
855
841
  }
856
842
  });
857
843
  }
858
- isChecked(option) {
859
- return this.valueViewModel().some((opt) => opt.value === option.value);
860
- }
861
844
  // ─────────────────────────────────────────────────────────────────────────────
862
845
  // Accessibility
863
846
  // ─────────────────────────────────────────────────────────────────────────────
864
- helpTextId = computed(() => `${this.key()}-help`, ...(ngDevMode ? [{ debugName: "helpTextId" }] : []));
847
+ hintId = computed(() => `${this.key()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : []));
865
848
  errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : []));
866
849
  ariaInvalid = computed(() => {
867
850
  const fieldState = this.field()();
@@ -870,22 +853,11 @@ class BsMultiCheckboxFieldComponent {
870
853
  ariaRequired = computed(() => {
871
854
  return this.field()().required?.() === true ? true : null;
872
855
  }, ...(ngDevMode ? [{ debugName: "ariaRequired" }] : []));
873
- ariaDescribedBy = computed(() => {
874
- const ids = [];
875
- if (this.props()?.helpText) {
876
- ids.push(this.helpTextId());
877
- }
878
- const errors = this.errorsToDisplay();
879
- errors.forEach((_, i) => {
880
- ids.push(`${this.errorId()}-${i}`);
881
- });
882
- return ids.length > 0 ? ids.join(' ') : null;
883
- }, ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : []));
884
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsMultiCheckboxFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
885
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: BsMultiCheckboxFieldComponent, isStandalone: true, selector: "df-bs-multi-checkbox", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className() || \"\"", "id": "`${key()}`", "attr.data-testid": "key()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
856
+ ariaDescribedBy = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () => !!this.props()?.hint);
857
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsMultiCheckboxFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
858
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: BsMultiCheckboxFieldComponent, isStandalone: true, selector: "df-bs-multi-checkbox", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className() || \"\"", "id": "`${key()}`", "attr.data-testid": "key()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
886
859
  @let f = field();
887
- @let ariaInvalid = this.ariaInvalid(); @let ariaRequired = this.ariaRequired();
888
- @let ariaDescribedBy = this.ariaDescribedBy();
860
+ @let checked = checkedValuesMap();
889
861
  @if (label(); as label) {
890
862
  <div class="form-label">{{ label | dynamicText | async }}</div>
891
863
  }
@@ -901,15 +873,15 @@ class BsMultiCheckboxFieldComponent {
901
873
  <input
902
874
  type="checkbox"
903
875
  [id]="key() + '_' + i"
904
- [checked]="isChecked(option)"
876
+ [checked]="checked['' + option.value]"
905
877
  [disabled]="f().disabled() || option.disabled"
906
- (change)="onCheckboxChange(option, $any($event.target).checked)"
878
+ (change)="onCheckboxChange(option, $event)"
907
879
  class="form-check-input"
908
880
  [class.is-invalid]="f().invalid() && f().touched()"
909
881
  [attr.tabindex]="tabIndex()"
910
- [attr.aria-invalid]="ariaInvalid"
911
- [attr.aria-required]="ariaRequired"
912
- [attr.aria-describedby]="ariaDescribedBy"
882
+ [attr.aria-invalid]="ariaInvalid()"
883
+ [attr.aria-required]="ariaRequired()"
884
+ [attr.aria-describedby]="ariaDescribedBy()"
913
885
  />
914
886
  <label [for]="key() + '_' + i" class="form-check-label">
915
887
  {{ option.label | dynamicText | async }}
@@ -918,22 +890,20 @@ class BsMultiCheckboxFieldComponent {
918
890
  }
919
891
  </div>
920
892
 
921
- @if (props()?.helpText; as helpText) {
922
- <div class="form-text" [id]="helpTextId()">
923
- {{ helpText | dynamicText | async }}
924
- </div>
925
- }
926
893
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
927
894
  <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
895
+ } @empty {
896
+ @if (props()?.hint; as hint) {
897
+ <div class="form-text" [id]="hintId()">{{ hint | dynamicText | async }}</div>
898
+ }
928
899
  }
929
900
  `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--bs-secondary-color, #6c757d);--df-hint-font-size: .875rem;--df-error-color: var(--bs-danger, #dc3545);--df-error-font-size: .875rem}.df-bs-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%;margin-bottom:1rem}.df-bs-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color);margin-bottom:0}.df-bs-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size);margin-top:.25rem}.df-bs-field:has(.df-bs-hint){margin-bottom:.5rem}:host([hidden]){display:none!important}\n", ":host{display:block}.checkbox-group{margin-bottom:.5rem}:host([hidden]){display:none!important}\n"], dependencies: [{ kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
930
901
  }
931
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsMultiCheckboxFieldComponent, decorators: [{
902
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsMultiCheckboxFieldComponent, decorators: [{
932
903
  type: Component,
933
904
  args: [{ selector: 'df-bs-multi-checkbox', imports: [DynamicTextPipe, AsyncPipe], template: `
934
905
  @let f = field();
935
- @let ariaInvalid = this.ariaInvalid(); @let ariaRequired = this.ariaRequired();
936
- @let ariaDescribedBy = this.ariaDescribedBy();
906
+ @let checked = checkedValuesMap();
937
907
  @if (label(); as label) {
938
908
  <div class="form-label">{{ label | dynamicText | async }}</div>
939
909
  }
@@ -949,15 +919,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
949
919
  <input
950
920
  type="checkbox"
951
921
  [id]="key() + '_' + i"
952
- [checked]="isChecked(option)"
922
+ [checked]="checked['' + option.value]"
953
923
  [disabled]="f().disabled() || option.disabled"
954
- (change)="onCheckboxChange(option, $any($event.target).checked)"
924
+ (change)="onCheckboxChange(option, $event)"
955
925
  class="form-check-input"
956
926
  [class.is-invalid]="f().invalid() && f().touched()"
957
927
  [attr.tabindex]="tabIndex()"
958
- [attr.aria-invalid]="ariaInvalid"
959
- [attr.aria-required]="ariaRequired"
960
- [attr.aria-describedby]="ariaDescribedBy"
928
+ [attr.aria-invalid]="ariaInvalid()"
929
+ [attr.aria-required]="ariaRequired()"
930
+ [attr.aria-describedby]="ariaDescribedBy()"
961
931
  />
962
932
  <label [for]="key() + '_' + i" class="form-check-label">
963
933
  {{ option.label | dynamicText | async }}
@@ -966,13 +936,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
966
936
  }
967
937
  </div>
968
938
 
969
- @if (props()?.helpText; as helpText) {
970
- <div class="form-text" [id]="helpTextId()">
971
- {{ helpText | dynamicText | async }}
972
- </div>
973
- }
974
939
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
975
940
  <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
941
+ } @empty {
942
+ @if (props()?.hint; as hint) {
943
+ <div class="form-text" [id]="hintId()">{{ hint | dynamicText | async }}</div>
944
+ }
976
945
  }
977
946
  `, host: {
978
947
  '[class]': 'className() || ""',
@@ -1013,10 +982,9 @@ class BsRadioGroupComponent {
1013
982
  this.value.set(newValue);
1014
983
  }
1015
984
  }
1016
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsRadioGroupComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1017
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: BsRadioGroupComponent, isStandalone: true, selector: "df-bs-radio-group", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: true, transformFunction: null }, properties: { classPropertyName: "properties", publicName: "properties", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, ngImport: i0, template: `
985
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsRadioGroupComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
986
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: BsRadioGroupComponent, isStandalone: true, selector: "df-bs-radio-group", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: true, transformFunction: null }, properties: { classPropertyName: "properties", publicName: "properties", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null }, ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "ariaDescribedBy", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { value: "valueChange" }, ngImport: i0, template: `
1018
987
  @let props = properties();
1019
- @let ariaDescribedBy = this.ariaDescribedBy();
1020
988
  @if (props?.buttonGroup) {
1021
989
  <div class="btn-group" role="group" [attr.aria-label]="label() | dynamicText | async">
1022
990
  @for (option of options(); track option.value; let i = $index) {
@@ -1027,7 +995,7 @@ class BsRadioGroupComponent {
1027
995
  [checked]="value() === option.value"
1028
996
  (change)="onRadioChange(option.value)"
1029
997
  [disabled]="disabled() || option.disabled || false"
1030
- [attr.aria-describedby]="ariaDescribedBy"
998
+ [attr.aria-describedby]="ariaDescribedBy()"
1031
999
  class="btn-check"
1032
1000
  [id]="name() + '_' + i"
1033
1001
  autocomplete="off"
@@ -1052,7 +1020,7 @@ class BsRadioGroupComponent {
1052
1020
  [checked]="value() === option.value"
1053
1021
  (change)="onRadioChange(option.value)"
1054
1022
  [disabled]="disabled() || option.disabled || false"
1055
- [attr.aria-describedby]="ariaDescribedBy"
1023
+ [attr.aria-describedby]="ariaDescribedBy()"
1056
1024
  class="form-check-input"
1057
1025
  [id]="name() + '_' + i"
1058
1026
  />
@@ -1064,11 +1032,10 @@ class BsRadioGroupComponent {
1064
1032
  }
1065
1033
  `, isInline: true, styles: [":host{display:block}\n"], dependencies: [{ kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1066
1034
  }
1067
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsRadioGroupComponent, decorators: [{
1035
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsRadioGroupComponent, decorators: [{
1068
1036
  type: Component,
1069
1037
  args: [{ selector: 'df-bs-radio-group', imports: [DynamicTextPipe, AsyncPipe], template: `
1070
1038
  @let props = properties();
1071
- @let ariaDescribedBy = this.ariaDescribedBy();
1072
1039
  @if (props?.buttonGroup) {
1073
1040
  <div class="btn-group" role="group" [attr.aria-label]="label() | dynamicText | async">
1074
1041
  @for (option of options(); track option.value; let i = $index) {
@@ -1079,7 +1046,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1079
1046
  [checked]="value() === option.value"
1080
1047
  (change)="onRadioChange(option.value)"
1081
1048
  [disabled]="disabled() || option.disabled || false"
1082
- [attr.aria-describedby]="ariaDescribedBy"
1049
+ [attr.aria-describedby]="ariaDescribedBy()"
1083
1050
  class="btn-check"
1084
1051
  [id]="name() + '_' + i"
1085
1052
  autocomplete="off"
@@ -1104,7 +1071,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1104
1071
  [checked]="value() === option.value"
1105
1072
  (change)="onRadioChange(option.value)"
1106
1073
  [disabled]="disabled() || option.disabled || false"
1107
- [attr.aria-describedby]="ariaDescribedBy"
1074
+ [attr.aria-describedby]="ariaDescribedBy()"
1108
1075
  class="form-check-input"
1109
1076
  [id]="name() + '_' + i"
1110
1077
  />
@@ -1142,7 +1109,7 @@ class BsRadioFieldComponent {
1142
1109
  // ─────────────────────────────────────────────────────────────────────────────
1143
1110
  // Accessibility
1144
1111
  // ─────────────────────────────────────────────────────────────────────────────
1145
- helpTextId = computed(() => `${this.key()}-help`, ...(ngDevMode ? [{ debugName: "helpTextId" }] : []));
1112
+ hintId = computed(() => `${this.key()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : []));
1146
1113
  errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : []));
1147
1114
  ariaInvalid = computed(() => {
1148
1115
  const fieldState = this.field()();
@@ -1151,21 +1118,10 @@ class BsRadioFieldComponent {
1151
1118
  ariaRequired = computed(() => {
1152
1119
  return this.field()().required?.() === true ? true : null;
1153
1120
  }, ...(ngDevMode ? [{ debugName: "ariaRequired" }] : []));
1154
- ariaDescribedBy = computed(() => {
1155
- const ids = [];
1156
- if (this.props()?.helpText) {
1157
- ids.push(this.helpTextId());
1158
- }
1159
- const errors = this.errorsToDisplay();
1160
- errors.forEach((_, i) => {
1161
- ids.push(`${this.errorId()}-${i}`);
1162
- });
1163
- return ids.length > 0 ? ids.join(' ') : null;
1164
- }, ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : []));
1165
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsRadioFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1166
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: BsRadioFieldComponent, isStandalone: true, selector: "df-bs-radio", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()", "class": "className()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
1121
+ ariaDescribedBy = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () => !!this.props()?.hint);
1122
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsRadioFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1123
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: BsRadioFieldComponent, isStandalone: true, selector: "df-bs-radio", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()", "class": "className()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
1167
1124
  @let f = field();
1168
- @let ariaDescribedBy = this.ariaDescribedBy();
1169
1125
 
1170
1126
  <div class="mb-3">
1171
1127
  @if (label(); as label) {
@@ -1173,27 +1129,27 @@ class BsRadioFieldComponent {
1173
1129
  }
1174
1130
 
1175
1131
  <df-bs-radio-group
1176
- [formField]="$any(f)"
1132
+ [formField]="f"
1177
1133
  [label]="label()"
1178
1134
  [options]="options()"
1179
1135
  [properties]="props()"
1180
- [ariaDescribedBy]="ariaDescribedBy"
1136
+ [ariaDescribedBy]="ariaDescribedBy()"
1181
1137
  />
1182
1138
 
1183
- @if (props()?.helpText; as helpText) {
1184
- <div class="form-text" [id]="helpTextId()">{{ helpText | dynamicText | async }}</div>
1185
- }
1186
1139
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1187
1140
  <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
1141
+ } @empty {
1142
+ @if (props()?.hint; as hint) {
1143
+ <div class="form-text" [id]="hintId()">{{ hint | dynamicText | async }}</div>
1144
+ }
1188
1145
  }
1189
1146
  </div>
1190
1147
  `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--bs-secondary-color, #6c757d);--df-hint-font-size: .875rem;--df-error-color: var(--bs-danger, #dc3545);--df-error-font-size: .875rem}.df-bs-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%;margin-bottom:1rem}.df-bs-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color);margin-bottom:0}.df-bs-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size);margin-top:.25rem}.df-bs-field:has(.df-bs-hint){margin-bottom:.5rem}:host([hidden]){display:none!important}\n", ":host{display:block}:host([hidden]){display:none!important}\n"], dependencies: [{ kind: "component", type: BsRadioGroupComponent, selector: "df-bs-radio-group", inputs: ["value", "disabled", "readonly", "name", "label", "options", "properties", "meta", "ariaDescribedBy"], outputs: ["valueChange"] }, { kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1191
1148
  }
1192
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsRadioFieldComponent, decorators: [{
1149
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsRadioFieldComponent, decorators: [{
1193
1150
  type: Component,
1194
1151
  args: [{ selector: 'df-bs-radio', imports: [BsRadioGroupComponent, FormField, DynamicTextPipe, AsyncPipe], template: `
1195
1152
  @let f = field();
1196
- @let ariaDescribedBy = this.ariaDescribedBy();
1197
1153
 
1198
1154
  <div class="mb-3">
1199
1155
  @if (label(); as label) {
@@ -1201,18 +1157,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1201
1157
  }
1202
1158
 
1203
1159
  <df-bs-radio-group
1204
- [formField]="$any(f)"
1160
+ [formField]="f"
1205
1161
  [label]="label()"
1206
1162
  [options]="options()"
1207
1163
  [properties]="props()"
1208
- [ariaDescribedBy]="ariaDescribedBy"
1164
+ [ariaDescribedBy]="ariaDescribedBy()"
1209
1165
  />
1210
1166
 
1211
- @if (props()?.helpText; as helpText) {
1212
- <div class="form-text" [id]="helpTextId()">{{ helpText | dynamicText | async }}</div>
1213
- }
1214
1167
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1215
1168
  <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
1169
+ } @empty {
1170
+ @if (props()?.hint; as hint) {
1171
+ <div class="form-text" [id]="hintId()">{{ hint | dynamicText | async }}</div>
1172
+ }
1216
1173
  }
1217
1174
  </div>
1218
1175
  `, changeDetection: ChangeDetectionStrategy.OnPush, host: {
@@ -1258,7 +1215,7 @@ class BsSelectFieldComponent {
1258
1215
  // ─────────────────────────────────────────────────────────────────────────────
1259
1216
  // Accessibility
1260
1217
  // ─────────────────────────────────────────────────────────────────────────────
1261
- helpTextId = computed(() => `${this.key()}-help`, ...(ngDevMode ? [{ debugName: "helpTextId" }] : []));
1218
+ hintId = computed(() => `${this.key()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : []));
1262
1219
  errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : []));
1263
1220
  ariaInvalid = computed(() => {
1264
1221
  const fieldState = this.field()();
@@ -1267,21 +1224,10 @@ class BsSelectFieldComponent {
1267
1224
  ariaRequired = computed(() => {
1268
1225
  return this.field()().required?.() === true ? true : null;
1269
1226
  }, ...(ngDevMode ? [{ debugName: "ariaRequired" }] : []));
1270
- ariaDescribedBy = computed(() => {
1271
- const ids = [];
1272
- if (this.props()?.helpText) {
1273
- ids.push(this.helpTextId());
1274
- }
1275
- const errors = this.errorsToDisplay();
1276
- errors.forEach((_, i) => {
1277
- ids.push(`${this.errorId()}-${i}`);
1278
- });
1279
- return ids.length > 0 ? ids.join(' ') : null;
1280
- }, ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : []));
1281
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsSelectFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1282
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: BsSelectFieldComponent, isStandalone: true, selector: "df-bs-select", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()", "class": "className()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
1283
- @let f = field(); @let selectId = key() + '-select'; @let ariaInvalid = this.ariaInvalid(); @let ariaRequired = this.ariaRequired();
1284
- @let ariaDescribedBy = this.ariaDescribedBy();
1227
+ ariaDescribedBy = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () => !!this.props()?.hint);
1228
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsSelectFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1229
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: BsSelectFieldComponent, isStandalone: true, selector: "df-bs-select", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()", "class": "className()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
1230
+ @let f = field(); @let selectId = key() + '-select';
1285
1231
 
1286
1232
  <div class="mb-3">
1287
1233
  @if (label(); as label) {
@@ -1296,9 +1242,9 @@ class BsSelectFieldComponent {
1296
1242
  [class.is-invalid]="f().invalid() && f().touched()"
1297
1243
  [multiple]="props()?.multiple || false"
1298
1244
  [size]="props()?.htmlSize"
1299
- [attr.aria-invalid]="ariaInvalid"
1300
- [attr.aria-required]="ariaRequired"
1301
- [attr.aria-describedby]="ariaDescribedBy"
1245
+ [attr.aria-invalid]="ariaInvalid()"
1246
+ [attr.aria-required]="ariaRequired()"
1247
+ [attr.aria-describedby]="ariaDescribedBy()"
1302
1248
  >
1303
1249
  @if (placeholder(); as placeholder) {
1304
1250
  <option value="" disabled [selected]="!f().value()">{{ placeholder | dynamicText | async }}</option>
@@ -1310,20 +1256,20 @@ class BsSelectFieldComponent {
1310
1256
  }
1311
1257
  </select>
1312
1258
 
1313
- @if (props()?.helpText; as helpText) {
1314
- <div class="form-text" [id]="helpTextId()">{{ helpText | dynamicText | async }}</div>
1315
- }
1316
1259
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1317
1260
  <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
1261
+ } @empty {
1262
+ @if (props()?.hint; as hint) {
1263
+ <div class="form-text" [id]="hintId()">{{ hint | dynamicText | async }}</div>
1264
+ }
1318
1265
  }
1319
1266
  </div>
1320
1267
  `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--bs-secondary-color, #6c757d);--df-hint-font-size: .875rem;--df-error-color: var(--bs-danger, #dc3545);--df-error-font-size: .875rem}.df-bs-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%;margin-bottom:1rem}.df-bs-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color);margin-bottom:0}.df-bs-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size);margin-top:.25rem}.df-bs-field:has(.df-bs-hint){margin-bottom:.5rem}:host([hidden]){display:none!important}\n", ":host([hidden]){display:none!important}\n"], dependencies: [{ kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1321
1268
  }
1322
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsSelectFieldComponent, decorators: [{
1269
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsSelectFieldComponent, decorators: [{
1323
1270
  type: Component,
1324
1271
  args: [{ selector: 'df-bs-select', imports: [FormField, DynamicTextPipe, AsyncPipe], template: `
1325
- @let f = field(); @let selectId = key() + '-select'; @let ariaInvalid = this.ariaInvalid(); @let ariaRequired = this.ariaRequired();
1326
- @let ariaDescribedBy = this.ariaDescribedBy();
1272
+ @let f = field(); @let selectId = key() + '-select';
1327
1273
 
1328
1274
  <div class="mb-3">
1329
1275
  @if (label(); as label) {
@@ -1338,9 +1284,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1338
1284
  [class.is-invalid]="f().invalid() && f().touched()"
1339
1285
  [multiple]="props()?.multiple || false"
1340
1286
  [size]="props()?.htmlSize"
1341
- [attr.aria-invalid]="ariaInvalid"
1342
- [attr.aria-required]="ariaRequired"
1343
- [attr.aria-describedby]="ariaDescribedBy"
1287
+ [attr.aria-invalid]="ariaInvalid()"
1288
+ [attr.aria-required]="ariaRequired()"
1289
+ [attr.aria-describedby]="ariaDescribedBy()"
1344
1290
  >
1345
1291
  @if (placeholder(); as placeholder) {
1346
1292
  <option value="" disabled [selected]="!f().value()">{{ placeholder | dynamicText | async }}</option>
@@ -1352,11 +1298,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1352
1298
  }
1353
1299
  </select>
1354
1300
 
1355
- @if (props()?.helpText; as helpText) {
1356
- <div class="form-text" [id]="helpTextId()">{{ helpText | dynamicText | async }}</div>
1357
- }
1358
1301
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1359
1302
  <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
1303
+ } @empty {
1304
+ @if (props()?.hint; as hint) {
1305
+ <div class="form-text" [id]="hintId()">{{ hint | dynamicText | async }}</div>
1306
+ }
1360
1307
  }
1361
1308
  </div>
1362
1309
  `, changeDetection: ChangeDetectionStrategy.OnPush, host: {
@@ -1396,7 +1343,7 @@ class BsSliderFieldComponent {
1396
1343
  // ─────────────────────────────────────────────────────────────────────────────
1397
1344
  // Accessibility
1398
1345
  // ─────────────────────────────────────────────────────────────────────────────
1399
- helpTextId = computed(() => `${this.key()}-help`, ...(ngDevMode ? [{ debugName: "helpTextId" }] : []));
1346
+ hintId = computed(() => `${this.key()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : []));
1400
1347
  errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : []));
1401
1348
  ariaInvalid = computed(() => {
1402
1349
  const fieldState = this.field()();
@@ -1405,21 +1352,10 @@ class BsSliderFieldComponent {
1405
1352
  ariaRequired = computed(() => {
1406
1353
  return this.field()().required?.() === true ? true : null;
1407
1354
  }, ...(ngDevMode ? [{ debugName: "ariaRequired" }] : []));
1408
- ariaDescribedBy = computed(() => {
1409
- const ids = [];
1410
- if (this.props()?.helpText) {
1411
- ids.push(this.helpTextId());
1412
- }
1413
- const errors = this.errorsToDisplay();
1414
- errors.forEach((_, i) => {
1415
- ids.push(`${this.errorId()}-${i}`);
1416
- });
1417
- return ids.length > 0 ? ids.join(' ') : null;
1418
- }, ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : []));
1419
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsSliderFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1420
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: BsSliderFieldComponent, isStandalone: true, selector: "df-bs-slider", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className()", "id": "`${key()}`", "attr.data-testid": "key()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
1421
- @let f = field(); @let inputId = key() + '-input'; @let ariaInvalid = this.ariaInvalid(); @let ariaRequired = this.ariaRequired();
1422
- @let ariaDescribedBy = this.ariaDescribedBy();
1355
+ ariaDescribedBy = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () => !!this.props()?.hint);
1356
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsSliderFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1357
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: BsSliderFieldComponent, isStandalone: true, selector: "df-bs-slider", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, min: { classPropertyName: "min", publicName: "min", isSignal: true, isRequired: false, transformFunction: null }, max: { classPropertyName: "max", publicName: "max", isSignal: true, isRequired: false, transformFunction: null }, step: { classPropertyName: "step", publicName: "step", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className()", "id": "`${key()}`", "attr.data-testid": "key()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
1358
+ @let f = field(); @let inputId = key() + '-input';
1423
1359
 
1424
1360
  <div class="mb-3">
1425
1361
  @if (label(); as label) {
@@ -1440,28 +1376,26 @@ class BsSliderFieldComponent {
1440
1376
  [dfMax]="props()?.max ?? max()"
1441
1377
  [dfStep]="props()?.step ?? step()"
1442
1378
  [attr.tabindex]="tabIndex()"
1443
- [attr.aria-invalid]="ariaInvalid"
1444
- [attr.aria-required]="ariaRequired"
1445
- [attr.aria-describedby]="ariaDescribedBy"
1379
+ [attr.aria-invalid]="ariaInvalid()"
1380
+ [attr.aria-required]="ariaRequired()"
1381
+ [attr.aria-describedby]="ariaDescribedBy()"
1446
1382
  class="form-range"
1447
1383
  />
1448
1384
 
1449
- @if (props()?.helpText; as helpText) {
1450
- <div class="form-text" [id]="helpTextId()">
1451
- {{ helpText | dynamicText | async }}
1452
- </div>
1453
- }
1454
1385
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1455
1386
  <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
1387
+ } @empty {
1388
+ @if (props()?.hint; as hint) {
1389
+ <div class="form-text" [id]="hintId()">{{ hint | dynamicText | async }}</div>
1390
+ }
1456
1391
  }
1457
1392
  </div>
1458
1393
  `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--bs-secondary-color, #6c757d);--df-hint-font-size: .875rem;--df-error-color: var(--bs-danger, #dc3545);--df-error-font-size: .875rem}.df-bs-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%;margin-bottom:1rem}.df-bs-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color);margin-bottom:0}.df-bs-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size);margin-top:.25rem}.df-bs-field:has(.df-bs-hint){margin-bottom:.5rem}:host([hidden]){display:none!important}\n", ":host{display:block}:host([hidden]){display:none!important}\n"], dependencies: [{ kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"] }, { kind: "directive", type: InputConstraintsDirective, selector: "[dfBsInputConstraints]", inputs: ["dfMin", "dfMax", "dfStep"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1459
1394
  }
1460
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsSliderFieldComponent, decorators: [{
1395
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsSliderFieldComponent, decorators: [{
1461
1396
  type: Component,
1462
1397
  args: [{ selector: 'df-bs-slider', imports: [FormField, DynamicTextPipe, AsyncPipe, InputConstraintsDirective], template: `
1463
- @let f = field(); @let inputId = key() + '-input'; @let ariaInvalid = this.ariaInvalid(); @let ariaRequired = this.ariaRequired();
1464
- @let ariaDescribedBy = this.ariaDescribedBy();
1398
+ @let f = field(); @let inputId = key() + '-input';
1465
1399
 
1466
1400
  <div class="mb-3">
1467
1401
  @if (label(); as label) {
@@ -1482,19 +1416,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1482
1416
  [dfMax]="props()?.max ?? max()"
1483
1417
  [dfStep]="props()?.step ?? step()"
1484
1418
  [attr.tabindex]="tabIndex()"
1485
- [attr.aria-invalid]="ariaInvalid"
1486
- [attr.aria-required]="ariaRequired"
1487
- [attr.aria-describedby]="ariaDescribedBy"
1419
+ [attr.aria-invalid]="ariaInvalid()"
1420
+ [attr.aria-required]="ariaRequired()"
1421
+ [attr.aria-describedby]="ariaDescribedBy()"
1488
1422
  class="form-range"
1489
1423
  />
1490
1424
 
1491
- @if (props()?.helpText; as helpText) {
1492
- <div class="form-text" [id]="helpTextId()">
1493
- {{ helpText | dynamicText | async }}
1494
- </div>
1495
- }
1496
1425
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1497
1426
  <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
1427
+ } @empty {
1428
+ @if (props()?.hint; as hint) {
1429
+ <div class="form-text" [id]="hintId()">{{ hint | dynamicText | async }}</div>
1430
+ }
1498
1431
  }
1499
1432
  </div>
1500
1433
  `, host: {
@@ -1531,7 +1464,7 @@ class BsTextareaFieldComponent {
1531
1464
  // ─────────────────────────────────────────────────────────────────────────────
1532
1465
  // Accessibility
1533
1466
  // ─────────────────────────────────────────────────────────────────────────────
1534
- helpTextId = computed(() => `${this.key()}-help`, ...(ngDevMode ? [{ debugName: "helpTextId" }] : []));
1467
+ hintId = computed(() => `${this.key()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : []));
1535
1468
  errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : []));
1536
1469
  ariaInvalid = computed(() => {
1537
1470
  const fieldState = this.field()();
@@ -1540,21 +1473,10 @@ class BsTextareaFieldComponent {
1540
1473
  ariaRequired = computed(() => {
1541
1474
  return this.field()().required?.() === true ? true : null;
1542
1475
  }, ...(ngDevMode ? [{ debugName: "ariaRequired" }] : []));
1543
- ariaDescribedBy = computed(() => {
1544
- const ids = [];
1545
- if (this.props()?.helpText) {
1546
- ids.push(this.helpTextId());
1547
- }
1548
- const errors = this.errorsToDisplay();
1549
- errors.forEach((_, i) => {
1550
- ids.push(`${this.errorId()}-${i}`);
1551
- });
1552
- return ids.length > 0 ? ids.join(' ') : null;
1553
- }, ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : []));
1554
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsTextareaFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1555
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: BsTextareaFieldComponent, isStandalone: true, selector: "df-bs-textarea", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()", "class": "className()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
1556
- @let f = field(); @let p = props(); @let ariaInvalid = this.ariaInvalid(); @let ariaRequired = this.ariaRequired();
1557
- @let ariaDescribedBy = this.ariaDescribedBy(); @let textareaId = key() + '-textarea';
1476
+ ariaDescribedBy = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () => !!this.props()?.hint);
1477
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsTextareaFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1478
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: BsTextareaFieldComponent, isStandalone: true, selector: "df-bs-textarea", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "id": "`${key()}`", "attr.data-testid": "key()", "class": "className()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
1479
+ @let f = field(); @let p = props(); @let textareaId = key() + '-textarea';
1558
1480
  @if (p?.floatingLabel) {
1559
1481
  <!-- Floating label variant -->
1560
1482
  <div class="form-floating mb-3">
@@ -1563,9 +1485,9 @@ class BsTextareaFieldComponent {
1563
1485
  [id]="textareaId"
1564
1486
  [placeholder]="(placeholder() | dynamicText | async) ?? ''"
1565
1487
  [attr.tabindex]="tabIndex()"
1566
- [attr.aria-invalid]="ariaInvalid"
1567
- [attr.aria-required]="ariaRequired"
1568
- [attr.aria-describedby]="ariaDescribedBy"
1488
+ [attr.aria-invalid]="ariaInvalid()"
1489
+ [attr.aria-required]="ariaRequired()"
1490
+ [attr.aria-describedby]="ariaDescribedBy()"
1569
1491
  class="form-control"
1570
1492
  [class.form-control-sm]="p?.size === 'sm'"
1571
1493
  [class.form-control-lg]="p?.size === 'lg'"
@@ -1597,9 +1519,9 @@ class BsTextareaFieldComponent {
1597
1519
  [id]="textareaId"
1598
1520
  [placeholder]="(placeholder() | dynamicText | async) ?? ''"
1599
1521
  [attr.tabindex]="tabIndex()"
1600
- [attr.aria-invalid]="ariaInvalid"
1601
- [attr.aria-required]="ariaRequired"
1602
- [attr.aria-describedby]="ariaDescribedBy"
1522
+ [attr.aria-invalid]="ariaInvalid()"
1523
+ [attr.aria-required]="ariaRequired()"
1524
+ [attr.aria-describedby]="ariaDescribedBy()"
1603
1525
  class="form-control"
1604
1526
  [class.form-control-sm]="p?.size === 'sm'"
1605
1527
  [class.form-control-lg]="p?.size === 'lg'"
@@ -1607,11 +1529,6 @@ class BsTextareaFieldComponent {
1607
1529
  [class.is-valid]="f().valid() && f().touched() && p?.validFeedback"
1608
1530
  ></textarea>
1609
1531
 
1610
- @if (p?.helpText) {
1611
- <div class="form-text" [id]="helpTextId()">
1612
- {{ p?.helpText | dynamicText | async }}
1613
- </div>
1614
- }
1615
1532
  @if (p?.validFeedback && f().valid() && f().touched()) {
1616
1533
  <div class="valid-feedback d-block">
1617
1534
  {{ p?.validFeedback | dynamicText | async }}
@@ -1619,16 +1536,19 @@ class BsTextareaFieldComponent {
1619
1536
  }
1620
1537
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1621
1538
  <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
1539
+ } @empty {
1540
+ @if (p?.hint) {
1541
+ <div class="form-text" [id]="hintId()">{{ p?.hint | dynamicText | async }}</div>
1542
+ }
1622
1543
  }
1623
1544
  </div>
1624
1545
  }
1625
1546
  `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--bs-secondary-color, #6c757d);--df-hint-font-size: .875rem;--df-error-color: var(--bs-danger, #dc3545);--df-error-font-size: .875rem}.df-bs-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%;margin-bottom:1rem}.df-bs-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color);margin-bottom:0}.df-bs-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size);margin-top:.25rem}.df-bs-field:has(.df-bs-hint){margin-bottom:.5rem}:host([hidden]){display:none!important}\n", ":host([hidden]){display:none!important}\n"], dependencies: [{ kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1626
1547
  }
1627
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsTextareaFieldComponent, decorators: [{
1548
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsTextareaFieldComponent, decorators: [{
1628
1549
  type: Component,
1629
1550
  args: [{ selector: 'df-bs-textarea', imports: [FormField, DynamicTextPipe, AsyncPipe], template: `
1630
- @let f = field(); @let p = props(); @let ariaInvalid = this.ariaInvalid(); @let ariaRequired = this.ariaRequired();
1631
- @let ariaDescribedBy = this.ariaDescribedBy(); @let textareaId = key() + '-textarea';
1551
+ @let f = field(); @let p = props(); @let textareaId = key() + '-textarea';
1632
1552
  @if (p?.floatingLabel) {
1633
1553
  <!-- Floating label variant -->
1634
1554
  <div class="form-floating mb-3">
@@ -1637,9 +1557,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1637
1557
  [id]="textareaId"
1638
1558
  [placeholder]="(placeholder() | dynamicText | async) ?? ''"
1639
1559
  [attr.tabindex]="tabIndex()"
1640
- [attr.aria-invalid]="ariaInvalid"
1641
- [attr.aria-required]="ariaRequired"
1642
- [attr.aria-describedby]="ariaDescribedBy"
1560
+ [attr.aria-invalid]="ariaInvalid()"
1561
+ [attr.aria-required]="ariaRequired()"
1562
+ [attr.aria-describedby]="ariaDescribedBy()"
1643
1563
  class="form-control"
1644
1564
  [class.form-control-sm]="p?.size === 'sm'"
1645
1565
  [class.form-control-lg]="p?.size === 'lg'"
@@ -1671,9 +1591,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1671
1591
  [id]="textareaId"
1672
1592
  [placeholder]="(placeholder() | dynamicText | async) ?? ''"
1673
1593
  [attr.tabindex]="tabIndex()"
1674
- [attr.aria-invalid]="ariaInvalid"
1675
- [attr.aria-required]="ariaRequired"
1676
- [attr.aria-describedby]="ariaDescribedBy"
1594
+ [attr.aria-invalid]="ariaInvalid()"
1595
+ [attr.aria-required]="ariaRequired()"
1596
+ [attr.aria-describedby]="ariaDescribedBy()"
1677
1597
  class="form-control"
1678
1598
  [class.form-control-sm]="p?.size === 'sm'"
1679
1599
  [class.form-control-lg]="p?.size === 'lg'"
@@ -1681,11 +1601,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1681
1601
  [class.is-valid]="f().valid() && f().touched() && p?.validFeedback"
1682
1602
  ></textarea>
1683
1603
 
1684
- @if (p?.helpText) {
1685
- <div class="form-text" [id]="helpTextId()">
1686
- {{ p?.helpText | dynamicText | async }}
1687
- </div>
1688
- }
1689
1604
  @if (p?.validFeedback && f().valid() && f().touched()) {
1690
1605
  <div class="valid-feedback d-block">
1691
1606
  {{ p?.validFeedback | dynamicText | async }}
@@ -1693,6 +1608,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1693
1608
  }
1694
1609
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1695
1610
  <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
1611
+ } @empty {
1612
+ @if (p?.hint) {
1613
+ <div class="form-text" [id]="hintId()">{{ p?.hint | dynamicText | async }}</div>
1614
+ }
1696
1615
  }
1697
1616
  </div>
1698
1617
  }
@@ -1730,7 +1649,7 @@ class BsToggleFieldComponent {
1730
1649
  // ─────────────────────────────────────────────────────────────────────────────
1731
1650
  // Accessibility
1732
1651
  // ─────────────────────────────────────────────────────────────────────────────
1733
- helpTextId = computed(() => `${this.key()}-help`, ...(ngDevMode ? [{ debugName: "helpTextId" }] : []));
1652
+ hintId = computed(() => `${this.key()}-hint`, ...(ngDevMode ? [{ debugName: "hintId" }] : []));
1734
1653
  errorId = computed(() => `${this.key()}-error`, ...(ngDevMode ? [{ debugName: "errorId" }] : []));
1735
1654
  ariaInvalid = computed(() => {
1736
1655
  const fieldState = this.field()();
@@ -1739,21 +1658,10 @@ class BsToggleFieldComponent {
1739
1658
  ariaRequired = computed(() => {
1740
1659
  return this.field()().required?.() === true ? true : null;
1741
1660
  }, ...(ngDevMode ? [{ debugName: "ariaRequired" }] : []));
1742
- ariaDescribedBy = computed(() => {
1743
- const ids = [];
1744
- if (this.props()?.helpText) {
1745
- ids.push(this.helpTextId());
1746
- }
1747
- const errors = this.errorsToDisplay();
1748
- errors.forEach((_, i) => {
1749
- ids.push(`${this.errorId()}-${i}`);
1750
- });
1751
- return ids.length > 0 ? ids.join(' ') : null;
1752
- }, ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : []));
1753
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsToggleFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1754
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: BsToggleFieldComponent, isStandalone: true, selector: "df-bs-toggle", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className()", "id": "`${key()}`", "attr.data-testid": "key()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
1755
- @let f = field(); @let inputId = key() + '-input'; @let ariaInvalid = this.ariaInvalid(); @let ariaRequired = this.ariaRequired();
1756
- @let ariaDescribedBy = this.ariaDescribedBy();
1661
+ ariaDescribedBy = createAriaDescribedBySignal(this.errorsToDisplay, this.errorId, this.hintId, () => !!this.props()?.hint);
1662
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsToggleFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
1663
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.0", type: BsToggleFieldComponent, isStandalone: true, selector: "df-bs-toggle", inputs: { field: { classPropertyName: "field", publicName: "field", isSignal: true, isRequired: true, transformFunction: null }, key: { classPropertyName: "key", publicName: "key", isSignal: true, isRequired: true, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, placeholder: { classPropertyName: "placeholder", publicName: "placeholder", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "className", isSignal: true, isRequired: false, transformFunction: null }, tabIndex: { classPropertyName: "tabIndex", publicName: "tabIndex", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "props", isSignal: true, isRequired: false, transformFunction: null }, validationMessages: { classPropertyName: "validationMessages", publicName: "validationMessages", isSignal: true, isRequired: false, transformFunction: null }, defaultValidationMessages: { classPropertyName: "defaultValidationMessages", publicName: "defaultValidationMessages", isSignal: true, isRequired: false, transformFunction: null }, meta: { classPropertyName: "meta", publicName: "meta", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "className()", "id": "`${key()}`", "attr.data-testid": "key()", "attr.hidden": "field()().hidden() || null" } }, ngImport: i0, template: `
1664
+ @let f = field(); @let inputId = key() + '-input';
1757
1665
 
1758
1666
  <div
1759
1667
  class="form-check form-switch"
@@ -1770,30 +1678,28 @@ class BsToggleFieldComponent {
1770
1678
  class="form-check-input"
1771
1679
  [class.is-invalid]="f().invalid() && f().touched()"
1772
1680
  [attr.tabindex]="tabIndex()"
1773
- [attr.aria-invalid]="ariaInvalid"
1774
- [attr.aria-required]="ariaRequired"
1775
- [attr.aria-describedby]="ariaDescribedBy"
1681
+ [attr.aria-invalid]="ariaInvalid()"
1682
+ [attr.aria-required]="ariaRequired()"
1683
+ [attr.aria-describedby]="ariaDescribedBy()"
1776
1684
  />
1777
1685
  <label [for]="inputId" class="form-check-label">
1778
1686
  {{ label() | dynamicText | async }}
1779
1687
  </label>
1780
1688
  </div>
1781
1689
 
1782
- @if (props()?.helpText; as helpText) {
1783
- <div class="form-text" [id]="helpTextId()" [attr.hidden]="f().hidden() || null">
1784
- {{ helpText | dynamicText | async }}
1785
- </div>
1786
- }
1787
1690
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1788
1691
  <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
1692
+ } @empty {
1693
+ @if (props()?.hint; as hint) {
1694
+ <div class="form-text" [id]="hintId()" [attr.hidden]="f().hidden() || null">{{ hint | dynamicText | async }}</div>
1695
+ }
1789
1696
  }
1790
1697
  `, isInline: true, styles: [":host{--df-field-gap: .5rem;--df-label-font-weight: 500;--df-label-color: inherit;--df-hint-color: var(--bs-secondary-color, #6c757d);--df-hint-font-size: .875rem;--df-error-color: var(--bs-danger, #dc3545);--df-error-font-size: .875rem}.df-bs-field{display:flex;flex-direction:column;gap:var(--df-field-gap);width:100%;margin-bottom:1rem}.df-bs-label{font-weight:var(--df-label-font-weight);color:var(--df-label-color);margin-bottom:0}.df-bs-hint{color:var(--df-hint-color);font-size:var(--df-hint-font-size);margin-top:.25rem}.df-bs-field:has(.df-bs-hint){margin-bottom:.5rem}:host([hidden]){display:none!important}\n", ":host{display:block}:host([hidden]){display:none!important}.form-switch-sm .form-check-input{width:1.75rem;height:1rem;font-size:.875rem}.form-switch-lg .form-check-input{width:3rem;height:1.75rem;font-size:1.125rem}\n"], dependencies: [{ kind: "directive", type: FormField, selector: "[formField]", inputs: ["formField"] }, { kind: "pipe", type: DynamicTextPipe, name: "dynamicText" }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1791
1698
  }
1792
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: BsToggleFieldComponent, decorators: [{
1699
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: BsToggleFieldComponent, decorators: [{
1793
1700
  type: Component,
1794
1701
  args: [{ selector: 'df-bs-toggle', imports: [FormField, DynamicTextPipe, AsyncPipe], template: `
1795
- @let f = field(); @let inputId = key() + '-input'; @let ariaInvalid = this.ariaInvalid(); @let ariaRequired = this.ariaRequired();
1796
- @let ariaDescribedBy = this.ariaDescribedBy();
1702
+ @let f = field(); @let inputId = key() + '-input';
1797
1703
 
1798
1704
  <div
1799
1705
  class="form-check form-switch"
@@ -1810,22 +1716,21 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImpor
1810
1716
  class="form-check-input"
1811
1717
  [class.is-invalid]="f().invalid() && f().touched()"
1812
1718
  [attr.tabindex]="tabIndex()"
1813
- [attr.aria-invalid]="ariaInvalid"
1814
- [attr.aria-required]="ariaRequired"
1815
- [attr.aria-describedby]="ariaDescribedBy"
1719
+ [attr.aria-invalid]="ariaInvalid()"
1720
+ [attr.aria-required]="ariaRequired()"
1721
+ [attr.aria-describedby]="ariaDescribedBy()"
1816
1722
  />
1817
1723
  <label [for]="inputId" class="form-check-label">
1818
1724
  {{ label() | dynamicText | async }}
1819
1725
  </label>
1820
1726
  </div>
1821
1727
 
1822
- @if (props()?.helpText; as helpText) {
1823
- <div class="form-text" [id]="helpTextId()" [attr.hidden]="f().hidden() || null">
1824
- {{ helpText | dynamicText | async }}
1825
- </div>
1826
- }
1827
1728
  @for (error of errorsToDisplay(); track error.kind; let i = $index) {
1828
1729
  <div class="invalid-feedback d-block" [id]="errorId() + '-' + i" role="alert">{{ error.message }}</div>
1730
+ } @empty {
1731
+ @if (props()?.hint; as hint) {
1732
+ <div class="form-text" [id]="hintId()" [attr.hidden]="f().hidden() || null">{{ hint | dynamicText | async }}</div>
1733
+ }
1829
1734
  }
1830
1735
  `, host: {
1831
1736
  '[class]': 'className()',
@@ -1873,9 +1778,9 @@ const BsField = {
1873
1778
  * @returns Signal containing Record of input names to values for ngComponentOutlet
1874
1779
  */
1875
1780
  function buttonFieldMapper(fieldDef) {
1876
- // Build base inputs (static, from field definition)
1877
- const baseInputs = buildBaseInputs(fieldDef);
1781
+ const defaultProps = inject(DEFAULT_PROPS);
1878
1782
  return computed(() => {
1783
+ const baseInputs = buildBaseInputs(fieldDef, defaultProps());
1879
1784
  const inputs = {
1880
1785
  ...baseInputs,
1881
1786
  };
@@ -1913,20 +1818,21 @@ function buttonFieldMapper(fieldDef) {
1913
1818
  * @returns Signal containing Record of input names to values for ngComponentOutlet
1914
1819
  */
1915
1820
  function submitButtonFieldMapper(fieldDef) {
1916
- // Inject field signal context to access form state and options
1917
- const fieldSignalContext = inject(FIELD_SIGNAL_CONTEXT);
1918
- // Build base inputs (static, from field definition)
1919
- const baseInputs = buildBaseInputs(fieldDef);
1920
- // Use button-logic-resolver to compute disabled state
1821
+ const rootFormRegistry = inject(RootFormRegistryService);
1822
+ const defaultProps = inject(DEFAULT_PROPS);
1823
+ const formOptions = inject(FORM_OPTIONS);
1921
1824
  const fieldWithLogic = fieldDef;
1922
- const disabledSignal = resolveSubmitButtonDisabled({
1923
- form: fieldSignalContext.form,
1924
- formOptions: fieldSignalContext.formOptions,
1925
- fieldLogic: fieldWithLogic.logic,
1926
- explicitlyDisabled: fieldDef.disabled,
1927
- });
1928
- // Return computed signal - evaluates disabledSignal inside for reactivity
1929
1825
  return computed(() => {
1826
+ const baseInputs = buildBaseInputs(fieldDef, defaultProps());
1827
+ // Use rootFormRegistry instead of fieldSignalContext.form because when the submit button
1828
+ // is inside a group/array, fieldSignalContext.form points to the nested form tree,
1829
+ // not the root form. We need root form validity for submit button disabled state (#157).
1830
+ const disabledSignal = resolveSubmitButtonDisabled({
1831
+ form: rootFormRegistry.getRootForm(),
1832
+ formOptions: formOptions(),
1833
+ fieldLogic: fieldWithLogic.logic,
1834
+ explicitlyDisabled: fieldDef.disabled,
1835
+ });
1930
1836
  const inputs = {
1931
1837
  ...baseInputs,
1932
1838
  // No event - native form submit handles it via form's onNativeSubmit
@@ -1953,21 +1859,19 @@ function submitButtonFieldMapper(fieldDef) {
1953
1859
  * @returns Signal containing Record of input names to values for ngComponentOutlet
1954
1860
  */
1955
1861
  function nextButtonFieldMapper(fieldDef) {
1956
- // Inject field signal context to access form state and options
1957
1862
  const fieldSignalContext = inject(FIELD_SIGNAL_CONTEXT);
1958
- // Build base inputs (static, from field definition)
1959
- const baseInputs = buildBaseInputs(fieldDef);
1960
- // Use button-logic-resolver to compute disabled state
1863
+ const defaultProps = inject(DEFAULT_PROPS);
1864
+ const formOptions = inject(FORM_OPTIONS);
1961
1865
  const fieldWithLogic = fieldDef;
1962
- const disabledSignal = resolveNextButtonDisabled({
1963
- form: fieldSignalContext.form,
1964
- formOptions: fieldSignalContext.formOptions,
1965
- fieldLogic: fieldWithLogic.logic,
1966
- explicitlyDisabled: fieldDef.disabled,
1967
- currentPageValid: fieldSignalContext.currentPageValid,
1968
- });
1969
- // Return computed signal - evaluates disabledSignal inside for reactivity
1970
1866
  return computed(() => {
1867
+ const baseInputs = buildBaseInputs(fieldDef, defaultProps());
1868
+ const disabledSignal = resolveNextButtonDisabled({
1869
+ form: fieldSignalContext.form,
1870
+ formOptions: formOptions(),
1871
+ fieldLogic: fieldWithLogic.logic,
1872
+ explicitlyDisabled: fieldDef.disabled,
1873
+ currentPageValid: fieldSignalContext.currentPageValid,
1874
+ });
1971
1875
  const inputs = {
1972
1876
  ...baseInputs,
1973
1877
  event: NextPageEvent,
@@ -1988,9 +1892,9 @@ function nextButtonFieldMapper(fieldDef) {
1988
1892
  * @returns Signal containing Record of input names to values for ngComponentOutlet
1989
1893
  */
1990
1894
  function previousButtonFieldMapper(fieldDef) {
1991
- // Build base inputs (static, from field definition)
1992
- const baseInputs = buildBaseInputs(fieldDef);
1895
+ const defaultProps = inject(DEFAULT_PROPS);
1993
1896
  return computed(() => {
1897
+ const baseInputs = buildBaseInputs(fieldDef, defaultProps());
1994
1898
  const inputs = {
1995
1899
  ...baseInputs,
1996
1900
  event: PreviousPageEvent,
@@ -2016,10 +1920,9 @@ function previousButtonFieldMapper(fieldDef) {
2016
1920
  * @returns Signal containing Record of input names to values for ngComponentOutlet
2017
1921
  */
2018
1922
  function addArrayItemButtonFieldMapper(fieldDef) {
2019
- // Try to get array context (available when inside an array)
2020
- // Use optional injection so it doesn't fail when outside an array
2021
1923
  const arrayContext = inject(ARRAY_CONTEXT, { optional: true });
2022
1924
  const logger = inject(DynamicFormLogger);
1925
+ const defaultProps = inject(DEFAULT_PROPS);
2023
1926
  // Determine the target array key
2024
1927
  // Priority: explicit arrayKey from fieldDef > arrayKey from context
2025
1928
  const targetArrayKey = fieldDef.arrayKey ?? arrayContext?.arrayKey;
@@ -2027,13 +1930,12 @@ function addArrayItemButtonFieldMapper(fieldDef) {
2027
1930
  logger.warn(`addArrayItem button "${fieldDef.key}" has no array context. ` +
2028
1931
  'Either place it inside an array field, or provide an explicit arrayKey property.');
2029
1932
  }
2030
- // Build base inputs (static, from field definition)
2031
- const baseInputs = buildBaseInputs(fieldDef);
2032
1933
  // Set default eventArgs for AddArrayItemEvent (arrayKey)
2033
1934
  // User can override by providing eventArgs in field definition
2034
1935
  const defaultEventArgs = ['$arrayKey'];
2035
1936
  const eventArgs = 'eventArgs' in fieldDef && fieldDef.eventArgs !== undefined ? fieldDef.eventArgs : defaultEventArgs;
2036
1937
  return computed(() => {
1938
+ const baseInputs = buildBaseInputs(fieldDef, defaultProps());
2037
1939
  // Read signal value if index is a signal (supports differential updates)
2038
1940
  const getIndex = () => {
2039
1941
  if (!arrayContext)
@@ -2072,10 +1974,9 @@ function addArrayItemButtonFieldMapper(fieldDef) {
2072
1974
  * @returns Signal containing Record of input names to values for ngComponentOutlet
2073
1975
  */
2074
1976
  function removeArrayItemButtonFieldMapper(fieldDef) {
2075
- // Try to get array context (available when inside an array)
2076
- // Use optional injection so it doesn't fail when outside an array
2077
1977
  const arrayContext = inject(ARRAY_CONTEXT, { optional: true });
2078
1978
  const logger = inject(DynamicFormLogger);
1979
+ const defaultProps = inject(DEFAULT_PROPS);
2079
1980
  // Determine the target array key
2080
1981
  // Priority: explicit arrayKey from fieldDef > arrayKey from context
2081
1982
  const targetArrayKey = fieldDef.arrayKey ?? arrayContext?.arrayKey;
@@ -2083,14 +1984,13 @@ function removeArrayItemButtonFieldMapper(fieldDef) {
2083
1984
  logger.warn(`removeArrayItem button "${fieldDef.key}" has no array context. ` +
2084
1985
  'Either place it inside an array field, or provide an explicit arrayKey property.');
2085
1986
  }
2086
- // Build base inputs (static, from field definition)
2087
- const baseInputs = buildBaseInputs(fieldDef);
2088
1987
  // Set default eventArgs for RemoveArrayItemEvent (arrayKey, index if inside array)
2089
1988
  // When outside array, only pass arrayKey (removes last by default)
2090
1989
  // User can override by providing eventArgs in field definition
2091
1990
  const defaultEventArgs = arrayContext ? ['$arrayKey', '$index'] : ['$arrayKey'];
2092
1991
  const eventArgs = 'eventArgs' in fieldDef && fieldDef.eventArgs !== undefined ? fieldDef.eventArgs : defaultEventArgs;
2093
1992
  return computed(() => {
1993
+ const baseInputs = buildBaseInputs(fieldDef, defaultProps());
2094
1994
  // Read signal value if index is a signal (supports differential updates)
2095
1995
  const getIndex = () => {
2096
1996
  if (!arrayContext)