Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 36 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
4553425
chore: add infrastructure for standalone (#27866)
liamdebeasi Jul 27, 2023
af68808
test(angular): update test app to account for standalone components (…
liamdebeasi Jul 28, 2023
291b131
chore: move lazy and standalone tests to subdirectories (#27881)
liamdebeasi Aug 1, 2023
5b31439
refactor(angular): move providers to common library (#27899)
liamdebeasi Aug 1, 2023
de48493
refactor(angular): move remaining directives to common module (#27909)
liamdebeasi Aug 4, 2023
3ea7488
feat(angular): add standalone router outlet (#27926)
liamdebeasi Aug 4, 2023
9f20780
feat(angular): add standalone back-button (#27927)
liamdebeasi Aug 4, 2023
37acdf9
feat(angular): add standalone modal (#27885)
liamdebeasi Aug 4, 2023
8a97e40
feat(angular): add standalone popover (#27883)
liamdebeasi Aug 4, 2023
c52a097
feat(angular): add standalone nav (#27876)
thetaPC Aug 4, 2023
e44a026
feat(angular): add standalone router-link (#27937)
thetaPC Aug 8, 2023
104b954
chore: typescript resolves @ionic/angular/common import paths (#27995)
sean-perkins Aug 15, 2023
84212ac
refactor(angular): use type for imports used as types (#27998)
liamdebeasi Aug 15, 2023
c65e08d
feat(angular): add standalone providers, route strategy, component bi…
liamdebeasi Aug 15, 2023
ca53682
feat(angular): add standalone provideIonicAngular (#27996)
liamdebeasi Aug 15, 2023
e57759f
chore(angular): generate standalone component wrappers (#27970)
sean-perkins Aug 18, 2023
e5b7f5f
feat(angular): add enhanced icon support with addIcons (#28009)
liamdebeasi Aug 22, 2023
0afa14e
feat(angular): add standalone tabs (#28093)
averyjohnston Sep 1, 2023
fd0f25a
Merge remote-tracking branch 'origin/main' into FW-4612-sync
thetaPC Sep 6, 2023
01c3473
chore(angular): add missing code from bbfb8f8
thetaPC Sep 6, 2023
3720ae6
chore(angular): remove unused type
thetaPC Sep 7, 2023
28deb56
chore: sync with main
liamdebeasi Sep 7, 2023
dad7e66
fix(angular): include core exports in standalone (#28158)
liamdebeasi Sep 12, 2023
28f2ec9
feat(angular): standalone form controls can participate in forms (#28…
sean-perkins Sep 19, 2023
b2562e7
Merge remote-tracking branch 'origin/main' into sp/sync-FW-4612-with-…
sean-perkins Sep 29, 2023
14eb906
Merge remote-tracking branch 'origin/main' into sp/sync-FW-4612-with-…
sean-perkins Oct 3, 2023
3f03299
chore: sync with main
liamdebeasi Oct 4, 2023
a6e4fc4
fix(angular): routerLink directives can be used (#28282)
liamdebeasi Oct 4, 2023
cfbc6e9
chore: sync with feature-7.5
liamdebeasi Oct 5, 2023
fac5a97
chore: sync with feature-7.5
liamdebeasi Oct 5, 2023
be8e5ba
Merge branch 'feature-7.5' into FW-4612
liamdebeasi Oct 9, 2023
be97434
Merge remote-tracking branch 'origin/feature-7.5' into FW-4612
liamdebeasi Oct 9, 2023
dcbbc36
Merge branch 'feature-7.5' into FW-4612
liamdebeasi Oct 9, 2023
b7273c4
chore: use production build of ionicons in test app
liamdebeasi Oct 10, 2023
4c87a06
Merge branch 'feature-7.5' into FW-4612
liamdebeasi Oct 10, 2023
ccd503d
Merge branch 'feature-7.5' into FW-4612
liamdebeasi Oct 10, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
94 changes: 68 additions & 26 deletions core/stencil.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,71 @@ import { vueOutputTarget } from '@stencil/vue-output-target';
// @ts-ignore
import { apiSpecGenerator } from './scripts/api-spec-generator';

const componentCorePackage = '@ionic/core';

const getAngularOutputTargets = () => {
const excludeComponents = [
// overlays that accept user components
'ion-modal',
'ion-popover',

// navigation
'ion-router',
'ion-route',
'ion-route-redirect',
'ion-router-link',
'ion-router-outlet',
'ion-nav',
'ion-back-button',

// tabs
'ion-tabs',
'ion-tab',

// auxiliar
'ion-picker-column',
]
return [
angularOutputTarget({
componentCorePackage,
directivesProxyFile: '../packages/angular/src/directives/proxies.ts',
directivesArrayFile: '../packages/angular/src/directives/proxies-list.ts',
excludeComponents,
outputType: 'component',
}),
angularOutputTarget({
componentCorePackage,
directivesProxyFile: '../packages/angular/standalone/src/directives/proxies.ts',
excludeComponents: [
...excludeComponents,
/**
* IonIcon is a special case because it does not come
* from the `@ionic/core` package, so generating proxies that
* are reliant on the CE build will reference the wrong
* import location.
*/
'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',
})
];
}

export const config: Config = {
autoprefixCss: true,
sourceMap: false,
Expand Down Expand Up @@ -61,7 +126,7 @@ export const config: Config = {
],
outputTargets: [
reactOutputTarget({
componentCorePackage: '@ionic/core',
componentCorePackage,
includeImportCustomElements: true,
includePolyfills: false,
includeDefineCustomElements: false,
Expand Down Expand Up @@ -98,7 +163,7 @@ export const config: Config = {
]
}),
vueOutputTarget({
componentCorePackage: '@ionic/core',
componentCorePackage,
includeImportCustomElements: true,
includePolyfills: false,
includeDefineCustomElements: false,
Expand Down Expand Up @@ -182,30 +247,7 @@ export const config: Config = {
// type: 'stats',
// file: 'stats.json'
// },
angularOutputTarget({
componentCorePackage: '@ionic/core',
directivesProxyFile: '../packages/angular/src/directives/proxies.ts',
directivesArrayFile: '../packages/angular/src/directives/proxies-list.ts',
excludeComponents: [
// overlays that accept user components
'ion-modal',
'ion-popover',

// navigation
'ion-router',
'ion-route',
'ion-route-redirect',
'ion-router-link',
'ion-router-outlet',

// tabs
'ion-tabs',
'ion-tab',

// auxiliar
'ion-picker-column',
],
}),
...getAngularOutputTargets(),
],
buildEs5: 'prod',
testing: {
Expand Down
20 changes: 20 additions & 0 deletions packages/angular/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,23 @@ $ npx schematics @ionic/angular:ng-add


You'll now be able to add ionic components to a vanilla Angular app setup.

## Project Structure

**common**

This is where logic that is shared between lazy loaded and standalone components live. For example, the lazy loaded IonPopover and standalone IonPopover components extend from a base IonPopover implementation that exists in this directory.

**Note:** This directory exposes internal APIs and is only accessed in the `standalone` and `src` submodules. Ionic developers should never import directly from `@ionic/angular/common`. Instead, they should import from `@ionic/angular` or `@ionic/angular/standalone`.

**standalone**

This is where the standalone component implementations live. It was added as a separate entry point to avoid any lazy loaded logic from accidentally being pulled in to the final build. Having a separate directory allows the lazy loaded implementation to remain accessible from `@ionic/angular` for backwards compatibility.

Ionic developers can access this by importing from `@ionic/angular/standalone`.

**src**

This is where the lazy loaded component implementations live.

Ionic developers can access this by importing from `@ionic/angular`.
5 changes: 5 additions & 0 deletions packages/angular/common/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"lib": {
"entryFile": "src/index.ts"
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './value-accessor';
Original file line number Diff line number Diff line change
Expand Up @@ -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

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

/**
Expand All @@ -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);
}
}

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

/**
Expand All @@ -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);
};
}
});
Expand All @@ -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-')) {
Expand Down
61 changes: 61 additions & 0 deletions packages/angular/common/src/directives/navigation/back-button.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { HostListener, Input, Optional, ElementRef, NgZone, ChangeDetectorRef, Directive } from '@angular/core';
import type { Components } from '@ionic/core';
import type { AnimationBuilder } from '@ionic/core/components';

import { Config } from '../../providers/config';
import { NavController } from '../../providers/nav-controller';
import { ProxyCmp } from '../../utils/proxy';

import { IonRouterOutlet } from './router-outlet';

const BACK_BUTTON_INPUTS = ['color', 'defaultHref', 'disabled', 'icon', 'mode', 'routerAnimation', 'text', 'type'];

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export declare interface IonBackButton extends Components.IonBackButton {}

@ProxyCmp({
inputs: BACK_BUTTON_INPUTS,
})
@Directive({
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: BACK_BUTTON_INPUTS,
})
// eslint-disable-next-line @angular-eslint/directive-class-suffix
export class IonBackButton {
@Input()
defaultHref: string | undefined;

@Input()
routerAnimation: AnimationBuilder | undefined;

protected el: HTMLElement;

constructor(
@Optional() private routerOutlet: IonRouterOutlet,
private navCtrl: NavController,
private config: Config,
private r: ElementRef,
protected z: NgZone,
c: ChangeDetectorRef
) {
c.detach();
this.el = this.r.nativeElement;
}

/**
* @internal
*/
@HostListener('click', ['$event'])
onClick(ev: Event): void {
const defaultHref = this.defaultHref || this.config.get('backButtonDefaultHref');

if (this.routerOutlet?.canGoBack()) {
this.navCtrl.setDirection('back', undefined, undefined, this.routerAnimation);
this.routerOutlet.pop();
ev.preventDefault();
} else if (defaultHref != null) {
this.navCtrl.navigateBack(defaultHref, { animation: this.routerAnimation });
ev.preventDefault();
}
}
}
52 changes: 52 additions & 0 deletions packages/angular/common/src/directives/navigation/nav.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { ElementRef, Injector, EnvironmentInjector, NgZone, ChangeDetectorRef, Directive } from '@angular/core';
import type { Components } from '@ionic/core';

import { AngularDelegate } from '../../providers/angular-delegate';
import { ProxyCmp, proxyOutputs } from '../../utils/proxy';

const NAV_INPUTS = ['animated', 'animation', 'root', 'rootParams', 'swipeGesture'];

const NAV_METHODS = [
'push',
'insert',
'insertPages',
'pop',
'popTo',
'popToRoot',
'removeIndex',
'setRoot',
'setPages',
'getActive',
'getByIndex',
'canGoBack',
'getPrevious',
];

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export declare interface IonNav extends Components.IonNav {}

@ProxyCmp({
inputs: NAV_INPUTS,
methods: NAV_METHODS,
})
@Directive({
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: NAV_INPUTS,
})
// eslint-disable-next-line @angular-eslint/directive-class-suffix
export class IonNav {
protected el: HTMLElement;
constructor(
ref: ElementRef,
environmentInjector: EnvironmentInjector,
injector: Injector,
angularDelegate: AngularDelegate,
protected z: NgZone,
c: ChangeDetectorRef
) {
c.detach();
this.el = ref.nativeElement;
ref.nativeElement.delegate = angularDelegate.create(environmentInjector, injector);
proxyOutputs(this, this.el, ['ionNavDidChange', 'ionNavWillChange']);
}
}
Loading
Loading