@ng-forge/dynamic-forms-ionic 1.0.0-rc.1 → 1.0.0-rc.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.
|
@@ -3,10 +3,10 @@ import { input, computed, ChangeDetectionStrategy, Component, signal, inject, In
|
|
|
3
3
|
import { IonButton, IonCheckbox, IonNote, IonInput, IonModal, IonDatetime, IonButtons, IonHeader, IonContent, IonToolbar, IonTitle, IonIcon, IonSpinner, IonItem, IonRadioGroup, IonRadio, IonSelect, IonSelectOption, IonRange, IonTextarea, IonToggle } from '@ionic/angular/standalone';
|
|
4
4
|
import { AsyncPipe } from '@angular/common';
|
|
5
5
|
import * as i1 from '@ng-forge/dynamic-forms/integration';
|
|
6
|
-
import { injectNgForgeAction, DynamicTextPipe, NgForgeActionHost, injectNgForgeField, NgForgeControl, NgForgeFieldHost, runPresetAction, injectNgForgeAddonAction, resolveDynamicValue, NgForgeAddonAction, injectNgForgeAddons, ADDON_PRESET_HANDLER, FIELD_SIGNAL_CONTEXT, NgForgeAddons, isEqual, NgForgeHostControl, DEFAULT_PROPS, ARRAY_CONTEXT, buildBaseInputs, valueFieldMapper, optionsFieldMapper, checkboxFieldMapper, submitButtonFieldMapper, nextButtonFieldMapper, previousButtonFieldMapper, addArrayItemButtonMapper, prependArrayItemButtonMapper, insertArrayItemButtonMapper, removeArrayItemButtonMapper, popArrayItemButtonMapper, shiftArrayItemButtonMapper, datepickerFieldMapper,
|
|
6
|
+
import { injectNgForgeAction, DynamicTextPipe, NgForgeActionHost, injectNgForgeField, NgForgeControl, NgForgeFieldHost, runPresetAction, injectNgForgeAddonAction, resolveDynamicValue, NgForgeAddonAction, injectNgForgeAddons, DfAddonSlot, ADDON_PRESET_HANDLER, FIELD_SIGNAL_CONTEXT, NgForgeAddons, isEqual, NgForgeHostControl, DEFAULT_PROPS, ARRAY_CONTEXT, buildBaseInputs, valueFieldMapper, optionsFieldMapper, checkboxFieldMapper, submitButtonFieldMapper, nextButtonFieldMapper, previousButtonFieldMapper, addArrayItemButtonMapper, prependArrayItemButtonMapper, insertArrayItemButtonMapper, removeArrayItemButtonMapper, popArrayItemButtonMapper, shiftArrayItemButtonMapper, datepickerFieldMapper, ADDON_TYPE_DEFINITIONS } from '@ng-forge/dynamic-forms/integration';
|
|
7
7
|
import { FormField } from '@angular/forms/signals';
|
|
8
8
|
import { format } from 'date-fns';
|
|
9
|
-
import {
|
|
9
|
+
import { DynamicFormLogger, DynamicFormError } from '@ng-forge/dynamic-forms';
|
|
10
10
|
import { explicitEffect } from 'ngxtension/explicit-effect';
|
|
11
11
|
|
|
12
12
|
class IonicButtonFieldComponent {
|
|
@@ -416,13 +416,13 @@ class IonicInputFieldComponent {
|
|
|
416
416
|
...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
|
|
417
417
|
type = computed(() => this.typeOverride() ?? this.props()?.type ?? 'text', /* @ts-ignore */
|
|
418
418
|
...(ngDevMode ? [{ debugName: "type" }] : /* istanbul ignore next */ []));
|
|
419
|
-
buttonPrefixAddons = computed(() => this.ngfa.prefixAddons().filter((a) => a.
|
|
419
|
+
buttonPrefixAddons = computed(() => this.ngfa.prefixAddons().filter((a) => a.type === 'ion-button'), /* @ts-ignore */
|
|
420
420
|
...(ngDevMode ? [{ debugName: "buttonPrefixAddons" }] : /* istanbul ignore next */ []));
|
|
421
|
-
buttonSuffixAddons = computed(() => this.ngfa.suffixAddons().filter((a) => a.
|
|
421
|
+
buttonSuffixAddons = computed(() => this.ngfa.suffixAddons().filter((a) => a.type === 'ion-button'), /* @ts-ignore */
|
|
422
422
|
...(ngDevMode ? [{ debugName: "buttonSuffixAddons" }] : /* istanbul ignore next */ []));
|
|
423
|
-
decorativePrefixAddons = computed(() => this.ngfa.prefixAddons().filter((a) => a.
|
|
423
|
+
decorativePrefixAddons = computed(() => this.ngfa.prefixAddons().filter((a) => a.type !== 'ion-button'), /* @ts-ignore */
|
|
424
424
|
...(ngDevMode ? [{ debugName: "decorativePrefixAddons" }] : /* istanbul ignore next */ []));
|
|
425
|
-
decorativeSuffixAddons = computed(() => this.ngfa.suffixAddons().filter((a) => a.
|
|
425
|
+
decorativeSuffixAddons = computed(() => this.ngfa.suffixAddons().filter((a) => a.type !== 'ion-button'), /* @ts-ignore */
|
|
426
426
|
...(ngDevMode ? [{ debugName: "decorativeSuffixAddons" }] : /* istanbul ignore next */ []));
|
|
427
427
|
destroyed = false;
|
|
428
428
|
constructor() {
|
|
@@ -1421,26 +1421,26 @@ function withIonicFields(config) {
|
|
|
1421
1421
|
}
|
|
1422
1422
|
return base;
|
|
1423
1423
|
}
|
|
1424
|
-
/* -- Ionic addon
|
|
1424
|
+
/* -- Ionic addon types ------------------------------------------------- */
|
|
1425
1425
|
const ION_ICON_KIND = {
|
|
1426
|
-
|
|
1426
|
+
type: 'ion-icon',
|
|
1427
1427
|
loadComponent: () => Promise.resolve().then(function () { return ionIconAddon_component; }).then((m) => m.IonIconAddonComponent),
|
|
1428
1428
|
validate: (addon, fieldKey) => {
|
|
1429
1429
|
if (typeof addon.icon !== 'string' || addon.icon.length === 0) {
|
|
1430
|
-
throw new DynamicFormError(`Addon
|
|
1430
|
+
throw new DynamicFormError(`Addon type 'ion-icon' requires a non-empty 'icon' string (field: '${fieldKey}').`);
|
|
1431
1431
|
}
|
|
1432
1432
|
},
|
|
1433
1433
|
};
|
|
1434
1434
|
const ION_BUTTON_KIND = {
|
|
1435
|
-
|
|
1435
|
+
type: 'ion-button',
|
|
1436
1436
|
loadComponent: () => Promise.resolve().then(function () { return ionButtonAddon_component; }).then((m) => m.IonButtonAddonComponent),
|
|
1437
1437
|
validate: (addon, fieldKey) => {
|
|
1438
1438
|
const set = [addon.preset, addon.actionRef, addon.action].filter((v) => v !== undefined);
|
|
1439
1439
|
if (set.length > 1) {
|
|
1440
|
-
throw new DynamicFormError(`Addon
|
|
1440
|
+
throw new DynamicFormError(`Addon type 'ion-button' on field '${fieldKey}' has more than one of preset/actionRef/action — exactly one allowed.`);
|
|
1441
1441
|
}
|
|
1442
1442
|
if (addon.icon && !addon.label && !addon.ariaLabel) {
|
|
1443
|
-
throw new DynamicFormError(`Addon
|
|
1443
|
+
throw new DynamicFormError(`Addon type 'ion-button' on field '${fieldKey}' is icon-only — provide 'ariaLabel' for accessibility.`);
|
|
1444
1444
|
}
|
|
1445
1445
|
},
|
|
1446
1446
|
};
|
|
@@ -1448,8 +1448,8 @@ function withIonicAddons() {
|
|
|
1448
1448
|
return {
|
|
1449
1449
|
ɵkind: 'addons',
|
|
1450
1450
|
ɵproviders: [
|
|
1451
|
-
{ provide:
|
|
1452
|
-
{ provide:
|
|
1451
|
+
{ provide: ADDON_TYPE_DEFINITIONS, useValue: ION_ICON_KIND, multi: true },
|
|
1452
|
+
{ provide: ADDON_TYPE_DEFINITIONS, useValue: ION_BUTTON_KIND, multi: true },
|
|
1453
1453
|
],
|
|
1454
1454
|
};
|
|
1455
1455
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ng-forge-dynamic-forms-ionic.mjs","mappings":";;;;;;;;;;;AAyCc;;AAGH;;AAEA;;AAEA;;;AAGP;;AACA;;;AAXiB;;;;;;;;;;;;;;;;;;;AAHlB;;;;AA7BS;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACwCE;;AAGH;;;AAHU;;;;;;;;;;;;;;;;;;;;;;;;;;AAxCT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACgGE;;;;;;;;AAMH;;;;;AAKP;;;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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACRV;AACF;;;AC6CmB;;AAGE;;;;;;;;;AAKA;;AACA;;;;AAKA;;AAEnB;AACE;AACE;AACE;;;AAGF;AACF;;;AAxBS;;;;;;;;;;AA3BV;;;;AAbS;;;;;;;;;;;wBA+BR;AACA;AACA;AACA;AACA;AACA;;;;;;;;ACwGU;AACK;;;;AAMR;;AAET,IAKS;;IAGQ;AAEE;;AACA;;IAEA;;AACA;;IAGA;;AAEnB;;;;;;;;;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;;;;;;;;AC9BW;;;;AAIH;;AAET;AAEI;;AAEF;IAKO;;;;;AAKP;;;AAGF;AACE;AACE;AAEA;AACE;;AAEJ;AAEA;AACE;AACA;;;AAIE;;AAEJ;;;;;AAMI;;;AAEA;;AAEJ;;;AAlDiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAxET;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC6DE;;;;AAIH;;AAET;;AANmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAtBlB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACQW;;;;AAIH;;AAET;;AANmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAXlB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACMW;;AAGH;;AACA;;;;AAJU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA3CT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACyCE;AACK;;AAIR;;AAET;wBAIE;;AAEI;AACA;;AAEI;;;AAEE;;;AAEA;;AAEJ;;AAEJ;AACF;;;AAzBiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAxCT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QC6DS;;IAGA;AACjB;;;AAGF,IAImB;;;;gBASnB;;;;AAME;;;AA7DS;;;;;;;;;;;;;;;;;;;;AAzBD;;;;;;;;;;;;;;;;;AAqBR;;;;ACiBU;;AAGH;;;AAHU;;;;;;;;;;;;;;;;;;;;;;;;;AAvCT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACNL;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;ACXI;AACJ;AACA;;;AAKE;AACE;;AAGF;AACE;;AAGF;AACE;;;AAKA;;;AAKA;;QAIF;AACA;;AAGI;;AACA;AACF;;;;AAKE;AACA;;;;AAKJ;AACF;AACF;;;AC5CA;;;AAIA;AACE;AACA;;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;;;;AC1HG;;;;AAOA;;AAE4B;AAC9B;;AAGF;AACF;AAEA;AAEA;AACE;AACA;AACA;AACE;AACE;;;;AAKN;AACE;AACA;AACA;;AAIE;AACE;;QAKF;AACE;;;;;;AAkBF;AACA;;;AAGC;;AAEL;;;;IC/CqB;;;;;;;AAKnB;;AAKmB;;AACnB,IAImB;;;AAnBR;;;;;;;;;;;;;;;;;;;;;;;;;AA1CD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ICWD;;;;;;AAIU;;;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 { FormEvent } from '@ng-forge/dynamic-forms';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\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/integration';\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/integration';\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 } from '@ng-forge/dynamic-forms';\nimport type { PresetCollaborators } from '@ng-forge/dynamic-forms/integration';\nimport { runPresetAction } from '@ng-forge/dynamic-forms/integration';\n\nexport type { PresetCollaborators };\n\n/** 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 } from '@ng-forge/dynamic-forms';\nimport { DynamicTextPipe, resolveDynamicValue } from '@ng-forge/dynamic-forms/integration';\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 { AddonActionContext, AddonActionPreset, DfAddonSlot, DynamicFormLogger } from '@ng-forge/dynamic-forms';\nimport { WrapperFieldInputs } from '@ng-forge/dynamic-forms/integration';\nimport { DynamicTextPipe, FIELD_SIGNAL_CONTEXT } from '@ng-forge/dynamic-forms/integration';\nimport {\n ADDON_PRESET_HANDLER,\n AddonPresetHandler,\n injectNgForgeAddons,\n injectNgForgeField,\n NgForgeAddons,\n NgForgeControl,\n NgForgeFieldHost,\n} from '@ng-forge/dynamic-forms/integration';\nimport { 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 { FieldOption, ValueType } from '@ng-forge/dynamic-forms';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\nimport { injectNgForgeField, NgForgeFieldHost, isEqual, NgForgeControl } from '@ng-forge/dynamic-forms/integration';\nimport { explicitEffect } from 'ngxtension/explicit-effect';\nimport { 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 { FieldOption, ValueType } from '@ng-forge/dynamic-forms';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\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 { FieldOption, ValueType } from '@ng-forge/dynamic-forms';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\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/integration';\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/integration';\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/integration';\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 { FieldDef } from '@ng-forge/dynamic-forms';\nimport { ARRAY_CONTEXT, buildBaseInputs, DEFAULT_PROPS } from '@ng-forge/dynamic-forms/integration';\n\n/**\n * Generic button mapper for custom events or basic buttons.\n * For specific button types (submit, next, prev, add/remove array items),\n * use the dedicated field types and their specific mappers.\n *\n * @param fieldDef The button field definition\n * @returns Signal containing Record of input names to values for ngComponentOutlet\n */\nexport function buttonFieldMapper(fieldDef: FieldDef<Record<string, unknown>>): Signal<Record<string, unknown>> {\n const defaultProps = inject(DEFAULT_PROPS);\n const arrayContext = inject(ARRAY_CONTEXT, { optional: true });\n\n return computed(() => {\n const baseInputs = buildBaseInputs(fieldDef, defaultProps());\n\n const inputs: Record<string, unknown> = {\n ...baseInputs,\n };\n\n if (fieldDef.disabled !== undefined) {\n inputs['disabled'] = fieldDef.disabled;\n }\n\n if (fieldDef.hidden !== undefined) {\n inputs['hidden'] = fieldDef.hidden;\n }\n\n // Add event binding for button events\n if ('event' in fieldDef && fieldDef.event !== undefined) {\n inputs['event'] = fieldDef.event;\n }\n\n // Add eventArgs binding if provided\n if ('eventArgs' in fieldDef && fieldDef.eventArgs !== undefined) {\n inputs['eventArgs'] = fieldDef.eventArgs;\n }\n\n // Add eventContext for token resolution (supports $template, $arrayKey, $index, etc.)\n const template = 'template' in fieldDef ? fieldDef.template : undefined;\n if (template || arrayContext) {\n // Read signal value if index is a signal (supports differential updates)\n const getIndex = () => {\n if (!arrayContext) return -1;\n return isSignal(arrayContext.index) ? arrayContext.index() : arrayContext.index;\n };\n\n inputs['eventContext'] = {\n key: fieldDef.key,\n index: getIndex(),\n arrayKey: arrayContext?.arrayKey ?? '',\n formValue: arrayContext?.formValue ?? {},\n template,\n };\n }\n\n return inputs;\n });\n}\n","import { FieldTypeDefinition } from '@ng-forge/dynamic-forms/integration';\nimport {\n addArrayItemButtonMapper,\n checkboxFieldMapper,\n datepickerFieldMapper,\n insertArrayItemButtonMapper,\n optionsFieldMapper,\n popArrayItemButtonMapper,\n prependArrayItemButtonMapper,\n removeArrayItemButtonMapper,\n shiftArrayItemButtonMapper,\n valueFieldMapper,\n} from '@ng-forge/dynamic-forms/integration';\nimport { 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 { DynamicFormError, type AddonKindDefinition } from '@ng-forge/dynamic-forms';\nimport { ADDON_KIND_DEFINITIONS, type FieldTypeDefinition } from '@ng-forge/dynamic-forms/integration';\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/integration';\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 { WrapperFieldInputs } from '@ng-forge/dynamic-forms/integration';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\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"]}
|
|
1
|
+
{"version":3,"file":"ng-forge-dynamic-forms-ionic.mjs","mappings":";;;;;;;;;;;AAyCc;;AAGH;;AAEA;;AAEA;;;AAGP;;AACA;;;AAXiB;;;;;;;;;;;;;;;;;;;AAHlB;;;;AA7BS;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACwCE;;AAGH;;;AAHU;;;;;;;;;;;;;;;;;;;;;;;;;;AAxCT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACgGE;;;;;;;;AAMH;;;;;AAKP;;;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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACRV;AACF;;;AC6CmB;;AAGE;;;;;;;;;AAKA;;AACA;;;;AAKA;;AAEnB;AACE;AACE;AACE;;;AAGF;AACF;;;AAxBS;;;;;;;;;;AA3BV;;;;AAbS;;;;;;;;;;;wBA+BR;AACA;AACA;AACA;AACA;AACA;;;;;;;;ACyGU;AACK;;;;AAMR;;AAET,IAKS;;IAGQ;AAEE;;AACA;;IAEA;;AACA;;IAGA;;AAEnB;;;;;;;;;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;;;;;;;;AC/BW;;;;AAIH;;AAET;AAEI;;AAEF;IAKO;;;;;AAKP;;;AAGF;AACE;AACE;AAEA;AACE;;AAEJ;AAEA;AACE;AACA;;;AAIE;;AAEJ;;;;;AAMI;;;AAEA;;AAEJ;;;AAlDiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAxET;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC6DE;;;;AAIH;;AAET;;AANmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAtBlB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACQW;;;;AAIH;;AAET;;AANmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAXlB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACMW;;AAGH;;AACA;;;;AAJU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA3CT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACyCE;AACK;;AAIR;;AAET;wBAIE;;AAEI;AACA;;AAEI;;;AAEE;;;AAEA;;AAEJ;;AAEJ;AACF;;;AAzBiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAxCT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;QC6DS;;IAGA;AACjB;;;AAGF,IAImB;;;;gBASnB;;;;AAME;;;AA7DS;;;;;;;;;;;;;;;;;;;;AAzBD;;;;;;;;;;;;;;;;;AAqBR;;;;ACiBU;;AAGH;;;AAHU;;;;;;;;;;;;;;;;;;;;;;;;;AAvCT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACNL;AACL;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;ACXI;AACJ;AACA;;;AAKE;AACE;;AAGF;AACE;;AAGF;AACE;;;AAKA;;;AAKA;;QAIF;AACA;;AAGI;;AACA;AACF;;;;AAKE;AACA;;;;AAKJ;AACF;AACF;;;AC5CA;;;AAIA;AACE;AACA;;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;;;;AC1HG;;;;AAOA;;AAE4B;AAC9B;;AAGF;AACF;AAEA;AAEA;AACE;AACA;AACA;AACE;AACE;;;;AAKN;AACE;AACA;AACA;;AAIE;AACE;;QAKF;AACE;;;;;;AAkBF;AACA;;;AAGC;;AAEL;;;;IC/CqB;;;;;;;AAKnB;;AAKmB;;AACnB,IAImB;;;AAnBR;;;;;;;;;;;;;;;;;;;;;;;;;AA1CD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ICWD;;;;;;AAIU;;;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 { FormEvent } from '@ng-forge/dynamic-forms';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\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/integration';\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/integration';\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 } from '@ng-forge/dynamic-forms';\nimport type { PresetCollaborators } from '@ng-forge/dynamic-forms/integration';\nimport { runPresetAction } from '@ng-forge/dynamic-forms/integration';\n\nexport type { PresetCollaborators };\n\n/** 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 } from '@ng-forge/dynamic-forms';\nimport { DynamicTextPipe, resolveDynamicValue } from '@ng-forge/dynamic-forms/integration';\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 { AddonActionContext, AddonActionPreset, DynamicFormLogger } from '@ng-forge/dynamic-forms';\nimport { DfAddonSlot } from '@ng-forge/dynamic-forms/integration';\nimport { WrapperFieldInputs } from '@ng-forge/dynamic-forms/integration';\nimport { DynamicTextPipe, FIELD_SIGNAL_CONTEXT } from '@ng-forge/dynamic-forms/integration';\nimport {\n ADDON_PRESET_HANDLER,\n AddonPresetHandler,\n injectNgForgeAddons,\n injectNgForgeField,\n NgForgeAddons,\n NgForgeControl,\n NgForgeFieldHost,\n} from '@ng-forge/dynamic-forms/integration';\nimport { 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-type 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 types) 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.type === 'ion-button'));\n protected readonly buttonSuffixAddons = computed(() => this.ngfa.suffixAddons().filter((a) => a.type === 'ion-button'));\n protected readonly decorativePrefixAddons = computed(() => this.ngfa.prefixAddons().filter((a) => a.type !== 'ion-button'));\n protected readonly decorativeSuffixAddons = computed(() => this.ngfa.suffixAddons().filter((a) => a.type !== '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 { FieldOption, ValueType } from '@ng-forge/dynamic-forms';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\nimport { injectNgForgeField, NgForgeFieldHost, isEqual, NgForgeControl } from '@ng-forge/dynamic-forms/integration';\nimport { explicitEffect } from 'ngxtension/explicit-effect';\nimport { 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 { FieldOption, ValueType } from '@ng-forge/dynamic-forms';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\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 { FieldOption, ValueType } from '@ng-forge/dynamic-forms';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\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/integration';\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/integration';\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/integration';\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 { FieldDef } from '@ng-forge/dynamic-forms';\nimport { ARRAY_CONTEXT, buildBaseInputs, DEFAULT_PROPS } from '@ng-forge/dynamic-forms/integration';\n\n/**\n * Generic button mapper for custom events or basic buttons.\n * For specific button types (submit, next, prev, add/remove array items),\n * use the dedicated field types and their specific mappers.\n *\n * @param fieldDef The button field definition\n * @returns Signal containing Record of input names to values for ngComponentOutlet\n */\nexport function buttonFieldMapper(fieldDef: FieldDef<Record<string, unknown>>): Signal<Record<string, unknown>> {\n const defaultProps = inject(DEFAULT_PROPS);\n const arrayContext = inject(ARRAY_CONTEXT, { optional: true });\n\n return computed(() => {\n const baseInputs = buildBaseInputs(fieldDef, defaultProps());\n\n const inputs: Record<string, unknown> = {\n ...baseInputs,\n };\n\n if (fieldDef.disabled !== undefined) {\n inputs['disabled'] = fieldDef.disabled;\n }\n\n if (fieldDef.hidden !== undefined) {\n inputs['hidden'] = fieldDef.hidden;\n }\n\n // Add event binding for button events\n if ('event' in fieldDef && fieldDef.event !== undefined) {\n inputs['event'] = fieldDef.event;\n }\n\n // Add eventArgs binding if provided\n if ('eventArgs' in fieldDef && fieldDef.eventArgs !== undefined) {\n inputs['eventArgs'] = fieldDef.eventArgs;\n }\n\n // Add eventContext for token resolution (supports $template, $arrayKey, $index, etc.)\n const template = 'template' in fieldDef ? fieldDef.template : undefined;\n if (template || arrayContext) {\n // Read signal value if index is a signal (supports differential updates)\n const getIndex = () => {\n if (!arrayContext) return -1;\n return isSignal(arrayContext.index) ? arrayContext.index() : arrayContext.index;\n };\n\n inputs['eventContext'] = {\n key: fieldDef.key,\n index: getIndex(),\n arrayKey: arrayContext?.arrayKey ?? '',\n formValue: arrayContext?.formValue ?? {},\n template,\n };\n }\n\n return inputs;\n });\n}\n","import { FieldTypeDefinition } from '@ng-forge/dynamic-forms/integration';\nimport {\n addArrayItemButtonMapper,\n checkboxFieldMapper,\n datepickerFieldMapper,\n insertArrayItemButtonMapper,\n optionsFieldMapper,\n popArrayItemButtonMapper,\n prependArrayItemButtonMapper,\n removeArrayItemButtonMapper,\n shiftArrayItemButtonMapper,\n valueFieldMapper,\n} from '@ng-forge/dynamic-forms/integration';\nimport { 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 { DynamicFormError, type AddonTypeDefinition } from '@ng-forge/dynamic-forms';\nimport { ADDON_TYPE_DEFINITIONS, type FieldTypeDefinition } from '@ng-forge/dynamic-forms/integration';\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 types (`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 types ------------------------------------------------- */\n\nconst ION_ICON_KIND: AddonTypeDefinition<IonIconAddon> = {\n type: '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 type 'ion-icon' requires a non-empty 'icon' string (field: '${fieldKey}').`);\n }\n },\n};\n\nconst ION_BUTTON_KIND: AddonTypeDefinition<IonButtonAddon> = {\n type: '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 type '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 type 'ion-button' on field '${fieldKey}' is icon-only — provide 'ariaLabel' for accessibility.`);\n }\n },\n};\n\n/**\n * Feature type discriminant for the Ionic addons feature. Matches core's\n * `'addons'` type so providers flow through the standard addon-type pipeline\n * in `provideDynamicForm`.\n */\ntype IonicAddonsFeature = {\n ɵkind: 'addons';\n ɵproviders: Provider[];\n};\n\n/** Register Ionic-shipped addon types (`ion-icon`, `ion-button`) standalone. */\nexport function withIonicAddons(): IonicAddonsFeature {\n return {\n ɵkind: 'addons',\n ɵproviders: [\n { provide: ADDON_TYPE_DEFINITIONS, useValue: ION_ICON_KIND, multi: true },\n { provide: ADDON_TYPE_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/integration';\nimport { injectNgForgeAddonAction, NgForgeAddonAction } from '@ng-forge/dynamic-forms/integration';\nimport type { IonButtonAddon } from '../types/addons';\n\n/** Renderer for the `ion-button` addon type. */\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 { WrapperFieldInputs } from '@ng-forge/dynamic-forms/integration';\nimport { DynamicTextPipe } from '@ng-forge/dynamic-forms/integration';\nimport type { IonIconAddon } from '../types/addons';\n\n/** Renderer for the `ion-icon` addon type. */\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 type 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": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.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": "~22.0.0",
|
|
44
44
|
"@angular/forms": "~22.0.0",
|
|
45
45
|
"@ionic/angular": ">=7.0.0",
|
|
46
|
-
"@ng-forge/dynamic-forms": "~1.0.0-rc.
|
|
46
|
+
"@ng-forge/dynamic-forms": "~1.0.0-rc.3",
|
|
47
47
|
"rxjs": ">=7.0.0"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _ng_forge_dynamic_forms from '@ng-forge/dynamic-forms';
|
|
2
|
-
import { FormEvent, NextPageEvent, PreviousPageEvent, AppendArrayItemEvent, ArrayAllowedChildren, PrependArrayItemEvent, InsertArrayItemEvent, RemoveAtIndexEvent, PopArrayItemEvent, ShiftArrayItemEvent, DynamicText, BaseAddon, DynamicValue, AddonActionPreset, RegisteredActionRef, TextAddon, TemplateAddon, ValueType, FieldOption, NarrowFields, RegisteredFieldTypes, InferFormValue, FormConfig } from '@ng-forge/dynamic-forms';
|
|
2
|
+
import { FormEvent, FormSubmitEvent, NextPageEvent, PreviousPageEvent, AppendArrayItemEvent, ArrayAllowedChildren, PrependArrayItemEvent, InsertArrayItemEvent, RemoveAtIndexEvent, PopArrayItemEvent, ShiftArrayItemEvent, DynamicText, BaseAddon, DynamicValue, AddonActionPreset, RegisteredActionRef, TextAddon, TemplateAddon, ValueType, FieldOption, NarrowFields, RegisteredFieldTypes, InferFormValue, FormConfig } from '@ng-forge/dynamic-forms';
|
|
3
3
|
import * as i1 from '@ng-forge/dynamic-forms/integration';
|
|
4
4
|
import { ButtonField, CheckboxField, DatepickerField, DatepickerProps, AddonActionHandler, InputField, InputProps, WrapperFieldInputs, MultiCheckboxField, RadioField, SelectField, SelectProps, SliderField, TextareaField, TextareaProps, ToggleField, FieldTypeDefinition } from '@ng-forge/dynamic-forms/integration';
|
|
5
5
|
import * as _angular_core from '@angular/core';
|
|
@@ -18,7 +18,7 @@ interface IonicButtonProps {
|
|
|
18
18
|
type IonicButtonField<TEvent extends FormEvent> = ButtonField<IonicButtonProps, TEvent>;
|
|
19
19
|
/** Specific button field types with preconfigured events */
|
|
20
20
|
/** Submit button field - automatically disabled when form is invalid */
|
|
21
|
-
type IonicSubmitButtonField = Omit<IonicButtonField<
|
|
21
|
+
type IonicSubmitButtonField = Omit<IonicButtonField<FormSubmitEvent>, 'event' | 'type' | 'eventArgs'> & {
|
|
22
22
|
type: 'submit';
|
|
23
23
|
};
|
|
24
24
|
/** Next page button field - with preconfigured NextPageEvent */
|
|
@@ -168,7 +168,7 @@ declare class IonicDatepickerFieldComponent {
|
|
|
168
168
|
|
|
169
169
|
/** Decorative icon addon for Ion fields. */
|
|
170
170
|
interface IonIconAddon extends BaseAddon {
|
|
171
|
-
readonly
|
|
171
|
+
readonly type: 'ion-icon';
|
|
172
172
|
/** Ionons name (e.g., `'search-outline'`, `'close-outline'`). */
|
|
173
173
|
readonly icon: string;
|
|
174
174
|
/** Accessible label for icons that convey meaning. */
|
|
@@ -183,7 +183,7 @@ type IonButtonFill = 'clear' | 'outline' | 'solid' | 'default';
|
|
|
183
183
|
* participate in either XOR axis.
|
|
184
184
|
*/
|
|
185
185
|
interface IonButtonBase extends BaseAddon {
|
|
186
|
-
readonly
|
|
186
|
+
readonly type: 'ion-button';
|
|
187
187
|
/** Ion button colour. */
|
|
188
188
|
readonly color?: IonButtonColor;
|
|
189
189
|
/** Ion button fill style. */
|
|
@@ -204,7 +204,7 @@ type IonButtonClickPreset = {
|
|
|
204
204
|
};
|
|
205
205
|
/**
|
|
206
206
|
* Value type for the `actionRef` slot. When no actions have been registered
|
|
207
|
-
* via `
|
|
207
|
+
* via `withAddonActions(...)`, `RegisteredActionRef` resolves to `never` —
|
|
208
208
|
* which would make this variant uninhabitable and break `addon.actionRef`
|
|
209
209
|
* narrowing in the renderer. Fall back to `string` so the variant stays
|
|
210
210
|
* usable; once the user augments `DynamicFormActionRegistry`, autocomplete
|
|
@@ -213,14 +213,14 @@ type IonButtonClickPreset = {
|
|
|
213
213
|
type IonButtonActionRef = [RegisteredActionRef] extends [never] ? string : RegisteredActionRef;
|
|
214
214
|
type IonButtonClickActionRef = {
|
|
215
215
|
readonly preset?: never;
|
|
216
|
-
/** Reference to a handler registered via `
|
|
216
|
+
/** Reference to a handler registered via `withAddonActions(...)`. JSON-safe. */
|
|
217
217
|
readonly actionRef: IonButtonActionRef;
|
|
218
218
|
readonly action?: never;
|
|
219
219
|
};
|
|
220
220
|
type IonButtonClickAction = {
|
|
221
221
|
readonly preset?: never;
|
|
222
222
|
readonly actionRef?: never;
|
|
223
|
-
/** Inline handler — code-only; dropped from JSON-derived configs. Matches the generic `AddonActionHandler` shape used by `
|
|
223
|
+
/** Inline handler — code-only; dropped from JSON-derived configs. Matches the generic `AddonActionHandler` shape used by `withAddonActions(...)`. */
|
|
224
224
|
readonly action: AddonActionHandler;
|
|
225
225
|
};
|
|
226
226
|
type IonButtonClickNone = {
|
|
@@ -252,7 +252,7 @@ type IonButtonContentDecorative = {
|
|
|
252
252
|
type IonButtonContent = IonButtonContentIconOnly | IonButtonContentLabeled | IonButtonContentDecorative;
|
|
253
253
|
/** Interactive button addon for Ion fields. */
|
|
254
254
|
type IonButtonAddon = IonButtonBase & IonButtonContent & IonButtonClick;
|
|
255
|
-
/** Union of all Ion-shipped addon
|
|
255
|
+
/** Union of all Ion-shipped addon types. */
|
|
256
256
|
type IonAddon = IonIconAddon | IonButtonAddon;
|
|
257
257
|
declare module '@ng-forge/dynamic-forms' {
|
|
258
258
|
interface DynamicFormAddonRegistry {
|
|
@@ -272,13 +272,13 @@ interface IonicInputProps extends InputProps {
|
|
|
272
272
|
clearInput?: boolean;
|
|
273
273
|
}
|
|
274
274
|
/**
|
|
275
|
-
* Module-augmentable seam for adding custom addon
|
|
275
|
+
* Module-augmentable seam for adding custom addon types to `ion-input` at
|
|
276
276
|
* the type level. Pair with `withCustomAddon(...)` for the runtime side:
|
|
277
277
|
*/
|
|
278
278
|
interface IonAddonExtensions {
|
|
279
279
|
}
|
|
280
280
|
type IonAddonExtension = IonAddonExtensions[keyof IonAddonExtensions];
|
|
281
|
-
/** Addon
|
|
281
|
+
/** Addon types accepted by `ion-input`. */
|
|
282
282
|
type IonInputAddon = IonIconAddon | IonButtonAddon | TextAddon | TemplateAddon | IonAddonExtension;
|
|
283
283
|
type IonicInputField = InputField<IonicInputProps> & {
|
|
284
284
|
addons?: ReadonlyArray<IonInputAddon>;
|
|
@@ -305,10 +305,10 @@ declare class IonicInputFieldComponent {
|
|
|
305
305
|
protected readonly color: _angular_core.Signal<"primary" | "secondary" | "tertiary" | "success" | "warning" | "danger" | "light" | "medium" | "dark" | undefined>;
|
|
306
306
|
/** Override (set by `toggle-password-visibility` preset) wins over `props().type`. */
|
|
307
307
|
protected readonly type: _angular_core.Signal<string>;
|
|
308
|
-
/** Per-
|
|
308
|
+
/** Per-type split: ion-button addons render inline via the attribute-
|
|
309
309
|
* selector inline component (so the slotted element is a native
|
|
310
310
|
* <ion-button> Ionic can style); everything else (icon/text/template/
|
|
311
|
-
* component
|
|
311
|
+
* component types) goes through the universal <df-addon-slot> dispatcher
|
|
312
312
|
* wrapped in <span slot="start|end"> for safe shadow-DOM projection. */
|
|
313
313
|
protected readonly buttonPrefixAddons: _angular_core.Signal<_ng_forge_dynamic_forms_ionic.IonButtonAddon[]>;
|
|
314
314
|
protected readonly buttonSuffixAddons: _angular_core.Signal<_ng_forge_dynamic_forms_ionic.IonButtonAddon[]>;
|
|
@@ -597,7 +597,7 @@ type IonicFieldsWithAddons = [...IonicFieldTypes, IonicAddonsFeature];
|
|
|
597
597
|
type IonicFieldsWithConfig = [...IonicFieldTypes, IonicAddonsFeature, IonicConfigFeature];
|
|
598
598
|
/**
|
|
599
599
|
* Provides Ionic field type definitions for the dynamic form system,
|
|
600
|
-
* with Ionic-shipped addon
|
|
600
|
+
* with Ionic-shipped addon types (`ion-icon`, `ion-button`) auto-included
|
|
601
601
|
* so addons work out of the box.
|
|
602
602
|
*
|
|
603
603
|
* @param config - Optional global configuration for Ionic form fields
|
|
@@ -608,18 +608,18 @@ declare function withIonicFields(): IonicFieldsWithAddons;
|
|
|
608
608
|
declare function withIonicFields(config: IonicConfig): IonicFieldsWithConfig;
|
|
609
609
|
declare function withIonicFields(config: IonicConfig | undefined): IonicFieldsWithAddons | IonicFieldsWithConfig;
|
|
610
610
|
/**
|
|
611
|
-
* Feature
|
|
612
|
-
* `'addons'`
|
|
611
|
+
* Feature type discriminant for the Ionic addons feature. Matches core's
|
|
612
|
+
* `'addons'` type so providers flow through the standard addon-type pipeline
|
|
613
613
|
* in `provideDynamicForm`.
|
|
614
614
|
*/
|
|
615
615
|
type IonicAddonsFeature = {
|
|
616
616
|
ɵkind: 'addons';
|
|
617
617
|
ɵproviders: Provider[];
|
|
618
618
|
};
|
|
619
|
-
/** Register Ionic-shipped addon
|
|
619
|
+
/** Register Ionic-shipped addon types (`ion-icon`, `ion-button`) standalone. */
|
|
620
620
|
declare function withIonicAddons(): IonicAddonsFeature;
|
|
621
621
|
|
|
622
|
-
/** Renderer for the `ion-button` addon
|
|
622
|
+
/** Renderer for the `ion-button` addon type. */
|
|
623
623
|
declare class IonButtonAddonComponent {
|
|
624
624
|
protected readonly action: i1.TypedNgForgeAddonAction<IonButtonAddon>;
|
|
625
625
|
/** Re-exposed for template binding — same signal stored on the directive. */
|
|
@@ -642,10 +642,10 @@ declare class IonButtonAddonComponent {
|
|
|
642
642
|
static ɵcmp: _angular_core.ɵɵComponentDeclaration<IonButtonAddonComponent, "df-ion-button-addon", never, {}, {}, never, never, true, [{ directive: typeof i1.NgForgeAddonAction; inputs: {}; outputs: {}; }]>;
|
|
643
643
|
}
|
|
644
644
|
|
|
645
|
-
/** Renderer for the `ion-icon` addon
|
|
645
|
+
/** Renderer for the `ion-icon` addon type. */
|
|
646
646
|
declare class IonIconAddonComponent {
|
|
647
647
|
readonly addon: _angular_core.InputSignal<IonIconAddon>;
|
|
648
|
-
/** Accepted for contract uniformity — `NgComponentOutlet` setInput is strict; every
|
|
648
|
+
/** Accepted for contract uniformity — `NgComponentOutlet` setInput is strict; every type must declare it. */
|
|
649
649
|
readonly fieldInputs: _angular_core.InputSignal<WrapperFieldInputs | undefined>;
|
|
650
650
|
protected readonly iconName: _angular_core.Signal<string>;
|
|
651
651
|
protected readonly ariaLabel: _angular_core.Signal<_ng_forge_dynamic_forms.DynamicText | undefined>;
|