@ng-forge/dynamic-forms-ionic 0.10.0-next.2 → 0.10.0-next.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ng-forge-dynamic-forms-ionic.mjs","mappings":";;;;;;;;;;;AAwCc;;;AAKH;;;AAKP;;AACA;;;AAXiB;;;;;;;;;;;;;;;;;;;AAHlB;;;;AA7BS;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACyCE;;;;AAAO;;;;;;;;;;;;;;;;;;;;;;;;;;AAxCT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACgGE;;AAGH;AACA;AACA;;AAGA;;AAGP;;;AAIA;;AAGF;AACE;gBAGA;AACE;AACA;;;AAEA;;;;AAKJ;AACE;AAAW;AACX;;AAGF;AACE;AAAW;;;AAKT;AACE;AACF;AACA;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;;;;AAxDa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AArFT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACTV;AACF;;;AC6CmB;;AAGE;AACA;AACA;AACA;AACA;AACA;AACA;QAIF;AACE;AAEnB;AACE;AACE;AACE;;;AAGF;AACF;;;AAxBS;;;;;;;;;;AA3BV;;;;AAbS;;;;;;;;;;;wBA+BR;AACA;AACA;AACA;AACA;AACA;;;;;;;;AC+GU;AACK;;;;;AAQjB;IAQiB;;AAGE;AACnB;AAImB;;AAKnB;;;;;AAaA;AACE;wBAIA;;;;AAGI;AACA;;AAEI;;;AAEE;;;AAEA;;AAEJ;;AAEJ;AACF;;;AA/DiB;AA7EjB;AACE;AACA;AACD;AACD;gFAME;;AAEE;AACA;AACA;oBAEA;;AAEE;AACE;AACA;;;;;;AAOC;;;;AAIR;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAnCF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAIC;AACE;AACA;AACD;AACD;4IAME;;AAEE;AACA;AACA;;;AAIE;AACE;AACA;;;;;;AAOC;;;;AAIR;AACF;;;;;;;;ACrCW;;AAGH;;AAGT;AAEI;;AAEF;IAKO;;;;;AAKP;AACF;AAEA;AACE;AACE;AAEA;AACE;;AAEJ;AAEA;AACE;AACA;;;AAIE;;AAEJ;;;;;AAMI;;;AAEA;;AAEJ;;;AAlDiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAxET;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC6DE;;AAGH;;AAGT;;AANmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAtBlB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACQW;;AAGH;;AAGT;;AANmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAXlB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACOW;;;;;;AAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA3CT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACyCE;AACK;;;AAMjB;wBAIE;;AAEI;AACA;;AAEI;;;AAEE;;;AAEA;;AAEJ;;AAEJ;AACF;;;AAzBiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAxCT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gBCgCD;IAGA;IAGA;IAGA;IAGA;IAGA;YAMA;AACA;AACA;AACA;AACA;;IAOU;AACjB;AACF;AAEA,IAImB;;AAEnB;gBAOA;;;;AAME;;;AA7DS;;;;;;;;;;;;;;;;;;;;AAzBD;;;;;;;;;;;;;;;;;AAqBR;;;;ACiBU;;;;AAAO;;;;;;;;;;;;;;;;;;;;;;;;;AAvCT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACNL;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;ACTI;AACJ;AACA;;;AAKE;AACE;;AAGF;AACE;;AAGF;AACE;;;AAKA;;;AAKA;;QAIF;AACA;;AAGI;;AACA;AACF;;;;AAKE;AACA;;;;AAKJ;AACF;AACF;;;AC9CA;;;AAIA;AACE;AACA;;AAOK;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;;;;ACtFG;;;;AAOA;;AAE4B;AAC9B;;AAGF;AACF;AAEA;AAEA;AACE;AACA;AACA;AACE;AACE;;;;AAKN;AACE;AACA;AACA;;AAIE;AACE;;QAKF;AACE;;;;;;AAsCF;AACA;;;AAGC;;AAEL;;;;IC/FqB;AAEA;AACA;AACA;AACnB,IAImB;AACA;AACnB,IAImB;;AAnBR;;;;;;;;;;;;;;;;;;;;;;;;;AA1CD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACMD;;AAIU;AACA;AACA;;AAPR;;;;AATF;AACT;AACA;AACA;AACA;AACE;AACD;;AAEF","names":[],"ignoreList":[],"sources":["../../../../packages/dynamic-forms-ionic/src/lib/fields/button/ionic-button.component.ts","../../../../packages/dynamic-forms-ionic/src/lib/fields/checkbox/ionic-checkbox.component.ts","../../../../packages/dynamic-forms-ionic/src/lib/fields/datepicker/ionic-datepicker.component.ts","../../../../packages/dynamic-forms-ionic/src/lib/addons/preset-actions.ts","../../../../packages/dynamic-forms-ionic/src/lib/addons/ion-inline-button-addon.component.ts","../../../../packages/dynamic-forms-ionic/src/lib/fields/input/ionic-input.component.ts","../../../../packages/dynamic-forms-ionic/src/lib/fields/multi-checkbox/ionic-multi-checkbox.component.ts","../../../../packages/dynamic-forms-ionic/src/lib/fields/radio/ionic-radio.component.ts","../../../../packages/dynamic-forms-ionic/src/lib/fields/select/ionic-select.component.ts","../../../../packages/dynamic-forms-ionic/src/lib/fields/slider/ionic-slider.component.ts","../../../../packages/dynamic-forms-ionic/src/lib/fields/textarea/ionic-textarea.component.ts","../../../../packages/dynamic-forms-ionic/src/lib/fields/toggle/ionic-toggle-control.component.ts","../../../../packages/dynamic-forms-ionic/src/lib/fields/toggle/ionic-toggle.component.ts","../../../../packages/dynamic-forms-ionic/src/lib/types/types.ts","../../../../packages/dynamic-forms-ionic/src/lib/fields/button/ionic-button.mapper.ts","../../../../packages/dynamic-forms-ionic/src/lib/config/ionic-field-config.ts","../../../../packages/dynamic-forms-ionic/src/lib/providers/ionic-providers.ts","../../../../packages/dynamic-forms-ionic/src/lib/addons/ion-button-addon.component.ts","../../../../packages/dynamic-forms-ionic/src/lib/addons/ion-icon-addon.component.ts"],"sourcesContent":["import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';\nimport { IonButton } from '@ionic/angular/standalone';\nimport { AsyncPipe } from '@angular/common';\nimport { DynamicTextPipe, FormEvent } from '@ng-forge/dynamic-forms';\nimport { injectNgForgeAction, NgForgeActionHost } from '@ng-forge/dynamic-forms/integration';\nimport { IonicButtonProps } from './ionic-button.type';\n\n@Component({\n selector: 'df-ion-button',\n imports: [IonButton, DynamicTextPipe, AsyncPipe],\n hostDirectives: [NgForgeActionHost],\n styles: [\n `\n :host([hidden]) {\n display: none !important;\n }\n `,\n ],\n template: `\n @let buttonId = action.key() + '-button';\n\n <ion-button\n [id]=\"buttonId\"\n [type]=\"buttonType()\"\n [expand]=\"props()?.expand\"\n [fill]=\"props()?.fill || 'solid'\"\n [shape]=\"props()?.shape\"\n [size]=\"props()?.size\"\n [color]=\"props()?.color || 'primary'\"\n [strong]=\"props()?.strong\"\n [disabled]=\"action.disabled()\"\n [attr.tabindex]=\"action.tabIndex()\"\n [attr.data-testid]=\"buttonTestId()\"\n (click)=\"onClick()\"\n >\n {{ action.label() | dynamicText | async }}\n </ion-button>\n `,\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport default class IonicButtonFieldComponent<TEvent extends FormEvent> {\n protected readonly action = injectNgForgeAction<TEvent>();\n\n readonly props = input<IonicButtonProps>();\n\n readonly buttonType = computed(() => this.props()?.type ?? 'button');\n\n readonly buttonTestId = computed(() => `${this.buttonType()}-${this.action.key()}`);\n\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 { IonCheckbox, IonNote } from '@ionic/angular/standalone';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms';\nimport { NgForgeControl, injectNgForgeField, NgForgeFieldHost } from '@ng-forge/dynamic-forms/integration';\nimport { IonicCheckboxProps } from './ionic-checkbox.type';\nimport { AsyncPipe } from '@angular/common';\n\n@Component({\n selector: 'df-ion-checkbox',\n imports: [IonCheckbox, IonNote, FormField, DynamicTextPipe, AsyncPipe, NgForgeControl],\n hostDirectives: [NgForgeFieldHost],\n template: `\n @let f = ngf.field();\n @let checkboxId = ngf.key() + '-checkbox';\n\n <ion-checkbox\n ngForgeControl\n [id]=\"checkboxId\"\n [formField]=\"f\"\n [labelPlacement]=\"props()?.labelPlacement ?? 'end'\"\n [justify]=\"props()?.justify\"\n [color]=\"props()?.color ?? 'primary'\"\n [indeterminate]=\"props()?.indeterminate ?? false\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n >\n {{ ngf.label() | dynamicText | async }}\n </ion-checkbox>\n\n @if (ngf.errorsToDisplay()[0]; as error) {\n <ion-note color=\"danger\" class=\"df-ion-error\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</ion-note>\n } @else if (props()?.hint; as hint) {\n <ion-note class=\"df-ion-hint\" [id]=\"ngf.hintId()\">{{ hint | dynamicText | async }}</ion-note>\n }\n `,\n styleUrl: '../../styles/_form-field.scss',\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 IonicCheckboxFieldComponent {\n protected readonly ngf = injectNgForgeField<boolean>();\n\n readonly props = input<IonicCheckboxProps>();\n}\n","import { ChangeDetectionStrategy, Component, input, signal } from '@angular/core';\nimport {\n IonButton,\n IonButtons,\n IonContent,\n IonDatetime,\n IonHeader,\n IonInput,\n IonModal,\n IonNote,\n IonTitle,\n IonToolbar,\n} from '@ionic/angular/standalone';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms';\nimport { NgForgeControl, injectNgForgeField, NgForgeFieldHost } from '@ng-forge/dynamic-forms/integration';\nimport { IonicDatepickerProps } from './ionic-datepicker.type';\nimport { AsyncPipe } from '@angular/common';\nimport { format } from 'date-fns';\n\n@Component({\n selector: 'df-ion-datepicker',\n imports: [\n IonInput,\n IonModal,\n IonDatetime,\n IonNote,\n IonButtons,\n IonHeader,\n IonContent,\n IonToolbar,\n IonTitle,\n IonButton,\n DynamicTextPipe,\n AsyncPipe,\n NgForgeControl,\n ],\n hostDirectives: [NgForgeFieldHost],\n template: `\n @let f = ngf.field();\n @let dateValue = f().value();\n @let inputId = ngf.key() + '-input';\n\n <ion-input\n ngForgeControl\n [id]=\"inputId\"\n [label]=\"(ngf.label() | dynamicText | async) ?? undefined\"\n [labelPlacement]=\"'stacked'\"\n [placeholder]=\"(ngf.placeholder() | dynamicText | async) ?? ''\"\n [disabled]=\"f().disabled()\"\n [value]=\"formatDisplayDate(dateValue)\"\n [readonly]=\"true\"\n [fill]=\"'outline'\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n (click)=\"!f().disabled() && openModal()\"\n />\n @if (ngf.errorsToDisplay()[0]; as error) {\n <ion-note color=\"danger\" class=\"df-ion-error\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</ion-note>\n } @else if (props()?.hint; as hint) {\n <ion-note class=\"df-ion-hint\" [id]=\"ngf.hintId()\">{{ hint | dynamicText | async }}</ion-note>\n }\n\n <ion-modal [isOpen]=\"isModalOpen()\" (didDismiss)=\"closeModal()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-title>{{ ngf.label() | dynamicText | async }}</ion-title>\n <ion-buttons slot=\"end\">\n <ion-button (click)=\"closeModal()\">Close</ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content>\n <ion-datetime\n [presentation]=\"props()?.presentation ?? 'date'\"\n [value]=\"dateToIsoString(dateValue)\"\n [multiple]=\"props()?.multiple ?? false\"\n [preferWheel]=\"props()?.preferWheel ?? false\"\n [showDefaultButtons]=\"props()?.showDefaultButtons ?? false\"\n [showDefaultTitle]=\"props()?.showDefaultTitle ?? false\"\n [showDefaultTimeLabel]=\"props()?.showDefaultTimeLabel ?? true\"\n [showClearButton]=\"props()?.showClearButton ?? false\"\n [color]=\"props()?.color\"\n [min]=\"dateToIsoString(minDate())\"\n [max]=\"dateToIsoString(maxDate())\"\n (ionChange)=\"onDateChange($event)\"\n >\n </ion-datetime>\n </ion-content>\n </ng-template>\n </ion-modal>\n `,\n styleUrl: '../../styles/_form-field.scss',\n styles: [\n `\n ion-input {\n cursor: pointer;\n }\n\n :host([hidden]) {\n display: none !important;\n }\n `,\n ],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport default class IonicDatepickerFieldComponent {\n protected readonly ngf = injectNgForgeField<Date | null>();\n\n readonly minDate = input<Date | null>(null);\n readonly maxDate = input<Date | null>(null);\n readonly startAt = input<Date | null>(null);\n readonly props = input<IonicDatepickerProps>();\n\n readonly isModalOpen = signal(false);\n\n openModal() {\n this.isModalOpen.set(true);\n }\n\n closeModal() {\n this.isModalOpen.set(false);\n }\n\n onDateChange(event: CustomEvent) {\n const value = event.detail.value;\n // ion-datetime emits ISO 8601 strings internally; convert to Date | null\n // to match the field tree's value type.\n if (value && typeof value === 'string' && value.length > 0) {\n const date = new Date(value);\n this.ngf.field()().value.set(date);\n } else {\n this.ngf.field()().value.set(null);\n }\n this.closeModal();\n }\n\n dateToIsoString(date: Date | null | undefined): string | undefined {\n if (!date) return undefined;\n return date.toISOString();\n }\n\n formatDisplayDate(date: Date | null | undefined): string {\n if (!date) return '';\n\n const presentation = this.props()?.presentation ?? 'date';\n\n switch (presentation) {\n case 'date':\n return format(date, 'MMM d, yyyy');\n case 'date-time':\n case 'time-date':\n return format(date, 'MMM d, yyyy, h:mm a');\n case 'time':\n return format(date, 'h:mm a');\n case 'month':\n return format(date, 'MMMM yyyy');\n case 'month-year':\n return format(date, 'MMM yyyy');\n case 'year':\n return format(date, 'yyyy');\n default:\n return format(date, 'MMM d, yyyy');\n }\n }\n}\n","import type { AddonActionContext, AddonActionPreset, PresetCollaborators } from '@ng-forge/dynamic-forms';\nimport { runPresetAction } from '@ng-forge/dynamic-forms';\n\nexport type { PresetCollaborators };\n\n/** Ionic adapter binding for the shared preset runner. */\nexport function runIonicPresetAction(\n preset: AddonActionPreset,\n ctx: AddonActionContext,\n collaborators: PresetCollaborators,\n): Promise<void> {\n return runPresetAction(preset, ctx, collaborators, 'Ionic', 'ion-input');\n}\n","import { AsyncPipe } from '@angular/common';\nimport { ChangeDetectionStrategy, Component, computed, inject, Injector, Signal, signal } from '@angular/core';\nimport { explicitEffect } from 'ngxtension/explicit-effect';\nimport { IonIcon, IonSpinner } from '@ionic/angular/standalone';\nimport { DynamicText, DynamicTextPipe, resolveDynamicValue } from '@ng-forge/dynamic-forms';\nimport { injectNgForgeAddonAction, NgForgeAddonAction } from '@ng-forge/dynamic-forms/integration';\nimport type { IonButtonAddon } from '../types/addons';\n\n/**\n * Inline `ion-button` addon — the host element IS `<ion-button>` so Ionic's\n * `::slotted(ion-button[slot=start|end])` shadow CSS matches and applies its\n * native icon-only sizing. `IonButtonAddonComponent` (tag selector) stays\n * for the universal `<df-addon-slot>` dispatcher when the addon is rendered\n * outside an `<ion-input>` host.\n */\n@Component({\n selector: 'ion-button[df-ion-button-addon]',\n imports: [IonIcon, IonSpinner, DynamicTextPipe, AsyncPipe],\n hostDirectives: [NgForgeAddonAction],\n template: `\n @if (action.loading()) {\n <ion-spinner [slot]=\"iconOnly() ? 'icon-only' : 'start'\" name=\"dots\"></ion-spinner>\n <span class=\"df-ion-sr-only\" role=\"status\">Loading…</span>\n } @else if (icon(); as ic) {\n <ion-icon [name]=\"ic\" [slot]=\"iconOnly() ? 'icon-only' : 'start'\"></ion-icon>\n }\n @if (label(); as lbl) {\n {{ lbl | dynamicText | async }}\n }\n `,\n styles: [\n `\n .df-ion-sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n }\n `,\n ],\n host: {\n // Stencil reflects [attr.*] to the underlying ion-button properties.\n '[attr.color]': 'color() ?? null',\n '[attr.fill]': 'fill()',\n '[attr.disabled]': 'action.disabled() || action.loading() ? \"true\" : null',\n '[attr.aria-label]': 'resolvedAriaLabel() ?? null',\n '[attr.aria-busy]': 'action.loading() || null',\n '(click)': 'action.dispatch()',\n },\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class IonInlineButtonAddonComponent {\n private readonly injector = inject(Injector);\n protected readonly action = injectNgForgeAddonAction<IonButtonAddon>();\n\n protected readonly addon = this.action.addon;\n protected readonly label = computed(() => this.addon().label);\n protected readonly ariaLabel = computed(() => this.addon().ariaLabel);\n protected readonly icon = computed(() => this.addon().icon);\n protected readonly color = computed(() => this.addon().color);\n protected readonly fill = computed(() => this.addon().fill ?? 'clear');\n protected readonly iconOnly = computed(() => !!this.icon() && !this.label());\n\n // Host bindings can't use pipes (NG5001), so `DynamicText` is materialised\n // via an effect (resolveDynamicValue's toSignal can't run inside computed).\n private readonly _ariaLabelSignal = signal<Signal<string | undefined> | undefined>(undefined);\n protected readonly resolvedAriaLabel = computed(() => this._ariaLabelSignal()?.() ?? undefined);\n\n constructor() {\n explicitEffect([this.ariaLabel], ([raw]) => {\n if (raw === undefined) {\n this._ariaLabelSignal.set(undefined);\n return;\n }\n this._ariaLabelSignal.set(resolveDynamicValue<string | undefined>(raw as DynamicText, undefined, this.injector));\n });\n }\n}\n","import { AsyncPipe } from '@angular/common';\nimport { ChangeDetectionStrategy, Component, computed, DestroyRef, ElementRef, forwardRef, inject, input, signal } from '@angular/core';\nimport { FormField } from '@angular/forms/signals';\nimport { IonInput, IonNote } from '@ionic/angular/standalone';\nimport {\n AddonActionContext,\n AddonActionPreset,\n DfAddonSlot,\n DynamicFormLogger,\n DynamicTextPipe,\n FIELD_SIGNAL_CONTEXT,\n WrapperFieldInputs,\n} from '@ng-forge/dynamic-forms';\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 { explicitEffect } from 'ngxtension/explicit-effect';\nimport { runIonicPresetAction } from '../../addons/preset-actions';\nimport { IonInlineButtonAddonComponent } from '../../addons/ion-inline-button-addon.component';\nimport { IONIC_CONFIG } from '../../models/ionic-config.token';\nimport { IONIC_INPUT_TYPE_OVERRIDE } from '../../tokens/input-type-override.token';\nimport { IonInputAddon, IonicInputProps } from './ionic-input.type';\n\n// minlength/maxlength bindings: Signal Forms auto-syncs these on NATIVE\n// inputs; <ion-input> is a Stencil web component so we bind directly from\n// FieldState (`f().maxLength?.()` — the signal itself is missing when no\n// validator is configured).\n@Component({\n selector: 'df-ion-input',\n // IonButton intentionally NOT imported — it would collide with\n // IonInlineButtonAddonComponent on `<ion-button>` (NG0300).\n imports: [IonInput, IonNote, FormField, DynamicTextPipe, AsyncPipe, NgForgeControl, DfAddonSlot, IonInlineButtonAddonComponent],\n hostDirectives: [NgForgeFieldHost, NgForgeAddons],\n template: `\n @let f = ngf.field();\n @let inputId = ngf.key() + '-input';\n\n <ion-input\n ngForgeControl\n [id]=\"inputId\"\n [type]=\"type()\"\n [formField]=\"f\"\n [label]=\"(ngf.label() | dynamicText | async) ?? undefined\"\n [labelPlacement]=\"labelPlacement()\"\n [placeholder]=\"(ngf.placeholder() | dynamicText | async) ?? ''\"\n [clearInput]=\"props()?.clearInput ?? false\"\n [counter]=\"props()?.counter ?? false\"\n [minlength]=\"f().minLength?.()\"\n [maxlength]=\"f().maxLength?.()\"\n [color]=\"color()\"\n [fill]=\"fill()\"\n [shape]=\"shape()\"\n [readonly]=\"f().readonly()\"\n [helperText]=\"ngf.errorsToDisplay().length === 0 ? ((props()?.hint | dynamicText | async) ?? undefined) : undefined\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n >\n @for (a of decorativePrefixAddons(); track $index) {\n <span slot=\"start\">\n <df-addon-slot [addon]=\"a\" [fieldInputs]=\"fieldInputs()\" [hidden]=\"ngfa.hiddenSignalCache().get(a)\" />\n </span>\n }\n @for (a of buttonPrefixAddons(); track $index) {\n <ion-button df-ion-button-addon slot=\"start\" [addon]=\"a\" [fieldInputs]=\"fieldInputs()\"></ion-button>\n }\n @for (a of buttonSuffixAddons(); track $index) {\n <ion-button df-ion-button-addon slot=\"end\" [addon]=\"a\" [fieldInputs]=\"fieldInputs()\"></ion-button>\n }\n @for (a of decorativeSuffixAddons(); track $index) {\n <span slot=\"end\">\n <df-addon-slot [addon]=\"a\" [fieldInputs]=\"fieldInputs()\" [hidden]=\"ngfa.hiddenSignalCache().get(a)\" />\n </span>\n }\n </ion-input>\n @if (ngf.errorsToDisplay()[0]; as error) {\n <ion-note color=\"danger\" class=\"df-ion-error\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</ion-note>\n }\n `,\n changeDetection: ChangeDetectionStrategy.OnPush,\n styleUrl: '../../styles/_form-field.scss',\n providers: [\n {\n provide: IONIC_INPUT_TYPE_OVERRIDE,\n useFactory: () => signal<string | undefined>(undefined),\n },\n {\n // Adapter-specific preset semantics for `ion-button` addons (clear /\n // reset / paste / copy / toggle-password-visibility). The directive\n // (`NgForgeAddonAction`) delegates here when an addon configures a\n // `preset`. Per-ion-input-instance so the `typeOverride` signal is\n // scoped to one field.\n provide: ADDON_PRESET_HANDLER,\n useFactory: (): AddonPresetHandler => {\n const typeOverride = inject(IONIC_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(() => IonicInputFieldComponent));\n return {\n run: (preset: string, ctx: AddonActionContext) => {\n const fieldKey = ctx.field.key;\n return runIonicPresetAction(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 knobs on each slot wrapper (independent prefix/suffix). */\n :host {\n --df-ion-addon-prefix-outer-padding: 0;\n --df-ion-addon-prefix-inner-padding: 0.5rem;\n --df-ion-addon-suffix-inner-padding: 0.5rem;\n --df-ion-addon-suffix-outer-padding: 0;\n }\n /* inline-flex on slot wrappers prevents baseline-pinning of decorative addon glyphs. */\n :host ::ng-deep ion-input [slot='start'],\n :host ::ng-deep ion-input [slot='end'] {\n display: inline-flex;\n align-items: center;\n align-self: center;\n line-height: 1;\n }\n :host ::ng-deep ion-input [slot='start'] df-addon-slot,\n :host ::ng-deep ion-input [slot='end'] df-addon-slot,\n :host ::ng-deep ion-input [slot='start'] df-ion-icon-addon,\n :host ::ng-deep ion-input [slot='end'] df-ion-icon-addon {\n display: inline-flex;\n align-items: center;\n line-height: 1;\n }\n :host ::ng-deep ion-input [slot='start'] {\n padding-inline-start: var(--df-ion-addon-prefix-outer-padding);\n padding-inline-end: var(--df-ion-addon-prefix-inner-padding);\n }\n :host ::ng-deep ion-input [slot='end'] {\n padding-inline-start: var(--df-ion-addon-suffix-inner-padding);\n padding-inline-end: var(--df-ion-addon-suffix-outer-padding);\n }\n /* Inherit input text color so icons stay readable on dark themes\n (Ionic's default \"medium\" greys out and fails WCAG on dark inputs). */\n :host ::ng-deep ion-input ion-icon {\n color: inherit;\n }\n `,\n ],\n})\nexport default class IonicInputFieldComponent {\n private readonly elementRef = inject(ElementRef<HTMLElement>);\n private ionicConfig = inject(IONIC_CONFIG, { optional: true });\n\n protected readonly ngf = injectNgForgeField<string>();\n protected readonly ngfa = injectNgForgeAddons<IonInputAddon>();\n\n readonly props = input<IonicInputProps>();\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(IONIC_INPUT_TYPE_OVERRIDE);\n\n protected readonly fill = computed(() => this.props()?.fill ?? this.ionicConfig?.fill ?? 'solid');\n protected readonly shape = computed(() => this.props()?.shape ?? this.ionicConfig?.shape);\n /** Default 'stacked' (label above the input) for adapter parity with Material /\n * Bootstrap / PrimeNG. Consumers can opt into Ionic's native inline 'start'\n * or 'fixed' via props.labelPlacement or the IONIC_CONFIG token. */\n protected readonly labelPlacement = computed(() => this.props()?.labelPlacement ?? this.ionicConfig?.labelPlacement ?? 'stacked');\n protected readonly color = computed(() => this.props()?.color ?? this.ionicConfig?.color);\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 /** Per-kind split: ion-button addons render inline via the attribute-\n * selector inline component (so the slotted element is a native\n * <ion-button> Ionic can style); everything else (icon/text/template/\n * component kinds) goes through the universal <df-addon-slot> dispatcher\n * wrapped in <span slot=\"start|end\"> for safe shadow-DOM projection. */\n protected readonly buttonPrefixAddons = computed(() => this.ngfa.prefixAddons().filter((a) => a.kind === 'ion-button'));\n protected readonly buttonSuffixAddons = computed(() => this.ngfa.suffixAddons().filter((a) => a.kind === 'ion-button'));\n protected readonly decorativePrefixAddons = computed(() => this.ngfa.prefixAddons().filter((a) => a.kind !== 'ion-button'));\n protected readonly decorativeSuffixAddons = computed(() => this.ngfa.suffixAddons().filter((a) => a.kind !== 'ion-button'));\n\n /** Set when the component is destroyed — guards async aria-describedby sync against teardown. */\n private destroyed = false;\n\n constructor() {\n inject(DestroyRef).onDestroy(() => (this.destroyed = true));\n // ion-input encapsulates a native <input> in shadow DOM and does not automatically\n // propagate aria-describedby to it. This effect imperatively syncs the attribute\n // after a microtask to ensure Ionic has resolved the internal element.\n explicitEffect([this.ngf.ariaDescribedBy], ([describedBy]) => {\n queueMicrotask(() => {\n if (this.destroyed) return;\n const ionInput = this.elementRef.nativeElement.querySelector('ion-input') as HTMLIonInputElement | null;\n if (ionInput?.getInputElement) {\n ionInput.getInputElement().then((inputEl) => {\n if (this.destroyed || !inputEl) return;\n if (describedBy) {\n inputEl.setAttribute('aria-describedby', describedBy);\n } else {\n inputEl.removeAttribute('aria-describedby');\n }\n });\n }\n });\n });\n }\n}\n","import { ChangeDetectionStrategy, Component, computed, input, linkedSignal } from '@angular/core';\nimport { IonCheckbox, IonItem, IonNote } from '@ionic/angular/standalone';\nimport { DynamicTextPipe, FieldOption, ValueType } from '@ng-forge/dynamic-forms';\nimport { injectNgForgeField, NgForgeFieldHost, isEqual, NgForgeControl } from '@ng-forge/dynamic-forms/integration';\nimport { explicitEffect } from 'ngxtension/explicit-effect';\nimport { IonicMultiCheckboxProps } from './ionic-multi-checkbox.type';\nimport { AsyncPipe } from '@angular/common';\n\n@Component({\n selector: 'df-ion-multi-checkbox',\n imports: [IonCheckbox, IonItem, IonNote, DynamicTextPipe, AsyncPipe, NgForgeControl],\n hostDirectives: [NgForgeFieldHost],\n template: `\n @let f = ngf.field();\n @let checked = checkedValuesMap();\n @if (ngf.label(); as label) {\n <div class=\"checkbox-group-label\">{{ label | dynamicText | async }}</div>\n }\n\n <div\n class=\"checkbox-group\"\n role=\"group\"\n [attr.aria-invalid]=\"ngf.ariaInvalid()\"\n [attr.aria-required]=\"ngf.ariaRequired()\"\n [attr.aria-describedby]=\"ngf.ariaDescribedBy()\"\n [class.ion-invalid]=\"ngf.showErrors()\"\n [class.ion-touched]=\"f().touched()\"\n >\n @for (option of options(); track option.value) {\n <ion-item lines=\"none\">\n <ion-checkbox\n ngForgeControl\n [checked]=\"checked['' + option.value]\"\n [disabled]=\"f().disabled() || option.disabled\"\n [labelPlacement]=\"props()?.labelPlacement ?? 'end'\"\n [justify]=\"props()?.justify ?? 'start'\"\n [color]=\"props()?.color ?? 'primary'\"\n (ionChange)=\"onCheckboxChange(option, $event.detail.checked)\"\n >\n {{ option.label | dynamicText | async }}\n </ion-checkbox>\n </ion-item>\n }\n </div>\n\n @if (ngf.errorsToDisplay()[0]; as error) {\n <ion-note color=\"danger\" class=\"df-ion-error\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</ion-note>\n } @else if (props()?.hint; as hint) {\n <ion-note class=\"df-ion-hint\" [id]=\"ngf.hintId()\">{{ hint | dynamicText | async }}</ion-note>\n }\n `,\n styleUrl: '../../styles/_form-field.scss',\n styles: [\n `\n :host {\n display: block;\n }\n\n :host([hidden]) {\n display: none !important;\n }\n\n .checkbox-group-label {\n margin-bottom: 8px;\n font-weight: 500;\n }\n\n .checkbox-group {\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n\n ion-item {\n --padding-start: 0;\n --inner-padding-end: 0;\n }\n `,\n ],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport default class IonicMultiCheckboxFieldComponent {\n protected readonly ngf = injectNgForgeField<ValueType[]>();\n\n readonly options = input<FieldOption<ValueType>[]>([]);\n readonly props = input<IonicMultiCheckboxProps>();\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 /** 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 constructor() {\n explicitEffect([this.valueViewModel], ([selectedOptions]) => {\n const selectedValues = selectedOptions.map((option) => 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]) => {\n const values = options.map((option) => 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 ionic-multi-checkbox: ${duplicates.join(', ')}`);\n }\n });\n }\n\n onCheckboxChange(option: FieldOption<ValueType>, checked: boolean): void {\n this.valueViewModel.update((currentOptions) => {\n if (checked) {\n return currentOptions.some((opt) => opt.value === option.value) ? currentOptions : [...currentOptions, option];\n } else {\n return currentOptions.filter((opt) => opt.value !== option.value);\n }\n });\n }\n}\n","import { ChangeDetectionStrategy, Component, input } from '@angular/core';\nimport { FormField } from '@angular/forms/signals';\nimport { IonItem, IonNote, IonRadio, IonRadioGroup } from '@ionic/angular/standalone';\nimport { DynamicTextPipe, FieldOption, ValueType } from '@ng-forge/dynamic-forms';\nimport { NgForgeControl, injectNgForgeField, NgForgeFieldHost } from '@ng-forge/dynamic-forms/integration';\nimport { IonicRadioProps } from './ionic-radio.type';\nimport { AsyncPipe } from '@angular/common';\n\n@Component({\n selector: 'df-ion-radio',\n imports: [IonRadioGroup, IonRadio, IonItem, IonNote, FormField, DynamicTextPipe, AsyncPipe, NgForgeControl],\n hostDirectives: [NgForgeFieldHost],\n template: `\n @let f = ngf.field();\n @let radioGroupId = ngf.key() + '-radio-group';\n @if (ngf.label(); as label) {\n <div class=\"radio-label\">{{ label | dynamicText | async }}</div>\n }\n\n <ion-radio-group\n [id]=\"radioGroupId\"\n [formField]=\"f\"\n [compareWith]=\"props()?.compareWith || defaultCompare\"\n [attr.aria-invalid]=\"ngf.ariaInvalid()\"\n [attr.aria-required]=\"ngf.ariaRequired()\"\n [attr.aria-describedby]=\"ngf.ariaDescribedBy()\"\n >\n @for (option of options(); track option.value) {\n <ion-item [lines]=\"'none'\">\n <ion-radio\n ngForgeControl\n [value]=\"option.value\"\n [disabled]=\"option.disabled || false\"\n [labelPlacement]=\"props()?.labelPlacement ?? 'end'\"\n [justify]=\"props()?.justify\"\n [color]=\"props()?.color ?? 'primary'\"\n >\n {{ option.label | dynamicText | async }}\n </ion-radio>\n </ion-item>\n }\n </ion-radio-group>\n\n @if (ngf.errorsToDisplay()[0]; as error) {\n <ion-note color=\"danger\" class=\"df-ion-error\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</ion-note>\n } @else if (props()?.hint; as hint) {\n <ion-note class=\"df-ion-hint\" [id]=\"ngf.hintId()\">{{ hint | dynamicText | async }}</ion-note>\n }\n `,\n styleUrl: '../../styles/_form-field.scss',\n styles: [\n `\n :host {\n display: block;\n }\n\n :host([hidden]) {\n display: none !important;\n }\n\n .radio-label {\n font-size: 0.875rem;\n font-weight: 500;\n margin-bottom: 0.5rem;\n color: var(--ion-text-color);\n }\n `,\n ],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport default class IonicRadioFieldComponent {\n protected readonly ngf = injectNgForgeField<ValueType>();\n\n readonly options = input<FieldOption<ValueType>[]>([]);\n readonly props = input<IonicRadioProps>();\n\n defaultCompare = Object.is;\n}\n","import { ChangeDetectionStrategy, Component, input } from '@angular/core';\nimport { FormField } from '@angular/forms/signals';\nimport { IonNote, IonSelect, IonSelectOption } from '@ionic/angular/standalone';\nimport { DynamicTextPipe, FieldOption, ValueType } from '@ng-forge/dynamic-forms';\nimport { NgForgeControl, injectNgForgeField, NgForgeFieldHost } from '@ng-forge/dynamic-forms/integration';\nimport { IonicSelectProps } from './ionic-select.type';\nimport { AsyncPipe } from '@angular/common';\n\n@Component({\n selector: 'df-ion-select',\n imports: [IonSelect, IonSelectOption, IonNote, FormField, DynamicTextPipe, AsyncPipe, NgForgeControl],\n hostDirectives: [NgForgeFieldHost],\n template: `\n @let f = ngf.field();\n @let selectId = ngf.key() + '-select';\n\n <ion-select\n ngForgeControl\n [id]=\"selectId\"\n [formField]=\"f\"\n [label]=\"(ngf.label() | dynamicText | async) ?? undefined\"\n [labelPlacement]=\"props()?.labelPlacement ?? 'stacked'\"\n [placeholder]=\"(ngf.placeholder() | dynamicText | async) ?? ''\"\n [multiple]=\"props()?.multiple ?? false\"\n [compareWith]=\"props()?.compareWith ?? defaultCompare\"\n [interface]=\"props()?.interface ?? 'alert'\"\n [interfaceOptions]=\"props()?.interfaceOptions ?? {}\"\n [cancelText]=\"props()?.cancelText ?? 'Cancel'\"\n [okText]=\"props()?.okText ?? 'OK'\"\n [color]=\"props()?.color\"\n [fill]=\"props()?.fill ?? 'outline'\"\n [shape]=\"props()?.shape\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n >\n @for (option of options(); track option.value) {\n <ion-select-option [value]=\"option.value\" [disabled]=\"option.disabled || false\">\n {{ option.label | dynamicText | async }}\n </ion-select-option>\n }\n </ion-select>\n @if (ngf.errorsToDisplay()[0]; as error) {\n <ion-note color=\"danger\" class=\"df-ion-error\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</ion-note>\n } @else if (props()?.hint; as hint) {\n <ion-note class=\"df-ion-hint\" [id]=\"ngf.hintId()\">{{ hint | dynamicText | async }}</ion-note>\n }\n `,\n changeDetection: ChangeDetectionStrategy.OnPush,\n styleUrl: '../../styles/_form-field.scss',\n styles: [\n `\n :host([hidden]) {\n display: none !important;\n }\n `,\n ],\n})\nexport default class IonicSelectFieldComponent {\n protected readonly ngf = injectNgForgeField<ValueType>();\n\n readonly options = input<FieldOption<ValueType>[]>([]);\n readonly props = input<IonicSelectProps>();\n\n defaultCompare = Object.is;\n}\n","import { ChangeDetectionStrategy, Component, input } from '@angular/core';\nimport { FormField } from '@angular/forms/signals';\nimport { IonNote, IonRange } from '@ionic/angular/standalone';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms';\nimport { NgForgeHostControl, injectNgForgeField, NgForgeFieldHost } from '@ng-forge/dynamic-forms/integration';\nimport { IonicSliderProps } from './ionic-slider.type';\nimport { AsyncPipe } from '@angular/common';\n\n@Component({\n selector: 'df-ion-slider',\n imports: [IonRange, IonNote, FormField, DynamicTextPipe, AsyncPipe],\n hostDirectives: [NgForgeFieldHost, NgForgeHostControl],\n template: `\n @let f = ngf.field();\n @let inputId = ngf.key() + '-input';\n\n <ion-range\n [id]=\"inputId\"\n [formField]=\"f\"\n [label]=\"(ngf.label() | dynamicText | async) ?? undefined\"\n [labelPlacement]=\"props()?.labelPlacement ?? 'stacked'\"\n [min]=\"f().min?.() ?? props()?.min ?? 0\"\n [max]=\"f().max?.() ?? props()?.max ?? 100\"\n [step]=\"step() ?? props()?.step ?? 1\"\n [dualKnobs]=\"props()?.dualKnobs ?? false\"\n [pin]=\"props()?.pin ?? false\"\n [pinFormatter]=\"props()?.pinFormatter ?? defaultPinFormatter\"\n [ticks]=\"props()?.ticks ?? false\"\n [snaps]=\"props()?.snaps ?? false\"\n [color]=\"props()?.color ?? 'primary'\"\n />\n\n @if (ngf.errorsToDisplay()[0]; as error) {\n <ion-note color=\"danger\" class=\"df-ion-error\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</ion-note>\n } @else if (props()?.hint; as hint) {\n <ion-note class=\"df-ion-hint\" [id]=\"ngf.hintId()\">{{ hint | dynamicText | async }}</ion-note>\n }\n `,\n styleUrl: '../../styles/_form-field.scss',\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 IonicSliderFieldComponent {\n protected readonly ngf = injectNgForgeField<number>();\n\n readonly step = input<number>();\n readonly props = input<IonicSliderProps>();\n\n protected defaultPinFormatter = (value: number) => String(value);\n}\n","import { ChangeDetectionStrategy, Component, ElementRef, inject, input } from '@angular/core';\nimport { explicitEffect } from 'ngxtension/explicit-effect';\nimport { FormField } from '@angular/forms/signals';\nimport { IonNote, IonTextarea } from '@ionic/angular/standalone';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms';\nimport { NgForgeControl, injectNgForgeField, NgForgeFieldHost } from '@ng-forge/dynamic-forms/integration';\nimport { IonicTextareaProps } from './ionic-textarea.type';\nimport { AsyncPipe } from '@angular/common';\n\n@Component({\n selector: 'df-ion-textarea',\n imports: [IonTextarea, IonNote, FormField, DynamicTextPipe, AsyncPipe, NgForgeControl],\n hostDirectives: [NgForgeFieldHost],\n template: `\n @let f = ngf.field();\n @let textareaId = ngf.key() + '-textarea';\n\n <ion-textarea\n ngForgeControl\n [id]=\"textareaId\"\n [formField]=\"f\"\n [label]=\"(ngf.label() | dynamicText | async) ?? undefined\"\n [labelPlacement]=\"props()?.labelPlacement ?? 'stacked'\"\n [placeholder]=\"(ngf.placeholder() | dynamicText | async) ?? ''\"\n [rows]=\"props()?.rows ?? 4\"\n [autoGrow]=\"props()?.autoGrow ?? false\"\n [counter]=\"props()?.counter ?? false\"\n [minlength]=\"f().minLength?.()\"\n [maxlength]=\"f().maxLength?.()\"\n [color]=\"props()?.color\"\n [fill]=\"props()?.fill ?? 'outline'\"\n [shape]=\"props()?.shape\"\n [readonly]=\"f().readonly()\"\n [helperText]=\"ngf.errorsToDisplay().length === 0 ? ((props()?.hint | dynamicText | async) ?? undefined) : undefined\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n />\n @if (ngf.errorsToDisplay()[0]; as error) {\n <ion-note color=\"danger\" class=\"df-ion-error\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</ion-note>\n }\n `,\n changeDetection: ChangeDetectionStrategy.OnPush,\n styleUrl: '../../styles/_form-field.scss',\n styles: [\n `\n :host([hidden]) {\n display: none !important;\n }\n `,\n ],\n})\nexport default class IonicTextareaFieldComponent {\n private readonly elementRef = inject(ElementRef<HTMLElement>);\n\n protected readonly ngf = injectNgForgeField<string>();\n\n readonly props = input<IonicTextareaProps>();\n\n constructor() {\n // ion-textarea encapsulates a native <textarea> in shadow DOM and does not automatically\n // propagate aria-describedby to it. This effect imperatively syncs the attribute\n // after a microtask to ensure Ionic has resolved the internal element.\n explicitEffect([this.ngf.ariaDescribedBy], ([describedBy]) => {\n queueMicrotask(() => {\n const ionTextarea = this.elementRef.nativeElement.querySelector('ion-textarea') as HTMLIonTextareaElement | null;\n if (ionTextarea?.getInputElement) {\n ionTextarea.getInputElement().then((textareaEl) => {\n if (!textareaEl) return;\n if (describedBy) {\n textareaEl.setAttribute('aria-describedby', describedBy);\n } else {\n textareaEl.removeAttribute('aria-describedby');\n }\n });\n }\n });\n });\n }\n}\n","import { ChangeDetectionStrategy, Component, computed, input, model } from '@angular/core';\nimport type { FormCheckboxControl } from '@angular/forms/signals';\nimport { IonToggle } from '@ionic/angular/standalone';\nimport { NgForgeControl } from '@ng-forge/dynamic-forms/integration';\n\n/**\n * ion-toggle wrapper implementing FormCheckboxControl. Rendered inside\n * `df-ion-toggle`. `NgForgeControl` on `<ion-toggle>` claims the ambient\n * parent NgForgeField and routes meta + aria onto that element.\n */\n@Component({\n selector: 'df-ion-toggle-control',\n imports: [IonToggle, NgForgeControl],\n template: `\n <ion-toggle\n ngForgeControl\n [checked]=\"isChecked()\"\n (ionChange)=\"onToggleChange($event)\"\n (ionBlur)=\"onBlur()\"\n [labelPlacement]=\"labelPlacement()\"\n [justify]=\"justify()\"\n [color]=\"color()\"\n [enableOnOffLabels]=\"enableOnOffLabels()\"\n [disabled]=\"isEffectivelyDisabled()\"\n [attr.tabindex]=\"tabIndex()\"\n [attr.aria-readonly]=\"ariaReadonly()\"\n >\n <ng-content />\n </ion-toggle>\n `,\n styleUrl: '../../styles/_form-field.scss',\n host: {\n style: 'display: block',\n },\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class IonicToggleControlComponent implements FormCheckboxControl {\n // ─────────────────────────────────────────────────────────────────────────────\n // FormCheckboxControl implementation\n // ─────────────────────────────────────────────────────────────────────────────\n\n /** The checked state - required by FormCheckboxControl */\n readonly checked = model<boolean>(false);\n\n /** Tracks whether the field has been touched - used by Field directive */\n readonly touched = model<boolean>(false);\n\n /** Whether the field is disabled */\n readonly disabled = input<boolean>(false);\n\n /** Whether the field is readonly */\n readonly readonly = input<boolean>(false);\n\n /** Whether the field is invalid (from Field directive) */\n readonly invalid = input<boolean>(false);\n\n /** Whether the field is required (from Field directive) */\n readonly required = input<boolean>(false);\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Ionic-specific props\n // ─────────────────────────────────────────────────────────────────────────────\n\n readonly labelPlacement = input<'start' | 'end' | 'fixed' | 'stacked'>('end');\n readonly justify = input<'start' | 'end' | 'space-between' | undefined>(undefined);\n readonly color = input<string>('primary');\n readonly enableOnOffLabels = input<boolean>(false);\n readonly tabIndex = input<number | undefined>(undefined);\n\n // aria-invalid / aria-required / aria-describedby are written to <ion-toggle>\n // by NgForgeControl. aria-readonly is Ionic-specific and stays template-side.\n protected readonly ariaReadonly = computed(() => (this.readonly() ? true : null));\n\n /** Computed checked state - ensures proper boolean conversion for ion-toggle */\n protected readonly isChecked = computed(() => {\n return this.checked() === true;\n });\n\n /**\n * ion-toggle doesn't have a native readonly attribute.\n * When readonly, we disable the toggle to prevent changes while maintaining the visual state.\n */\n protected readonly isEffectivelyDisabled = computed(() => {\n return this.disabled() || this.readonly();\n });\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Event Handlers\n // ─────────────────────────────────────────────────────────────────────────────\n\n /** Handle toggle change - sync ion-toggle's checked state to our checked model */\n onToggleChange(event: CustomEvent<{ checked: boolean; value: string }>): void {\n this.checked.set(event.detail.checked);\n }\n\n /** Marks the field as touched when toggle loses focus */\n onBlur(): void {\n this.touched.set(true);\n }\n}\n","import { ChangeDetectionStrategy, Component, input } from '@angular/core';\nimport { FormField } from '@angular/forms/signals';\nimport { IonNote } from '@ionic/angular/standalone';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms';\nimport { injectNgForgeField, NgForgeFieldHost } from '@ng-forge/dynamic-forms/integration';\nimport { IonicToggleProps } from './ionic-toggle.type';\nimport { IonicToggleControlComponent } from './ionic-toggle-control.component';\nimport { AsyncPipe } from '@angular/common';\n\n@Component({\n selector: 'df-ion-toggle',\n imports: [IonicToggleControlComponent, IonNote, FormField, DynamicTextPipe, AsyncPipe],\n hostDirectives: [NgForgeFieldHost],\n template: `\n @let f = ngf.field();\n @let toggleId = ngf.key() + '-toggle';\n\n <df-ion-toggle-control\n [id]=\"toggleId\"\n [formField]=\"f\"\n [labelPlacement]=\"props()?.labelPlacement ?? 'end'\"\n [justify]=\"props()?.justify\"\n [color]=\"props()?.color ?? 'primary'\"\n [enableOnOffLabels]=\"props()?.enableOnOffLabels ?? false\"\n [tabIndex]=\"ngf.tabIndex()\"\n >\n {{ ngf.label() | dynamicText | async }}\n </df-ion-toggle-control>\n\n @if (ngf.errorsToDisplay()[0]; as error) {\n <ion-note color=\"danger\" class=\"df-ion-error\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</ion-note>\n } @else if (props()?.hint; as hint) {\n <ion-note class=\"df-ion-hint\" [id]=\"ngf.hintId()\">{{ hint | dynamicText | async }}</ion-note>\n }\n `,\n styleUrl: '../../styles/_form-field.scss',\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 IonicToggleFieldComponent {\n protected readonly ngf = injectNgForgeField<boolean>();\n\n readonly props = input<IonicToggleProps>();\n}\n","/**\n * Ionic field type constants\n * Based on available field components in the /fields folder\n */\nexport const IonicField = {\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 IonicFieldType = (typeof IonicField)[keyof typeof IonicField];\n","import { computed, inject, isSignal, Signal } from '@angular/core';\nimport { ARRAY_CONTEXT, buildBaseInputs, DEFAULT_PROPS, FieldDef } from '@ng-forge/dynamic-forms';\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 * Supports template property for array events (AppendArrayItemEvent, PrependArrayItemEvent, InsertArrayItemEvent)\n * which enables the $template token in eventArgs.\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';\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 { IonicField } from '../types/types';\nimport { buttonFieldMapper } from '../fields/button/ionic-button.mapper';\nimport { nextButtonFieldMapper, previousButtonFieldMapper, submitButtonFieldMapper } from '../fields/button/ionic-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\n/**\n * Ionic field type definitions\n * Follows the FieldTypeDefinition interface for proper registry integration\n */\nexport const IONIC_FIELD_TYPES: FieldTypeDefinition[] = [\n {\n name: IonicField.Input,\n loadComponent: () => import('../fields/input/ionic-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: IonicField.Select,\n loadComponent: () => import('../fields/select/ionic-select.component'),\n mapper: optionsFieldMapper,\n scope: 'single-select',\n ...VALUE_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.Checkbox,\n loadComponent: () => import('../fields/checkbox/ionic-checkbox.component'),\n mapper: checkboxFieldMapper,\n scope: 'boolean',\n ...VALUE_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.Button,\n loadComponent: () => import('../fields/button/ionic-button.component'),\n mapper: buttonFieldMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.Submit,\n loadComponent: () => import('../fields/button/ionic-button.component'),\n mapper: submitButtonFieldMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.Next,\n loadComponent: () => import('../fields/button/ionic-button.component'),\n mapper: nextButtonFieldMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.Previous,\n loadComponent: () => import('../fields/button/ionic-button.component'),\n mapper: previousButtonFieldMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.AddArrayItem,\n loadComponent: () => import('../fields/button/ionic-button.component'),\n mapper: addArrayItemButtonMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.PrependArrayItem,\n loadComponent: () => import('../fields/button/ionic-button.component'),\n mapper: prependArrayItemButtonMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.InsertArrayItem,\n loadComponent: () => import('../fields/button/ionic-button.component'),\n mapper: insertArrayItemButtonMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.RemoveArrayItem,\n loadComponent: () => import('../fields/button/ionic-button.component'),\n mapper: removeArrayItemButtonMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.PopArrayItem,\n loadComponent: () => import('../fields/button/ionic-button.component'),\n mapper: popArrayItemButtonMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.ShiftArrayItem,\n loadComponent: () => import('../fields/button/ionic-button.component'),\n mapper: shiftArrayItemButtonMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.Textarea,\n loadComponent: () => import('../fields/textarea/ionic-textarea.component'),\n mapper: valueFieldMapper,\n propsToMeta: ['rows'],\n scope: 'text-input',\n ...VALUE_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.Radio,\n loadComponent: () => import('../fields/radio/ionic-radio.component'),\n mapper: optionsFieldMapper,\n scope: 'single-select',\n ...VALUE_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.MultiCheckbox,\n loadComponent: () => import('../fields/multi-checkbox/ionic-multi-checkbox.component'),\n mapper: optionsFieldMapper,\n scope: 'multi-select',\n ...VALUE_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.Datepicker,\n loadComponent: () => import('../fields/datepicker/ionic-datepicker.component'),\n mapper: datepickerFieldMapper,\n scope: 'date',\n ...VALUE_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.Slider,\n loadComponent: () => import('../fields/slider/ionic-slider.component'),\n mapper: valueFieldMapper,\n scope: 'numeric',\n ...VALUE_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.Toggle,\n loadComponent: () => import('../fields/toggle/ionic-toggle.component'),\n mapper: checkboxFieldMapper,\n scope: 'boolean',\n ...VALUE_FIELD_TYPES_BASE,\n },\n];\n","import type { Provider } from '@angular/core';\nimport { ADDON_KIND_DEFINITIONS, DynamicFormError, type AddonKindDefinition, type FieldTypeDefinition } from '@ng-forge/dynamic-forms';\nimport { IONIC_FIELD_TYPES } from '../config/ionic-field-config';\nimport { IonicConfig } from '../models/ionic-config';\nimport { IONIC_CONFIG } from '../models/ionic-config.token';\nimport type { IonButtonAddon, IonIconAddon } from '../types/addons';\n\n/**\n * Field type definitions for Ionic components.\n */\nexport type IonicFieldTypes = FieldTypeDefinition[];\n\ntype IonicConfigFeature = {\n ɵkind: 'ionic-config';\n ɵproviders: Provider[];\n};\n\n/**\n * Default `withIonicFields()` shape — field defs + the auto-included\n * addons feature so `ion-icon` / `ion-button` work out of the box.\n */\ntype IonicFieldsWithAddons = [...IonicFieldTypes, IonicAddonsFeature];\n\ntype IonicFieldsWithConfig = [...IonicFieldTypes, IonicAddonsFeature, IonicConfigFeature];\n\n/**\n * Provides Ionic field type definitions for the dynamic form system,\n * with Ionic-shipped addon kinds (`ion-icon`, `ion-button`) auto-included\n * so addons work out of the box.\n *\n * If you want field types WITHOUT addons (rare), pass them through\n * `provideDynamicForm` directly and skip this helper. If you want addons\n * WITHOUT the field types (also rare — e.g., adding addons to a form that\n * uses custom fields), call `withIonicAddons()` standalone.\n *\n * @param config - Optional global configuration for Ionic form fields\n *\n * @example\n * ```typescript\n * // Application-level setup — addons (ion-icon, ion-button) ship in automatically\n * import { ApplicationConfig } from '@angular/core';\n * import { provideDynamicForm } from '@ng-forge/dynamic-forms';\n * import { withIonicFields } from '@ng-forge/dynamic-forms-ionic';\n *\n * export const appConfig: ApplicationConfig = {\n * providers: [\n * provideDynamicForm(...withIonicFields())\n * ]\n * };\n * ```\n *\n * @example\n * ```typescript\n * // With global configuration\n * export const appConfig: ApplicationConfig = {\n * providers: [\n * provideDynamicForm(\n * ...withIonicFields({\n * fill: 'outline',\n * labelPlacement: 'floating'\n * })\n * )\n * ]\n * };\n * ```\n *\n * @returns Tuple of field type definitions, the addons feature, and\n * optionally a config feature.\n */\nexport function withIonicFields(): IonicFieldsWithAddons;\nexport function withIonicFields(config: IonicConfig): IonicFieldsWithConfig;\nexport function withIonicFields(config: IonicConfig | undefined): IonicFieldsWithAddons | IonicFieldsWithConfig;\nexport function withIonicFields(config?: IonicConfig): IonicFieldsWithAddons | IonicFieldsWithConfig {\n // Always include the addons feature — ion-icon / ion-button are part of\n // the canonical Ionic surface.\n const base: unknown[] = [...IONIC_FIELD_TYPES, withIonicAddons()];\n\n if (config) {\n base.push({\n ɵkind: 'ionic-config',\n ɵproviders: [{ provide: IONIC_CONFIG, useValue: config }],\n } satisfies IonicConfigFeature);\n return base as IonicFieldsWithConfig;\n }\n\n return base as IonicFieldsWithAddons;\n}\n\n/* -- Ionic addon kinds ------------------------------------------------- */\n\nconst ION_ICON_KIND: AddonKindDefinition<IonIconAddon> = {\n kind: 'ion-icon',\n loadComponent: () => import('../addons/ion-icon-addon.component').then((m) => m.IonIconAddonComponent),\n validate: (addon, fieldKey) => {\n if (typeof addon.icon !== 'string' || addon.icon.length === 0) {\n throw new DynamicFormError(`Addon kind 'ion-icon' requires a non-empty 'icon' string (field: '${fieldKey}').`);\n }\n },\n};\n\nconst ION_BUTTON_KIND: AddonKindDefinition<IonButtonAddon> = {\n kind: 'ion-button',\n loadComponent: () => import('../addons/ion-button-addon.component').then((m) => m.IonButtonAddonComponent),\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 'ion-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 'ion-button' on field '${fieldKey}' is icon-only — provide 'ariaLabel' for accessibility.`);\n }\n },\n};\n\n/**\n * Feature kind discriminant for the Ionic addons feature. Matches core's\n * `'addons'` kind so providers flow through the standard addon-kind pipeline\n * in `provideDynamicForm`.\n */\ntype IonicAddonsFeature = {\n ɵkind: 'addons';\n ɵproviders: Provider[];\n};\n\n/**\n * Register Ionic-shipped addon kinds (`ion-icon`, `ion-button`) standalone.\n *\n * **Most users don't need this** — `withIonicFields()` auto-includes\n * these kinds. Call `withIonicAddons()` directly only when you want\n * Ionic addon kinds without the Ionic field types (e.g., a custom\n * field set that wants to render `ion-icon` prefixes), or when you're\n * stitching addons through a different DI scope.\n *\n * @example\n * ```typescript\n * // Custom field types + Ionic addon kinds.\n * provideDynamicForm(\n * ...myCustomFields(),\n * withIonicAddons(),\n * );\n * ```\n *\n * Adapter authors who need to override a kind with a customised renderer\n * should call `withCustomAddon(...)` directly instead.\n */\nexport function withIonicAddons(): IonicAddonsFeature {\n return {\n ɵkind: 'addons',\n ɵproviders: [\n { provide: ADDON_KIND_DEFINITIONS, useValue: ION_ICON_KIND, multi: true },\n { provide: ADDON_KIND_DEFINITIONS, useValue: ION_BUTTON_KIND, multi: true },\n ],\n };\n}\n","import { AsyncPipe } from '@angular/common';\nimport { ChangeDetectionStrategy, Component, computed } from '@angular/core';\nimport { IonButton, IonIcon, IonSpinner } from '@ionic/angular/standalone';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms';\nimport { injectNgForgeAddonAction, NgForgeAddonAction } from '@ng-forge/dynamic-forms/integration';\nimport type { IonButtonAddon } from '../types/addons';\n\n/**\n * Renderer for the `ion-button` addon kind.\n *\n * Wraps Ionic's `<ion-button>`. Click dispatch (preset / actionRef /\n * action precedence, multi-set warning, `disabled` / `loading`\n * resolution) lives on `NgForgeAddonAction`; this component focuses on\n * the visual layer. While `action.loading()`, an `<ion-spinner>` is\n * rendered in place of the icon and the button is disabled.\n */\n@Component({\n selector: 'df-ion-button-addon',\n imports: [IonButton, IonIcon, IonSpinner, DynamicTextPipe, AsyncPipe],\n hostDirectives: [NgForgeAddonAction],\n template: `\n <ion-button\n [attr.color]=\"color() ?? null\"\n [fill]=\"fill()\"\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 <ion-spinner [slot]=\"iconOnly() ? 'icon-only' : 'start'\" name=\"dots\"></ion-spinner>\n } @else if (icon(); as ic) {\n <ion-icon [name]=\"ic\" [slot]=\"iconOnly() ? 'icon-only' : 'start'\"></ion-icon>\n }\n @if (label(); as lbl) {\n {{ lbl | dynamicText | async }}\n }\n </ion-button>\n @if (action.loading()) {\n <span class=\"df-ion-sr-only\" role=\"status\">Loading…</span>\n }\n `,\n styles: [\n `\n .df-ion-sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n }\n `,\n ],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class IonButtonAddonComponent {\n protected readonly action = injectNgForgeAddonAction<IonButtonAddon>();\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 icon = computed(() => this.addon().icon);\n /** Default color is undefined so the button inherits the theme's text color\n * (Ionic's `medium` would force low-contrast grey, which fails WCAG on dark\n * surfaces). Consumers can still set `color: 'primary' | 'success' | ...`\n * explicitly when they want adapter-styled coloring. */\n protected readonly color = computed(() => this.addon().color);\n protected readonly fill = computed(() => this.addon().fill ?? 'clear');\n /** Icon-only when there is an icon but no label — drives the `slot` choice.\n * Truthy check on `label` mirrors Material's heuristic; empty-string label\n * is treated as \"no label\" so the button doesn't render an invisible text\n * slot beside the icon. */\n protected readonly iconOnly = computed(() => !!this.icon() && !this.label());\n}\n","import { AsyncPipe } from '@angular/common';\nimport { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';\nimport { IonIcon } from '@ionic/angular/standalone';\nimport { DynamicTextPipe, WrapperFieldInputs } from '@ng-forge/dynamic-forms';\nimport type { IonIconAddon } from '../types/addons';\n\n/**\n * Renderer for the `ion-icon` addon kind.\n *\n * Outputs `<ion-icon [name]=\"icon\">`. The host is set `aria-hidden=\"true\"`\n * by default; if the addon supplies an `ariaLabel`, it is applied so the\n * icon is announced by screen readers.\n */\n@Component({\n selector: 'df-ion-icon-addon',\n imports: [IonIcon, AsyncPipe, DynamicTextPipe],\n template: `<ion-icon [name]=\"iconName()\" [attr.aria-label]=\"(ariaLabel() | dynamicText | async) ?? null\"></ion-icon>`,\n host: {\n '[attr.aria-hidden]': 'hasAriaLabel() ? null : \"true\"',\n },\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class IonIconAddonComponent {\n readonly addon = input.required<IonIconAddon>();\n /** Accepted for contract uniformity — `NgComponentOutlet` setInput is strict; every kind must declare it. */\n readonly fieldInputs = input<WrapperFieldInputs | undefined>();\n\n protected readonly iconName = computed(() => this.addon().icon);\n protected readonly ariaLabel = computed(() => this.addon().ariaLabel);\n protected readonly hasAriaLabel = computed(() => this.addon().ariaLabel !== undefined);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ng-forge-dynamic-forms-ionic.mjs","mappings":";;;;;;;;;;;AAwCc;;;AAKH;;;AAKP;;AACA;;;AAXiB;;;;;;;;;;;;;;;;;;;AAHlB;;;;AA7BS;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACyCE;;;;AAAO;;;;;;;;;;;;;;;;;;;;;;;;;;AAxCT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACgGE;;AAGH;AACA;AACA;;AAGA;;AAGP;;;AAIA;;AAGF;AACE;gBAGA;AACE;AACA;;;AAEA;;;;AAKJ;AACE;AAAW;AACX;;AAGF;AACE;AAAW;;;AAKT;AACE;AACF;AACA;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;AACF;AACE;;;;AAxDa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AArFT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACTV;AACF;;;AC6CmB;;AAGE;AACA;AACA;AACA;AACA;AACA;AACA;QAIF;AACE;AAEnB;AACE;AACE;AACE;;;AAGF;AACF;;;AAxBS;;;;;;;;;;AA3BV;;;;AAbS;;;;;;;;;;;wBA+BR;AACA;AACA;AACA;AACA;AACA;;;;;;;;AC+GU;AACK;;;;;AAQjB;IAQiB;;AAGE;;AAGA;;AAKnB;;;;;AAaA;AACE;wBAIA;;;;AAGI;AACA;;AAEI;;;AAEE;;;AAEA;;AAEJ;;AAEJ;AACF;;;AA7DiB;AA7EjB;AACE;AACA;AACD;AACD;gFAME;;AAEE;AACA;AACA;oBAEA;;AAEE;AACE;AACA;;;;;;AAOC;;;;AAIR;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAnCF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAIC;AACE;AACA;AACD;AACD;4IAME;;AAEE;AACA;AACA;;;AAIE;AACE;AACA;;;;;;AAOC;;;;AAIR;AACF;;;;;;;;ACrCW;;AAGH;;AAGT;AAEI;;AAEF;IAKO;;;;;AAKP;AACF;AAEA;AACE;AACE;AAEA;AACE;;AAEJ;AAEA;AACE;AACA;;;AAIE;;AAEJ;;;;;AAMI;;;AAEA;;AAEJ;;;AAlDiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAxET;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC6DE;;AAGH;;AAGT;;AANmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAtBlB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACQW;;AAGH;;AAGT;;AANmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAXlB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACOW;;;;;;AAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA3CT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACyCE;AACK;;;AAMjB;wBAIE;;AAEI;AACA;;AAEI;;;AAEE;;;AAEA;;AAEJ;;AAEJ;AACF;;;AAzBiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAxCT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gBCgCD;IAGA;IAGA;IAGA;IAGA;IAGA;YAMA;AACA;AACA;AACA;AACA;;IAOU;AACjB;AACF;AAEA,IAImB;;AAEnB;gBAOA;;;;AAME;;;AA7DS;;;;;;;;;;;;;;;;;;;;AAzBD;;;;;;;;;;;;;;;;;AAqBR;;;;ACiBU;;;;AAAO;;;;;;;;;;;;;;;;;;;;;;;;;AAvCT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACNL;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;ACZI;AACJ;AACA;;;AAKE;AACE;;AAGF;AACE;;AAGF;AACE;;;AAKA;;;AAKA;;QAIF;AACA;;AAGI;;AACA;AACF;;;;AAKE;AACA;;;;AAKJ;AACF;AACF;;;AC3CA;;;AAIA;AACE;AACA;;AAOK;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;;;;AC3HG;;;;AAOA;;AAE4B;AAC9B;;AAGF;AACF;AAEA;AAEA;AACE;AACA;AACA;AACE;AACE;;;;AAKN;AACE;AACA;AACA;;AAIE;AACE;;QAKF;AACE;;;;;;AAkBF;AACA;;;AAGC;;AAEL;;;;IC9CqB;AAEA;AACA;AACA;AACnB,IAImB;AACA;AACnB,IAImB;;AAnBR;;;;;;;;;;;;;;;;;;;;;;;;;AA1CD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACQD;;AAIU;AACA;AACA;;AAPR;;;;AATF;AACT;AACA;AACA;AACA;AACE;AACD;;AAEF","names":[],"ignoreList":[],"sources":["../../../../packages/dynamic-forms-ionic/src/lib/fields/button/ionic-button.component.ts","../../../../packages/dynamic-forms-ionic/src/lib/fields/checkbox/ionic-checkbox.component.ts","../../../../packages/dynamic-forms-ionic/src/lib/fields/datepicker/ionic-datepicker.component.ts","../../../../packages/dynamic-forms-ionic/src/lib/addons/preset-actions.ts","../../../../packages/dynamic-forms-ionic/src/lib/addons/ion-inline-button-addon.component.ts","../../../../packages/dynamic-forms-ionic/src/lib/fields/input/ionic-input.component.ts","../../../../packages/dynamic-forms-ionic/src/lib/fields/multi-checkbox/ionic-multi-checkbox.component.ts","../../../../packages/dynamic-forms-ionic/src/lib/fields/radio/ionic-radio.component.ts","../../../../packages/dynamic-forms-ionic/src/lib/fields/select/ionic-select.component.ts","../../../../packages/dynamic-forms-ionic/src/lib/fields/slider/ionic-slider.component.ts","../../../../packages/dynamic-forms-ionic/src/lib/fields/textarea/ionic-textarea.component.ts","../../../../packages/dynamic-forms-ionic/src/lib/fields/toggle/ionic-toggle-control.component.ts","../../../../packages/dynamic-forms-ionic/src/lib/fields/toggle/ionic-toggle.component.ts","../../../../packages/dynamic-forms-ionic/src/lib/types/types.ts","../../../../packages/dynamic-forms-ionic/src/lib/fields/button/ionic-button.mapper.ts","../../../../packages/dynamic-forms-ionic/src/lib/config/ionic-field-config.ts","../../../../packages/dynamic-forms-ionic/src/lib/providers/ionic-providers.ts","../../../../packages/dynamic-forms-ionic/src/lib/addons/ion-button-addon.component.ts","../../../../packages/dynamic-forms-ionic/src/lib/addons/ion-icon-addon.component.ts"],"sourcesContent":["import { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';\nimport { IonButton } from '@ionic/angular/standalone';\nimport { AsyncPipe } from '@angular/common';\nimport { DynamicTextPipe, FormEvent } from '@ng-forge/dynamic-forms';\nimport { injectNgForgeAction, NgForgeActionHost } from '@ng-forge/dynamic-forms/integration';\nimport { IonicButtonProps } from './ionic-button.type';\n\n@Component({\n selector: 'df-ion-button',\n imports: [IonButton, DynamicTextPipe, AsyncPipe],\n hostDirectives: [NgForgeActionHost],\n styles: [\n `\n :host([hidden]) {\n display: none !important;\n }\n `,\n ],\n template: `\n @let buttonId = action.key() + '-button';\n\n <ion-button\n [id]=\"buttonId\"\n [type]=\"buttonType()\"\n [expand]=\"props()?.expand\"\n [fill]=\"props()?.fill || 'solid'\"\n [shape]=\"props()?.shape\"\n [size]=\"props()?.size\"\n [color]=\"props()?.color || 'primary'\"\n [strong]=\"props()?.strong\"\n [disabled]=\"action.disabled()\"\n [attr.tabindex]=\"action.tabIndex()\"\n [attr.data-testid]=\"buttonTestId()\"\n (click)=\"onClick()\"\n >\n {{ action.label() | dynamicText | async }}\n </ion-button>\n `,\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport default class IonicButtonFieldComponent<TEvent extends FormEvent> {\n protected readonly action = injectNgForgeAction<TEvent>();\n\n readonly props = input<IonicButtonProps>();\n\n readonly buttonType = computed(() => this.props()?.type ?? 'button');\n\n readonly buttonTestId = computed(() => `${this.buttonType()}-${this.action.key()}`);\n\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 { IonCheckbox, IonNote } from '@ionic/angular/standalone';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms';\nimport { NgForgeControl, injectNgForgeField, NgForgeFieldHost } from '@ng-forge/dynamic-forms/integration';\nimport { IonicCheckboxProps } from './ionic-checkbox.type';\nimport { AsyncPipe } from '@angular/common';\n\n@Component({\n selector: 'df-ion-checkbox',\n imports: [IonCheckbox, IonNote, FormField, DynamicTextPipe, AsyncPipe, NgForgeControl],\n hostDirectives: [NgForgeFieldHost],\n template: `\n @let f = ngf.field();\n @let checkboxId = ngf.key() + '-checkbox';\n\n <ion-checkbox\n ngForgeControl\n [id]=\"checkboxId\"\n [formField]=\"f\"\n [labelPlacement]=\"props()?.labelPlacement ?? 'end'\"\n [justify]=\"props()?.justify\"\n [color]=\"props()?.color ?? 'primary'\"\n [indeterminate]=\"props()?.indeterminate ?? false\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n >\n {{ ngf.label() | dynamicText | async }}\n </ion-checkbox>\n\n @if (ngf.errorsToDisplay()[0]; as error) {\n <ion-note color=\"danger\" class=\"df-ion-error\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</ion-note>\n } @else if (props()?.hint; as hint) {\n <ion-note class=\"df-ion-hint\" [id]=\"ngf.hintId()\">{{ hint | dynamicText | async }}</ion-note>\n }\n `,\n styleUrl: '../../styles/_form-field.scss',\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 IonicCheckboxFieldComponent {\n protected readonly ngf = injectNgForgeField<boolean>();\n\n readonly props = input<IonicCheckboxProps>();\n}\n","import { ChangeDetectionStrategy, Component, input, signal } from '@angular/core';\nimport {\n IonButton,\n IonButtons,\n IonContent,\n IonDatetime,\n IonHeader,\n IonInput,\n IonModal,\n IonNote,\n IonTitle,\n IonToolbar,\n} from '@ionic/angular/standalone';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms';\nimport { NgForgeControl, injectNgForgeField, NgForgeFieldHost } from '@ng-forge/dynamic-forms/integration';\nimport { IonicDatepickerProps } from './ionic-datepicker.type';\nimport { AsyncPipe } from '@angular/common';\nimport { format } from 'date-fns';\n\n@Component({\n selector: 'df-ion-datepicker',\n imports: [\n IonInput,\n IonModal,\n IonDatetime,\n IonNote,\n IonButtons,\n IonHeader,\n IonContent,\n IonToolbar,\n IonTitle,\n IonButton,\n DynamicTextPipe,\n AsyncPipe,\n NgForgeControl,\n ],\n hostDirectives: [NgForgeFieldHost],\n template: `\n @let f = ngf.field();\n @let dateValue = f().value();\n @let inputId = ngf.key() + '-input';\n\n <ion-input\n ngForgeControl\n [id]=\"inputId\"\n [label]=\"(ngf.label() | dynamicText | async) ?? undefined\"\n [labelPlacement]=\"'stacked'\"\n [placeholder]=\"(ngf.placeholder() | dynamicText | async) ?? ''\"\n [disabled]=\"f().disabled()\"\n [value]=\"formatDisplayDate(dateValue)\"\n [readonly]=\"true\"\n [fill]=\"'outline'\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n (click)=\"!f().disabled() && openModal()\"\n />\n @if (ngf.errorsToDisplay()[0]; as error) {\n <ion-note color=\"danger\" class=\"df-ion-error\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</ion-note>\n } @else if (props()?.hint; as hint) {\n <ion-note class=\"df-ion-hint\" [id]=\"ngf.hintId()\">{{ hint | dynamicText | async }}</ion-note>\n }\n\n <ion-modal [isOpen]=\"isModalOpen()\" (didDismiss)=\"closeModal()\">\n <ng-template>\n <ion-header>\n <ion-toolbar>\n <ion-title>{{ ngf.label() | dynamicText | async }}</ion-title>\n <ion-buttons slot=\"end\">\n <ion-button (click)=\"closeModal()\">Close</ion-button>\n </ion-buttons>\n </ion-toolbar>\n </ion-header>\n <ion-content>\n <ion-datetime\n [presentation]=\"props()?.presentation ?? 'date'\"\n [value]=\"dateToIsoString(dateValue)\"\n [multiple]=\"props()?.multiple ?? false\"\n [preferWheel]=\"props()?.preferWheel ?? false\"\n [showDefaultButtons]=\"props()?.showDefaultButtons ?? false\"\n [showDefaultTitle]=\"props()?.showDefaultTitle ?? false\"\n [showDefaultTimeLabel]=\"props()?.showDefaultTimeLabel ?? true\"\n [showClearButton]=\"props()?.showClearButton ?? false\"\n [color]=\"props()?.color\"\n [min]=\"dateToIsoString(minDate())\"\n [max]=\"dateToIsoString(maxDate())\"\n (ionChange)=\"onDateChange($event)\"\n >\n </ion-datetime>\n </ion-content>\n </ng-template>\n </ion-modal>\n `,\n styleUrl: '../../styles/_form-field.scss',\n styles: [\n `\n ion-input {\n cursor: pointer;\n }\n\n :host([hidden]) {\n display: none !important;\n }\n `,\n ],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport default class IonicDatepickerFieldComponent {\n protected readonly ngf = injectNgForgeField<Date | null>();\n\n readonly minDate = input<Date | null>(null);\n readonly maxDate = input<Date | null>(null);\n readonly startAt = input<Date | null>(null);\n readonly props = input<IonicDatepickerProps>();\n\n readonly isModalOpen = signal(false);\n\n openModal() {\n this.isModalOpen.set(true);\n }\n\n closeModal() {\n this.isModalOpen.set(false);\n }\n\n onDateChange(event: CustomEvent) {\n const value = event.detail.value;\n // ion-datetime emits ISO 8601 strings internally; convert to Date | null\n // to match the field tree's value type.\n if (value && typeof value === 'string' && value.length > 0) {\n const date = new Date(value);\n this.ngf.field()().value.set(date);\n } else {\n this.ngf.field()().value.set(null);\n }\n this.closeModal();\n }\n\n dateToIsoString(date: Date | null | undefined): string | undefined {\n if (!date) return undefined;\n return date.toISOString();\n }\n\n formatDisplayDate(date: Date | null | undefined): string {\n if (!date) return '';\n\n const presentation = this.props()?.presentation ?? 'date';\n\n switch (presentation) {\n case 'date':\n return format(date, 'MMM d, yyyy');\n case 'date-time':\n case 'time-date':\n return format(date, 'MMM d, yyyy, h:mm a');\n case 'time':\n return format(date, 'h:mm a');\n case 'month':\n return format(date, 'MMMM yyyy');\n case 'month-year':\n return format(date, 'MMM yyyy');\n case 'year':\n return format(date, 'yyyy');\n default:\n return format(date, 'MMM d, yyyy');\n }\n }\n}\n","import type { AddonActionContext, AddonActionPreset, PresetCollaborators } from '@ng-forge/dynamic-forms';\nimport { runPresetAction } from '@ng-forge/dynamic-forms';\n\nexport type { PresetCollaborators };\n\n/** Ionic adapter binding for the shared preset runner. */\nexport function runIonicPresetAction(\n preset: AddonActionPreset,\n ctx: AddonActionContext,\n collaborators: PresetCollaborators,\n): Promise<void> {\n return runPresetAction(preset, ctx, collaborators, 'Ionic', 'ion-input');\n}\n","import { AsyncPipe } from '@angular/common';\nimport { ChangeDetectionStrategy, Component, computed, inject, Injector, Signal, signal } from '@angular/core';\nimport { explicitEffect } from 'ngxtension/explicit-effect';\nimport { IonIcon, IonSpinner } from '@ionic/angular/standalone';\nimport { DynamicText, DynamicTextPipe, resolveDynamicValue } from '@ng-forge/dynamic-forms';\nimport { injectNgForgeAddonAction, NgForgeAddonAction } from '@ng-forge/dynamic-forms/integration';\nimport type { IonButtonAddon } from '../types/addons';\n\n/**\n * Inline `ion-button` addon — the host element IS `<ion-button>` so Ionic's\n * `::slotted(ion-button[slot=start|end])` shadow CSS matches and applies its\n * native icon-only sizing. `IonButtonAddonComponent` (tag selector) stays\n * for the universal `<df-addon-slot>` dispatcher when the addon is rendered\n * outside an `<ion-input>` host.\n */\n@Component({\n selector: 'ion-button[df-ion-button-addon]',\n imports: [IonIcon, IonSpinner, DynamicTextPipe, AsyncPipe],\n hostDirectives: [NgForgeAddonAction],\n template: `\n @if (action.loading()) {\n <ion-spinner [slot]=\"iconOnly() ? 'icon-only' : 'start'\" name=\"dots\"></ion-spinner>\n <span class=\"df-ion-sr-only\" role=\"status\">Loading…</span>\n } @else if (icon(); as ic) {\n <ion-icon [name]=\"ic\" [slot]=\"iconOnly() ? 'icon-only' : 'start'\"></ion-icon>\n }\n @if (label(); as lbl) {\n {{ lbl | dynamicText | async }}\n }\n `,\n styles: [\n `\n .df-ion-sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n }\n `,\n ],\n host: {\n // Stencil reflects [attr.*] to the underlying ion-button properties.\n '[attr.color]': 'color() ?? null',\n '[attr.fill]': 'fill()',\n '[attr.disabled]': 'action.disabled() || action.loading() ? \"true\" : null',\n '[attr.aria-label]': 'resolvedAriaLabel() ?? null',\n '[attr.aria-busy]': 'action.loading() || null',\n '(click)': 'action.dispatch()',\n },\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class IonInlineButtonAddonComponent {\n private readonly injector = inject(Injector);\n protected readonly action = injectNgForgeAddonAction<IonButtonAddon>();\n\n protected readonly addon = this.action.addon;\n protected readonly label = computed(() => this.addon().label);\n protected readonly ariaLabel = computed(() => this.addon().ariaLabel);\n protected readonly icon = computed(() => this.addon().icon);\n protected readonly color = computed(() => this.addon().color);\n protected readonly fill = computed(() => this.addon().fill ?? 'clear');\n protected readonly iconOnly = computed(() => !!this.icon() && !this.label());\n\n // Host bindings can't use pipes (NG5001), so `DynamicText` is materialised\n // via an effect (resolveDynamicValue's toSignal can't run inside computed).\n private readonly _ariaLabelSignal = signal<Signal<string | undefined> | undefined>(undefined);\n protected readonly resolvedAriaLabel = computed(() => this._ariaLabelSignal()?.() ?? undefined);\n\n constructor() {\n explicitEffect([this.ariaLabel], ([raw]) => {\n if (raw === undefined) {\n this._ariaLabelSignal.set(undefined);\n return;\n }\n this._ariaLabelSignal.set(resolveDynamicValue<string | undefined>(raw as DynamicText, undefined, this.injector));\n });\n }\n}\n","import { AsyncPipe } from '@angular/common';\nimport { ChangeDetectionStrategy, Component, computed, DestroyRef, ElementRef, forwardRef, inject, input, signal } from '@angular/core';\nimport { FormField } from '@angular/forms/signals';\nimport { IonInput, IonNote } from '@ionic/angular/standalone';\nimport {\n AddonActionContext,\n AddonActionPreset,\n DfAddonSlot,\n DynamicFormLogger,\n DynamicTextPipe,\n FIELD_SIGNAL_CONTEXT,\n WrapperFieldInputs,\n} from '@ng-forge/dynamic-forms';\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 { explicitEffect } from 'ngxtension/explicit-effect';\nimport { runIonicPresetAction } from '../../addons/preset-actions';\nimport { IonInlineButtonAddonComponent } from '../../addons/ion-inline-button-addon.component';\nimport { IONIC_CONFIG } from '../../models/ionic-config.token';\nimport { IONIC_INPUT_TYPE_OVERRIDE } from '../../tokens/input-type-override.token';\nimport { IonInputAddon, IonicInputProps } from './ionic-input.type';\n\n// minlength/maxlength bindings: Signal Forms auto-syncs these on NATIVE\n// inputs; <ion-input> is a Stencil web component so we bind directly from\n// FieldState (`f().maxLength?.()` — the signal itself is missing when no\n// validator is configured).\n@Component({\n selector: 'df-ion-input',\n // IonButton intentionally NOT imported — it would collide with\n // IonInlineButtonAddonComponent on `<ion-button>` (NG0300).\n imports: [IonInput, IonNote, FormField, DynamicTextPipe, AsyncPipe, NgForgeControl, DfAddonSlot, IonInlineButtonAddonComponent],\n hostDirectives: [NgForgeFieldHost, NgForgeAddons],\n template: `\n @let f = ngf.field();\n @let inputId = ngf.key() + '-input';\n\n <ion-input\n ngForgeControl\n [id]=\"inputId\"\n [type]=\"type()\"\n [formField]=\"f\"\n [label]=\"(ngf.label() | dynamicText | async) ?? undefined\"\n [labelPlacement]=\"labelPlacement()\"\n [placeholder]=\"(ngf.placeholder() | dynamicText | async) ?? ''\"\n [clearInput]=\"props()?.clearInput ?? false\"\n [counter]=\"props()?.counter ?? false\"\n [minlength]=\"f().minLength?.()\"\n [maxlength]=\"f().maxLength?.()\"\n [color]=\"color()\"\n [fill]=\"fill()\"\n [shape]=\"shape()\"\n [readonly]=\"f().readonly()\"\n [helperText]=\"ngf.errorsToDisplay().length === 0 ? ((props()?.hint | dynamicText | async) ?? undefined) : undefined\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n >\n @for (a of decorativePrefixAddons(); track $index) {\n <span slot=\"start\">\n <df-addon-slot [addon]=\"a\" [fieldInputs]=\"fieldInputs()\" [hidden]=\"ngfa.hiddenSignalCache().get(a)\" />\n </span>\n }\n @for (a of buttonPrefixAddons(); track $index) {\n <ion-button df-ion-button-addon slot=\"start\" [addon]=\"a\" [fieldInputs]=\"fieldInputs()\"></ion-button>\n }\n @for (a of buttonSuffixAddons(); track $index) {\n <ion-button df-ion-button-addon slot=\"end\" [addon]=\"a\" [fieldInputs]=\"fieldInputs()\"></ion-button>\n }\n @for (a of decorativeSuffixAddons(); track $index) {\n <span slot=\"end\">\n <df-addon-slot [addon]=\"a\" [fieldInputs]=\"fieldInputs()\" [hidden]=\"ngfa.hiddenSignalCache().get(a)\" />\n </span>\n }\n </ion-input>\n @if (ngf.errorsToDisplay()[0]; as error) {\n <ion-note color=\"danger\" class=\"df-ion-error\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</ion-note>\n }\n `,\n changeDetection: ChangeDetectionStrategy.OnPush,\n styleUrl: '../../styles/_form-field.scss',\n providers: [\n {\n provide: IONIC_INPUT_TYPE_OVERRIDE,\n useFactory: () => signal<string | undefined>(undefined),\n },\n {\n // Adapter-specific preset semantics for `ion-button` addons (clear /\n // reset / paste / copy / toggle-password-visibility). The directive\n // (`NgForgeAddonAction`) delegates here when an addon configures a\n // `preset`. Per-ion-input-instance so the `typeOverride` signal is\n // scoped to one field.\n provide: ADDON_PRESET_HANDLER,\n useFactory: (): AddonPresetHandler => {\n const typeOverride = inject(IONIC_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(() => IonicInputFieldComponent));\n return {\n run: (preset: string, ctx: AddonActionContext) => {\n const fieldKey = ctx.field.key;\n return runIonicPresetAction(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 knobs on each slot wrapper (independent prefix/suffix). */\n :host {\n --df-ion-addon-prefix-outer-padding: 0;\n --df-ion-addon-prefix-inner-padding: 0.5rem;\n --df-ion-addon-suffix-inner-padding: 0.5rem;\n --df-ion-addon-suffix-outer-padding: 0;\n }\n /* inline-flex on slot wrappers prevents baseline-pinning of decorative addon glyphs. */\n :host ::ng-deep ion-input [slot='start'],\n :host ::ng-deep ion-input [slot='end'] {\n display: inline-flex;\n align-items: center;\n align-self: center;\n line-height: 1;\n }\n :host ::ng-deep ion-input [slot='start'] df-addon-slot,\n :host ::ng-deep ion-input [slot='end'] df-addon-slot,\n :host ::ng-deep ion-input [slot='start'] df-ion-icon-addon,\n :host ::ng-deep ion-input [slot='end'] df-ion-icon-addon {\n display: inline-flex;\n align-items: center;\n line-height: 1;\n }\n :host ::ng-deep ion-input [slot='start'] {\n padding-inline-start: var(--df-ion-addon-prefix-outer-padding);\n padding-inline-end: var(--df-ion-addon-prefix-inner-padding);\n }\n :host ::ng-deep ion-input [slot='end'] {\n padding-inline-start: var(--df-ion-addon-suffix-inner-padding);\n padding-inline-end: var(--df-ion-addon-suffix-outer-padding);\n }\n /* Inherit input text color so icons stay readable on dark themes\n (Ionic's default \"medium\" greys out and fails WCAG on dark inputs). */\n :host ::ng-deep ion-input ion-icon {\n color: inherit;\n }\n `,\n ],\n})\nexport default class IonicInputFieldComponent {\n private readonly elementRef = inject(ElementRef<HTMLElement>);\n private ionicConfig = inject(IONIC_CONFIG, { optional: true });\n\n protected readonly ngf = injectNgForgeField<string>();\n protected readonly ngfa = injectNgForgeAddons<IonInputAddon>();\n\n readonly props = input<IonicInputProps>();\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(IONIC_INPUT_TYPE_OVERRIDE);\n\n protected readonly fill = computed(() => this.props()?.fill ?? this.ionicConfig?.fill ?? 'solid');\n protected readonly shape = computed(() => this.props()?.shape ?? this.ionicConfig?.shape);\n /** Bootstrap / PrimeNG. Consumers can opt into Ionic's native inline 'start' */\n protected readonly labelPlacement = computed(() => this.props()?.labelPlacement ?? this.ionicConfig?.labelPlacement ?? 'stacked');\n protected readonly color = computed(() => this.props()?.color ?? this.ionicConfig?.color);\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 /** Per-kind split: ion-button addons render inline via the attribute-\n * selector inline component (so the slotted element is a native\n * <ion-button> Ionic can style); everything else (icon/text/template/\n * component kinds) goes through the universal <df-addon-slot> dispatcher\n * wrapped in <span slot=\"start|end\"> for safe shadow-DOM projection. */\n protected readonly buttonPrefixAddons = computed(() => this.ngfa.prefixAddons().filter((a) => a.kind === 'ion-button'));\n protected readonly buttonSuffixAddons = computed(() => this.ngfa.suffixAddons().filter((a) => a.kind === 'ion-button'));\n protected readonly decorativePrefixAddons = computed(() => this.ngfa.prefixAddons().filter((a) => a.kind !== 'ion-button'));\n protected readonly decorativeSuffixAddons = computed(() => this.ngfa.suffixAddons().filter((a) => a.kind !== 'ion-button'));\n\n /** Set when the component is destroyed — guards async aria-describedby sync against teardown. */\n private destroyed = false;\n\n constructor() {\n inject(DestroyRef).onDestroy(() => (this.destroyed = true));\n // ion-input encapsulates a native <input> in shadow DOM and does not automatically\n // propagate aria-describedby to it. This effect imperatively syncs the attribute\n // after a microtask to ensure Ionic has resolved the internal element.\n explicitEffect([this.ngf.ariaDescribedBy], ([describedBy]) => {\n queueMicrotask(() => {\n if (this.destroyed) return;\n const ionInput = this.elementRef.nativeElement.querySelector('ion-input') as HTMLIonInputElement | null;\n if (ionInput?.getInputElement) {\n ionInput.getInputElement().then((inputEl) => {\n if (this.destroyed || !inputEl) return;\n if (describedBy) {\n inputEl.setAttribute('aria-describedby', describedBy);\n } else {\n inputEl.removeAttribute('aria-describedby');\n }\n });\n }\n });\n });\n }\n}\n","import { ChangeDetectionStrategy, Component, computed, input, linkedSignal } from '@angular/core';\nimport { IonCheckbox, IonItem, IonNote } from '@ionic/angular/standalone';\nimport { DynamicTextPipe, FieldOption, ValueType } from '@ng-forge/dynamic-forms';\nimport { injectNgForgeField, NgForgeFieldHost, isEqual, NgForgeControl } from '@ng-forge/dynamic-forms/integration';\nimport { explicitEffect } from 'ngxtension/explicit-effect';\nimport { IonicMultiCheckboxProps } from './ionic-multi-checkbox.type';\nimport { AsyncPipe } from '@angular/common';\n\n@Component({\n selector: 'df-ion-multi-checkbox',\n imports: [IonCheckbox, IonItem, IonNote, DynamicTextPipe, AsyncPipe, NgForgeControl],\n hostDirectives: [NgForgeFieldHost],\n template: `\n @let f = ngf.field();\n @let checked = checkedValuesMap();\n @if (ngf.label(); as label) {\n <div class=\"checkbox-group-label\">{{ label | dynamicText | async }}</div>\n }\n\n <div\n class=\"checkbox-group\"\n role=\"group\"\n [attr.aria-invalid]=\"ngf.ariaInvalid()\"\n [attr.aria-required]=\"ngf.ariaRequired()\"\n [attr.aria-describedby]=\"ngf.ariaDescribedBy()\"\n [class.ion-invalid]=\"ngf.showErrors()\"\n [class.ion-touched]=\"f().touched()\"\n >\n @for (option of options(); track option.value) {\n <ion-item lines=\"none\">\n <ion-checkbox\n ngForgeControl\n [checked]=\"checked['' + option.value]\"\n [disabled]=\"f().disabled() || option.disabled\"\n [labelPlacement]=\"props()?.labelPlacement ?? 'end'\"\n [justify]=\"props()?.justify ?? 'start'\"\n [color]=\"props()?.color ?? 'primary'\"\n (ionChange)=\"onCheckboxChange(option, $event.detail.checked)\"\n >\n {{ option.label | dynamicText | async }}\n </ion-checkbox>\n </ion-item>\n }\n </div>\n\n @if (ngf.errorsToDisplay()[0]; as error) {\n <ion-note color=\"danger\" class=\"df-ion-error\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</ion-note>\n } @else if (props()?.hint; as hint) {\n <ion-note class=\"df-ion-hint\" [id]=\"ngf.hintId()\">{{ hint | dynamicText | async }}</ion-note>\n }\n `,\n styleUrl: '../../styles/_form-field.scss',\n styles: [\n `\n :host {\n display: block;\n }\n\n :host([hidden]) {\n display: none !important;\n }\n\n .checkbox-group-label {\n margin-bottom: 8px;\n font-weight: 500;\n }\n\n .checkbox-group {\n display: flex;\n flex-direction: column;\n gap: 4px;\n }\n\n ion-item {\n --padding-start: 0;\n --inner-padding-end: 0;\n }\n `,\n ],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport default class IonicMultiCheckboxFieldComponent {\n protected readonly ngf = injectNgForgeField<ValueType[]>();\n\n readonly options = input<FieldOption<ValueType>[]>([]);\n readonly props = input<IonicMultiCheckboxProps>();\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 /** 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 constructor() {\n explicitEffect([this.valueViewModel], ([selectedOptions]) => {\n const selectedValues = selectedOptions.map((option) => 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]) => {\n const values = options.map((option) => 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 ionic-multi-checkbox: ${duplicates.join(', ')}`);\n }\n });\n }\n\n onCheckboxChange(option: FieldOption<ValueType>, checked: boolean): void {\n this.valueViewModel.update((currentOptions) => {\n if (checked) {\n return currentOptions.some((opt) => opt.value === option.value) ? currentOptions : [...currentOptions, option];\n } else {\n return currentOptions.filter((opt) => opt.value !== option.value);\n }\n });\n }\n}\n","import { ChangeDetectionStrategy, Component, input } from '@angular/core';\nimport { FormField } from '@angular/forms/signals';\nimport { IonItem, IonNote, IonRadio, IonRadioGroup } from '@ionic/angular/standalone';\nimport { DynamicTextPipe, FieldOption, ValueType } from '@ng-forge/dynamic-forms';\nimport { NgForgeControl, injectNgForgeField, NgForgeFieldHost } from '@ng-forge/dynamic-forms/integration';\nimport { IonicRadioProps } from './ionic-radio.type';\nimport { AsyncPipe } from '@angular/common';\n\n@Component({\n selector: 'df-ion-radio',\n imports: [IonRadioGroup, IonRadio, IonItem, IonNote, FormField, DynamicTextPipe, AsyncPipe, NgForgeControl],\n hostDirectives: [NgForgeFieldHost],\n template: `\n @let f = ngf.field();\n @let radioGroupId = ngf.key() + '-radio-group';\n @if (ngf.label(); as label) {\n <div class=\"radio-label\">{{ label | dynamicText | async }}</div>\n }\n\n <ion-radio-group\n [id]=\"radioGroupId\"\n [formField]=\"f\"\n [compareWith]=\"props()?.compareWith || defaultCompare\"\n [attr.aria-invalid]=\"ngf.ariaInvalid()\"\n [attr.aria-required]=\"ngf.ariaRequired()\"\n [attr.aria-describedby]=\"ngf.ariaDescribedBy()\"\n >\n @for (option of options(); track option.value) {\n <ion-item [lines]=\"'none'\">\n <ion-radio\n ngForgeControl\n [value]=\"option.value\"\n [disabled]=\"option.disabled || false\"\n [labelPlacement]=\"props()?.labelPlacement ?? 'end'\"\n [justify]=\"props()?.justify\"\n [color]=\"props()?.color ?? 'primary'\"\n >\n {{ option.label | dynamicText | async }}\n </ion-radio>\n </ion-item>\n }\n </ion-radio-group>\n\n @if (ngf.errorsToDisplay()[0]; as error) {\n <ion-note color=\"danger\" class=\"df-ion-error\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</ion-note>\n } @else if (props()?.hint; as hint) {\n <ion-note class=\"df-ion-hint\" [id]=\"ngf.hintId()\">{{ hint | dynamicText | async }}</ion-note>\n }\n `,\n styleUrl: '../../styles/_form-field.scss',\n styles: [\n `\n :host {\n display: block;\n }\n\n :host([hidden]) {\n display: none !important;\n }\n\n .radio-label {\n font-size: 0.875rem;\n font-weight: 500;\n margin-bottom: 0.5rem;\n color: var(--ion-text-color);\n }\n `,\n ],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport default class IonicRadioFieldComponent {\n protected readonly ngf = injectNgForgeField<ValueType>();\n\n readonly options = input<FieldOption<ValueType>[]>([]);\n readonly props = input<IonicRadioProps>();\n\n defaultCompare = Object.is;\n}\n","import { ChangeDetectionStrategy, Component, input } from '@angular/core';\nimport { FormField } from '@angular/forms/signals';\nimport { IonNote, IonSelect, IonSelectOption } from '@ionic/angular/standalone';\nimport { DynamicTextPipe, FieldOption, ValueType } from '@ng-forge/dynamic-forms';\nimport { NgForgeControl, injectNgForgeField, NgForgeFieldHost } from '@ng-forge/dynamic-forms/integration';\nimport { IonicSelectProps } from './ionic-select.type';\nimport { AsyncPipe } from '@angular/common';\n\n@Component({\n selector: 'df-ion-select',\n imports: [IonSelect, IonSelectOption, IonNote, FormField, DynamicTextPipe, AsyncPipe, NgForgeControl],\n hostDirectives: [NgForgeFieldHost],\n template: `\n @let f = ngf.field();\n @let selectId = ngf.key() + '-select';\n\n <ion-select\n ngForgeControl\n [id]=\"selectId\"\n [formField]=\"f\"\n [label]=\"(ngf.label() | dynamicText | async) ?? undefined\"\n [labelPlacement]=\"props()?.labelPlacement ?? 'stacked'\"\n [placeholder]=\"(ngf.placeholder() | dynamicText | async) ?? ''\"\n [multiple]=\"props()?.multiple ?? false\"\n [compareWith]=\"props()?.compareWith ?? defaultCompare\"\n [interface]=\"props()?.interface ?? 'alert'\"\n [interfaceOptions]=\"props()?.interfaceOptions ?? {}\"\n [cancelText]=\"props()?.cancelText ?? 'Cancel'\"\n [okText]=\"props()?.okText ?? 'OK'\"\n [color]=\"props()?.color\"\n [fill]=\"props()?.fill ?? 'outline'\"\n [shape]=\"props()?.shape\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n >\n @for (option of options(); track option.value) {\n <ion-select-option [value]=\"option.value\" [disabled]=\"option.disabled || false\">\n {{ option.label | dynamicText | async }}\n </ion-select-option>\n }\n </ion-select>\n @if (ngf.errorsToDisplay()[0]; as error) {\n <ion-note color=\"danger\" class=\"df-ion-error\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</ion-note>\n } @else if (props()?.hint; as hint) {\n <ion-note class=\"df-ion-hint\" [id]=\"ngf.hintId()\">{{ hint | dynamicText | async }}</ion-note>\n }\n `,\n changeDetection: ChangeDetectionStrategy.OnPush,\n styleUrl: '../../styles/_form-field.scss',\n styles: [\n `\n :host([hidden]) {\n display: none !important;\n }\n `,\n ],\n})\nexport default class IonicSelectFieldComponent {\n protected readonly ngf = injectNgForgeField<ValueType>();\n\n readonly options = input<FieldOption<ValueType>[]>([]);\n readonly props = input<IonicSelectProps>();\n\n defaultCompare = Object.is;\n}\n","import { ChangeDetectionStrategy, Component, input } from '@angular/core';\nimport { FormField } from '@angular/forms/signals';\nimport { IonNote, IonRange } from '@ionic/angular/standalone';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms';\nimport { NgForgeHostControl, injectNgForgeField, NgForgeFieldHost } from '@ng-forge/dynamic-forms/integration';\nimport { IonicSliderProps } from './ionic-slider.type';\nimport { AsyncPipe } from '@angular/common';\n\n@Component({\n selector: 'df-ion-slider',\n imports: [IonRange, IonNote, FormField, DynamicTextPipe, AsyncPipe],\n hostDirectives: [NgForgeFieldHost, NgForgeHostControl],\n template: `\n @let f = ngf.field();\n @let inputId = ngf.key() + '-input';\n\n <ion-range\n [id]=\"inputId\"\n [formField]=\"f\"\n [label]=\"(ngf.label() | dynamicText | async) ?? undefined\"\n [labelPlacement]=\"props()?.labelPlacement ?? 'stacked'\"\n [min]=\"f().min?.() ?? props()?.min ?? 0\"\n [max]=\"f().max?.() ?? props()?.max ?? 100\"\n [step]=\"step() ?? props()?.step ?? 1\"\n [dualKnobs]=\"props()?.dualKnobs ?? false\"\n [pin]=\"props()?.pin ?? false\"\n [pinFormatter]=\"props()?.pinFormatter ?? defaultPinFormatter\"\n [ticks]=\"props()?.ticks ?? false\"\n [snaps]=\"props()?.snaps ?? false\"\n [color]=\"props()?.color ?? 'primary'\"\n />\n\n @if (ngf.errorsToDisplay()[0]; as error) {\n <ion-note color=\"danger\" class=\"df-ion-error\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</ion-note>\n } @else if (props()?.hint; as hint) {\n <ion-note class=\"df-ion-hint\" [id]=\"ngf.hintId()\">{{ hint | dynamicText | async }}</ion-note>\n }\n `,\n styleUrl: '../../styles/_form-field.scss',\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 IonicSliderFieldComponent {\n protected readonly ngf = injectNgForgeField<number>();\n\n readonly step = input<number>();\n readonly props = input<IonicSliderProps>();\n\n protected defaultPinFormatter = (value: number) => String(value);\n}\n","import { ChangeDetectionStrategy, Component, ElementRef, inject, input } from '@angular/core';\nimport { explicitEffect } from 'ngxtension/explicit-effect';\nimport { FormField } from '@angular/forms/signals';\nimport { IonNote, IonTextarea } from '@ionic/angular/standalone';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms';\nimport { NgForgeControl, injectNgForgeField, NgForgeFieldHost } from '@ng-forge/dynamic-forms/integration';\nimport { IonicTextareaProps } from './ionic-textarea.type';\nimport { AsyncPipe } from '@angular/common';\n\n@Component({\n selector: 'df-ion-textarea',\n imports: [IonTextarea, IonNote, FormField, DynamicTextPipe, AsyncPipe, NgForgeControl],\n hostDirectives: [NgForgeFieldHost],\n template: `\n @let f = ngf.field();\n @let textareaId = ngf.key() + '-textarea';\n\n <ion-textarea\n ngForgeControl\n [id]=\"textareaId\"\n [formField]=\"f\"\n [label]=\"(ngf.label() | dynamicText | async) ?? undefined\"\n [labelPlacement]=\"props()?.labelPlacement ?? 'stacked'\"\n [placeholder]=\"(ngf.placeholder() | dynamicText | async) ?? ''\"\n [rows]=\"props()?.rows ?? 4\"\n [autoGrow]=\"props()?.autoGrow ?? false\"\n [counter]=\"props()?.counter ?? false\"\n [minlength]=\"f().minLength?.()\"\n [maxlength]=\"f().maxLength?.()\"\n [color]=\"props()?.color\"\n [fill]=\"props()?.fill ?? 'outline'\"\n [shape]=\"props()?.shape\"\n [readonly]=\"f().readonly()\"\n [helperText]=\"ngf.errorsToDisplay().length === 0 ? ((props()?.hint | dynamicText | async) ?? undefined) : undefined\"\n [attr.tabindex]=\"ngf.tabIndex()\"\n />\n @if (ngf.errorsToDisplay()[0]; as error) {\n <ion-note color=\"danger\" class=\"df-ion-error\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</ion-note>\n }\n `,\n changeDetection: ChangeDetectionStrategy.OnPush,\n styleUrl: '../../styles/_form-field.scss',\n styles: [\n `\n :host([hidden]) {\n display: none !important;\n }\n `,\n ],\n})\nexport default class IonicTextareaFieldComponent {\n private readonly elementRef = inject(ElementRef<HTMLElement>);\n\n protected readonly ngf = injectNgForgeField<string>();\n\n readonly props = input<IonicTextareaProps>();\n\n constructor() {\n // ion-textarea encapsulates a native <textarea> in shadow DOM and does not automatically\n // propagate aria-describedby to it. This effect imperatively syncs the attribute\n // after a microtask to ensure Ionic has resolved the internal element.\n explicitEffect([this.ngf.ariaDescribedBy], ([describedBy]) => {\n queueMicrotask(() => {\n const ionTextarea = this.elementRef.nativeElement.querySelector('ion-textarea') as HTMLIonTextareaElement | null;\n if (ionTextarea?.getInputElement) {\n ionTextarea.getInputElement().then((textareaEl) => {\n if (!textareaEl) return;\n if (describedBy) {\n textareaEl.setAttribute('aria-describedby', describedBy);\n } else {\n textareaEl.removeAttribute('aria-describedby');\n }\n });\n }\n });\n });\n }\n}\n","import { ChangeDetectionStrategy, Component, computed, input, model } from '@angular/core';\nimport type { FormCheckboxControl } from '@angular/forms/signals';\nimport { IonToggle } from '@ionic/angular/standalone';\nimport { NgForgeControl } from '@ng-forge/dynamic-forms/integration';\n\n/**\n * ion-toggle wrapper implementing FormCheckboxControl. Rendered inside\n * `df-ion-toggle`. `NgForgeControl` on `<ion-toggle>` claims the ambient\n * parent NgForgeField and routes meta + aria onto that element.\n */\n@Component({\n selector: 'df-ion-toggle-control',\n imports: [IonToggle, NgForgeControl],\n template: `\n <ion-toggle\n ngForgeControl\n [checked]=\"isChecked()\"\n (ionChange)=\"onToggleChange($event)\"\n (ionBlur)=\"onBlur()\"\n [labelPlacement]=\"labelPlacement()\"\n [justify]=\"justify()\"\n [color]=\"color()\"\n [enableOnOffLabels]=\"enableOnOffLabels()\"\n [disabled]=\"isEffectivelyDisabled()\"\n [attr.tabindex]=\"tabIndex()\"\n [attr.aria-readonly]=\"ariaReadonly()\"\n >\n <ng-content />\n </ion-toggle>\n `,\n styleUrl: '../../styles/_form-field.scss',\n host: {\n style: 'display: block',\n },\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class IonicToggleControlComponent implements FormCheckboxControl {\n // ─────────────────────────────────────────────────────────────────────────────\n // FormCheckboxControl implementation\n // ─────────────────────────────────────────────────────────────────────────────\n\n /** The checked state - required by FormCheckboxControl */\n readonly checked = model<boolean>(false);\n\n /** Tracks whether the field has been touched - used by Field directive */\n readonly touched = model<boolean>(false);\n\n /** Whether the field is disabled */\n readonly disabled = input<boolean>(false);\n\n /** Whether the field is readonly */\n readonly readonly = input<boolean>(false);\n\n /** Whether the field is invalid (from Field directive) */\n readonly invalid = input<boolean>(false);\n\n /** Whether the field is required (from Field directive) */\n readonly required = input<boolean>(false);\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Ionic-specific props\n // ─────────────────────────────────────────────────────────────────────────────\n\n readonly labelPlacement = input<'start' | 'end' | 'fixed' | 'stacked'>('end');\n readonly justify = input<'start' | 'end' | 'space-between' | undefined>(undefined);\n readonly color = input<string>('primary');\n readonly enableOnOffLabels = input<boolean>(false);\n readonly tabIndex = input<number | undefined>(undefined);\n\n // aria-invalid / aria-required / aria-describedby are written to <ion-toggle>\n // by NgForgeControl. aria-readonly is Ionic-specific and stays template-side.\n protected readonly ariaReadonly = computed(() => (this.readonly() ? true : null));\n\n /** Computed checked state - ensures proper boolean conversion for ion-toggle */\n protected readonly isChecked = computed(() => {\n return this.checked() === true;\n });\n\n /**\n * ion-toggle doesn't have a native readonly attribute.\n * When readonly, we disable the toggle to prevent changes while maintaining the visual state.\n */\n protected readonly isEffectivelyDisabled = computed(() => {\n return this.disabled() || this.readonly();\n });\n\n // ─────────────────────────────────────────────────────────────────────────────\n // Event Handlers\n // ─────────────────────────────────────────────────────────────────────────────\n\n /** Handle toggle change - sync ion-toggle's checked state to our checked model */\n onToggleChange(event: CustomEvent<{ checked: boolean; value: string }>): void {\n this.checked.set(event.detail.checked);\n }\n\n /** Marks the field as touched when toggle loses focus */\n onBlur(): void {\n this.touched.set(true);\n }\n}\n","import { ChangeDetectionStrategy, Component, input } from '@angular/core';\nimport { FormField } from '@angular/forms/signals';\nimport { IonNote } from '@ionic/angular/standalone';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms';\nimport { injectNgForgeField, NgForgeFieldHost } from '@ng-forge/dynamic-forms/integration';\nimport { IonicToggleProps } from './ionic-toggle.type';\nimport { IonicToggleControlComponent } from './ionic-toggle-control.component';\nimport { AsyncPipe } from '@angular/common';\n\n@Component({\n selector: 'df-ion-toggle',\n imports: [IonicToggleControlComponent, IonNote, FormField, DynamicTextPipe, AsyncPipe],\n hostDirectives: [NgForgeFieldHost],\n template: `\n @let f = ngf.field();\n @let toggleId = ngf.key() + '-toggle';\n\n <df-ion-toggle-control\n [id]=\"toggleId\"\n [formField]=\"f\"\n [labelPlacement]=\"props()?.labelPlacement ?? 'end'\"\n [justify]=\"props()?.justify\"\n [color]=\"props()?.color ?? 'primary'\"\n [enableOnOffLabels]=\"props()?.enableOnOffLabels ?? false\"\n [tabIndex]=\"ngf.tabIndex()\"\n >\n {{ ngf.label() | dynamicText | async }}\n </df-ion-toggle-control>\n\n @if (ngf.errorsToDisplay()[0]; as error) {\n <ion-note color=\"danger\" class=\"df-ion-error\" [id]=\"ngf.errorId()\" role=\"alert\">{{ error.message }}</ion-note>\n } @else if (props()?.hint; as hint) {\n <ion-note class=\"df-ion-hint\" [id]=\"ngf.hintId()\">{{ hint | dynamicText | async }}</ion-note>\n }\n `,\n styleUrl: '../../styles/_form-field.scss',\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 IonicToggleFieldComponent {\n protected readonly ngf = injectNgForgeField<boolean>();\n\n readonly props = input<IonicToggleProps>();\n}\n","/**\n * Ionic field type constants\n * Based on available field components in the /fields folder\n */\nexport const IonicField = {\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 IonicFieldType = (typeof IonicField)[keyof typeof IonicField];\n","import { computed, inject, isSignal, Signal } from '@angular/core';\nimport { ARRAY_CONTEXT, buildBaseInputs, DEFAULT_PROPS, FieldDef } from '@ng-forge/dynamic-forms';\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';\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 { IonicField } from '../types/types';\nimport { buttonFieldMapper } from '../fields/button/ionic-button.mapper';\nimport { nextButtonFieldMapper, previousButtonFieldMapper, submitButtonFieldMapper } from '../fields/button/ionic-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\n/**\n * Ionic field type definitions\n * Follows the FieldTypeDefinition interface for proper registry integration\n */\nexport const IONIC_FIELD_TYPES: FieldTypeDefinition[] = [\n {\n name: IonicField.Input,\n loadComponent: () => import('../fields/input/ionic-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: IonicField.Select,\n loadComponent: () => import('../fields/select/ionic-select.component'),\n mapper: optionsFieldMapper,\n scope: 'single-select',\n ...VALUE_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.Checkbox,\n loadComponent: () => import('../fields/checkbox/ionic-checkbox.component'),\n mapper: checkboxFieldMapper,\n scope: 'boolean',\n ...VALUE_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.Button,\n loadComponent: () => import('../fields/button/ionic-button.component'),\n mapper: buttonFieldMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.Submit,\n loadComponent: () => import('../fields/button/ionic-button.component'),\n mapper: submitButtonFieldMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.Next,\n loadComponent: () => import('../fields/button/ionic-button.component'),\n mapper: nextButtonFieldMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.Previous,\n loadComponent: () => import('../fields/button/ionic-button.component'),\n mapper: previousButtonFieldMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.AddArrayItem,\n loadComponent: () => import('../fields/button/ionic-button.component'),\n mapper: addArrayItemButtonMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.PrependArrayItem,\n loadComponent: () => import('../fields/button/ionic-button.component'),\n mapper: prependArrayItemButtonMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.InsertArrayItem,\n loadComponent: () => import('../fields/button/ionic-button.component'),\n mapper: insertArrayItemButtonMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.RemoveArrayItem,\n loadComponent: () => import('../fields/button/ionic-button.component'),\n mapper: removeArrayItemButtonMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.PopArrayItem,\n loadComponent: () => import('../fields/button/ionic-button.component'),\n mapper: popArrayItemButtonMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.ShiftArrayItem,\n loadComponent: () => import('../fields/button/ionic-button.component'),\n mapper: shiftArrayItemButtonMapper,\n ...BUTTON_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.Textarea,\n loadComponent: () => import('../fields/textarea/ionic-textarea.component'),\n mapper: valueFieldMapper,\n propsToMeta: ['rows'],\n scope: 'text-input',\n ...VALUE_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.Radio,\n loadComponent: () => import('../fields/radio/ionic-radio.component'),\n mapper: optionsFieldMapper,\n scope: 'single-select',\n ...VALUE_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.MultiCheckbox,\n loadComponent: () => import('../fields/multi-checkbox/ionic-multi-checkbox.component'),\n mapper: optionsFieldMapper,\n scope: 'multi-select',\n ...VALUE_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.Datepicker,\n loadComponent: () => import('../fields/datepicker/ionic-datepicker.component'),\n mapper: datepickerFieldMapper,\n scope: 'date',\n ...VALUE_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.Slider,\n loadComponent: () => import('../fields/slider/ionic-slider.component'),\n mapper: valueFieldMapper,\n scope: 'numeric',\n ...VALUE_FIELD_TYPES_BASE,\n },\n {\n name: IonicField.Toggle,\n loadComponent: () => import('../fields/toggle/ionic-toggle.component'),\n mapper: checkboxFieldMapper,\n scope: 'boolean',\n ...VALUE_FIELD_TYPES_BASE,\n },\n];\n","import type { Provider } from '@angular/core';\nimport { ADDON_KIND_DEFINITIONS, DynamicFormError, type AddonKindDefinition, type FieldTypeDefinition } from '@ng-forge/dynamic-forms';\nimport { IONIC_FIELD_TYPES } from '../config/ionic-field-config';\nimport { IonicConfig } from '../models/ionic-config';\nimport { IONIC_CONFIG } from '../models/ionic-config.token';\nimport type { IonButtonAddon, IonIconAddon } from '../types/addons';\n\n/** Field type definitions for Ionic components. */\nexport type IonicFieldTypes = FieldTypeDefinition[];\n\ntype IonicConfigFeature = {\n ɵkind: 'ionic-config';\n ɵproviders: Provider[];\n};\n\n/**\n * Default `withIonicFields()` shape — field defs + the auto-included\n * addons feature so `ion-icon` / `ion-button` work out of the box.\n */\ntype IonicFieldsWithAddons = [...IonicFieldTypes, IonicAddonsFeature];\n\ntype IonicFieldsWithConfig = [...IonicFieldTypes, IonicAddonsFeature, IonicConfigFeature];\n\n/**\n * Provides Ionic field type definitions for the dynamic form system,\n * with Ionic-shipped addon kinds (`ion-icon`, `ion-button`) auto-included\n * so addons work out of the box.\n *\n * @param config - Optional global configuration for Ionic form fields\n * @returns Tuple of field type definitions, the addons feature, and\n * optionally a config feature.\n */\nexport function withIonicFields(): IonicFieldsWithAddons;\nexport function withIonicFields(config: IonicConfig): IonicFieldsWithConfig;\nexport function withIonicFields(config: IonicConfig | undefined): IonicFieldsWithAddons | IonicFieldsWithConfig;\nexport function withIonicFields(config?: IonicConfig): IonicFieldsWithAddons | IonicFieldsWithConfig {\n // Always include the addons feature — ion-icon / ion-button are part of\n // the canonical Ionic surface.\n const base: unknown[] = [...IONIC_FIELD_TYPES, withIonicAddons()];\n\n if (config) {\n base.push({\n ɵkind: 'ionic-config',\n ɵproviders: [{ provide: IONIC_CONFIG, useValue: config }],\n } satisfies IonicConfigFeature);\n return base as IonicFieldsWithConfig;\n }\n\n return base as IonicFieldsWithAddons;\n}\n\n/* -- Ionic addon kinds ------------------------------------------------- */\n\nconst ION_ICON_KIND: AddonKindDefinition<IonIconAddon> = {\n kind: 'ion-icon',\n loadComponent: () => import('../addons/ion-icon-addon.component').then((m) => m.IonIconAddonComponent),\n validate: (addon, fieldKey) => {\n if (typeof addon.icon !== 'string' || addon.icon.length === 0) {\n throw new DynamicFormError(`Addon kind 'ion-icon' requires a non-empty 'icon' string (field: '${fieldKey}').`);\n }\n },\n};\n\nconst ION_BUTTON_KIND: AddonKindDefinition<IonButtonAddon> = {\n kind: 'ion-button',\n loadComponent: () => import('../addons/ion-button-addon.component').then((m) => m.IonButtonAddonComponent),\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 'ion-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 'ion-button' on field '${fieldKey}' is icon-only — provide 'ariaLabel' for accessibility.`);\n }\n },\n};\n\n/**\n * Feature kind discriminant for the Ionic addons feature. Matches core's\n * `'addons'` kind so providers flow through the standard addon-kind pipeline\n * in `provideDynamicForm`.\n */\ntype IonicAddonsFeature = {\n ɵkind: 'addons';\n ɵproviders: Provider[];\n};\n\n/** Register Ionic-shipped addon kinds (`ion-icon`, `ion-button`) standalone. */\nexport function withIonicAddons(): IonicAddonsFeature {\n return {\n ɵkind: 'addons',\n ɵproviders: [\n { provide: ADDON_KIND_DEFINITIONS, useValue: ION_ICON_KIND, multi: true },\n { provide: ADDON_KIND_DEFINITIONS, useValue: ION_BUTTON_KIND, multi: true },\n ],\n };\n}\n","import { AsyncPipe } from '@angular/common';\nimport { ChangeDetectionStrategy, Component, computed } from '@angular/core';\nimport { IonButton, IonIcon, IonSpinner } from '@ionic/angular/standalone';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms';\nimport { injectNgForgeAddonAction, NgForgeAddonAction } from '@ng-forge/dynamic-forms/integration';\nimport type { IonButtonAddon } from '../types/addons';\n\n/** Renderer for the `ion-button` addon kind. */\n@Component({\n selector: 'df-ion-button-addon',\n imports: [IonButton, IonIcon, IonSpinner, DynamicTextPipe, AsyncPipe],\n hostDirectives: [NgForgeAddonAction],\n template: `\n <ion-button\n [attr.color]=\"color() ?? null\"\n [fill]=\"fill()\"\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 <ion-spinner [slot]=\"iconOnly() ? 'icon-only' : 'start'\" name=\"dots\"></ion-spinner>\n } @else if (icon(); as ic) {\n <ion-icon [name]=\"ic\" [slot]=\"iconOnly() ? 'icon-only' : 'start'\"></ion-icon>\n }\n @if (label(); as lbl) {\n {{ lbl | dynamicText | async }}\n }\n </ion-button>\n @if (action.loading()) {\n <span class=\"df-ion-sr-only\" role=\"status\">Loading…</span>\n }\n `,\n styles: [\n `\n .df-ion-sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n }\n `,\n ],\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class IonButtonAddonComponent {\n protected readonly action = injectNgForgeAddonAction<IonButtonAddon>();\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 icon = computed(() => this.addon().icon);\n /** Default color is undefined so the button inherits the theme's text color\n * (Ionic's `medium` would force low-contrast grey, which fails WCAG on dark\n * surfaces). Consumers can still set `color: 'primary' | 'success' | ...`\n * explicitly when they want adapter-styled coloring. */\n protected readonly color = computed(() => this.addon().color);\n protected readonly fill = computed(() => this.addon().fill ?? 'clear');\n /** Icon-only when there is an icon but no label — drives the `slot` choice.\n * Truthy check on `label` mirrors Material's heuristic; empty-string label\n * is treated as \"no label\" so the button doesn't render an invisible text\n * slot beside the icon. */\n protected readonly iconOnly = computed(() => !!this.icon() && !this.label());\n}\n","import { AsyncPipe } from '@angular/common';\nimport { ChangeDetectionStrategy, Component, computed, input } from '@angular/core';\nimport { IonIcon } from '@ionic/angular/standalone';\nimport { DynamicTextPipe, WrapperFieldInputs } from '@ng-forge/dynamic-forms';\nimport type { IonIconAddon } from '../types/addons';\n\n/** Renderer for the `ion-icon` addon kind. */\n@Component({\n selector: 'df-ion-icon-addon',\n imports: [IonIcon, AsyncPipe, DynamicTextPipe],\n template: `<ion-icon [name]=\"iconName()\" [attr.aria-label]=\"(ariaLabel() | dynamicText | async) ?? null\"></ion-icon>`,\n host: {\n '[attr.aria-hidden]': 'hasAriaLabel() ? null : \"true\"',\n },\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class IonIconAddonComponent {\n readonly addon = input.required<IonIconAddon>();\n /** Accepted for contract uniformity — `NgComponentOutlet` setInput is strict; every kind must declare it. */\n readonly fieldInputs = input<WrapperFieldInputs | undefined>();\n\n protected readonly iconName = computed(() => this.addon().icon);\n protected readonly ariaLabel = computed(() => this.addon().ariaLabel);\n protected readonly hasAriaLabel = computed(() => this.addon().ariaLabel !== undefined);\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ng-forge/dynamic-forms-ionic",
|
|
3
|
-
"version": "0.10.0-next.
|
|
3
|
+
"version": "0.10.0-next.3",
|
|
4
4
|
"description": "Ionic integration for @ng-forge/dynamic-forms. Pre-built Ionic form components for mobile and desktop.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"@angular/core": "~21.2.0",
|
|
44
44
|
"@angular/forms": "~21.2.0",
|
|
45
45
|
"@ionic/angular": ">=7.0.0",
|
|
46
|
-
"@ng-forge/dynamic-forms": "~0.10.0-next.
|
|
46
|
+
"@ng-forge/dynamic-forms": "~0.10.0-next.3",
|
|
47
47
|
"rxjs": ">=7.0.0"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
@@ -16,9 +16,7 @@ interface IonicButtonProps {
|
|
|
16
16
|
type?: 'button' | 'submit' | 'reset';
|
|
17
17
|
}
|
|
18
18
|
type IonicButtonField<TEvent extends FormEvent> = ButtonField<IonicButtonProps, TEvent>;
|
|
19
|
-
/**
|
|
20
|
-
* Specific button field types with preconfigured events
|
|
21
|
-
*/
|
|
19
|
+
/** Specific button field types with preconfigured events */
|
|
22
20
|
/** Submit button field - automatically disabled when form is invalid */
|
|
23
21
|
type IonicSubmitButtonField = Omit<IonicButtonField<SubmitEvent>, 'event' | 'type' | 'eventArgs'> & {
|
|
24
22
|
type: 'submit';
|
|
@@ -72,9 +70,7 @@ type InsertArrayItemButtonField = Omit<IonicButtonField<InsertArrayItemEvent>, '
|
|
|
72
70
|
* When inside an array, this is automatically determined from context.
|
|
73
71
|
*/
|
|
74
72
|
arrayKey?: string;
|
|
75
|
-
/**
|
|
76
|
-
* The index at which to insert the new item.
|
|
77
|
-
*/
|
|
73
|
+
/** The index at which to insert the new item. */
|
|
78
74
|
index: number;
|
|
79
75
|
/**
|
|
80
76
|
* Template for the new array item. REQUIRED.
|
|
@@ -174,16 +170,7 @@ declare class IonicDatepickerFieldComponent {
|
|
|
174
170
|
static ɵcmp: _angular_core.ɵɵComponentDeclaration<IonicDatepickerFieldComponent, "df-ion-datepicker", never, { "minDate": { "alias": "minDate"; "required": false; "isSignal": true; }; "maxDate": { "alias": "maxDate"; "required": false; "isSignal": true; }; "startAt": { "alias": "startAt"; "required": false; "isSignal": true; }; "props": { "alias": "props"; "required": false; "isSignal": true; }; }, {}, never, never, true, [{ directive: typeof i1.NgForgeFieldHost; inputs: {}; outputs: {}; }]>;
|
|
175
171
|
}
|
|
176
172
|
|
|
177
|
-
/**
|
|
178
|
-
* Decorative icon addon for Ion fields.
|
|
179
|
-
*
|
|
180
|
-
* Renders `<ion-icon name="{icon}">` (Ionons). The `icon` string is the
|
|
181
|
-
* Ionons name — e.g., `'search-outline'` produces
|
|
182
|
-
* `<ion-icon name="search-outline">`.
|
|
183
|
-
*
|
|
184
|
-
* Add `ariaLabel` for icons that convey meaning; leave it omitted for
|
|
185
|
-
* purely decorative icons (will be `aria-hidden="true"`).
|
|
186
|
-
*/
|
|
173
|
+
/** Decorative icon addon for Ion fields. */
|
|
187
174
|
interface IonIconAddon extends BaseAddon {
|
|
188
175
|
readonly kind: 'ion-icon';
|
|
189
176
|
/** Ionons name (e.g., `'search-outline'`, `'close-outline'`). */
|
|
@@ -191,13 +178,9 @@ interface IonIconAddon extends BaseAddon {
|
|
|
191
178
|
/** Accessible label for icons that convey meaning. */
|
|
192
179
|
readonly ariaLabel?: DynamicText;
|
|
193
180
|
}
|
|
194
|
-
/**
|
|
195
|
-
* Ionic-supported button colour palette.
|
|
196
|
-
*/
|
|
181
|
+
/** Ionic-supported button colour palette. */
|
|
197
182
|
type IonButtonColor = 'primary' | 'secondary' | 'tertiary' | 'success' | 'warning' | 'danger' | 'light' | 'medium' | 'dark';
|
|
198
|
-
/**
|
|
199
|
-
* Ionic-supported button fill variants.
|
|
200
|
-
*/
|
|
183
|
+
/** Ionic-supported button fill variants. */
|
|
201
184
|
type IonButtonFill = 'clear' | 'outline' | 'solid' | 'default';
|
|
202
185
|
/**
|
|
203
186
|
* Common shape of every `ion-button` addon — properties that don't
|
|
@@ -216,14 +199,6 @@ interface IonButtonBase extends BaseAddon {
|
|
|
216
199
|
* Click axis — XOR enforced at type level so configurations that combine
|
|
217
200
|
* two click handlers (e.g., `preset` AND `actionRef`) are rejected by
|
|
218
201
|
* TypeScript. The four shapes:
|
|
219
|
-
*
|
|
220
|
-
* - `Preset` — built-in preset action.
|
|
221
|
-
* - `ActionRef` — typed reference to a handler registered via `provideAddonActions(...)`.
|
|
222
|
-
* - `Action` — inline handler (code-only; dropped from JSON-derived configs).
|
|
223
|
-
* - `None` — decorative button with no handler.
|
|
224
|
-
*
|
|
225
|
-
* Runtime XOR validation stays as defence-in-depth for JSON-source configs
|
|
226
|
-
* that bypass the type checker.
|
|
227
202
|
*/
|
|
228
203
|
type IonButtonClickPreset = {
|
|
229
204
|
/** Built-in preset action (e.g., `'clear'`, `'toggle-password-visibility'`). JSON-safe. */
|
|
@@ -261,13 +236,6 @@ type IonButtonClick = IonButtonClickPreset | IonButtonClickActionRef | IonButton
|
|
|
261
236
|
/**
|
|
262
237
|
* Content axis — XOR enforced at type level so an icon-only button is
|
|
263
238
|
* forced to declare `ariaLabel`. The three shapes:
|
|
264
|
-
*
|
|
265
|
-
* - `IconOnly` — just `icon`; `ariaLabel` is REQUIRED
|
|
266
|
-
* - `Labeled` — `label` set; `icon` and `ariaLabel` optional
|
|
267
|
-
* - `Decorative` — neither icon nor label (e.g., custom-rendered child)
|
|
268
|
-
*
|
|
269
|
-
* Combining keys is rejected by TypeScript — `icon: 'x'` without either
|
|
270
|
-
* `label` or `ariaLabel` doesn't satisfy any branch.
|
|
271
239
|
*/
|
|
272
240
|
type IonButtonContentIconOnly = {
|
|
273
241
|
readonly icon: string;
|
|
@@ -286,23 +254,7 @@ type IonButtonContentDecorative = {
|
|
|
286
254
|
readonly ariaLabel?: DynamicText;
|
|
287
255
|
};
|
|
288
256
|
type IonButtonContent = IonButtonContentIconOnly | IonButtonContentLabeled | IonButtonContentDecorative;
|
|
289
|
-
/**
|
|
290
|
-
* Interactive button addon for Ion fields.
|
|
291
|
-
*
|
|
292
|
-
* Renders `<ion-button>` with optional icon (via `<ion-icon>`), label,
|
|
293
|
-
* colour / fill variants, and a reactive loading state (rendered as
|
|
294
|
-
* `<ion-spinner>`).
|
|
295
|
-
*
|
|
296
|
-
* Type-level guarantees:
|
|
297
|
-
*
|
|
298
|
-
* - **Content axis (XOR):** `IconOnly` (icon + required ariaLabel) |
|
|
299
|
-
* `Labeled` (label, icon optional) | `Decorative` (neither). The IDE
|
|
300
|
-
* rejects icon-only configs that omit `ariaLabel`.
|
|
301
|
-
* - **Click axis (XOR):** exactly one of `preset` / `actionRef` / `action`,
|
|
302
|
-
* or none. Combining two is rejected by TypeScript at the call site;
|
|
303
|
-
* the runtime validator still drops the addon with a clear warning when
|
|
304
|
-
* JSON-source configs slip a multi-set past the type checker.
|
|
305
|
-
*/
|
|
257
|
+
/** Interactive button addon for Ion fields. */
|
|
306
258
|
type IonButtonAddon = IonButtonBase & IonButtonContent & IonButtonClick;
|
|
307
259
|
/** Union of all Ion-shipped addon kinds. */
|
|
308
260
|
type IonAddon = IonIconAddon | IonButtonAddon;
|
|
@@ -326,31 +278,11 @@ interface IonicInputProps extends InputProps {
|
|
|
326
278
|
/**
|
|
327
279
|
* Module-augmentable seam for adding custom addon kinds to `ion-input` at
|
|
328
280
|
* the type level. Pair with `withCustomAddon(...)` for the runtime side:
|
|
329
|
-
*
|
|
330
|
-
* ```ts
|
|
331
|
-
* declare module '@ng-forge/dynamic-forms-ionic' {
|
|
332
|
-
* interface IonAddonExtensions {
|
|
333
|
-
* 'my-rating': MyRatingAddon;
|
|
334
|
-
* }
|
|
335
|
-
* }
|
|
336
|
-
* ```
|
|
337
|
-
*
|
|
338
|
-
* Empty by default — the extension lookup resolves to `never` and contributes
|
|
339
|
-
* nothing to the union.
|
|
340
281
|
*/
|
|
341
282
|
interface IonAddonExtensions {
|
|
342
283
|
}
|
|
343
284
|
type IonAddonExtension = IonAddonExtensions[keyof IonAddonExtensions];
|
|
344
|
-
/**
|
|
345
|
-
* Addon kinds accepted by `ion-input`.
|
|
346
|
-
*
|
|
347
|
-
* Ionic-specific kinds (`ion-icon`, `ion-button`) plus the universal `text`
|
|
348
|
-
* and `template` kinds. `component` is permitted at runtime via the broader
|
|
349
|
-
* `BaseAddon` union (and dropped in JSON-derived configs by the validator)
|
|
350
|
-
* but excluded here so the IDE narrows tightly to declarative shapes.
|
|
351
|
-
*
|
|
352
|
-
* To extend with custom kinds, augment `IonAddonExtensions`.
|
|
353
|
-
*/
|
|
285
|
+
/** Addon kinds accepted by `ion-input`. */
|
|
354
286
|
type IonInputAddon = IonIconAddon | IonButtonAddon | TextAddon | TemplateAddon | IonAddonExtension;
|
|
355
287
|
type IonicInputField = InputField<IonicInputProps> & {
|
|
356
288
|
addons?: ReadonlyArray<IonInputAddon>;
|
|
@@ -374,9 +306,7 @@ declare class IonicInputFieldComponent {
|
|
|
374
306
|
private readonly typeOverride;
|
|
375
307
|
protected readonly fill: _angular_core.Signal<"outline" | "solid">;
|
|
376
308
|
protected readonly shape: _angular_core.Signal<"round" | undefined>;
|
|
377
|
-
/**
|
|
378
|
-
* Bootstrap / PrimeNG. Consumers can opt into Ionic's native inline 'start'
|
|
379
|
-
* or 'fixed' via props.labelPlacement or the IONIC_CONFIG token. */
|
|
309
|
+
/** Bootstrap / PrimeNG. Consumers can opt into Ionic's native inline 'start' */
|
|
380
310
|
protected readonly labelPlacement: _angular_core.Signal<"start" | "end" | "fixed" | "stacked" | "floating">;
|
|
381
311
|
protected readonly color: _angular_core.Signal<"primary" | "secondary" | "tertiary" | "success" | "warning" | "danger" | "light" | "medium" | "dark" | undefined>;
|
|
382
312
|
/** Override (set by `toggle-password-visibility` preset) wins over `props().type`. */
|
|
@@ -551,65 +481,53 @@ declare class IonicToggleFieldComponent {
|
|
|
551
481
|
*/
|
|
552
482
|
declare const IONIC_FIELD_TYPES: FieldTypeDefinition[];
|
|
553
483
|
|
|
554
|
-
/**
|
|
555
|
-
* Configuration options for Ionic form fields.
|
|
556
|
-
*
|
|
557
|
-
* These settings can be applied at two levels:
|
|
558
|
-
* - **Library-level**: Via `withIonicFields({ ... })` - applies to all forms
|
|
559
|
-
* - **Form-level**: Via `defaultProps` in form config - applies to a specific form
|
|
560
|
-
*
|
|
561
|
-
* The cascade hierarchy is: Library-level → Form-level → Field-level
|
|
562
|
-
*
|
|
563
|
-
* @example
|
|
564
|
-
* ```typescript
|
|
565
|
-
* // Library-level (in app config)
|
|
566
|
-
* provideDynamicForms(withIonicFields({ fill: 'outline', labelPlacement: 'floating' }))
|
|
567
|
-
*
|
|
568
|
-
* // Form-level (in form config)
|
|
569
|
-
* const config: IonicFormConfig = {
|
|
570
|
-
* defaultProps: { color: 'tertiary' },
|
|
571
|
-
* fields: [...]
|
|
572
|
-
* };
|
|
573
|
-
* ```
|
|
574
|
-
*/
|
|
484
|
+
/** Configuration options for Ionic form fields. */
|
|
575
485
|
interface IonicConfig {
|
|
576
486
|
/**
|
|
577
487
|
* Default fill style for form inputs
|
|
488
|
+
*
|
|
578
489
|
* @default 'solid'
|
|
579
490
|
*/
|
|
580
491
|
fill?: 'solid' | 'outline';
|
|
581
492
|
/**
|
|
582
493
|
* Default shape for form controls
|
|
494
|
+
*
|
|
583
495
|
* @default undefined
|
|
584
496
|
*/
|
|
585
497
|
shape?: 'round';
|
|
586
498
|
/**
|
|
587
499
|
* Default label placement for form inputs
|
|
500
|
+
*
|
|
588
501
|
* @default 'start'
|
|
589
502
|
*/
|
|
590
503
|
labelPlacement?: 'start' | 'end' | 'fixed' | 'stacked' | 'floating';
|
|
591
504
|
/**
|
|
592
505
|
* Default color theme for form controls
|
|
506
|
+
*
|
|
593
507
|
* @default 'primary'
|
|
594
508
|
*/
|
|
595
509
|
color?: 'primary' | 'secondary' | 'tertiary' | 'success' | 'warning' | 'danger' | 'light' | 'medium' | 'dark';
|
|
596
510
|
/**
|
|
597
511
|
* Default size for buttons
|
|
512
|
+
*
|
|
598
513
|
* @default 'default'
|
|
599
514
|
*/
|
|
600
515
|
size?: 'small' | 'default' | 'large';
|
|
601
516
|
/**
|
|
602
517
|
* Default expand behavior for buttons
|
|
518
|
+
*
|
|
603
519
|
* @default undefined
|
|
604
520
|
*/
|
|
605
521
|
expand?: 'full' | 'block';
|
|
606
522
|
/**
|
|
607
523
|
* Default fill style for buttons (overrides general fill if set)
|
|
524
|
+
*
|
|
608
525
|
* @default 'solid'
|
|
609
526
|
*/
|
|
610
527
|
buttonFill?: 'clear' | 'outline' | 'solid' | 'default';
|
|
611
528
|
/**
|
|
612
529
|
* Whether buttons should be strong (bold) by default
|
|
530
|
+
*
|
|
613
531
|
* @default false
|
|
614
532
|
*/
|
|
615
533
|
strong?: boolean;
|
|
@@ -618,23 +536,6 @@ interface IonicConfig {
|
|
|
618
536
|
/**
|
|
619
537
|
* Injection token for Ionic form configuration.
|
|
620
538
|
* Use this to provide global configuration for Ionic form fields.
|
|
621
|
-
*
|
|
622
|
-
* @example
|
|
623
|
-
* ```typescript
|
|
624
|
-
* import { provideDynamicForm } from '@ng-forge/dynamic-forms';
|
|
625
|
-
* import { withIonicFields } from '@ng-forge/dynamic-forms-ionic';
|
|
626
|
-
*
|
|
627
|
-
* export const appConfig: ApplicationConfig = {
|
|
628
|
-
* providers: [
|
|
629
|
-
* provideDynamicForm(
|
|
630
|
-
* ...withIonicFields({
|
|
631
|
-
* fill: 'outline',
|
|
632
|
-
* labelPlacement: 'floating'
|
|
633
|
-
* })
|
|
634
|
-
* )
|
|
635
|
-
* ]
|
|
636
|
-
* };
|
|
637
|
-
* ```
|
|
638
539
|
*/
|
|
639
540
|
declare const IONIC_CONFIG: InjectionToken<IonicConfig>;
|
|
640
541
|
|
|
@@ -665,50 +566,9 @@ declare const IonicField: {
|
|
|
665
566
|
};
|
|
666
567
|
type IonicFieldType = (typeof IonicField)[keyof typeof IonicField];
|
|
667
568
|
|
|
668
|
-
/**
|
|
669
|
-
* Ionic-specific props that can be set at form level and cascade to all fields.
|
|
670
|
-
*
|
|
671
|
-
* This is the same type as `IonicConfig` used in `withIonicFields()`.
|
|
672
|
-
* Using a single type ensures consistency between library-level and form-level configuration.
|
|
673
|
-
*
|
|
674
|
-
* The cascade hierarchy is: Library-level → Form-level → Field-level
|
|
675
|
-
*
|
|
676
|
-
* @example
|
|
677
|
-
* ```typescript
|
|
678
|
-
* const config: IonicFormConfig = {
|
|
679
|
-
* defaultProps: {
|
|
680
|
-
* fill: 'outline',
|
|
681
|
-
* labelPlacement: 'floating',
|
|
682
|
-
* color: 'tertiary',
|
|
683
|
-
* },
|
|
684
|
-
* fields: [
|
|
685
|
-
* { type: 'ionic-input', key: 'name', label: 'Name' },
|
|
686
|
-
* ],
|
|
687
|
-
* };
|
|
688
|
-
* ```
|
|
689
|
-
*/
|
|
569
|
+
/** Ionic-specific props that can be set at form level and cascade to all fields. */
|
|
690
570
|
type IonicFormProps = IonicConfig;
|
|
691
|
-
/**
|
|
692
|
-
* Ionic-specific FormConfig with type-safe defaultProps.
|
|
693
|
-
*
|
|
694
|
-
* Use this type alias when defining form configurations with Ionic components
|
|
695
|
-
* to get IDE autocomplete and type checking for `defaultProps`.
|
|
696
|
-
*
|
|
697
|
-
* @example
|
|
698
|
-
* ```typescript
|
|
699
|
-
* const formConfig: IonicFormConfig = {
|
|
700
|
-
* defaultProps: {
|
|
701
|
-
* fill: 'outline',
|
|
702
|
-
* labelPlacement: 'floating',
|
|
703
|
-
* color: 'primary',
|
|
704
|
-
* },
|
|
705
|
-
* fields: [
|
|
706
|
-
* { type: 'ionic-input', key: 'name', label: 'Name' }, // Uses form defaultProps
|
|
707
|
-
* { type: 'ionic-input', key: 'email', props: { fill: 'solid' } }, // Override
|
|
708
|
-
* ],
|
|
709
|
-
* };
|
|
710
|
-
* ```
|
|
711
|
-
*/
|
|
571
|
+
/** Ionic-specific FormConfig with type-safe defaultProps. */
|
|
712
572
|
type IonicFormConfig<TFields extends NarrowFields | RegisteredFieldTypes[] = RegisteredFieldTypes[], TValue = InferFormValue<TFields extends readonly RegisteredFieldTypes[] ? TFields : RegisteredFieldTypes[]>> = FormConfig<TFields, TValue, IonicFormProps>;
|
|
713
573
|
|
|
714
574
|
/**
|
|
@@ -741,9 +601,7 @@ declare module '@ng-forge/dynamic-forms' {
|
|
|
741
601
|
}
|
|
742
602
|
}
|
|
743
603
|
|
|
744
|
-
/**
|
|
745
|
-
* Field type definitions for Ionic components.
|
|
746
|
-
*/
|
|
604
|
+
/** Field type definitions for Ionic components. */
|
|
747
605
|
type IonicFieldTypes = FieldTypeDefinition[];
|
|
748
606
|
type IonicConfigFeature = {
|
|
749
607
|
ɵkind: 'ionic-config';
|
|
@@ -760,42 +618,7 @@ type IonicFieldsWithConfig = [...IonicFieldTypes, IonicAddonsFeature, IonicConfi
|
|
|
760
618
|
* with Ionic-shipped addon kinds (`ion-icon`, `ion-button`) auto-included
|
|
761
619
|
* so addons work out of the box.
|
|
762
620
|
*
|
|
763
|
-
* If you want field types WITHOUT addons (rare), pass them through
|
|
764
|
-
* `provideDynamicForm` directly and skip this helper. If you want addons
|
|
765
|
-
* WITHOUT the field types (also rare — e.g., adding addons to a form that
|
|
766
|
-
* uses custom fields), call `withIonicAddons()` standalone.
|
|
767
|
-
*
|
|
768
621
|
* @param config - Optional global configuration for Ionic form fields
|
|
769
|
-
*
|
|
770
|
-
* @example
|
|
771
|
-
* ```typescript
|
|
772
|
-
* // Application-level setup — addons (ion-icon, ion-button) ship in automatically
|
|
773
|
-
* import { ApplicationConfig } from '@angular/core';
|
|
774
|
-
* import { provideDynamicForm } from '@ng-forge/dynamic-forms';
|
|
775
|
-
* import { withIonicFields } from '@ng-forge/dynamic-forms-ionic';
|
|
776
|
-
*
|
|
777
|
-
* export const appConfig: ApplicationConfig = {
|
|
778
|
-
* providers: [
|
|
779
|
-
* provideDynamicForm(...withIonicFields())
|
|
780
|
-
* ]
|
|
781
|
-
* };
|
|
782
|
-
* ```
|
|
783
|
-
*
|
|
784
|
-
* @example
|
|
785
|
-
* ```typescript
|
|
786
|
-
* // With global configuration
|
|
787
|
-
* export const appConfig: ApplicationConfig = {
|
|
788
|
-
* providers: [
|
|
789
|
-
* provideDynamicForm(
|
|
790
|
-
* ...withIonicFields({
|
|
791
|
-
* fill: 'outline',
|
|
792
|
-
* labelPlacement: 'floating'
|
|
793
|
-
* })
|
|
794
|
-
* )
|
|
795
|
-
* ]
|
|
796
|
-
* };
|
|
797
|
-
* ```
|
|
798
|
-
*
|
|
799
622
|
* @returns Tuple of field type definitions, the addons feature, and
|
|
800
623
|
* optionally a config feature.
|
|
801
624
|
*/
|
|
@@ -811,38 +634,10 @@ type IonicAddonsFeature = {
|
|
|
811
634
|
ɵkind: 'addons';
|
|
812
635
|
ɵproviders: Provider[];
|
|
813
636
|
};
|
|
814
|
-
/**
|
|
815
|
-
* Register Ionic-shipped addon kinds (`ion-icon`, `ion-button`) standalone.
|
|
816
|
-
*
|
|
817
|
-
* **Most users don't need this** — `withIonicFields()` auto-includes
|
|
818
|
-
* these kinds. Call `withIonicAddons()` directly only when you want
|
|
819
|
-
* Ionic addon kinds without the Ionic field types (e.g., a custom
|
|
820
|
-
* field set that wants to render `ion-icon` prefixes), or when you're
|
|
821
|
-
* stitching addons through a different DI scope.
|
|
822
|
-
*
|
|
823
|
-
* @example
|
|
824
|
-
* ```typescript
|
|
825
|
-
* // Custom field types + Ionic addon kinds.
|
|
826
|
-
* provideDynamicForm(
|
|
827
|
-
* ...myCustomFields(),
|
|
828
|
-
* withIonicAddons(),
|
|
829
|
-
* );
|
|
830
|
-
* ```
|
|
831
|
-
*
|
|
832
|
-
* Adapter authors who need to override a kind with a customised renderer
|
|
833
|
-
* should call `withCustomAddon(...)` directly instead.
|
|
834
|
-
*/
|
|
637
|
+
/** Register Ionic-shipped addon kinds (`ion-icon`, `ion-button`) standalone. */
|
|
835
638
|
declare function withIonicAddons(): IonicAddonsFeature;
|
|
836
639
|
|
|
837
|
-
/**
|
|
838
|
-
* Renderer for the `ion-button` addon kind.
|
|
839
|
-
*
|
|
840
|
-
* Wraps Ionic's `<ion-button>`. Click dispatch (preset / actionRef /
|
|
841
|
-
* action precedence, multi-set warning, `disabled` / `loading`
|
|
842
|
-
* resolution) lives on `NgForgeAddonAction`; this component focuses on
|
|
843
|
-
* the visual layer. While `action.loading()`, an `<ion-spinner>` is
|
|
844
|
-
* rendered in place of the icon and the button is disabled.
|
|
845
|
-
*/
|
|
640
|
+
/** Renderer for the `ion-button` addon kind. */
|
|
846
641
|
declare class IonButtonAddonComponent {
|
|
847
642
|
protected readonly action: i1.TypedNgForgeAddonAction<IonButtonAddon>;
|
|
848
643
|
/** Re-exposed for template binding — same signal stored on the directive. */
|
|
@@ -865,13 +660,7 @@ declare class IonButtonAddonComponent {
|
|
|
865
660
|
static ɵcmp: _angular_core.ɵɵComponentDeclaration<IonButtonAddonComponent, "df-ion-button-addon", never, {}, {}, never, never, true, [{ directive: typeof i1.NgForgeAddonAction; inputs: {}; outputs: {}; }]>;
|
|
866
661
|
}
|
|
867
662
|
|
|
868
|
-
/**
|
|
869
|
-
* Renderer for the `ion-icon` addon kind.
|
|
870
|
-
*
|
|
871
|
-
* Outputs `<ion-icon [name]="icon">`. The host is set `aria-hidden="true"`
|
|
872
|
-
* by default; if the addon supplies an `ariaLabel`, it is applied so the
|
|
873
|
-
* icon is announced by screen readers.
|
|
874
|
-
*/
|
|
663
|
+
/** Renderer for the `ion-icon` addon kind. */
|
|
875
664
|
declare class IonIconAddonComponent {
|
|
876
665
|
readonly addon: _angular_core.InputSignal<IonIconAddon>;
|
|
877
666
|
/** Accepted for contract uniformity — `NgComponentOutlet` setInput is strict; every kind must declare it. */
|
|
@@ -883,17 +672,7 @@ declare class IonIconAddonComponent {
|
|
|
883
672
|
static ɵcmp: _angular_core.ɵɵComponentDeclaration<IonIconAddonComponent, "df-ion-icon-addon", never, { "addon": { "alias": "addon"; "required": true; "isSignal": true; }; "fieldInputs": { "alias": "fieldInputs"; "required": false; "isSignal": true; }; }, {}, never, never, true, never>;
|
|
884
673
|
}
|
|
885
674
|
|
|
886
|
-
/**
|
|
887
|
-
* Per-field writable signal that overrides the input's `type` attribute.
|
|
888
|
-
*
|
|
889
|
-
* Provided at the `ion-input` field component level. The button addon's
|
|
890
|
-
* `'toggle-password-visibility'` preset writes to it; the field component
|
|
891
|
-
* reads it to compute its effective `type`.
|
|
892
|
-
*
|
|
893
|
-
* Optional from a button's perspective — when the button is hosted inside a
|
|
894
|
-
* field that doesn't provide this token (e.g., textarea or a future
|
|
895
|
-
* non-input field), the toggle preset is a no-op.
|
|
896
|
-
*/
|
|
675
|
+
/** Per-field writable signal that overrides the input's `type` attribute. */
|
|
897
676
|
declare const IONIC_INPUT_TYPE_OVERRIDE: InjectionToken<WritableSignal<string | undefined>>;
|
|
898
677
|
|
|
899
678
|
export { IONIC_CONFIG, IONIC_FIELD_TYPES, IONIC_INPUT_TYPE_OVERRIDE, IonButtonAddonComponent, IonIconAddonComponent, IonicButtonFieldComponent, IonicCheckboxFieldComponent, IonicDatepickerFieldComponent, IonicField, IonicInputFieldComponent, IonicMultiCheckboxFieldComponent, IonicRadioFieldComponent, IonicSelectFieldComponent, IonicSliderFieldComponent, IonicTextareaFieldComponent, IonicToggleFieldComponent, withIonicAddons, withIonicFields };
|