diff --git a/core/stencil.config.ts b/core/stencil.config.ts index 300a8fd634..0107c8f76b 100644 --- a/core/stencil.config.ts +++ b/core/stencil.config.ts @@ -50,7 +50,22 @@ const getAngularOutputTargets = () => { * are reliant on the CE build will reference the wrong * import location. */ - 'ion-icon' + 'ion-icon', + /** + * Value Accessors are manually implemented in the `@ionic/angular/standalone` package. + */ + 'ion-input', + 'ion-textarea', + 'ion-searchbar', + 'ion-datetime', + 'ion-radio', + 'ion-segment', + 'ion-checkbox', + 'ion-toggle', + 'ion-range', + 'ion-radio-group', + 'ion-select' + ], outputType: 'standalone', }) diff --git a/packages/angular/common/src/directives/control-value-accessors/index.ts b/packages/angular/common/src/directives/control-value-accessors/index.ts new file mode 100644 index 0000000000..e00caae1ac --- /dev/null +++ b/packages/angular/common/src/directives/control-value-accessors/index.ts @@ -0,0 +1 @@ +export * from './value-accessor'; diff --git a/packages/angular/src/directives/control-value-accessors/value-accessor.ts b/packages/angular/common/src/directives/control-value-accessors/value-accessor.ts similarity index 87% rename from packages/angular/src/directives/control-value-accessors/value-accessor.ts rename to packages/angular/common/src/directives/control-value-accessors/value-accessor.ts index 67042a1a2d..9c4dc7444b 100644 --- a/packages/angular/src/directives/control-value-accessors/value-accessor.ts +++ b/packages/angular/common/src/directives/control-value-accessors/value-accessor.ts @@ -2,7 +2,7 @@ import { AfterViewInit, ElementRef, Injector, OnDestroy, Directive, HostListener import { ControlValueAccessor, NgControl } from '@angular/forms'; import { Subscription } from 'rxjs'; -import { raf } from '../../util/util'; +import { raf } from '../../utils/util'; // TODO(FW-2827): types @@ -17,11 +17,11 @@ export class ValueAccessor implements ControlValueAccessor, AfterViewInit, OnDes protected lastValue: any; private statusChanges?: Subscription; - constructor(protected injector: Injector, protected el: ElementRef) {} + constructor(protected injector: Injector, protected elementRef: ElementRef) {} writeValue(value: any): void { - this.el.nativeElement.value = this.lastValue = value; - setIonicClasses(this.el); + this.elementRef.nativeElement.value = this.lastValue = value; + setIonicClasses(this.elementRef); } /** @@ -38,20 +38,20 @@ export class ValueAccessor implements ControlValueAccessor, AfterViewInit, OnDes * @param value The new value of the control. */ handleValueChange(el: HTMLElement, value: any): void { - if (el === this.el.nativeElement) { + if (el === this.elementRef.nativeElement) { if (value !== this.lastValue) { this.lastValue = value; this.onChange(value); } - setIonicClasses(this.el); + setIonicClasses(this.elementRef); } } @HostListener('ionBlur', ['$event.target']) _handleBlurEvent(el: any): void { - if (el === this.el.nativeElement) { + if (el === this.elementRef.nativeElement) { this.onTouched(); - setIonicClasses(this.el); + setIonicClasses(this.elementRef); } } @@ -64,7 +64,7 @@ export class ValueAccessor implements ControlValueAccessor, AfterViewInit, OnDes } setDisabledState(isDisabled: boolean): void { - this.el.nativeElement.disabled = isDisabled; + this.elementRef.nativeElement.disabled = isDisabled; } ngOnDestroy(): void { @@ -87,7 +87,7 @@ export class ValueAccessor implements ControlValueAccessor, AfterViewInit, OnDes // Listen for changes in validity, disabled, or pending states if (ngControl.statusChanges) { - this.statusChanges = ngControl.statusChanges.subscribe(() => setIonicClasses(this.el)); + this.statusChanges = ngControl.statusChanges.subscribe(() => setIonicClasses(this.elementRef)); } /** @@ -102,7 +102,7 @@ export class ValueAccessor implements ControlValueAccessor, AfterViewInit, OnDes const oldFn = formControl[method].bind(formControl); formControl[method] = (...params: any[]) => { oldFn(...params); - setIonicClasses(this.el); + setIonicClasses(this.elementRef); }; } }); @@ -129,7 +129,7 @@ export const setIonicClasses = (element: ElementRef): void => { const getClasses = (element: HTMLElement) => { const classList = element.classList; - const classes = []; + const classes: string[] = []; for (let i = 0; i < classList.length; i++) { const item = classList.item(i); if (item !== null && startsWith(item, 'ng-')) { diff --git a/packages/angular/common/src/index.ts b/packages/angular/common/src/index.ts index e223fb9878..7b834ffe14 100644 --- a/packages/angular/common/src/index.ts +++ b/packages/angular/common/src/index.ts @@ -33,7 +33,10 @@ export { } from './directives/navigation/router-link-delegate'; export { IonNav } from './directives/navigation/nav'; export { IonTabs } from './directives/navigation/tabs'; +export * from './directives/control-value-accessors'; export { ProxyCmp } from './utils/proxy'; export { IonicRouteStrategy } from './utils/routing'; + +export { raf } from './utils/util'; diff --git a/packages/angular/src/util/util.ts b/packages/angular/common/src/utils/util.ts similarity index 100% rename from packages/angular/src/util/util.ts rename to packages/angular/common/src/utils/util.ts diff --git a/packages/angular/src/app-initialize.ts b/packages/angular/src/app-initialize.ts index 557777eaf6..69cf4030f4 100644 --- a/packages/angular/src/app-initialize.ts +++ b/packages/angular/src/app-initialize.ts @@ -1,10 +1,9 @@ import { NgZone } from '@angular/core'; import type { Config, IonicWindow } from '@ionic/angular/common'; +import { raf } from '@ionic/angular/common'; import { setupConfig } from '@ionic/core'; import { applyPolyfills, defineCustomElements } from '@ionic/core/loader'; -import { raf } from './util/util'; - // TODO(FW-2827): types export const appInitialize = (config: Config, doc: Document, zone: NgZone) => { diff --git a/packages/angular/src/directives/control-value-accessors/boolean-value-accessor.ts b/packages/angular/src/directives/control-value-accessors/boolean-value-accessor.ts index be5f8d276c..9c4332fc97 100644 --- a/packages/angular/src/directives/control-value-accessors/boolean-value-accessor.ts +++ b/packages/angular/src/directives/control-value-accessors/boolean-value-accessor.ts @@ -1,7 +1,6 @@ import { Directive, HostListener, ElementRef, Injector } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; - -import { ValueAccessor, setIonicClasses } from './value-accessor'; +import { ValueAccessor, setIonicClasses } from '@ionic/angular/common'; @Directive({ selector: 'ion-checkbox,ion-toggle', @@ -19,8 +18,8 @@ export class BooleanValueAccessorDirective extends ValueAccessor { } writeValue(value: boolean): void { - this.el.nativeElement.checked = this.lastValue = value; - setIonicClasses(this.el); + this.elementRef.nativeElement.checked = this.lastValue = value; + setIonicClasses(this.elementRef); } @HostListener('ionChange', ['$event.target']) diff --git a/packages/angular/src/directives/control-value-accessors/numeric-value-accessor.ts b/packages/angular/src/directives/control-value-accessors/numeric-value-accessor.ts index 33401ee0e1..e63a91f3c7 100644 --- a/packages/angular/src/directives/control-value-accessors/numeric-value-accessor.ts +++ b/packages/angular/src/directives/control-value-accessors/numeric-value-accessor.ts @@ -1,7 +1,6 @@ import { Directive, HostListener, ElementRef, Injector } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; - -import { ValueAccessor } from './value-accessor'; +import { ValueAccessor } from '@ionic/angular/common'; @Directive({ selector: 'ion-input[type=number]', diff --git a/packages/angular/src/directives/control-value-accessors/radio-value-accessor.ts b/packages/angular/src/directives/control-value-accessors/radio-value-accessor.ts index 9c120a6bdf..014a2cf1c0 100644 --- a/packages/angular/src/directives/control-value-accessors/radio-value-accessor.ts +++ b/packages/angular/src/directives/control-value-accessors/radio-value-accessor.ts @@ -1,7 +1,6 @@ import { ElementRef, Injector, Directive, HostListener } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; - -import { ValueAccessor } from './value-accessor'; +import { ValueAccessor } from '@ionic/angular/common'; @Directive({ /* tslint:disable-next-line:directive-selector */ @@ -19,9 +18,12 @@ export class RadioValueAccessorDirective extends ValueAccessor { super(injector, el); } - // TODO(FW-2827): type (HTMLIonRadioElement and HTMLElement are both missing `checked`) @HostListener('ionSelect', ['$event.target']) _handleIonSelect(el: any): void { + /** + * The `el` type is any to access the `checked` state property + * that is not exposed on the type interface. + */ this.handleValueChange(el, el.checked); } } diff --git a/packages/angular/src/directives/control-value-accessors/select-value-accessor.ts b/packages/angular/src/directives/control-value-accessors/select-value-accessor.ts index ad3ec60e52..7e2683edaf 100644 --- a/packages/angular/src/directives/control-value-accessors/select-value-accessor.ts +++ b/packages/angular/src/directives/control-value-accessors/select-value-accessor.ts @@ -1,7 +1,6 @@ import { ElementRef, Injector, Directive, HostListener } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; - -import { ValueAccessor } from './value-accessor'; +import { ValueAccessor } from '@ionic/angular/common'; @Directive({ /* tslint:disable-next-line:directive-selector */ diff --git a/packages/angular/src/directives/control-value-accessors/text-value-accessor.ts b/packages/angular/src/directives/control-value-accessors/text-value-accessor.ts index aec3447db2..f4b52198cf 100644 --- a/packages/angular/src/directives/control-value-accessors/text-value-accessor.ts +++ b/packages/angular/src/directives/control-value-accessors/text-value-accessor.ts @@ -1,7 +1,6 @@ import { ElementRef, Injector, Directive, HostListener } from '@angular/core'; import { NG_VALUE_ACCESSOR } from '@angular/forms'; - -import { ValueAccessor } from './value-accessor'; +import { ValueAccessor } from '@ionic/angular/common'; @Directive({ selector: 'ion-input:not([type=number]),ion-textarea,ion-searchbar', diff --git a/packages/angular/standalone/src/directives/checkbox.ts b/packages/angular/standalone/src/directives/checkbox.ts new file mode 100644 index 0000000000..26f139f379 --- /dev/null +++ b/packages/angular/standalone/src/directives/checkbox.ts @@ -0,0 +1,86 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + HostListener, + Injector, + NgZone, +} from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +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 { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; + +const CHECKBOX_INPUTS = [ + 'checked', + 'color', + 'disabled', + 'indeterminate', + 'justify', + 'labelPlacement', + 'legacy', + 'mode', + 'name', + 'value', +]; + +@ProxyCmp({ + defineCustomElementFn: defineCustomElement, + inputs: CHECKBOX_INPUTS, +}) +@Component({ + selector: 'ion-checkbox', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: CHECKBOX_INPUTS, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: IonCheckbox, + multi: true, + }, + ], + standalone: true, +}) +export class IonCheckbox extends ValueAccessor { + protected el: HTMLElement; + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) { + super(injector, r); + c.detach(); + this.el = r.nativeElement; + proxyOutputs(this, this.el, ['ionChange', 'ionFocus', 'ionBlur']); + } + + writeValue(value: boolean): void { + this.elementRef.nativeElement.checked = this.lastValue = value; + setIonicClasses(this.elementRef); + } + + @HostListener('ionChange', ['$event.target']) + handleIonChange(el: HTMLIonCheckboxElement | HTMLIonToggleElement): void { + this.handleValueChange(el, el.checked); + } +} + +export declare interface IonCheckbox extends Components.IonCheckbox { + /** + * Emitted when the checked property has changed +as a result of a user action such as a click. +This event will not emit when programmatically +setting the checked property. + */ + ionChange: EventEmitter>; + /** + * Emitted when the checkbox has focus. + */ + ionFocus: EventEmitter>; + /** + * Emitted when the checkbox loses focus. + */ + ionBlur: EventEmitter>; +} diff --git a/packages/angular/standalone/src/directives/datetime.ts b/packages/angular/standalone/src/directives/datetime.ts new file mode 100644 index 0000000000..57068f71b9 --- /dev/null +++ b/packages/angular/standalone/src/directives/datetime.ts @@ -0,0 +1,103 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + HostListener, + Injector, + NgZone, +} from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ValueAccessor } from '@ionic/angular/common'; +import type { DatetimeChangeEventDetail, Components } from '@ionic/core/components'; +import { defineCustomElement } from '@ionic/core/components/ion-datetime.js'; + +import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; + +const DATETIME_INPUTS = [ + 'cancelText', + 'clearText', + 'color', + 'dayValues', + 'disabled', + 'doneText', + 'firstDayOfWeek', + 'highlightedDates', + 'hourCycle', + 'hourValues', + 'isDateEnabled', + 'locale', + 'max', + 'min', + 'minuteValues', + 'mode', + 'monthValues', + 'multiple', + 'name', + 'preferWheel', + 'presentation', + 'readonly', + 'showClearButton', + 'showDefaultButtons', + 'showDefaultTimeLabel', + 'showDefaultTitle', + 'size', + 'titleSelectedDatesFormatter', + 'value', + 'yearValues', +]; + +@ProxyCmp({ + defineCustomElementFn: defineCustomElement, + inputs: DATETIME_INPUTS, + methods: ['confirm', 'reset', 'cancel'], +}) +@Component({ + selector: 'ion-datetime', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: DATETIME_INPUTS, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: IonDatetime, + multi: true, + }, + ], + standalone: true, +}) +export class IonDatetime extends ValueAccessor { + protected el: HTMLElement; + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) { + super(injector, r); + c.detach(); + this.el = r.nativeElement; + proxyOutputs(this, this.el, ['ionCancel', 'ionChange', 'ionFocus', 'ionBlur']); + } + + @HostListener('ionChange', ['$event.target']) + handleIonChange(el: HTMLIonDatetimeElement): void { + this.handleValueChange(el, el.value); + } +} + +export declare interface IonDatetime extends Components.IonDatetime { + /** + * Emitted when the datetime selection was cancelled. + */ + ionCancel: EventEmitter>; + /** + * Emitted when the value (selected date) has changed. + */ + ionChange: EventEmitter>; + /** + * Emitted when the datetime has focus. + */ + ionFocus: EventEmitter>; + /** + * Emitted when the datetime loses focus. + */ + ionBlur: EventEmitter>; +} diff --git a/packages/angular/standalone/src/directives/index.ts b/packages/angular/standalone/src/directives/index.ts new file mode 100644 index 0000000000..8a2d8c4abe --- /dev/null +++ b/packages/angular/standalone/src/directives/index.ts @@ -0,0 +1,12 @@ +export * from './checkbox'; +export * from './datetime'; +export * from './icon'; +export * from './input'; +export * from './radio-group'; +export * from './radio'; +export * from './range'; +export * from './searchbar'; +export * from './segment'; +export * from './select'; +export * from './textarea'; +export * from './toggle'; diff --git a/packages/angular/standalone/src/directives/input.ts b/packages/angular/standalone/src/directives/input.ts new file mode 100644 index 0000000000..1fb55006f7 --- /dev/null +++ b/packages/angular/standalone/src/directives/input.ts @@ -0,0 +1,145 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + HostListener, + Injector, + NgZone, +} from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ValueAccessor } from '@ionic/angular/common'; +import type { + InputInputEventDetail as IIonInputInputInputEventDetail, + InputChangeEventDetail as IIonInputInputChangeEventDetail, + Components, +} from '@ionic/core/components'; +import { defineCustomElement } from '@ionic/core/components/ion-input.js'; + +import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; + +const INPUT_INPUTS = [ + 'accept', + 'autocapitalize', + 'autocomplete', + 'autocorrect', + 'autofocus', + 'clearInput', + 'clearOnEdit', + 'color', + 'counter', + 'counterFormatter', + 'debounce', + 'disabled', + 'enterkeyhint', + 'errorText', + 'fill', + 'helperText', + 'inputmode', + 'label', + 'labelPlacement', + 'legacy', + 'max', + 'maxlength', + 'min', + 'minlength', + 'mode', + 'multiple', + 'name', + 'pattern', + 'placeholder', + 'readonly', + 'required', + 'shape', + 'size', + 'spellcheck', + 'step', + 'type', + 'value', +]; + +@ProxyCmp({ + defineCustomElementFn: defineCustomElement, + inputs: INPUT_INPUTS, + methods: ['setFocus', 'getInputElement'], +}) +@Component({ + selector: 'ion-input', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: INPUT_INPUTS, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: IonInput, + multi: true, + }, + ], + standalone: true, +}) +export class IonInput extends ValueAccessor { + protected el: HTMLElement; + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) { + super(injector, r); + c.detach(); + this.el = r.nativeElement; + proxyOutputs(this, this.el, ['ionInput', 'ionChange', 'ionBlur', 'ionFocus']); + } + + @HostListener('ionInput', ['$event.target']) + handleIonInput(el: HTMLIonInputElement): void { + this.handleValueChange(el, el.value); + } + + registerOnChange(fn: (_: any) => void): void { + super.registerOnChange((value: string) => { + if (this.type === 'number') { + /** + * If the input type is `number`, we need to convert the value to a number + * when the value is not empty. If the value is empty, we want to treat + * the value as null. + */ + fn(value === '' ? null : parseFloat(value)); + } else { + fn(value); + } + }); + } +} + +export declare interface IonInput extends Components.IonInput { + /** + * The `ionInput` event is fired each time the user modifies the input's value. +Unlike the `ionChange` event, the `ionInput` event is fired for each alteration +to the input's value. This typically happens for each keystroke as the user types. + +For elements that accept text input (`type=text`, `type=tel`, etc.), the interface +is [`InputEvent`](https://developer.mozilla.org/en-US/docs/Web/API/InputEvent); for others, +the interface is [`Event`](https://developer.mozilla.org/en-US/docs/Web/API/Event). If +the input is cleared on edit, the type is `null`. + */ + ionInput: EventEmitter>; + /** + * The `ionChange` event is fired when the user modifies the input's value. +Unlike the `ionInput` event, the `ionChange` event is only fired when changes +are committed, not as the user types. + +Depending on the way the users interacts with the element, the `ionChange` +event fires at a different moment: +- When the user commits the change explicitly (e.g. by selecting a date +from a date picker for ``, pressing the "Enter" key, etc.). +- When the element loses focus after its value has changed: for elements +where the user's interaction is typing. + */ + ionChange: EventEmitter>; + /** + * Emitted when the input loses focus. + */ + ionBlur: EventEmitter>; + /** + * Emitted when the input has focus. + */ + ionFocus: EventEmitter>; +} diff --git a/packages/angular/standalone/src/directives/proxies.ts b/packages/angular/standalone/src/directives/proxies.ts index dde743002d..d112c3b93c 100644 --- a/packages/angular/standalone/src/directives/proxies.ts +++ b/packages/angular/standalone/src/directives/proxies.ts @@ -23,11 +23,9 @@ import { defineCustomElement as defineIonCardContent } from '@ionic/core/compone import { defineCustomElement as defineIonCardHeader } from '@ionic/core/components/ion-card-header.js'; import { defineCustomElement as defineIonCardSubtitle } from '@ionic/core/components/ion-card-subtitle.js'; import { defineCustomElement as defineIonCardTitle } from '@ionic/core/components/ion-card-title.js'; -import { defineCustomElement as defineIonCheckbox } from '@ionic/core/components/ion-checkbox.js'; import { defineCustomElement as defineIonChip } from '@ionic/core/components/ion-chip.js'; import { defineCustomElement as defineIonCol } from '@ionic/core/components/ion-col.js'; import { defineCustomElement as defineIonContent } from '@ionic/core/components/ion-content.js'; -import { defineCustomElement as defineIonDatetime } from '@ionic/core/components/ion-datetime.js'; import { defineCustomElement as defineIonDatetimeButton } from '@ionic/core/components/ion-datetime-button.js'; import { defineCustomElement as defineIonFab } from '@ionic/core/components/ion-fab.js'; import { defineCustomElement as defineIonFabButton } from '@ionic/core/components/ion-fab-button.js'; @@ -38,7 +36,6 @@ import { defineCustomElement as defineIonHeader } from '@ionic/core/components/i import { defineCustomElement as defineIonImg } from '@ionic/core/components/ion-img.js'; import { defineCustomElement as defineIonInfiniteScroll } from '@ionic/core/components/ion-infinite-scroll.js'; import { defineCustomElement as defineIonInfiniteScrollContent } from '@ionic/core/components/ion-infinite-scroll-content.js'; -import { defineCustomElement as defineIonInput } from '@ionic/core/components/ion-input.js'; import { defineCustomElement as defineIonItem } from '@ionic/core/components/ion-item.js'; import { defineCustomElement as defineIonItemDivider } from '@ionic/core/components/ion-item-divider.js'; import { defineCustomElement as defineIonItemGroup } from '@ionic/core/components/ion-item-group.js'; @@ -56,19 +53,13 @@ import { defineCustomElement as defineIonNavLink } from '@ionic/core/components/ import { defineCustomElement as defineIonNote } from '@ionic/core/components/ion-note.js'; import { defineCustomElement as defineIonPicker } from '@ionic/core/components/ion-picker.js'; import { defineCustomElement as defineIonProgressBar } from '@ionic/core/components/ion-progress-bar.js'; -import { defineCustomElement as defineIonRadio } from '@ionic/core/components/ion-radio.js'; -import { defineCustomElement as defineIonRadioGroup } from '@ionic/core/components/ion-radio-group.js'; -import { defineCustomElement as defineIonRange } from '@ionic/core/components/ion-range.js'; import { defineCustomElement as defineIonRefresher } from '@ionic/core/components/ion-refresher.js'; import { defineCustomElement as defineIonRefresherContent } from '@ionic/core/components/ion-refresher-content.js'; import { defineCustomElement as defineIonReorder } from '@ionic/core/components/ion-reorder.js'; import { defineCustomElement as defineIonReorderGroup } from '@ionic/core/components/ion-reorder-group.js'; import { defineCustomElement as defineIonRippleEffect } from '@ionic/core/components/ion-ripple-effect.js'; import { defineCustomElement as defineIonRow } from '@ionic/core/components/ion-row.js'; -import { defineCustomElement as defineIonSearchbar } from '@ionic/core/components/ion-searchbar.js'; -import { defineCustomElement as defineIonSegment } from '@ionic/core/components/ion-segment.js'; import { defineCustomElement as defineIonSegmentButton } from '@ionic/core/components/ion-segment-button.js'; -import { defineCustomElement as defineIonSelect } from '@ionic/core/components/ion-select.js'; import { defineCustomElement as defineIonSelectOption } from '@ionic/core/components/ion-select-option.js'; import { defineCustomElement as defineIonSkeletonText } from '@ionic/core/components/ion-skeleton-text.js'; import { defineCustomElement as defineIonSpinner } from '@ionic/core/components/ion-spinner.js'; @@ -76,11 +67,9 @@ import { defineCustomElement as defineIonSplitPane } from '@ionic/core/component import { defineCustomElement as defineIonTabBar } from '@ionic/core/components/ion-tab-bar.js'; import { defineCustomElement as defineIonTabButton } from '@ionic/core/components/ion-tab-button.js'; import { defineCustomElement as defineIonText } from '@ionic/core/components/ion-text.js'; -import { defineCustomElement as defineIonTextarea } from '@ionic/core/components/ion-textarea.js'; import { defineCustomElement as defineIonThumbnail } from '@ionic/core/components/ion-thumbnail.js'; import { defineCustomElement as defineIonTitle } from '@ionic/core/components/ion-title.js'; import { defineCustomElement as defineIonToast } from '@ionic/core/components/ion-toast.js'; -import { defineCustomElement as defineIonToggle } from '@ionic/core/components/ion-toggle.js'; import { defineCustomElement as defineIonToolbar } from '@ionic/core/components/ion-toolbar.js'; @ProxyCmp({ defineCustomElementFn: defineIonAccordion, @@ -747,69 +736,6 @@ export class IonCardTitle { export declare interface IonCardTitle extends Components.IonCardTitle {} -@ProxyCmp({ - defineCustomElementFn: defineIonCheckbox, - inputs: [ - 'checked', - 'color', - 'disabled', - 'indeterminate', - 'justify', - 'labelPlacement', - 'legacy', - 'mode', - 'name', - 'value', - ], -}) -@Component({ - selector: 'ion-checkbox', - changeDetection: ChangeDetectionStrategy.OnPush, - template: '', - // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: [ - 'checked', - 'color', - 'disabled', - 'indeterminate', - 'justify', - 'labelPlacement', - 'legacy', - 'mode', - 'name', - 'value', - ], - standalone: true, -}) -export class IonCheckbox { - protected el: HTMLElement; - constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { - c.detach(); - this.el = r.nativeElement; - proxyOutputs(this, this.el, ['ionChange', 'ionFocus', 'ionBlur']); - } -} - -import type { CheckboxChangeEventDetail as IIonCheckboxCheckboxChangeEventDetail } from '@ionic/core/components'; - -export declare interface IonCheckbox extends Components.IonCheckbox { - /** - * Emitted when the checked property has changed -as a result of a user action such as a click. -This event will not emit when programmatically -setting the checked property. - */ - ionChange: EventEmitter>; - /** - * Emitted when the checkbox has focus. - */ - ionFocus: EventEmitter>; - /** - * Emitted when the checkbox loses focus. - */ - ionBlur: EventEmitter>; -} - @ProxyCmp({ defineCustomElementFn: defineIonChip, inputs: ['color', 'disabled', 'mode', 'outline'], @@ -947,111 +873,6 @@ Set `scrollEvents` to `true` to enable. ionScrollEnd: EventEmitter>; } -@ProxyCmp({ - defineCustomElementFn: defineIonDatetime, - inputs: [ - 'cancelText', - 'clearText', - 'color', - 'dayValues', - 'disabled', - 'doneText', - 'firstDayOfWeek', - 'highlightedDates', - 'hourCycle', - 'hourValues', - 'isDateEnabled', - 'locale', - 'max', - 'min', - 'minuteValues', - 'mode', - 'monthValues', - 'multiple', - 'name', - 'preferWheel', - 'presentation', - 'readonly', - 'showClearButton', - 'showDefaultButtons', - 'showDefaultTimeLabel', - 'showDefaultTitle', - 'size', - 'titleSelectedDatesFormatter', - 'value', - 'yearValues', - ], - methods: ['confirm', 'reset', 'cancel'], -}) -@Component({ - selector: 'ion-datetime', - changeDetection: ChangeDetectionStrategy.OnPush, - template: '', - // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: [ - 'cancelText', - 'clearText', - 'color', - 'dayValues', - 'disabled', - 'doneText', - 'firstDayOfWeek', - 'highlightedDates', - 'hourCycle', - 'hourValues', - 'isDateEnabled', - 'locale', - 'max', - 'min', - 'minuteValues', - 'mode', - 'monthValues', - 'multiple', - 'name', - 'preferWheel', - 'presentation', - 'readonly', - 'showClearButton', - 'showDefaultButtons', - 'showDefaultTimeLabel', - 'showDefaultTitle', - 'size', - 'titleSelectedDatesFormatter', - 'value', - 'yearValues', - ], - standalone: true, -}) -export class IonDatetime { - protected el: HTMLElement; - constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { - c.detach(); - this.el = r.nativeElement; - proxyOutputs(this, this.el, ['ionCancel', 'ionChange', 'ionFocus', 'ionBlur']); - } -} - -import type { DatetimeChangeEventDetail as IIonDatetimeDatetimeChangeEventDetail } from '@ionic/core/components'; - -export declare interface IonDatetime extends Components.IonDatetime { - /** - * Emitted when the datetime selection was cancelled. - */ - ionCancel: EventEmitter>; - /** - * Emitted when the value (selected date) has changed. - */ - ionChange: EventEmitter>; - /** - * Emitted when the datetime has focus. - */ - ionFocus: EventEmitter>; - /** - * Emitted when the datetime loses focus. - */ - ionBlur: EventEmitter>; -} - @ProxyCmp({ defineCustomElementFn: defineIonDatetimeButton, inputs: ['color', 'datetime', 'disabled', 'mode'], @@ -1339,142 +1160,6 @@ export class IonInfiniteScrollContent { export declare interface IonInfiniteScrollContent extends Components.IonInfiniteScrollContent {} -@ProxyCmp({ - defineCustomElementFn: defineIonInput, - inputs: [ - 'accept', - 'autocapitalize', - 'autocomplete', - 'autocorrect', - 'autofocus', - 'clearInput', - 'clearOnEdit', - 'color', - 'counter', - 'counterFormatter', - 'debounce', - 'disabled', - 'enterkeyhint', - 'errorText', - 'fill', - 'helperText', - 'inputmode', - 'label', - 'labelPlacement', - 'legacy', - 'max', - 'maxlength', - 'min', - 'minlength', - 'mode', - 'multiple', - 'name', - 'pattern', - 'placeholder', - 'readonly', - 'required', - 'shape', - 'size', - 'spellcheck', - 'step', - 'type', - 'value', - ], - methods: ['setFocus', 'getInputElement'], -}) -@Component({ - selector: 'ion-input', - changeDetection: ChangeDetectionStrategy.OnPush, - template: '', - // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: [ - 'accept', - 'autocapitalize', - 'autocomplete', - 'autocorrect', - 'autofocus', - 'clearInput', - 'clearOnEdit', - 'color', - 'counter', - 'counterFormatter', - 'debounce', - 'disabled', - 'enterkeyhint', - 'errorText', - 'fill', - 'helperText', - 'inputmode', - 'label', - 'labelPlacement', - 'legacy', - 'max', - 'maxlength', - 'min', - 'minlength', - 'mode', - 'multiple', - 'name', - 'pattern', - 'placeholder', - 'readonly', - 'required', - 'shape', - 'size', - 'spellcheck', - 'step', - 'type', - 'value', - ], - standalone: true, -}) -export class IonInput { - protected el: HTMLElement; - constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { - c.detach(); - this.el = r.nativeElement; - proxyOutputs(this, this.el, ['ionInput', 'ionChange', 'ionBlur', 'ionFocus']); - } -} - -import type { InputInputEventDetail as IIonInputInputInputEventDetail } from '@ionic/core/components'; -import type { InputChangeEventDetail as IIonInputInputChangeEventDetail } from '@ionic/core/components'; - -export declare interface IonInput extends Components.IonInput { - /** - * The `ionInput` event is fired each time the user modifies the input's value. -Unlike the `ionChange` event, the `ionInput` event is fired for each alteration -to the input's value. This typically happens for each keystroke as the user types. - -For elements that accept text input (`type=text`, `type=tel`, etc.), the interface -is [`InputEvent`](https://developer.mozilla.org/en-US/docs/Web/API/InputEvent); for others, -the interface is [`Event`](https://developer.mozilla.org/en-US/docs/Web/API/Event). If -the input is cleared on edit, the type is `null`. - */ - ionInput: EventEmitter>; - /** - * The `ionChange` event is fired when the user modifies the input's value. -Unlike the `ionInput` event, the `ionChange` event is only fired when changes -are committed, not as the user types. - -Depending on the way the users interacts with the element, the `ionChange` -event fires at a different moment: -- When the user commits the change explicitly (e.g. by selecting a date -from a date picker for ``, pressing the "Enter" key, etc.). -- When the element loses focus after its value has changed: for elements -where the user's interaction is typing. - */ - ionChange: EventEmitter>; - /** - * Emitted when the input loses focus. - */ - ionBlur: EventEmitter>; - /** - * Emitted when the input has focus. - */ - ionFocus: EventEmitter>; -} - @ProxyCmp({ defineCustomElementFn: defineIonItem, inputs: [ @@ -2081,167 +1766,6 @@ export class IonProgressBar { export declare interface IonProgressBar extends Components.IonProgressBar {} -@ProxyCmp({ - defineCustomElementFn: defineIonRadio, - inputs: ['color', 'disabled', 'justify', 'labelPlacement', 'legacy', 'mode', 'name', 'value'], -}) -@Component({ - selector: 'ion-radio', - changeDetection: ChangeDetectionStrategy.OnPush, - template: '', - // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'disabled', 'justify', 'labelPlacement', 'legacy', 'mode', 'name', 'value'], - standalone: true, -}) -export class IonRadio { - protected el: HTMLElement; - constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { - c.detach(); - this.el = r.nativeElement; - proxyOutputs(this, this.el, ['ionFocus', 'ionBlur']); - } -} - -export declare interface IonRadio extends Components.IonRadio { - /** - * Emitted when the radio button has focus. - */ - ionFocus: EventEmitter>; - /** - * Emitted when the radio button loses focus. - */ - ionBlur: EventEmitter>; -} - -@ProxyCmp({ - defineCustomElementFn: defineIonRadioGroup, - inputs: ['allowEmptySelection', 'name', 'value'], -}) -@Component({ - selector: 'ion-radio-group', - changeDetection: ChangeDetectionStrategy.OnPush, - template: '', - // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['allowEmptySelection', 'name', 'value'], - standalone: true, -}) -export class IonRadioGroup { - protected el: HTMLElement; - constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { - c.detach(); - this.el = r.nativeElement; - proxyOutputs(this, this.el, ['ionChange']); - } -} - -import type { RadioGroupChangeEventDetail as IIonRadioGroupRadioGroupChangeEventDetail } from '@ionic/core/components'; - -export declare interface IonRadioGroup extends Components.IonRadioGroup { - /** - * Emitted when the value has changed. - */ - ionChange: EventEmitter>; -} - -@ProxyCmp({ - defineCustomElementFn: defineIonRange, - inputs: [ - 'activeBarStart', - 'color', - 'debounce', - 'disabled', - 'dualKnobs', - 'label', - 'labelPlacement', - 'legacy', - 'max', - 'min', - 'mode', - 'name', - 'pin', - 'pinFormatter', - 'snaps', - 'step', - 'ticks', - 'value', - ], -}) -@Component({ - selector: 'ion-range', - changeDetection: ChangeDetectionStrategy.OnPush, - template: '', - // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: [ - 'activeBarStart', - 'color', - 'debounce', - 'disabled', - 'dualKnobs', - 'label', - 'labelPlacement', - 'legacy', - 'max', - 'min', - 'mode', - 'name', - 'pin', - 'pinFormatter', - 'snaps', - 'step', - 'ticks', - 'value', - ], - standalone: true, -}) -export class IonRange { - protected el: HTMLElement; - constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { - c.detach(); - this.el = r.nativeElement; - proxyOutputs(this, this.el, ['ionChange', 'ionInput', 'ionFocus', 'ionBlur', 'ionKnobMoveStart', 'ionKnobMoveEnd']); - } -} - -import type { RangeChangeEventDetail as IIonRangeRangeChangeEventDetail } from '@ionic/core/components'; -import type { RangeKnobMoveStartEventDetail as IIonRangeRangeKnobMoveStartEventDetail } from '@ionic/core/components'; -import type { RangeKnobMoveEndEventDetail as IIonRangeRangeKnobMoveEndEventDetail } from '@ionic/core/components'; - -export declare interface IonRange extends Components.IonRange { - /** - * The `ionChange` event is fired for `` elements when the user -modifies the element's value: -- When the user releases the knob after dragging; -- When the user moves the knob with keyboard arrows - -`ionChange` is not fired when the value is changed programmatically. - */ - ionChange: EventEmitter>; - /** - * The `ionInput` event is fired for `` elements when the value -is modified. Unlike `ionChange`, `ionInput` is fired continuously -while the user is dragging the knob. - */ - ionInput: EventEmitter>; - /** - * Emitted when the range has focus. - */ - ionFocus: EventEmitter>; - /** - * Emitted when the range loses focus. - */ - ionBlur: EventEmitter>; - /** - * Emitted when the user starts moving the range knob, whether through -mouse drag, touch gesture, or keyboard interaction. - */ - ionKnobMoveStart: EventEmitter>; - /** - * Emitted when the user finishes moving the range knob, whether through -mouse drag, touch gesture, or keyboard interaction. - */ - ionKnobMoveEnd: EventEmitter>; -} - @ProxyCmp({ defineCustomElementFn: defineIonRefresher, inputs: ['closeDuration', 'disabled', 'pullFactor', 'pullMax', 'pullMin', 'snapbackDuration'], @@ -2404,138 +1928,6 @@ export class IonRow { export declare interface IonRow extends Components.IonRow {} -@ProxyCmp({ - defineCustomElementFn: defineIonSearchbar, - inputs: [ - 'animated', - 'autocomplete', - 'autocorrect', - 'cancelButtonIcon', - 'cancelButtonText', - 'clearIcon', - 'color', - 'debounce', - 'disabled', - 'enterkeyhint', - 'inputmode', - 'mode', - 'name', - 'placeholder', - 'searchIcon', - 'showCancelButton', - 'showClearButton', - 'spellcheck', - 'type', - 'value', - ], - methods: ['setFocus', 'getInputElement'], -}) -@Component({ - selector: 'ion-searchbar', - changeDetection: ChangeDetectionStrategy.OnPush, - template: '', - // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: [ - 'animated', - 'autocomplete', - 'autocorrect', - 'cancelButtonIcon', - 'cancelButtonText', - 'clearIcon', - 'color', - 'debounce', - 'disabled', - 'enterkeyhint', - 'inputmode', - 'mode', - 'name', - 'placeholder', - 'searchIcon', - 'showCancelButton', - 'showClearButton', - 'spellcheck', - 'type', - 'value', - ], - standalone: true, -}) -export class IonSearchbar { - protected el: HTMLElement; - constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { - c.detach(); - this.el = r.nativeElement; - proxyOutputs(this, this.el, ['ionInput', 'ionChange', 'ionCancel', 'ionClear', 'ionBlur', 'ionFocus']); - } -} - -import type { SearchbarInputEventDetail as IIonSearchbarSearchbarInputEventDetail } from '@ionic/core/components'; -import type { SearchbarChangeEventDetail as IIonSearchbarSearchbarChangeEventDetail } from '@ionic/core/components'; - -export declare interface IonSearchbar extends Components.IonSearchbar { - /** - * Emitted when the `value` of the `ion-searchbar` element has changed. - */ - ionInput: EventEmitter>; - /** - * The `ionChange` event is fired for `` elements when the user -modifies the element's value. Unlike the `ionInput` event, the `ionChange` -event is not necessarily fired for each alteration to an element's value. - -The `ionChange` event is fired when the value has been committed -by the user. This can happen when the element loses focus or -when the "Enter" key is pressed. `ionChange` can also fire -when clicking the clear or cancel buttons. - */ - ionChange: EventEmitter>; - /** - * Emitted when the cancel button is clicked. - */ - ionCancel: EventEmitter>; - /** - * Emitted when the clear input button is clicked. - */ - ionClear: EventEmitter>; - /** - * Emitted when the input loses focus. - */ - ionBlur: EventEmitter>; - /** - * Emitted when the input has focus. - */ - ionFocus: EventEmitter>; -} - -@ProxyCmp({ - defineCustomElementFn: defineIonSegment, - inputs: ['color', 'disabled', 'mode', 'scrollable', 'selectOnFocus', 'swipeGesture', 'value'], -}) -@Component({ - selector: 'ion-segment', - changeDetection: ChangeDetectionStrategy.OnPush, - template: '', - // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: ['color', 'disabled', 'mode', 'scrollable', 'selectOnFocus', 'swipeGesture', 'value'], - standalone: true, -}) -export class IonSegment { - protected el: HTMLElement; - constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { - c.detach(); - this.el = r.nativeElement; - proxyOutputs(this, this.el, ['ionChange']); - } -} - -import type { SegmentChangeEventDetail as IIonSegmentSegmentChangeEventDetail } from '@ionic/core/components'; - -export declare interface IonSegment extends Components.IonSegment { - /** - * Emitted when the value property has changed and any -dragging pointer has been released from `ion-segment`. - */ - ionChange: EventEmitter>; -} - @ProxyCmp({ defineCustomElementFn: defineIonSegmentButton, inputs: ['disabled', 'layout', 'mode', 'type', 'value'], @@ -2558,97 +1950,6 @@ export class IonSegmentButton { export declare interface IonSegmentButton extends Components.IonSegmentButton {} -@ProxyCmp({ - defineCustomElementFn: defineIonSelect, - inputs: [ - 'cancelText', - 'color', - 'compareWith', - 'disabled', - 'expandedIcon', - 'fill', - 'interface', - 'interfaceOptions', - 'justify', - 'label', - 'labelPlacement', - 'legacy', - 'mode', - 'multiple', - 'name', - 'okText', - 'placeholder', - 'selectedText', - 'shape', - 'toggleIcon', - 'value', - ], - methods: ['open'], -}) -@Component({ - selector: 'ion-select', - changeDetection: ChangeDetectionStrategy.OnPush, - template: '', - // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: [ - 'cancelText', - 'color', - 'compareWith', - 'disabled', - 'expandedIcon', - 'fill', - 'interface', - 'interfaceOptions', - 'justify', - 'label', - 'labelPlacement', - 'legacy', - 'mode', - 'multiple', - 'name', - 'okText', - 'placeholder', - 'selectedText', - 'shape', - 'toggleIcon', - 'value', - ], - standalone: true, -}) -export class IonSelect { - protected el: HTMLElement; - constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { - c.detach(); - this.el = r.nativeElement; - proxyOutputs(this, this.el, ['ionChange', 'ionCancel', 'ionDismiss', 'ionFocus', 'ionBlur']); - } -} - -import type { SelectChangeEventDetail as IIonSelectSelectChangeEventDetail } from '@ionic/core/components'; - -export declare interface IonSelect extends Components.IonSelect { - /** - * Emitted when the value has changed. - */ - ionChange: EventEmitter>; - /** - * Emitted when the selection is cancelled. - */ - ionCancel: EventEmitter>; - /** - * Emitted when the overlay is dismissed. - */ - ionDismiss: EventEmitter>; - /** - * Emitted when the select has focus. - */ - ionFocus: EventEmitter>; - /** - * Emitted when the select loses focus. - */ - ionBlur: EventEmitter>; -} - @ProxyCmp({ defineCustomElementFn: defineIonSelectOption, inputs: ['disabled', 'value'], @@ -2809,119 +2110,6 @@ export class IonText { export declare interface IonText extends Components.IonText {} -@ProxyCmp({ - defineCustomElementFn: defineIonTextarea, - inputs: [ - 'autoGrow', - 'autocapitalize', - 'autofocus', - 'clearOnEdit', - 'color', - 'cols', - 'counter', - 'counterFormatter', - 'debounce', - 'disabled', - 'enterkeyhint', - 'errorText', - 'fill', - 'helperText', - 'inputmode', - 'label', - 'labelPlacement', - 'legacy', - 'maxlength', - 'minlength', - 'mode', - 'name', - 'placeholder', - 'readonly', - 'required', - 'rows', - 'shape', - 'spellcheck', - 'value', - 'wrap', - ], - methods: ['setFocus', 'getInputElement'], -}) -@Component({ - selector: 'ion-textarea', - 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', - 'legacy', - 'maxlength', - 'minlength', - 'mode', - 'name', - 'placeholder', - 'readonly', - 'required', - 'rows', - 'shape', - 'spellcheck', - 'value', - 'wrap', - ], - standalone: true, -}) -export class IonTextarea { - protected el: HTMLElement; - constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { - c.detach(); - this.el = r.nativeElement; - proxyOutputs(this, this.el, ['ionChange', 'ionInput', 'ionBlur', 'ionFocus']); - } -} - -import type { TextareaChangeEventDetail as IIonTextareaTextareaChangeEventDetail } from '@ionic/core/components'; -import type { TextareaInputEventDetail as IIonTextareaTextareaInputEventDetail } from '@ionic/core/components'; - -export declare interface IonTextarea extends Components.IonTextarea { - /** - * The `ionChange` event is fired when the user modifies the textarea's value. -Unlike the `ionInput` event, the `ionChange` event is fired when -the element loses focus after its value has been modified. - */ - ionChange: EventEmitter>; - /** - * The `ionInput` event is fired each time the user modifies the textarea's value. -Unlike the `ionChange` event, the `ionInput` event is fired for each alteration -to the textarea's value. This typically happens for each keystroke as the user types. - -When `clearOnEdit` is enabled, the `ionInput` event will be fired when -the user clears the textarea by performing a keydown event. - */ - ionInput: EventEmitter>; - /** - * Emitted when the input loses focus. - */ - ionBlur: EventEmitter>; - /** - * Emitted when the input has focus. - */ - ionFocus: EventEmitter>; -} - @ProxyCmp({ defineCustomElementFn: defineIonThumbnail, }) @@ -3075,67 +2263,6 @@ Shorthand for ionToastDidDismiss. didDismiss: EventEmitter>; } -@ProxyCmp({ - defineCustomElementFn: defineIonToggle, - inputs: [ - 'checked', - 'color', - 'disabled', - 'enableOnOffLabels', - 'justify', - 'labelPlacement', - 'legacy', - 'mode', - 'name', - 'value', - ], -}) -@Component({ - selector: 'ion-toggle', - changeDetection: ChangeDetectionStrategy.OnPush, - template: '', - // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property - inputs: [ - 'checked', - 'color', - 'disabled', - 'enableOnOffLabels', - 'justify', - 'labelPlacement', - 'legacy', - 'mode', - 'name', - 'value', - ], - standalone: true, -}) -export class IonToggle { - protected el: HTMLElement; - constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) { - c.detach(); - this.el = r.nativeElement; - proxyOutputs(this, this.el, ['ionChange', 'ionFocus', 'ionBlur']); - } -} - -import type { ToggleChangeEventDetail as IIonToggleToggleChangeEventDetail } from '@ionic/core/components'; - -export declare interface IonToggle extends Components.IonToggle { - /** - * Emitted when the user switches the toggle on or off. Does not emit -when programmatically changing the value of the `checked` property. - */ - ionChange: EventEmitter>; - /** - * Emitted when the toggle has focus. - */ - ionFocus: EventEmitter>; - /** - * Emitted when the toggle loses focus. - */ - ionBlur: EventEmitter>; -} - @ProxyCmp({ defineCustomElementFn: defineIonToolbar, inputs: ['color', 'mode'], diff --git a/packages/angular/standalone/src/directives/radio-group.ts b/packages/angular/standalone/src/directives/radio-group.ts new file mode 100644 index 0000000000..998057a5e3 --- /dev/null +++ b/packages/angular/standalone/src/directives/radio-group.ts @@ -0,0 +1,59 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + HostListener, + Injector, + NgZone, +} from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +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 { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; + +const RADIO_GROUP_INPUTS = ['allowEmptySelection', 'name', 'value']; + +@ProxyCmp({ + defineCustomElementFn: defineCustomElement, + inputs: RADIO_GROUP_INPUTS, +}) +@Component({ + selector: 'ion-radio-group', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: RADIO_GROUP_INPUTS, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: IonRadioGroup, + multi: true, + }, + ], + standalone: true, +}) +export class IonRadioGroup extends ValueAccessor { + protected el: HTMLElement; + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) { + super(injector, r); + c.detach(); + this.el = r.nativeElement; + proxyOutputs(this, this.el, ['ionChange']); + } + + @HostListener('ionChange', ['$event.target']) + handleIonChange(el: HTMLIonRadioGroupElement): void { + this.handleValueChange(el, el.value); + } +} + +export declare interface IonRadioGroup extends Components.IonRadioGroup { + /** + * Emitted when the value has changed. + */ + ionChange: EventEmitter>; +} diff --git a/packages/angular/standalone/src/directives/radio.ts b/packages/angular/standalone/src/directives/radio.ts new file mode 100644 index 0000000000..9ad32fadf8 --- /dev/null +++ b/packages/angular/standalone/src/directives/radio.ts @@ -0,0 +1,67 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + HostListener, + Injector, + NgZone, +} from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ValueAccessor } from '@ionic/angular/common'; +import type { Components } from '@ionic/core/components'; +import { defineCustomElement } from '@ionic/core/components/ion-radio.js'; + +import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; + +const RADIO_INPUTS = ['color', 'disabled', 'justify', 'labelPlacement', 'legacy', 'mode', 'name', 'value']; + +@ProxyCmp({ + defineCustomElementFn: defineCustomElement, + inputs: RADIO_INPUTS, +}) +@Component({ + selector: 'ion-radio', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: RADIO_INPUTS, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: IonRadio, + multi: true, + }, + ], + standalone: true, +}) +export class IonRadio extends ValueAccessor { + protected el: HTMLElement; + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) { + super(injector, r); + c.detach(); + this.el = r.nativeElement; + proxyOutputs(this, this.el, ['ionFocus', 'ionBlur']); + } + + @HostListener('ionSelect', ['$event.target']) + handleIonSelect(el: any): void { + /** + * The `el` type is any to access the `checked` state property + * that is not exposed on the type interface. + */ + this.handleValueChange(el, el.checked); + } +} + +export declare interface IonRadio extends Components.IonRadio { + /** + * Emitted when the radio button has focus. + */ + ionFocus: EventEmitter>; + /** + * Emitted when the radio button loses focus. + */ + ionBlur: EventEmitter>; +} diff --git a/packages/angular/standalone/src/directives/range.ts b/packages/angular/standalone/src/directives/range.ts new file mode 100644 index 0000000000..36aa4c93fc --- /dev/null +++ b/packages/angular/standalone/src/directives/range.ts @@ -0,0 +1,112 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + HostListener, + Injector, + NgZone, +} from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ValueAccessor } from '@ionic/angular/common'; +import type { + RangeChangeEventDetail, + RangeKnobMoveStartEventDetail, + RangeKnobMoveEndEventDetail, + Components, +} from '@ionic/core/components'; +import { defineCustomElement } from '@ionic/core/components/ion-range.js'; + +import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; + +const RANGE_INPUTS = [ + 'activeBarStart', + 'color', + 'debounce', + 'disabled', + 'dualKnobs', + 'label', + 'labelPlacement', + 'legacy', + 'max', + 'min', + 'mode', + 'name', + 'pin', + 'pinFormatter', + 'snaps', + 'step', + 'ticks', + 'value', +]; + +@ProxyCmp({ + defineCustomElementFn: defineCustomElement, + inputs: RANGE_INPUTS, +}) +@Component({ + selector: 'ion-range', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: RANGE_INPUTS, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: IonRange, + multi: true, + }, + ], + standalone: true, +}) +export class IonRange extends ValueAccessor { + protected el: HTMLElement; + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) { + super(injector, r); + c.detach(); + this.el = r.nativeElement; + proxyOutputs(this, this.el, ['ionChange', 'ionInput', 'ionFocus', 'ionBlur', 'ionKnobMoveStart', 'ionKnobMoveEnd']); + } + + @HostListener('ionChange', ['$event.target']) + handleIonChange(el: HTMLIonRangeElement): void { + this.handleValueChange(el, el.value); + } +} + +export declare interface IonRange extends Components.IonRange { + /** + * The `ionChange` event is fired for `` elements when the user +modifies the element's value: +- When the user releases the knob after dragging; +- When the user moves the knob with keyboard arrows + +`ionChange` is not fired when the value is changed programmatically. + */ + ionChange: EventEmitter>; + /** + * The `ionInput` event is fired for `` elements when the value +is modified. Unlike `ionChange`, `ionInput` is fired continuously +while the user is dragging the knob. + */ + ionInput: EventEmitter>; + /** + * Emitted when the range has focus. + */ + ionFocus: EventEmitter>; + /** + * Emitted when the range loses focus. + */ + ionBlur: EventEmitter>; + /** + * Emitted when the user starts moving the range knob, whether through +mouse drag, touch gesture, or keyboard interaction. + */ + ionKnobMoveStart: EventEmitter>; + /** + * Emitted when the user finishes moving the range knob, whether through +mouse drag, touch gesture, or keyboard interaction. + */ + ionKnobMoveEnd: EventEmitter>; +} diff --git a/packages/angular/standalone/src/directives/searchbar.ts b/packages/angular/standalone/src/directives/searchbar.ts new file mode 100644 index 0000000000..5a4abd6d31 --- /dev/null +++ b/packages/angular/standalone/src/directives/searchbar.ts @@ -0,0 +1,108 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + HostListener, + Injector, + NgZone, +} from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +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 { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; + +const SEARCHBAR_INPUTS = [ + 'animated', + 'autocomplete', + 'autocorrect', + 'cancelButtonIcon', + 'cancelButtonText', + 'clearIcon', + 'color', + 'debounce', + 'disabled', + 'enterkeyhint', + 'inputmode', + 'mode', + 'name', + 'placeholder', + 'searchIcon', + 'showCancelButton', + 'showClearButton', + 'spellcheck', + 'type', + 'value', +]; + +@ProxyCmp({ + defineCustomElementFn: defineCustomElement, + inputs: SEARCHBAR_INPUTS, + methods: ['setFocus', 'getInputElement'], +}) +@Component({ + selector: 'ion-searchbar', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: SEARCHBAR_INPUTS, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: IonSearchbar, + multi: true, + }, + ], + standalone: true, +}) +export class IonSearchbar extends ValueAccessor { + protected el: HTMLElement; + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) { + super(injector, r); + c.detach(); + this.el = r.nativeElement; + proxyOutputs(this, this.el, ['ionInput', 'ionChange', 'ionCancel', 'ionClear', 'ionBlur', 'ionFocus']); + } + + @HostListener('ionInput', ['$event.target']) + handleIonInput(el: HTMLIonSearchbarElement): void { + this.handleValueChange(el, el.value); + } +} + +export declare interface IonSearchbar extends Components.IonSearchbar { + /** + * Emitted when the `value` of the `ion-searchbar` element has changed. + */ + ionInput: EventEmitter>; + /** + * The `ionChange` event is fired for `` elements when the user +modifies the element's value. Unlike the `ionInput` event, the `ionChange` +event is not necessarily fired for each alteration to an element's value. + +The `ionChange` event is fired when the value has been committed +by the user. This can happen when the element loses focus or +when the "Enter" key is pressed. `ionChange` can also fire +when clicking the clear or cancel buttons. + */ + ionChange: EventEmitter>; + /** + * Emitted when the cancel button is clicked. + */ + ionCancel: EventEmitter>; + /** + * Emitted when the clear input button is clicked. + */ + ionClear: EventEmitter>; + /** + * Emitted when the input loses focus. + */ + ionBlur: EventEmitter>; + /** + * Emitted when the input has focus. + */ + ionFocus: EventEmitter>; +} diff --git a/packages/angular/standalone/src/directives/segment.ts b/packages/angular/standalone/src/directives/segment.ts new file mode 100644 index 0000000000..760b990d5e --- /dev/null +++ b/packages/angular/standalone/src/directives/segment.ts @@ -0,0 +1,60 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + HostListener, + Injector, + NgZone, +} from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ValueAccessor } from '@ionic/angular/common'; +import type { SegmentChangeEventDetail, Components } from '@ionic/core/components'; +import { defineCustomElement } from '@ionic/core/components/ion-segment.js'; + +import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; + +const SEGMENT_INPUTS = ['color', 'disabled', 'mode', 'scrollable', 'selectOnFocus', 'swipeGesture', 'value']; + +@ProxyCmp({ + defineCustomElementFn: defineCustomElement, + inputs: SEGMENT_INPUTS, +}) +@Component({ + selector: 'ion-segment', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: SEGMENT_INPUTS, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: IonSegment, + multi: true, + }, + ], + standalone: true, +}) +export class IonSegment extends ValueAccessor { + protected el: HTMLElement; + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) { + super(injector, r); + c.detach(); + this.el = r.nativeElement; + proxyOutputs(this, this.el, ['ionChange']); + } + + @HostListener('ionChange', ['$event.target']) + handleIonChange(el: HTMLIonSegmentElement): void { + this.handleValueChange(el, el.value); + } +} + +export declare interface IonSegment extends Components.IonSegment { + /** + * Emitted when the value property has changed and any +dragging pointer has been released from `ion-segment`. + */ + ionChange: EventEmitter>; +} diff --git a/packages/angular/standalone/src/directives/select.ts b/packages/angular/standalone/src/directives/select.ts new file mode 100644 index 0000000000..f0d19ce078 --- /dev/null +++ b/packages/angular/standalone/src/directives/select.ts @@ -0,0 +1,98 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + HostListener, + Injector, + NgZone, +} from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ValueAccessor } from '@ionic/angular/common'; +import type { SelectChangeEventDetail, Components } from '@ionic/core/components'; +import { defineCustomElement } from '@ionic/core/components/ion-select.js'; + +import { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; + +const SELECT_INPUTS = [ + 'cancelText', + 'color', + 'compareWith', + 'disabled', + 'expandedIcon', + 'fill', + 'interface', + 'interfaceOptions', + 'justify', + 'label', + 'labelPlacement', + 'legacy', + 'mode', + 'multiple', + 'name', + 'okText', + 'placeholder', + 'selectedText', + 'shape', + 'toggleIcon', + 'value', +]; + +@ProxyCmp({ + defineCustomElementFn: defineCustomElement, + inputs: SELECT_INPUTS, + methods: ['open'], +}) +@Component({ + selector: 'ion-select', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: SELECT_INPUTS, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: IonSelect, + multi: true, + }, + ], + standalone: true, +}) +export class IonSelect extends ValueAccessor { + protected el: HTMLElement; + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) { + super(injector, r); + c.detach(); + this.el = r.nativeElement; + proxyOutputs(this, this.el, ['ionChange', 'ionCancel', 'ionDismiss', 'ionFocus', 'ionBlur']); + } + + @HostListener('ionChange', ['$event.target']) + handleIonChange(el: HTMLIonSelectElement): void { + this.handleValueChange(el, el.value); + } +} + +export declare interface IonSelect extends Components.IonSelect { + /** + * Emitted when the value has changed. + */ + ionChange: EventEmitter>; + /** + * Emitted when the selection is cancelled. + */ + ionCancel: EventEmitter>; + /** + * Emitted when the overlay is dismissed. + */ + ionDismiss: EventEmitter>; + /** + * Emitted when the select has focus. + */ + ionFocus: EventEmitter>; + /** + * Emitted when the select loses focus. + */ + ionBlur: EventEmitter>; +} diff --git a/packages/angular/standalone/src/directives/textarea.ts b/packages/angular/standalone/src/directives/textarea.ts new file mode 100644 index 0000000000..7523963232 --- /dev/null +++ b/packages/angular/standalone/src/directives/textarea.ts @@ -0,0 +1,110 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + HostListener, + Injector, + NgZone, +} from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +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 { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; + +const TEXTAREA_INPUTS = [ + 'autoGrow', + 'autocapitalize', + 'autofocus', + 'clearOnEdit', + 'color', + 'cols', + 'counter', + 'counterFormatter', + 'debounce', + 'disabled', + 'enterkeyhint', + 'errorText', + 'fill', + 'helperText', + 'inputmode', + 'label', + 'labelPlacement', + 'legacy', + 'maxlength', + 'minlength', + 'mode', + 'name', + 'placeholder', + 'readonly', + 'required', + 'rows', + 'shape', + 'spellcheck', + 'value', + 'wrap', +]; + +@ProxyCmp({ + defineCustomElementFn: defineCustomElement, + inputs: TEXTAREA_INPUTS, + methods: ['setFocus', 'getInputElement'], +}) +@Component({ + selector: 'ion-textarea', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: TEXTAREA_INPUTS, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: IonTextarea, + multi: true, + }, + ], + standalone: true, +}) +export class IonTextarea extends ValueAccessor { + protected el: HTMLElement; + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) { + super(injector, r); + c.detach(); + this.el = r.nativeElement; + proxyOutputs(this, this.el, ['ionChange', 'ionInput', 'ionBlur', 'ionFocus']); + } + + @HostListener('ionInput', ['$event.target']) + handleIonInput(el: HTMLIonTextareaElement): void { + this.handleValueChange(el, el.value); + } +} + +export declare interface IonTextarea extends Components.IonTextarea { + /** + * The `ionChange` event is fired when the user modifies the textarea's value. +Unlike the `ionInput` event, the `ionChange` event is fired when +the element loses focus after its value has been modified. + */ + ionChange: EventEmitter>; + /** + * The `ionInput` event is fired each time the user modifies the textarea's value. +Unlike the `ionChange` event, the `ionInput` event is fired for each alteration +to the textarea's value. This typically happens for each keystroke as the user types. + +When `clearOnEdit` is enabled, the `ionInput` event will be fired when +the user clears the textarea by performing a keydown event. + */ + ionInput: EventEmitter>; + /** + * Emitted when the input loses focus. + */ + ionBlur: EventEmitter>; + /** + * Emitted when the input has focus. + */ + ionFocus: EventEmitter>; +} diff --git a/packages/angular/standalone/src/directives/toggle.ts b/packages/angular/standalone/src/directives/toggle.ts new file mode 100644 index 0000000000..e1ece5f856 --- /dev/null +++ b/packages/angular/standalone/src/directives/toggle.ts @@ -0,0 +1,84 @@ +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + HostListener, + Injector, + NgZone, +} from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +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 { ProxyCmp, proxyOutputs } from './angular-component-lib/utils'; + +const TOGGLE_INPUTS = [ + 'checked', + 'color', + 'disabled', + 'enableOnOffLabels', + 'justify', + 'labelPlacement', + 'legacy', + 'mode', + 'name', + 'value', +]; + +@ProxyCmp({ + defineCustomElementFn: defineCustomElement, + inputs: TOGGLE_INPUTS, +}) +@Component({ + selector: 'ion-toggle', + changeDetection: ChangeDetectionStrategy.OnPush, + template: '', + // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property + inputs: TOGGLE_INPUTS, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: IonToggle, + multi: true, + }, + ], + standalone: true, +}) +export class IonToggle extends ValueAccessor { + protected el: HTMLElement; + constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone, injector: Injector) { + super(injector, r); + c.detach(); + this.el = r.nativeElement; + proxyOutputs(this, this.el, ['ionChange', 'ionFocus', 'ionBlur']); + } + + writeValue(value: boolean): void { + this.elementRef.nativeElement.checked = this.lastValue = value; + setIonicClasses(this.elementRef); + } + + @HostListener('ionChange', ['$event.target']) + handleIonChange(el: HTMLIonToggleElement): void { + this.handleValueChange(el, el.checked); + } +} + +export declare interface IonToggle extends Components.IonToggle { + /** + * Emitted when the user switches the toggle on or off. Does not emit +when programmatically changing the value of the `checked` property. + */ + ionChange: EventEmitter>; + /** + * Emitted when the toggle has focus. + */ + ionFocus: EventEmitter>; + /** + * Emitted when the toggle loses focus. + */ + ionBlur: EventEmitter>; +} diff --git a/packages/angular/standalone/src/index.ts b/packages/angular/standalone/src/index.ts index 15895b0fef..209fa25d77 100644 --- a/packages/angular/standalone/src/index.ts +++ b/packages/angular/standalone/src/index.ts @@ -24,7 +24,20 @@ export { IonicRouteStrategy, } from '@ionic/angular/common'; export { IonNav } from './navigation/nav'; -export { IonIcon } from './directives/icon'; +export { + IonCheckbox, + IonDatetime, + IonInput, + IonIcon, + IonRadioGroup, + IonRadio, + IonRange, + IonSearchbar, + IonSegment, + IonSelect, + IonTextarea, + IonToggle, +} from './directives'; export * from './directives/proxies'; export { diff --git a/packages/angular/test/base/e2e/src/standalone/value-accessors.spec.ts b/packages/angular/test/base/e2e/src/standalone/value-accessors.spec.ts new file mode 100644 index 0000000000..a8e1c36e90 --- /dev/null +++ b/packages/angular/test/base/e2e/src/standalone/value-accessors.spec.ts @@ -0,0 +1,132 @@ + +describe('Value Accessors', () => { + + describe('Checkbox', () => { + beforeEach(() => cy.visit('/standalone/value-accessors/checkbox')); + + it('should update the form value', () => { + cy.get('#formValue').should('have.text', JSON.stringify({ checkbox: false }, null, 2)); + cy.get('ion-checkbox').should('have.class', 'ion-pristine'); + + cy.get('ion-checkbox').click(); + + cy.get('#formValue').should('have.text', JSON.stringify({ checkbox: true }, null, 2)); + cy.get('ion-checkbox').should('have.class', 'ion-dirty'); + cy.get('ion-checkbox').should('have.class', 'ion-valid'); + }); + }); + + describe('Input', () => { + beforeEach(() => cy.visit('/standalone/value-accessors/input')); + + it('should update the form value', () => { + cy.get('#formValue').should('have.text', JSON.stringify({ + inputString: '', + inputNumber: '' + }, null, 2)); + cy.get('ion-input[formControlName="inputString"]').should('have.class', 'ion-pristine'); + cy.get('ion-input[formControlName="inputNumber"]').should('have.class', 'ion-pristine'); + + cy.get('ion-input[formControlName="inputString"]').should('have.class', 'ion-invalid'); + cy.get('ion-input[formControlName="inputNumber"]').should('have.class', 'ion-invalid'); + + cy.get('ion-input[formControlName="inputString"] input').type('test'); + cy.get('ion-input[formControlName="inputString"] input').blur(); + + cy.get('ion-input[formControlName="inputNumber"] input').type(1); + cy.get('ion-input[formControlName="inputNumber"] input').blur(); + + cy.get('#formValue').should('have.text', JSON.stringify({ + inputString: 'test', + inputNumber: 1 + }, null, 2)); + + cy.get('ion-input[formControlName="inputString"]').should('have.class', 'ion-dirty'); + cy.get('ion-input[formControlName="inputNumber"]').should('have.class', 'ion-dirty'); + + cy.get('ion-input[formControlName="inputString"]').should('have.class', 'ion-valid'); + cy.get('ion-input[formControlName="inputNumber"]').should('have.class', 'ion-valid'); + + }); + }); + + describe('Radio Group', () => { + beforeEach(() => cy.visit('/standalone/value-accessors/radio-group')); + + it('should update the form value', () => { + cy.get('#formValue').should('have.text', JSON.stringify({ radioGroup: '1' }, null, 2)); + cy.get('ion-radio-group').should('have.class', 'ion-pristine'); + + cy.get('ion-radio').contains('Two').click(); + + cy.get('#formValue').should('have.text', JSON.stringify({ radioGroup: '2' }, null, 2)); + cy.get('ion-radio-group').should('have.class', 'ion-dirty'); + cy.get('ion-radio-group').should('have.class', 'ion-valid'); + }); + }); + + describe('Searchbar', () => { + beforeEach(() => cy.visit('/standalone/value-accessors/searchbar')); + + it('should update the form value', () => { + cy.get('#formValue').should('have.text', JSON.stringify({ searchbar: '' }, null, 2)); + cy.get('ion-searchbar').should('have.class', 'ion-pristine'); + cy.get('ion-searchbar').should('have.class', 'ion-invalid'); + + cy.get('ion-searchbar').type('test'); + cy.get('ion-searchbar input').blur(); + + cy.get('#formValue').should('have.text', JSON.stringify({ searchbar: 'test' }, null, 2)); + cy.get('ion-searchbar').should('have.class', 'ion-dirty'); + cy.get('ion-searchbar').should('have.class', 'ion-valid'); + }); + }); + + describe('Segment', () => { + beforeEach(() => cy.visit('/standalone/value-accessors/segment')); + + it('should update the form value', () => { + cy.get('#formValue').should('have.text', JSON.stringify({ segment: 'Paid' }, null, 2)); + cy.get('ion-segment').should('have.class', 'ion-pristine'); + + cy.get('ion-segment-button').eq(1).click(); + + cy.get('#formValue').should('have.text', JSON.stringify({ segment: 'Free' }, null, 2)); + cy.get('ion-segment').should('have.class', 'ion-dirty'); + cy.get('ion-segment').should('have.class', 'ion-valid'); + }); + }); + + describe('Textarea', () => { + beforeEach(() => cy.visit('/standalone/value-accessors/textarea')); + + it('should update the form value', () => { + cy.get('#formValue').should('have.text', JSON.stringify({ textarea: '' }, null, 2)); + cy.get('ion-textarea').should('have.class', 'ion-pristine'); + cy.get('ion-textarea').should('have.class', 'ion-invalid'); + + cy.get('ion-textarea').click(); + cy.get('ion-textarea').type('test'); + + cy.get('#formValue').should('have.text', JSON.stringify({ textarea: 'test' }, null, 2)); + cy.get('ion-textarea').should('have.class', 'ion-dirty'); + cy.get('ion-textarea').should('have.class', 'ion-valid'); + }); + }); + + describe('Toggle', () => { + beforeEach(() => cy.visit('/standalone/value-accessors/toggle')); + + it('should update the form value', () => { + cy.get('#formValue').should('have.text', JSON.stringify({ toggle: false }, null, 2)); + cy.get('ion-toggle').should('have.class', 'ion-pristine'); + + cy.get('ion-toggle').click(); + + cy.get('#formValue').should('have.text', JSON.stringify({ toggle: true }, null, 2)); + cy.get('ion-toggle').should('have.class', 'ion-dirty'); + cy.get('ion-toggle').should('have.class', 'ion-valid'); + }); + }); + +}); diff --git a/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts b/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts index d97d34c66b..93e869651a 100644 --- a/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts +++ b/packages/angular/test/base/src/app/standalone/app-standalone/app.routes.ts @@ -26,6 +26,21 @@ export const routes: Routes = [ { path: 'tab-three', loadComponent: () => import('../tabs/tab3.component').then(c => c.TabThreeComponent) } ] }, + { + path: 'value-accessors', + children: [ + { path: 'checkbox', loadComponent: () => import('../value-accessors/checkbox/checkbox.component').then(c => c.CheckboxComponent) }, + { path: 'datetime', loadComponent: () => import('../value-accessors/datetime/datetime.component').then(c => c.DatetimeComponent) }, + { path: 'input', loadComponent: () => import('../value-accessors/input/input.component').then(c => c.InputComponent) }, + { path: 'radio-group', loadComponent: () => import('../value-accessors/radio-group/radio-group.component').then(c => c.RadioGroupComponent) }, + { path: 'range', loadComponent: () => import('../value-accessors/range/range.component').then(c => c.RangeComponent) }, + { path: 'searchbar', loadComponent: () => import('../value-accessors/searchbar/searchbar.component').then(c => c.SearchbarComponent) }, + { path: 'segment', loadComponent: () => import('../value-accessors/segment/segment.component').then(c => c.SegmentComponent) }, + { path: 'textarea', loadComponent: () => import('../value-accessors/textarea/textarea.component').then(c => c.TextareaComponent) }, + { path: 'toggle', loadComponent: () => import('../value-accessors/toggle/toggle.component').then(c => c.ToggleComponent) }, + { path: '**', redirectTo: 'checkbox' } + ] + } ] }, ]; diff --git a/packages/angular/test/base/src/app/standalone/value-accessors/checkbox/checkbox.component.html b/packages/angular/test/base/src/app/standalone/value-accessors/checkbox/checkbox.component.html new file mode 100644 index 0000000000..2921e06b91 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/value-accessors/checkbox/checkbox.component.html @@ -0,0 +1,11 @@ +
+

IonCheckbox Value Accessors

+

+ This test checks the form integrations with ion-checkbox to make sure values are correctly assigned to the form + group. +

+ + + + +
diff --git a/packages/angular/test/base/src/app/standalone/value-accessors/checkbox/checkbox.component.ts b/packages/angular/test/base/src/app/standalone/value-accessors/checkbox/checkbox.component.ts new file mode 100644 index 0000000000..d58672d954 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/value-accessors/checkbox/checkbox.component.ts @@ -0,0 +1,25 @@ +import { Component } from "@angular/core"; +import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from "@angular/forms"; +import { IonCheckbox } from "@ionic/angular/standalone"; +import { ValueAccessorTestComponent } from "../value-accessor-test/value-accessor-test.component"; + +@Component({ + selector: 'app-checkbox', + templateUrl: 'checkbox.component.html', + standalone: true, + imports: [ + IonCheckbox, + ReactiveFormsModule, + FormsModule, + ValueAccessorTestComponent + ] +}) +export class CheckboxComponent { + + form = this.fb.group({ + checkbox: [false, Validators.required], + }); + + constructor(private fb: FormBuilder) { } + +} diff --git a/packages/angular/test/base/src/app/standalone/value-accessors/datetime/datetime.component.html b/packages/angular/test/base/src/app/standalone/value-accessors/datetime/datetime.component.html new file mode 100644 index 0000000000..22c3633971 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/value-accessors/datetime/datetime.component.html @@ -0,0 +1,11 @@ +
+

IonDatetime Value Accessors

+

+ This test checks the form integrations with ion-datetime to make sure values are correctly assigned to the form + group. +

+ + + + +
diff --git a/packages/angular/test/base/src/app/standalone/value-accessors/datetime/datetime.component.ts b/packages/angular/test/base/src/app/standalone/value-accessors/datetime/datetime.component.ts new file mode 100644 index 0000000000..7d647e7e03 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/value-accessors/datetime/datetime.component.ts @@ -0,0 +1,25 @@ +import { Component } from "@angular/core"; +import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from "@angular/forms"; +import { IonDatetime } from "@ionic/angular/standalone"; +import { ValueAccessorTestComponent } from "../value-accessor-test/value-accessor-test.component"; + +@Component({ + selector: 'app-datetime', + templateUrl: 'datetime.component.html', + standalone: true, + imports: [ + IonDatetime, + ReactiveFormsModule, + FormsModule, + ValueAccessorTestComponent + ] +}) +export class DatetimeComponent { + + form = this.fb.group({ + datetime: ['2023-05-10T04:00:00', Validators.required], + }); + + constructor(private fb: FormBuilder) { } + +} diff --git a/packages/angular/test/base/src/app/standalone/value-accessors/input/input.component.html b/packages/angular/test/base/src/app/standalone/value-accessors/input/input.component.html new file mode 100644 index 0000000000..e63235cea5 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/value-accessors/input/input.component.html @@ -0,0 +1,11 @@ +
+

IonInput Value Accessors

+

+ This test checks the form integrations with ion-input to make sure values are correctly assigned to the form group. +

+ + + + + +
diff --git a/packages/angular/test/base/src/app/standalone/value-accessors/input/input.component.ts b/packages/angular/test/base/src/app/standalone/value-accessors/input/input.component.ts new file mode 100644 index 0000000000..1afa4a3b01 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/value-accessors/input/input.component.ts @@ -0,0 +1,26 @@ +import { Component } from "@angular/core"; +import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from "@angular/forms"; +import { IonInput } from "@ionic/angular/standalone"; +import { ValueAccessorTestComponent } from "../value-accessor-test/value-accessor-test.component"; + +@Component({ + selector: 'app-input', + templateUrl: 'input.component.html', + standalone: true, + imports: [ + IonInput, + ReactiveFormsModule, + FormsModule, + ValueAccessorTestComponent, + ] +}) +export class InputComponent { + + form = this.fb.group({ + inputString: ['', Validators.required], + inputNumber: ['', Validators.required], + }); + + constructor(private fb: FormBuilder) { } + +} diff --git a/packages/angular/test/base/src/app/standalone/value-accessors/radio-group/radio-group.component.html b/packages/angular/test/base/src/app/standalone/value-accessors/radio-group/radio-group.component.html new file mode 100644 index 0000000000..a511dbbbe5 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/value-accessors/radio-group/radio-group.component.html @@ -0,0 +1,14 @@ +
+

IonRadioGroup Value Accessors

+

+ This test checks the form integrations with ion-radio-group to make sure values are correctly assigned to the form + group. +

+ + + + One + Two + + +
diff --git a/packages/angular/test/base/src/app/standalone/value-accessors/radio-group/radio-group.component.ts b/packages/angular/test/base/src/app/standalone/value-accessors/radio-group/radio-group.component.ts new file mode 100644 index 0000000000..b4890240c5 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/value-accessors/radio-group/radio-group.component.ts @@ -0,0 +1,26 @@ +import { Component } from "@angular/core"; +import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from "@angular/forms"; +import { IonRadioGroup, IonRadio } from "@ionic/angular/standalone"; +import { ValueAccessorTestComponent } from "../value-accessor-test/value-accessor-test.component"; + +@Component({ + selector: 'app-radio-group', + templateUrl: 'radio-group.component.html', + standalone: true, + imports: [ + IonRadioGroup, + IonRadio, + ReactiveFormsModule, + FormsModule, + ValueAccessorTestComponent + ] +}) +export class RadioGroupComponent { + + form = this.fb.group({ + radioGroup: ['1', Validators.required], + }); + + constructor(private fb: FormBuilder) { } + +} diff --git a/packages/angular/test/base/src/app/standalone/value-accessors/range/range.component.html b/packages/angular/test/base/src/app/standalone/value-accessors/range/range.component.html new file mode 100644 index 0000000000..9a496fbcd3 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/value-accessors/range/range.component.html @@ -0,0 +1,9 @@ +
+

IonRange Value Accessors

+

+ This test checks the form integrations with ion-range to make sure values are correctly assigned to the form group. +

+ + + +
diff --git a/packages/angular/test/base/src/app/standalone/value-accessors/range/range.component.ts b/packages/angular/test/base/src/app/standalone/value-accessors/range/range.component.ts new file mode 100644 index 0000000000..f303826524 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/value-accessors/range/range.component.ts @@ -0,0 +1,25 @@ +import { Component } from "@angular/core"; +import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from "@angular/forms"; +import { IonRange } from "@ionic/angular/standalone"; +import { ValueAccessorTestComponent } from "../value-accessor-test/value-accessor-test.component"; + +@Component({ + selector: 'app-range', + templateUrl: 'range.component.html', + standalone: true, + imports: [ + IonRange, + ReactiveFormsModule, + FormsModule, + ValueAccessorTestComponent + ] +}) +export class RangeComponent { + + form = this.fb.group({ + range: [0, Validators.required], + }); + + constructor(private fb: FormBuilder) { } + +} diff --git a/packages/angular/test/base/src/app/standalone/value-accessors/searchbar/searchbar.component.html b/packages/angular/test/base/src/app/standalone/value-accessors/searchbar/searchbar.component.html new file mode 100644 index 0000000000..9a6137c65b --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/value-accessors/searchbar/searchbar.component.html @@ -0,0 +1,10 @@ +
+

IonSearchbar Value Accessors

+

+ This test checks the form integrations with ion-searchbar to make sure values are correctly assigned to the form + group. +

+ + + +
diff --git a/packages/angular/test/base/src/app/standalone/value-accessors/searchbar/searchbar.component.ts b/packages/angular/test/base/src/app/standalone/value-accessors/searchbar/searchbar.component.ts new file mode 100644 index 0000000000..6113ed5ddc --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/value-accessors/searchbar/searchbar.component.ts @@ -0,0 +1,25 @@ +import { Component } from "@angular/core"; +import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from "@angular/forms"; +import { IonSearchbar } from "@ionic/angular/standalone"; +import { ValueAccessorTestComponent } from "../value-accessor-test/value-accessor-test.component"; + +@Component({ + selector: 'app-searchbar', + templateUrl: 'searchbar.component.html', + standalone: true, + imports: [ + IonSearchbar, + ReactiveFormsModule, + FormsModule, + ValueAccessorTestComponent + ] +}) +export class SearchbarComponent { + + form = this.fb.group({ + searchbar: ['', Validators.required], + }); + + constructor(private fb: FormBuilder) { } + +} diff --git a/packages/angular/test/base/src/app/standalone/value-accessors/segment/segment.component.html b/packages/angular/test/base/src/app/standalone/value-accessors/segment/segment.component.html new file mode 100644 index 0000000000..98016fcde6 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/value-accessors/segment/segment.component.html @@ -0,0 +1,18 @@ +
+

IonSegment Value Accessors

+

+ This test checks the form integrations with ion-segment to make sure values are correctly assigned to the form + group. +

+ + + + + Paid + + + Free + + + +
diff --git a/packages/angular/test/base/src/app/standalone/value-accessors/segment/segment.component.ts b/packages/angular/test/base/src/app/standalone/value-accessors/segment/segment.component.ts new file mode 100644 index 0000000000..d3de204881 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/value-accessors/segment/segment.component.ts @@ -0,0 +1,27 @@ +import { Component } from "@angular/core"; +import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from "@angular/forms"; +import { IonSegment, IonSegmentButton, IonLabel } from "@ionic/angular/standalone"; +import { ValueAccessorTestComponent } from "../value-accessor-test/value-accessor-test.component"; + +@Component({ + selector: 'app-segment', + templateUrl: 'segment.component.html', + standalone: true, + imports: [ + IonSegment, + IonSegmentButton, + IonLabel, + ReactiveFormsModule, + FormsModule, + ValueAccessorTestComponent + ] +}) +export class SegmentComponent { + + form = this.fb.group({ + segment: ['Paid', Validators.required], + }); + + constructor(private fb: FormBuilder) { } + +} diff --git a/packages/angular/test/base/src/app/standalone/value-accessors/textarea/textarea.component.html b/packages/angular/test/base/src/app/standalone/value-accessors/textarea/textarea.component.html new file mode 100644 index 0000000000..678d42d391 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/value-accessors/textarea/textarea.component.html @@ -0,0 +1,11 @@ +
+

IonTextarea Value Accessors

+

+ This test checks the form integrations with ion-textarea to make sure values are correctly assigned to the form + group. +

+ + + + +
diff --git a/packages/angular/test/base/src/app/standalone/value-accessors/textarea/textarea.component.ts b/packages/angular/test/base/src/app/standalone/value-accessors/textarea/textarea.component.ts new file mode 100644 index 0000000000..9410c03dce --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/value-accessors/textarea/textarea.component.ts @@ -0,0 +1,25 @@ +import { Component } from "@angular/core"; +import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from "@angular/forms"; +import { IonTextarea } from "@ionic/angular/standalone"; +import { ValueAccessorTestComponent } from "../value-accessor-test/value-accessor-test.component"; + +@Component({ + selector: 'app-textarea', + templateUrl: 'textarea.component.html', + standalone: true, + imports: [ + IonTextarea, + ReactiveFormsModule, + FormsModule, + ValueAccessorTestComponent + ] +}) +export class TextareaComponent { + + form = this.fb.group({ + textarea: ['', Validators.required], + }); + + constructor(private fb: FormBuilder) { } + +} diff --git a/packages/angular/test/base/src/app/standalone/value-accessors/toggle/toggle.component.html b/packages/angular/test/base/src/app/standalone/value-accessors/toggle/toggle.component.html new file mode 100644 index 0000000000..08888059f9 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/value-accessors/toggle/toggle.component.html @@ -0,0 +1,10 @@ +
+

IonToggle Value Accessors

+

+ This test checks the form integrations with ion-toggle to make sure values are correctly assigned to the form group. +

+ + + + +
diff --git a/packages/angular/test/base/src/app/standalone/value-accessors/toggle/toggle.component.ts b/packages/angular/test/base/src/app/standalone/value-accessors/toggle/toggle.component.ts new file mode 100644 index 0000000000..3b72b4c16d --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/value-accessors/toggle/toggle.component.ts @@ -0,0 +1,25 @@ +import { Component } from "@angular/core"; +import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from "@angular/forms"; +import { IonToggle } from "@ionic/angular/standalone"; +import { ValueAccessorTestComponent } from "../value-accessor-test/value-accessor-test.component"; + +@Component({ + selector: 'app-toggle', + templateUrl: 'toggle.component.html', + standalone: true, + imports: [ + IonToggle, + ReactiveFormsModule, + FormsModule, + ValueAccessorTestComponent + ] +}) +export class ToggleComponent { + + form = this.fb.group({ + toggle: [false, Validators.required], + }); + + constructor(private fb: FormBuilder) { } + +} diff --git a/packages/angular/test/base/src/app/standalone/value-accessors/value-accessor-test/value-accessor-test.component.html b/packages/angular/test/base/src/app/standalone/value-accessors/value-accessor-test/value-accessor-test.component.html new file mode 100644 index 0000000000..754b47c799 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/value-accessors/value-accessor-test/value-accessor-test.component.html @@ -0,0 +1,16 @@ +
+
+ +
+ + + +

Form Value

+
{{ formGroup.value | json }}
+ +

Form Validity

+
    +
  • Form valid: {{ formGroup.valid }}
  • +
  • {{ control.key }} valid: {{ control.value.valid }}
  • +
+
diff --git a/packages/angular/test/base/src/app/standalone/value-accessors/value-accessor-test/value-accessor-test.component.ts b/packages/angular/test/base/src/app/standalone/value-accessors/value-accessor-test/value-accessor-test.component.ts new file mode 100644 index 0000000000..6796f64564 --- /dev/null +++ b/packages/angular/test/base/src/app/standalone/value-accessors/value-accessor-test/value-accessor-test.component.ts @@ -0,0 +1,27 @@ +import { CommonModule, JsonPipe, KeyValuePipe } from "@angular/common"; +import { Component, Input } from "@angular/core"; +import { FormGroup, FormsModule, ReactiveFormsModule } from "@angular/forms"; + +@Component({ + selector: 'app-value-accessor-test', + templateUrl: 'value-accessor-test.component.html', + standalone: true, + imports: [ + ReactiveFormsModule, + FormsModule, + JsonPipe, + KeyValuePipe, + /** + * NgFor directive is not available until Angular 15. + * We import the CommonModule for now. + * + * TODO: FW-5197 Replace with NgFor when dropping Angular 14 support. + */ + CommonModule + ] +}) +export class ValueAccessorTestComponent { + + @Input() formGroup!: FormGroup; + +}