feat(angular): ship Ionic components as Angular standalone components (#28311)

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. -->

**1. Bundle Size Reductions**

All Ionic UI components and Ionicons are added to the final bundle of an
Ionic Angular application. This is because all components and icons are
lazily loaded as needed. This prevents the compiler from properly tree
shaking applications. This does not cause all components and icons to be
loaded on application start, but it does increase the size of the final
app output that all users need to download.

**Related Issues**

https://github.com/ionic-team/ionicons/issues/910

https://github.com/ionic-team/ionicons/issues/536

https://github.com/ionic-team/ionic-framework/issues/27280

https://github.com/ionic-team/ionic-framework/issues/24352

**2. Standalone Component Support**

Standalone Components are a stable API as of Angular 15. The Ionic
starter apps on the CLI have NgModule and Standalone options, but all of
the Ionic components are still lazily/dynamically loaded using
`IonicModule`. Standalone components in Ionic also enable support for
new Angular features such as bundling with ESBuild instead of Webpack.
ESBuild does not work in Ionic Angular right now because components
cannot be statically analyzed since they are dynamically imported.

We added preliminary support for standalone components in Ionic v6.3.0.
This enabled developers to use their own custom standalone components
when routing with `ion-router-outlet`. However, we did not ship
standalone components for Ionic's UI components.

**Related Issues**

https://github.com/ionic-team/ionic-framework/issues/25404

https://github.com/ionic-team/ionic-framework/issues/27251

https://github.com/ionic-team/ionic-framework/issues/27387

**3. Faster Component Load Times**

Since Ionic Angular components are lazily loaded, they also need to be
hydrated. However, this hydration does not happen immediately which
prevents components from being usable for multiple frames.

**Related Issues**

https://github.com/ionic-team/ionic-framework/issues/24352

https://github.com/ionic-team/ionic-framework/issues/26474

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

- Ionic components and directives are accessible as Angular standalone
components/directives

## 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. -->

Associated documentation branch:
https://github.com/ionic-team/ionic-docs/tree/feature-7.5

---------

Co-authored-by: Maria Hutt <thetaPC@users.noreply.github.com>
Co-authored-by: Sean Perkins <sean@ionic.io>
Co-authored-by: Amanda Johnston <90629384+amandaejohnston@users.noreply.github.com>
Co-authored-by: Maria Hutt <maria@ionic.io>
Co-authored-by: Sean Perkins <13732623+sean-perkins@users.noreply.github.com>
This commit is contained in:
Liam DeBeasi
2023-10-10 13:06:23 -04:00
committed by GitHub
parent 4f43d5ce08
commit 57e2476370
302 changed files with 6918 additions and 1624 deletions

View File

@@ -0,0 +1,57 @@
/* eslint-disable */
/* tslint:disable */
import { fromEvent } from 'rxjs';
export const proxyInputs = (Cmp: any, inputs: string[]) => {
const Prototype = Cmp.prototype;
inputs.forEach((item) => {
Object.defineProperty(Prototype, item, {
get() {
return this.el[item];
},
set(val: any) {
this.z.runOutsideAngular(() => (this.el[item] = val));
},
});
});
};
export const proxyMethods = (Cmp: any, methods: string[]) => {
const Prototype = Cmp.prototype;
methods.forEach((methodName) => {
Prototype[methodName] = function () {
const args = arguments;
return this.z.runOutsideAngular(() => this.el[methodName].apply(this.el, args));
};
});
};
export const proxyOutputs = (instance: any, el: any, events: string[]) => {
events.forEach((eventName) => (instance[eventName] = fromEvent(el, eventName)));
};
export const defineCustomElement = (tagName: string, customElement: any) => {
if (customElement !== undefined && typeof customElements !== 'undefined' && !customElements.get(tagName)) {
customElements.define(tagName, customElement);
}
};
// tslint:disable-next-line: only-arrow-functions
export function ProxyCmp(opts: { defineCustomElementFn?: () => void; inputs?: any; methods?: any }) {
const decorator = function (cls: any) {
const { defineCustomElementFn, inputs, methods } = opts;
if (defineCustomElementFn !== undefined) {
defineCustomElementFn();
}
if (inputs) {
proxyInputs(cls, inputs);
}
if (methods) {
proxyMethods(cls, methods);
}
return cls;
};
return decorator;
}

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,24 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, NgZone } from '@angular/core';
import { defineCustomElement as defineIonIcon } from 'ionicons/components/ion-icon.js';
import { ProxyCmp } from './angular-component-lib/utils';
@ProxyCmp({
defineCustomElementFn: defineIonIcon,
inputs: ['color', 'flipRtl', 'icon', 'ios', 'lazy', 'md', 'mode', 'name', 'sanitize', 'size', 'src'],
})
@Component({
selector: 'ion-icon',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['color', 'flipRtl', 'icon', 'ios', 'lazy', 'md', 'mode', 'name', 'sanitize', 'size', 'src'],
standalone: true,
})
export class IonIcon {
protected el: HTMLElement;
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
c.detach();
this.el = r.nativeElement;
}
}

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

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,127 @@
export { IonBackButton } from './navigation/back-button';
export { IonModal } from './overlays/modal';
export { IonPopover } from './overlays/popover';
export { IonRouterOutlet } from './navigation/router-outlet';
export { IonRouterLink, IonRouterLinkWithHref } from './navigation/router-link-delegate';
export { IonTabs } from './navigation/tabs';
export { provideIonicAngular } from './providers/ionic-angular';
export {
ActionSheetController,
AlertController,
LoadingController,
MenuController,
ModalController,
PickerController,
PopoverController,
ToastController,
AnimationController,
GestureController,
DomController,
NavController,
Config,
Platform,
NavParams,
IonicRouteStrategy,
} from '@ionic/angular/common';
export { IonNav } from './navigation/nav';
export {
IonCheckbox,
IonDatetime,
IonInput,
IonIcon,
IonRadioGroup,
IonRadio,
IonRange,
IonSearchbar,
IonSegment,
IonSelect,
IonTextarea,
IonToggle,
} from './directives';
export * from './directives/proxies';
export {
// UTILS
createAnimation,
createGesture,
iosTransitionAnimation,
mdTransitionAnimation,
IonicSlides,
getPlatforms,
isPlatform,
getTimeGivenProgression,
// TYPES
Animation,
AnimationBuilder,
AnimationCallbackOptions,
AnimationDirection,
AnimationFill,
AnimationKeyFrames,
AnimationLifecycle,
Gesture,
GestureConfig,
GestureDetail,
NavComponentWithProps,
SpinnerTypes,
AccordionGroupCustomEvent,
AccordionGroupChangeEventDetail,
BreadcrumbCustomEvent,
BreadcrumbCollapsedClickEventDetail,
ActionSheetOptions,
ActionSheetButton,
AlertOptions,
AlertInput,
AlertButton,
BackButtonEvent,
CheckboxCustomEvent,
CheckboxChangeEventDetail,
DatetimeCustomEvent,
DatetimeChangeEventDetail,
InfiniteScrollCustomEvent,
InputCustomEvent,
InputChangeEventDetail,
ItemReorderEventDetail,
ItemReorderCustomEvent,
ItemSlidingCustomEvent,
IonicSafeString,
LoadingOptions,
MenuCustomEvent,
ModalOptions,
NavCustomEvent,
PickerOptions,
PickerButton,
PickerColumn,
PickerColumnOption,
PlatformConfig,
PopoverOptions,
RadioGroupCustomEvent,
RadioGroupChangeEventDetail,
RangeCustomEvent,
RangeChangeEventDetail,
RangeKnobMoveStartEventDetail,
RangeKnobMoveEndEventDetail,
RefresherCustomEvent,
RefresherEventDetail,
RouterEventDetail,
RouterCustomEvent,
ScrollBaseCustomEvent,
ScrollBaseDetail,
ScrollDetail,
ScrollCustomEvent,
SearchbarCustomEvent,
SearchbarChangeEventDetail,
SearchbarInputEventDetail,
SegmentChangeEventDetail,
SegmentCustomEvent,
SegmentValue,
SelectChangeEventDetail,
SelectCustomEvent,
TabsCustomEvent,
TextareaChangeEventDetail,
TextareaCustomEvent,
ToastOptions,
ToastButton,
ToastLayout,
ToggleChangeEventDetail,
ToggleCustomEvent,
} from '@ionic/core/components';

View File

@@ -0,0 +1,28 @@
import { Component, Optional, ChangeDetectionStrategy, ElementRef, NgZone, ChangeDetectorRef } from '@angular/core';
import { IonBackButton as IonBackButtonBase, NavController, Config, ProxyCmp } from '@ionic/angular/common';
import { defineCustomElement } from '@ionic/core/components/ion-back-button.js';
import { IonRouterOutlet } from './router-outlet';
@ProxyCmp({
defineCustomElementFn: defineCustomElement,
})
@Component({
selector: 'ion-back-button',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
standalone: true,
})
// eslint-disable-next-line @angular-eslint/directive-class-suffix
export class IonBackButton extends IonBackButtonBase {
constructor(
@Optional() routerOutlet: IonRouterOutlet,
navCtrl: NavController,
config: Config,
r: ElementRef,
z: NgZone,
c: ChangeDetectorRef
) {
super(routerOutlet, navCtrl, config, r, z, c);
}
}

View File

@@ -0,0 +1,24 @@
import { Component, ElementRef, Injector, EnvironmentInjector, NgZone, ChangeDetectorRef } from '@angular/core';
import { IonNav as IonNavBase, ProxyCmp, AngularDelegate } from '@ionic/angular/common';
import { defineCustomElement } from '@ionic/core/components/ion-nav.js';
@ProxyCmp({
defineCustomElementFn: defineCustomElement,
})
@Component({
selector: 'ion-nav',
template: '<ng-content></ng-content>',
standalone: true,
})
export class IonNav extends IonNavBase {
constructor(
ref: ElementRef,
environmentInjector: EnvironmentInjector,
injector: Injector,
angularDelegate: AngularDelegate,
z: NgZone,
c: ChangeDetectorRef
) {
super(ref, environmentInjector, injector, angularDelegate, z, c);
}
}

View File

@@ -0,0 +1,19 @@
import { Directive } from '@angular/core';
import {
RouterLinkDelegateDirective as RouterLinkDelegateBase,
RouterLinkWithHrefDelegateDirective as RouterLinkHrefDelegateBase,
} from '@ionic/angular/common';
@Directive({
selector: ':not(a):not(area)[routerLink]',
standalone: true,
})
// eslint-disable-next-line @angular-eslint/directive-class-suffix
export class IonRouterLink extends RouterLinkDelegateBase {}
@Directive({
selector: 'a[routerLink],area[routerLink]',
standalone: true,
})
// eslint-disable-next-line @angular-eslint/directive-class-suffix
export class IonRouterLinkWithHref extends RouterLinkHrefDelegateBase {}

View File

@@ -0,0 +1,13 @@
import { Directive } from '@angular/core';
import { IonRouterOutlet as IonRouterOutletBase, ProxyCmp } from '@ionic/angular/common';
import { defineCustomElement } from '@ionic/core/components/ion-router-outlet.js';
@ProxyCmp({
defineCustomElementFn: defineCustomElement,
})
@Directive({
selector: 'ion-router-outlet',
standalone: true,
})
// eslint-disable-next-line @angular-eslint/directive-class-suffix
export class IonRouterOutlet extends IonRouterOutletBase {}

View File

@@ -0,0 +1,57 @@
import { Component, ContentChild, ContentChildren, ViewChild, QueryList } from '@angular/core';
import { IonTabs as IonTabsBase } from '@ionic/angular/common';
import { IonTabBar } from '../directives/proxies';
import { IonRouterOutlet } from './router-outlet';
@Component({
selector: 'ion-tabs',
template: `
<ng-content select="[slot=top]"></ng-content>
<div class="tabs-inner" #tabsInner>
<ion-router-outlet
#outlet
tabs="true"
(stackWillChange)="onStackWillChange($event)"
(stackDidChange)="onStackDidChange($event)"
></ion-router-outlet>
</div>
<ng-content></ng-content>
`,
standalone: true,
styles: [
`
:host {
display: flex;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
flex-direction: column;
width: 100%;
height: 100%;
contain: layout size style;
}
.tabs-inner {
position: relative;
flex: 1;
contain: layout size style;
}
`,
],
imports: [IonRouterOutlet],
})
// eslint-disable-next-line @angular-eslint/component-class-suffix
export class IonTabs extends IonTabsBase {
@ViewChild('outlet', { read: IonRouterOutlet, static: false }) outlet: IonRouterOutlet;
@ContentChild(IonTabBar, { static: false }) tabBar: IonTabBar | undefined;
@ContentChildren(IonTabBar) tabBars: QueryList<IonTabBar>;
}

View File

@@ -0,0 +1,18 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { IonModal as IonModalBase, ProxyCmp } from '@ionic/angular/common';
import { defineCustomElement } from '@ionic/core/components/ion-modal.js';
@ProxyCmp({
defineCustomElementFn: defineCustomElement,
})
@Component({
selector: 'ion-modal',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<div class="ion-delegate-host ion-page" *ngIf="isCmpOpen || keepContentsMounted">
<ng-container [ngTemplateOutlet]="template"></ng-container>
</div>`,
standalone: true,
imports: [CommonModule],
})
export class IonModal extends IonModalBase {}

View File

@@ -0,0 +1,16 @@
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { IonPopover as IonPopoverBase, ProxyCmp } from '@ionic/angular/common';
import { defineCustomElement } from '@ionic/core/components/ion-popover.js';
@ProxyCmp({
defineCustomElementFn: defineCustomElement,
})
@Component({
selector: 'ion-popover',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<ng-container [ngTemplateOutlet]="template" *ngIf="isCmpOpen || keepContentsMounted"></ng-container>`,
standalone: true,
imports: [CommonModule],
})
export class IonPopover extends IonPopoverBase {}

View File

@@ -0,0 +1,51 @@
import { DOCUMENT } from '@angular/common';
import { APP_INITIALIZER } from '@angular/core';
import type { Provider } from '@angular/core';
import {
AngularDelegate,
ConfigToken,
ModalController,
PopoverController,
provideComponentInputBinding,
} from '@ionic/angular/common';
import { initialize } from '@ionic/core/components';
import type { IonicConfig } from '@ionic/core/components';
export const provideIonicAngular = (config?: IonicConfig): Provider[] => {
/**
* TODO FW-4967
* Use makeEnvironmentProviders once Angular 14 support is dropped.
* This prevents provideIonicAngular from being accidentally referenced in an @Component.
*/
return [
{
provide: ConfigToken,
useValue: config,
},
{
provide: APP_INITIALIZER,
useFactory: initializeIonicAngular,
multi: true,
deps: [ConfigToken, DOCUMENT],
},
provideComponentInputBinding(),
AngularDelegate,
ModalController,
PopoverController,
];
};
const initializeIonicAngular = (config: IonicConfig, doc: Document) => {
return () => {
/**
* By default Ionic Framework hides elements that
* are not hydrated, but in the CE build there is no
* hydration.
* TODO FW-2797: Remove when all integrations have been
* migrated to CE build.
*/
doc.documentElement.classList.add('ion-ce');
initialize(config);
};
};