From c7a3fc1b71268be99ba6cacb13463420a2cf81cb Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Fri, 18 Sep 2026 16:00:53 -0700 Subject: [PATCH 01/13] feat(angular): support setting boolean props by attribute presence --- core/package-lock.json | 8 +- core/package.json | 2 +- core/stencil.config.ts | 2 + .../directives/navigation/back-button.ts | 17 ++- .../src/common/directives/navigation/nav.ts | 14 ++- .../directives/navigation/router-outlet.ts | 8 +- packages/angular/src/common/overlays/modal.ts | 24 ++-- .../angular/src/common/overlays/popover.ts | 26 ++-- .../src/common/utils/boolean-attribute.ts | 36 ++++++ .../boolean-attribute.ts | 43 +++++++ .../angular/src/lazy/directives/proxies.ts | 119 +++++++++--------- .../boolean-attribute.ts | 43 +++++++ .../src/standalone/directives/checkbox.ts | 12 +- .../src/standalone/directives/datetime.ts | 24 ++-- .../angular/src/standalone/directives/icon.ts | 22 +++- .../src/standalone/directives/input-otp.ts | 10 +- .../src/standalone/directives/input.ts | 26 ++-- .../directives/ion-accordion-group.ts | 3 +- .../standalone/directives/ion-accordion.ts | 3 +- .../standalone/directives/ion-action-sheet.ts | 3 +- .../src/standalone/directives/ion-alert.ts | 3 +- .../src/standalone/directives/ion-avatar.ts | 3 +- .../src/standalone/directives/ion-backdrop.ts | 3 +- .../standalone/directives/ion-breadcrumb.ts | 3 +- .../src/standalone/directives/ion-button.ts | 3 +- .../src/standalone/directives/ion-buttons.ts | 3 +- .../standalone/directives/ion-card-header.ts | 3 +- .../src/standalone/directives/ion-card.ts | 3 +- .../src/standalone/directives/ion-chip.ts | 3 +- .../src/standalone/directives/ion-content.ts | 3 +- .../directives/ion-datetime-button.ts | 3 +- .../src/standalone/directives/ion-divider.ts | 3 +- .../standalone/directives/ion-fab-button.ts | 3 +- .../src/standalone/directives/ion-fab-list.ts | 3 +- .../src/standalone/directives/ion-fab.ts | 3 +- .../src/standalone/directives/ion-footer.ts | 3 +- .../src/standalone/directives/ion-grid.ts | 3 +- .../src/standalone/directives/ion-header.ts | 3 +- .../directives/ion-infinite-scroll.ts | 3 +- .../standalone/directives/ion-item-divider.ts | 3 +- .../standalone/directives/ion-item-option.ts | 3 +- .../standalone/directives/ion-item-sliding.ts | 3 +- .../src/standalone/directives/ion-item.ts | 3 +- .../src/standalone/directives/ion-list.ts | 3 +- .../src/standalone/directives/ion-loading.ts | 3 +- .../standalone/directives/ion-menu-button.ts | 3 +- .../standalone/directives/ion-menu-toggle.ts | 3 +- .../src/standalone/directives/ion-menu.ts | 3 +- .../directives/ion-picker-column-option.ts | 3 +- .../directives/ion-picker-column.ts | 3 +- .../standalone/directives/ion-progress-bar.ts | 3 +- .../src/standalone/directives/ion-radio.ts | 3 +- .../standalone/directives/ion-refresher.ts | 3 +- .../directives/ion-reorder-group.ts | 3 +- .../directives/ion-segment-button.ts | 3 +- .../standalone/directives/ion-segment-view.ts | 3 +- .../standalone/directives/ion-select-modal.ts | 3 +- .../directives/ion-select-option.ts | 3 +- .../directives/ion-skeleton-text.ts | 3 +- .../src/standalone/directives/ion-spinner.ts | 3 +- .../standalone/directives/ion-split-pane.ts | 3 +- .../src/standalone/directives/ion-tab-bar.ts | 3 +- .../standalone/directives/ion-tab-button.ts | 3 +- .../src/standalone/directives/ion-toast.ts | 3 +- .../src/standalone/directives/radio-group.ts | 15 ++- .../src/standalone/directives/range.ts | 16 ++- .../src/standalone/directives/searchbar.ts | 14 ++- .../src/standalone/directives/segment.ts | 16 ++- .../src/standalone/directives/select.ts | 10 +- .../src/standalone/directives/textarea.ts | 22 ++-- .../src/standalone/directives/toggle.ts | 12 +- 71 files changed, 484 insertions(+), 198 deletions(-) create mode 100644 packages/angular/src/common/utils/boolean-attribute.ts create mode 100644 packages/angular/src/lazy/directives/angular-component-lib/boolean-attribute.ts create mode 100644 packages/angular/src/standalone/directives/angular-component-lib/boolean-attribute.ts diff --git a/core/package-lock.json b/core/package-lock.json index c266f45523b..10a0a410175 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -27,7 +27,7 @@ "@playwright/test": "^1.62.1", "@rollup/plugin-node-resolve": "^8.4.0", "@rollup/plugin-virtual": "^2.0.3", - "@stencil/angular-output-target": "^1.4.1", + "@stencil/angular-output-target": "^1.5.0", "@stencil/react-output-target": "^1.6.2", "@stencil/sass": "^3.0.9", "@stencil/vue-output-target": "0.14.2", @@ -2377,9 +2377,9 @@ } }, "node_modules/@stencil/angular-output-target": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@stencil/angular-output-target/-/angular-output-target-1.4.1.tgz", - "integrity": "sha512-sy+k9B5jcT6hpaVU9n+/ZUjz3qVr9PAdPgy2c3LFQouW1aFSa3Ng0O+Ia3wk7LMeL3vDetCn2X+p2ewdHGc1vA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@stencil/angular-output-target/-/angular-output-target-1.5.0.tgz", + "integrity": "sha512-zr3n6PP3Gcj5pEyJgn32eJkSXSW5sH6XuSe7mG/MbSCZ6Odds4J7zplBDAc7WgTIcaxY1xS0cR21q1SCncwpkA==", "dev": true, "license": "MIT", "peerDependencies": { diff --git a/core/package.json b/core/package.json index efed9c9dfdb..ad1f9f5be0d 100644 --- a/core/package.json +++ b/core/package.json @@ -84,7 +84,7 @@ "@playwright/test": "^1.62.1", "@rollup/plugin-node-resolve": "^8.4.0", "@rollup/plugin-virtual": "^2.0.3", - "@stencil/angular-output-target": "^1.4.1", + "@stencil/angular-output-target": "^1.5.0", "@stencil/react-output-target": "^1.6.2", "@stencil/sass": "^3.0.9", "@stencil/vue-output-target": "0.14.2", diff --git a/core/stencil.config.ts b/core/stencil.config.ts index 4c2dd94f309..614e72cd432 100644 --- a/core/stencil.config.ts +++ b/core/stencil.config.ts @@ -34,6 +34,7 @@ const getAngularOutputTargets = () => { directivesArrayFile: '../packages/angular/src/lazy/directives/proxies-list.ts', excludeComponents, outputType: 'component', + booleanAttributes: true, }), angularOutputTarget({ componentCorePackage, @@ -66,6 +67,7 @@ const getAngularOutputTargets = () => { outputType: 'standalone', // Emit each component in a separate file rather than putting them all in one large file. esModules: true, + booleanAttributes: true, }) ]; } diff --git a/packages/angular/src/common/directives/navigation/back-button.ts b/packages/angular/src/common/directives/navigation/back-button.ts index 35a889d4d84..9dc4a97cf5c 100644 --- a/packages/angular/src/common/directives/navigation/back-button.ts +++ b/packages/angular/src/common/directives/navigation/back-button.ts @@ -3,17 +3,30 @@ import type { Components } from '@ionic/core'; import { Config } from '../../providers/config'; import { NavController } from '../../providers/nav-controller'; +import { nullableBooleanAttribute } from '../../utils/boolean-attribute'; import { ProxyCmp } from '../../utils/proxy'; import { IonRouterOutlet } from './router-outlet'; -const BACK_BUTTON_INPUTS = ['color', 'defaultHref', 'disabled', 'icon', 'mode', 'routerAnimation', 'text', 'type']; +const BACK_BUTTON_INPUTS = [ + 'color', + 'defaultHref', + { name: 'disabled', transform: nullableBooleanAttribute }, + 'icon', + 'mode', + 'routerAnimation', + 'text', + 'type', +]; + +/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ +const BACK_BUTTON_PROXY_INPUTS = BACK_BUTTON_INPUTS.map((input) => (typeof input === 'string' ? input : input.name)); // eslint-disable-next-line @typescript-eslint/no-empty-object-type export declare interface IonBackButton extends Components.IonBackButton {} @ProxyCmp({ - inputs: BACK_BUTTON_INPUTS, + inputs: BACK_BUTTON_PROXY_INPUTS, }) @Directive({ // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property diff --git a/packages/angular/src/common/directives/navigation/nav.ts b/packages/angular/src/common/directives/navigation/nav.ts index 30cfa903a01..b3daebf1383 100644 --- a/packages/angular/src/common/directives/navigation/nav.ts +++ b/packages/angular/src/common/directives/navigation/nav.ts @@ -10,9 +10,19 @@ import { import type { Components } from '@ionic/core'; import { AngularDelegate } from '../../providers/angular-delegate'; +import { nullableBooleanAttribute } from '../../utils/boolean-attribute'; import { ProxyCmp, proxyOutputs } from '../../utils/proxy'; -const NAV_INPUTS = ['animated', 'animation', 'root', 'rootParams', 'swipeGesture']; +const NAV_INPUTS = [ + { name: 'animated', transform: nullableBooleanAttribute }, + 'animation', + 'root', + 'rootParams', + { name: 'swipeGesture', transform: nullableBooleanAttribute }, +]; + +/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ +const NAV_PROXY_INPUTS = NAV_INPUTS.map((input) => (typeof input === 'string' ? input : input.name)); const NAV_METHODS = [ 'push', @@ -42,7 +52,7 @@ export declare interface IonNav extends Components.IonNav { } @ProxyCmp({ - inputs: NAV_INPUTS, + inputs: NAV_PROXY_INPUTS, methods: NAV_METHODS, }) @Directive({ diff --git a/packages/angular/src/common/directives/navigation/router-outlet.ts b/packages/angular/src/common/directives/navigation/router-outlet.ts index 71b60f5fdf5..79ec674b78c 100644 --- a/packages/angular/src/common/directives/navigation/router-outlet.ts +++ b/packages/angular/src/common/directives/navigation/router-outlet.ts @@ -29,6 +29,7 @@ import { distinctUntilChanged, filter, switchMap } from 'rxjs/operators'; import { Config } from '../../providers/config'; import { NavController } from '../../providers/nav-controller'; +import { nullableBooleanAttribute } from '../../utils/boolean-attribute'; import { StackController } from './stack-controller'; import { RouteView, StackDidChangeEvent, StackWillChangeEvent, getUrl, isTabSwitch } from './stack-utils'; @@ -39,7 +40,12 @@ import { RouteView, StackDidChangeEvent, StackWillChangeEvent, getUrl, isTabSwit selector: 'ion-router-outlet', exportAs: 'outlet', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['animated', 'animation', 'mode', 'swipeGesture'], + inputs: [ + { name: 'animated', transform: nullableBooleanAttribute }, + 'animation', + 'mode', + { name: 'swipeGesture', transform: nullableBooleanAttribute }, + ], }) export abstract class IonRouterOutlet implements OnDestroy, OnInit { abstract outletContent: any; diff --git a/packages/angular/src/common/overlays/modal.ts b/packages/angular/src/common/overlays/modal.ts index 12bf86ff5cf..3e149554481 100644 --- a/packages/angular/src/common/overlays/modal.ts +++ b/packages/angular/src/common/overlays/modal.ts @@ -9,6 +9,7 @@ import { } from '@angular/core'; import type { Components, ModalBreakpointChangeEventDetail, ModalDragEventDetail } from '@ionic/core/components'; +import { nullableBooleanAttribute } from '../utils/boolean-attribute'; import { ProxyCmp, proxyOutputs } from '../utils/proxy'; export declare interface IonModal extends Components.IonModal { @@ -63,30 +64,33 @@ export declare interface IonModal extends Components.IonModal { } const MODAL_INPUTS = [ - 'animated', - 'keepContentsMounted', + { name: 'animated', transform: nullableBooleanAttribute }, + { name: 'keepContentsMounted', transform: nullableBooleanAttribute }, 'backdropBreakpoint', - 'backdropDismiss', + { name: 'backdropDismiss', transform: nullableBooleanAttribute }, 'breakpoints', 'canDismiss', 'cssClass', 'enterAnimation', - 'expandToScroll', + { name: 'expandToScroll', transform: nullableBooleanAttribute }, 'event', - 'focusTrap', - 'handle', + { name: 'focusTrap', transform: nullableBooleanAttribute }, + { name: 'handle', transform: nullableBooleanAttribute }, 'handleBehavior', 'initialBreakpoint', - 'isOpen', - 'keyboardClose', + { name: 'isOpen', transform: nullableBooleanAttribute }, + { name: 'keyboardClose', transform: nullableBooleanAttribute }, 'leaveAnimation', 'mode', 'presentingElement', - 'showBackdrop', + { name: 'showBackdrop', transform: nullableBooleanAttribute }, 'translucent', 'trigger', ]; +/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ +const MODAL_PROXY_INPUTS = MODAL_INPUTS.map((input) => (typeof input === 'string' ? input : input.name)); + const MODAL_METHODS = [ 'present', 'dismiss', @@ -97,7 +101,7 @@ const MODAL_METHODS = [ ]; @ProxyCmp({ - inputs: MODAL_INPUTS, + inputs: MODAL_PROXY_INPUTS, methods: MODAL_METHODS, }) /** diff --git a/packages/angular/src/common/overlays/popover.ts b/packages/angular/src/common/overlays/popover.ts index f4091a37c79..856391b8029 100644 --- a/packages/angular/src/common/overlays/popover.ts +++ b/packages/angular/src/common/overlays/popover.ts @@ -9,6 +9,7 @@ import { } from '@angular/core'; import type { Components } from '@ionic/core/components'; +import { nullableBooleanAttribute } from '../utils/boolean-attribute'; import { ProxyCmp, proxyOutputs } from '../utils/proxy'; export declare interface IonPopover extends Components.IonPopover { @@ -48,21 +49,21 @@ export declare interface IonPopover extends Components.IonPopover { const POPOVER_INPUTS = [ 'alignment', - 'animated', - 'arrow', - 'keepContentsMounted', - 'backdropDismiss', + { name: 'animated', transform: nullableBooleanAttribute }, + { name: 'arrow', transform: nullableBooleanAttribute }, + { name: 'keepContentsMounted', transform: nullableBooleanAttribute }, + { name: 'backdropDismiss', transform: nullableBooleanAttribute }, 'cssClass', - 'dismissOnSelect', + { name: 'dismissOnSelect', transform: nullableBooleanAttribute }, 'enterAnimation', 'event', - 'focusTrap', - 'isOpen', - 'keyboardClose', + { name: 'focusTrap', transform: nullableBooleanAttribute }, + { name: 'isOpen', transform: nullableBooleanAttribute }, + { name: 'keyboardClose', transform: nullableBooleanAttribute }, 'leaveAnimation', 'mode', - 'showBackdrop', - 'translucent', + { name: 'showBackdrop', transform: nullableBooleanAttribute }, + { name: 'translucent', transform: nullableBooleanAttribute }, 'trigger', 'triggerAction', 'reference', @@ -70,10 +71,13 @@ const POPOVER_INPUTS = [ 'side', ]; +/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ +const POPOVER_PROXY_INPUTS = POPOVER_INPUTS.map((input) => (typeof input === 'string' ? input : input.name)); + const POPOVER_METHODS = ['present', 'dismiss', 'onDidDismiss', 'onWillDismiss']; @ProxyCmp({ - inputs: POPOVER_INPUTS, + inputs: POPOVER_PROXY_INPUTS, methods: POPOVER_METHODS, }) /** diff --git a/packages/angular/src/common/utils/boolean-attribute.ts b/packages/angular/src/common/utils/boolean-attribute.ts new file mode 100644 index 00000000000..047a0bdc48a --- /dev/null +++ b/packages/angular/src/common/utils/boolean-attribute.ts @@ -0,0 +1,36 @@ +/* + * Duplicates `angular-component-lib/boolean-attribute.ts`, which the output + * target copies next to each generated proxies file and so is not reachable + * from here. `proxy.ts` duplicates `ProxyCmp` for the same reason. Refer to + * the TODO at the top of `proxy.ts`. + */ + +/** + * Transforms a value to a boolean so that boolean properties can be set by attribute presence, + * e.g. `` instead of ``. + * + * Strings are coerced the same way Angular's `booleanAttribute` coerces them, so `''` (a bare + * attribute) becomes `true` and `'false'` becomes `false`. + * + * Unlike Angular's `booleanAttribute`, `null` and `undefined` are passed through rather than + * coerced to `false`. Components frequently treat them as a state distinct from `false`, and both + * reach inputs routinely from the `async` pipe before its first emission and from form control + * values: + * + * ```tsx + * // `undefined` means "decide based on the mode", which is not the same as `false` + * const showDetail = detail !== undefined ? detail : mode === 'ios'; + * + * // a strict comparison also behaves differently for `null` than it does for `false` + * const showHandle = handle !== false; + * ``` + * + * Declared as a function rather than an arrow constant because Angular has to resolve input + * transforms statically when compiling a library in partial compilation mode. + */ +export function nullableBooleanAttribute(value: boolean | string | null | undefined): boolean | null | undefined { + if (value === null || value === undefined) { + return value; + } + return typeof value === 'boolean' ? value : value !== 'false'; +} diff --git a/packages/angular/src/lazy/directives/angular-component-lib/boolean-attribute.ts b/packages/angular/src/lazy/directives/angular-component-lib/boolean-attribute.ts new file mode 100644 index 00000000000..c116dd5bc7f --- /dev/null +++ b/packages/angular/src/lazy/directives/angular-component-lib/boolean-attribute.ts @@ -0,0 +1,43 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Transforms a value to a boolean so that boolean properties can be set by attribute presence, + * e.g. `` instead of ``. + * + * Strings are coerced the same way Angular's `booleanAttribute` coerces them, so `''` (a bare + * attribute) becomes `true` and `'false'` becomes `false`. + * + * Unlike Angular's `booleanAttribute`, `null` and `undefined` are passed through rather than + * coerced to `false`. Components frequently treat them as a state distinct from `false`: + * + * ```tsx + * // `undefined` means "decide based on the mode", which is not the same as `false` + * const showDetail = detail !== undefined ? detail : mode === 'ios'; + * + * // a strict comparison also behaves differently for `null` than it does for `false` + * const showHandle = handle !== false; + * ``` + * + * Both values reach inputs routinely in Angular templates, from the `async` pipe before its + * first emission and from form control values, so coercing them would change the behavior of + * bindings that work today. + * + * This is implemented here rather than imported from `@angular/core` so that consumers on + * Angular versions without `booleanAttribute` are unaffected by this file being generated. + * + * The parameter type is what Angular derives `ngAcceptInputType_*` from, so it decides which + * template bindings compile. Widening it to `unknown` would let any expression through. + * + * Declared as a function rather than an arrow constant because Angular has to resolve input + * transforms statically when compiling a library in partial compilation mode. + * + * This lives in its own file, separate from `utils.ts`, so that it carries no runtime imports. + * That keeps it independently type-checkable without pulling `rxjs` in for `proxyOutputs`. + */ +export function nullableBooleanAttribute(value: boolean | string | null | undefined): boolean | null | undefined { + if (value === null || value === undefined) { + return value; + } + return typeof value === 'boolean' ? value : value !== 'false'; +} diff --git a/packages/angular/src/lazy/directives/proxies.ts b/packages/angular/src/lazy/directives/proxies.ts index 67e09a3e135..885b667cfef 100644 --- a/packages/angular/src/lazy/directives/proxies.ts +++ b/packages/angular/src/lazy/directives/proxies.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, Output, NgZone } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import { Components } from '@ionic/core'; @@ -15,7 +16,7 @@ import { Components } from '@ionic/core'; changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['disabled', 'mode', 'readonly', 'theme', 'toggleIcon', 'toggleIconSlot', 'value'], + inputs: [{ name: 'disabled', transform: nullableBooleanAttribute }, 'mode', { name: 'readonly', transform: nullableBooleanAttribute }, 'theme', 'toggleIcon', 'toggleIconSlot', 'value'], standalone: false }) export class IonAccordion { @@ -38,7 +39,7 @@ export declare interface IonAccordion extends Components.IonAccordion {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['animated', 'disabled', 'expand', 'mode', 'multiple', 'readonly', 'shape', 'theme', 'value'], + inputs: [{ name: 'animated', transform: nullableBooleanAttribute }, { name: 'disabled', transform: nullableBooleanAttribute }, 'expand', 'mode', { name: 'multiple', transform: nullableBooleanAttribute }, { name: 'readonly', transform: nullableBooleanAttribute }, 'shape', 'theme', 'value'], outputs: ['ionChange'], standalone: false }) @@ -74,7 +75,7 @@ This event will not emit when programmatically setting the `value` property. changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['animated', 'backdropDismiss', 'buttons', 'cssClass', 'enterAnimation', 'header', 'htmlAttributes', 'isOpen', 'keyboardClose', 'leaveAnimation', 'mode', 'subHeader', 'theme', 'translucent', 'trigger'], + inputs: [{ name: 'animated', transform: nullableBooleanAttribute }, { name: 'backdropDismiss', transform: nullableBooleanAttribute }, 'buttons', 'cssClass', 'enterAnimation', 'header', 'htmlAttributes', { name: 'isOpen', transform: nullableBooleanAttribute }, { name: 'keyboardClose', transform: nullableBooleanAttribute }, 'leaveAnimation', 'mode', 'subHeader', 'theme', { name: 'translucent', transform: nullableBooleanAttribute }, 'trigger'], outputs: ['ionActionSheetDidPresent', 'ionActionSheetWillPresent', 'ionActionSheetWillDismiss', 'ionActionSheetDidDismiss', 'didPresent', 'willPresent', 'willDismiss', 'didDismiss'], standalone: false }) @@ -147,7 +148,7 @@ Shorthand for ionActionSheetDidDismiss. changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['animated', 'backdropDismiss', 'buttons', 'cssClass', 'enterAnimation', 'header', 'htmlAttributes', 'inputs', 'isOpen', 'keyboardClose', 'leaveAnimation', 'message', 'mode', 'subHeader', 'theme', 'translucent', 'trigger'], + inputs: [{ name: 'animated', transform: nullableBooleanAttribute }, { name: 'backdropDismiss', transform: nullableBooleanAttribute }, 'buttons', 'cssClass', 'enterAnimation', 'header', 'htmlAttributes', 'inputs', { name: 'isOpen', transform: nullableBooleanAttribute }, { name: 'keyboardClose', transform: nullableBooleanAttribute }, 'leaveAnimation', 'message', 'mode', 'subHeader', 'theme', { name: 'translucent', transform: nullableBooleanAttribute }, 'trigger'], outputs: ['ionAlertDidPresent', 'ionAlertWillPresent', 'ionAlertWillDismiss', 'ionAlertDidDismiss', 'didPresent', 'willPresent', 'willDismiss', 'didDismiss'], standalone: false }) @@ -243,7 +244,7 @@ export declare interface IonApp extends Components.IonApp {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['disabled', 'mode', 'shape', 'size', 'theme'], + inputs: [{ name: 'disabled', transform: nullableBooleanAttribute }, 'mode', 'shape', 'size', 'theme'], standalone: false }) export class IonAvatar { @@ -266,7 +267,7 @@ export declare interface IonAvatar extends Components.IonAvatar {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['mode', 'stopPropagation', 'tappable', 'theme', 'visible'], + inputs: ['mode', { name: 'stopPropagation', transform: nullableBooleanAttribute }, { name: 'tappable', transform: nullableBooleanAttribute }, 'theme', { name: 'visible', transform: nullableBooleanAttribute }], outputs: ['ionBackdropTap'], standalone: false }) @@ -321,7 +322,7 @@ export declare interface IonBadge extends Components.IonBadge {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['active', 'color', 'disabled', 'download', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'separator', 'target', 'theme'], + inputs: [{ name: 'active', transform: nullableBooleanAttribute }, 'color', { name: 'disabled', transform: nullableBooleanAttribute }, 'download', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', { name: 'separator', transform: nullableBooleanAttribute }, 'target', 'theme'], outputs: ['ionFocus', 'ionBlur'], standalone: false }) @@ -391,7 +392,7 @@ export declare interface IonBreadcrumbs extends Components.IonBreadcrumbs { changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['buttonType', 'color', 'disabled', 'download', 'expand', 'fill', 'form', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'size', 'strong', 'target', 'theme', 'type'], + inputs: ['buttonType', 'color', { name: 'disabled', transform: nullableBooleanAttribute }, 'download', 'expand', 'fill', 'form', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'size', { name: 'strong', transform: nullableBooleanAttribute }, 'target', 'theme', 'type'], outputs: ['ionFocus', 'ionBlur'], standalone: false }) @@ -428,7 +429,7 @@ export declare interface IonButton extends Components.IonButton { changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['collapse', 'mode', 'theme'], + inputs: [{ name: 'collapse', transform: nullableBooleanAttribute }, 'mode', 'theme'], standalone: false }) export class IonButtons { @@ -451,7 +452,7 @@ export declare interface IonButtons extends Components.IonButtons {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['button', 'color', 'disabled', 'download', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'target', 'theme', 'type'], + inputs: [{ name: 'button', transform: nullableBooleanAttribute }, 'color', { name: 'disabled', transform: nullableBooleanAttribute }, 'download', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'target', 'theme', 'type'], standalone: false }) export class IonCard { @@ -497,7 +498,7 @@ export declare interface IonCardContent extends Components.IonCardContent {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'mode', 'theme', 'translucent'], + inputs: ['color', 'mode', 'theme', { name: 'translucent', transform: nullableBooleanAttribute }], standalone: false }) export class IonCardHeader { @@ -566,7 +567,7 @@ export declare interface IonCardTitle extends Components.IonCardTitle {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['alignment', 'checked', 'color', 'disabled', 'errorText', 'helperText', 'indeterminate', 'justify', 'labelPlacement', 'mode', 'name', 'required', 'shape', 'size', 'theme', 'value'], + inputs: ['alignment', { name: 'checked', transform: nullableBooleanAttribute }, 'color', { name: 'disabled', transform: nullableBooleanAttribute }, 'errorText', 'helperText', { name: 'indeterminate', transform: nullableBooleanAttribute }, 'justify', 'labelPlacement', 'mode', 'name', { name: 'required', transform: nullableBooleanAttribute }, 'shape', 'size', 'theme', 'value'], outputs: ['ionChange', 'ionFocus', 'ionBlur'], standalone: false }) @@ -612,7 +613,7 @@ setting the `checked` property. changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'disabled', 'hue', 'mode', 'outline', 'shape', 'size', 'theme'], + inputs: ['color', { name: 'disabled', transform: nullableBooleanAttribute }, 'hue', 'mode', { name: 'outline', transform: nullableBooleanAttribute }, 'shape', 'size', 'theme'], standalone: false }) export class IonChip { @@ -659,7 +660,7 @@ export declare interface IonCol extends Components.IonCol {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'fixedSlotPlacement', 'forceOverscroll', 'fullscreen', 'mode', 'scrollEvents', 'scrollX', 'scrollY', 'theme'], + inputs: ['color', 'fixedSlotPlacement', { name: 'forceOverscroll', transform: nullableBooleanAttribute }, { name: 'fullscreen', transform: nullableBooleanAttribute }, 'mode', { name: 'scrollEvents', transform: nullableBooleanAttribute }, { name: 'scrollX', transform: nullableBooleanAttribute }, { name: 'scrollY', transform: nullableBooleanAttribute }, 'theme'], outputs: ['ionScrollStart', 'ionScroll', 'ionScrollEnd'], standalone: false }) @@ -707,7 +708,7 @@ Set `scrollEvents` to `true` to enable. changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['cancelText', 'clearText', 'color', 'dayValues', 'disabled', 'doneText', 'firstDayOfWeek', 'formatOptions', 'highlightedDates', 'hourCycle', 'hourValues', 'isDateEnabled', 'locale', 'max', 'min', 'minuteValues', 'mode', 'monthValues', 'multiple', 'name', 'preferWheel', 'presentation', 'readonly', 'showAdjacentDays', 'showClearButton', 'showDefaultButtons', 'showDefaultTimeLabel', 'showDefaultTitle', 'size', 'theme', 'titleSelectedDatesFormatter', 'value', 'yearValues'], + inputs: ['cancelText', 'clearText', 'color', 'dayValues', { name: 'disabled', transform: nullableBooleanAttribute }, 'doneText', 'firstDayOfWeek', 'formatOptions', 'highlightedDates', 'hourCycle', 'hourValues', 'isDateEnabled', 'locale', 'max', 'min', 'minuteValues', 'mode', 'monthValues', { name: 'multiple', transform: nullableBooleanAttribute }, 'name', { name: 'preferWheel', transform: nullableBooleanAttribute }, 'presentation', { name: 'readonly', transform: nullableBooleanAttribute }, { name: 'showAdjacentDays', transform: nullableBooleanAttribute }, { name: 'showClearButton', transform: nullableBooleanAttribute }, { name: 'showDefaultButtons', transform: nullableBooleanAttribute }, { name: 'showDefaultTimeLabel', transform: nullableBooleanAttribute }, { name: 'showDefaultTitle', transform: nullableBooleanAttribute }, 'size', 'theme', 'titleSelectedDatesFormatter', 'value', 'yearValues'], outputs: ['ionCancel', 'ionChange', 'ionFocus', 'ionBlur'], standalone: false }) @@ -757,7 +758,7 @@ This event will not emit when programmatically setting the `value` property. changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'datetime', 'disabled', 'mode', 'theme'], + inputs: ['color', 'datetime', { name: 'disabled', transform: nullableBooleanAttribute }, 'mode', 'theme'], standalone: false }) export class IonDatetimeButton { @@ -780,7 +781,7 @@ export declare interface IonDatetimeButton extends Components.IonDatetimeButton changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['inset', 'spacing'], + inputs: [{ name: 'inset', transform: nullableBooleanAttribute }, 'spacing'], standalone: false }) export class IonDivider { @@ -804,7 +805,7 @@ export declare interface IonDivider extends Components.IonDivider {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['activated', 'edge', 'horizontal', 'mode', 'theme', 'vertical'], + inputs: [{ name: 'activated', transform: nullableBooleanAttribute }, { name: 'edge', transform: nullableBooleanAttribute }, 'horizontal', 'mode', 'theme', 'vertical'], standalone: false }) export class IonFab { @@ -827,7 +828,7 @@ export declare interface IonFab extends Components.IonFab {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['activated', 'closeIcon', 'color', 'disabled', 'download', 'form', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'show', 'size', 'target', 'theme', 'translucent', 'type'], + inputs: [{ name: 'activated', transform: nullableBooleanAttribute }, 'closeIcon', 'color', { name: 'disabled', transform: nullableBooleanAttribute }, 'download', 'form', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', { name: 'show', transform: nullableBooleanAttribute }, 'size', 'target', 'theme', { name: 'translucent', transform: nullableBooleanAttribute }, 'type'], outputs: ['ionFocus', 'ionBlur'], standalone: false }) @@ -864,7 +865,7 @@ export declare interface IonFabButton extends Components.IonFabButton { changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['activated', 'mode', 'side', 'theme'], + inputs: [{ name: 'activated', transform: nullableBooleanAttribute }, 'mode', 'side', 'theme'], standalone: false }) export class IonFabList { @@ -887,7 +888,7 @@ export declare interface IonFabList extends Components.IonFabList {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['collapse', 'mode', 'scrollEffect', 'theme', 'translucent'], + inputs: ['collapse', 'mode', 'scrollEffect', 'theme', { name: 'translucent', transform: nullableBooleanAttribute }], standalone: false }) export class IonFooter { @@ -956,7 +957,7 @@ export declare interface IonGalleryItem extends Components.IonGalleryItem {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['fixed', 'mode', 'theme'], + inputs: [{ name: 'fixed', transform: nullableBooleanAttribute }, 'mode', 'theme'], standalone: false }) export class IonGrid { @@ -979,7 +980,7 @@ export declare interface IonGrid extends Components.IonGrid {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['collapse', 'divider', 'mode', 'scrollEffect', 'theme', 'translucent'], + inputs: ['collapse', { name: 'divider', transform: nullableBooleanAttribute }, 'mode', 'scrollEffect', 'theme', { name: 'translucent', transform: nullableBooleanAttribute }], standalone: false }) export class IonHeader { @@ -1002,7 +1003,7 @@ export declare interface IonHeader extends Components.IonHeader {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'flipRtl', 'icon', 'ios', 'lazy', 'md', 'mode', 'name', 'sanitize', 'size', 'src'], + inputs: ['color', { name: 'flipRtl', transform: nullableBooleanAttribute }, 'icon', 'ios', { name: 'lazy', transform: nullableBooleanAttribute }, 'md', 'mode', 'name', { name: 'sanitize', transform: nullableBooleanAttribute }, 'size', 'src'], standalone: false }) export class IonIcon { @@ -1068,7 +1069,7 @@ export declare interface IonImg extends Components.IonImg { changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['disabled', 'mode', 'position', 'preserveRerenderScrollPosition', 'theme', 'threshold'], + inputs: [{ name: 'disabled', transform: nullableBooleanAttribute }, 'mode', 'position', { name: 'preserveRerenderScrollPosition', transform: nullableBooleanAttribute }, 'theme', 'threshold'], outputs: ['ionInfinite'], standalone: false }) @@ -1127,7 +1128,7 @@ export declare interface IonInfiniteScrollContent extends Components.IonInfinite changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['autocapitalize', 'autocomplete', 'autocorrect', 'autofocus', 'clearInput', 'clearInputIcon', 'clearOnEdit', 'color', 'counter', 'counterFormatter', 'debounce', 'disabled', 'enterkeyhint', 'errorText', 'fill', 'helperText', 'inputmode', 'label', 'labelPlacement', 'max', 'maxlength', 'min', 'minlength', 'mode', 'multiple', 'name', 'pattern', 'placeholder', 'readonly', 'required', 'shape', 'size', 'spellcheck', 'step', 'theme', 'type', 'value'], + inputs: ['autocapitalize', 'autocomplete', { name: 'autocorrect', transform: nullableBooleanAttribute }, { name: 'autofocus', transform: nullableBooleanAttribute }, { name: 'clearInput', transform: nullableBooleanAttribute }, 'clearInputIcon', { name: 'clearOnEdit', transform: nullableBooleanAttribute }, 'color', { name: 'counter', transform: nullableBooleanAttribute }, 'counterFormatter', 'debounce', { name: 'disabled', transform: nullableBooleanAttribute }, 'enterkeyhint', 'errorText', 'fill', 'helperText', 'inputmode', 'label', 'labelPlacement', 'max', 'maxlength', 'min', 'minlength', 'mode', { name: 'multiple', transform: nullableBooleanAttribute }, 'name', 'pattern', 'placeholder', { name: 'readonly', transform: nullableBooleanAttribute }, { name: 'required', transform: nullableBooleanAttribute }, 'shape', 'size', { name: 'spellcheck', transform: nullableBooleanAttribute }, 'step', 'theme', 'type', 'value'], outputs: ['ionInput', 'ionChange', 'ionBlur', 'ionFocus'], standalone: false }) @@ -1195,7 +1196,7 @@ This event will not emit when programmatically setting the `value` property. changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['autocapitalize', 'color', 'disabled', 'fill', 'inputmode', 'length', 'mode', 'pattern', 'readonly', 'separators', 'shape', 'size', 'theme', 'type', 'value'], + inputs: ['autocapitalize', 'color', { name: 'disabled', transform: nullableBooleanAttribute }, 'fill', 'inputmode', 'length', 'mode', 'pattern', { name: 'readonly', transform: nullableBooleanAttribute }, 'separators', 'shape', 'size', 'theme', 'type', 'value'], outputs: ['ionInput', 'ionChange', 'ionComplete', 'ionBlur', 'ionFocus'], standalone: false }) @@ -1287,7 +1288,7 @@ export declare interface IonInputPasswordToggle extends Components.IonInputPassw changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['button', 'color', 'detail', 'detailIcon', 'disabled', 'download', 'href', 'lines', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'target', 'theme', 'type'], + inputs: [{ name: 'button', transform: nullableBooleanAttribute }, 'color', { name: 'detail', transform: nullableBooleanAttribute }, 'detailIcon', { name: 'disabled', transform: nullableBooleanAttribute }, 'download', 'href', 'lines', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'target', 'theme', 'type'], standalone: false }) export class IonItem { @@ -1310,7 +1311,7 @@ export declare interface IonItem extends Components.IonItem {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'mode', 'sticky', 'theme'], + inputs: ['color', 'mode', { name: 'sticky', transform: nullableBooleanAttribute }, 'theme'], standalone: false }) export class IonItemDivider { @@ -1356,7 +1357,7 @@ export declare interface IonItemGroup extends Components.IonItemGroup {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'disabled', 'download', 'expandable', 'href', 'hue', 'mode', 'rel', 'shape', 'target', 'theme', 'type'], + inputs: ['color', { name: 'disabled', transform: nullableBooleanAttribute }, 'download', { name: 'expandable', transform: nullableBooleanAttribute }, 'href', 'hue', 'mode', 'rel', 'shape', 'target', 'theme', 'type'], standalone: false }) export class IonItemOption { @@ -1412,7 +1413,7 @@ export declare interface IonItemOptions extends Components.IonItemOptions { changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['disabled', 'mode', 'theme'], + inputs: [{ name: 'disabled', transform: nullableBooleanAttribute }, 'mode', 'theme'], outputs: ['ionDrag'], standalone: false }) @@ -1468,7 +1469,7 @@ export declare interface IonLabel extends Components.IonLabel {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['inset', 'lines', 'mode', 'shape', 'theme'], + inputs: [{ name: 'inset', transform: nullableBooleanAttribute }, 'lines', 'mode', 'shape', 'theme'], standalone: false }) export class IonList { @@ -1515,7 +1516,7 @@ export declare interface IonListHeader extends Components.IonListHeader {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['animated', 'backdropDismiss', 'cssClass', 'duration', 'enterAnimation', 'htmlAttributes', 'isOpen', 'keyboardClose', 'leaveAnimation', 'message', 'mode', 'showBackdrop', 'spinner', 'theme', 'translucent', 'trigger'], + inputs: [{ name: 'animated', transform: nullableBooleanAttribute }, { name: 'backdropDismiss', transform: nullableBooleanAttribute }, 'cssClass', 'duration', 'enterAnimation', 'htmlAttributes', { name: 'isOpen', transform: nullableBooleanAttribute }, { name: 'keyboardClose', transform: nullableBooleanAttribute }, 'leaveAnimation', 'message', 'mode', { name: 'showBackdrop', transform: nullableBooleanAttribute }, 'spinner', 'theme', { name: 'translucent', transform: nullableBooleanAttribute }, 'trigger'], outputs: ['ionLoadingDidPresent', 'ionLoadingWillPresent', 'ionLoadingWillDismiss', 'ionLoadingDidDismiss', 'didPresent', 'willPresent', 'willDismiss', 'didDismiss'], standalone: false }) @@ -1588,7 +1589,7 @@ Shorthand for ionLoadingDidDismiss. changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['contentId', 'disabled', 'maxEdgeStart', 'menuId', 'mode', 'side', 'swipeGesture', 'theme', 'type'], + inputs: ['contentId', { name: 'disabled', transform: nullableBooleanAttribute }, 'maxEdgeStart', 'menuId', 'mode', 'side', { name: 'swipeGesture', transform: nullableBooleanAttribute }, 'theme', 'type'], outputs: ['ionWillOpen', 'ionWillClose', 'ionDidOpen', 'ionDidClose'], standalone: false }) @@ -1636,7 +1637,7 @@ export declare interface IonMenu extends Components.IonMenu { changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['autoHide', 'color', 'disabled', 'menu', 'mode', 'theme', 'type'], + inputs: [{ name: 'autoHide', transform: nullableBooleanAttribute }, 'color', { name: 'disabled', transform: nullableBooleanAttribute }, 'menu', 'mode', 'theme', 'type'], standalone: false }) export class IonMenuButton { @@ -1659,7 +1660,7 @@ export declare interface IonMenuButton extends Components.IonMenuButton {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['autoHide', 'menu', 'mode', 'theme'], + inputs: [{ name: 'autoHide', transform: nullableBooleanAttribute }, 'menu', 'mode', 'theme'], standalone: false }) export class IonMenuToggle { @@ -1752,7 +1753,7 @@ export declare interface IonPicker extends Components.IonPicker {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'disabled', 'mode', 'theme', 'value'], + inputs: ['color', { name: 'disabled', transform: nullableBooleanAttribute }, 'mode', 'theme', 'value'], outputs: ['ionChange'], standalone: false }) @@ -1787,7 +1788,7 @@ This event will not emit when programmatically setting the `value` property. changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'disabled', 'mode', 'theme', 'value'], + inputs: ['color', { name: 'disabled', transform: nullableBooleanAttribute }, 'mode', 'theme', 'value'], standalone: false }) export class IonPickerColumnOption { @@ -1810,7 +1811,7 @@ export declare interface IonPickerColumnOption extends Components.IonPickerColum changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['buffer', 'color', 'mode', 'reversed', 'shape', 'theme', 'type', 'value'], + inputs: ['buffer', 'color', 'mode', { name: 'reversed', transform: nullableBooleanAttribute }, 'shape', 'theme', 'type', 'value'], standalone: false }) export class IonProgressBar { @@ -1833,7 +1834,7 @@ export declare interface IonProgressBar extends Components.IonProgressBar {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['alignment', 'color', 'disabled', 'justify', 'labelPlacement', 'mode', 'name', 'theme', 'value'], + inputs: ['alignment', 'color', { name: 'disabled', transform: nullableBooleanAttribute }, 'justify', 'labelPlacement', 'mode', 'name', 'theme', 'value'], outputs: ['ionFocus', 'ionBlur'], standalone: false }) @@ -1870,7 +1871,7 @@ export declare interface IonRadio extends Components.IonRadio { changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['allowEmptySelection', 'compareWith', 'errorText', 'helperText', 'mode', 'name', 'theme', 'value'], + inputs: [{ name: 'allowEmptySelection', transform: nullableBooleanAttribute }, 'compareWith', 'errorText', 'helperText', 'mode', 'name', 'theme', 'value'], outputs: ['ionChange'], standalone: false }) @@ -1905,7 +1906,7 @@ This event will not emit when programmatically setting the `value` property. changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['activeBarStart', 'color', 'debounce', 'disabled', 'dualKnobs', 'label', 'labelPlacement', 'max', 'min', 'mode', 'name', 'pin', 'pinFormatter', 'snaps', 'step', 'theme', 'ticks', 'value'], + inputs: ['activeBarStart', 'color', 'debounce', { name: 'disabled', transform: nullableBooleanAttribute }, { name: 'dualKnobs', transform: nullableBooleanAttribute }, 'label', 'labelPlacement', 'max', 'min', 'mode', 'name', { name: 'pin', transform: nullableBooleanAttribute }, 'pinFormatter', { name: 'snaps', transform: nullableBooleanAttribute }, 'step', 'theme', { name: 'ticks', transform: nullableBooleanAttribute }, 'value'], outputs: ['ionChange', 'ionInput', 'ionFocus', 'ionBlur', 'ionKnobMoveStart', 'ionKnobMoveEnd'], standalone: false }) @@ -1975,7 +1976,7 @@ mouse drag, touch gesture, or keyboard interaction. changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['closeDuration', 'disabled', 'mode', 'pullFactor', 'pullMax', 'pullMin', 'snapbackDuration', 'theme'], + inputs: ['closeDuration', { name: 'disabled', transform: nullableBooleanAttribute }, 'mode', 'pullFactor', 'pullMax', 'pullMin', 'snapbackDuration', 'theme'], outputs: ['ionRefresh', 'ionPull', 'ionStart', 'ionPullStart', 'ionPullEnd'], standalone: false }) @@ -2081,7 +2082,7 @@ export declare interface IonReorder extends Components.IonReorder {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['disabled', 'mode', 'theme'], + inputs: [{ name: 'disabled', transform: nullableBooleanAttribute }, 'mode', 'theme'], outputs: ['ionItemReorder', 'ionReorderStart', 'ionReorderMove', 'ionReorderEnd'], standalone: false }) @@ -2187,7 +2188,7 @@ export declare interface IonRow extends Components.IonRow {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['animated', 'autocapitalize', 'autocomplete', 'autocorrect', 'cancelButtonIcon', 'cancelButtonText', 'clearIcon', 'color', 'debounce', 'disabled', 'enterkeyhint', 'inputmode', 'maxlength', 'minlength', 'mode', 'name', 'placeholder', 'searchIcon', 'shape', 'showCancelButton', 'showClearButton', 'size', 'spellcheck', 'theme', 'type', 'value'], + inputs: [{ name: 'animated', transform: nullableBooleanAttribute }, 'autocapitalize', 'autocomplete', { name: 'autocorrect', transform: nullableBooleanAttribute }, 'cancelButtonIcon', 'cancelButtonText', 'clearIcon', 'color', 'debounce', { name: 'disabled', transform: nullableBooleanAttribute }, 'enterkeyhint', 'inputmode', 'maxlength', 'minlength', 'mode', 'name', 'placeholder', 'searchIcon', 'shape', 'showCancelButton', 'showClearButton', 'size', { name: 'spellcheck', transform: nullableBooleanAttribute }, 'theme', 'type', 'value'], outputs: ['ionInput', 'ionChange', 'ionCancel', 'ionClear', 'ionBlur', 'ionFocus'], standalone: false }) @@ -2255,7 +2256,7 @@ This event will not emit when programmatically setting the `value` property. changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'disabled', 'mode', 'scrollable', 'selectOnFocus', 'swipeGesture', 'theme', 'value'], + inputs: ['color', { name: 'disabled', transform: nullableBooleanAttribute }, 'mode', { name: 'scrollable', transform: nullableBooleanAttribute }, { name: 'selectOnFocus', transform: nullableBooleanAttribute }, { name: 'swipeGesture', transform: nullableBooleanAttribute }, 'theme', 'value'], outputs: ['ionChange'], standalone: false }) @@ -2290,7 +2291,7 @@ This event will not emit when programmatically setting the `value` property. changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['contentId', 'disabled', 'layout', 'mode', 'theme', 'type', 'value'], + inputs: ['contentId', { name: 'disabled', transform: nullableBooleanAttribute }, 'layout', 'mode', 'theme', 'type', 'value'], standalone: false }) export class IonSegmentButton { @@ -2335,7 +2336,7 @@ export declare interface IonSegmentContent extends Components.IonSegmentContent changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['disabled', 'swipeGesture'], + inputs: [{ name: 'disabled', transform: nullableBooleanAttribute }, { name: 'swipeGesture', transform: nullableBooleanAttribute }], outputs: ['ionSegmentViewScroll'], standalone: false }) @@ -2369,7 +2370,7 @@ export declare interface IonSegmentView extends Components.IonSegmentView { changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['cancelIcon', 'cancelText', 'color', 'compareWith', 'disabled', 'errorText', 'expandedIcon', 'fill', 'helperText', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'mode', 'multiple', 'name', 'okText', 'placeholder', 'required', 'selectedText', 'shape', 'size', 'theme', 'toggleIcon', 'value'], + inputs: [{ name: 'cancelIcon', transform: nullableBooleanAttribute }, 'cancelText', 'color', 'compareWith', { name: 'disabled', transform: nullableBooleanAttribute }, 'errorText', 'expandedIcon', 'fill', 'helperText', 'interface', 'interfaceOptions', 'justify', 'label', 'labelPlacement', 'mode', { name: 'multiple', transform: nullableBooleanAttribute }, 'name', 'okText', 'placeholder', { name: 'required', transform: nullableBooleanAttribute }, 'selectedText', 'shape', 'size', 'theme', 'toggleIcon', 'value'], outputs: ['ionChange', 'ionCancel', 'ionDismiss', 'ionFocus', 'ionBlur'], standalone: false }) @@ -2424,7 +2425,7 @@ This event will not emit when programmatically setting the `value` property. changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['cancelIcon', 'cancelText', 'header', 'multiple', 'options'], + inputs: [{ name: 'cancelIcon', transform: nullableBooleanAttribute }, 'cancelText', 'header', { name: 'multiple', transform: nullableBooleanAttribute }, 'options'], standalone: false }) export class IonSelectModal { @@ -2447,7 +2448,7 @@ export declare interface IonSelectModal extends Components.IonSelectModal {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['description', 'disabled', 'justify', 'labelPlacement', 'mode', 'theme', 'value'], + inputs: ['description', { name: 'disabled', transform: nullableBooleanAttribute }, 'justify', 'labelPlacement', 'mode', 'theme', 'value'], standalone: false }) export class IonSelectOption { @@ -2470,7 +2471,7 @@ export declare interface IonSelectOption extends Components.IonSelectOption {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['animated', 'mode', 'theme'], + inputs: [{ name: 'animated', transform: nullableBooleanAttribute }, 'mode', 'theme'], standalone: false }) export class IonSkeletonText { @@ -2493,7 +2494,7 @@ export declare interface IonSkeletonText extends Components.IonSkeletonText {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'duration', 'mode', 'name', 'paused', 'size', 'theme'], + inputs: ['color', 'duration', 'mode', 'name', { name: 'paused', transform: nullableBooleanAttribute }, 'size', 'theme'], standalone: false }) export class IonSpinner { @@ -2516,7 +2517,7 @@ export declare interface IonSpinner extends Components.IonSpinner {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['contentId', 'disabled', 'mode', 'theme', 'when'], + inputs: ['contentId', { name: 'disabled', transform: nullableBooleanAttribute }, 'mode', 'theme', 'when'], outputs: ['ionSplitPaneVisible'], standalone: false }) @@ -2572,7 +2573,7 @@ export declare interface IonTab extends Components.IonTab {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'expand', 'mode', 'scrollEffect', 'selectedTab', 'shape', 'theme', 'translucent'], + inputs: ['color', 'expand', 'mode', 'scrollEffect', 'selectedTab', 'shape', 'theme', { name: 'translucent', transform: nullableBooleanAttribute }], standalone: false }) export class IonTabBar { @@ -2595,7 +2596,7 @@ export declare interface IonTabBar extends Components.IonTabBar {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['disabled', 'download', 'href', 'layout', 'mode', 'rel', 'selected', 'shape', 'tab', 'target', 'theme'], + inputs: [{ name: 'disabled', transform: nullableBooleanAttribute }, 'download', 'href', 'layout', 'mode', 'rel', { name: 'selected', transform: nullableBooleanAttribute }, 'shape', 'tab', 'target', 'theme'], standalone: false }) export class IonTabButton { @@ -2642,7 +2643,7 @@ export declare interface IonText extends Components.IonText {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['autoGrow', 'autocapitalize', 'autofocus', 'clearOnEdit', 'color', 'cols', 'counter', 'counterFormatter', 'debounce', 'disabled', 'enterkeyhint', 'errorText', 'fill', 'helperText', 'inputmode', 'label', 'labelPlacement', 'maxlength', 'minlength', 'mode', 'name', 'placeholder', 'readonly', 'required', 'rows', 'shape', 'size', 'spellcheck', 'theme', 'value', 'wrap'], + inputs: [{ name: 'autoGrow', transform: nullableBooleanAttribute }, 'autocapitalize', { name: 'autofocus', transform: nullableBooleanAttribute }, { name: 'clearOnEdit', transform: nullableBooleanAttribute }, 'color', 'cols', { name: 'counter', transform: nullableBooleanAttribute }, 'counterFormatter', 'debounce', { name: 'disabled', transform: nullableBooleanAttribute }, 'enterkeyhint', 'errorText', 'fill', 'helperText', 'inputmode', 'label', 'labelPlacement', 'maxlength', 'minlength', 'mode', 'name', 'placeholder', { name: 'readonly', transform: nullableBooleanAttribute }, { name: 'required', transform: nullableBooleanAttribute }, 'rows', 'shape', 'size', { name: 'spellcheck', transform: nullableBooleanAttribute }, 'theme', 'value', 'wrap'], outputs: ['ionChange', 'ionInput', 'ionBlur', 'ionFocus'], standalone: false }) @@ -2747,7 +2748,7 @@ export declare interface IonTitle extends Components.IonTitle {} changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'htmlAttributes', 'hue', 'icon', 'isOpen', 'keyboardClose', 'layout', 'leaveAnimation', 'message', 'mode', 'position', 'positionAnchor', 'shape', 'swipeGesture', 'theme', 'translucent', 'trigger'], + inputs: [{ name: 'animated', transform: nullableBooleanAttribute }, 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'htmlAttributes', 'hue', 'icon', { name: 'isOpen', transform: nullableBooleanAttribute }, { name: 'keyboardClose', transform: nullableBooleanAttribute }, 'layout', 'leaveAnimation', 'message', 'mode', 'position', 'positionAnchor', 'shape', 'swipeGesture', 'theme', { name: 'translucent', transform: nullableBooleanAttribute }, 'trigger'], outputs: ['ionToastDidPresent', 'ionToastWillPresent', 'ionToastWillDismiss', 'ionToastDidDismiss', 'didPresent', 'willPresent', 'willDismiss', 'didDismiss'], standalone: false }) @@ -2819,7 +2820,7 @@ Shorthand for ionToastDidDismiss. changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['alignment', 'checked', 'color', 'disabled', 'enableOnOffLabels', 'errorText', 'helperText', 'justify', 'labelPlacement', 'mode', 'name', 'required', 'theme', 'value'], + inputs: ['alignment', { name: 'checked', transform: nullableBooleanAttribute }, 'color', { name: 'disabled', transform: nullableBooleanAttribute }, { name: 'enableOnOffLabels', transform: nullableBooleanAttribute }, 'errorText', 'helperText', 'justify', 'labelPlacement', 'mode', 'name', { name: 'required', transform: nullableBooleanAttribute }, 'theme', 'value'], outputs: ['ionChange', 'ionFocus', 'ionBlur'], standalone: false }) diff --git a/packages/angular/src/standalone/directives/angular-component-lib/boolean-attribute.ts b/packages/angular/src/standalone/directives/angular-component-lib/boolean-attribute.ts new file mode 100644 index 00000000000..c116dd5bc7f --- /dev/null +++ b/packages/angular/src/standalone/directives/angular-component-lib/boolean-attribute.ts @@ -0,0 +1,43 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Transforms a value to a boolean so that boolean properties can be set by attribute presence, + * e.g. `` instead of ``. + * + * Strings are coerced the same way Angular's `booleanAttribute` coerces them, so `''` (a bare + * attribute) becomes `true` and `'false'` becomes `false`. + * + * Unlike Angular's `booleanAttribute`, `null` and `undefined` are passed through rather than + * coerced to `false`. Components frequently treat them as a state distinct from `false`: + * + * ```tsx + * // `undefined` means "decide based on the mode", which is not the same as `false` + * const showDetail = detail !== undefined ? detail : mode === 'ios'; + * + * // a strict comparison also behaves differently for `null` than it does for `false` + * const showHandle = handle !== false; + * ``` + * + * Both values reach inputs routinely in Angular templates, from the `async` pipe before its + * first emission and from form control values, so coercing them would change the behavior of + * bindings that work today. + * + * This is implemented here rather than imported from `@angular/core` so that consumers on + * Angular versions without `booleanAttribute` are unaffected by this file being generated. + * + * The parameter type is what Angular derives `ngAcceptInputType_*` from, so it decides which + * template bindings compile. Widening it to `unknown` would let any expression through. + * + * Declared as a function rather than an arrow constant because Angular has to resolve input + * transforms statically when compiling a library in partial compilation mode. + * + * This lives in its own file, separate from `utils.ts`, so that it carries no runtime imports. + * That keeps it independently type-checkable without pulling `rxjs` in for `proxyOutputs`. + */ +export function nullableBooleanAttribute(value: boolean | string | null | undefined): boolean | null | undefined { + if (value === null || value === undefined) { + return value; + } + return typeof value === 'boolean' ? value : value !== 'false'; +} diff --git a/packages/angular/src/standalone/directives/checkbox.ts b/packages/angular/src/standalone/directives/checkbox.ts index a905e050d5c..8e4a873a52a 100644 --- a/packages/angular/src/standalone/directives/checkbox.ts +++ b/packages/angular/src/standalone/directives/checkbox.ts @@ -14,15 +14,16 @@ import { ValueAccessor, setIonicClasses } from '@ionic/angular/common'; import type { CheckboxChangeEventDetail, Components } from '@ionic/core/components'; import { defineCustomElement } from '@ionic/core/components/ion-checkbox.js'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; const CHECKBOX_INPUTS = [ - 'checked', + { name: 'checked', transform: nullableBooleanAttribute }, 'color', - 'disabled', + { name: 'disabled', transform: nullableBooleanAttribute }, 'errorText', 'helperText', - 'indeterminate', + { name: 'indeterminate', transform: nullableBooleanAttribute }, 'justify', 'labelPlacement', 'mode', @@ -30,9 +31,12 @@ const CHECKBOX_INPUTS = [ 'value', ]; +/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ +const CHECKBOX_PROXY_INPUTS = CHECKBOX_INPUTS.map((input) => (typeof input === 'string' ? input : input.name)); + @ProxyCmp({ defineCustomElementFn: defineCustomElement, - inputs: CHECKBOX_INPUTS, + inputs: CHECKBOX_PROXY_INPUTS, }) @Component({ selector: 'ion-checkbox', diff --git a/packages/angular/src/standalone/directives/datetime.ts b/packages/angular/src/standalone/directives/datetime.ts index e808011dfd1..b2d80e54108 100644 --- a/packages/angular/src/standalone/directives/datetime.ts +++ b/packages/angular/src/standalone/directives/datetime.ts @@ -14,6 +14,7 @@ import { ValueAccessor } from '@ionic/angular/common'; import type { DatetimeChangeEventDetail, Components } from '@ionic/core/components'; import { defineCustomElement } from '@ionic/core/components/ion-datetime.js'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; const DATETIME_INPUTS = [ @@ -21,7 +22,7 @@ const DATETIME_INPUTS = [ 'clearText', 'color', 'dayValues', - 'disabled', + { name: 'disabled', transform: nullableBooleanAttribute }, 'doneText', 'firstDayOfWeek', 'formatOptions', @@ -35,25 +36,28 @@ const DATETIME_INPUTS = [ 'minuteValues', 'mode', 'monthValues', - 'multiple', + { name: 'multiple', transform: nullableBooleanAttribute }, 'name', - 'preferWheel', + { name: 'preferWheel', transform: nullableBooleanAttribute }, 'presentation', - 'readonly', - 'showAdjacentDays', - 'showClearButton', - 'showDefaultButtons', - 'showDefaultTimeLabel', - 'showDefaultTitle', + { name: 'readonly', transform: nullableBooleanAttribute }, + { name: 'showAdjacentDays', transform: nullableBooleanAttribute }, + { name: 'showClearButton', transform: nullableBooleanAttribute }, + { name: 'showDefaultButtons', transform: nullableBooleanAttribute }, + { name: 'showDefaultTimeLabel', transform: nullableBooleanAttribute }, + { name: 'showDefaultTitle', transform: nullableBooleanAttribute }, 'size', 'titleSelectedDatesFormatter', 'value', 'yearValues', ]; +/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ +const DATETIME_PROXY_INPUTS = DATETIME_INPUTS.map((input) => (typeof input === 'string' ? input : input.name)); + @ProxyCmp({ defineCustomElementFn: defineCustomElement, - inputs: DATETIME_INPUTS, + inputs: DATETIME_PROXY_INPUTS, methods: ['confirm', 'reset', 'cancel'], }) @Component({ diff --git a/packages/angular/src/standalone/directives/icon.ts b/packages/angular/src/standalone/directives/icon.ts index 132a3a30b29..3fc63bde395 100644 --- a/packages/angular/src/standalone/directives/icon.ts +++ b/packages/angular/src/standalone/directives/icon.ts @@ -1,18 +1,36 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone } from '@angular/core'; import { defineCustomElement as defineIonIcon } from 'ionicons/components/ion-icon.js'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import { ProxyCmp } from './angular-component-lib/utils'; +const ICON_INPUTS = [ + 'color', + { name: 'flipRtl', transform: nullableBooleanAttribute }, + 'icon', + 'ios', + { name: 'lazy', transform: nullableBooleanAttribute }, + 'md', + 'mode', + 'name', + { name: 'sanitize', transform: nullableBooleanAttribute }, + 'size', + 'src', +]; + +/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ +const ICON_PROXY_INPUTS = ICON_INPUTS.map((input) => (typeof input === 'string' ? input : input.name)); + @ProxyCmp({ defineCustomElementFn: defineIonIcon, - inputs: ['color', 'flipRtl', 'icon', 'ios', 'lazy', 'md', 'mode', 'name', 'sanitize', 'size', 'src'], + inputs: ICON_PROXY_INPUTS, }) @Component({ selector: 'ion-icon', changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'flipRtl', 'icon', 'ios', 'lazy', 'md', 'mode', 'name', 'sanitize', 'size', 'src'], + inputs: ICON_INPUTS, standalone: true, }) export class IonIcon { diff --git a/packages/angular/src/standalone/directives/input-otp.ts b/packages/angular/src/standalone/directives/input-otp.ts index fe6b983174f..c0b46624681 100644 --- a/packages/angular/src/standalone/directives/input-otp.ts +++ b/packages/angular/src/standalone/directives/input-otp.ts @@ -19,17 +19,18 @@ import type { } from '@ionic/core/components'; import { defineCustomElement } from '@ionic/core/components/ion-input-otp.js'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; const INPUT_OTP_INPUTS = [ 'autocapitalize', 'color', - 'disabled', + { name: 'disabled', transform: nullableBooleanAttribute }, 'fill', 'inputmode', 'length', 'pattern', - 'readonly', + { name: 'readonly', transform: nullableBooleanAttribute }, 'separators', 'shape', 'size', @@ -37,9 +38,12 @@ const INPUT_OTP_INPUTS = [ 'value', ]; +/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ +const INPUT_OTP_PROXY_INPUTS = INPUT_OTP_INPUTS.map((input) => (typeof input === 'string' ? input : input.name)); + @ProxyCmp({ defineCustomElementFn: defineCustomElement, - inputs: INPUT_OTP_INPUTS, + inputs: INPUT_OTP_PROXY_INPUTS, methods: ['setFocus'], }) @Component({ diff --git a/packages/angular/src/standalone/directives/input.ts b/packages/angular/src/standalone/directives/input.ts index b05ca0e3bcf..e8ba4e1c85c 100644 --- a/packages/angular/src/standalone/directives/input.ts +++ b/packages/angular/src/standalone/directives/input.ts @@ -18,21 +18,22 @@ import type { } from '@ionic/core/components'; import { defineCustomElement } from '@ionic/core/components/ion-input.js'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; const INPUT_INPUTS = [ 'accept', 'autocapitalize', 'autocomplete', - 'autocorrect', - 'autofocus', - 'clearInput', - 'clearOnEdit', + { name: 'autocorrect', transform: nullableBooleanAttribute }, + { name: 'autofocus', transform: nullableBooleanAttribute }, + { name: 'clearInput', transform: nullableBooleanAttribute }, + { name: 'clearOnEdit', transform: nullableBooleanAttribute }, 'color', - 'counter', + { name: 'counter', transform: nullableBooleanAttribute }, 'counterFormatter', 'debounce', - 'disabled', + { name: 'disabled', transform: nullableBooleanAttribute }, 'enterkeyhint', 'errorText', 'fill', @@ -45,23 +46,26 @@ const INPUT_INPUTS = [ 'min', 'minlength', 'mode', - 'multiple', + { name: 'multiple', transform: nullableBooleanAttribute }, 'name', 'pattern', 'placeholder', - 'readonly', - 'required', + { name: 'readonly', transform: nullableBooleanAttribute }, + { name: 'required', transform: nullableBooleanAttribute }, 'shape', 'size', - 'spellcheck', + { name: 'spellcheck', transform: nullableBooleanAttribute }, 'step', 'type', 'value', ]; +/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ +const INPUT_PROXY_INPUTS = INPUT_INPUTS.map((input) => (typeof input === 'string' ? input : input.name)); + @ProxyCmp({ defineCustomElementFn: defineCustomElement, - inputs: INPUT_INPUTS, + inputs: INPUT_PROXY_INPUTS, methods: ['setFocus', 'getInputElement'], }) @Component({ diff --git a/packages/angular/src/standalone/directives/ion-accordion-group.ts b/packages/angular/src/standalone/directives/ion-accordion-group.ts index 8278f1ef302..26f7b8d43a0 100644 --- a/packages/angular/src/standalone/directives/ion-accordion-group.ts +++ b/packages/angular/src/standalone/directives/ion-accordion-group.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone, EventEmitter, Output } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonAccordionGroup } from '@ionic/core/comp changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['animated', 'disabled', 'expand', 'mode', 'multiple', 'readonly', 'shape', 'theme', 'value'], + inputs: [{ name: 'animated', transform: nullableBooleanAttribute }, { name: 'disabled', transform: nullableBooleanAttribute }, 'expand', 'mode', { name: 'multiple', transform: nullableBooleanAttribute }, { name: 'readonly', transform: nullableBooleanAttribute }, 'shape', 'theme', 'value'], outputs: ['ionChange'], }) export class IonAccordionGroup { diff --git a/packages/angular/src/standalone/directives/ion-accordion.ts b/packages/angular/src/standalone/directives/ion-accordion.ts index 12e392a2c4d..5828dd882bb 100644 --- a/packages/angular/src/standalone/directives/ion-accordion.ts +++ b/packages/angular/src/standalone/directives/ion-accordion.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonAccordion } from '@ionic/core/component changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['disabled', 'mode', 'readonly', 'theme', 'toggleIcon', 'toggleIconSlot', 'value'], + inputs: [{ name: 'disabled', transform: nullableBooleanAttribute }, 'mode', { name: 'readonly', transform: nullableBooleanAttribute }, 'theme', 'toggleIcon', 'toggleIconSlot', 'value'], }) export class IonAccordion { protected el: HTMLIonAccordionElement; diff --git a/packages/angular/src/standalone/directives/ion-action-sheet.ts b/packages/angular/src/standalone/directives/ion-action-sheet.ts index a61e72cffa3..defe90be74e 100644 --- a/packages/angular/src/standalone/directives/ion-action-sheet.ts +++ b/packages/angular/src/standalone/directives/ion-action-sheet.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone, EventEmitter, Output } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -18,7 +19,7 @@ import { defineCustomElement as defineIonActionSheet } from '@ionic/core/compone changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['animated', 'backdropDismiss', 'buttons', 'cssClass', 'enterAnimation', 'header', 'htmlAttributes', 'isOpen', 'keyboardClose', 'leaveAnimation', 'mode', 'subHeader', 'theme', 'translucent', 'trigger'], + inputs: [{ name: 'animated', transform: nullableBooleanAttribute }, { name: 'backdropDismiss', transform: nullableBooleanAttribute }, 'buttons', 'cssClass', 'enterAnimation', 'header', 'htmlAttributes', { name: 'isOpen', transform: nullableBooleanAttribute }, { name: 'keyboardClose', transform: nullableBooleanAttribute }, 'leaveAnimation', 'mode', 'subHeader', 'theme', { name: 'translucent', transform: nullableBooleanAttribute }, 'trigger'], outputs: ['ionActionSheetDidPresent', 'ionActionSheetWillPresent', 'ionActionSheetWillDismiss', 'ionActionSheetDidDismiss', 'didPresent', 'willPresent', 'willDismiss', 'didDismiss'], }) export class IonActionSheet { diff --git a/packages/angular/src/standalone/directives/ion-alert.ts b/packages/angular/src/standalone/directives/ion-alert.ts index 153bdb2ca7a..2c793b184f7 100644 --- a/packages/angular/src/standalone/directives/ion-alert.ts +++ b/packages/angular/src/standalone/directives/ion-alert.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone, EventEmitter, Output } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -18,7 +19,7 @@ import { defineCustomElement as defineIonAlert } from '@ionic/core/components/io changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['animated', 'backdropDismiss', 'buttons', 'cssClass', 'enterAnimation', 'header', 'htmlAttributes', 'inputs', 'isOpen', 'keyboardClose', 'leaveAnimation', 'message', 'mode', 'subHeader', 'theme', 'translucent', 'trigger'], + inputs: [{ name: 'animated', transform: nullableBooleanAttribute }, { name: 'backdropDismiss', transform: nullableBooleanAttribute }, 'buttons', 'cssClass', 'enterAnimation', 'header', 'htmlAttributes', 'inputs', { name: 'isOpen', transform: nullableBooleanAttribute }, { name: 'keyboardClose', transform: nullableBooleanAttribute }, 'leaveAnimation', 'message', 'mode', 'subHeader', 'theme', { name: 'translucent', transform: nullableBooleanAttribute }, 'trigger'], outputs: ['ionAlertDidPresent', 'ionAlertWillPresent', 'ionAlertWillDismiss', 'ionAlertDidDismiss', 'didPresent', 'willPresent', 'willDismiss', 'didDismiss'], }) export class IonAlert { diff --git a/packages/angular/src/standalone/directives/ion-avatar.ts b/packages/angular/src/standalone/directives/ion-avatar.ts index 423b8880870..768ffc588e2 100644 --- a/packages/angular/src/standalone/directives/ion-avatar.ts +++ b/packages/angular/src/standalone/directives/ion-avatar.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonAvatar } from '@ionic/core/components/i changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['disabled', 'mode', 'shape', 'size', 'theme'], + inputs: [{ name: 'disabled', transform: nullableBooleanAttribute }, 'mode', 'shape', 'size', 'theme'], }) export class IonAvatar { protected el: HTMLIonAvatarElement; diff --git a/packages/angular/src/standalone/directives/ion-backdrop.ts b/packages/angular/src/standalone/directives/ion-backdrop.ts index 33505eb3f01..a1a21f3dee1 100644 --- a/packages/angular/src/standalone/directives/ion-backdrop.ts +++ b/packages/angular/src/standalone/directives/ion-backdrop.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone, EventEmitter, Output } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonBackdrop } from '@ionic/core/components changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['mode', 'stopPropagation', 'tappable', 'theme', 'visible'], + inputs: ['mode', { name: 'stopPropagation', transform: nullableBooleanAttribute }, { name: 'tappable', transform: nullableBooleanAttribute }, 'theme', { name: 'visible', transform: nullableBooleanAttribute }], outputs: ['ionBackdropTap'], }) export class IonBackdrop { diff --git a/packages/angular/src/standalone/directives/ion-breadcrumb.ts b/packages/angular/src/standalone/directives/ion-breadcrumb.ts index 31c55d9e5bc..db3b35937d4 100644 --- a/packages/angular/src/standalone/directives/ion-breadcrumb.ts +++ b/packages/angular/src/standalone/directives/ion-breadcrumb.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone, EventEmitter, Output } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonBreadcrumb } from '@ionic/core/componen changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['active', 'color', 'disabled', 'download', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'separator', 'target', 'theme'], + inputs: [{ name: 'active', transform: nullableBooleanAttribute }, 'color', { name: 'disabled', transform: nullableBooleanAttribute }, 'download', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', { name: 'separator', transform: nullableBooleanAttribute }, 'target', 'theme'], outputs: ['ionFocus', 'ionBlur'], }) export class IonBreadcrumb { diff --git a/packages/angular/src/standalone/directives/ion-button.ts b/packages/angular/src/standalone/directives/ion-button.ts index 1f764900f69..d5f9b45a65f 100644 --- a/packages/angular/src/standalone/directives/ion-button.ts +++ b/packages/angular/src/standalone/directives/ion-button.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone, EventEmitter, Output } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonButton } from '@ionic/core/components/i changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['buttonType', 'color', 'disabled', 'download', 'expand', 'fill', 'form', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'size', 'strong', 'target', 'theme', 'type'], + inputs: ['buttonType', 'color', { name: 'disabled', transform: nullableBooleanAttribute }, 'download', 'expand', 'fill', 'form', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'size', { name: 'strong', transform: nullableBooleanAttribute }, 'target', 'theme', 'type'], outputs: ['ionFocus', 'ionBlur'], }) export class IonButton { diff --git a/packages/angular/src/standalone/directives/ion-buttons.ts b/packages/angular/src/standalone/directives/ion-buttons.ts index c4e9284a99b..f9d528cac04 100644 --- a/packages/angular/src/standalone/directives/ion-buttons.ts +++ b/packages/angular/src/standalone/directives/ion-buttons.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonButtons } from '@ionic/core/components/ changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['collapse', 'mode', 'theme'], + inputs: [{ name: 'collapse', transform: nullableBooleanAttribute }, 'mode', 'theme'], }) export class IonButtons { protected el: HTMLIonButtonsElement; diff --git a/packages/angular/src/standalone/directives/ion-card-header.ts b/packages/angular/src/standalone/directives/ion-card-header.ts index 92fd365a16f..632d825bd73 100644 --- a/packages/angular/src/standalone/directives/ion-card-header.ts +++ b/packages/angular/src/standalone/directives/ion-card-header.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonCardHeader } from '@ionic/core/componen changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'mode', 'theme', 'translucent'], + inputs: ['color', 'mode', 'theme', { name: 'translucent', transform: nullableBooleanAttribute }], }) export class IonCardHeader { protected el: HTMLIonCardHeaderElement; diff --git a/packages/angular/src/standalone/directives/ion-card.ts b/packages/angular/src/standalone/directives/ion-card.ts index 44c634020ed..7b17c614969 100644 --- a/packages/angular/src/standalone/directives/ion-card.ts +++ b/packages/angular/src/standalone/directives/ion-card.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonCard } from '@ionic/core/components/ion changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['button', 'color', 'disabled', 'download', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'target', 'theme', 'type'], + inputs: [{ name: 'button', transform: nullableBooleanAttribute }, 'color', { name: 'disabled', transform: nullableBooleanAttribute }, 'download', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'shape', 'target', 'theme', 'type'], }) export class IonCard { protected el: HTMLIonCardElement; diff --git a/packages/angular/src/standalone/directives/ion-chip.ts b/packages/angular/src/standalone/directives/ion-chip.ts index 06ddc81a617..769511d0014 100644 --- a/packages/angular/src/standalone/directives/ion-chip.ts +++ b/packages/angular/src/standalone/directives/ion-chip.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonChip } from '@ionic/core/components/ion changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'disabled', 'hue', 'mode', 'outline', 'shape', 'size', 'theme'], + inputs: ['color', { name: 'disabled', transform: nullableBooleanAttribute }, 'hue', 'mode', { name: 'outline', transform: nullableBooleanAttribute }, 'shape', 'size', 'theme'], }) export class IonChip { protected el: HTMLIonChipElement; diff --git a/packages/angular/src/standalone/directives/ion-content.ts b/packages/angular/src/standalone/directives/ion-content.ts index 67ab7940048..a583ecd431e 100644 --- a/packages/angular/src/standalone/directives/ion-content.ts +++ b/packages/angular/src/standalone/directives/ion-content.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone, EventEmitter, Output } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -18,7 +19,7 @@ import { defineCustomElement as defineIonContent } from '@ionic/core/components/ changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'fixedSlotPlacement', 'forceOverscroll', 'fullscreen', 'mode', 'scrollEvents', 'scrollX', 'scrollY', 'theme'], + inputs: ['color', 'fixedSlotPlacement', { name: 'forceOverscroll', transform: nullableBooleanAttribute }, { name: 'fullscreen', transform: nullableBooleanAttribute }, 'mode', { name: 'scrollEvents', transform: nullableBooleanAttribute }, { name: 'scrollX', transform: nullableBooleanAttribute }, { name: 'scrollY', transform: nullableBooleanAttribute }, 'theme'], outputs: ['ionScrollStart', 'ionScroll', 'ionScrollEnd'], }) export class IonContent { diff --git a/packages/angular/src/standalone/directives/ion-datetime-button.ts b/packages/angular/src/standalone/directives/ion-datetime-button.ts index 428e6db03f0..f8bc086c00e 100644 --- a/packages/angular/src/standalone/directives/ion-datetime-button.ts +++ b/packages/angular/src/standalone/directives/ion-datetime-button.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonDatetimeButton } from '@ionic/core/comp changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'datetime', 'disabled', 'mode', 'theme'], + inputs: ['color', 'datetime', { name: 'disabled', transform: nullableBooleanAttribute }, 'mode', 'theme'], }) export class IonDatetimeButton { protected el: HTMLIonDatetimeButtonElement; diff --git a/packages/angular/src/standalone/directives/ion-divider.ts b/packages/angular/src/standalone/directives/ion-divider.ts index 49d771173f4..6234d0aca28 100644 --- a/packages/angular/src/standalone/directives/ion-divider.ts +++ b/packages/angular/src/standalone/directives/ion-divider.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonDivider } from '@ionic/core/components/ changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['inset', 'spacing'], + inputs: [{ name: 'inset', transform: nullableBooleanAttribute }, 'spacing'], }) export class IonDivider { protected el: HTMLIonDividerElement; diff --git a/packages/angular/src/standalone/directives/ion-fab-button.ts b/packages/angular/src/standalone/directives/ion-fab-button.ts index 9145a4ba329..f3e4e3b46af 100644 --- a/packages/angular/src/standalone/directives/ion-fab-button.ts +++ b/packages/angular/src/standalone/directives/ion-fab-button.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone, EventEmitter, Output } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonFabButton } from '@ionic/core/component changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['activated', 'closeIcon', 'color', 'disabled', 'download', 'form', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'show', 'size', 'target', 'theme', 'translucent', 'type'], + inputs: [{ name: 'activated', transform: nullableBooleanAttribute }, 'closeIcon', 'color', { name: 'disabled', transform: nullableBooleanAttribute }, 'download', 'form', 'href', 'mode', 'rel', 'routerAnimation', 'routerDirection', { name: 'show', transform: nullableBooleanAttribute }, 'size', 'target', 'theme', { name: 'translucent', transform: nullableBooleanAttribute }, 'type'], outputs: ['ionFocus', 'ionBlur'], }) export class IonFabButton { diff --git a/packages/angular/src/standalone/directives/ion-fab-list.ts b/packages/angular/src/standalone/directives/ion-fab-list.ts index 3668da35637..d29ec64591b 100644 --- a/packages/angular/src/standalone/directives/ion-fab-list.ts +++ b/packages/angular/src/standalone/directives/ion-fab-list.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonFabList } from '@ionic/core/components/ changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['activated', 'mode', 'side', 'theme'], + inputs: [{ name: 'activated', transform: nullableBooleanAttribute }, 'mode', 'side', 'theme'], }) export class IonFabList { protected el: HTMLIonFabListElement; diff --git a/packages/angular/src/standalone/directives/ion-fab.ts b/packages/angular/src/standalone/directives/ion-fab.ts index 64bd6b59a60..682dca1eb49 100644 --- a/packages/angular/src/standalone/directives/ion-fab.ts +++ b/packages/angular/src/standalone/directives/ion-fab.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -18,7 +19,7 @@ import { defineCustomElement as defineIonFab } from '@ionic/core/components/ion- changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['activated', 'edge', 'horizontal', 'mode', 'theme', 'vertical'], + inputs: [{ name: 'activated', transform: nullableBooleanAttribute }, { name: 'edge', transform: nullableBooleanAttribute }, 'horizontal', 'mode', 'theme', 'vertical'], }) export class IonFab { protected el: HTMLIonFabElement; diff --git a/packages/angular/src/standalone/directives/ion-footer.ts b/packages/angular/src/standalone/directives/ion-footer.ts index a33dee3f146..c225b13b943 100644 --- a/packages/angular/src/standalone/directives/ion-footer.ts +++ b/packages/angular/src/standalone/directives/ion-footer.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonFooter } from '@ionic/core/components/i changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['collapse', 'mode', 'scrollEffect', 'theme', 'translucent'], + inputs: ['collapse', 'mode', 'scrollEffect', 'theme', { name: 'translucent', transform: nullableBooleanAttribute }], }) export class IonFooter { protected el: HTMLIonFooterElement; diff --git a/packages/angular/src/standalone/directives/ion-grid.ts b/packages/angular/src/standalone/directives/ion-grid.ts index e2ae3aa64fe..3d5f3ae7e52 100644 --- a/packages/angular/src/standalone/directives/ion-grid.ts +++ b/packages/angular/src/standalone/directives/ion-grid.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonGrid } from '@ionic/core/components/ion changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['fixed', 'mode', 'theme'], + inputs: [{ name: 'fixed', transform: nullableBooleanAttribute }, 'mode', 'theme'], }) export class IonGrid { protected el: HTMLIonGridElement; diff --git a/packages/angular/src/standalone/directives/ion-header.ts b/packages/angular/src/standalone/directives/ion-header.ts index e77b039692b..6f4942380ae 100644 --- a/packages/angular/src/standalone/directives/ion-header.ts +++ b/packages/angular/src/standalone/directives/ion-header.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonHeader } from '@ionic/core/components/i changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['collapse', 'divider', 'mode', 'scrollEffect', 'theme', 'translucent'], + inputs: ['collapse', { name: 'divider', transform: nullableBooleanAttribute }, 'mode', 'scrollEffect', 'theme', { name: 'translucent', transform: nullableBooleanAttribute }], }) export class IonHeader { protected el: HTMLIonHeaderElement; diff --git a/packages/angular/src/standalone/directives/ion-infinite-scroll.ts b/packages/angular/src/standalone/directives/ion-infinite-scroll.ts index 2e4d9b4cd96..3367f9b1b5f 100644 --- a/packages/angular/src/standalone/directives/ion-infinite-scroll.ts +++ b/packages/angular/src/standalone/directives/ion-infinite-scroll.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone, EventEmitter, Output } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -18,7 +19,7 @@ import { defineCustomElement as defineIonInfiniteScroll } from '@ionic/core/comp changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['disabled', 'mode', 'position', 'preserveRerenderScrollPosition', 'theme', 'threshold'], + inputs: [{ name: 'disabled', transform: nullableBooleanAttribute }, 'mode', 'position', { name: 'preserveRerenderScrollPosition', transform: nullableBooleanAttribute }, 'theme', 'threshold'], outputs: ['ionInfinite'], }) export class IonInfiniteScroll { diff --git a/packages/angular/src/standalone/directives/ion-item-divider.ts b/packages/angular/src/standalone/directives/ion-item-divider.ts index d6c03e0ca09..7f7c21c9399 100644 --- a/packages/angular/src/standalone/directives/ion-item-divider.ts +++ b/packages/angular/src/standalone/directives/ion-item-divider.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonItemDivider } from '@ionic/core/compone changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'mode', 'sticky', 'theme'], + inputs: ['color', 'mode', { name: 'sticky', transform: nullableBooleanAttribute }, 'theme'], }) export class IonItemDivider { protected el: HTMLIonItemDividerElement; diff --git a/packages/angular/src/standalone/directives/ion-item-option.ts b/packages/angular/src/standalone/directives/ion-item-option.ts index 6ae14174968..506cc17faf2 100644 --- a/packages/angular/src/standalone/directives/ion-item-option.ts +++ b/packages/angular/src/standalone/directives/ion-item-option.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonItemOption } from '@ionic/core/componen changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'disabled', 'download', 'expandable', 'href', 'hue', 'mode', 'rel', 'shape', 'target', 'theme', 'type'], + inputs: ['color', { name: 'disabled', transform: nullableBooleanAttribute }, 'download', { name: 'expandable', transform: nullableBooleanAttribute }, 'href', 'hue', 'mode', 'rel', 'shape', 'target', 'theme', 'type'], }) export class IonItemOption { protected el: HTMLIonItemOptionElement; diff --git a/packages/angular/src/standalone/directives/ion-item-sliding.ts b/packages/angular/src/standalone/directives/ion-item-sliding.ts index d4ee5b2dc9e..db0d07a1009 100644 --- a/packages/angular/src/standalone/directives/ion-item-sliding.ts +++ b/packages/angular/src/standalone/directives/ion-item-sliding.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone, EventEmitter, Output } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -18,7 +19,7 @@ import { defineCustomElement as defineIonItemSliding } from '@ionic/core/compone changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['disabled', 'mode', 'theme'], + inputs: [{ name: 'disabled', transform: nullableBooleanAttribute }, 'mode', 'theme'], outputs: ['ionDrag'], }) export class IonItemSliding { diff --git a/packages/angular/src/standalone/directives/ion-item.ts b/packages/angular/src/standalone/directives/ion-item.ts index ee35fcae062..fdcc2b81654 100644 --- a/packages/angular/src/standalone/directives/ion-item.ts +++ b/packages/angular/src/standalone/directives/ion-item.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonItem } from '@ionic/core/components/ion changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['button', 'color', 'detail', 'detailIcon', 'disabled', 'download', 'href', 'lines', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'target', 'theme', 'type'], + inputs: [{ name: 'button', transform: nullableBooleanAttribute }, 'color', { name: 'detail', transform: nullableBooleanAttribute }, 'detailIcon', { name: 'disabled', transform: nullableBooleanAttribute }, 'download', 'href', 'lines', 'mode', 'rel', 'routerAnimation', 'routerDirection', 'target', 'theme', 'type'], }) export class IonItem { protected el: HTMLIonItemElement; diff --git a/packages/angular/src/standalone/directives/ion-list.ts b/packages/angular/src/standalone/directives/ion-list.ts index 0c6287fd9d3..a61888b29c5 100644 --- a/packages/angular/src/standalone/directives/ion-list.ts +++ b/packages/angular/src/standalone/directives/ion-list.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -18,7 +19,7 @@ import { defineCustomElement as defineIonList } from '@ionic/core/components/ion changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['inset', 'lines', 'mode', 'shape', 'theme'], + inputs: [{ name: 'inset', transform: nullableBooleanAttribute }, 'lines', 'mode', 'shape', 'theme'], }) export class IonList { protected el: HTMLIonListElement; diff --git a/packages/angular/src/standalone/directives/ion-loading.ts b/packages/angular/src/standalone/directives/ion-loading.ts index e2092ced2dd..24da22255d3 100644 --- a/packages/angular/src/standalone/directives/ion-loading.ts +++ b/packages/angular/src/standalone/directives/ion-loading.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone, EventEmitter, Output } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -18,7 +19,7 @@ import { defineCustomElement as defineIonLoading } from '@ionic/core/components/ changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['animated', 'backdropDismiss', 'cssClass', 'duration', 'enterAnimation', 'htmlAttributes', 'isOpen', 'keyboardClose', 'leaveAnimation', 'message', 'mode', 'showBackdrop', 'spinner', 'theme', 'translucent', 'trigger'], + inputs: [{ name: 'animated', transform: nullableBooleanAttribute }, { name: 'backdropDismiss', transform: nullableBooleanAttribute }, 'cssClass', 'duration', 'enterAnimation', 'htmlAttributes', { name: 'isOpen', transform: nullableBooleanAttribute }, { name: 'keyboardClose', transform: nullableBooleanAttribute }, 'leaveAnimation', 'message', 'mode', { name: 'showBackdrop', transform: nullableBooleanAttribute }, 'spinner', 'theme', { name: 'translucent', transform: nullableBooleanAttribute }, 'trigger'], outputs: ['ionLoadingDidPresent', 'ionLoadingWillPresent', 'ionLoadingWillDismiss', 'ionLoadingDidDismiss', 'didPresent', 'willPresent', 'willDismiss', 'didDismiss'], }) export class IonLoading { diff --git a/packages/angular/src/standalone/directives/ion-menu-button.ts b/packages/angular/src/standalone/directives/ion-menu-button.ts index 224d594e576..782d80e7221 100644 --- a/packages/angular/src/standalone/directives/ion-menu-button.ts +++ b/packages/angular/src/standalone/directives/ion-menu-button.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonMenuButton } from '@ionic/core/componen changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['autoHide', 'color', 'disabled', 'menu', 'mode', 'theme', 'type'], + inputs: [{ name: 'autoHide', transform: nullableBooleanAttribute }, 'color', { name: 'disabled', transform: nullableBooleanAttribute }, 'menu', 'mode', 'theme', 'type'], }) export class IonMenuButton { protected el: HTMLIonMenuButtonElement; diff --git a/packages/angular/src/standalone/directives/ion-menu-toggle.ts b/packages/angular/src/standalone/directives/ion-menu-toggle.ts index 9c9202d084a..b067410ac54 100644 --- a/packages/angular/src/standalone/directives/ion-menu-toggle.ts +++ b/packages/angular/src/standalone/directives/ion-menu-toggle.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonMenuToggle } from '@ionic/core/componen changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['autoHide', 'menu', 'mode', 'theme'], + inputs: [{ name: 'autoHide', transform: nullableBooleanAttribute }, 'menu', 'mode', 'theme'], }) export class IonMenuToggle { protected el: HTMLIonMenuToggleElement; diff --git a/packages/angular/src/standalone/directives/ion-menu.ts b/packages/angular/src/standalone/directives/ion-menu.ts index 34900e3465a..e5c5b4dc7a2 100644 --- a/packages/angular/src/standalone/directives/ion-menu.ts +++ b/packages/angular/src/standalone/directives/ion-menu.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone, EventEmitter, Output } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -18,7 +19,7 @@ import { defineCustomElement as defineIonMenu } from '@ionic/core/components/ion changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['contentId', 'disabled', 'maxEdgeStart', 'menuId', 'mode', 'side', 'swipeGesture', 'theme', 'type'], + inputs: ['contentId', { name: 'disabled', transform: nullableBooleanAttribute }, 'maxEdgeStart', 'menuId', 'mode', 'side', { name: 'swipeGesture', transform: nullableBooleanAttribute }, 'theme', 'type'], outputs: ['ionWillOpen', 'ionWillClose', 'ionDidOpen', 'ionDidClose'], }) export class IonMenu { diff --git a/packages/angular/src/standalone/directives/ion-picker-column-option.ts b/packages/angular/src/standalone/directives/ion-picker-column-option.ts index 641139c3c10..4c67b616d9d 100644 --- a/packages/angular/src/standalone/directives/ion-picker-column-option.ts +++ b/packages/angular/src/standalone/directives/ion-picker-column-option.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonPickerColumnOption } from '@ionic/core/ changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'disabled', 'mode', 'theme', 'value'], + inputs: ['color', { name: 'disabled', transform: nullableBooleanAttribute }, 'mode', 'theme', 'value'], }) export class IonPickerColumnOption { protected el: HTMLIonPickerColumnOptionElement; diff --git a/packages/angular/src/standalone/directives/ion-picker-column.ts b/packages/angular/src/standalone/directives/ion-picker-column.ts index 88e2ca0dfe9..47b1595dfcb 100644 --- a/packages/angular/src/standalone/directives/ion-picker-column.ts +++ b/packages/angular/src/standalone/directives/ion-picker-column.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone, EventEmitter, Output } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -18,7 +19,7 @@ import { defineCustomElement as defineIonPickerColumn } from '@ionic/core/compon changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'disabled', 'mode', 'theme', 'value'], + inputs: ['color', { name: 'disabled', transform: nullableBooleanAttribute }, 'mode', 'theme', 'value'], outputs: ['ionChange'], }) export class IonPickerColumn { diff --git a/packages/angular/src/standalone/directives/ion-progress-bar.ts b/packages/angular/src/standalone/directives/ion-progress-bar.ts index 4203703db55..42fa1aef756 100644 --- a/packages/angular/src/standalone/directives/ion-progress-bar.ts +++ b/packages/angular/src/standalone/directives/ion-progress-bar.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonProgressBar } from '@ionic/core/compone changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['buffer', 'color', 'mode', 'reversed', 'shape', 'theme', 'type', 'value'], + inputs: ['buffer', 'color', 'mode', { name: 'reversed', transform: nullableBooleanAttribute }, 'shape', 'theme', 'type', 'value'], }) export class IonProgressBar { protected el: HTMLIonProgressBarElement; diff --git a/packages/angular/src/standalone/directives/ion-radio.ts b/packages/angular/src/standalone/directives/ion-radio.ts index 0c6d0f46de3..f8c4314e93a 100644 --- a/packages/angular/src/standalone/directives/ion-radio.ts +++ b/packages/angular/src/standalone/directives/ion-radio.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone, EventEmitter, Output } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonRadio } from '@ionic/core/components/io changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['alignment', 'color', 'disabled', 'justify', 'labelPlacement', 'mode', 'name', 'theme', 'value'], + inputs: ['alignment', 'color', { name: 'disabled', transform: nullableBooleanAttribute }, 'justify', 'labelPlacement', 'mode', 'name', 'theme', 'value'], outputs: ['ionFocus', 'ionBlur'], }) export class IonRadio { diff --git a/packages/angular/src/standalone/directives/ion-refresher.ts b/packages/angular/src/standalone/directives/ion-refresher.ts index bc8c4821959..8c455b9477d 100644 --- a/packages/angular/src/standalone/directives/ion-refresher.ts +++ b/packages/angular/src/standalone/directives/ion-refresher.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone, EventEmitter, Output } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -18,7 +19,7 @@ import { defineCustomElement as defineIonRefresher } from '@ionic/core/component changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['closeDuration', 'disabled', 'mode', 'pullFactor', 'pullMax', 'pullMin', 'snapbackDuration', 'theme'], + inputs: ['closeDuration', { name: 'disabled', transform: nullableBooleanAttribute }, 'mode', 'pullFactor', 'pullMax', 'pullMin', 'snapbackDuration', 'theme'], outputs: ['ionRefresh', 'ionPull', 'ionStart', 'ionPullStart', 'ionPullEnd'], }) export class IonRefresher { diff --git a/packages/angular/src/standalone/directives/ion-reorder-group.ts b/packages/angular/src/standalone/directives/ion-reorder-group.ts index 89b7e200da5..da36bbb09d1 100644 --- a/packages/angular/src/standalone/directives/ion-reorder-group.ts +++ b/packages/angular/src/standalone/directives/ion-reorder-group.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone, EventEmitter, Output } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -18,7 +19,7 @@ import { defineCustomElement as defineIonReorderGroup } from '@ionic/core/compon changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['disabled', 'mode', 'theme'], + inputs: [{ name: 'disabled', transform: nullableBooleanAttribute }, 'mode', 'theme'], outputs: ['ionItemReorder', 'ionReorderStart', 'ionReorderMove', 'ionReorderEnd'], }) export class IonReorderGroup { diff --git a/packages/angular/src/standalone/directives/ion-segment-button.ts b/packages/angular/src/standalone/directives/ion-segment-button.ts index 64f643afb15..ba2ee9488d4 100644 --- a/packages/angular/src/standalone/directives/ion-segment-button.ts +++ b/packages/angular/src/standalone/directives/ion-segment-button.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonSegmentButton } from '@ionic/core/compo changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['contentId', 'disabled', 'layout', 'mode', 'theme', 'type', 'value'], + inputs: ['contentId', { name: 'disabled', transform: nullableBooleanAttribute }, 'layout', 'mode', 'theme', 'type', 'value'], }) export class IonSegmentButton { protected el: HTMLIonSegmentButtonElement; diff --git a/packages/angular/src/standalone/directives/ion-segment-view.ts b/packages/angular/src/standalone/directives/ion-segment-view.ts index 0ac2736ef37..cd4c898fce6 100644 --- a/packages/angular/src/standalone/directives/ion-segment-view.ts +++ b/packages/angular/src/standalone/directives/ion-segment-view.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone, EventEmitter, Output } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonSegmentView } from '@ionic/core/compone changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['disabled', 'swipeGesture'], + inputs: [{ name: 'disabled', transform: nullableBooleanAttribute }, { name: 'swipeGesture', transform: nullableBooleanAttribute }], outputs: ['ionSegmentViewScroll'], }) export class IonSegmentView { diff --git a/packages/angular/src/standalone/directives/ion-select-modal.ts b/packages/angular/src/standalone/directives/ion-select-modal.ts index b81d2a36576..8529e0306f3 100644 --- a/packages/angular/src/standalone/directives/ion-select-modal.ts +++ b/packages/angular/src/standalone/directives/ion-select-modal.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonSelectModal } from '@ionic/core/compone changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['cancelIcon', 'cancelText', 'header', 'multiple', 'options'], + inputs: [{ name: 'cancelIcon', transform: nullableBooleanAttribute }, 'cancelText', 'header', { name: 'multiple', transform: nullableBooleanAttribute }, 'options'], }) export class IonSelectModal { protected el: HTMLIonSelectModalElement; diff --git a/packages/angular/src/standalone/directives/ion-select-option.ts b/packages/angular/src/standalone/directives/ion-select-option.ts index ceaedd70a6b..88262a94799 100644 --- a/packages/angular/src/standalone/directives/ion-select-option.ts +++ b/packages/angular/src/standalone/directives/ion-select-option.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonSelectOption } from '@ionic/core/compon changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['description', 'disabled', 'justify', 'labelPlacement', 'mode', 'theme', 'value'], + inputs: ['description', { name: 'disabled', transform: nullableBooleanAttribute }, 'justify', 'labelPlacement', 'mode', 'theme', 'value'], }) export class IonSelectOption { protected el: HTMLIonSelectOptionElement; diff --git a/packages/angular/src/standalone/directives/ion-skeleton-text.ts b/packages/angular/src/standalone/directives/ion-skeleton-text.ts index bf7de3107a7..9d232dd4f13 100644 --- a/packages/angular/src/standalone/directives/ion-skeleton-text.ts +++ b/packages/angular/src/standalone/directives/ion-skeleton-text.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonSkeletonText } from '@ionic/core/compon changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['animated', 'mode', 'theme'], + inputs: [{ name: 'animated', transform: nullableBooleanAttribute }, 'mode', 'theme'], }) export class IonSkeletonText { protected el: HTMLIonSkeletonTextElement; diff --git a/packages/angular/src/standalone/directives/ion-spinner.ts b/packages/angular/src/standalone/directives/ion-spinner.ts index 7d46562640d..d6103340ea3 100644 --- a/packages/angular/src/standalone/directives/ion-spinner.ts +++ b/packages/angular/src/standalone/directives/ion-spinner.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonSpinner } from '@ionic/core/components/ changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'duration', 'mode', 'name', 'paused', 'size', 'theme'], + inputs: ['color', 'duration', 'mode', 'name', { name: 'paused', transform: nullableBooleanAttribute }, 'size', 'theme'], }) export class IonSpinner { protected el: HTMLIonSpinnerElement; diff --git a/packages/angular/src/standalone/directives/ion-split-pane.ts b/packages/angular/src/standalone/directives/ion-split-pane.ts index 36845819c8c..e410941f79e 100644 --- a/packages/angular/src/standalone/directives/ion-split-pane.ts +++ b/packages/angular/src/standalone/directives/ion-split-pane.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone, EventEmitter, Output } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonSplitPane } from '@ionic/core/component changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['contentId', 'disabled', 'mode', 'theme', 'when'], + inputs: ['contentId', { name: 'disabled', transform: nullableBooleanAttribute }, 'mode', 'theme', 'when'], outputs: ['ionSplitPaneVisible'], }) export class IonSplitPane { diff --git a/packages/angular/src/standalone/directives/ion-tab-bar.ts b/packages/angular/src/standalone/directives/ion-tab-bar.ts index 1f2d31b2243..82c0a6db0f6 100644 --- a/packages/angular/src/standalone/directives/ion-tab-bar.ts +++ b/packages/angular/src/standalone/directives/ion-tab-bar.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonTabBar } from '@ionic/core/components/i changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'expand', 'mode', 'scrollEffect', 'selectedTab', 'shape', 'theme', 'translucent'], + inputs: ['color', 'expand', 'mode', 'scrollEffect', 'selectedTab', 'shape', 'theme', { name: 'translucent', transform: nullableBooleanAttribute }], }) export class IonTabBar { protected el: HTMLIonTabBarElement; diff --git a/packages/angular/src/standalone/directives/ion-tab-button.ts b/packages/angular/src/standalone/directives/ion-tab-button.ts index 425a3ce3ab0..eeb650eee09 100644 --- a/packages/angular/src/standalone/directives/ion-tab-button.ts +++ b/packages/angular/src/standalone/directives/ion-tab-button.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -17,7 +18,7 @@ import { defineCustomElement as defineIonTabButton } from '@ionic/core/component changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['disabled', 'download', 'href', 'layout', 'mode', 'rel', 'selected', 'shape', 'tab', 'target', 'theme'], + inputs: [{ name: 'disabled', transform: nullableBooleanAttribute }, 'download', 'href', 'layout', 'mode', 'rel', { name: 'selected', transform: nullableBooleanAttribute }, 'shape', 'tab', 'target', 'theme'], }) export class IonTabButton { protected el: HTMLIonTabButtonElement; diff --git a/packages/angular/src/standalone/directives/ion-toast.ts b/packages/angular/src/standalone/directives/ion-toast.ts index fc73cf7ccc3..8fa0719b259 100644 --- a/packages/angular/src/standalone/directives/ion-toast.ts +++ b/packages/angular/src/standalone/directives/ion-toast.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone, EventEmitter, Output } from '@angular/core'; import { ProxyCmp } from './angular-component-lib/utils'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import type { Components } from '@ionic/core/components'; @@ -18,7 +19,7 @@ import { defineCustomElement as defineIonToast } from '@ionic/core/components/io changeDetection: ChangeDetectionStrategy.OnPush, template: '', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'htmlAttributes', 'hue', 'icon', 'isOpen', 'keyboardClose', 'layout', 'leaveAnimation', 'message', 'mode', 'position', 'positionAnchor', 'shape', 'swipeGesture', 'theme', 'translucent', 'trigger'], + inputs: [{ name: 'animated', transform: nullableBooleanAttribute }, 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'htmlAttributes', 'hue', 'icon', { name: 'isOpen', transform: nullableBooleanAttribute }, { name: 'keyboardClose', transform: nullableBooleanAttribute }, 'layout', 'leaveAnimation', 'message', 'mode', 'position', 'positionAnchor', 'shape', 'swipeGesture', 'theme', { name: 'translucent', transform: nullableBooleanAttribute }, 'trigger'], outputs: ['ionToastDidPresent', 'ionToastWillPresent', 'ionToastWillDismiss', 'ionToastDidDismiss', 'didPresent', 'willPresent', 'willDismiss', 'didDismiss'], }) export class IonToast { diff --git a/packages/angular/src/standalone/directives/radio-group.ts b/packages/angular/src/standalone/directives/radio-group.ts index f5e9a899aad..5a476bbdf62 100644 --- a/packages/angular/src/standalone/directives/radio-group.ts +++ b/packages/angular/src/standalone/directives/radio-group.ts @@ -14,13 +14,24 @@ import { ValueAccessor } from '@ionic/angular/common'; import type { RadioGroupChangeEventDetail, Components } from '@ionic/core/components'; import { defineCustomElement } from '@ionic/core/components/ion-radio-group.js'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; -const RADIO_GROUP_INPUTS = ['allowEmptySelection', 'compareWith', 'errorText', 'helperText', 'name', 'value']; +const RADIO_GROUP_INPUTS = [ + { name: 'allowEmptySelection', transform: nullableBooleanAttribute }, + 'compareWith', + 'errorText', + 'helperText', + 'name', + 'value', +]; + +/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ +const RADIO_GROUP_PROXY_INPUTS = RADIO_GROUP_INPUTS.map((input) => (typeof input === 'string' ? input : input.name)); @ProxyCmp({ defineCustomElementFn: defineCustomElement, - inputs: RADIO_GROUP_INPUTS, + inputs: RADIO_GROUP_PROXY_INPUTS, }) @Component({ selector: 'ion-radio-group', diff --git a/packages/angular/src/standalone/directives/range.ts b/packages/angular/src/standalone/directives/range.ts index 8016a7ddef2..9b335bcbe13 100644 --- a/packages/angular/src/standalone/directives/range.ts +++ b/packages/angular/src/standalone/directives/range.ts @@ -19,31 +19,35 @@ import type { } from '@ionic/core/components'; import { defineCustomElement } from '@ionic/core/components/ion-range.js'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; const RANGE_INPUTS = [ 'activeBarStart', 'color', 'debounce', - 'disabled', - 'dualKnobs', + { name: 'disabled', transform: nullableBooleanAttribute }, + { name: 'dualKnobs', transform: nullableBooleanAttribute }, 'label', 'labelPlacement', 'max', 'min', 'mode', 'name', - 'pin', + { name: 'pin', transform: nullableBooleanAttribute }, 'pinFormatter', - 'snaps', + { name: 'snaps', transform: nullableBooleanAttribute }, 'step', - 'ticks', + { name: 'ticks', transform: nullableBooleanAttribute }, 'value', ]; +/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ +const RANGE_PROXY_INPUTS = RANGE_INPUTS.map((input) => (typeof input === 'string' ? input : input.name)); + @ProxyCmp({ defineCustomElementFn: defineCustomElement, - inputs: RANGE_INPUTS, + inputs: RANGE_PROXY_INPUTS, }) @Component({ selector: 'ion-range', diff --git a/packages/angular/src/standalone/directives/searchbar.ts b/packages/angular/src/standalone/directives/searchbar.ts index 1f9ca61d623..233e9288ba9 100644 --- a/packages/angular/src/standalone/directives/searchbar.ts +++ b/packages/angular/src/standalone/directives/searchbar.ts @@ -14,18 +14,19 @@ import { ValueAccessor } from '@ionic/angular/common'; import type { SearchbarInputEventDetail, SearchbarChangeEventDetail, Components } from '@ionic/core/components'; import { defineCustomElement } from '@ionic/core/components/ion-searchbar.js'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; const SEARCHBAR_INPUTS = [ - 'animated', + { name: 'animated', transform: nullableBooleanAttribute }, 'autocomplete', - 'autocorrect', + { name: 'autocorrect', transform: nullableBooleanAttribute }, 'cancelButtonIcon', 'cancelButtonText', 'clearIcon', 'color', 'debounce', - 'disabled', + { name: 'disabled', transform: nullableBooleanAttribute }, 'enterkeyhint', 'inputmode', 'mode', @@ -34,14 +35,17 @@ const SEARCHBAR_INPUTS = [ 'searchIcon', 'showCancelButton', 'showClearButton', - 'spellcheck', + { name: 'spellcheck', transform: nullableBooleanAttribute }, 'type', 'value', ]; +/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ +const SEARCHBAR_PROXY_INPUTS = SEARCHBAR_INPUTS.map((input) => (typeof input === 'string' ? input : input.name)); + @ProxyCmp({ defineCustomElementFn: defineCustomElement, - inputs: SEARCHBAR_INPUTS, + inputs: SEARCHBAR_PROXY_INPUTS, methods: ['setFocus', 'getInputElement'], }) @Component({ diff --git a/packages/angular/src/standalone/directives/segment.ts b/packages/angular/src/standalone/directives/segment.ts index 0220069ab92..5d27d432d25 100644 --- a/packages/angular/src/standalone/directives/segment.ts +++ b/packages/angular/src/standalone/directives/segment.ts @@ -14,13 +14,25 @@ import { ValueAccessor } from '@ionic/angular/common'; import type { SegmentChangeEventDetail, Components } from '@ionic/core/components'; import { defineCustomElement } from '@ionic/core/components/ion-segment.js'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; -const SEGMENT_INPUTS = ['color', 'disabled', 'mode', 'scrollable', 'selectOnFocus', 'swipeGesture', 'value']; +const SEGMENT_INPUTS = [ + 'color', + { name: 'disabled', transform: nullableBooleanAttribute }, + 'mode', + { name: 'scrollable', transform: nullableBooleanAttribute }, + { name: 'selectOnFocus', transform: nullableBooleanAttribute }, + { name: 'swipeGesture', transform: nullableBooleanAttribute }, + 'value', +]; + +/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ +const SEGMENT_PROXY_INPUTS = SEGMENT_INPUTS.map((input) => (typeof input === 'string' ? input : input.name)); @ProxyCmp({ defineCustomElementFn: defineCustomElement, - inputs: SEGMENT_INPUTS, + inputs: SEGMENT_PROXY_INPUTS, }) @Component({ selector: 'ion-segment', diff --git a/packages/angular/src/standalone/directives/select.ts b/packages/angular/src/standalone/directives/select.ts index b6dac74ab80..671fbfbee2e 100644 --- a/packages/angular/src/standalone/directives/select.ts +++ b/packages/angular/src/standalone/directives/select.ts @@ -14,13 +14,14 @@ import { ValueAccessor } from '@ionic/angular/common'; import type { SelectChangeEventDetail, Components } from '@ionic/core/components'; import { defineCustomElement } from '@ionic/core/components/ion-select.js'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; const SELECT_INPUTS = [ 'cancelText', 'color', 'compareWith', - 'disabled', + { name: 'disabled', transform: nullableBooleanAttribute }, 'errorText', 'expandedIcon', 'fill', @@ -31,7 +32,7 @@ const SELECT_INPUTS = [ 'label', 'labelPlacement', 'mode', - 'multiple', + { name: 'multiple', transform: nullableBooleanAttribute }, 'name', 'okText', 'placeholder', @@ -41,9 +42,12 @@ const SELECT_INPUTS = [ 'value', ]; +/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ +const SELECT_PROXY_INPUTS = SELECT_INPUTS.map((input) => (typeof input === 'string' ? input : input.name)); + @ProxyCmp({ defineCustomElementFn: defineCustomElement, - inputs: SELECT_INPUTS, + inputs: SELECT_PROXY_INPUTS, methods: ['open'], }) @Component({ diff --git a/packages/angular/src/standalone/directives/textarea.ts b/packages/angular/src/standalone/directives/textarea.ts index 4afa1dacb79..f3418e84eb0 100644 --- a/packages/angular/src/standalone/directives/textarea.ts +++ b/packages/angular/src/standalone/directives/textarea.ts @@ -14,19 +14,20 @@ import { ValueAccessor } from '@ionic/angular/common'; import type { TextareaChangeEventDetail, TextareaInputEventDetail, Components } from '@ionic/core/components'; import { defineCustomElement } from '@ionic/core/components/ion-textarea.js'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; const TEXTAREA_INPUTS = [ - 'autoGrow', + { name: 'autoGrow', transform: nullableBooleanAttribute }, 'autocapitalize', - 'autofocus', - 'clearOnEdit', + { name: 'autofocus', transform: nullableBooleanAttribute }, + { name: 'clearOnEdit', transform: nullableBooleanAttribute }, 'color', 'cols', - 'counter', + { name: 'counter', transform: nullableBooleanAttribute }, 'counterFormatter', 'debounce', - 'disabled', + { name: 'disabled', transform: nullableBooleanAttribute }, 'enterkeyhint', 'errorText', 'fill', @@ -39,18 +40,21 @@ const TEXTAREA_INPUTS = [ 'mode', 'name', 'placeholder', - 'readonly', - 'required', + { name: 'readonly', transform: nullableBooleanAttribute }, + { name: 'required', transform: nullableBooleanAttribute }, 'rows', 'shape', - 'spellcheck', + { name: 'spellcheck', transform: nullableBooleanAttribute }, 'value', 'wrap', ]; +/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ +const TEXTAREA_PROXY_INPUTS = TEXTAREA_INPUTS.map((input) => (typeof input === 'string' ? input : input.name)); + @ProxyCmp({ defineCustomElementFn: defineCustomElement, - inputs: TEXTAREA_INPUTS, + inputs: TEXTAREA_PROXY_INPUTS, methods: ['setFocus', 'getInputElement'], }) @Component({ diff --git a/packages/angular/src/standalone/directives/toggle.ts b/packages/angular/src/standalone/directives/toggle.ts index a04899046bd..f218daf5c5e 100644 --- a/packages/angular/src/standalone/directives/toggle.ts +++ b/packages/angular/src/standalone/directives/toggle.ts @@ -14,13 +14,14 @@ import { ValueAccessor, setIonicClasses } from '@ionic/angular/common'; import type { ToggleChangeEventDetail, Components } from '@ionic/core/components'; import { defineCustomElement } from '@ionic/core/components/ion-toggle.js'; +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; const TOGGLE_INPUTS = [ - 'checked', + { name: 'checked', transform: nullableBooleanAttribute }, 'color', - 'disabled', - 'enableOnOffLabels', + { name: 'disabled', transform: nullableBooleanAttribute }, + { name: 'enableOnOffLabels', transform: nullableBooleanAttribute }, 'errorText', 'helperText', 'justify', @@ -30,9 +31,12 @@ const TOGGLE_INPUTS = [ 'value', ]; +/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ +const TOGGLE_PROXY_INPUTS = TOGGLE_INPUTS.map((input) => (typeof input === 'string' ? input : input.name)); + @ProxyCmp({ defineCustomElementFn: defineCustomElement, - inputs: TOGGLE_INPUTS, + inputs: TOGGLE_PROXY_INPUTS, }) @Component({ selector: 'ion-toggle', From 91a0abc01d66c3b22cf811c1e94f1029ae098bc2 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Fri, 18 Sep 2026 16:16:20 -0700 Subject: [PATCH 02/13] docs(angular): add breaking change entry for boolean input type checking --- BREAKING.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/BREAKING.md b/BREAKING.md index 2c034b42598..eb0fdec7706 100644 --- a/BREAKING.md +++ b/BREAKING.md @@ -24,6 +24,8 @@ This is a comprehensive list of the breaking changes introduced in the major ver - [Input Otp](#version-10x-input-otp) - [Radio Group](#version-10x-radio-group) - [Textarea](#version-10x-textarea) +- [Framework Specific](#version-10x-framework-specific) + - [Angular](#version-10x-angular)

Components

@@ -221,3 +223,24 @@ The internal wrappers that Ionic 9 introduced are no longer reachable as descend ``` To style the wrappers themselves rather than the slotted content, use `part="start"` and `part="end"`. + +

Framework Specific

+ +

Angular

+ +**Boolean Inputs Are Type Checked** + +Boolean inputs can now be set by attribute presence, so `` is valid where it previously reported `Type 'string' is not assignable to type 'boolean'`. + +Declaring the transform that allows this also makes Angular type check these inputs, which it did not do before. Bindings that pass a value outside `boolean | string | null | undefined` now fail to compile. The common case is a truthiness binding: + +```diff +- ++ +``` + +``` +error TS2322: Type 'number' is not assignable to type 'string | boolean | null | undefined'. +``` + +Coerce the expression to a boolean, with `!!value` or an explicit comparison such as `items.length > 0`. Bindings that already pass a boolean, a string, `null` or `undefined` are unaffected, and runtime behavior is unchanged. From 478f9840a6119e399cf2d76f3179a3954c14ebce Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Fri, 18 Sep 2026 16:24:02 -0700 Subject: [PATCH 03/13] docs(angular): document the boolean input transform in the component guide --- docs/component-guide.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/component-guide.md b/docs/component-guide.md index e475159da73..c9e35aed53b 100644 --- a/docs/component-guide.md +++ b/docs/component-guide.md @@ -922,6 +922,21 @@ For standalone components, create a directive in the [standalone package](/packa - For boolean inputs: See [ion-checkbox](/packages/angular/src/standalone/directives/checkbox.ts) or [ion-toggle](/packages/angular/src/standalone/directives/toggle.ts) - For select-like inputs: See [ion-select](/packages/angular/src/standalone/directives/select.ts) or [ion-radio-group](/packages/angular/src/standalone/directives/radio-group.ts) +Boolean inputs take the `nullableBooleanAttribute` transform, so that they can be set by attribute presence the same way they can on the generated proxies: + +```typescript +import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; + +const NEW_COMPONENT_INPUTS = [{ name: 'disabled', transform: nullableBooleanAttribute }, 'mode']; + +/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ +const NEW_COMPONENT_PROXY_INPUTS = NEW_COMPONENT_INPUTS.map((input) => + typeof input === 'string' ? input : input.name +); +``` + +Pass `NEW_COMPONENT_INPUTS` to `@Component({ inputs })` and `NEW_COMPONENT_PROXY_INPUTS` to `@ProxyCmp({ inputs })`. Unlike Angular's own `booleanAttribute`, the transform passes `null` and `undefined` through rather than coercing them to `false`, because components frequently treat them as a state distinct from `false`. Wrappers under [`common/`](/packages/angular/src/common) import it from `../utils/boolean-attribute`, since the output target only copies `angular-component-lib/` next to the files it generates. + After creating the directive, you need to export it in two places: 1. First, add your component to the directives export group in [`packages/angular/src/standalone/directives/index.ts`](/packages/angular/src/standalone/directives/index.ts): From d3f65a8b68a0ce88283fe7f5201781fa5a66880f Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Mon, 21 Sep 2026 10:16:32 -0700 Subject: [PATCH 04/13] docs(angular): correct the breaking change wording for boolean inputs --- BREAKING.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/BREAKING.md b/BREAKING.md index eb0fdec7706..ddb5add07a5 100644 --- a/BREAKING.md +++ b/BREAKING.md @@ -230,17 +230,23 @@ To style the wrappers themselves rather than the slotted content, use `part="sta **Boolean Inputs Are Type Checked** -Boolean inputs can now be set by attribute presence, so `` is valid where it previously reported `Type 'string' is not assignable to type 'boolean'`. +Boolean inputs now declare an input transform, so attribute presence is an explicitly supported way to set them: -Declaring the transform that allows this also makes Angular type check these inputs, which it did not do before. Bindings that pass a value outside `boolean | string | null | undefined` now fail to compile. The common case is a truthiness binding: +```html + + + +``` + +Declaring the transform also makes Angular type check these inputs, which it did not do before. The generated proxies declare no class fields, so Angular had nothing to check a binding against and accepted any value. Bindings that pass a value outside `boolean | string | null | undefined` now fail to compile. The common case is a truthiness binding: ```diff - -+ ++ ``` ``` error TS2322: Type 'number' is not assignable to type 'string | boolean | null | undefined'. ``` -Coerce the expression to a boolean, with `!!value` or an explicit comparison such as `items.length > 0`. Bindings that already pass a boolean, a string, `null` or `undefined` are unaffected, and runtime behavior is unchanged. +Coerce the expression to a boolean, with an explicit comparison or `!!value`. Bindings that already pass a boolean, a string, `null` or `undefined` are unaffected, and runtime behavior is unchanged. From da642deaf9d5470913613b1ea106a8be35ba242c Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Mon, 21 Sep 2026 11:33:52 -0700 Subject: [PATCH 05/13] docs(angular): document the constraints on the boolean attribute helper --- .../src/common/utils/boolean-attribute.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/angular/src/common/utils/boolean-attribute.ts b/packages/angular/src/common/utils/boolean-attribute.ts index 047a0bdc48a..1edbbdd30db 100644 --- a/packages/angular/src/common/utils/boolean-attribute.ts +++ b/packages/angular/src/common/utils/boolean-attribute.ts @@ -12,18 +12,13 @@ * Strings are coerced the same way Angular's `booleanAttribute` coerces them, so `''` (a bare * attribute) becomes `true` and `'false'` becomes `false`. * - * Unlike Angular's `booleanAttribute`, `null` and `undefined` are passed through rather than - * coerced to `false`. Components frequently treat them as a state distinct from `false`, and both - * reach inputs routinely from the `async` pipe before its first emission and from form control - * values: + * Angular's own `booleanAttribute` is not used because it coerces `null` and `undefined` to + * `false`. Components frequently treat those as a state distinct from `false`, such as + * `ion-item`'s `detail` resolving `undefined` to a computed default, and both reach inputs + * routinely from the `async` pipe before its first emission and from form control values. * - * ```tsx - * // `undefined` means "decide based on the mode", which is not the same as `false` - * const showDetail = detail !== undefined ? detail : mode === 'ios'; - * - * // a strict comparison also behaves differently for `null` than it does for `false` - * const showHandle = handle !== false; - * ``` + * The parameter type is what Angular derives `ngAcceptInputType_*` from, so it decides which + * template bindings compile. Widening it to `unknown` would let any expression through. * * Declared as a function rather than an arrow constant because Angular has to resolve input * transforms statically when compiling a library in partial compilation mode. From 726210700b82652e402a2e38dcfca95c32050616 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Mon, 21 Sep 2026 14:52:26 -0700 Subject: [PATCH 06/13] docs(angular): document the number coercion change for boolean inputs --- BREAKING.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/BREAKING.md b/BREAKING.md index ddb5add07a5..b1d5c49ae87 100644 --- a/BREAKING.md +++ b/BREAKING.md @@ -249,4 +249,6 @@ Declaring the transform also makes Angular type check these inputs, which it did error TS2322: Type 'number' is not assignable to type 'string | boolean | null | undefined'. ``` -Coerce the expression to a boolean, with an explicit comparison or `!!value`. Bindings that already pass a boolean, a string, `null` or `undefined` are unaffected, and runtime behavior is unchanged. +Coerce the expression to a boolean, with an explicit comparison or `!!value`. Bindings that already pass a boolean, a string, `null` or `undefined` are unaffected. + +Coercion also moves from Stencil to Angular, which changes the result for numbers. `0` and `NaN` previously became `false` and now become `true`, matching Angular's own `booleanAttribute`. An app without `strictTemplates` gets no compile error for the binding above, so an empty list now disables the item rather than enabling it. Coercing the expression fixes both the type error and the runtime change. From 70c75575caad70718e037f7b40a64f1efa5ef19c Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Mon, 21 Sep 2026 15:11:47 -0700 Subject: [PATCH 07/13] fix(angular): add missing boolean inputs to hand-written wrappers Co-authored-by: Shane --- packages/angular/src/common/overlays/popover.ts | 1 + packages/angular/src/standalone/directives/checkbox.ts | 1 + packages/angular/src/standalone/directives/select.ts | 2 ++ packages/angular/src/standalone/directives/toggle.ts | 1 + 4 files changed, 5 insertions(+) diff --git a/packages/angular/src/common/overlays/popover.ts b/packages/angular/src/common/overlays/popover.ts index 856391b8029..6483d51d720 100644 --- a/packages/angular/src/common/overlays/popover.ts +++ b/packages/angular/src/common/overlays/popover.ts @@ -60,6 +60,7 @@ const POPOVER_INPUTS = [ { name: 'focusTrap', transform: nullableBooleanAttribute }, { name: 'isOpen', transform: nullableBooleanAttribute }, { name: 'keyboardClose', transform: nullableBooleanAttribute }, + { name: 'keyboardEvents', transform: nullableBooleanAttribute }, 'leaveAnimation', 'mode', { name: 'showBackdrop', transform: nullableBooleanAttribute }, diff --git a/packages/angular/src/standalone/directives/checkbox.ts b/packages/angular/src/standalone/directives/checkbox.ts index 8e4a873a52a..150588813bd 100644 --- a/packages/angular/src/standalone/directives/checkbox.ts +++ b/packages/angular/src/standalone/directives/checkbox.ts @@ -28,6 +28,7 @@ const CHECKBOX_INPUTS = [ 'labelPlacement', 'mode', 'name', + { name: 'required', transform: nullableBooleanAttribute }, 'value', ]; diff --git a/packages/angular/src/standalone/directives/select.ts b/packages/angular/src/standalone/directives/select.ts index 671fbfbee2e..05228b18bd8 100644 --- a/packages/angular/src/standalone/directives/select.ts +++ b/packages/angular/src/standalone/directives/select.ts @@ -18,6 +18,7 @@ import { nullableBooleanAttribute } from './angular-component-lib/boolean-attrib import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; const SELECT_INPUTS = [ + { name: 'cancelIcon', transform: nullableBooleanAttribute }, 'cancelText', 'color', 'compareWith', @@ -36,6 +37,7 @@ const SELECT_INPUTS = [ 'name', 'okText', 'placeholder', + { name: 'required', transform: nullableBooleanAttribute }, 'selectedText', 'shape', 'toggleIcon', diff --git a/packages/angular/src/standalone/directives/toggle.ts b/packages/angular/src/standalone/directives/toggle.ts index f218daf5c5e..10365c70ad1 100644 --- a/packages/angular/src/standalone/directives/toggle.ts +++ b/packages/angular/src/standalone/directives/toggle.ts @@ -28,6 +28,7 @@ const TOGGLE_INPUTS = [ 'labelPlacement', 'mode', 'name', + { name: 'required', transform: nullableBooleanAttribute }, 'value', ]; From 515015a9450f54c6caf8bf44a73218baa988e509 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Mon, 21 Sep 2026 15:19:18 -0700 Subject: [PATCH 08/13] refactor(angular): extract the shared proxy input name helper --- .../src/common/directives/navigation/back-button.ts | 5 ++--- packages/angular/src/common/directives/navigation/nav.ts | 5 ++--- packages/angular/src/common/index.ts | 2 +- packages/angular/src/common/overlays/modal.ts | 5 ++--- packages/angular/src/common/overlays/popover.ts | 5 ++--- packages/angular/src/common/utils/proxy.ts | 7 +++++++ packages/angular/src/standalone/directives/checkbox.ts | 5 ++--- packages/angular/src/standalone/directives/datetime.ts | 5 ++--- packages/angular/src/standalone/directives/input-otp.ts | 5 ++--- packages/angular/src/standalone/directives/input.ts | 5 ++--- packages/angular/src/standalone/directives/radio-group.ts | 5 ++--- packages/angular/src/standalone/directives/range.ts | 5 ++--- packages/angular/src/standalone/directives/searchbar.ts | 5 ++--- packages/angular/src/standalone/directives/segment.ts | 5 ++--- packages/angular/src/standalone/directives/select.ts | 5 ++--- packages/angular/src/standalone/directives/textarea.ts | 5 ++--- packages/angular/src/standalone/directives/toggle.ts | 5 ++--- 17 files changed, 38 insertions(+), 46 deletions(-) diff --git a/packages/angular/src/common/directives/navigation/back-button.ts b/packages/angular/src/common/directives/navigation/back-button.ts index 9dc4a97cf5c..e6ce2c3ad6d 100644 --- a/packages/angular/src/common/directives/navigation/back-button.ts +++ b/packages/angular/src/common/directives/navigation/back-button.ts @@ -4,7 +4,7 @@ import type { Components } from '@ionic/core'; import { Config } from '../../providers/config'; import { NavController } from '../../providers/nav-controller'; import { nullableBooleanAttribute } from '../../utils/boolean-attribute'; -import { ProxyCmp } from '../../utils/proxy'; +import { inputNames, ProxyCmp } from '../../utils/proxy'; import { IonRouterOutlet } from './router-outlet'; @@ -19,8 +19,7 @@ const BACK_BUTTON_INPUTS = [ 'type', ]; -/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ -const BACK_BUTTON_PROXY_INPUTS = BACK_BUTTON_INPUTS.map((input) => (typeof input === 'string' ? input : input.name)); +const BACK_BUTTON_PROXY_INPUTS = inputNames(BACK_BUTTON_INPUTS); // eslint-disable-next-line @typescript-eslint/no-empty-object-type export declare interface IonBackButton extends Components.IonBackButton {} diff --git a/packages/angular/src/common/directives/navigation/nav.ts b/packages/angular/src/common/directives/navigation/nav.ts index b3daebf1383..eb9deaa0ee5 100644 --- a/packages/angular/src/common/directives/navigation/nav.ts +++ b/packages/angular/src/common/directives/navigation/nav.ts @@ -11,7 +11,7 @@ import type { Components } from '@ionic/core'; import { AngularDelegate } from '../../providers/angular-delegate'; import { nullableBooleanAttribute } from '../../utils/boolean-attribute'; -import { ProxyCmp, proxyOutputs } from '../../utils/proxy'; +import { inputNames, ProxyCmp, proxyOutputs } from '../../utils/proxy'; const NAV_INPUTS = [ { name: 'animated', transform: nullableBooleanAttribute }, @@ -21,8 +21,7 @@ const NAV_INPUTS = [ { name: 'swipeGesture', transform: nullableBooleanAttribute }, ]; -/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ -const NAV_PROXY_INPUTS = NAV_INPUTS.map((input) => (typeof input === 'string' ? input : input.name)); +const NAV_PROXY_INPUTS = inputNames(NAV_INPUTS); const NAV_METHODS = [ 'push', diff --git a/packages/angular/src/common/index.ts b/packages/angular/src/common/index.ts index 4130c3f978c..3d48eb16fa5 100644 --- a/packages/angular/src/common/index.ts +++ b/packages/angular/src/common/index.ts @@ -27,7 +27,7 @@ export { } from './directives/navigation/router-link-delegate'; export { IonTabs } from './directives/navigation/tabs'; -export { ProxyCmp } from './utils/proxy'; +export { ProxyCmp, inputNames } from './utils/proxy'; export { OverlayBaseController } from './utils/overlay'; export { IonicRouteStrategy } from './utils/routing'; diff --git a/packages/angular/src/common/overlays/modal.ts b/packages/angular/src/common/overlays/modal.ts index 3e149554481..3437040cb0b 100644 --- a/packages/angular/src/common/overlays/modal.ts +++ b/packages/angular/src/common/overlays/modal.ts @@ -10,7 +10,7 @@ import { import type { Components, ModalBreakpointChangeEventDetail, ModalDragEventDetail } from '@ionic/core/components'; import { nullableBooleanAttribute } from '../utils/boolean-attribute'; -import { ProxyCmp, proxyOutputs } from '../utils/proxy'; +import { inputNames, ProxyCmp, proxyOutputs } from '../utils/proxy'; export declare interface IonModal extends Components.IonModal { /** @@ -88,8 +88,7 @@ const MODAL_INPUTS = [ 'trigger', ]; -/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ -const MODAL_PROXY_INPUTS = MODAL_INPUTS.map((input) => (typeof input === 'string' ? input : input.name)); +const MODAL_PROXY_INPUTS = inputNames(MODAL_INPUTS); const MODAL_METHODS = [ 'present', diff --git a/packages/angular/src/common/overlays/popover.ts b/packages/angular/src/common/overlays/popover.ts index 6483d51d720..d69cf6e0cc7 100644 --- a/packages/angular/src/common/overlays/popover.ts +++ b/packages/angular/src/common/overlays/popover.ts @@ -10,7 +10,7 @@ import { import type { Components } from '@ionic/core/components'; import { nullableBooleanAttribute } from '../utils/boolean-attribute'; -import { ProxyCmp, proxyOutputs } from '../utils/proxy'; +import { inputNames, ProxyCmp, proxyOutputs } from '../utils/proxy'; export declare interface IonPopover extends Components.IonPopover { /** @@ -72,8 +72,7 @@ const POPOVER_INPUTS = [ 'side', ]; -/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ -const POPOVER_PROXY_INPUTS = POPOVER_INPUTS.map((input) => (typeof input === 'string' ? input : input.name)); +const POPOVER_PROXY_INPUTS = inputNames(POPOVER_INPUTS); const POPOVER_METHODS = ['present', 'dismiss', 'onDidDismiss', 'onWillDismiss']; diff --git a/packages/angular/src/common/utils/proxy.ts b/packages/angular/src/common/utils/proxy.ts index b9070978db1..421c60f38af 100644 --- a/packages/angular/src/common/utils/proxy.ts +++ b/packages/angular/src/common/utils/proxy.ts @@ -28,6 +28,13 @@ export const proxyMethods = (Cmp: any, methods: string[]) => { }); }; +/** + * Extracts the plain names from an inputs array that may contain transform entries. + * `ProxyCmp` only needs the names, and runs at runtime rather than through the Angular compiler. + */ +export const inputNames = (inputs: (string | { name: string })[]): string[] => + inputs.map((input) => (typeof input === 'string' ? input : input.name)); + export const proxyOutputs = (instance: any, el: any, events: string[]) => { events.forEach((eventName) => (instance[eventName] = fromEvent(el, eventName))); }; diff --git a/packages/angular/src/standalone/directives/checkbox.ts b/packages/angular/src/standalone/directives/checkbox.ts index 150588813bd..4d534e3f1aa 100644 --- a/packages/angular/src/standalone/directives/checkbox.ts +++ b/packages/angular/src/standalone/directives/checkbox.ts @@ -10,7 +10,7 @@ import { forwardRef, } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; -import { ValueAccessor, setIonicClasses } from '@ionic/angular/common'; +import { inputNames, setIonicClasses, ValueAccessor } from '@ionic/angular/common'; import type { CheckboxChangeEventDetail, Components } from '@ionic/core/components'; import { defineCustomElement } from '@ionic/core/components/ion-checkbox.js'; @@ -32,8 +32,7 @@ const CHECKBOX_INPUTS = [ 'value', ]; -/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ -const CHECKBOX_PROXY_INPUTS = CHECKBOX_INPUTS.map((input) => (typeof input === 'string' ? input : input.name)); +const CHECKBOX_PROXY_INPUTS = inputNames(CHECKBOX_INPUTS); @ProxyCmp({ defineCustomElementFn: defineCustomElement, diff --git a/packages/angular/src/standalone/directives/datetime.ts b/packages/angular/src/standalone/directives/datetime.ts index b2d80e54108..f8ccd7ec918 100644 --- a/packages/angular/src/standalone/directives/datetime.ts +++ b/packages/angular/src/standalone/directives/datetime.ts @@ -10,7 +10,7 @@ import { forwardRef, } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; -import { ValueAccessor } from '@ionic/angular/common'; +import { inputNames, ValueAccessor } from '@ionic/angular/common'; import type { DatetimeChangeEventDetail, Components } from '@ionic/core/components'; import { defineCustomElement } from '@ionic/core/components/ion-datetime.js'; @@ -52,8 +52,7 @@ const DATETIME_INPUTS = [ 'yearValues', ]; -/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ -const DATETIME_PROXY_INPUTS = DATETIME_INPUTS.map((input) => (typeof input === 'string' ? input : input.name)); +const DATETIME_PROXY_INPUTS = inputNames(DATETIME_INPUTS); @ProxyCmp({ defineCustomElementFn: defineCustomElement, diff --git a/packages/angular/src/standalone/directives/input-otp.ts b/packages/angular/src/standalone/directives/input-otp.ts index c0b46624681..dabec934993 100644 --- a/packages/angular/src/standalone/directives/input-otp.ts +++ b/packages/angular/src/standalone/directives/input-otp.ts @@ -10,7 +10,7 @@ import { forwardRef, } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; -import { ValueAccessor } from '@ionic/angular/common'; +import { inputNames, ValueAccessor } from '@ionic/angular/common'; import type { InputOtpInputEventDetail as IIonInputOtpInputEventDetail, InputOtpChangeEventDetail as IIonInputOtpChangeEventDetail, @@ -38,8 +38,7 @@ const INPUT_OTP_INPUTS = [ 'value', ]; -/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ -const INPUT_OTP_PROXY_INPUTS = INPUT_OTP_INPUTS.map((input) => (typeof input === 'string' ? input : input.name)); +const INPUT_OTP_PROXY_INPUTS = inputNames(INPUT_OTP_INPUTS); @ProxyCmp({ defineCustomElementFn: defineCustomElement, diff --git a/packages/angular/src/standalone/directives/input.ts b/packages/angular/src/standalone/directives/input.ts index e8ba4e1c85c..cbe8ff2f1f5 100644 --- a/packages/angular/src/standalone/directives/input.ts +++ b/packages/angular/src/standalone/directives/input.ts @@ -10,7 +10,7 @@ import { forwardRef, } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; -import { ValueAccessor } from '@ionic/angular/common'; +import { inputNames, ValueAccessor } from '@ionic/angular/common'; import type { InputInputEventDetail as IIonInputInputInputEventDetail, InputChangeEventDetail as IIonInputInputChangeEventDetail, @@ -60,8 +60,7 @@ const INPUT_INPUTS = [ 'value', ]; -/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ -const INPUT_PROXY_INPUTS = INPUT_INPUTS.map((input) => (typeof input === 'string' ? input : input.name)); +const INPUT_PROXY_INPUTS = inputNames(INPUT_INPUTS); @ProxyCmp({ defineCustomElementFn: defineCustomElement, diff --git a/packages/angular/src/standalone/directives/radio-group.ts b/packages/angular/src/standalone/directives/radio-group.ts index 5a476bbdf62..fbf7fa62338 100644 --- a/packages/angular/src/standalone/directives/radio-group.ts +++ b/packages/angular/src/standalone/directives/radio-group.ts @@ -10,7 +10,7 @@ import { forwardRef, } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; -import { ValueAccessor } from '@ionic/angular/common'; +import { inputNames, ValueAccessor } from '@ionic/angular/common'; import type { RadioGroupChangeEventDetail, Components } from '@ionic/core/components'; import { defineCustomElement } from '@ionic/core/components/ion-radio-group.js'; @@ -26,8 +26,7 @@ const RADIO_GROUP_INPUTS = [ 'value', ]; -/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ -const RADIO_GROUP_PROXY_INPUTS = RADIO_GROUP_INPUTS.map((input) => (typeof input === 'string' ? input : input.name)); +const RADIO_GROUP_PROXY_INPUTS = inputNames(RADIO_GROUP_INPUTS); @ProxyCmp({ defineCustomElementFn: defineCustomElement, diff --git a/packages/angular/src/standalone/directives/range.ts b/packages/angular/src/standalone/directives/range.ts index 9b335bcbe13..b7affb09a0b 100644 --- a/packages/angular/src/standalone/directives/range.ts +++ b/packages/angular/src/standalone/directives/range.ts @@ -10,7 +10,7 @@ import { forwardRef, } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; -import { ValueAccessor } from '@ionic/angular/common'; +import { inputNames, ValueAccessor } from '@ionic/angular/common'; import type { RangeChangeEventDetail, RangeKnobMoveStartEventDetail, @@ -42,8 +42,7 @@ const RANGE_INPUTS = [ 'value', ]; -/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ -const RANGE_PROXY_INPUTS = RANGE_INPUTS.map((input) => (typeof input === 'string' ? input : input.name)); +const RANGE_PROXY_INPUTS = inputNames(RANGE_INPUTS); @ProxyCmp({ defineCustomElementFn: defineCustomElement, diff --git a/packages/angular/src/standalone/directives/searchbar.ts b/packages/angular/src/standalone/directives/searchbar.ts index 233e9288ba9..1281337c1d7 100644 --- a/packages/angular/src/standalone/directives/searchbar.ts +++ b/packages/angular/src/standalone/directives/searchbar.ts @@ -10,7 +10,7 @@ import { forwardRef, } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; -import { ValueAccessor } from '@ionic/angular/common'; +import { inputNames, ValueAccessor } from '@ionic/angular/common'; import type { SearchbarInputEventDetail, SearchbarChangeEventDetail, Components } from '@ionic/core/components'; import { defineCustomElement } from '@ionic/core/components/ion-searchbar.js'; @@ -40,8 +40,7 @@ const SEARCHBAR_INPUTS = [ 'value', ]; -/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ -const SEARCHBAR_PROXY_INPUTS = SEARCHBAR_INPUTS.map((input) => (typeof input === 'string' ? input : input.name)); +const SEARCHBAR_PROXY_INPUTS = inputNames(SEARCHBAR_INPUTS); @ProxyCmp({ defineCustomElementFn: defineCustomElement, diff --git a/packages/angular/src/standalone/directives/segment.ts b/packages/angular/src/standalone/directives/segment.ts index 5d27d432d25..e08a5277787 100644 --- a/packages/angular/src/standalone/directives/segment.ts +++ b/packages/angular/src/standalone/directives/segment.ts @@ -10,7 +10,7 @@ import { forwardRef, } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; -import { ValueAccessor } from '@ionic/angular/common'; +import { inputNames, ValueAccessor } from '@ionic/angular/common'; import type { SegmentChangeEventDetail, Components } from '@ionic/core/components'; import { defineCustomElement } from '@ionic/core/components/ion-segment.js'; @@ -27,8 +27,7 @@ const SEGMENT_INPUTS = [ 'value', ]; -/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ -const SEGMENT_PROXY_INPUTS = SEGMENT_INPUTS.map((input) => (typeof input === 'string' ? input : input.name)); +const SEGMENT_PROXY_INPUTS = inputNames(SEGMENT_INPUTS); @ProxyCmp({ defineCustomElementFn: defineCustomElement, diff --git a/packages/angular/src/standalone/directives/select.ts b/packages/angular/src/standalone/directives/select.ts index 05228b18bd8..7196f81770c 100644 --- a/packages/angular/src/standalone/directives/select.ts +++ b/packages/angular/src/standalone/directives/select.ts @@ -10,7 +10,7 @@ import { forwardRef, } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; -import { ValueAccessor } from '@ionic/angular/common'; +import { inputNames, ValueAccessor } from '@ionic/angular/common'; import type { SelectChangeEventDetail, Components } from '@ionic/core/components'; import { defineCustomElement } from '@ionic/core/components/ion-select.js'; @@ -44,8 +44,7 @@ const SELECT_INPUTS = [ 'value', ]; -/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ -const SELECT_PROXY_INPUTS = SELECT_INPUTS.map((input) => (typeof input === 'string' ? input : input.name)); +const SELECT_PROXY_INPUTS = inputNames(SELECT_INPUTS); @ProxyCmp({ defineCustomElementFn: defineCustomElement, diff --git a/packages/angular/src/standalone/directives/textarea.ts b/packages/angular/src/standalone/directives/textarea.ts index f3418e84eb0..f3145c6cc86 100644 --- a/packages/angular/src/standalone/directives/textarea.ts +++ b/packages/angular/src/standalone/directives/textarea.ts @@ -10,7 +10,7 @@ import { forwardRef, } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; -import { ValueAccessor } from '@ionic/angular/common'; +import { inputNames, ValueAccessor } from '@ionic/angular/common'; import type { TextareaChangeEventDetail, TextareaInputEventDetail, Components } from '@ionic/core/components'; import { defineCustomElement } from '@ionic/core/components/ion-textarea.js'; @@ -49,8 +49,7 @@ const TEXTAREA_INPUTS = [ 'wrap', ]; -/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ -const TEXTAREA_PROXY_INPUTS = TEXTAREA_INPUTS.map((input) => (typeof input === 'string' ? input : input.name)); +const TEXTAREA_PROXY_INPUTS = inputNames(TEXTAREA_INPUTS); @ProxyCmp({ defineCustomElementFn: defineCustomElement, diff --git a/packages/angular/src/standalone/directives/toggle.ts b/packages/angular/src/standalone/directives/toggle.ts index 10365c70ad1..68732936c7a 100644 --- a/packages/angular/src/standalone/directives/toggle.ts +++ b/packages/angular/src/standalone/directives/toggle.ts @@ -10,7 +10,7 @@ import { forwardRef, } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; -import { ValueAccessor, setIonicClasses } from '@ionic/angular/common'; +import { inputNames, setIonicClasses, ValueAccessor } from '@ionic/angular/common'; import type { ToggleChangeEventDetail, Components } from '@ionic/core/components'; import { defineCustomElement } from '@ionic/core/components/ion-toggle.js'; @@ -32,8 +32,7 @@ const TOGGLE_INPUTS = [ 'value', ]; -/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ -const TOGGLE_PROXY_INPUTS = TOGGLE_INPUTS.map((input) => (typeof input === 'string' ? input : input.name)); +const TOGGLE_PROXY_INPUTS = inputNames(TOGGLE_INPUTS); @ProxyCmp({ defineCustomElementFn: defineCustomElement, From ad54a3f7704b93a773fcbdf4bc463b91305dc7ec Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Tue, 22 Sep 2026 11:33:27 -0700 Subject: [PATCH 09/13] docs(breaking): update coercion bullet Co-authored-by: Shane --- BREAKING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BREAKING.md b/BREAKING.md index b1d5c49ae87..4a9b41f23f9 100644 --- a/BREAKING.md +++ b/BREAKING.md @@ -251,4 +251,4 @@ error TS2322: Type 'number' is not assignable to type 'string | boolean | null | Coerce the expression to a boolean, with an explicit comparison or `!!value`. Bindings that already pass a boolean, a string, `null` or `undefined` are unaffected. -Coercion also moves from Stencil to Angular, which changes the result for numbers. `0` and `NaN` previously became `false` and now become `true`, matching Angular's own `booleanAttribute`. An app without `strictTemplates` gets no compile error for the binding above, so an empty list now disables the item rather than enabling it. Coercing the expression fixes both the type error and the runtime change. +Coercion also moves from Stencil to Angular, which changes the result for numbers. `0` and `NaN` previously became `false` and now become `true`, matching Angular's own `booleanAttribute`. An app without `strictTemplates` gets no compile error for the binding above, so an empty list now renders the item as tappable rather than plain. Coercing the expression fixes both the type error and the runtime change. From 571f69aea1da851636d3e03f9a1ad0794af0358e Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Tue, 22 Sep 2026 11:34:29 -0700 Subject: [PATCH 10/13] docs(component-guide): update to use inputNames Co-authored-by: Shane --- docs/component-guide.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/component-guide.md b/docs/component-guide.md index c9e35aed53b..37624ad8100 100644 --- a/docs/component-guide.md +++ b/docs/component-guide.md @@ -925,14 +925,12 @@ For standalone components, create a directive in the [standalone package](/packa Boolean inputs take the `nullableBooleanAttribute` transform, so that they can be set by attribute presence the same way they can on the generated proxies: ```typescript +import { inputNames } from '@ionic/angular/common'; + import { nullableBooleanAttribute } from './angular-component-lib/boolean-attribute'; const NEW_COMPONENT_INPUTS = [{ name: 'disabled', transform: nullableBooleanAttribute }, 'mode']; - -/* ProxyCmp only needs the names, and runs at runtime rather than through the Angular compiler. */ -const NEW_COMPONENT_PROXY_INPUTS = NEW_COMPONENT_INPUTS.map((input) => - typeof input === 'string' ? input : input.name -); +const NEW_COMPONENT_PROXY_INPUTS = inputNames(NEW_COMPONENT_INPUTS); ``` Pass `NEW_COMPONENT_INPUTS` to `@Component({ inputs })` and `NEW_COMPONENT_PROXY_INPUTS` to `@ProxyCmp({ inputs })`. Unlike Angular's own `booleanAttribute`, the transform passes `null` and `undefined` through rather than coercing them to `false`, because components frequently treat them as a state distinct from `false`. Wrappers under [`common/`](/packages/angular/src/common) import it from `../utils/boolean-attribute`, since the output target only copies `angular-component-lib/` next to the files it generates. From 910cd3689e0a39fc80a42fdc48516c35751217d3 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Tue, 22 Sep 2026 11:39:08 -0700 Subject: [PATCH 11/13] docs(angular): note the common wrapper import paths in the component guide --- docs/component-guide.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/component-guide.md b/docs/component-guide.md index 37624ad8100..bcb8351c507 100644 --- a/docs/component-guide.md +++ b/docs/component-guide.md @@ -933,7 +933,9 @@ const NEW_COMPONENT_INPUTS = [{ name: 'disabled', transform: nullableBooleanAttr const NEW_COMPONENT_PROXY_INPUTS = inputNames(NEW_COMPONENT_INPUTS); ``` -Pass `NEW_COMPONENT_INPUTS` to `@Component({ inputs })` and `NEW_COMPONENT_PROXY_INPUTS` to `@ProxyCmp({ inputs })`. Unlike Angular's own `booleanAttribute`, the transform passes `null` and `undefined` through rather than coercing them to `false`, because components frequently treat them as a state distinct from `false`. Wrappers under [`common/`](/packages/angular/src/common) import it from `../utils/boolean-attribute`, since the output target only copies `angular-component-lib/` next to the files it generates. +Pass `NEW_COMPONENT_INPUTS` to `@Component({ inputs })` and `NEW_COMPONENT_PROXY_INPUTS` to `@ProxyCmp({ inputs })`. Unlike Angular's own `booleanAttribute`, the transform passes `null` and `undefined` through rather than coercing them to `false`, because components frequently treat them as a state distinct from `false`. + +Wrappers under [`common/`](/packages/angular/src/common) import both by relative path instead: `nullableBooleanAttribute` from `../utils/boolean-attribute`, since the output target only copies `angular-component-lib/` next to the files it generates, and `inputNames` from `../utils/proxy`, since they sit inside the package that defines it. After creating the directive, you need to export it in two places: From e02913d332477900f9b8df0b16287dc13b183e8f Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Tue, 22 Sep 2026 14:19:58 -0700 Subject: [PATCH 12/13] chore(angular): ignore the generated boolean attribute helper in prettier --- packages/angular/.prettierignore | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/angular/.prettierignore b/packages/angular/.prettierignore index daee675330e..3fbe2f57cab 100644 --- a/packages/angular/.prettierignore +++ b/packages/angular/.prettierignore @@ -5,3 +5,4 @@ proxies.ts src/lazy/directives/proxies-list.ts src/standalone/directives/ion-*.ts **/*/angular-component-lib/utils.ts +**/*/angular-component-lib/boolean-attribute.ts From bf6c456017fde12ac3460901752bf92810aadad1 Mon Sep 17 00:00:00 2001 From: Maria Hutt Date: Tue, 22 Sep 2026 15:01:19 -0700 Subject: [PATCH 13/13] test(angular): pin the undefined passthrough on boolean input transforms --- .../angular/test/base/e2e/src/standalone/modal.spec.ts | 9 +++++++++ .../base/src/app/standalone/modal/modal.component.html | 3 ++- .../base/src/app/standalone/modal/modal.component.ts | 2 ++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/angular/test/base/e2e/src/standalone/modal.spec.ts b/packages/angular/test/base/e2e/src/standalone/modal.spec.ts index 081ac8fc252..c9355a91d66 100644 --- a/packages/angular/test/base/e2e/src/standalone/modal.spec.ts +++ b/packages/angular/test/base/e2e/src/standalone/modal.spec.ts @@ -5,6 +5,15 @@ test.describe('Modals: Inline', () => { await page.goto('/standalone/modal'); }); + /* + * Pins the one behavior that distinguishes `nullableBooleanAttribute` from Angular's + * `booleanAttribute`. Attribute presence, `'false'` and an absent attribute all behave + * the same under either, because Stencil coerces them itself. + */ + test('should leave an undefined boolean input as undefined', async ({ page }) => { + await expect(page.locator('ion-modal')).toHaveJSProperty('focusTrap', undefined); + }); + test('should render modal', async ({ page }) => { await page.locator('button#open-modal').click(); diff --git a/packages/angular/test/base/src/app/standalone/modal/modal.component.html b/packages/angular/test/base/src/app/standalone/modal/modal.component.html index af65317ff2a..a6697d84c29 100644 --- a/packages/angular/test/base/src/app/standalone/modal/modal.component.html +++ b/packages/angular/test/base/src/app/standalone/modal/modal.component.html @@ -1,6 +1,7 @@ - + + diff --git a/packages/angular/test/base/src/app/standalone/modal/modal.component.ts b/packages/angular/test/base/src/app/standalone/modal/modal.component.ts index 9868c0b7140..396126a2594 100644 --- a/packages/angular/test/base/src/app/standalone/modal/modal.component.ts +++ b/packages/angular/test/base/src/app/standalone/modal/modal.component.ts @@ -8,4 +8,6 @@ import { IonModal } from '@ionic/angular'; imports: [IonModal] }) export class ModalComponent { + /* Reaches the input as `undefined`, the way an unresolved `async` pipe would. */ + unsetFocusTrap: boolean | undefined = undefined; }