@ng-forge/dynamic-forms-bootstrap 1.0.0-rc.1 → 1.0.0-rc.2

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.
@@ -2,10 +2,10 @@ import { AsyncPipe } from '@angular/common';
2
2
  import * as i0 from '@angular/core';
3
3
  import { input, computed, ChangeDetectionStrategy, Component, inject, ElementRef, Directive, InjectionToken, signal, forwardRef, linkedSignal, model, isSignal } from '@angular/core';
4
4
  import * as i1 from '@ng-forge/dynamic-forms/integration';
5
- import { injectNgForgeAction, DynamicTextPipe, NgForgeActionHost, injectNgForgeField, NgForgeControl, NgForgeFieldHost, runPresetAction, injectNgForgeAddons, ADDON_PRESET_HANDLER, FIELD_SIGNAL_CONTEXT, NgForgeAddons, isEqual, DEFAULT_PROPS, ARRAY_CONTEXT, buildBaseInputs, valueFieldMapper, optionsFieldMapper, checkboxFieldMapper, submitButtonFieldMapper, nextButtonFieldMapper, previousButtonFieldMapper, addArrayItemButtonMapper, prependArrayItemButtonMapper, insertArrayItemButtonMapper, removeArrayItemButtonMapper, popArrayItemButtonMapper, shiftArrayItemButtonMapper, datepickerFieldMapper, ADDON_KIND_DEFINITIONS, injectNgForgeAddonAction, NgForgeAddonAction } from '@ng-forge/dynamic-forms/integration';
5
+ import { injectNgForgeAction, DynamicTextPipe, NgForgeActionHost, injectNgForgeField, NgForgeControl, NgForgeFieldHost, runPresetAction, injectNgForgeAddons, DfAddonSlot, ADDON_PRESET_HANDLER, FIELD_SIGNAL_CONTEXT, NgForgeAddons, isEqual, DEFAULT_PROPS, ARRAY_CONTEXT, buildBaseInputs, valueFieldMapper, optionsFieldMapper, checkboxFieldMapper, submitButtonFieldMapper, nextButtonFieldMapper, previousButtonFieldMapper, addArrayItemButtonMapper, prependArrayItemButtonMapper, insertArrayItemButtonMapper, removeArrayItemButtonMapper, popArrayItemButtonMapper, shiftArrayItemButtonMapper, datepickerFieldMapper, ADDON_TYPE_DEFINITIONS, injectNgForgeAddonAction, NgForgeAddonAction } from '@ng-forge/dynamic-forms/integration';
6
6
  import { FormField } from '@angular/forms/signals';
7
7
  import { explicitEffect } from 'ngxtension/explicit-effect';
8
- import { DfAddonSlot, DynamicFormLogger, DynamicFormError } from '@ng-forge/dynamic-forms';
8
+ import { DynamicFormLogger, DynamicFormError } from '@ng-forge/dynamic-forms';
9
9
 
10
10
  class BsButtonFieldComponent {
11
11
  action = injectNgForgeAction();
@@ -1638,26 +1638,26 @@ function withBootstrapFields(config) {
1638
1638
  }
1639
1639
  return base;
1640
1640
  }
1641
- /* -- Bootstrap addon kinds --------------------------------------------- */
1641
+ /* -- Bootstrap addon types --------------------------------------------- */
1642
1642
  const BS_ICON_KIND = {
1643
- kind: 'bs-icon',
1643
+ type: 'bs-icon',
1644
1644
  loadComponent: () => Promise.resolve().then(function () { return bsIconAddon_component; }).then((m) => m.BsIconAddonComponent),
1645
1645
  validate: (addon, fieldKey) => {
1646
1646
  if (typeof addon.icon !== 'string' || addon.icon.length === 0) {
1647
- throw new DynamicFormError(`Addon kind 'bs-icon' requires a non-empty 'icon' string (field: '${fieldKey}').`);
1647
+ throw new DynamicFormError(`Addon type 'bs-icon' requires a non-empty 'icon' string (field: '${fieldKey}').`);
1648
1648
  }
1649
1649
  },
1650
1650
  };
1651
1651
  const BS_BUTTON_KIND = {
1652
- kind: 'bs-button',
1652
+ type: 'bs-button',
1653
1653
  loadComponent: () => Promise.resolve().then(function () { return bsButtonAddon_component; }).then((m) => m.BsButtonAddonComponent),
1654
1654
  validate: (addon, fieldKey) => {
1655
1655
  const set = [addon.preset, addon.actionRef, addon.action].filter((v) => v !== undefined);
1656
1656
  if (set.length > 1) {
1657
- throw new DynamicFormError(`Addon kind 'bs-button' on field '${fieldKey}' has more than one of preset/actionRef/action — exactly one allowed.`);
1657
+ throw new DynamicFormError(`Addon type 'bs-button' on field '${fieldKey}' has more than one of preset/actionRef/action — exactly one allowed.`);
1658
1658
  }
1659
1659
  if (addon.icon && !addon.label && !addon.ariaLabel) {
1660
- throw new DynamicFormError(`Addon kind 'bs-button' on field '${fieldKey}' is icon-only — provide 'ariaLabel' for accessibility.`);
1660
+ throw new DynamicFormError(`Addon type 'bs-button' on field '${fieldKey}' is icon-only — provide 'ariaLabel' for accessibility.`);
1661
1661
  }
1662
1662
  },
1663
1663
  };
@@ -1665,8 +1665,8 @@ function withBootstrapAddons() {
1665
1665
  return {
1666
1666
  ɵkind: 'addons',
1667
1667
  ɵproviders: [
1668
- { provide: ADDON_KIND_DEFINITIONS, useValue: BS_ICON_KIND, multi: true },
1669
- { provide: ADDON_KIND_DEFINITIONS, useValue: BS_BUTTON_KIND, multi: true },
1668
+ { provide: ADDON_TYPE_DEFINITIONS, useValue: BS_ICON_KIND, multi: true },
1669
+ { provide: ADDON_TYPE_DEFINITIONS, useValue: BS_BUTTON_KIND, multi: true },
1670
1670
  ],
1671
1671
  };
1672
1672
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ng-forge-dynamic-forms-bootstrap.mjs","mappings":";;;;;;;;;AA4Bc;;AAGH;;IAGA;;AAEA;;AAEA;AACP;AACA;AACA;;;;AAKE;AACA;;;AAGA;AACD;;;;;;AAOD;;AACA;;;AA/BiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC4BP;;AAGH;;;AAHU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAdlB;;;;AAlCS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACAD;;AACA;;AACA;;AAEQ;AAEA;AACf;;;;;AAIE;;AAEJ;AAEiB;AACf;;;;;AAIE;;AAEJ;AAEiB;AACf;;;;;AAIE;;AAEJ;;;;;;AAnCS;AACT;AACD;;;ACsFa;;;;;;;;AAMH;;IAGA;AACP;;;;AAIO;AACP;;;;;AAfiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAnFT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACDV;AACF;;;;ACqOc;;;;AAMH;;AAET,IAKS;;IAGQ;AAER;;AACA;;IAGU;;;AAtBA;AApFjB;AACE;AACA;AACD;AACD;gFAME;;AAEE;AACA;AACA;oBAEA;;AAEE;AACE;AACA;;;;;;AAOC;;;;AAIR;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AApKO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqIR;AACE;AACA;AACD;AACD;4IAME;;AAEE;AACA;AACA;;;AAIE;AACE;AACA;;;;;;AAOC;;;;AAIR;AACF;;;;;;;;ACrHW;;;;AAIH;;IAGA;;;;;AAKP;;;AAGF;AAEI;;AAEF;AAIF;AACE;AACE;AAEA;AACE;;AAEJ;AAEA;AACE;AACA;;;AAIE;;AAEJ;;;AAIA;;;AAGI;AACE;AACA;;;AAEF;;AAEJ;;;AArDiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAlBlB;;;;AA1CS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ICsFD;;;;AAEA;;IAGC;AACR;AACE;;;;AAjBO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAVV;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC/BW;;;;AAIH;;;AAJU;;;;;;;;;;;;;;;;AAdlB;;;;AApBS;;;;;;;;;;;;;;;;;;;;;;;;ACgDE;;;;AAIH;;AAET;;AAGE;AAEA;AACE;;;;;AAZe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAVlB;;;;AAvCS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACkDE;;;;;;AAKH;;AAEA;;;AAPU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAlDT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC6EE;;AAGH;;;AAHU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAVlB;;;;AApES;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC6DE;;AAGH;;;AAHU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA3BlB;;;;AAlCS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACJL;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;ACXI;AACJ;AACA;;;AAKE;AACE;;AAGF;AACE;;AAGF;AACE;;;AAKA;;;AAKA;;QAIF;AACA;;AAGI;;AACA;AACF;;;;AAKE;AACA;;;;AAKJ;AACF;AACF;;;AC5CA;;;AAIA;AACE;AACA;;AAGK;AACL;;AAEE;AACA;;AAEA;AACA;AACE;AACD;AACD;AACD;AACD;;AAEE;AACA;AACA;AACA;AACD;AACD;;AAEE;AACA;AACA;AACA;AACD;AACD;;AAEE;AACA;AACA;AACD;AACD;;AAEE;AACA;AACA;AACD;AACD;;AAEE;AACA;AACA;AACD;AACD;;AAEE;AACA;AACA;AACD;AACD;;AAEE;AACA;AACA;AACD;AACD;;AAEE;AACA;AACA;AACD;AACD;;AAEE;AACA;AACA;AACD;AACD;;AAEE;AACA;AACA;AACD;AACD;;AAEE;AACA;AACA;AACD;AACD;;AAEE;AACA;AACA;AACD;AACD;;AAEE;AACA;;AAEA;AACA;AACD;AACD;;AAEE;AACA;AACA;AACA;AACD;AACD;;AAEE;AACA;AACA;AACA;AACD;AACD;;AAEE;AACA;AACA;AACA;AACD;AACD;;AAEE;AACA;AACA;AACA;AACD;AACD;;AAEE;AACA;AACA;AACA;AACD;;;;ACtHG;;;;AAOA;;AAEgC;AAClC;;AAGF;AACF;AAEA;AAEA;AACE;AACA;AACA;AACE;AACE;;;;AAKN;AACE;AACA;AACA;;AAIE;AACE;;QAKF;AACE;;;;;;AAkBF;AACA;;;AAGC;;AAEL;;;;;ICnFW;;AAEU;;;;AAEA;;;AAPR;;;;AATF;AACT;AACA;AACA;AACA;AACE;AACD;;AAEF;;;;;;;;;;ICuBoB;;;;;AAIA;;;;;AAIA;;;AAZR;;;;;;;;;;;;;;;;;;;;;;;;AA3BF;AACT;AACA;;AAEA;;;;;;;;;;;;;;;;;;;;AAoBC;;AAEF","names":[],"ignoreList":[],"sources":["../../../../packages/dynamic-forms-bootstrap/src/lib/fields/button/bs-button.component.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/fields/checkbox/bs-checkbox.component.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/directives/input-constraints.directive.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/fields/datepicker/bs-datepicker.component.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/addons/preset-actions.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/fields/input/bs-input.component.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/fields/multi-checkbox/bs-multi-checkbox.component.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/fields/radio/bs-radio-group.component.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/fields/radio/bs-radio.component.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/fields/select/bs-select.component.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/fields/slider/bs-slider.component.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/fields/textarea/bs-textarea.component.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/fields/toggle/bs-toggle.component.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/types/types.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/fields/button/bs-button.mapper.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/config/bootstrap-field-config.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/providers/bootstrap-providers.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/addons/bs-icon-addon.component.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/addons/bs-button-addon.component.ts"],"sourcesContent":["import { AsyncPipe } from '@angular/common';\nimport { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';\nimport { FormEvent } from '@ng-forge/dynamic-forms';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\nimport { injectNgForgeAction, NgForgeActionHost } from '@ng-forge/dynamic-forms/integration';\nimport { BsButtonProps } from './bs-button.type';\n\n@Component({\n selector: 'df-bs-button',\n imports: [DynamicTextPipe, AsyncPipe],\n styleUrl: '../../styles/_form-field.scss',\n hostDirectives: [NgForgeActionHost],\n template: `\n @let buttonId = action.key() + '-button';\n <button\n [id]=\"buttonId\"\n [type]=\"buttonType()\"\n [disabled]=\"action.disabled()\"\n [class]=\"buttonClasses()\"\n [attr.tabindex]=\"action.tabIndex()\"\n [attr.data-testid]=\"buttonTestId()\"\n (click)=\"onClick()\"\n >\n {{ action.label() | dynamicText | async }}\n </button>\n `,\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport default class BsButtonFieldComponent<TEvent extends FormEvent> {\n protected readonly action = injectNgForgeAction<TEvent>();\n\n readonly props = input<BsButtonProps>();\n\n /** Resolved button type — defaults to 'button' unless overridden via props. */\n readonly buttonType = computed(() => this.props()?.type ?? 'button');\n\n readonly buttonTestId = computed(() => `${this.buttonType()}-${this.action.key()}`);\n\n readonly buttonClasses = computed(() => {\n const p = this.props();\n const variant = p?.variant || 'primary';\n const outline = p?.outline ? 'outline-' : '';\n\n return [\n 'btn',\n `btn-${outline}${variant}`,\n p?.size === 'sm' && 'btn-sm',\n p?.size === 'lg' && 'btn-lg',\n p?.block && 'w-100',\n p?.active && 'active',\n this.action.className(),\n ]\n .filter(Boolean)\n .join(' ');\n });\n\n /** Submit buttons let the native form handle submission; other buttons dispatch via the directive. */\n onClick(): void {\n if (this.buttonType() === 'submit') return;\n this.action.dispatch();\n }\n}\n","import { ChangeDetectionStrategy, Component, input } from '@angular/core';\nimport { FormField } from '@angular/forms/signals';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\nimport { NgForgeControl, injectNgForgeField, NgForgeFieldHost } from '@ng-forge/dynamic-forms/integration';\nimport { BsCheckboxProps } from './bs-checkbox.type';\nimport { AsyncPipe } from '@angular/common';\n\n@Component({\n selector: 'df-bs-checkbox',\n imports: [FormField, DynamicTextPipe, AsyncPipe, NgForgeControl],\n styleUrl: '../../styles/_form-field.scss',\n hostDirectives: [NgForgeFieldHost],\n template: `\n @let f = ngf.field(); @let checkboxId = ngf.key() + '-checkbox';\n\n <div\n class=\"form-check\"\n [class.form-switch]=\"props()?.switch\"\n [class.form-check-inline]=\"props()?.inline\"\n [class.form-check-reverse]=\"props()?.reverse\"\n [attr.hidden]=\"f().hidden() || null\"\n >\n <input\n ngForgeControl\n type=\"checkbox\"\n [formField]=\"f\"\n [id]=\"checkboxId\"\n [indeterminate]=\"props()?.indeterminate ?? false\"\n class=\"form-check-input\"\n [class.is-invalid]=\"f().invalid() && f().touched()\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n />\n <label [for]=\"checkboxId\" class=\"form-check-label\">\n {{ ngf.label() | dynamicText | async }}\n </label>\n </div>\n\n @if (ngf.errorsToDisplay()[0]; as error) {\n <div class=\"invalid-feedback d-block\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</div>\n } @else if (props()?.hint; as hint) {\n <div class=\"form-text\" [id]=\"ngf.hintId()\" [attr.hidden]=\"f().hidden() || null\">{{ hint | dynamicText | async }}</div>\n }\n `,\n styles: [\n `\n :host {\n display: block;\n }\n\n :host([hidden]) {\n display: none !important;\n }\n `,\n ],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport default class BsCheckboxFieldComponent {\n protected readonly ngf = injectNgForgeField<boolean>();\n\n readonly props = input<BsCheckboxProps>();\n}\n","import { Directive, ElementRef, inject, input } from '@angular/core';\nimport { explicitEffect } from 'ngxtension/explicit-effect';\n\n/** Directive to set min, max, and step attributes on form inputs */\n@Directive({\n selector: '[dfBsInputConstraints]',\n})\nexport class InputConstraintsDirective {\n readonly dfMin = input<number | string | null | undefined>();\n readonly dfMax = input<number | string | null | undefined>();\n readonly dfStep = input<number | string | null | undefined>();\n\n private readonly el = inject<ElementRef<HTMLInputElement>>(ElementRef);\n\n private readonly minEffect = explicitEffect([this.dfMin], ([minValue]) => {\n const nativeElement = this.el.nativeElement;\n if (minValue !== null && minValue !== undefined) {\n nativeElement.setAttribute('min', String(minValue));\n } else {\n nativeElement.removeAttribute('min');\n }\n });\n\n private readonly maxEffect = explicitEffect([this.dfMax], ([maxValue]) => {\n const nativeElement = this.el.nativeElement;\n if (maxValue !== null && maxValue !== undefined) {\n nativeElement.setAttribute('max', String(maxValue));\n } else {\n nativeElement.removeAttribute('max');\n }\n });\n\n private readonly stepEffect = explicitEffect([this.dfStep], ([stepValue]) => {\n const nativeElement = this.el.nativeElement;\n if (stepValue !== null && stepValue !== undefined) {\n nativeElement.setAttribute('step', String(stepValue));\n } else {\n nativeElement.removeAttribute('step');\n }\n });\n}\n","import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';\nimport { FormField } from '@angular/forms/signals';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\nimport { NgForgeControl, injectNgForgeField, NgForgeFieldHost } from '@ng-forge/dynamic-forms/integration';\nimport { BsDatepickerProps } from './bs-datepicker.type';\nimport { AsyncPipe } from '@angular/common';\nimport { InputConstraintsDirective } from '../../directives/input-constraints.directive';\n\n@Component({\n selector: 'df-bs-datepicker',\n imports: [FormField, DynamicTextPipe, AsyncPipe, InputConstraintsDirective, NgForgeControl],\n styleUrl: '../../styles/_form-field.scss',\n hostDirectives: [NgForgeFieldHost],\n template: `\n @let f = ngf.field(); @let p = props(); @let inputId = ngf.key() + '-input';\n @if (p?.floatingLabel) {\n <!-- Floating label variant -->\n <div class=\"form-floating mb-3\">\n <input\n ngForgeControl\n dfBsInputConstraints\n [formField]=\"f\"\n [id]=\"inputId\"\n type=\"date\"\n [placeholder]=\"(ngf.placeholder() | dynamicText | async) ?? ''\"\n [dfMin]=\"minAsString()\"\n [dfMax]=\"maxAsString()\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n class=\"form-control\"\n [class.form-control-sm]=\"p?.size === 'sm'\"\n [class.form-control-lg]=\"p?.size === 'lg'\"\n [class.is-invalid]=\"f().invalid() && f().touched()\"\n [class.is-valid]=\"f().valid() && f().touched() && p?.validFeedback\"\n />\n @if (ngf.label()) {\n <label [for]=\"inputId\">{{ ngf.label() | dynamicText | async }}</label>\n }\n @if (p?.validFeedback && f().valid() && f().touched()) {\n <div class=\"valid-feedback d-block\">\n {{ p?.validFeedback | dynamicText | async }}\n </div>\n }\n @if (ngf.errorsToDisplay()[0]; as error) {\n <div class=\"invalid-feedback d-block\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</div>\n }\n </div>\n } @else {\n <!-- Standard variant -->\n <div class=\"mb-3\">\n @if (ngf.label()) {\n <label [for]=\"inputId\" class=\"form-label\">{{ ngf.label() | dynamicText | async }}</label>\n }\n\n <input\n ngForgeControl\n dfBsInputConstraints\n [formField]=\"f\"\n [id]=\"inputId\"\n type=\"date\"\n [placeholder]=\"(ngf.placeholder() | dynamicText | async) ?? ''\"\n [dfMin]=\"minAsString()\"\n [dfMax]=\"maxAsString()\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n class=\"form-control\"\n [class.form-control-sm]=\"p?.size === 'sm'\"\n [class.form-control-lg]=\"p?.size === 'lg'\"\n [class.is-invalid]=\"f().invalid() && f().touched()\"\n [class.is-valid]=\"f().valid() && f().touched() && p?.validFeedback\"\n />\n\n @if (p?.validFeedback && f().valid() && f().touched()) {\n <div class=\"valid-feedback d-block\">\n {{ p?.validFeedback | dynamicText | async }}\n </div>\n }\n @if (ngf.errorsToDisplay()[0]; as error) {\n <div class=\"invalid-feedback d-block\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</div>\n } @else if (p?.hint) {\n <div class=\"form-text\" [id]=\"ngf.hintId()\">{{ p?.hint | dynamicText | async }}</div>\n }\n </div>\n }\n `,\n changeDetection: ChangeDetectionStrategy.OnPush,\n styles: [\n `\n :host([hidden]) {\n display: none !important;\n }\n `,\n ],\n})\nexport default class BsDatepickerFieldComponent {\n protected readonly ngf = injectNgForgeField<string>();\n\n readonly minDate = input<Date | string | null>(null);\n readonly maxDate = input<Date | string | null>(null);\n readonly startAt = input<Date | null>(null);\n readonly props = input<BsDatepickerProps>();\n\n // Helper methods to convert Date to string for HTML attributes\n readonly minAsString = computed(() => {\n const min = this.minDate();\n return min instanceof Date ? min.toISOString().split('T')[0] : min;\n });\n\n readonly maxAsString = computed(() => {\n const max = this.maxDate();\n return max instanceof Date ? max.toISOString().split('T')[0] : max;\n });\n}\n","import type { AddonActionContext, AddonActionPreset } from '@ng-forge/dynamic-forms';\nimport type { PresetCollaborators } from '@ng-forge/dynamic-forms/integration';\nimport { runPresetAction } from '@ng-forge/dynamic-forms/integration';\n\nexport type { PresetCollaborators };\n\n/** Bootstrap adapter binding for the shared preset runner. */\nexport function runBsPresetAction(preset: AddonActionPreset, ctx: AddonActionContext, collaborators: PresetCollaborators): Promise<void> {\n return runPresetAction(preset, ctx, collaborators, 'Bootstrap', 'bs-input');\n}\n","import { AsyncPipe } from '@angular/common';\nimport { ChangeDetectionStrategy, Component, computed, forwardRef, inject, input, signal } from '@angular/core';\nimport { FormField } from '@angular/forms/signals';\nimport { AddonActionContext, AddonActionPreset, DfAddonSlot, DynamicFormLogger } from '@ng-forge/dynamic-forms';\nimport { WrapperFieldInputs } from '@ng-forge/dynamic-forms/integration';\nimport { DynamicTextPipe, FIELD_SIGNAL_CONTEXT } from '@ng-forge/dynamic-forms/integration';\nimport {\n ADDON_PRESET_HANDLER,\n AddonPresetHandler,\n injectNgForgeAddons,\n injectNgForgeField,\n NgForgeAddons,\n NgForgeControl,\n NgForgeFieldHost,\n} from '@ng-forge/dynamic-forms/integration';\nimport { BOOTSTRAP_CONFIG } from '../../models/bootstrap-config.token';\nimport { runBsPresetAction } from '../../addons/preset-actions';\nimport { BS_INPUT_TYPE_OVERRIDE } from '../../tokens/input-type-override.token';\nimport { BsInputAddon, BsInputProps } from './bs-input.type';\n\n@Component({\n selector: 'df-bs-input',\n imports: [FormField, DynamicTextPipe, AsyncPipe, NgForgeControl, DfAddonSlot],\n styleUrl: '../../styles/_form-field.scss',\n hostDirectives: [NgForgeFieldHost, NgForgeAddons],\n template: `\n @let f = ngf.field(); @let p = props(); @let inputId = ngf.key() + '-input';\n @if (floatingLabel()) {\n <div class=\"mb-3\">\n @if (ngfa.hasAddons()) {\n <div class=\"input-group\">\n @for (a of ngfa.prefixAddons(); track $index) {\n <span class=\"input-group-text\">\n <df-addon-slot [addon]=\"a\" [fieldInputs]=\"fieldInputs()\" [hidden]=\"ngfa.hiddenSignalCache().get(a)\" />\n </span>\n }\n <div class=\"form-floating\">\n <input\n ngForgeControl\n [formField]=\"f\"\n [id]=\"inputId\"\n [type]=\"type()\"\n [placeholder]=\"(ngf.placeholder() | dynamicText | async) ?? ''\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n class=\"form-control\"\n [class.form-control-sm]=\"size() === 'sm'\"\n [class.form-control-lg]=\"size() === 'lg'\"\n [class.form-control-plaintext]=\"p?.plaintext\"\n [class.is-invalid]=\"f().invalid() && f().touched()\"\n [class.is-valid]=\"f().valid() && f().touched() && p?.validFeedback\"\n />\n @if (ngf.label()) {\n <label [for]=\"inputId\">{{ ngf.label() | dynamicText | async }}</label>\n }\n </div>\n @for (a of ngfa.suffixAddons(); track $index) {\n <span class=\"input-group-text\">\n <df-addon-slot [addon]=\"a\" [fieldInputs]=\"fieldInputs()\" [hidden]=\"ngfa.hiddenSignalCache().get(a)\" />\n </span>\n }\n </div>\n } @else {\n <div class=\"form-floating\">\n <input\n ngForgeControl\n [formField]=\"f\"\n [id]=\"inputId\"\n [type]=\"type()\"\n [placeholder]=\"(ngf.placeholder() | dynamicText | async) ?? ''\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n class=\"form-control\"\n [class.form-control-sm]=\"size() === 'sm'\"\n [class.form-control-lg]=\"size() === 'lg'\"\n [class.form-control-plaintext]=\"p?.plaintext\"\n [class.is-invalid]=\"f().invalid() && f().touched()\"\n [class.is-valid]=\"f().valid() && f().touched() && p?.validFeedback\"\n />\n @if (ngf.label()) {\n <label [for]=\"inputId\">{{ ngf.label() | dynamicText | async }}</label>\n }\n </div>\n }\n @if (p?.validFeedback && f().valid() && f().touched()) {\n <div class=\"valid-feedback d-block\">\n {{ p?.validFeedback | dynamicText | async }}\n </div>\n }\n @if (ngf.errorsToDisplay()[0]; as error) {\n <div class=\"invalid-feedback d-block\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</div>\n }\n </div>\n } @else {\n <div class=\"mb-3\">\n @if (ngf.label()) {\n <label [for]=\"inputId\" class=\"form-label\">{{ ngf.label() | dynamicText | async }}</label>\n }\n @if (ngfa.hasAddons()) {\n <div class=\"input-group\">\n @for (a of ngfa.prefixAddons(); track $index) {\n <span class=\"input-group-text\">\n <df-addon-slot [addon]=\"a\" [fieldInputs]=\"fieldInputs()\" [hidden]=\"ngfa.hiddenSignalCache().get(a)\" />\n </span>\n }\n <input\n ngForgeControl\n [formField]=\"f\"\n [id]=\"inputId\"\n [type]=\"type()\"\n [placeholder]=\"(ngf.placeholder() | dynamicText | async) ?? ''\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n class=\"form-control\"\n [class.form-control-sm]=\"size() === 'sm'\"\n [class.form-control-lg]=\"size() === 'lg'\"\n [class.form-control-plaintext]=\"p?.plaintext\"\n [class.is-invalid]=\"f().invalid() && f().touched()\"\n [class.is-valid]=\"f().valid() && f().touched() && p?.validFeedback\"\n />\n @for (a of ngfa.suffixAddons(); track $index) {\n <span class=\"input-group-text\">\n <df-addon-slot [addon]=\"a\" [fieldInputs]=\"fieldInputs()\" [hidden]=\"ngfa.hiddenSignalCache().get(a)\" />\n </span>\n }\n </div>\n } @else {\n <input\n ngForgeControl\n [formField]=\"f\"\n [id]=\"inputId\"\n [type]=\"type()\"\n [placeholder]=\"(ngf.placeholder() | dynamicText | async) ?? ''\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n class=\"form-control\"\n [class.form-control-sm]=\"size() === 'sm'\"\n [class.form-control-lg]=\"size() === 'lg'\"\n [class.form-control-plaintext]=\"p?.plaintext\"\n [class.is-invalid]=\"f().invalid() && f().touched()\"\n [class.is-valid]=\"f().valid() && f().touched() && p?.validFeedback\"\n />\n }\n @if (p?.validFeedback && f().valid() && f().touched()) {\n <div class=\"valid-feedback d-block\">\n {{ p?.validFeedback | dynamicText | async }}\n </div>\n }\n @if (ngf.errorsToDisplay()[0]; as error) {\n <div class=\"invalid-feedback d-block\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</div>\n } @else if (p?.hint) {\n <div class=\"form-text\" [id]=\"ngf.hintId()\">{{ p?.hint | dynamicText | async }}</div>\n }\n </div>\n }\n `,\n changeDetection: ChangeDetectionStrategy.OnPush,\n providers: [\n {\n provide: BS_INPUT_TYPE_OVERRIDE,\n useFactory: () => signal<string | undefined>(undefined),\n },\n {\n // Adapter-specific preset semantics for `bs-button` addons (clear /\n // reset / paste / copy / toggle-password-visibility). The directive\n // (`NgForgeAddonAction`) delegates here when an addon configures a\n // `preset`. Per-bs-input-instance so the `typeOverride` signal is\n // scoped to one field.\n provide: ADDON_PRESET_HANDLER,\n useFactory: (): AddonPresetHandler => {\n const typeOverride = inject(BS_INPUT_TYPE_OVERRIDE);\n const fsc = inject(FIELD_SIGNAL_CONTEXT, { optional: true });\n const logger = inject(DynamicFormLogger);\n // forwardRef for baselineType only — host.props()?.type gates toggle-password-visibility.\n const host = inject(forwardRef(() => BsInputFieldComponent));\n return {\n run: (preset: string, ctx: AddonActionContext) => {\n const fieldKey = ctx.field.key;\n return runBsPresetAction(preset as AddonActionPreset, ctx, {\n typeOverride,\n fieldValueSetter: ctx.setValue,\n fieldDefaultValueGetter:\n fsc && fieldKey ? () => (fsc.defaultValues() as Record<string, unknown> | undefined)?.[fieldKey] : undefined,\n baselineType: () => host.props()?.type,\n logger,\n });\n },\n };\n },\n },\n ],\n styles: [\n `\n :host([hidden]) {\n display: none !important;\n }\n /* Per-side padding on the .input-group-text addon containers — each\n side is its own knob so consumers can tune the prefix's gap from\n the border (outer-left), the prefix's gap to the input text\n (inner-right), and the symmetric suffix counterparts independently. */\n :host {\n --df-bs-addon-prefix-outer-padding: 0.75rem;\n --df-bs-addon-prefix-inner-padding: 0.75rem;\n --df-bs-addon-suffix-inner-padding: 0.75rem;\n --df-bs-addon-suffix-outer-padding: 0.75rem;\n }\n :host ::ng-deep .input-group > .input-group-text:first-child {\n padding-left: var(--df-bs-addon-prefix-outer-padding);\n padding-right: var(--df-bs-addon-prefix-inner-padding);\n }\n :host ::ng-deep .input-group > .input-group-text:last-child {\n padding-left: var(--df-bs-addon-suffix-inner-padding);\n padding-right: var(--df-bs-addon-suffix-outer-padding);\n }\n /* bs-button addons live inside .input-group-text for visual parity with\n bs-icon (shared grey container). Strip the inner .btn's own visual\n chrome (border, background, focus ring) so the button blends with\n the addon container — done via Bootstrap's own CSS variables so the\n cascade reaches the rendered <button> in any theme. No !important. */\n :host ::ng-deep .input-group-text df-bs-button-addon .btn {\n --bs-btn-bg: transparent;\n --bs-btn-color: var(--bs-body-color);\n --bs-btn-border-color: transparent;\n --bs-btn-hover-bg: rgba(127, 127, 127, 0.12);\n --bs-btn-hover-color: var(--bs-body-color);\n --bs-btn-hover-border-color: transparent;\n --bs-btn-active-bg: rgba(127, 127, 127, 0.18);\n --bs-btn-active-border-color: transparent;\n --bs-btn-focus-shadow-rgb: 0, 0, 0;\n --bs-btn-padding-x: 0;\n --bs-btn-padding-y: 0;\n box-shadow: none;\n }\n :host ::ng-deep .input-group-text df-bs-button-addon .btn:focus-visible {\n /* 2px solid for WCAG 2.4.11; currentColor adapts to light/dark themes. */\n box-shadow: none;\n outline: 2px solid currentColor;\n outline-offset: 2px;\n }\n `,\n ],\n})\nexport default class BsInputFieldComponent {\n private bootstrapConfig = inject(BOOTSTRAP_CONFIG, { optional: true });\n\n protected readonly ngf = injectNgForgeField<string>();\n protected readonly ngfa = injectNgForgeAddons<BsInputAddon>();\n\n readonly props = input<BsInputProps>();\n\n /**\n * Wrapper-style host bag pushed by `DfFieldOutlet`. Declared at the\n * component level so `setInputIfDeclared` (which uses\n * `reflectComponentType`) can write it.\n */\n readonly fieldInputs = input<WrapperFieldInputs | undefined>();\n\n /** Per-instance type override populated by `toggle-password-visibility` preset. */\n private readonly typeOverride = inject(BS_INPUT_TYPE_OVERRIDE);\n\n readonly size = computed(() => this.props()?.size ?? this.bootstrapConfig?.size);\n readonly floatingLabel = computed(() => this.props()?.floatingLabel ?? this.bootstrapConfig?.floatingLabel ?? false);\n\n /** Override (set by `toggle-password-visibility` preset) wins over `props().type`. */\n protected readonly type = computed(() => this.typeOverride() ?? this.props()?.type ?? 'text');\n}\n","import { ChangeDetectionStrategy, Component, computed, input, linkedSignal } from '@angular/core';\nimport { FieldOption, ValueType } from '@ng-forge/dynamic-forms';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\nimport { injectNgForgeField, NgForgeFieldHost, isEqual, NgForgeControl } from '@ng-forge/dynamic-forms/integration';\nimport { explicitEffect } from 'ngxtension/explicit-effect';\nimport { BsMultiCheckboxProps } from './bs-multi-checkbox.type';\nimport { AsyncPipe } from '@angular/common';\n\n@Component({\n selector: 'df-bs-multi-checkbox',\n imports: [DynamicTextPipe, AsyncPipe, NgForgeControl],\n styleUrl: '../../styles/_form-field.scss',\n hostDirectives: [NgForgeFieldHost],\n template: `\n @let f = ngf.field();\n @let checked = checkedValuesMap();\n @if (ngf.label(); as label) {\n <div class=\"form-label\">{{ label | dynamicText | async }}</div>\n }\n\n <div class=\"checkbox-group\">\n @for (option of options(); track option.value; let i = $index) {\n <div\n class=\"form-check\"\n [class.form-switch]=\"props()?.switch\"\n [class.form-check-inline]=\"props()?.inline\"\n [class.form-check-reverse]=\"props()?.reverse\"\n >\n <input\n ngForgeControl\n type=\"checkbox\"\n [id]=\"ngf.key() + '_' + i\"\n [checked]=\"checked['' + option.value]\"\n [disabled]=\"f().disabled() || option.disabled\"\n (change)=\"onCheckboxChange(option, $event)\"\n class=\"form-check-input\"\n [class.is-invalid]=\"f().invalid() && f().touched()\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n />\n <label [for]=\"ngf.key() + '_' + i\" class=\"form-check-label\">\n {{ option.label | dynamicText | async }}\n </label>\n </div>\n }\n </div>\n\n @if (ngf.errorsToDisplay()[0]; as error) {\n <div class=\"invalid-feedback d-block\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</div>\n } @else if (props()?.hint; as hint) {\n <div class=\"form-text\" [id]=\"ngf.hintId()\">{{ hint | dynamicText | async }}</div>\n }\n `,\n styles: [\n `\n :host {\n display: block;\n }\n\n .checkbox-group {\n margin-bottom: 0.5rem;\n }\n\n :host([hidden]) {\n display: none !important;\n }\n `,\n ],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport default class BsMultiCheckboxFieldComponent {\n protected readonly ngf = injectNgForgeField<ValueType[]>();\n\n readonly options = input<FieldOption<ValueType>[]>([]);\n readonly props = input<BsMultiCheckboxProps>();\n\n /** Computed map of checked option values for O(1) lookup in template */\n readonly checkedValuesMap = computed(() => {\n const map: Record<string, boolean> = {};\n for (const opt of this.valueViewModel()) {\n map[String(opt.value)] = true;\n }\n return map;\n });\n\n valueViewModel = linkedSignal<FieldOption<ValueType>[]>(\n () => {\n const currentValues = this.ngf.field()().value();\n return this.options().filter((option) => currentValues.includes(option.value));\n },\n { equal: isEqual },\n );\n\n constructor() {\n explicitEffect([this.valueViewModel], ([selectedOptions]: [FieldOption<ValueType>[]]) => {\n const selectedValues = selectedOptions.map((option: FieldOption<ValueType>) => option.value);\n\n if (!isEqual(selectedValues, this.ngf.field()().value())) {\n this.ngf.field()().value.set(selectedValues);\n }\n });\n\n explicitEffect([this.options], ([options]: [FieldOption<ValueType>[]]) => {\n const values = options.map((option: FieldOption<ValueType>) => option.value);\n const uniqueValues = new Set(values);\n\n if (values.length !== uniqueValues.size) {\n const duplicates = values.filter((value, index) => values.indexOf(value) !== index);\n throw new Error(`Duplicate option values detected in bs-multi-checkbox: ${duplicates.join(', ')}`);\n }\n });\n }\n\n onCheckboxChange(option: FieldOption<ValueType>, event: Event): void {\n const checked = (event.target as HTMLInputElement).checked;\n this.valueViewModel.update((currentOptions: FieldOption<ValueType>[]) => {\n if (checked) {\n return currentOptions.some((opt: FieldOption<ValueType>) => opt.value === option.value)\n ? currentOptions\n : [...currentOptions, option];\n } else {\n return currentOptions.filter((opt: FieldOption<ValueType>) => opt.value !== option.value);\n }\n });\n }\n}\n","import { ChangeDetectionStrategy, Component, input, model } from '@angular/core';\nimport type { FormValueControl } from '@angular/forms/signals';\nimport { DynamicText, FieldOption, ValueType } from '@ng-forge/dynamic-forms';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\nimport { NgForgeControl } from '@ng-forge/dynamic-forms/integration';\nimport { AsyncPipe } from '@angular/common';\n\nexport interface BsRadioGroupProps {\n /** Display radio buttons inline (horizontal) */\n inline?: boolean;\n /** Reverse the position of the radio button and label */\n reverse?: boolean;\n /** Display as button group instead of radio buttons */\n buttonGroup?: boolean;\n /** Button size when using button group */\n buttonSize?: 'sm' | 'lg';\n /** Hint text to display below the radio group */\n hint?: DynamicText;\n}\n\n/**\n * Bootstrap radio group implementing FormValueControl. Rendered inside\n * `df-bs-radio` — each `<input type=\"radio\">` carries `ngForgeControl`, so\n * the marker absorbs meta + aria from the ambient parent NgForgeField.\n */\n@Component({\n selector: 'df-bs-radio-group',\n imports: [DynamicTextPipe, AsyncPipe, NgForgeControl],\n template: `\n @let props = properties();\n @if (props?.buttonGroup) {\n <div class=\"btn-group\" role=\"group\" [attr.aria-label]=\"label() | dynamicText | async\">\n @for (option of options(); track option.value; let i = $index) {\n <input\n ngForgeControl\n type=\"radio\"\n [name]=\"name()\"\n [value]=\"option.value\"\n [checked]=\"value() === option.value\"\n (change)=\"onRadioChange(option.value)\"\n [disabled]=\"disabled() || option.disabled || false\"\n class=\"btn-check\"\n [id]=\"name() + '_' + i\"\n autocomplete=\"off\"\n />\n <label\n class=\"btn btn-outline-primary\"\n [class.btn-sm]=\"props?.buttonSize === 'sm'\"\n [class.btn-lg]=\"props?.buttonSize === 'lg'\"\n [for]=\"name() + '_' + i\"\n >\n {{ option.label | dynamicText | async }}\n </label>\n }\n </div>\n } @else {\n @for (option of options(); track option.value; let i = $index) {\n <div class=\"form-check\" [class.form-check-inline]=\"props?.inline\" [class.form-check-reverse]=\"props?.reverse\">\n <input\n ngForgeControl\n type=\"radio\"\n [name]=\"name()\"\n [value]=\"option.value\"\n [checked]=\"value() === option.value\"\n (change)=\"onRadioChange(option.value)\"\n [disabled]=\"disabled() || option.disabled || false\"\n class=\"form-check-input\"\n [id]=\"name() + '_' + i\"\n />\n <label class=\"form-check-label\" [for]=\"name() + '_' + i\">\n {{ option.label | dynamicText | async }}\n </label>\n </div>\n }\n }\n `,\n styles: [\n `\n :host {\n display: block;\n }\n `,\n ],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class BsRadioGroupComponent implements FormValueControl<ValueType | undefined> {\n // Value model - FormField directive binds form value to this\n readonly value = model<ValueType | undefined>(undefined);\n\n // Optional FormValueControl properties - Field directive will bind these\n readonly disabled = input<boolean>(false);\n readonly readonly = input<boolean>(false);\n readonly name = input<string>('');\n\n // Component-specific inputs\n readonly label = input<DynamicText>();\n readonly options = input.required<FieldOption<ValueType>[]>();\n readonly properties = input<BsRadioGroupProps>();\n\n /** Handle radio button change event */\n protected onRadioChange(newValue: ValueType): void {\n if (!this.disabled() && !this.readonly()) {\n this.value.set(newValue);\n }\n }\n}\n","import { ChangeDetectionStrategy, Component, input } from '@angular/core';\nimport { FormField } from '@angular/forms/signals';\nimport { FieldOption, ValueType } from '@ng-forge/dynamic-forms';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\nimport { injectNgForgeField, NgForgeFieldHost } from '@ng-forge/dynamic-forms/integration';\nimport { BsRadioProps } from './bs-radio.type';\nimport { AsyncPipe } from '@angular/common';\nimport { BsRadioGroupComponent } from './bs-radio-group.component';\n\n@Component({\n selector: 'df-bs-radio',\n imports: [BsRadioGroupComponent, FormField, DynamicTextPipe, AsyncPipe],\n styleUrl: '../../styles/_form-field.scss',\n hostDirectives: [NgForgeFieldHost],\n template: `\n @let f = ngf.field();\n\n <div class=\"mb-3\">\n @if (ngf.label(); as label) {\n <div class=\"form-label\">{{ label | dynamicText | async }}</div>\n }\n\n <df-bs-radio-group [formField]=\"f\" [label]=\"ngf.label()\" [options]=\"options()\" [properties]=\"props()\" />\n\n @if (ngf.errorsToDisplay()[0]; as error) {\n <div class=\"invalid-feedback d-block\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</div>\n } @else if (props()?.hint; as hint) {\n <div class=\"form-text\" [id]=\"ngf.hintId()\">{{ hint | dynamicText | async }}</div>\n }\n </div>\n `,\n styles: [\n `\n :host {\n display: block;\n }\n\n :host([hidden]) {\n display: none !important;\n }\n `,\n ],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport default class BsRadioFieldComponent {\n protected readonly ngf = injectNgForgeField<ValueType>();\n\n readonly options = input<FieldOption<ValueType>[]>([]);\n readonly props = input<BsRadioProps>();\n}\n","import { ChangeDetectionStrategy, Component, input } from '@angular/core';\nimport { FormField } from '@angular/forms/signals';\nimport { FieldOption } from '@ng-forge/dynamic-forms';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\nimport { NgForgeControl, injectNgForgeField, NgForgeFieldHost } from '@ng-forge/dynamic-forms/integration';\nimport { BsSelectProps } from './bs-select.type';\nimport { AsyncPipe } from '@angular/common';\n\n@Component({\n selector: 'df-bs-select',\n imports: [FormField, DynamicTextPipe, AsyncPipe, NgForgeControl],\n styleUrl: '../../styles/_form-field.scss',\n hostDirectives: [NgForgeFieldHost],\n template: `\n @let f = ngf.field(); @let selectId = ngf.key() + '-select';\n\n <div class=\"mb-3\">\n @if (ngf.label(); as label) {\n <label [for]=\"selectId\" class=\"form-label\">{{ label | dynamicText | async }}</label>\n }\n <select\n ngForgeControl\n [formField]=\"f\"\n [id]=\"selectId\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n class=\"form-select\"\n [class.form-select-sm]=\"props()?.size === 'sm'\"\n [class.form-select-lg]=\"props()?.size === 'lg'\"\n [class.is-invalid]=\"f().invalid() && f().touched()\"\n [multiple]=\"props()?.multiple || false\"\n [size]=\"props()?.htmlSize\"\n >\n @if (ngf.placeholder(); as placeholder) {\n <option value=\"\" disabled [selected]=\"!f().value()\">{{ placeholder | dynamicText | async }}</option>\n }\n @for (option of options(); track option.value) {\n <option [value]=\"option.value\" [disabled]=\"option.disabled || false\" [selected]=\"isSelected(option.value, f().value())\">\n {{ option.label | dynamicText | async }}\n </option>\n }\n </select>\n\n @if (ngf.errorsToDisplay()[0]; as error) {\n <div class=\"invalid-feedback d-block\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</div>\n } @else if (props()?.hint; as hint) {\n <div class=\"form-text\" [id]=\"ngf.hintId()\">{{ hint | dynamicText | async }}</div>\n }\n </div>\n `,\n changeDetection: ChangeDetectionStrategy.OnPush,\n styles: [\n `\n :host([hidden]) {\n display: none !important;\n }\n `,\n ],\n})\nexport default class BsSelectFieldComponent {\n protected readonly ngf = injectNgForgeField<string>();\n\n readonly options = input<FieldOption<string>[]>([]);\n readonly props = input<BsSelectProps>();\n\n defaultCompare = Object.is;\n\n protected isSelected(optionValue: string, fieldValue: string | string[] | null): boolean {\n const compareWith = this.props()?.compareWith || this.defaultCompare;\n\n if (Array.isArray(fieldValue)) {\n return fieldValue.some((v) => compareWith(v, optionValue));\n }\n\n return fieldValue !== null && compareWith(fieldValue, optionValue);\n }\n}\n","import { ChangeDetectionStrategy, Component, input } from '@angular/core';\nimport { FormField } from '@angular/forms/signals';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\nimport { NgForgeControl, injectNgForgeField, NgForgeFieldHost } from '@ng-forge/dynamic-forms/integration';\nimport { BsSliderProps } from './bs-slider.type';\nimport { AsyncPipe } from '@angular/common';\nimport { InputConstraintsDirective } from '../../directives/input-constraints.directive';\n\n@Component({\n selector: 'df-bs-slider',\n imports: [FormField, DynamicTextPipe, AsyncPipe, InputConstraintsDirective, NgForgeControl],\n styleUrl: '../../styles/_form-field.scss',\n hostDirectives: [NgForgeFieldHost],\n template: `\n @let f = ngf.field(); @let inputId = ngf.key() + '-input';\n\n <div class=\"mb-3\">\n @if (ngf.label(); as label) {\n <label [for]=\"inputId\" class=\"form-label\">\n {{ label | dynamicText | async }}\n @if (props()?.showValue) {\n <span class=\"ms-2 badge bg-secondary\"> {{ props()?.valuePrefix }}{{ f().value() }}{{ props()?.valueSuffix }} </span>\n }\n </label>\n }\n\n <input\n ngForgeControl\n type=\"range\"\n dfBsInputConstraints\n [formField]=\"f\"\n [id]=\"inputId\"\n [dfMin]=\"f().min?.() ?? props()?.min ?? min()\"\n [dfMax]=\"f().max?.() ?? props()?.max ?? max()\"\n [dfStep]=\"step() ?? props()?.step ?? 1\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n class=\"form-range\"\n />\n\n @if (ngf.errorsToDisplay()[0]; as error) {\n <div class=\"invalid-feedback d-block\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</div>\n } @else if (props()?.hint; as hint) {\n <div class=\"form-text\" [id]=\"ngf.hintId()\">{{ hint | dynamicText | async }}</div>\n }\n </div>\n `,\n styles: [\n `\n :host {\n display: block;\n }\n\n :host([hidden]) {\n display: none !important;\n }\n `,\n ],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport default class BsSliderFieldComponent {\n protected readonly ngf = injectNgForgeField<number>();\n\n readonly min = input<number>(0);\n readonly max = input<number>(100);\n readonly step = input<number>();\n\n readonly props = input<BsSliderProps>();\n}\n","import { ChangeDetectionStrategy, Component, input } from '@angular/core';\nimport { FormField } from '@angular/forms/signals';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\nimport { NgForgeControl, injectNgForgeField, NgForgeFieldHost } from '@ng-forge/dynamic-forms/integration';\nimport { BsTextareaProps } from './bs-textarea.type';\nimport { AsyncPipe } from '@angular/common';\n\n@Component({\n selector: 'df-bs-textarea',\n imports: [FormField, DynamicTextPipe, AsyncPipe, NgForgeControl],\n styleUrl: '../../styles/_form-field.scss',\n hostDirectives: [NgForgeFieldHost],\n template: `\n @let f = ngf.field(); @let p = props(); @let textareaId = ngf.key() + '-textarea';\n @if (p?.floatingLabel) {\n <!-- Floating label variant -->\n <div class=\"form-floating mb-3\">\n <textarea\n ngForgeControl\n [formField]=\"f\"\n [id]=\"textareaId\"\n [placeholder]=\"(ngf.placeholder() | dynamicText | async) ?? ''\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n class=\"form-control\"\n [class.form-control-sm]=\"p?.size === 'sm'\"\n [class.form-control-lg]=\"p?.size === 'lg'\"\n [class.is-invalid]=\"f().invalid() && f().touched()\"\n [class.is-valid]=\"f().valid() && f().touched() && p?.validFeedback\"\n ></textarea>\n\n @if (ngf.label()) {\n <label [for]=\"textareaId\">{{ ngf.label() | dynamicText | async }}</label>\n }\n @if (p?.validFeedback && f().valid() && f().touched()) {\n <div class=\"valid-feedback d-block\">\n {{ p?.validFeedback | dynamicText | async }}\n </div>\n }\n @if (ngf.errorsToDisplay()[0]; as error) {\n <div class=\"invalid-feedback d-block\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</div>\n } @else if (p?.hint) {\n <div class=\"form-text\" [id]=\"ngf.hintId()\">{{ p?.hint | dynamicText | async }}</div>\n }\n </div>\n } @else {\n <!-- Standard variant -->\n <div class=\"mb-3\">\n @if (ngf.label()) {\n <label [for]=\"textareaId\" class=\"form-label\">{{ ngf.label() | dynamicText | async }}</label>\n }\n\n <textarea\n ngForgeControl\n [formField]=\"f\"\n [id]=\"textareaId\"\n [placeholder]=\"(ngf.placeholder() | dynamicText | async) ?? ''\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n class=\"form-control\"\n [class.form-control-sm]=\"p?.size === 'sm'\"\n [class.form-control-lg]=\"p?.size === 'lg'\"\n [class.is-invalid]=\"f().invalid() && f().touched()\"\n [class.is-valid]=\"f().valid() && f().touched() && p?.validFeedback\"\n ></textarea>\n\n @if (p?.validFeedback && f().valid() && f().touched()) {\n <div class=\"valid-feedback d-block\">\n {{ p?.validFeedback | dynamicText | async }}\n </div>\n }\n @if (ngf.errorsToDisplay()[0]; as error) {\n <div class=\"invalid-feedback d-block\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</div>\n } @else if (p?.hint) {\n <div class=\"form-text\" [id]=\"ngf.hintId()\">{{ p?.hint | dynamicText | async }}</div>\n }\n </div>\n }\n `,\n changeDetection: ChangeDetectionStrategy.OnPush,\n styles: [\n `\n :host([hidden]) {\n display: none !important;\n }\n `,\n ],\n})\nexport default class BsTextareaFieldComponent {\n protected readonly ngf = injectNgForgeField<string>();\n\n readonly props = input<BsTextareaProps>();\n}\n","import { ChangeDetectionStrategy, Component, input } from '@angular/core';\nimport { FormField } from '@angular/forms/signals';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\nimport { NgForgeControl, injectNgForgeField, NgForgeFieldHost } from '@ng-forge/dynamic-forms/integration';\nimport { BsToggleProps } from './bs-toggle.type';\nimport { AsyncPipe } from '@angular/common';\n\n@Component({\n selector: 'df-bs-toggle',\n imports: [FormField, DynamicTextPipe, AsyncPipe, NgForgeControl],\n styleUrl: '../../styles/_form-field.scss',\n hostDirectives: [NgForgeFieldHost],\n template: `\n @let f = ngf.field(); @let inputId = ngf.key() + '-input';\n\n <div\n class=\"form-check form-switch\"\n [class.form-check-inline]=\"props()?.inline\"\n [class.form-check-reverse]=\"props()?.reverse\"\n [class.form-switch-sm]=\"props()?.size === 'sm'\"\n [class.form-switch-lg]=\"props()?.size === 'lg'\"\n [attr.hidden]=\"f().hidden() || null\"\n >\n <input\n ngForgeControl\n type=\"checkbox\"\n [formField]=\"f\"\n [id]=\"inputId\"\n class=\"form-check-input\"\n [class.is-invalid]=\"f().invalid() && f().touched()\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n />\n <label [for]=\"inputId\" class=\"form-check-label\">\n {{ ngf.label() | dynamicText | async }}\n </label>\n </div>\n\n @if (ngf.errorsToDisplay()[0]; as error) {\n <div class=\"invalid-feedback d-block\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</div>\n } @else if (props()?.hint; as hint) {\n <div class=\"form-text\" [id]=\"ngf.hintId()\" [attr.hidden]=\"f().hidden() || null\">{{ hint | dynamicText | async }}</div>\n }\n `,\n styles: [\n `\n :host {\n display: block;\n }\n\n :host([hidden]) {\n display: none !important;\n }\n\n /* Custom size variants for switches */\n .form-switch-sm .form-check-input {\n width: 1.75rem;\n height: 1rem;\n font-size: 0.875rem;\n }\n\n .form-switch-lg .form-check-input {\n width: 3rem;\n height: 1.75rem;\n font-size: 1.125rem;\n }\n `,\n ],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport default class BsToggleFieldComponent {\n protected readonly ngf = injectNgForgeField<boolean>();\n\n readonly props = input<BsToggleProps>();\n}\n","/**\n * Bootstrap field type constants\n * Based on available field components in the /fields folder\n */\nexport const BsField = {\n Input: 'input',\n Select: 'select',\n Checkbox: 'checkbox',\n Button: 'button',\n Submit: 'submit',\n Next: 'next',\n Previous: 'previous',\n AddArrayItem: 'addArrayItem',\n PrependArrayItem: 'prependArrayItem',\n InsertArrayItem: 'insertArrayItem',\n RemoveArrayItem: 'removeArrayItem',\n PopArrayItem: 'popArrayItem',\n ShiftArrayItem: 'shiftArrayItem',\n Textarea: 'textarea',\n Radio: 'radio',\n MultiCheckbox: 'multi-checkbox',\n Datepicker: 'datepicker',\n Slider: 'slider',\n Toggle: 'toggle',\n} as const;\n\nexport type BsFieldType = (typeof BsField)[keyof typeof BsField];\n","import { computed, inject, isSignal, Signal } from '@angular/core';\nimport { FieldDef } from '@ng-forge/dynamic-forms';\nimport { ARRAY_CONTEXT, buildBaseInputs, DEFAULT_PROPS } from '@ng-forge/dynamic-forms/integration';\n\n/**\n * Generic button mapper for custom events or basic buttons.\n * For specific button types (submit, next, prev, add/remove array items),\n * use the dedicated field types and their specific mappers.\n *\n * @param fieldDef The button field definition\n * @returns Signal containing Record of input names to values for ngComponentOutlet\n */\nexport function buttonFieldMapper(fieldDef: FieldDef<Record<string, unknown>>): Signal<Record<string, unknown>> {\n const defaultProps = inject(DEFAULT_PROPS);\n const arrayContext = inject(ARRAY_CONTEXT, { optional: true });\n\n return computed(() => {\n const baseInputs = buildBaseInputs(fieldDef, defaultProps());\n\n const inputs: Record<string, unknown> = {\n ...baseInputs,\n };\n\n if (fieldDef.disabled !== undefined) {\n inputs['disabled'] = fieldDef.disabled;\n }\n\n if (fieldDef.hidden !== undefined) {\n inputs['hidden'] = fieldDef.hidden;\n }\n\n // Add event binding for button events\n if ('event' in fieldDef && fieldDef.event !== undefined) {\n inputs['event'] = fieldDef.event;\n }\n\n // Add eventArgs binding if provided\n if ('eventArgs' in fieldDef && fieldDef.eventArgs !== undefined) {\n inputs['eventArgs'] = fieldDef.eventArgs;\n }\n\n // Add eventContext for token resolution (supports $template, $arrayKey, $index, etc.)\n const template = 'template' in fieldDef ? fieldDef.template : undefined;\n if (template || arrayContext) {\n // Read signal value if index is a signal (supports differential updates)\n const getIndex = () => {\n if (!arrayContext) return -1;\n return isSignal(arrayContext.index) ? arrayContext.index() : arrayContext.index;\n };\n\n inputs['eventContext'] = {\n key: fieldDef.key,\n index: getIndex(),\n arrayKey: arrayContext?.arrayKey ?? '',\n formValue: arrayContext?.formValue ?? {},\n template,\n };\n }\n\n return inputs;\n });\n}\n","import { FieldTypeDefinition } from '@ng-forge/dynamic-forms/integration';\nimport {\n addArrayItemButtonMapper,\n checkboxFieldMapper,\n datepickerFieldMapper,\n insertArrayItemButtonMapper,\n optionsFieldMapper,\n popArrayItemButtonMapper,\n prependArrayItemButtonMapper,\n removeArrayItemButtonMapper,\n shiftArrayItemButtonMapper,\n valueFieldMapper,\n} from '@ng-forge/dynamic-forms/integration';\nimport { BsField } from '../types/types';\nimport { buttonFieldMapper } from '../fields/button/bs-button.mapper';\nimport { nextButtonFieldMapper, previousButtonFieldMapper, submitButtonFieldMapper } from '../fields/button/bs-specific-button.mapper';\n\nconst VALUE_FIELD_TYPES_BASE = {\n renderReadyWhen: ['field'],\n} as const;\n\nconst BUTTON_FIELD_TYPES_BASE = {\n renderReadyWhen: [],\n valueHandling: 'exclude',\n} as const;\n\nexport const BOOTSTRAP_FIELD_TYPES: FieldTypeDefinition[] = [\n {\n name: BsField.Input,\n loadComponent: () => import('../fields/input/bs-input.component'),\n mapper: valueFieldMapper,\n propsToMeta: ['type'],\n scope: ['text-input', 'numeric'],\n addons: {\n slots: ['prefix', 'suffix'],\n },\n ...VALUE_FIELD_TYPES_BASE,\n },\n {\n name: BsField.Select,\n loadComponent: () => import('../fields/select/bs-select.component'),\n mapper: optionsFieldMapper,\n scope: 'single-select',\n ...VALUE_FIELD_TYPES_BASE,\n },\n {\n name: BsField.Checkbox,\n loadComponent: () => import('../fields/checkbox/bs-checkbox.component'),\n mapper: checkboxFieldMapper,\n scope: 'boolean',\n ...VALUE_FIELD_TYPES_BASE,\n },\n {\n name: BsField.Button,\n loadComponent: () => import('../fields/button/bs-button.component'),\n mapper: buttonFieldMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: BsField.Submit,\n loadComponent: () => import('../fields/button/bs-button.component'),\n mapper: submitButtonFieldMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: BsField.Next,\n loadComponent: () => import('../fields/button/bs-button.component'),\n mapper: nextButtonFieldMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: BsField.Previous,\n loadComponent: () => import('../fields/button/bs-button.component'),\n mapper: previousButtonFieldMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: BsField.AddArrayItem,\n loadComponent: () => import('../fields/button/bs-button.component'),\n mapper: addArrayItemButtonMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: BsField.PrependArrayItem,\n loadComponent: () => import('../fields/button/bs-button.component'),\n mapper: prependArrayItemButtonMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: BsField.InsertArrayItem,\n loadComponent: () => import('../fields/button/bs-button.component'),\n mapper: insertArrayItemButtonMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: BsField.RemoveArrayItem,\n loadComponent: () => import('../fields/button/bs-button.component'),\n mapper: removeArrayItemButtonMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: BsField.PopArrayItem,\n loadComponent: () => import('../fields/button/bs-button.component'),\n mapper: popArrayItemButtonMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: BsField.ShiftArrayItem,\n loadComponent: () => import('../fields/button/bs-button.component'),\n mapper: shiftArrayItemButtonMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: BsField.Textarea,\n loadComponent: () => import('../fields/textarea/bs-textarea.component'),\n mapper: valueFieldMapper,\n propsToMeta: ['rows'],\n scope: 'text-input',\n ...VALUE_FIELD_TYPES_BASE,\n },\n {\n name: BsField.Radio,\n loadComponent: () => import('../fields/radio/bs-radio.component'),\n mapper: optionsFieldMapper,\n scope: 'single-select',\n ...VALUE_FIELD_TYPES_BASE,\n },\n {\n name: BsField.MultiCheckbox,\n loadComponent: () => import('../fields/multi-checkbox/bs-multi-checkbox.component'),\n mapper: optionsFieldMapper,\n scope: 'multi-select',\n ...VALUE_FIELD_TYPES_BASE,\n },\n {\n name: BsField.Datepicker,\n loadComponent: () => import('../fields/datepicker/bs-datepicker.component'),\n mapper: datepickerFieldMapper,\n scope: 'date',\n ...VALUE_FIELD_TYPES_BASE,\n },\n {\n name: BsField.Slider,\n loadComponent: () => import('../fields/slider/bs-slider.component'),\n mapper: valueFieldMapper,\n scope: 'numeric',\n ...VALUE_FIELD_TYPES_BASE,\n },\n {\n name: BsField.Toggle,\n loadComponent: () => import('../fields/toggle/bs-toggle.component'),\n mapper: checkboxFieldMapper,\n scope: 'boolean',\n ...VALUE_FIELD_TYPES_BASE,\n },\n];\n","import type { Provider } from '@angular/core';\nimport { DynamicFormError, type AddonKindDefinition } from '@ng-forge/dynamic-forms';\nimport { ADDON_KIND_DEFINITIONS, type FieldTypeDefinition } from '@ng-forge/dynamic-forms/integration';\nimport { BOOTSTRAP_FIELD_TYPES } from '../config/bootstrap-field-config';\nimport { BootstrapConfig } from '../models/bootstrap-config';\nimport { BOOTSTRAP_CONFIG } from '../models/bootstrap-config.token';\nimport type { BsButtonAddon, BsIconAddon } from '../types/addons';\n\n/** Field type definitions for Bootstrap components. */\nexport type BootstrapFieldTypes = FieldTypeDefinition[];\n\ntype BootstrapConfigFeature = {\n ɵkind: 'bootstrap-config';\n ɵproviders: Provider[];\n};\n\n/**\n * Default `withBootstrapFields()` shape — field defs + the auto-included\n * addons feature so `bs-icon` / `bs-button` work out of the box.\n */\ntype BootstrapFieldsWithAddons = [...BootstrapFieldTypes, BootstrapAddonsFeature];\n\ntype BootstrapFieldsWithConfig = [...BootstrapFieldTypes, BootstrapAddonsFeature, BootstrapConfigFeature];\n\n/**\n * Provides Bootstrap field type definitions for the dynamic form system,\n * with Bootstrap-shipped addon kinds (`bs-icon`, `bs-button`) auto-included\n * so addons work out of the box.\n *\n * @param config - Optional global configuration for Bootstrap form fields\n * @returns Tuple of field type definitions, the addons feature, and\n * optionally a config feature.\n */\nexport function withBootstrapFields(): BootstrapFieldsWithAddons;\nexport function withBootstrapFields(config: BootstrapConfig): BootstrapFieldsWithConfig;\nexport function withBootstrapFields(config: BootstrapConfig | undefined): BootstrapFieldsWithAddons | BootstrapFieldsWithConfig;\nexport function withBootstrapFields(config?: BootstrapConfig): BootstrapFieldsWithAddons | BootstrapFieldsWithConfig {\n // Always include the addons feature — bs-icon / bs-button are part of\n // the canonical Bootstrap surface.\n const base: unknown[] = [...BOOTSTRAP_FIELD_TYPES, withBootstrapAddons()];\n\n if (config) {\n base.push({\n ɵkind: 'bootstrap-config',\n ɵproviders: [{ provide: BOOTSTRAP_CONFIG, useValue: config }],\n } satisfies BootstrapConfigFeature);\n return base as BootstrapFieldsWithConfig;\n }\n\n return base as BootstrapFieldsWithAddons;\n}\n\n/* -- Bootstrap addon kinds --------------------------------------------- */\n\nconst BS_ICON_KIND: AddonKindDefinition<BsIconAddon> = {\n kind: 'bs-icon',\n loadComponent: () => import('../addons/bs-icon-addon.component').then((m) => m.BsIconAddonComponent),\n validate: (addon, fieldKey) => {\n if (typeof addon.icon !== 'string' || addon.icon.length === 0) {\n throw new DynamicFormError(`Addon kind 'bs-icon' requires a non-empty 'icon' string (field: '${fieldKey}').`);\n }\n },\n};\n\nconst BS_BUTTON_KIND: AddonKindDefinition<BsButtonAddon> = {\n kind: 'bs-button',\n loadComponent: () => import('../addons/bs-button-addon.component').then((m) => m.BsButtonAddonComponent),\n validate: (addon, fieldKey) => {\n // Exactly one of preset / actionRef / action — validator drops the addon\n // (with warning) if the rule is violated.\n const set = [addon.preset, addon.actionRef, addon.action].filter((v) => v !== undefined);\n if (set.length > 1) {\n throw new DynamicFormError(\n `Addon kind 'bs-button' on field '${fieldKey}' has more than one of preset/actionRef/action — exactly one allowed.`,\n );\n }\n // Icon-only buttons require ariaLabel for screen readers.\n if (addon.icon && !addon.label && !addon.ariaLabel) {\n throw new DynamicFormError(`Addon kind 'bs-button' on field '${fieldKey}' is icon-only — provide 'ariaLabel' for accessibility.`);\n }\n },\n};\n\n/**\n * Feature kind discriminant for the Bootstrap addons feature. Matches core's\n * `'addons'` kind so providers flow through the standard addon-kind pipeline\n * in `provideDynamicForm`.\n */\ntype BootstrapAddonsFeature = {\n ɵkind: 'addons';\n ɵproviders: Provider[];\n};\n\n/** Register Bootstrap-shipped addon kinds (`bs-icon`, `bs-button`) standalone. */\nexport function withBootstrapAddons(): BootstrapAddonsFeature {\n return {\n ɵkind: 'addons',\n ɵproviders: [\n { provide: ADDON_KIND_DEFINITIONS, useValue: BS_ICON_KIND, multi: true },\n { provide: ADDON_KIND_DEFINITIONS, useValue: BS_BUTTON_KIND, multi: true },\n ],\n };\n}\n","import { AsyncPipe } from '@angular/common';\nimport { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';\nimport { WrapperFieldInputs } from '@ng-forge/dynamic-forms/integration';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\nimport type { BsIconAddon } from '../types/addons';\n\n/** Renderer for the `bs-icon` addon kind. */\n@Component({\n selector: 'df-bs-icon-addon',\n imports: [AsyncPipe, DynamicTextPipe],\n template: `<i [class]=\"iconClass()\" [attr.aria-label]=\"(ariaLabel() | dynamicText | async) ?? null\"></i>`,\n host: {\n '[attr.aria-hidden]': 'hasAriaLabel() ? null : \"true\"',\n },\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class BsIconAddonComponent {\n readonly addon = input.required<BsIconAddon>();\n /** Accepted for contract uniformity — `NgComponentOutlet` setInput is strict; every kind must declare it. */\n readonly fieldInputs = input<WrapperFieldInputs | undefined>();\n\n protected readonly iconClass = computed(() => `bi bi-${this.addon().icon}`);\n protected readonly ariaLabel = computed(() => this.addon().ariaLabel);\n protected readonly hasAriaLabel = computed(() => this.addon().ariaLabel !== undefined);\n}\n","import { AsyncPipe } from '@angular/common';\nimport { ChangeDetectionStrategy, Component, computed } from '@angular/core';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\nimport { injectNgForgeAddonAction, NgForgeAddonAction } from '@ng-forge/dynamic-forms/integration';\nimport type { BsButtonAddon } from '../types/addons';\n\n/** Renderer for the `bs-button` addon kind. */\n@Component({\n selector: 'df-bs-button-addon',\n imports: [DynamicTextPipe, AsyncPipe],\n hostDirectives: [NgForgeAddonAction],\n template: `\n <button\n type=\"button\"\n [class]=\"buttonClass()\"\n [disabled]=\"action.disabled() || action.loading()\"\n [attr.aria-label]=\"(ariaLabel() | dynamicText | async) ?? null\"\n [attr.aria-busy]=\"action.loading() || null\"\n (click)=\"action.dispatch()\"\n >\n @if (action.loading()) {\n <!-- Visually-hidden role=status text gives a reliable AT announcement (VO/JAWS/NVDA). -->\n <span class=\"spinner-border spinner-border-sm\" aria-hidden=\"true\"></span>\n <span class=\"visually-hidden\" role=\"status\">Loading…</span>\n } @else if (iconClass(); as ic) {\n <i [class]=\"ic\" aria-hidden=\"true\"></i>\n }\n @if (label(); as l) {\n <span>{{ l | dynamicText | async }}</span>\n }\n </button>\n `,\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class BsButtonAddonComponent {\n protected readonly action = injectNgForgeAddonAction<BsButtonAddon>();\n\n /** Re-exposed for template binding — same signal stored on the directive. */\n protected readonly addon = this.action.addon;\n\n protected readonly label = computed(() => this.addon().label);\n protected readonly ariaLabel = computed(() => this.addon().ariaLabel);\n protected readonly iconClass = computed(() => {\n const icon = this.addon().icon;\n return icon ? `bi bi-${icon}` : '';\n });\n protected readonly buttonClass = computed(() => `btn btn-outline-${this.addon().severity ?? 'secondary'}`);\n}\n"]}
1
+ {"version":3,"file":"ng-forge-dynamic-forms-bootstrap.mjs","mappings":";;;;;;;;;AA4Bc;;AAGH;;IAGA;;AAEA;;AAEA;AACP;AACA;AACA;;;;AAKE;AACA;;;AAGA;AACD;;;;;;AAOD;;AACA;;;AA/BiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC4BP;;AAGH;;;AAHU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAdlB;;;;AAlCS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACAD;;AACA;;AACA;;AAEQ;AAEA;AACf;;;;;AAIE;;AAEJ;AAEiB;AACf;;;;;AAIE;;AAEJ;AAEiB;AACf;;;;;AAIE;;AAEJ;;;;;;AAnCS;AACT;AACD;;;ACsFa;;;;;;;;AAMH;;IAGA;AACP;;;;AAIO;AACP;;;;;AAfiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAnFT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACDV;AACF;;;;ACsOc;;;;AAMH;;AAET,IAKS;;IAGQ;AAER;;AACA;;IAGU;;;AAtBA;AApFjB;AACE;AACA;AACD;AACD;gFAME;;AAEE;AACA;AACA;oBAEA;;AAEE;AACE;AACA;;;;;;AAOC;;;;AAIR;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AApKO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqIR;AACE;AACA;AACD;AACD;4IAME;;AAEE;AACA;AACA;;;AAIE;AACE;AACA;;;;;;AAOC;;;;AAIR;AACF;;;;;;;;ACtHW;;;;AAIH;;IAGA;;;;;AAKP;;;AAGF;AAEI;;AAEF;AAIF;AACE;AACE;AAEA;AACE;;AAEJ;AAEA;AACE;AACA;;;AAIE;;AAEJ;;;AAIA;;;AAGI;AACE;AACA;;;AAEF;;AAEJ;;;AArDiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAlBlB;;;;AA1CS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ICsFD;;;;AAEA;;IAGC;AACR;AACE;;;;AAjBO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAVV;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC/BW;;;;AAIH;;;AAJU;;;;;;;;;;;;;;;;AAdlB;;;;AApBS;;;;;;;;;;;;;;;;;;;;;;;;ACgDE;;;;AAIH;;AAET;;AAGE;AAEA;AACE;;;;;AAZe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAVlB;;;;AAvCS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACkDE;;;;;;AAKH;;AAEA;;;AAPU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAlDT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC6EE;;AAGH;;;AAHU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAVlB;;;;AApES;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC6DE;;AAGH;;;AAHU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA3BlB;;;;AAlCS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACJL;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;ACXI;AACJ;AACA;;;AAKE;AACE;;AAGF;AACE;;AAGF;AACE;;;AAKA;;;AAKA;;QAIF;AACA;;AAGI;;AACA;AACF;;;;AAKE;AACA;;;;AAKJ;AACF;AACF;;;AC5CA;;;AAIA;AACE;AACA;;AAGK;AACL;;AAEE;AACA;;AAEA;AACA;AACE;AACD;AACD;AACD;AACD;;AAEE;AACA;AACA;AACA;AACD;AACD;;AAEE;AACA;AACA;AACA;AACD;AACD;;AAEE;AACA;AACA;AACD;AACD;;AAEE;AACA;AACA;AACD;AACD;;AAEE;AACA;AACA;AACD;AACD;;AAEE;AACA;AACA;AACD;AACD;;AAEE;AACA;AACA;AACD;AACD;;AAEE;AACA;AACA;AACD;AACD;;AAEE;AACA;AACA;AACD;AACD;;AAEE;AACA;AACA;AACD;AACD;;AAEE;AACA;AACA;AACD;AACD;;AAEE;AACA;AACA;AACD;AACD;;AAEE;AACA;;AAEA;AACA;AACD;AACD;;AAEE;AACA;AACA;AACA;AACD;AACD;;AAEE;AACA;AACA;AACA;AACD;AACD;;AAEE;AACA;AACA;AACA;AACD;AACD;;AAEE;AACA;AACA;AACA;AACD;AACD;;AAEE;AACA;AACA;AACA;AACD;;;;ACtHG;;;;AAOA;;AAEgC;AAClC;;AAGF;AACF;AAEA;AAEA;AACE;AACA;AACA;AACE;AACE;;;;AAKN;AACE;AACA;AACA;;AAIE;AACE;;QAKF;AACE;;;;;;AAkBF;AACA;;;AAGC;;AAEL;;;;;ICnFW;;AAEU;;;;AAEA;;;AAPR;;;;AATF;AACT;AACA;AACA;AACA;AACE;AACD;;AAEF;;;;;;;;;;ICuBoB;;;;;AAIA;;;;;AAIA;;;AAZR;;;;;;;;;;;;;;;;;;;;;;;;AA3BF;AACT;AACA;;AAEA;;;;;;;;;;;;;;;;;;;;AAoBC;;AAEF","names":[],"ignoreList":[],"sources":["../../../../packages/dynamic-forms-bootstrap/src/lib/fields/button/bs-button.component.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/fields/checkbox/bs-checkbox.component.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/directives/input-constraints.directive.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/fields/datepicker/bs-datepicker.component.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/addons/preset-actions.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/fields/input/bs-input.component.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/fields/multi-checkbox/bs-multi-checkbox.component.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/fields/radio/bs-radio-group.component.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/fields/radio/bs-radio.component.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/fields/select/bs-select.component.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/fields/slider/bs-slider.component.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/fields/textarea/bs-textarea.component.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/fields/toggle/bs-toggle.component.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/types/types.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/fields/button/bs-button.mapper.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/config/bootstrap-field-config.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/providers/bootstrap-providers.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/addons/bs-icon-addon.component.ts","../../../../packages/dynamic-forms-bootstrap/src/lib/addons/bs-button-addon.component.ts"],"sourcesContent":["import { AsyncPipe } from '@angular/common';\nimport { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';\nimport { FormEvent } from '@ng-forge/dynamic-forms';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\nimport { injectNgForgeAction, NgForgeActionHost } from '@ng-forge/dynamic-forms/integration';\nimport { BsButtonProps } from './bs-button.type';\n\n@Component({\n selector: 'df-bs-button',\n imports: [DynamicTextPipe, AsyncPipe],\n styleUrl: '../../styles/_form-field.scss',\n hostDirectives: [NgForgeActionHost],\n template: `\n @let buttonId = action.key() + '-button';\n <button\n [id]=\"buttonId\"\n [type]=\"buttonType()\"\n [disabled]=\"action.disabled()\"\n [class]=\"buttonClasses()\"\n [attr.tabindex]=\"action.tabIndex()\"\n [attr.data-testid]=\"buttonTestId()\"\n (click)=\"onClick()\"\n >\n {{ action.label() | dynamicText | async }}\n </button>\n `,\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport default class BsButtonFieldComponent<TEvent extends FormEvent> {\n protected readonly action = injectNgForgeAction<TEvent>();\n\n readonly props = input<BsButtonProps>();\n\n /** Resolved button type — defaults to 'button' unless overridden via props. */\n readonly buttonType = computed(() => this.props()?.type ?? 'button');\n\n readonly buttonTestId = computed(() => `${this.buttonType()}-${this.action.key()}`);\n\n readonly buttonClasses = computed(() => {\n const p = this.props();\n const variant = p?.variant || 'primary';\n const outline = p?.outline ? 'outline-' : '';\n\n return [\n 'btn',\n `btn-${outline}${variant}`,\n p?.size === 'sm' && 'btn-sm',\n p?.size === 'lg' && 'btn-lg',\n p?.block && 'w-100',\n p?.active && 'active',\n this.action.className(),\n ]\n .filter(Boolean)\n .join(' ');\n });\n\n /** Submit buttons let the native form handle submission; other buttons dispatch via the directive. */\n onClick(): void {\n if (this.buttonType() === 'submit') return;\n this.action.dispatch();\n }\n}\n","import { ChangeDetectionStrategy, Component, input } from '@angular/core';\nimport { FormField } from '@angular/forms/signals';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\nimport { NgForgeControl, injectNgForgeField, NgForgeFieldHost } from '@ng-forge/dynamic-forms/integration';\nimport { BsCheckboxProps } from './bs-checkbox.type';\nimport { AsyncPipe } from '@angular/common';\n\n@Component({\n selector: 'df-bs-checkbox',\n imports: [FormField, DynamicTextPipe, AsyncPipe, NgForgeControl],\n styleUrl: '../../styles/_form-field.scss',\n hostDirectives: [NgForgeFieldHost],\n template: `\n @let f = ngf.field(); @let checkboxId = ngf.key() + '-checkbox';\n\n <div\n class=\"form-check\"\n [class.form-switch]=\"props()?.switch\"\n [class.form-check-inline]=\"props()?.inline\"\n [class.form-check-reverse]=\"props()?.reverse\"\n [attr.hidden]=\"f().hidden() || null\"\n >\n <input\n ngForgeControl\n type=\"checkbox\"\n [formField]=\"f\"\n [id]=\"checkboxId\"\n [indeterminate]=\"props()?.indeterminate ?? false\"\n class=\"form-check-input\"\n [class.is-invalid]=\"f().invalid() && f().touched()\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n />\n <label [for]=\"checkboxId\" class=\"form-check-label\">\n {{ ngf.label() | dynamicText | async }}\n </label>\n </div>\n\n @if (ngf.errorsToDisplay()[0]; as error) {\n <div class=\"invalid-feedback d-block\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</div>\n } @else if (props()?.hint; as hint) {\n <div class=\"form-text\" [id]=\"ngf.hintId()\" [attr.hidden]=\"f().hidden() || null\">{{ hint | dynamicText | async }}</div>\n }\n `,\n styles: [\n `\n :host {\n display: block;\n }\n\n :host([hidden]) {\n display: none !important;\n }\n `,\n ],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport default class BsCheckboxFieldComponent {\n protected readonly ngf = injectNgForgeField<boolean>();\n\n readonly props = input<BsCheckboxProps>();\n}\n","import { Directive, ElementRef, inject, input } from '@angular/core';\nimport { explicitEffect } from 'ngxtension/explicit-effect';\n\n/** Directive to set min, max, and step attributes on form inputs */\n@Directive({\n selector: '[dfBsInputConstraints]',\n})\nexport class InputConstraintsDirective {\n readonly dfMin = input<number | string | null | undefined>();\n readonly dfMax = input<number | string | null | undefined>();\n readonly dfStep = input<number | string | null | undefined>();\n\n private readonly el = inject<ElementRef<HTMLInputElement>>(ElementRef);\n\n private readonly minEffect = explicitEffect([this.dfMin], ([minValue]) => {\n const nativeElement = this.el.nativeElement;\n if (minValue !== null && minValue !== undefined) {\n nativeElement.setAttribute('min', String(minValue));\n } else {\n nativeElement.removeAttribute('min');\n }\n });\n\n private readonly maxEffect = explicitEffect([this.dfMax], ([maxValue]) => {\n const nativeElement = this.el.nativeElement;\n if (maxValue !== null && maxValue !== undefined) {\n nativeElement.setAttribute('max', String(maxValue));\n } else {\n nativeElement.removeAttribute('max');\n }\n });\n\n private readonly stepEffect = explicitEffect([this.dfStep], ([stepValue]) => {\n const nativeElement = this.el.nativeElement;\n if (stepValue !== null && stepValue !== undefined) {\n nativeElement.setAttribute('step', String(stepValue));\n } else {\n nativeElement.removeAttribute('step');\n }\n });\n}\n","import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';\nimport { FormField } from '@angular/forms/signals';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\nimport { NgForgeControl, injectNgForgeField, NgForgeFieldHost } from '@ng-forge/dynamic-forms/integration';\nimport { BsDatepickerProps } from './bs-datepicker.type';\nimport { AsyncPipe } from '@angular/common';\nimport { InputConstraintsDirective } from '../../directives/input-constraints.directive';\n\n@Component({\n selector: 'df-bs-datepicker',\n imports: [FormField, DynamicTextPipe, AsyncPipe, InputConstraintsDirective, NgForgeControl],\n styleUrl: '../../styles/_form-field.scss',\n hostDirectives: [NgForgeFieldHost],\n template: `\n @let f = ngf.field(); @let p = props(); @let inputId = ngf.key() + '-input';\n @if (p?.floatingLabel) {\n <!-- Floating label variant -->\n <div class=\"form-floating mb-3\">\n <input\n ngForgeControl\n dfBsInputConstraints\n [formField]=\"f\"\n [id]=\"inputId\"\n type=\"date\"\n [placeholder]=\"(ngf.placeholder() | dynamicText | async) ?? ''\"\n [dfMin]=\"minAsString()\"\n [dfMax]=\"maxAsString()\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n class=\"form-control\"\n [class.form-control-sm]=\"p?.size === 'sm'\"\n [class.form-control-lg]=\"p?.size === 'lg'\"\n [class.is-invalid]=\"f().invalid() && f().touched()\"\n [class.is-valid]=\"f().valid() && f().touched() && p?.validFeedback\"\n />\n @if (ngf.label()) {\n <label [for]=\"inputId\">{{ ngf.label() | dynamicText | async }}</label>\n }\n @if (p?.validFeedback && f().valid() && f().touched()) {\n <div class=\"valid-feedback d-block\">\n {{ p?.validFeedback | dynamicText | async }}\n </div>\n }\n @if (ngf.errorsToDisplay()[0]; as error) {\n <div class=\"invalid-feedback d-block\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</div>\n }\n </div>\n } @else {\n <!-- Standard variant -->\n <div class=\"mb-3\">\n @if (ngf.label()) {\n <label [for]=\"inputId\" class=\"form-label\">{{ ngf.label() | dynamicText | async }}</label>\n }\n\n <input\n ngForgeControl\n dfBsInputConstraints\n [formField]=\"f\"\n [id]=\"inputId\"\n type=\"date\"\n [placeholder]=\"(ngf.placeholder() | dynamicText | async) ?? ''\"\n [dfMin]=\"minAsString()\"\n [dfMax]=\"maxAsString()\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n class=\"form-control\"\n [class.form-control-sm]=\"p?.size === 'sm'\"\n [class.form-control-lg]=\"p?.size === 'lg'\"\n [class.is-invalid]=\"f().invalid() && f().touched()\"\n [class.is-valid]=\"f().valid() && f().touched() && p?.validFeedback\"\n />\n\n @if (p?.validFeedback && f().valid() && f().touched()) {\n <div class=\"valid-feedback d-block\">\n {{ p?.validFeedback | dynamicText | async }}\n </div>\n }\n @if (ngf.errorsToDisplay()[0]; as error) {\n <div class=\"invalid-feedback d-block\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</div>\n } @else if (p?.hint) {\n <div class=\"form-text\" [id]=\"ngf.hintId()\">{{ p?.hint | dynamicText | async }}</div>\n }\n </div>\n }\n `,\n changeDetection: ChangeDetectionStrategy.OnPush,\n styles: [\n `\n :host([hidden]) {\n display: none !important;\n }\n `,\n ],\n})\nexport default class BsDatepickerFieldComponent {\n protected readonly ngf = injectNgForgeField<string>();\n\n readonly minDate = input<Date | string | null>(null);\n readonly maxDate = input<Date | string | null>(null);\n readonly startAt = input<Date | null>(null);\n readonly props = input<BsDatepickerProps>();\n\n // Helper methods to convert Date to string for HTML attributes\n readonly minAsString = computed(() => {\n const min = this.minDate();\n return min instanceof Date ? min.toISOString().split('T')[0] : min;\n });\n\n readonly maxAsString = computed(() => {\n const max = this.maxDate();\n return max instanceof Date ? max.toISOString().split('T')[0] : max;\n });\n}\n","import type { AddonActionContext, AddonActionPreset } from '@ng-forge/dynamic-forms';\nimport type { PresetCollaborators } from '@ng-forge/dynamic-forms/integration';\nimport { runPresetAction } from '@ng-forge/dynamic-forms/integration';\n\nexport type { PresetCollaborators };\n\n/** Bootstrap adapter binding for the shared preset runner. */\nexport function runBsPresetAction(preset: AddonActionPreset, ctx: AddonActionContext, collaborators: PresetCollaborators): Promise<void> {\n return runPresetAction(preset, ctx, collaborators, 'Bootstrap', 'bs-input');\n}\n","import { AsyncPipe } from '@angular/common';\nimport { ChangeDetectionStrategy, Component, computed, forwardRef, inject, input, signal } from '@angular/core';\nimport { FormField } from '@angular/forms/signals';\nimport { AddonActionContext, AddonActionPreset, DynamicFormLogger } from '@ng-forge/dynamic-forms';\nimport { DfAddonSlot } from '@ng-forge/dynamic-forms/integration';\nimport { WrapperFieldInputs } from '@ng-forge/dynamic-forms/integration';\nimport { DynamicTextPipe, FIELD_SIGNAL_CONTEXT } from '@ng-forge/dynamic-forms/integration';\nimport {\n ADDON_PRESET_HANDLER,\n AddonPresetHandler,\n injectNgForgeAddons,\n injectNgForgeField,\n NgForgeAddons,\n NgForgeControl,\n NgForgeFieldHost,\n} from '@ng-forge/dynamic-forms/integration';\nimport { BOOTSTRAP_CONFIG } from '../../models/bootstrap-config.token';\nimport { runBsPresetAction } from '../../addons/preset-actions';\nimport { BS_INPUT_TYPE_OVERRIDE } from '../../tokens/input-type-override.token';\nimport { BsInputAddon, BsInputProps } from './bs-input.type';\n\n@Component({\n selector: 'df-bs-input',\n imports: [FormField, DynamicTextPipe, AsyncPipe, NgForgeControl, DfAddonSlot],\n styleUrl: '../../styles/_form-field.scss',\n hostDirectives: [NgForgeFieldHost, NgForgeAddons],\n template: `\n @let f = ngf.field(); @let p = props(); @let inputId = ngf.key() + '-input';\n @if (floatingLabel()) {\n <div class=\"mb-3\">\n @if (ngfa.hasAddons()) {\n <div class=\"input-group\">\n @for (a of ngfa.prefixAddons(); track $index) {\n <span class=\"input-group-text\">\n <df-addon-slot [addon]=\"a\" [fieldInputs]=\"fieldInputs()\" [hidden]=\"ngfa.hiddenSignalCache().get(a)\" />\n </span>\n }\n <div class=\"form-floating\">\n <input\n ngForgeControl\n [formField]=\"f\"\n [id]=\"inputId\"\n [type]=\"type()\"\n [placeholder]=\"(ngf.placeholder() | dynamicText | async) ?? ''\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n class=\"form-control\"\n [class.form-control-sm]=\"size() === 'sm'\"\n [class.form-control-lg]=\"size() === 'lg'\"\n [class.form-control-plaintext]=\"p?.plaintext\"\n [class.is-invalid]=\"f().invalid() && f().touched()\"\n [class.is-valid]=\"f().valid() && f().touched() && p?.validFeedback\"\n />\n @if (ngf.label()) {\n <label [for]=\"inputId\">{{ ngf.label() | dynamicText | async }}</label>\n }\n </div>\n @for (a of ngfa.suffixAddons(); track $index) {\n <span class=\"input-group-text\">\n <df-addon-slot [addon]=\"a\" [fieldInputs]=\"fieldInputs()\" [hidden]=\"ngfa.hiddenSignalCache().get(a)\" />\n </span>\n }\n </div>\n } @else {\n <div class=\"form-floating\">\n <input\n ngForgeControl\n [formField]=\"f\"\n [id]=\"inputId\"\n [type]=\"type()\"\n [placeholder]=\"(ngf.placeholder() | dynamicText | async) ?? ''\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n class=\"form-control\"\n [class.form-control-sm]=\"size() === 'sm'\"\n [class.form-control-lg]=\"size() === 'lg'\"\n [class.form-control-plaintext]=\"p?.plaintext\"\n [class.is-invalid]=\"f().invalid() && f().touched()\"\n [class.is-valid]=\"f().valid() && f().touched() && p?.validFeedback\"\n />\n @if (ngf.label()) {\n <label [for]=\"inputId\">{{ ngf.label() | dynamicText | async }}</label>\n }\n </div>\n }\n @if (p?.validFeedback && f().valid() && f().touched()) {\n <div class=\"valid-feedback d-block\">\n {{ p?.validFeedback | dynamicText | async }}\n </div>\n }\n @if (ngf.errorsToDisplay()[0]; as error) {\n <div class=\"invalid-feedback d-block\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</div>\n }\n </div>\n } @else {\n <div class=\"mb-3\">\n @if (ngf.label()) {\n <label [for]=\"inputId\" class=\"form-label\">{{ ngf.label() | dynamicText | async }}</label>\n }\n @if (ngfa.hasAddons()) {\n <div class=\"input-group\">\n @for (a of ngfa.prefixAddons(); track $index) {\n <span class=\"input-group-text\">\n <df-addon-slot [addon]=\"a\" [fieldInputs]=\"fieldInputs()\" [hidden]=\"ngfa.hiddenSignalCache().get(a)\" />\n </span>\n }\n <input\n ngForgeControl\n [formField]=\"f\"\n [id]=\"inputId\"\n [type]=\"type()\"\n [placeholder]=\"(ngf.placeholder() | dynamicText | async) ?? ''\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n class=\"form-control\"\n [class.form-control-sm]=\"size() === 'sm'\"\n [class.form-control-lg]=\"size() === 'lg'\"\n [class.form-control-plaintext]=\"p?.plaintext\"\n [class.is-invalid]=\"f().invalid() && f().touched()\"\n [class.is-valid]=\"f().valid() && f().touched() && p?.validFeedback\"\n />\n @for (a of ngfa.suffixAddons(); track $index) {\n <span class=\"input-group-text\">\n <df-addon-slot [addon]=\"a\" [fieldInputs]=\"fieldInputs()\" [hidden]=\"ngfa.hiddenSignalCache().get(a)\" />\n </span>\n }\n </div>\n } @else {\n <input\n ngForgeControl\n [formField]=\"f\"\n [id]=\"inputId\"\n [type]=\"type()\"\n [placeholder]=\"(ngf.placeholder() | dynamicText | async) ?? ''\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n class=\"form-control\"\n [class.form-control-sm]=\"size() === 'sm'\"\n [class.form-control-lg]=\"size() === 'lg'\"\n [class.form-control-plaintext]=\"p?.plaintext\"\n [class.is-invalid]=\"f().invalid() && f().touched()\"\n [class.is-valid]=\"f().valid() && f().touched() && p?.validFeedback\"\n />\n }\n @if (p?.validFeedback && f().valid() && f().touched()) {\n <div class=\"valid-feedback d-block\">\n {{ p?.validFeedback | dynamicText | async }}\n </div>\n }\n @if (ngf.errorsToDisplay()[0]; as error) {\n <div class=\"invalid-feedback d-block\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</div>\n } @else if (p?.hint) {\n <div class=\"form-text\" [id]=\"ngf.hintId()\">{{ p?.hint | dynamicText | async }}</div>\n }\n </div>\n }\n `,\n changeDetection: ChangeDetectionStrategy.OnPush,\n providers: [\n {\n provide: BS_INPUT_TYPE_OVERRIDE,\n useFactory: () => signal<string | undefined>(undefined),\n },\n {\n // Adapter-specific preset semantics for `bs-button` addons (clear /\n // reset / paste / copy / toggle-password-visibility). The directive\n // (`NgForgeAddonAction`) delegates here when an addon configures a\n // `preset`. Per-bs-input-instance so the `typeOverride` signal is\n // scoped to one field.\n provide: ADDON_PRESET_HANDLER,\n useFactory: (): AddonPresetHandler => {\n const typeOverride = inject(BS_INPUT_TYPE_OVERRIDE);\n const fsc = inject(FIELD_SIGNAL_CONTEXT, { optional: true });\n const logger = inject(DynamicFormLogger);\n // forwardRef for baselineType only — host.props()?.type gates toggle-password-visibility.\n const host = inject(forwardRef(() => BsInputFieldComponent));\n return {\n run: (preset: string, ctx: AddonActionContext) => {\n const fieldKey = ctx.field.key;\n return runBsPresetAction(preset as AddonActionPreset, ctx, {\n typeOverride,\n fieldValueSetter: ctx.setValue,\n fieldDefaultValueGetter:\n fsc && fieldKey ? () => (fsc.defaultValues() as Record<string, unknown> | undefined)?.[fieldKey] : undefined,\n baselineType: () => host.props()?.type,\n logger,\n });\n },\n };\n },\n },\n ],\n styles: [\n `\n :host([hidden]) {\n display: none !important;\n }\n /* Per-side padding on the .input-group-text addon containers — each\n side is its own knob so consumers can tune the prefix's gap from\n the border (outer-left), the prefix's gap to the input text\n (inner-right), and the symmetric suffix counterparts independently. */\n :host {\n --df-bs-addon-prefix-outer-padding: 0.75rem;\n --df-bs-addon-prefix-inner-padding: 0.75rem;\n --df-bs-addon-suffix-inner-padding: 0.75rem;\n --df-bs-addon-suffix-outer-padding: 0.75rem;\n }\n :host ::ng-deep .input-group > .input-group-text:first-child {\n padding-left: var(--df-bs-addon-prefix-outer-padding);\n padding-right: var(--df-bs-addon-prefix-inner-padding);\n }\n :host ::ng-deep .input-group > .input-group-text:last-child {\n padding-left: var(--df-bs-addon-suffix-inner-padding);\n padding-right: var(--df-bs-addon-suffix-outer-padding);\n }\n /* bs-button addons live inside .input-group-text for visual parity with\n bs-icon (shared grey container). Strip the inner .btn's own visual\n chrome (border, background, focus ring) so the button blends with\n the addon container — done via Bootstrap's own CSS variables so the\n cascade reaches the rendered <button> in any theme. No !important. */\n :host ::ng-deep .input-group-text df-bs-button-addon .btn {\n --bs-btn-bg: transparent;\n --bs-btn-color: var(--bs-body-color);\n --bs-btn-border-color: transparent;\n --bs-btn-hover-bg: rgba(127, 127, 127, 0.12);\n --bs-btn-hover-color: var(--bs-body-color);\n --bs-btn-hover-border-color: transparent;\n --bs-btn-active-bg: rgba(127, 127, 127, 0.18);\n --bs-btn-active-border-color: transparent;\n --bs-btn-focus-shadow-rgb: 0, 0, 0;\n --bs-btn-padding-x: 0;\n --bs-btn-padding-y: 0;\n box-shadow: none;\n }\n :host ::ng-deep .input-group-text df-bs-button-addon .btn:focus-visible {\n /* 2px solid for WCAG 2.4.11; currentColor adapts to light/dark themes. */\n box-shadow: none;\n outline: 2px solid currentColor;\n outline-offset: 2px;\n }\n `,\n ],\n})\nexport default class BsInputFieldComponent {\n private bootstrapConfig = inject(BOOTSTRAP_CONFIG, { optional: true });\n\n protected readonly ngf = injectNgForgeField<string>();\n protected readonly ngfa = injectNgForgeAddons<BsInputAddon>();\n\n readonly props = input<BsInputProps>();\n\n /**\n * Wrapper-style host bag pushed by `DfFieldOutlet`. Declared at the\n * component level so `setInputIfDeclared` (which uses\n * `reflectComponentType`) can write it.\n */\n readonly fieldInputs = input<WrapperFieldInputs | undefined>();\n\n /** Per-instance type override populated by `toggle-password-visibility` preset. */\n private readonly typeOverride = inject(BS_INPUT_TYPE_OVERRIDE);\n\n readonly size = computed(() => this.props()?.size ?? this.bootstrapConfig?.size);\n readonly floatingLabel = computed(() => this.props()?.floatingLabel ?? this.bootstrapConfig?.floatingLabel ?? false);\n\n /** Override (set by `toggle-password-visibility` preset) wins over `props().type`. */\n protected readonly type = computed(() => this.typeOverride() ?? this.props()?.type ?? 'text');\n}\n","import { ChangeDetectionStrategy, Component, computed, input, linkedSignal } from '@angular/core';\nimport { FieldOption, ValueType } from '@ng-forge/dynamic-forms';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\nimport { injectNgForgeField, NgForgeFieldHost, isEqual, NgForgeControl } from '@ng-forge/dynamic-forms/integration';\nimport { explicitEffect } from 'ngxtension/explicit-effect';\nimport { BsMultiCheckboxProps } from './bs-multi-checkbox.type';\nimport { AsyncPipe } from '@angular/common';\n\n@Component({\n selector: 'df-bs-multi-checkbox',\n imports: [DynamicTextPipe, AsyncPipe, NgForgeControl],\n styleUrl: '../../styles/_form-field.scss',\n hostDirectives: [NgForgeFieldHost],\n template: `\n @let f = ngf.field();\n @let checked = checkedValuesMap();\n @if (ngf.label(); as label) {\n <div class=\"form-label\">{{ label | dynamicText | async }}</div>\n }\n\n <div class=\"checkbox-group\">\n @for (option of options(); track option.value; let i = $index) {\n <div\n class=\"form-check\"\n [class.form-switch]=\"props()?.switch\"\n [class.form-check-inline]=\"props()?.inline\"\n [class.form-check-reverse]=\"props()?.reverse\"\n >\n <input\n ngForgeControl\n type=\"checkbox\"\n [id]=\"ngf.key() + '_' + i\"\n [checked]=\"checked['' + option.value]\"\n [disabled]=\"f().disabled() || option.disabled\"\n (change)=\"onCheckboxChange(option, $event)\"\n class=\"form-check-input\"\n [class.is-invalid]=\"f().invalid() && f().touched()\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n />\n <label [for]=\"ngf.key() + '_' + i\" class=\"form-check-label\">\n {{ option.label | dynamicText | async }}\n </label>\n </div>\n }\n </div>\n\n @if (ngf.errorsToDisplay()[0]; as error) {\n <div class=\"invalid-feedback d-block\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</div>\n } @else if (props()?.hint; as hint) {\n <div class=\"form-text\" [id]=\"ngf.hintId()\">{{ hint | dynamicText | async }}</div>\n }\n `,\n styles: [\n `\n :host {\n display: block;\n }\n\n .checkbox-group {\n margin-bottom: 0.5rem;\n }\n\n :host([hidden]) {\n display: none !important;\n }\n `,\n ],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport default class BsMultiCheckboxFieldComponent {\n protected readonly ngf = injectNgForgeField<ValueType[]>();\n\n readonly options = input<FieldOption<ValueType>[]>([]);\n readonly props = input<BsMultiCheckboxProps>();\n\n /** Computed map of checked option values for O(1) lookup in template */\n readonly checkedValuesMap = computed(() => {\n const map: Record<string, boolean> = {};\n for (const opt of this.valueViewModel()) {\n map[String(opt.value)] = true;\n }\n return map;\n });\n\n valueViewModel = linkedSignal<FieldOption<ValueType>[]>(\n () => {\n const currentValues = this.ngf.field()().value();\n return this.options().filter((option) => currentValues.includes(option.value));\n },\n { equal: isEqual },\n );\n\n constructor() {\n explicitEffect([this.valueViewModel], ([selectedOptions]: [FieldOption<ValueType>[]]) => {\n const selectedValues = selectedOptions.map((option: FieldOption<ValueType>) => option.value);\n\n if (!isEqual(selectedValues, this.ngf.field()().value())) {\n this.ngf.field()().value.set(selectedValues);\n }\n });\n\n explicitEffect([this.options], ([options]: [FieldOption<ValueType>[]]) => {\n const values = options.map((option: FieldOption<ValueType>) => option.value);\n const uniqueValues = new Set(values);\n\n if (values.length !== uniqueValues.size) {\n const duplicates = values.filter((value, index) => values.indexOf(value) !== index);\n throw new Error(`Duplicate option values detected in bs-multi-checkbox: ${duplicates.join(', ')}`);\n }\n });\n }\n\n onCheckboxChange(option: FieldOption<ValueType>, event: Event): void {\n const checked = (event.target as HTMLInputElement).checked;\n this.valueViewModel.update((currentOptions: FieldOption<ValueType>[]) => {\n if (checked) {\n return currentOptions.some((opt: FieldOption<ValueType>) => opt.value === option.value)\n ? currentOptions\n : [...currentOptions, option];\n } else {\n return currentOptions.filter((opt: FieldOption<ValueType>) => opt.value !== option.value);\n }\n });\n }\n}\n","import { ChangeDetectionStrategy, Component, input, model } from '@angular/core';\nimport type { FormValueControl } from '@angular/forms/signals';\nimport { DynamicText, FieldOption, ValueType } from '@ng-forge/dynamic-forms';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\nimport { NgForgeControl } from '@ng-forge/dynamic-forms/integration';\nimport { AsyncPipe } from '@angular/common';\n\nexport interface BsRadioGroupProps {\n /** Display radio buttons inline (horizontal) */\n inline?: boolean;\n /** Reverse the position of the radio button and label */\n reverse?: boolean;\n /** Display as button group instead of radio buttons */\n buttonGroup?: boolean;\n /** Button size when using button group */\n buttonSize?: 'sm' | 'lg';\n /** Hint text to display below the radio group */\n hint?: DynamicText;\n}\n\n/**\n * Bootstrap radio group implementing FormValueControl. Rendered inside\n * `df-bs-radio` — each `<input type=\"radio\">` carries `ngForgeControl`, so\n * the marker absorbs meta + aria from the ambient parent NgForgeField.\n */\n@Component({\n selector: 'df-bs-radio-group',\n imports: [DynamicTextPipe, AsyncPipe, NgForgeControl],\n template: `\n @let props = properties();\n @if (props?.buttonGroup) {\n <div class=\"btn-group\" role=\"group\" [attr.aria-label]=\"label() | dynamicText | async\">\n @for (option of options(); track option.value; let i = $index) {\n <input\n ngForgeControl\n type=\"radio\"\n [name]=\"name()\"\n [value]=\"option.value\"\n [checked]=\"value() === option.value\"\n (change)=\"onRadioChange(option.value)\"\n [disabled]=\"disabled() || option.disabled || false\"\n class=\"btn-check\"\n [id]=\"name() + '_' + i\"\n autocomplete=\"off\"\n />\n <label\n class=\"btn btn-outline-primary\"\n [class.btn-sm]=\"props?.buttonSize === 'sm'\"\n [class.btn-lg]=\"props?.buttonSize === 'lg'\"\n [for]=\"name() + '_' + i\"\n >\n {{ option.label | dynamicText | async }}\n </label>\n }\n </div>\n } @else {\n @for (option of options(); track option.value; let i = $index) {\n <div class=\"form-check\" [class.form-check-inline]=\"props?.inline\" [class.form-check-reverse]=\"props?.reverse\">\n <input\n ngForgeControl\n type=\"radio\"\n [name]=\"name()\"\n [value]=\"option.value\"\n [checked]=\"value() === option.value\"\n (change)=\"onRadioChange(option.value)\"\n [disabled]=\"disabled() || option.disabled || false\"\n class=\"form-check-input\"\n [id]=\"name() + '_' + i\"\n />\n <label class=\"form-check-label\" [for]=\"name() + '_' + i\">\n {{ option.label | dynamicText | async }}\n </label>\n </div>\n }\n }\n `,\n styles: [\n `\n :host {\n display: block;\n }\n `,\n ],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class BsRadioGroupComponent implements FormValueControl<ValueType | undefined> {\n // Value model - FormField directive binds form value to this\n readonly value = model<ValueType | undefined>(undefined);\n\n // Optional FormValueControl properties - Field directive will bind these\n readonly disabled = input<boolean>(false);\n readonly readonly = input<boolean>(false);\n readonly name = input<string>('');\n\n // Component-specific inputs\n readonly label = input<DynamicText>();\n readonly options = input.required<FieldOption<ValueType>[]>();\n readonly properties = input<BsRadioGroupProps>();\n\n /** Handle radio button change event */\n protected onRadioChange(newValue: ValueType): void {\n if (!this.disabled() && !this.readonly()) {\n this.value.set(newValue);\n }\n }\n}\n","import { ChangeDetectionStrategy, Component, input } from '@angular/core';\nimport { FormField } from '@angular/forms/signals';\nimport { FieldOption, ValueType } from '@ng-forge/dynamic-forms';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\nimport { injectNgForgeField, NgForgeFieldHost } from '@ng-forge/dynamic-forms/integration';\nimport { BsRadioProps } from './bs-radio.type';\nimport { AsyncPipe } from '@angular/common';\nimport { BsRadioGroupComponent } from './bs-radio-group.component';\n\n@Component({\n selector: 'df-bs-radio',\n imports: [BsRadioGroupComponent, FormField, DynamicTextPipe, AsyncPipe],\n styleUrl: '../../styles/_form-field.scss',\n hostDirectives: [NgForgeFieldHost],\n template: `\n @let f = ngf.field();\n\n <div class=\"mb-3\">\n @if (ngf.label(); as label) {\n <div class=\"form-label\">{{ label | dynamicText | async }}</div>\n }\n\n <df-bs-radio-group [formField]=\"f\" [label]=\"ngf.label()\" [options]=\"options()\" [properties]=\"props()\" />\n\n @if (ngf.errorsToDisplay()[0]; as error) {\n <div class=\"invalid-feedback d-block\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</div>\n } @else if (props()?.hint; as hint) {\n <div class=\"form-text\" [id]=\"ngf.hintId()\">{{ hint | dynamicText | async }}</div>\n }\n </div>\n `,\n styles: [\n `\n :host {\n display: block;\n }\n\n :host([hidden]) {\n display: none !important;\n }\n `,\n ],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport default class BsRadioFieldComponent {\n protected readonly ngf = injectNgForgeField<ValueType>();\n\n readonly options = input<FieldOption<ValueType>[]>([]);\n readonly props = input<BsRadioProps>();\n}\n","import { ChangeDetectionStrategy, Component, input } from '@angular/core';\nimport { FormField } from '@angular/forms/signals';\nimport { FieldOption } from '@ng-forge/dynamic-forms';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\nimport { NgForgeControl, injectNgForgeField, NgForgeFieldHost } from '@ng-forge/dynamic-forms/integration';\nimport { BsSelectProps } from './bs-select.type';\nimport { AsyncPipe } from '@angular/common';\n\n@Component({\n selector: 'df-bs-select',\n imports: [FormField, DynamicTextPipe, AsyncPipe, NgForgeControl],\n styleUrl: '../../styles/_form-field.scss',\n hostDirectives: [NgForgeFieldHost],\n template: `\n @let f = ngf.field(); @let selectId = ngf.key() + '-select';\n\n <div class=\"mb-3\">\n @if (ngf.label(); as label) {\n <label [for]=\"selectId\" class=\"form-label\">{{ label | dynamicText | async }}</label>\n }\n <select\n ngForgeControl\n [formField]=\"f\"\n [id]=\"selectId\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n class=\"form-select\"\n [class.form-select-sm]=\"props()?.size === 'sm'\"\n [class.form-select-lg]=\"props()?.size === 'lg'\"\n [class.is-invalid]=\"f().invalid() && f().touched()\"\n [multiple]=\"props()?.multiple || false\"\n [size]=\"props()?.htmlSize\"\n >\n @if (ngf.placeholder(); as placeholder) {\n <option value=\"\" disabled [selected]=\"!f().value()\">{{ placeholder | dynamicText | async }}</option>\n }\n @for (option of options(); track option.value) {\n <option [value]=\"option.value\" [disabled]=\"option.disabled || false\" [selected]=\"isSelected(option.value, f().value())\">\n {{ option.label | dynamicText | async }}\n </option>\n }\n </select>\n\n @if (ngf.errorsToDisplay()[0]; as error) {\n <div class=\"invalid-feedback d-block\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</div>\n } @else if (props()?.hint; as hint) {\n <div class=\"form-text\" [id]=\"ngf.hintId()\">{{ hint | dynamicText | async }}</div>\n }\n </div>\n `,\n changeDetection: ChangeDetectionStrategy.OnPush,\n styles: [\n `\n :host([hidden]) {\n display: none !important;\n }\n `,\n ],\n})\nexport default class BsSelectFieldComponent {\n protected readonly ngf = injectNgForgeField<string>();\n\n readonly options = input<FieldOption<string>[]>([]);\n readonly props = input<BsSelectProps>();\n\n defaultCompare = Object.is;\n\n protected isSelected(optionValue: string, fieldValue: string | string[] | null): boolean {\n const compareWith = this.props()?.compareWith || this.defaultCompare;\n\n if (Array.isArray(fieldValue)) {\n return fieldValue.some((v) => compareWith(v, optionValue));\n }\n\n return fieldValue !== null && compareWith(fieldValue, optionValue);\n }\n}\n","import { ChangeDetectionStrategy, Component, input } from '@angular/core';\nimport { FormField } from '@angular/forms/signals';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\nimport { NgForgeControl, injectNgForgeField, NgForgeFieldHost } from '@ng-forge/dynamic-forms/integration';\nimport { BsSliderProps } from './bs-slider.type';\nimport { AsyncPipe } from '@angular/common';\nimport { InputConstraintsDirective } from '../../directives/input-constraints.directive';\n\n@Component({\n selector: 'df-bs-slider',\n imports: [FormField, DynamicTextPipe, AsyncPipe, InputConstraintsDirective, NgForgeControl],\n styleUrl: '../../styles/_form-field.scss',\n hostDirectives: [NgForgeFieldHost],\n template: `\n @let f = ngf.field(); @let inputId = ngf.key() + '-input';\n\n <div class=\"mb-3\">\n @if (ngf.label(); as label) {\n <label [for]=\"inputId\" class=\"form-label\">\n {{ label | dynamicText | async }}\n @if (props()?.showValue) {\n <span class=\"ms-2 badge bg-secondary\"> {{ props()?.valuePrefix }}{{ f().value() }}{{ props()?.valueSuffix }} </span>\n }\n </label>\n }\n\n <input\n ngForgeControl\n type=\"range\"\n dfBsInputConstraints\n [formField]=\"f\"\n [id]=\"inputId\"\n [dfMin]=\"f().min?.() ?? props()?.min ?? min()\"\n [dfMax]=\"f().max?.() ?? props()?.max ?? max()\"\n [dfStep]=\"step() ?? props()?.step ?? 1\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n class=\"form-range\"\n />\n\n @if (ngf.errorsToDisplay()[0]; as error) {\n <div class=\"invalid-feedback d-block\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</div>\n } @else if (props()?.hint; as hint) {\n <div class=\"form-text\" [id]=\"ngf.hintId()\">{{ hint | dynamicText | async }}</div>\n }\n </div>\n `,\n styles: [\n `\n :host {\n display: block;\n }\n\n :host([hidden]) {\n display: none !important;\n }\n `,\n ],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport default class BsSliderFieldComponent {\n protected readonly ngf = injectNgForgeField<number>();\n\n readonly min = input<number>(0);\n readonly max = input<number>(100);\n readonly step = input<number>();\n\n readonly props = input<BsSliderProps>();\n}\n","import { ChangeDetectionStrategy, Component, input } from '@angular/core';\nimport { FormField } from '@angular/forms/signals';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\nimport { NgForgeControl, injectNgForgeField, NgForgeFieldHost } from '@ng-forge/dynamic-forms/integration';\nimport { BsTextareaProps } from './bs-textarea.type';\nimport { AsyncPipe } from '@angular/common';\n\n@Component({\n selector: 'df-bs-textarea',\n imports: [FormField, DynamicTextPipe, AsyncPipe, NgForgeControl],\n styleUrl: '../../styles/_form-field.scss',\n hostDirectives: [NgForgeFieldHost],\n template: `\n @let f = ngf.field(); @let p = props(); @let textareaId = ngf.key() + '-textarea';\n @if (p?.floatingLabel) {\n <!-- Floating label variant -->\n <div class=\"form-floating mb-3\">\n <textarea\n ngForgeControl\n [formField]=\"f\"\n [id]=\"textareaId\"\n [placeholder]=\"(ngf.placeholder() | dynamicText | async) ?? ''\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n class=\"form-control\"\n [class.form-control-sm]=\"p?.size === 'sm'\"\n [class.form-control-lg]=\"p?.size === 'lg'\"\n [class.is-invalid]=\"f().invalid() && f().touched()\"\n [class.is-valid]=\"f().valid() && f().touched() && p?.validFeedback\"\n ></textarea>\n\n @if (ngf.label()) {\n <label [for]=\"textareaId\">{{ ngf.label() | dynamicText | async }}</label>\n }\n @if (p?.validFeedback && f().valid() && f().touched()) {\n <div class=\"valid-feedback d-block\">\n {{ p?.validFeedback | dynamicText | async }}\n </div>\n }\n @if (ngf.errorsToDisplay()[0]; as error) {\n <div class=\"invalid-feedback d-block\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</div>\n } @else if (p?.hint) {\n <div class=\"form-text\" [id]=\"ngf.hintId()\">{{ p?.hint | dynamicText | async }}</div>\n }\n </div>\n } @else {\n <!-- Standard variant -->\n <div class=\"mb-3\">\n @if (ngf.label()) {\n <label [for]=\"textareaId\" class=\"form-label\">{{ ngf.label() | dynamicText | async }}</label>\n }\n\n <textarea\n ngForgeControl\n [formField]=\"f\"\n [id]=\"textareaId\"\n [placeholder]=\"(ngf.placeholder() | dynamicText | async) ?? ''\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n class=\"form-control\"\n [class.form-control-sm]=\"p?.size === 'sm'\"\n [class.form-control-lg]=\"p?.size === 'lg'\"\n [class.is-invalid]=\"f().invalid() && f().touched()\"\n [class.is-valid]=\"f().valid() && f().touched() && p?.validFeedback\"\n ></textarea>\n\n @if (p?.validFeedback && f().valid() && f().touched()) {\n <div class=\"valid-feedback d-block\">\n {{ p?.validFeedback | dynamicText | async }}\n </div>\n }\n @if (ngf.errorsToDisplay()[0]; as error) {\n <div class=\"invalid-feedback d-block\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</div>\n } @else if (p?.hint) {\n <div class=\"form-text\" [id]=\"ngf.hintId()\">{{ p?.hint | dynamicText | async }}</div>\n }\n </div>\n }\n `,\n changeDetection: ChangeDetectionStrategy.OnPush,\n styles: [\n `\n :host([hidden]) {\n display: none !important;\n }\n `,\n ],\n})\nexport default class BsTextareaFieldComponent {\n protected readonly ngf = injectNgForgeField<string>();\n\n readonly props = input<BsTextareaProps>();\n}\n","import { ChangeDetectionStrategy, Component, input } from '@angular/core';\nimport { FormField } from '@angular/forms/signals';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\nimport { NgForgeControl, injectNgForgeField, NgForgeFieldHost } from '@ng-forge/dynamic-forms/integration';\nimport { BsToggleProps } from './bs-toggle.type';\nimport { AsyncPipe } from '@angular/common';\n\n@Component({\n selector: 'df-bs-toggle',\n imports: [FormField, DynamicTextPipe, AsyncPipe, NgForgeControl],\n styleUrl: '../../styles/_form-field.scss',\n hostDirectives: [NgForgeFieldHost],\n template: `\n @let f = ngf.field(); @let inputId = ngf.key() + '-input';\n\n <div\n class=\"form-check form-switch\"\n [class.form-check-inline]=\"props()?.inline\"\n [class.form-check-reverse]=\"props()?.reverse\"\n [class.form-switch-sm]=\"props()?.size === 'sm'\"\n [class.form-switch-lg]=\"props()?.size === 'lg'\"\n [attr.hidden]=\"f().hidden() || null\"\n >\n <input\n ngForgeControl\n type=\"checkbox\"\n [formField]=\"f\"\n [id]=\"inputId\"\n class=\"form-check-input\"\n [class.is-invalid]=\"f().invalid() && f().touched()\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n />\n <label [for]=\"inputId\" class=\"form-check-label\">\n {{ ngf.label() | dynamicText | async }}\n </label>\n </div>\n\n @if (ngf.errorsToDisplay()[0]; as error) {\n <div class=\"invalid-feedback d-block\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</div>\n } @else if (props()?.hint; as hint) {\n <div class=\"form-text\" [id]=\"ngf.hintId()\" [attr.hidden]=\"f().hidden() || null\">{{ hint | dynamicText | async }}</div>\n }\n `,\n styles: [\n `\n :host {\n display: block;\n }\n\n :host([hidden]) {\n display: none !important;\n }\n\n /* Custom size variants for switches */\n .form-switch-sm .form-check-input {\n width: 1.75rem;\n height: 1rem;\n font-size: 0.875rem;\n }\n\n .form-switch-lg .form-check-input {\n width: 3rem;\n height: 1.75rem;\n font-size: 1.125rem;\n }\n `,\n ],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport default class BsToggleFieldComponent {\n protected readonly ngf = injectNgForgeField<boolean>();\n\n readonly props = input<BsToggleProps>();\n}\n","/**\n * Bootstrap field type constants\n * Based on available field components in the /fields folder\n */\nexport const BsField = {\n Input: 'input',\n Select: 'select',\n Checkbox: 'checkbox',\n Button: 'button',\n Submit: 'submit',\n Next: 'next',\n Previous: 'previous',\n AddArrayItem: 'addArrayItem',\n PrependArrayItem: 'prependArrayItem',\n InsertArrayItem: 'insertArrayItem',\n RemoveArrayItem: 'removeArrayItem',\n PopArrayItem: 'popArrayItem',\n ShiftArrayItem: 'shiftArrayItem',\n Textarea: 'textarea',\n Radio: 'radio',\n MultiCheckbox: 'multi-checkbox',\n Datepicker: 'datepicker',\n Slider: 'slider',\n Toggle: 'toggle',\n} as const;\n\nexport type BsFieldType = (typeof BsField)[keyof typeof BsField];\n","import { computed, inject, isSignal, Signal } from '@angular/core';\nimport { FieldDef } from '@ng-forge/dynamic-forms';\nimport { ARRAY_CONTEXT, buildBaseInputs, DEFAULT_PROPS } from '@ng-forge/dynamic-forms/integration';\n\n/**\n * Generic button mapper for custom events or basic buttons.\n * For specific button types (submit, next, prev, add/remove array items),\n * use the dedicated field types and their specific mappers.\n *\n * @param fieldDef The button field definition\n * @returns Signal containing Record of input names to values for ngComponentOutlet\n */\nexport function buttonFieldMapper(fieldDef: FieldDef<Record<string, unknown>>): Signal<Record<string, unknown>> {\n const defaultProps = inject(DEFAULT_PROPS);\n const arrayContext = inject(ARRAY_CONTEXT, { optional: true });\n\n return computed(() => {\n const baseInputs = buildBaseInputs(fieldDef, defaultProps());\n\n const inputs: Record<string, unknown> = {\n ...baseInputs,\n };\n\n if (fieldDef.disabled !== undefined) {\n inputs['disabled'] = fieldDef.disabled;\n }\n\n if (fieldDef.hidden !== undefined) {\n inputs['hidden'] = fieldDef.hidden;\n }\n\n // Add event binding for button events\n if ('event' in fieldDef && fieldDef.event !== undefined) {\n inputs['event'] = fieldDef.event;\n }\n\n // Add eventArgs binding if provided\n if ('eventArgs' in fieldDef && fieldDef.eventArgs !== undefined) {\n inputs['eventArgs'] = fieldDef.eventArgs;\n }\n\n // Add eventContext for token resolution (supports $template, $arrayKey, $index, etc.)\n const template = 'template' in fieldDef ? fieldDef.template : undefined;\n if (template || arrayContext) {\n // Read signal value if index is a signal (supports differential updates)\n const getIndex = () => {\n if (!arrayContext) return -1;\n return isSignal(arrayContext.index) ? arrayContext.index() : arrayContext.index;\n };\n\n inputs['eventContext'] = {\n key: fieldDef.key,\n index: getIndex(),\n arrayKey: arrayContext?.arrayKey ?? '',\n formValue: arrayContext?.formValue ?? {},\n template,\n };\n }\n\n return inputs;\n });\n}\n","import { FieldTypeDefinition } from '@ng-forge/dynamic-forms/integration';\nimport {\n addArrayItemButtonMapper,\n checkboxFieldMapper,\n datepickerFieldMapper,\n insertArrayItemButtonMapper,\n optionsFieldMapper,\n popArrayItemButtonMapper,\n prependArrayItemButtonMapper,\n removeArrayItemButtonMapper,\n shiftArrayItemButtonMapper,\n valueFieldMapper,\n} from '@ng-forge/dynamic-forms/integration';\nimport { BsField } from '../types/types';\nimport { buttonFieldMapper } from '../fields/button/bs-button.mapper';\nimport { nextButtonFieldMapper, previousButtonFieldMapper, submitButtonFieldMapper } from '../fields/button/bs-specific-button.mapper';\n\nconst VALUE_FIELD_TYPES_BASE = {\n renderReadyWhen: ['field'],\n} as const;\n\nconst BUTTON_FIELD_TYPES_BASE = {\n renderReadyWhen: [],\n valueHandling: 'exclude',\n} as const;\n\nexport const BOOTSTRAP_FIELD_TYPES: FieldTypeDefinition[] = [\n {\n name: BsField.Input,\n loadComponent: () => import('../fields/input/bs-input.component'),\n mapper: valueFieldMapper,\n propsToMeta: ['type'],\n scope: ['text-input', 'numeric'],\n addons: {\n slots: ['prefix', 'suffix'],\n },\n ...VALUE_FIELD_TYPES_BASE,\n },\n {\n name: BsField.Select,\n loadComponent: () => import('../fields/select/bs-select.component'),\n mapper: optionsFieldMapper,\n scope: 'single-select',\n ...VALUE_FIELD_TYPES_BASE,\n },\n {\n name: BsField.Checkbox,\n loadComponent: () => import('../fields/checkbox/bs-checkbox.component'),\n mapper: checkboxFieldMapper,\n scope: 'boolean',\n ...VALUE_FIELD_TYPES_BASE,\n },\n {\n name: BsField.Button,\n loadComponent: () => import('../fields/button/bs-button.component'),\n mapper: buttonFieldMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: BsField.Submit,\n loadComponent: () => import('../fields/button/bs-button.component'),\n mapper: submitButtonFieldMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: BsField.Next,\n loadComponent: () => import('../fields/button/bs-button.component'),\n mapper: nextButtonFieldMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: BsField.Previous,\n loadComponent: () => import('../fields/button/bs-button.component'),\n mapper: previousButtonFieldMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: BsField.AddArrayItem,\n loadComponent: () => import('../fields/button/bs-button.component'),\n mapper: addArrayItemButtonMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: BsField.PrependArrayItem,\n loadComponent: () => import('../fields/button/bs-button.component'),\n mapper: prependArrayItemButtonMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: BsField.InsertArrayItem,\n loadComponent: () => import('../fields/button/bs-button.component'),\n mapper: insertArrayItemButtonMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: BsField.RemoveArrayItem,\n loadComponent: () => import('../fields/button/bs-button.component'),\n mapper: removeArrayItemButtonMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: BsField.PopArrayItem,\n loadComponent: () => import('../fields/button/bs-button.component'),\n mapper: popArrayItemButtonMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: BsField.ShiftArrayItem,\n loadComponent: () => import('../fields/button/bs-button.component'),\n mapper: shiftArrayItemButtonMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: BsField.Textarea,\n loadComponent: () => import('../fields/textarea/bs-textarea.component'),\n mapper: valueFieldMapper,\n propsToMeta: ['rows'],\n scope: 'text-input',\n ...VALUE_FIELD_TYPES_BASE,\n },\n {\n name: BsField.Radio,\n loadComponent: () => import('../fields/radio/bs-radio.component'),\n mapper: optionsFieldMapper,\n scope: 'single-select',\n ...VALUE_FIELD_TYPES_BASE,\n },\n {\n name: BsField.MultiCheckbox,\n loadComponent: () => import('../fields/multi-checkbox/bs-multi-checkbox.component'),\n mapper: optionsFieldMapper,\n scope: 'multi-select',\n ...VALUE_FIELD_TYPES_BASE,\n },\n {\n name: BsField.Datepicker,\n loadComponent: () => import('../fields/datepicker/bs-datepicker.component'),\n mapper: datepickerFieldMapper,\n scope: 'date',\n ...VALUE_FIELD_TYPES_BASE,\n },\n {\n name: BsField.Slider,\n loadComponent: () => import('../fields/slider/bs-slider.component'),\n mapper: valueFieldMapper,\n scope: 'numeric',\n ...VALUE_FIELD_TYPES_BASE,\n },\n {\n name: BsField.Toggle,\n loadComponent: () => import('../fields/toggle/bs-toggle.component'),\n mapper: checkboxFieldMapper,\n scope: 'boolean',\n ...VALUE_FIELD_TYPES_BASE,\n },\n];\n","import type { Provider } from '@angular/core';\nimport { DynamicFormError, type AddonTypeDefinition } from '@ng-forge/dynamic-forms';\nimport { ADDON_TYPE_DEFINITIONS, type FieldTypeDefinition } from '@ng-forge/dynamic-forms/integration';\nimport { BOOTSTRAP_FIELD_TYPES } from '../config/bootstrap-field-config';\nimport { BootstrapConfig } from '../models/bootstrap-config';\nimport { BOOTSTRAP_CONFIG } from '../models/bootstrap-config.token';\nimport type { BsButtonAddon, BsIconAddon } from '../types/addons';\n\n/** Field type definitions for Bootstrap components. */\nexport type BootstrapFieldTypes = FieldTypeDefinition[];\n\ntype BootstrapConfigFeature = {\n ɵkind: 'bootstrap-config';\n ɵproviders: Provider[];\n};\n\n/**\n * Default `withBootstrapFields()` shape — field defs + the auto-included\n * addons feature so `bs-icon` / `bs-button` work out of the box.\n */\ntype BootstrapFieldsWithAddons = [...BootstrapFieldTypes, BootstrapAddonsFeature];\n\ntype BootstrapFieldsWithConfig = [...BootstrapFieldTypes, BootstrapAddonsFeature, BootstrapConfigFeature];\n\n/**\n * Provides Bootstrap field type definitions for the dynamic form system,\n * with Bootstrap-shipped addon types (`bs-icon`, `bs-button`) auto-included\n * so addons work out of the box.\n *\n * @param config - Optional global configuration for Bootstrap form fields\n * @returns Tuple of field type definitions, the addons feature, and\n * optionally a config feature.\n */\nexport function withBootstrapFields(): BootstrapFieldsWithAddons;\nexport function withBootstrapFields(config: BootstrapConfig): BootstrapFieldsWithConfig;\nexport function withBootstrapFields(config: BootstrapConfig | undefined): BootstrapFieldsWithAddons | BootstrapFieldsWithConfig;\nexport function withBootstrapFields(config?: BootstrapConfig): BootstrapFieldsWithAddons | BootstrapFieldsWithConfig {\n // Always include the addons feature — bs-icon / bs-button are part of\n // the canonical Bootstrap surface.\n const base: unknown[] = [...BOOTSTRAP_FIELD_TYPES, withBootstrapAddons()];\n\n if (config) {\n base.push({\n ɵkind: 'bootstrap-config',\n ɵproviders: [{ provide: BOOTSTRAP_CONFIG, useValue: config }],\n } satisfies BootstrapConfigFeature);\n return base as BootstrapFieldsWithConfig;\n }\n\n return base as BootstrapFieldsWithAddons;\n}\n\n/* -- Bootstrap addon types --------------------------------------------- */\n\nconst BS_ICON_KIND: AddonTypeDefinition<BsIconAddon> = {\n type: 'bs-icon',\n loadComponent: () => import('../addons/bs-icon-addon.component').then((m) => m.BsIconAddonComponent),\n validate: (addon, fieldKey) => {\n if (typeof addon.icon !== 'string' || addon.icon.length === 0) {\n throw new DynamicFormError(`Addon type 'bs-icon' requires a non-empty 'icon' string (field: '${fieldKey}').`);\n }\n },\n};\n\nconst BS_BUTTON_KIND: AddonTypeDefinition<BsButtonAddon> = {\n type: 'bs-button',\n loadComponent: () => import('../addons/bs-button-addon.component').then((m) => m.BsButtonAddonComponent),\n validate: (addon, fieldKey) => {\n // Exactly one of preset / actionRef / action — validator drops the addon\n // (with warning) if the rule is violated.\n const set = [addon.preset, addon.actionRef, addon.action].filter((v) => v !== undefined);\n if (set.length > 1) {\n throw new DynamicFormError(\n `Addon type 'bs-button' on field '${fieldKey}' has more than one of preset/actionRef/action — exactly one allowed.`,\n );\n }\n // Icon-only buttons require ariaLabel for screen readers.\n if (addon.icon && !addon.label && !addon.ariaLabel) {\n throw new DynamicFormError(`Addon type 'bs-button' on field '${fieldKey}' is icon-only — provide 'ariaLabel' for accessibility.`);\n }\n },\n};\n\n/**\n * Feature type discriminant for the Bootstrap addons feature. Matches core's\n * `'addons'` type so providers flow through the standard addon-type pipeline\n * in `provideDynamicForm`.\n */\ntype BootstrapAddonsFeature = {\n ɵkind: 'addons';\n ɵproviders: Provider[];\n};\n\n/** Register Bootstrap-shipped addon types (`bs-icon`, `bs-button`) standalone. */\nexport function withBootstrapAddons(): BootstrapAddonsFeature {\n return {\n ɵkind: 'addons',\n ɵproviders: [\n { provide: ADDON_TYPE_DEFINITIONS, useValue: BS_ICON_KIND, multi: true },\n { provide: ADDON_TYPE_DEFINITIONS, useValue: BS_BUTTON_KIND, multi: true },\n ],\n };\n}\n","import { AsyncPipe } from '@angular/common';\nimport { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';\nimport { WrapperFieldInputs } from '@ng-forge/dynamic-forms/integration';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\nimport type { BsIconAddon } from '../types/addons';\n\n/** Renderer for the `bs-icon` addon type. */\n@Component({\n selector: 'df-bs-icon-addon',\n imports: [AsyncPipe, DynamicTextPipe],\n template: `<i [class]=\"iconClass()\" [attr.aria-label]=\"(ariaLabel() | dynamicText | async) ?? null\"></i>`,\n host: {\n '[attr.aria-hidden]': 'hasAriaLabel() ? null : \"true\"',\n },\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class BsIconAddonComponent {\n readonly addon = input.required<BsIconAddon>();\n /** Accepted for contract uniformity — `NgComponentOutlet` setInput is strict; every type must declare it. */\n readonly fieldInputs = input<WrapperFieldInputs | undefined>();\n\n protected readonly iconClass = computed(() => `bi bi-${this.addon().icon}`);\n protected readonly ariaLabel = computed(() => this.addon().ariaLabel);\n protected readonly hasAriaLabel = computed(() => this.addon().ariaLabel !== undefined);\n}\n","import { AsyncPipe } from '@angular/common';\nimport { ChangeDetectionStrategy, Component, computed } from '@angular/core';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\nimport { injectNgForgeAddonAction, NgForgeAddonAction } from '@ng-forge/dynamic-forms/integration';\nimport type { BsButtonAddon } from '../types/addons';\n\n/** Renderer for the `bs-button` addon type. */\n@Component({\n selector: 'df-bs-button-addon',\n imports: [DynamicTextPipe, AsyncPipe],\n hostDirectives: [NgForgeAddonAction],\n template: `\n <button\n type=\"button\"\n [class]=\"buttonClass()\"\n [disabled]=\"action.disabled() || action.loading()\"\n [attr.aria-label]=\"(ariaLabel() | dynamicText | async) ?? null\"\n [attr.aria-busy]=\"action.loading() || null\"\n (click)=\"action.dispatch()\"\n >\n @if (action.loading()) {\n <!-- Visually-hidden role=status text gives a reliable AT announcement (VO/JAWS/NVDA). -->\n <span class=\"spinner-border spinner-border-sm\" aria-hidden=\"true\"></span>\n <span class=\"visually-hidden\" role=\"status\">Loading…</span>\n } @else if (iconClass(); as ic) {\n <i [class]=\"ic\" aria-hidden=\"true\"></i>\n }\n @if (label(); as l) {\n <span>{{ l | dynamicText | async }}</span>\n }\n </button>\n `,\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class BsButtonAddonComponent {\n protected readonly action = injectNgForgeAddonAction<BsButtonAddon>();\n\n /** Re-exposed for template binding — same signal stored on the directive. */\n protected readonly addon = this.action.addon;\n\n protected readonly label = computed(() => this.addon().label);\n protected readonly ariaLabel = computed(() => this.addon().ariaLabel);\n protected readonly iconClass = computed(() => {\n const icon = this.addon().icon;\n return icon ? `bi bi-${icon}` : '';\n });\n protected readonly buttonClass = computed(() => `btn btn-outline-${this.addon().severity ?? 'secondary'}`);\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ng-forge/dynamic-forms-bootstrap",
3
- "version": "1.0.0-rc.1",
3
+ "version": "1.0.0-rc.2",
4
4
  "description": "Bootstrap 5 integration for @ng-forge/dynamic-forms. Pre-built Bootstrap form components.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -41,7 +41,7 @@
41
41
  "@angular/common": "~22.0.0",
42
42
  "@angular/core": "~22.0.0",
43
43
  "@angular/forms": "~22.0.0",
44
- "@ng-forge/dynamic-forms": "~1.0.0-rc.1",
44
+ "@ng-forge/dynamic-forms": "~1.0.0-rc.2",
45
45
  "rxjs": ">=7.0.0"
46
46
  },
47
47
  "module": "fesm2022/ng-forge-dynamic-forms-bootstrap.mjs",
@@ -1,5 +1,5 @@
1
1
  import * as _ng_forge_dynamic_forms from '@ng-forge/dynamic-forms';
2
- import { FormEvent, NextPageEvent, PreviousPageEvent, AppendArrayItemEvent, ArrayAllowedChildren, PrependArrayItemEvent, InsertArrayItemEvent, RemoveAtIndexEvent, PopArrayItemEvent, ShiftArrayItemEvent, DynamicText, BaseAddon, DynamicValue, AddonActionPreset, RegisteredActionRef, TextAddon, TemplateAddon, ValueType, FieldOption, NarrowFields, RegisteredFieldTypes, InferFormValue, FormConfig } from '@ng-forge/dynamic-forms';
2
+ import { FormEvent, FormSubmitEvent, NextPageEvent, PreviousPageEvent, AppendArrayItemEvent, ArrayAllowedChildren, PrependArrayItemEvent, InsertArrayItemEvent, RemoveAtIndexEvent, PopArrayItemEvent, ShiftArrayItemEvent, DynamicText, BaseAddon, DynamicValue, AddonActionPreset, RegisteredActionRef, TextAddon, TemplateAddon, ValueType, FieldOption, NarrowFields, RegisteredFieldTypes, InferFormValue, FormConfig } from '@ng-forge/dynamic-forms';
3
3
  import * as _ng_forge_dynamic_forms_integration from '@ng-forge/dynamic-forms/integration';
4
4
  import { ButtonField, CheckboxField, DatepickerField, DatepickerProps, AddonActionHandler, InputField, InputProps, WrapperFieldInputs, MultiCheckboxField, RadioField, SelectField, SelectProps, SliderField, TextareaField, TextareaProps, ToggleField, FieldTypeDefinition } from '@ng-forge/dynamic-forms/integration';
5
5
  import * as _angular_core from '@angular/core';
@@ -16,7 +16,7 @@ interface BsButtonProps {
16
16
  type BsButtonField<TEvent extends FormEvent> = ButtonField<BsButtonProps, TEvent>;
17
17
  /** Specific button field types with preconfigured events */
18
18
  /** Submit button field - automatically disabled when form is invalid */
19
- type BsSubmitButtonField = Omit<BsButtonField<SubmitEvent>, 'event' | 'type' | 'eventArgs'> & {
19
+ type BsSubmitButtonField = Omit<BsButtonField<FormSubmitEvent>, 'event' | 'type' | 'eventArgs'> & {
20
20
  type: 'submit';
21
21
  };
22
22
  /** Next page button field - with preconfigured NextPageEvent */
@@ -163,7 +163,7 @@ declare class BsDatepickerFieldComponent {
163
163
 
164
164
  /** Decorative icon addon for Bootstrap fields. */
165
165
  interface BsIconAddon extends BaseAddon {
166
- readonly kind: 'bs-icon';
166
+ readonly type: 'bs-icon';
167
167
  /** Bootstrap Icons name without the `bi-` prefix (e.g., `'search'`, `'x'`). */
168
168
  readonly icon: string;
169
169
  /** Accessible label for icons that convey meaning. */
@@ -174,7 +174,7 @@ interface BsIconAddon extends BaseAddon {
174
174
  * participate in either XOR axis.
175
175
  */
176
176
  interface BsButtonBase extends BaseAddon {
177
- readonly kind: 'bs-button';
177
+ readonly type: 'bs-button';
178
178
  /** Bootstrap button variant (mapped to `btn-outline-{severity}`). */
179
179
  readonly severity?: 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'light' | 'dark';
180
180
  /** Reactive loading state — swaps the button content for a spinner. */
@@ -193,7 +193,7 @@ type BsButtonClickPreset = {
193
193
  };
194
194
  /**
195
195
  * Value type for the `actionRef` slot. When no actions have been registered
196
- * via `provideAddonActions(...)`, `RegisteredActionRef` resolves to `never` —
196
+ * via `withAddonActions(...)`, `RegisteredActionRef` resolves to `never` —
197
197
  * which would make this variant uninhabitable. Fall back to `string` so the
198
198
  * variant stays usable; once the user augments `DynamicFormActionRegistry`,
199
199
  * autocomplete tightens to the registered keys.
@@ -201,14 +201,14 @@ type BsButtonClickPreset = {
201
201
  type BsButtonActionRef = [RegisteredActionRef] extends [never] ? string : RegisteredActionRef;
202
202
  type BsButtonClickActionRef = {
203
203
  readonly preset?: never;
204
- /** Reference to a handler registered via `provideAddonActions(...)`. JSON-safe. */
204
+ /** Reference to a handler registered via `withAddonActions(...)`. JSON-safe. */
205
205
  readonly actionRef: BsButtonActionRef;
206
206
  readonly action?: never;
207
207
  };
208
208
  type BsButtonClickAction = {
209
209
  readonly preset?: never;
210
210
  readonly actionRef?: never;
211
- /** Inline handler — code-only; dropped from JSON-derived configs. Matches the generic `AddonActionHandler` shape used by `provideAddonActions(...)`. */
211
+ /** Inline handler — code-only; dropped from JSON-derived configs. Matches the generic `AddonActionHandler` shape used by `withAddonActions(...)`. */
212
212
  readonly action: AddonActionHandler;
213
213
  };
214
214
  type BsButtonClickNone = {
@@ -240,7 +240,7 @@ type BsButtonContentDecorative = {
240
240
  type BsButtonContent = BsButtonContentIconOnly | BsButtonContentLabeled | BsButtonContentDecorative;
241
241
  /** Interactive button addon for Bootstrap fields. */
242
242
  type BsButtonAddon = BsButtonBase & BsButtonContent & BsButtonClick;
243
- /** Union of all Bootstrap-shipped addon kinds. */
243
+ /** Union of all Bootstrap-shipped addon types. */
244
244
  type BsAddon = BsIconAddon | BsButtonAddon;
245
245
  declare module '@ng-forge/dynamic-forms' {
246
246
  interface DynamicFormAddonRegistry {
@@ -259,13 +259,13 @@ interface BsInputProps extends InputProps {
259
259
  type?: 'text' | 'email' | 'password' | 'number' | 'tel' | 'url';
260
260
  }
261
261
  /**
262
- * Module-augmentable seam for adding custom addon kinds to `bs-input` at
262
+ * Module-augmentable seam for adding custom addon types to `bs-input` at
263
263
  * the type level. Pair with `withCustomAddon(...)` for the runtime side:
264
264
  */
265
265
  interface BsAddonExtensions {
266
266
  }
267
267
  type BsAddonExtension = BsAddonExtensions[keyof BsAddonExtensions];
268
- /** Addon kinds accepted by `bs-input`. */
268
+ /** Addon types accepted by `bs-input`. */
269
269
  type BsInputAddon = BsIconAddon | BsButtonAddon | TextAddon | TemplateAddon | BsAddonExtension;
270
270
  type BsInputField = InputField<BsInputProps> & {
271
271
  addons?: ReadonlyArray<BsInputAddon>;
@@ -538,7 +538,7 @@ type BootstrapFieldsWithAddons = [...BootstrapFieldTypes, BootstrapAddonsFeature
538
538
  type BootstrapFieldsWithConfig = [...BootstrapFieldTypes, BootstrapAddonsFeature, BootstrapConfigFeature];
539
539
  /**
540
540
  * Provides Bootstrap field type definitions for the dynamic form system,
541
- * with Bootstrap-shipped addon kinds (`bs-icon`, `bs-button`) auto-included
541
+ * with Bootstrap-shipped addon types (`bs-icon`, `bs-button`) auto-included
542
542
  * so addons work out of the box.
543
543
  *
544
544
  * @param config - Optional global configuration for Bootstrap form fields
@@ -549,21 +549,21 @@ declare function withBootstrapFields(): BootstrapFieldsWithAddons;
549
549
  declare function withBootstrapFields(config: BootstrapConfig): BootstrapFieldsWithConfig;
550
550
  declare function withBootstrapFields(config: BootstrapConfig | undefined): BootstrapFieldsWithAddons | BootstrapFieldsWithConfig;
551
551
  /**
552
- * Feature kind discriminant for the Bootstrap addons feature. Matches core's
553
- * `'addons'` kind so providers flow through the standard addon-kind pipeline
552
+ * Feature type discriminant for the Bootstrap addons feature. Matches core's
553
+ * `'addons'` type so providers flow through the standard addon-type pipeline
554
554
  * in `provideDynamicForm`.
555
555
  */
556
556
  type BootstrapAddonsFeature = {
557
557
  ɵkind: 'addons';
558
558
  ɵproviders: Provider[];
559
559
  };
560
- /** Register Bootstrap-shipped addon kinds (`bs-icon`, `bs-button`) standalone. */
560
+ /** Register Bootstrap-shipped addon types (`bs-icon`, `bs-button`) standalone. */
561
561
  declare function withBootstrapAddons(): BootstrapAddonsFeature;
562
562
 
563
- /** Renderer for the `bs-icon` addon kind. */
563
+ /** Renderer for the `bs-icon` addon type. */
564
564
  declare class BsIconAddonComponent {
565
565
  readonly addon: _angular_core.InputSignal<BsIconAddon>;
566
- /** Accepted for contract uniformity — `NgComponentOutlet` setInput is strict; every kind must declare it. */
566
+ /** Accepted for contract uniformity — `NgComponentOutlet` setInput is strict; every type must declare it. */
567
567
  readonly fieldInputs: _angular_core.InputSignal<WrapperFieldInputs | undefined>;
568
568
  protected readonly iconClass: _angular_core.Signal<string>;
569
569
  protected readonly ariaLabel: _angular_core.Signal<_ng_forge_dynamic_forms.DynamicText | undefined>;
@@ -572,7 +572,7 @@ declare class BsIconAddonComponent {
572
572
  static ɵcmp: _angular_core.ɵɵComponentDeclaration<BsIconAddonComponent, "df-bs-icon-addon", never, { "addon": { "alias": "addon"; "required": true; "isSignal": true; }; "fieldInputs": { "alias": "fieldInputs"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
573
573
  }
574
574
 
575
- /** Renderer for the `bs-button` addon kind. */
575
+ /** Renderer for the `bs-button` addon type. */
576
576
  declare class BsButtonAddonComponent {
577
577
  protected readonly action: _ng_forge_dynamic_forms_integration.TypedNgForgeAddonAction<BsButtonAddon>;
578
578
  /** Re-exposed for template binding — same signal stored on the directive. */