feat(angular): standalone form controls can participate in forms (#28125)

Issue number: N/A

---------

<!-- Please do not submit updates to dependencies unless it fixes an
issue. -->

<!-- Please try to limit your pull request to one type (bugfix, feature,
etc). Submit multiple pull requests if needed. -->

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying. -->

Ionic standalone form controls cannot participate in Angular forms.

## What is the new behavior?
<!-- Please describe the behavior or changes that are being added by
this PR. -->

- Ionic form controls can participate in Angular forms by importing the
standalone component
- Applies to: `ion-input`, `ion-textarea`, `ion-searchbar`,
`ion-toggle`, `ion-checkbox`, `ion-segment`, `ion-radio`,
`ion-radio-group`, `ion-datetime` and `ion-range`.
- Refactors `ValueAccessor` from `@ionic/angular` to
`@ionic/angular/common`
- Refactors `raf` utility from `@ionic/angular` to
`@ionic/angular/common`

## Does this introduce a breaking change?

- [ ] Yes
- [x] No

<!-- If this introduces a breaking change, please describe the impact
and migration path for existing applications below. -->


## Other information

<!-- Any other information that is important to this PR such as
screenshots of how the component looks before and after the change. -->

---------
This commit is contained in:
Sean Perkins
2023-09-19 12:46:28 -04:00
committed by GitHub
parent dad7e66cf6
commit 28f2ec9c62
47 changed files with 1626 additions and 902 deletions

View File

@@ -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',
})

View File

@@ -0,0 +1 @@
export * from './value-accessor';

View File

@@ -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-')) {

View File

@@ -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';

View File

@@ -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) => {

View File

@@ -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'])

View File

@@ -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]',

View File

@@ -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);
}
}

View File

@@ -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 */

View File

@@ -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',

View File

@@ -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: '<ng-content></ng-content>',
// 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<CustomEvent<CheckboxChangeEventDetail>>;
/**
* Emitted when the checkbox has focus.
*/
ionFocus: EventEmitter<CustomEvent<void>>;
/**
* Emitted when the checkbox loses focus.
*/
ionBlur: EventEmitter<CustomEvent<void>>;
}

View File

@@ -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: '<ng-content></ng-content>',
// 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<CustomEvent<void>>;
/**
* Emitted when the value (selected date) has changed.
*/
ionChange: EventEmitter<CustomEvent<DatetimeChangeEventDetail>>;
/**
* Emitted when the datetime has focus.
*/
ionFocus: EventEmitter<CustomEvent<void>>;
/**
* Emitted when the datetime loses focus.
*/
ionBlur: EventEmitter<CustomEvent<void>>;
}

View File

@@ -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';

View File

@@ -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: '<ng-content></ng-content>',
// 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<CustomEvent<IIonInputInputInputEventDetail>>;
/**
* 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 `<ion-input type="date">`, 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<CustomEvent<IIonInputInputChangeEventDetail>>;
/**
* Emitted when the input loses focus.
*/
ionBlur: EventEmitter<CustomEvent<FocusEvent>>;
/**
* Emitted when the input has focus.
*/
ionFocus: EventEmitter<CustomEvent<FocusEvent>>;
}

View File

@@ -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: '<ng-content></ng-content>',
// 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<CustomEvent<IIonCheckboxCheckboxChangeEventDetail>>;
/**
* Emitted when the checkbox has focus.
*/
ionFocus: EventEmitter<CustomEvent<void>>;
/**
* Emitted when the checkbox loses focus.
*/
ionBlur: EventEmitter<CustomEvent<void>>;
}
@ProxyCmp({
defineCustomElementFn: defineIonChip,
inputs: ['color', 'disabled', 'mode', 'outline'],
@@ -947,111 +873,6 @@ Set `scrollEvents` to `true` to enable.
ionScrollEnd: EventEmitter<CustomEvent<IIonContentScrollBaseDetail>>;
}
@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: '<ng-content></ng-content>',
// 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<CustomEvent<void>>;
/**
* Emitted when the value (selected date) has changed.
*/
ionChange: EventEmitter<CustomEvent<IIonDatetimeDatetimeChangeEventDetail>>;
/**
* Emitted when the datetime has focus.
*/
ionFocus: EventEmitter<CustomEvent<void>>;
/**
* Emitted when the datetime loses focus.
*/
ionBlur: EventEmitter<CustomEvent<void>>;
}
@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: '<ng-content></ng-content>',
// 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<CustomEvent<IIonInputInputInputEventDetail>>;
/**
* 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 `<ion-input type="date">`, 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<CustomEvent<IIonInputInputChangeEventDetail>>;
/**
* Emitted when the input loses focus.
*/
ionBlur: EventEmitter<CustomEvent<FocusEvent>>;
/**
* Emitted when the input has focus.
*/
ionFocus: EventEmitter<CustomEvent<FocusEvent>>;
}
@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: '<ng-content></ng-content>',
// 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<CustomEvent<void>>;
/**
* Emitted when the radio button loses focus.
*/
ionBlur: EventEmitter<CustomEvent<void>>;
}
@ProxyCmp({
defineCustomElementFn: defineIonRadioGroup,
inputs: ['allowEmptySelection', 'name', 'value'],
})
@Component({
selector: 'ion-radio-group',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// 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<CustomEvent<IIonRadioGroupRadioGroupChangeEventDetail>>;
}
@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: '<ng-content></ng-content>',
// 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 `<ion-range>` 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<CustomEvent<IIonRangeRangeChangeEventDetail>>;
/**
* The `ionInput` event is fired for `<ion-range>` elements when the value
is modified. Unlike `ionChange`, `ionInput` is fired continuously
while the user is dragging the knob.
*/
ionInput: EventEmitter<CustomEvent<IIonRangeRangeChangeEventDetail>>;
/**
* Emitted when the range has focus.
*/
ionFocus: EventEmitter<CustomEvent<void>>;
/**
* Emitted when the range loses focus.
*/
ionBlur: EventEmitter<CustomEvent<void>>;
/**
* Emitted when the user starts moving the range knob, whether through
mouse drag, touch gesture, or keyboard interaction.
*/
ionKnobMoveStart: EventEmitter<CustomEvent<IIonRangeRangeKnobMoveStartEventDetail>>;
/**
* Emitted when the user finishes moving the range knob, whether through
mouse drag, touch gesture, or keyboard interaction.
*/
ionKnobMoveEnd: EventEmitter<CustomEvent<IIonRangeRangeKnobMoveEndEventDetail>>;
}
@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: '<ng-content></ng-content>',
// 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<CustomEvent<IIonSearchbarSearchbarInputEventDetail>>;
/**
* The `ionChange` event is fired for `<ion-searchbar>` 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<CustomEvent<IIonSearchbarSearchbarChangeEventDetail>>;
/**
* Emitted when the cancel button is clicked.
*/
ionCancel: EventEmitter<CustomEvent<void>>;
/**
* Emitted when the clear input button is clicked.
*/
ionClear: EventEmitter<CustomEvent<void>>;
/**
* Emitted when the input loses focus.
*/
ionBlur: EventEmitter<CustomEvent<void>>;
/**
* Emitted when the input has focus.
*/
ionFocus: EventEmitter<CustomEvent<void>>;
}
@ProxyCmp({
defineCustomElementFn: defineIonSegment,
inputs: ['color', 'disabled', 'mode', 'scrollable', 'selectOnFocus', 'swipeGesture', 'value'],
})
@Component({
selector: 'ion-segment',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// 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<CustomEvent<IIonSegmentSegmentChangeEventDetail>>;
}
@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: '<ng-content></ng-content>',
// 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<CustomEvent<IIonSelectSelectChangeEventDetail>>;
/**
* Emitted when the selection is cancelled.
*/
ionCancel: EventEmitter<CustomEvent<void>>;
/**
* Emitted when the overlay is dismissed.
*/
ionDismiss: EventEmitter<CustomEvent<void>>;
/**
* Emitted when the select has focus.
*/
ionFocus: EventEmitter<CustomEvent<void>>;
/**
* Emitted when the select loses focus.
*/
ionBlur: EventEmitter<CustomEvent<void>>;
}
@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: '<ng-content></ng-content>',
// 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<CustomEvent<IIonTextareaTextareaChangeEventDetail>>;
/**
* 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<CustomEvent<IIonTextareaTextareaInputEventDetail>>;
/**
* Emitted when the input loses focus.
*/
ionBlur: EventEmitter<CustomEvent<FocusEvent>>;
/**
* Emitted when the input has focus.
*/
ionFocus: EventEmitter<CustomEvent<FocusEvent>>;
}
@ProxyCmp({
defineCustomElementFn: defineIonThumbnail,
})
@@ -3075,67 +2263,6 @@ Shorthand for ionToastDidDismiss.
didDismiss: EventEmitter<CustomEvent<IIonToastOverlayEventDetail>>;
}
@ProxyCmp({
defineCustomElementFn: defineIonToggle,
inputs: [
'checked',
'color',
'disabled',
'enableOnOffLabels',
'justify',
'labelPlacement',
'legacy',
'mode',
'name',
'value',
],
})
@Component({
selector: 'ion-toggle',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// 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<CustomEvent<IIonToggleToggleChangeEventDetail>>;
/**
* Emitted when the toggle has focus.
*/
ionFocus: EventEmitter<CustomEvent<void>>;
/**
* Emitted when the toggle loses focus.
*/
ionBlur: EventEmitter<CustomEvent<void>>;
}
@ProxyCmp({
defineCustomElementFn: defineIonToolbar,
inputs: ['color', 'mode'],

View File

@@ -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: '<ng-content></ng-content>',
// 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<CustomEvent<RadioGroupChangeEventDetail>>;
}

View File

@@ -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: '<ng-content></ng-content>',
// 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<CustomEvent<void>>;
/**
* Emitted when the radio button loses focus.
*/
ionBlur: EventEmitter<CustomEvent<void>>;
}

View File

@@ -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: '<ng-content></ng-content>',
// 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 `<ion-range>` 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<CustomEvent<RangeChangeEventDetail>>;
/**
* The `ionInput` event is fired for `<ion-range>` elements when the value
is modified. Unlike `ionChange`, `ionInput` is fired continuously
while the user is dragging the knob.
*/
ionInput: EventEmitter<CustomEvent<RangeChangeEventDetail>>;
/**
* Emitted when the range has focus.
*/
ionFocus: EventEmitter<CustomEvent<void>>;
/**
* Emitted when the range loses focus.
*/
ionBlur: EventEmitter<CustomEvent<void>>;
/**
* Emitted when the user starts moving the range knob, whether through
mouse drag, touch gesture, or keyboard interaction.
*/
ionKnobMoveStart: EventEmitter<CustomEvent<RangeKnobMoveStartEventDetail>>;
/**
* Emitted when the user finishes moving the range knob, whether through
mouse drag, touch gesture, or keyboard interaction.
*/
ionKnobMoveEnd: EventEmitter<CustomEvent<RangeKnobMoveEndEventDetail>>;
}

View File

@@ -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: '<ng-content></ng-content>',
// 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<CustomEvent<SearchbarInputEventDetail>>;
/**
* The `ionChange` event is fired for `<ion-searchbar>` 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<CustomEvent<SearchbarChangeEventDetail>>;
/**
* Emitted when the cancel button is clicked.
*/
ionCancel: EventEmitter<CustomEvent<void>>;
/**
* Emitted when the clear input button is clicked.
*/
ionClear: EventEmitter<CustomEvent<void>>;
/**
* Emitted when the input loses focus.
*/
ionBlur: EventEmitter<CustomEvent<void>>;
/**
* Emitted when the input has focus.
*/
ionFocus: EventEmitter<CustomEvent<void>>;
}

View File

@@ -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: '<ng-content></ng-content>',
// 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<CustomEvent<SegmentChangeEventDetail>>;
}

View File

@@ -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: '<ng-content></ng-content>',
// 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<CustomEvent<SelectChangeEventDetail>>;
/**
* Emitted when the selection is cancelled.
*/
ionCancel: EventEmitter<CustomEvent<void>>;
/**
* Emitted when the overlay is dismissed.
*/
ionDismiss: EventEmitter<CustomEvent<void>>;
/**
* Emitted when the select has focus.
*/
ionFocus: EventEmitter<CustomEvent<void>>;
/**
* Emitted when the select loses focus.
*/
ionBlur: EventEmitter<CustomEvent<void>>;
}

View File

@@ -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: '<ng-content></ng-content>',
// 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<CustomEvent<TextareaChangeEventDetail>>;
/**
* 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<CustomEvent<TextareaInputEventDetail>>;
/**
* Emitted when the input loses focus.
*/
ionBlur: EventEmitter<CustomEvent<FocusEvent>>;
/**
* Emitted when the input has focus.
*/
ionFocus: EventEmitter<CustomEvent<FocusEvent>>;
}

View File

@@ -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: '<ng-content></ng-content>',
// 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<CustomEvent<ToggleChangeEventDetail>>;
/**
* Emitted when the toggle has focus.
*/
ionFocus: EventEmitter<CustomEvent<void>>;
/**
* Emitted when the toggle loses focus.
*/
ionBlur: EventEmitter<CustomEvent<void>>;
}

View File

@@ -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 {

View File

@@ -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');
});
});
});

View File

@@ -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' }
]
}
]
},
];

View File

@@ -0,0 +1,11 @@
<div>
<h1>IonCheckbox Value Accessors</h1>
<p>
This test checks the form integrations with ion-checkbox to make sure values are correctly assigned to the form
group.
</p>
<app-value-accessor-test [formGroup]="form">
<ion-checkbox formControlName="checkbox"></ion-checkbox>
</app-value-accessor-test>
</div>

View File

@@ -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) { }
}

View File

@@ -0,0 +1,11 @@
<div>
<h1>IonDatetime Value Accessors</h1>
<p>
This test checks the form integrations with ion-datetime to make sure values are correctly assigned to the form
group.
</p>
<app-value-accessor-test [formGroup]="form">
<ion-datetime formControlName="datetime"></ion-datetime>
</app-value-accessor-test>
</div>

View File

@@ -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) { }
}

View File

@@ -0,0 +1,11 @@
<div>
<h1>IonInput Value Accessors</h1>
<p>
This test checks the form integrations with ion-input to make sure values are correctly assigned to the form group.
</p>
<app-value-accessor-test [formGroup]="form">
<ion-input label="String" formControlName="inputString"></ion-input>
<ion-input label="Number" type="number" formControlName="inputNumber"></ion-input>
</app-value-accessor-test>
</div>

View File

@@ -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) { }
}

View File

@@ -0,0 +1,14 @@
<div>
<h1>IonRadioGroup Value Accessors</h1>
<p>
This test checks the form integrations with ion-radio-group to make sure values are correctly assigned to the form
group.
</p>
<app-value-accessor-test [formGroup]="form">
<ion-radio-group formControlName="radioGroup">
<ion-radio value="1">One</ion-radio>
<ion-radio value="2">Two</ion-radio>
</ion-radio-group>
</app-value-accessor-test>
</div>

View File

@@ -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) { }
}

View File

@@ -0,0 +1,9 @@
<div>
<h1>IonRange Value Accessors</h1>
<p>
This test checks the form integrations with ion-range to make sure values are correctly assigned to the form group.
</p>
<app-value-accessor-test [formGroup]="form">
<ion-range formControlName="range"></ion-range>
</app-value-accessor-test>
</div>

View File

@@ -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) { }
}

View File

@@ -0,0 +1,10 @@
<div>
<h1>IonSearchbar Value Accessors</h1>
<p>
This test checks the form integrations with ion-searchbar to make sure values are correctly assigned to the form
group.
</p>
<app-value-accessor-test [formGroup]="form">
<ion-searchbar label="String" formControlName="searchbar"></ion-searchbar>
</app-value-accessor-test>
</div>

View File

@@ -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) { }
}

View File

@@ -0,0 +1,18 @@
<div>
<h1>IonSegment Value Accessors</h1>
<p>
This test checks the form integrations with ion-segment to make sure values are correctly assigned to the form
group.
</p>
<app-value-accessor-test [formGroup]="form">
<ion-segment formControlName="segment">
<ion-segment-button value="Paid">
<ion-label>Paid</ion-label>
</ion-segment-button>
<ion-segment-button value="Free">
<ion-label>Free</ion-label>
</ion-segment-button>
</ion-segment>
</app-value-accessor-test>
</div>

View File

@@ -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) { }
}

View File

@@ -0,0 +1,11 @@
<div>
<h1>IonTextarea Value Accessors</h1>
<p>
This test checks the form integrations with ion-textarea to make sure values are correctly assigned to the form
group.
</p>
<app-value-accessor-test [formGroup]="form">
<ion-textarea label="String" formControlName="textarea"></ion-textarea>
</app-value-accessor-test>
</div>

View File

@@ -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) { }
}

View File

@@ -0,0 +1,10 @@
<div>
<h1>IonToggle Value Accessors</h1>
<p>
This test checks the form integrations with ion-toggle to make sure values are correctly assigned to the form group.
</p>
<app-value-accessor-test [formGroup]="form">
<ion-toggle formControlName="toggle"></ion-toggle>
</app-value-accessor-test>
</div>

View File

@@ -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) { }
}

View File

@@ -0,0 +1,16 @@
<form [formGroup]="formGroup">
<div>
<ng-content></ng-content>
</div>
<input type="submit" value="Submit" [disabled]="!formGroup.valid" />
<h3>Form Value</h3>
<pre id="formValue">{{ formGroup.value | json }}</pre>
<h3>Form Validity</h3>
<ul id="formValidity">
<li>Form valid: {{ formGroup.valid }}</li>
<li *ngFor="let control of formGroup.controls | keyvalue">{{ control.key }} valid: {{ control.value.valid }}</li>
</ul>
</form>

View File

@@ -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;
}