Compare commits

...

22 Commits

Author SHA1 Message Date
Liam DeBeasi
28deb56e9c chore: sync with main
chore: sync with main
2023-09-07 11:51:13 -04:00
Maria Hutt
3720ae6a09 chore(angular): remove unused type 2023-09-07 08:08:14 -07:00
Maria Hutt
01c34738a6 chore(angular): add missing code from bbfb8f8 2023-09-06 14:07:56 -07:00
Maria Hutt
fd0f25aa72 Merge remote-tracking branch 'origin/main' into FW-4612-sync 2023-09-06 13:24:09 -07:00
Amanda Johnston
0afa14ed7a feat(angular): add standalone tabs (#28093)
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?

Tabs cannot be used as a standalone component.

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

- Added tabs as a standalone component.
- Added a quick test. I included the event checking from the lazy test
in the HTML as a smoke check, but didn't bring over the Cypress tests
for them since I noticed the other standalone tests have been quick,
stripped down affairs. I'm assuming I would just be duplicating effort.
Let me know if I should bring more tests over from the lazy version, or
even get rid of the event logging from the standalone HTML.

## 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. -->
2023-09-01 13:08:20 -05:00
Liam DeBeasi
e5b7f5ff55 feat(angular): add enhanced icon support with addIcons (#28009) 2023-08-22 09:46:46 -04:00
Sean Perkins
e57759f4b6 chore(angular): generate standalone component wrappers (#27970)
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 Framework currently only generates the lazy/hydrated Angular
component wrappers for the web components.

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

- Generates Angular standalone component wrappers for the web
components, using the CE build.
  - Adds manual component wrapper for `ion-icon` with the CE build.
- Migrates the `ion-back-button` and `ion-nav` to be manual component
wrappers
- Continues to generate the lazy/hydrated Angular component wrappers.
- Refactors "NavDelegate" etc. language to `IonNav` to avoid exporting
as and simplify navigating the code.

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

Related output targets PR:
https://github.com/ionic-team/stencil-ds-output-targets/pull/367
2023-08-18 11:02:16 -04:00
Liam DeBeasi
ca53682684 feat(angular): add standalone provideIonicAngular (#27996) 2023-08-15 15:32:27 -04:00
Liam DeBeasi
c65e08dcd1 feat(angular): add standalone providers, route strategy, component binding provider (#27997) 2023-08-15 11:45:37 -04:00
Liam DeBeasi
84212acce4 refactor(angular): use type for imports used as types (#27998) 2023-08-15 11:44:58 -04:00
Sean Perkins
104b9547e5 chore: typescript resolves @ionic/angular/common import paths (#27995)
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. -->

IDEs that perform type checking/include a language service will error on
the `@ionic/angular/common` import paths and not provide intelisense or
auto import detection.

![CleanShot 2023-08-15 at 09 49
34](https://github.com/ionic-team/ionic-framework/assets/13732623/2e9913b2-5b44-4dd7-8cf7-8fa0d6aaac69)


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

- `@ionic/angular/common` import paths are detected by IDEs such as
VSCode

![CleanShot 2023-08-15 at 09 48
40](https://github.com/ionic-team/ionic-framework/assets/13732623/cd502093-b095-47a8-b5f7-b28a0fc9fe27)


## 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. -->
2023-08-15 10:50:38 -04:00
Maria Hutt
e44a02632d feat(angular): add standalone router-link (#27937) 2023-08-08 11:15:50 -05:00
Maria Hutt
c52a0972b9 feat(angular): add standalone nav (#27876) 2023-08-04 16:20:39 -07:00
Liam DeBeasi
8a97e406a0 feat(angular): add standalone popover (#27883) 2023-08-04 16:55:46 -04:00
Liam DeBeasi
37acdf9711 feat(angular): add standalone modal (#27885) 2023-08-04 15:45:50 -04:00
Liam DeBeasi
9f20780d66 feat(angular): add standalone back-button (#27927) 2023-08-04 15:12:11 -04:00
Liam DeBeasi
3ea7488b47 feat(angular): add standalone router outlet (#27926) 2023-08-04 14:57:32 -04:00
Liam DeBeasi
de48493a16 refactor(angular): move remaining directives to common module (#27909) 2023-08-04 13:46:45 -04:00
Liam DeBeasi
5b31439ca0 refactor(angular): move providers to common library (#27899) 2023-08-01 15:59:53 -04:00
Liam DeBeasi
291b1310a6 chore: move lazy and standalone tests to subdirectories (#27881) 2023-08-01 15:44:26 -04:00
Liam DeBeasi
af68808e69 test(angular): update test app to account for standalone components (#27867) 2023-07-28 10:33:09 -04:00
Liam DeBeasi
4553425502 chore: add infrastructure for standalone (#27866) 2023-07-27 13:37:42 -04:00
263 changed files with 6094 additions and 1589 deletions

14
core/package-lock.json generated
View File

@@ -25,7 +25,7 @@
"@playwright/test": "^1.37.1",
"@rollup/plugin-node-resolve": "^8.4.0",
"@rollup/plugin-virtual": "^2.0.3",
"@stencil/angular-output-target": "^0.7.1",
"@stencil/angular-output-target": "^0.8.1",
"@stencil/react-output-target": "^0.5.3",
"@stencil/sass": "^3.0.5",
"@stencil/vue-output-target": "^0.8.6",
@@ -1625,9 +1625,9 @@
}
},
"node_modules/@stencil/angular-output-target": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/@stencil/angular-output-target/-/angular-output-target-0.7.1.tgz",
"integrity": "sha512-lxJbCAbyAQVAKGgEpNTjSF7GZZszbrJnNdNVgzuD1hLRFJyElA6kUSL0GQrZMbiPG5lC/cYdbQwpyWHX4xN8mw==",
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@stencil/angular-output-target/-/angular-output-target-0.8.1.tgz",
"integrity": "sha512-nO04lC7JxUT0bi2sTvBslz6aut1dVOvYRckf29a0l08XnRsDzIfT5e6f+c5HL9vCl4UFRq8fTQ+Og/G7Hlwtng==",
"dev": true,
"peerDependencies": {
"@stencil/core": ">=2.0.0 || >=3 || >= 4.0.0-beta.0 || >= 4.0.0"
@@ -11517,9 +11517,9 @@
}
},
"@stencil/angular-output-target": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/@stencil/angular-output-target/-/angular-output-target-0.7.1.tgz",
"integrity": "sha512-lxJbCAbyAQVAKGgEpNTjSF7GZZszbrJnNdNVgzuD1hLRFJyElA6kUSL0GQrZMbiPG5lC/cYdbQwpyWHX4xN8mw==",
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@stencil/angular-output-target/-/angular-output-target-0.8.1.tgz",
"integrity": "sha512-nO04lC7JxUT0bi2sTvBslz6aut1dVOvYRckf29a0l08XnRsDzIfT5e6f+c5HL9vCl4UFRq8fTQ+Og/G7Hlwtng==",
"dev": true,
"requires": {}
},

View File

@@ -47,7 +47,7 @@
"@playwright/test": "^1.37.1",
"@rollup/plugin-node-resolve": "^8.4.0",
"@rollup/plugin-virtual": "^2.0.3",
"@stencil/angular-output-target": "^0.7.1",
"@stencil/angular-output-target": "^0.8.1",
"@stencil/react-output-target": "^0.5.3",
"@stencil/sass": "^3.0.5",
"@stencil/vue-output-target": "^0.8.6",

View File

@@ -7,6 +7,56 @@ 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'
],
outputType: 'standalone',
})
];
}
export const config: Config = {
autoprefixCss: true,
sourceMap: false,
@@ -61,7 +111,7 @@ export const config: Config = {
],
outputTargets: [
reactOutputTarget({
componentCorePackage: '@ionic/core',
componentCorePackage,
includeImportCustomElements: true,
includePolyfills: false,
includeDefineCustomElements: false,
@@ -98,7 +148,7 @@ export const config: Config = {
]
}),
vueOutputTarget({
componentCorePackage: '@ionic/core',
componentCorePackage,
includeImportCustomElements: true,
includePolyfills: false,
includeDefineCustomElements: false,
@@ -182,30 +232,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: {

View File

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

View File

@@ -0,0 +1,5 @@
{
"lib": {
"entryFile": "src/index.ts"
},
}

View File

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

View File

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

View File

@@ -0,0 +1,106 @@
import { LocationStrategy } from '@angular/common';
import { ElementRef, OnChanges, OnInit, Directive, HostListener, Input, Optional } from '@angular/core';
import { Router, RouterLink } from '@angular/router';
import type { AnimationBuilder, RouterDirection } from '@ionic/core/components';
import { NavController } from '../../providers/nav-controller';
/**
* Adds support for Ionic routing directions and animations to the base Angular router link directive.
*
* When the router link is clicked, the directive will assign the direction and
* animation so that the routing integration will transition correctly.
*/
@Directive({
selector: ':not(a):not(area)[routerLink]',
})
export class RouterLinkDelegateDirective implements OnInit, OnChanges {
@Input()
routerDirection: RouterDirection = 'forward';
@Input()
routerAnimation?: AnimationBuilder;
constructor(
private locationStrategy: LocationStrategy,
private navCtrl: NavController,
private elementRef: ElementRef,
private router: Router,
@Optional() private routerLink?: RouterLink
) {}
ngOnInit(): void {
this.updateTargetUrlAndHref();
}
ngOnChanges(): void {
this.updateTargetUrlAndHref();
}
private updateTargetUrlAndHref() {
if (this.routerLink?.urlTree) {
const href = this.locationStrategy.prepareExternalUrl(this.router.serializeUrl(this.routerLink.urlTree));
this.elementRef.nativeElement.href = href;
}
}
/**
* @internal
*/
@HostListener('click', ['$event'])
onClick(ev: UIEvent): void {
this.navCtrl.setDirection(this.routerDirection, undefined, undefined, this.routerAnimation);
/**
* This prevents the browser from
* performing a page reload when pressing
* an Ionic component with routerLink.
* The page reload interferes with routing
* and causes ion-back-button to disappear
* since the local history is wiped on reload.
*/
ev.preventDefault();
}
}
@Directive({
selector: 'a[routerLink],area[routerLink]',
})
export class RouterLinkWithHrefDelegateDirective implements OnInit, OnChanges {
@Input()
routerDirection: RouterDirection = 'forward';
@Input()
routerAnimation?: AnimationBuilder;
constructor(
private locationStrategy: LocationStrategy,
private navCtrl: NavController,
private elementRef: ElementRef,
private router: Router,
@Optional() private routerLink?: RouterLink
) {}
ngOnInit(): void {
this.updateTargetUrlAndHref();
}
ngOnChanges(): void {
this.updateTargetUrlAndHref();
}
private updateTargetUrlAndHref() {
if (this.routerLink?.urlTree) {
const href = this.locationStrategy.prepareExternalUrl(this.router.serializeUrl(this.routerLink.urlTree));
this.elementRef.nativeElement.href = href;
}
}
/**
* @internal
*/
@HostListener('click')
onClick(): void {
this.navCtrl.setDirection(this.routerDirection, undefined, undefined, this.routerAnimation);
}
}

View File

@@ -0,0 +1,540 @@
import { Location } from '@angular/common';
import {
ComponentRef,
ElementRef,
Injector,
NgZone,
OnDestroy,
OnInit,
ViewContainerRef,
inject,
Attribute,
Directive,
EventEmitter,
Optional,
Output,
SkipSelf,
EnvironmentInjector,
Input,
InjectionToken,
Injectable,
reflectComponentType,
} from '@angular/core';
import type { Provider } from '@angular/core';
import { OutletContext, Router, ActivatedRoute, ChildrenOutletContexts, PRIMARY_OUTLET, Data } from '@angular/router';
import { componentOnReady } from '@ionic/core/components';
import type { AnimationBuilder } from '@ionic/core/components';
import { Observable, BehaviorSubject, Subscription, combineLatest, of } from 'rxjs';
import { distinctUntilChanged, filter, switchMap } from 'rxjs/operators';
import { Config } from '../../providers/config';
import { NavController } from '../../providers/nav-controller';
import { StackController } from './stack-controller';
import { RouteView, StackDidChangeEvent, StackWillChangeEvent, getUrl, isTabSwitch } from './stack-utils';
// TODO(FW-2827): types
@Directive({
selector: 'ion-router-outlet',
exportAs: 'outlet',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['animated', 'animation', 'mode', 'swipeGesture'],
})
// eslint-disable-next-line @angular-eslint/directive-class-suffix
export class IonRouterOutlet implements OnDestroy, OnInit {
nativeEl: HTMLIonRouterOutletElement;
activatedView: RouteView | null = null;
tabsPrefix: string | undefined;
private _swipeGesture?: boolean;
private stackCtrl: StackController;
// Maintain map of activated route proxies for each component instance
private proxyMap = new WeakMap<any, ActivatedRoute>();
// Keep the latest activated route in a subject for the proxy routes to switch map to
private currentActivatedRoute$ = new BehaviorSubject<{ component: any; activatedRoute: ActivatedRoute } | null>(null);
private activated: ComponentRef<any> | null = null;
/** @internal */
get activatedComponentRef(): ComponentRef<any> | null {
return this.activated;
}
private _activatedRoute: ActivatedRoute | null = null;
/**
* The name of the outlet
*/
@Input() name = PRIMARY_OUTLET;
/** @internal */
@Output() stackWillChange = new EventEmitter<StackWillChangeEvent>();
/** @internal */
@Output() stackDidChange = new EventEmitter<StackDidChangeEvent>();
// eslint-disable-next-line @angular-eslint/no-output-rename
@Output('activate') activateEvents = new EventEmitter<any>();
// eslint-disable-next-line @angular-eslint/no-output-rename
@Output('deactivate') deactivateEvents = new EventEmitter<any>();
private parentContexts = inject(ChildrenOutletContexts);
private location = inject(ViewContainerRef);
private environmentInjector = inject(EnvironmentInjector);
private inputBinder = inject(INPUT_BINDER, { optional: true });
/** @nodoc */
readonly supportsBindingToComponentInputs = true;
// Ionic providers
private config = inject(Config);
private navCtrl = inject(NavController);
set animation(animation: AnimationBuilder) {
this.nativeEl.animation = animation;
}
set animated(animated: boolean) {
this.nativeEl.animated = animated;
}
set swipeGesture(swipe: boolean) {
this._swipeGesture = swipe;
this.nativeEl.swipeHandler = swipe
? {
canStart: () => this.stackCtrl.canGoBack(1) && !this.stackCtrl.hasRunningTask(),
onStart: () => this.stackCtrl.startBackTransition(),
onEnd: (shouldContinue) => this.stackCtrl.endBackTransition(shouldContinue),
}
: undefined;
}
constructor(
@Attribute('name') name: string,
@Optional() @Attribute('tabs') tabs: string,
commonLocation: Location,
elementRef: ElementRef,
router: Router,
zone: NgZone,
activatedRoute: ActivatedRoute,
@SkipSelf() @Optional() readonly parentOutlet?: IonRouterOutlet
) {
this.nativeEl = elementRef.nativeElement;
this.name = name || PRIMARY_OUTLET;
this.tabsPrefix = tabs === 'true' ? getUrl(router, activatedRoute) : undefined;
this.stackCtrl = new StackController(this.tabsPrefix, this.nativeEl, router, this.navCtrl, zone, commonLocation);
this.parentContexts.onChildOutletCreated(this.name, this as any);
}
ngOnDestroy(): void {
this.stackCtrl.destroy();
this.inputBinder?.unsubscribeFromRouteData(this);
}
getContext(): OutletContext | null {
return this.parentContexts.getContext(this.name);
}
ngOnInit(): void {
this.initializeOutletWithName();
}
// Note: Ionic deviates from the Angular Router implementation here
private initializeOutletWithName() {
if (!this.activated) {
// If the outlet was not instantiated at the time the route got activated we need to populate
// the outlet when it is initialized (ie inside a NgIf)
const context = this.getContext();
if (context?.route) {
this.activateWith(context.route, context.injector);
}
}
new Promise((resolve) => componentOnReady(this.nativeEl, resolve)).then(() => {
if (this._swipeGesture === undefined) {
this.swipeGesture = this.config.getBoolean('swipeBackEnabled', (this.nativeEl as any).mode === 'ios');
}
});
}
get isActivated(): boolean {
return !!this.activated;
}
get component(): Record<string, unknown> {
if (!this.activated) {
throw new Error('Outlet is not activated');
}
return this.activated.instance;
}
get activatedRoute(): ActivatedRoute {
if (!this.activated) {
throw new Error('Outlet is not activated');
}
return this._activatedRoute as ActivatedRoute;
}
get activatedRouteData(): Data {
if (this._activatedRoute) {
return this._activatedRoute.snapshot.data;
}
return {};
}
/**
* Called when the `RouteReuseStrategy` instructs to detach the subtree
*/
detach(): ComponentRef<any> {
throw new Error('incompatible reuse strategy');
}
/**
* Called when the `RouteReuseStrategy` instructs to re-attach a previously detached subtree
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
attach(_ref: ComponentRef<any>, _activatedRoute: ActivatedRoute): void {
throw new Error('incompatible reuse strategy');
}
deactivate(): void {
if (this.activated) {
if (this.activatedView) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const context = this.getContext()!;
this.activatedView.savedData = new Map(context.children['contexts']);
/**
* Angular v11.2.10 introduced a change
* where this route context is cleared out when
* a router-outlet is deactivated, However,
* we need this route information in order to
* return a user back to the correct tab when
* leaving and then going back to the tab context.
*/
const primaryOutlet = this.activatedView.savedData.get('primary');
if (primaryOutlet && context.route) {
primaryOutlet.route = { ...context.route };
}
/**
* Ensure we are saving the NavigationExtras
* data otherwise it will be lost
*/
this.activatedView.savedExtras = {};
if (context.route) {
const contextSnapshot = context.route.snapshot;
this.activatedView.savedExtras.queryParams = contextSnapshot.queryParams;
(this.activatedView.savedExtras.fragment as string | null) = contextSnapshot.fragment;
}
}
const c = this.component;
this.activatedView = null;
this.activated = null;
this._activatedRoute = null;
this.deactivateEvents.emit(c);
}
}
activateWith(activatedRoute: ActivatedRoute, environmentInjector: EnvironmentInjector | null): void {
if (this.isActivated) {
throw new Error('Cannot activate an already activated outlet');
}
this._activatedRoute = activatedRoute;
let cmpRef: any;
let enteringView = this.stackCtrl.getExistingView(activatedRoute);
if (enteringView) {
cmpRef = this.activated = enteringView.ref;
const saved = enteringView.savedData;
if (saved) {
// self-restore
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const context = this.getContext()!;
context.children['contexts'] = saved;
}
// Updated activated route proxy for this component
this.updateActivatedRouteProxy(cmpRef.instance, activatedRoute);
} else {
const snapshot = (activatedRoute as any)._futureSnapshot;
/**
* Angular 14 introduces a new `loadComponent` property to the route config.
* This function will assign a `component` property to the route snapshot.
* We check for the presence of this property to determine if the route is
* using standalone components.
*/
const childContexts = this.parentContexts.getOrCreateContext(this.name).children;
// We create an activated route proxy object that will maintain future updates for this component
// over its lifecycle in the stack.
const component$ = new BehaviorSubject<any>(null);
const activatedRouteProxy = this.createActivatedRouteProxy(component$, activatedRoute);
const injector = new OutletInjector(activatedRouteProxy, childContexts, this.location.injector);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const component = snapshot.routeConfig!.component ?? snapshot.component;
cmpRef = this.activated = this.location.createComponent(component, {
index: this.location.length,
injector,
environmentInjector: environmentInjector ?? this.environmentInjector,
});
// Once the component is created we can push it to our local subject supplied to the proxy
component$.next(cmpRef.instance);
// Calling `markForCheck` to make sure we will run the change detection when the
// `RouterOutlet` is inside a `ChangeDetectionStrategy.OnPush` component.
enteringView = this.stackCtrl.createView(this.activated, activatedRoute);
// Store references to the proxy by component
this.proxyMap.set(cmpRef.instance, activatedRouteProxy);
this.currentActivatedRoute$.next({ component: cmpRef.instance, activatedRoute });
}
this.inputBinder?.bindActivatedRouteToOutletComponent(this);
this.activatedView = enteringView;
/**
* The top outlet is set prior to the entering view's transition completing,
* so that when we have nested outlets (e.g. ion-tabs inside an ion-router-outlet),
* the tabs outlet will be assigned as the top outlet when a view inside tabs is
* activated.
*
* In this scenario, activeWith is called for both the tabs and the root router outlet.
* To avoid a race condition, we assign the top outlet synchronously.
*/
this.navCtrl.setTopOutlet(this);
const leavingView = this.stackCtrl.getActiveView();
this.stackWillChange.emit({
enteringView,
tabSwitch: isTabSwitch(enteringView, leavingView),
});
this.stackCtrl.setActive(enteringView).then((data) => {
this.activateEvents.emit(cmpRef.instance);
this.stackDidChange.emit(data);
});
}
/**
* Returns `true` if there are pages in the stack to go back.
*/
canGoBack(deep = 1, stackId?: string): boolean {
return this.stackCtrl.canGoBack(deep, stackId);
}
/**
* Resolves to `true` if it the outlet was able to sucessfully pop the last N pages.
*/
pop(deep = 1, stackId?: string): Promise<boolean> {
return this.stackCtrl.pop(deep, stackId);
}
/**
* Returns the URL of the active page of each stack.
*/
getLastUrl(stackId?: string): string | undefined {
const active = this.stackCtrl.getLastUrl(stackId);
return active ? active.url : undefined;
}
/**
* Returns the RouteView of the active page of each stack.
* @internal
*/
getLastRouteView(stackId?: string): RouteView | undefined {
return this.stackCtrl.getLastUrl(stackId);
}
/**
* Returns the root view in the tab stack.
* @internal
*/
getRootView(stackId?: string): RouteView | undefined {
return this.stackCtrl.getRootUrl(stackId);
}
/**
* Returns the active stack ID. In the context of ion-tabs, it means the active tab.
*/
getActiveStackId(): string | undefined {
return this.stackCtrl.getActiveStackId();
}
/**
* Since the activated route can change over the life time of a component in an ion router outlet, we create
* a proxy so that we can update the values over time as a user navigates back to components already in the stack.
*/
private createActivatedRouteProxy(component$: Observable<any>, activatedRoute: ActivatedRoute): ActivatedRoute {
const proxy: any = new ActivatedRoute();
proxy._futureSnapshot = (activatedRoute as any)._futureSnapshot;
proxy._routerState = (activatedRoute as any)._routerState;
proxy.snapshot = activatedRoute.snapshot;
proxy.outlet = activatedRoute.outlet;
proxy.component = activatedRoute.component;
// Setup wrappers for the observables so consumers don't have to worry about switching to new observables as the state updates
(proxy as any)._paramMap = this.proxyObservable(component$, 'paramMap');
(proxy as any)._queryParamMap = this.proxyObservable(component$, 'queryParamMap');
proxy.url = this.proxyObservable(component$, 'url');
proxy.params = this.proxyObservable(component$, 'params');
proxy.queryParams = this.proxyObservable(component$, 'queryParams');
proxy.fragment = this.proxyObservable(component$, 'fragment');
proxy.data = this.proxyObservable(component$, 'data');
return proxy as ActivatedRoute;
}
/**
* Create a wrapped observable that will switch to the latest activated route matched by the given component
*/
private proxyObservable(component$: Observable<any>, path: string): Observable<any> {
return component$.pipe(
// First wait until the component instance is pushed
filter((component) => !!component),
switchMap((component) =>
this.currentActivatedRoute$.pipe(
filter((current) => current !== null && current.component === component),
switchMap((current) => current && (current.activatedRoute as any)[path]),
distinctUntilChanged()
)
)
);
}
/**
* Updates the activated route proxy for the given component to the new incoming router state
*/
private updateActivatedRouteProxy(component: any, activatedRoute: ActivatedRoute): void {
const proxy = this.proxyMap.get(component);
if (!proxy) {
throw new Error(`Could not find activated route proxy for view`);
}
(proxy as any)._futureSnapshot = (activatedRoute as any)._futureSnapshot;
(proxy as any)._routerState = (activatedRoute as any)._routerState;
proxy.snapshot = activatedRoute.snapshot;
proxy.outlet = activatedRoute.outlet;
proxy.component = activatedRoute.component;
this.currentActivatedRoute$.next({ component, activatedRoute });
}
}
class OutletInjector implements Injector {
constructor(private route: ActivatedRoute, private childContexts: ChildrenOutletContexts, private parent: Injector) {}
get(token: any, notFoundValue?: any): any {
if (token === ActivatedRoute) {
return this.route;
}
if (token === ChildrenOutletContexts) {
return this.childContexts;
}
return this.parent.get(token, notFoundValue);
}
}
// TODO: FW-4785 - Remove this once Angular 15 support is dropped
const INPUT_BINDER = new InjectionToken<RoutedComponentInputBinder>('');
/**
* Injectable used as a tree-shakable provider for opting in to binding router data to component
* inputs.
*
* The RouterOutlet registers itself with this service when an `ActivatedRoute` is attached or
* activated. When this happens, the service subscribes to the `ActivatedRoute` observables (params,
* queryParams, data) and sets the inputs of the component using `ComponentRef.setInput`.
* Importantly, when an input does not have an item in the route data with a matching key, this
* input is set to `undefined`. If it were not done this way, the previous information would be
* retained if the data got removed from the route (i.e. if a query parameter is removed).
*
* The `RouterOutlet` should unregister itself when destroyed via `unsubscribeFromRouteData` so that
* the subscriptions are cleaned up.
*/
@Injectable()
class RoutedComponentInputBinder {
private outletDataSubscriptions = new Map<IonRouterOutlet, Subscription>();
bindActivatedRouteToOutletComponent(outlet: IonRouterOutlet): void {
this.unsubscribeFromRouteData(outlet);
this.subscribeToRouteData(outlet);
}
unsubscribeFromRouteData(outlet: IonRouterOutlet): void {
this.outletDataSubscriptions.get(outlet)?.unsubscribe();
this.outletDataSubscriptions.delete(outlet);
}
private subscribeToRouteData(outlet: IonRouterOutlet) {
const { activatedRoute } = outlet;
const dataSubscription = combineLatest([activatedRoute.queryParams, activatedRoute.params, activatedRoute.data])
.pipe(
switchMap(([queryParams, params, data], index) => {
data = { ...queryParams, ...params, ...data };
// Get the first result from the data subscription synchronously so it's available to
// the component as soon as possible (and doesn't require a second change detection).
if (index === 0) {
return of(data);
}
// Promise.resolve is used to avoid synchronously writing the wrong data when
// two of the Observables in the `combineLatest` stream emit one after
// another.
return Promise.resolve(data);
})
)
.subscribe((data) => {
// Outlet may have been deactivated or changed names to be associated with a different
// route
if (
!outlet.isActivated ||
!outlet.activatedComponentRef ||
outlet.activatedRoute !== activatedRoute ||
activatedRoute.component === null
) {
this.unsubscribeFromRouteData(outlet);
return;
}
const mirror = reflectComponentType(activatedRoute.component);
if (!mirror) {
this.unsubscribeFromRouteData(outlet);
return;
}
for (const { templateName } of mirror.inputs) {
outlet.activatedComponentRef.setInput(templateName, data[templateName]);
}
});
this.outletDataSubscriptions.set(outlet, dataSubscription);
}
}
export const provideComponentInputBinding = (): Provider => {
return {
provide: INPUT_BINDER,
useFactory: componentInputBindingFactory,
deps: [Router],
};
};
function componentInputBindingFactory(router?: Router) {
/**
* We cast the router to any here, since the componentInputBindingEnabled
* property is not available until Angular v16.
*/
if ((router as any)?.componentInputBindingEnabled) {
return new RoutedComponentInputBinder();
}
return null;
}

View File

@@ -1,7 +1,7 @@
import { Location } from '@angular/common';
import { ComponentRef, NgZone } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { AnimationBuilder, RouterDirection } from '@ionic/core';
import type { AnimationBuilder, RouterDirection } from '@ionic/core/components';
import { bindLifecycleEvents } from '../../providers/angular-delegate';
import { NavController } from '../../providers/nav-controller';

View File

@@ -1,6 +1,6 @@
import { ComponentRef } from '@angular/core';
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router';
import { AnimationBuilder, NavDirection, RouterDirection } from '@ionic/core';
import type { AnimationBuilder, NavDirection, RouterDirection } from '@ionic/core/components';
export const insertView = (views: RouteView[], view: RouteView, direction: RouterDirection): RouteView[] => {
if (direction === 'root') {

View File

@@ -0,0 +1,192 @@
import {
AfterContentChecked,
AfterContentInit,
Directive,
ElementRef,
EventEmitter,
HostListener,
Output,
ViewChild,
} from '@angular/core';
import { NavController } from '../../providers/nav-controller';
import { StackDidChangeEvent, StackWillChangeEvent } from './stack-utils';
@Directive({
selector: 'ion-tabs',
})
// eslint-disable-next-line @angular-eslint/directive-class-suffix
export class IonTabs implements AfterContentInit, AfterContentChecked {
/**
* Note: These must be redeclared on each child class since it needs
* access to generated components such as IonRouterOutlet and IonTabBar.
*/
outlet: any;
tabBar: any;
tabBars: any;
@ViewChild('tabsInner', { read: ElementRef, static: true }) tabsInner: ElementRef<HTMLDivElement>;
/**
* Emitted before the tab view is changed.
*/
@Output() ionTabsWillChange = new EventEmitter<{ tab: string }>();
/**
* Emitted after the tab view is changed.
*/
@Output() ionTabsDidChange = new EventEmitter<{ tab: string }>();
private tabBarSlot = 'bottom';
constructor(private navCtrl: NavController) {}
ngAfterContentInit(): void {
this.detectSlotChanges();
}
ngAfterContentChecked(): void {
this.detectSlotChanges();
}
/**
* @internal
*/
onStackWillChange({ enteringView, tabSwitch }: StackWillChangeEvent): void {
const stackId = enteringView.stackId;
if (tabSwitch && stackId !== undefined) {
this.ionTabsWillChange.emit({ tab: stackId });
}
}
/**
* @internal
*/
onStackDidChange({ enteringView, tabSwitch }: StackDidChangeEvent): void {
const stackId = enteringView.stackId;
if (tabSwitch && stackId !== undefined) {
if (this.tabBar) {
this.tabBar.selectedTab = stackId;
}
this.ionTabsDidChange.emit({ tab: stackId });
}
}
/**
* When a tab button is clicked, there are several scenarios:
* 1. If the selected tab is currently active (the tab button has been clicked
* again), then it should go to the root view for that tab.
*
* a. Get the saved root view from the router outlet. If the saved root view
* matches the tabRootUrl, set the route view to this view including the
* navigation extras.
* b. If the saved root view from the router outlet does
* not match, navigate to the tabRootUrl. No navigation extras are
* included.
*
* 2. If the current tab tab is not currently selected, get the last route
* view from the router outlet.
*
* a. If the last route view exists, navigate to that view including any
* navigation extras
* b. If the last route view doesn't exist, then navigate
* to the default tabRootUrl
*/
@HostListener('ionTabButtonClick', ['$event'])
select(tabOrEvent: string | CustomEvent): Promise<boolean> | undefined {
const isTabString = typeof tabOrEvent === 'string';
const tab = isTabString ? tabOrEvent : (tabOrEvent as CustomEvent).detail.tab;
const alreadySelected = this.outlet.getActiveStackId() === tab;
const tabRootUrl = `${this.outlet.tabsPrefix}/${tab}`;
/**
* If this is a nested tab, prevent the event
* from bubbling otherwise the outer tabs
* will respond to this event too, causing
* the app to get directed to the wrong place.
*/
if (!isTabString) {
(tabOrEvent as CustomEvent).stopPropagation();
}
if (alreadySelected) {
const activeStackId = this.outlet.getActiveStackId();
const activeView = this.outlet.getLastRouteView(activeStackId);
// If on root tab, do not navigate to root tab again
if (activeView?.url === tabRootUrl) {
return;
}
const rootView = this.outlet.getRootView(tab);
const navigationExtras = rootView && tabRootUrl === rootView.url && rootView.savedExtras;
return this.navCtrl.navigateRoot(tabRootUrl, {
...navigationExtras,
animated: true,
animationDirection: 'back',
});
} else {
const lastRoute = this.outlet.getLastRouteView(tab);
/**
* If there is a lastRoute, goto that, otherwise goto the fallback url of the
* selected tab
*/
const url = lastRoute?.url || tabRootUrl;
const navigationExtras = lastRoute?.savedExtras;
return this.navCtrl.navigateRoot(url, {
...navigationExtras,
animated: true,
animationDirection: 'back',
});
}
}
getSelected(): string | undefined {
return this.outlet.getActiveStackId();
}
/**
* Detects changes to the slot attribute of the tab bar.
*
* If the slot attribute has changed, then the tab bar
* should be relocated to the new slot position.
*/
private detectSlotChanges(): void {
this.tabBars.forEach((tabBar: any) => {
// el is a protected attribute from the generated component wrapper
const currentSlot = tabBar.el.getAttribute('slot');
if (currentSlot !== this.tabBarSlot) {
this.tabBarSlot = currentSlot;
this.relocateTabBar();
}
});
}
/**
* Relocates the tab bar to the new slot position.
*/
private relocateTabBar(): void {
/**
* `el` is a protected attribute from the generated component wrapper.
* To avoid having to manually create the wrapper for tab bar, we
* cast the tab bar to any and access the protected attribute.
*/
const tabBar = (this.tabBar as any).el as HTMLElement;
if (this.tabBarSlot === 'top') {
/**
* A tab bar with a slot of "top" should be inserted
* at the top of the container.
*/
this.tabsInner.nativeElement.before(tabBar);
} else {
/**
* A tab bar with a slot of "bottom" or without a slot
* should be inserted at the end of the container.
*/
this.tabsInner.nativeElement.after(tabBar);
}
}
}

View File

@@ -0,0 +1,39 @@
export { ActionSheetController } from './providers/action-sheet-controller';
export { AlertController } from './providers/alert-controller';
export { LoadingController } from './providers/loading-controller';
export { MenuController } from './providers/menu-controller';
export { ModalController } from './providers/modal-controller';
export { PickerController } from './providers/picker-controller';
export { PopoverController } from './providers/popover-controller';
export { ToastController } from './providers/toast-controller';
export { AnimationController } from './providers/animation-controller';
export { GestureController } from './providers/gesture-controller';
export { DomController } from './providers/dom-controller';
export { NavController } from './providers/nav-controller';
export { Config, ConfigToken } from './providers/config';
export { Platform } from './providers/platform';
export { bindLifecycleEvents, AngularDelegate } from './providers/angular-delegate';
export type { IonicWindow } from './types/interfaces';
export { NavParams } from './directives/navigation/nav-params';
export { IonPopover } from './overlays/popover';
export { IonModal } from './overlays/modal';
export { IonRouterOutlet, provideComponentInputBinding } from './directives/navigation/router-outlet';
export { IonBackButton } from './directives/navigation/back-button';
export {
RouterLinkDelegateDirective,
RouterLinkWithHrefDelegateDirective,
} from './directives/navigation/router-link-delegate';
export { IonNav } from './directives/navigation/nav';
export { IonTabs } from './directives/navigation/tabs';
export { ProxyCmp } from './utils/proxy';
export { IonicRouteStrategy } from './utils/routing';

View File

@@ -0,0 +1,133 @@
import {
ChangeDetectorRef,
ContentChild,
Directive,
ElementRef,
EventEmitter,
NgZone,
TemplateRef,
} from '@angular/core';
import type { Components, ModalBreakpointChangeEventDetail } from '@ionic/core/components';
import { ProxyCmp, proxyOutputs } from '../utils/proxy';
export declare interface IonModal extends Components.IonModal {
/**
* Emitted after the modal has presented.
**/
ionModalDidPresent: EventEmitter<CustomEvent>;
/**
* Emitted before the modal has presented.
*/
ionModalWillPresent: EventEmitter<CustomEvent>;
/**
* Emitted before the modal has dismissed.
*/
ionModalWillDismiss: EventEmitter<CustomEvent>;
/**
* Emitted after the modal has dismissed.
*/
ionModalDidDismiss: EventEmitter<CustomEvent>;
/**
* Emitted after the modal breakpoint has changed.
*/
ionBreakpointDidChange: EventEmitter<CustomEvent<ModalBreakpointChangeEventDetail>>;
/**
* Emitted after the modal has presented. Shorthand for ionModalDidPresent.
*/
didPresent: EventEmitter<CustomEvent>;
/**
* Emitted before the modal has presented. Shorthand for ionModalWillPresent.
*/
willPresent: EventEmitter<CustomEvent>;
/**
* Emitted before the modal has dismissed. Shorthand for ionModalWillDismiss.
*/
willDismiss: EventEmitter<CustomEvent>;
/**
* Emitted after the modal has dismissed. Shorthand for ionModalDidDismiss.
*/
didDismiss: EventEmitter<CustomEvent>;
}
const MODAL_INPUTS = [
'animated',
'keepContentsMounted',
'backdropBreakpoint',
'backdropDismiss',
'breakpoints',
'canDismiss',
'cssClass',
'enterAnimation',
'event',
'handle',
'handleBehavior',
'initialBreakpoint',
'isOpen',
'keyboardClose',
'leaveAnimation',
'mode',
'presentingElement',
'showBackdrop',
'translucent',
'trigger',
];
const MODAL_METHODS = [
'present',
'dismiss',
'onDidDismiss',
'onWillDismiss',
'setCurrentBreakpoint',
'getCurrentBreakpoint',
];
@ProxyCmp({
inputs: MODAL_INPUTS,
methods: MODAL_METHODS,
})
/**
* @Component extends from @Directive
* so by defining the inputs here we
* do not need to re-define them for the
* lazy loaded popover.
*/
@Directive({
selector: 'ion-modal',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: MODAL_INPUTS,
})
// eslint-disable-next-line @angular-eslint/directive-class-suffix
export class IonModal {
// TODO(FW-2827): type
@ContentChild(TemplateRef, { static: false }) template: TemplateRef<any>;
isCmpOpen = false;
protected el: HTMLElement;
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
this.el = r.nativeElement;
this.el.addEventListener('ionMount', () => {
this.isCmpOpen = true;
c.detectChanges();
});
this.el.addEventListener('didDismiss', () => {
this.isCmpOpen = false;
c.detectChanges();
});
proxyOutputs(this, this.el, [
'ionModalDidPresent',
'ionModalWillPresent',
'ionModalWillDismiss',
'ionModalDidDismiss',
'ionBreakpointDidChange',
'didPresent',
'willPresent',
'willDismiss',
'didDismiss',
]);
}
}

View File

@@ -0,0 +1,121 @@
import {
ChangeDetectorRef,
ContentChild,
Directive,
ElementRef,
EventEmitter,
NgZone,
TemplateRef,
} from '@angular/core';
import type { Components } from '@ionic/core/components';
import { ProxyCmp, proxyOutputs } from '../utils/proxy';
export declare interface IonPopover extends Components.IonPopover {
/**
* Emitted after the popover has presented.
*/
ionPopoverDidPresent: EventEmitter<CustomEvent>;
/**
* Emitted before the popover has presented.
*/
ionPopoverWillPresent: EventEmitter<CustomEvent>;
/**
* Emitted after the popover has dismissed.
*/
ionPopoverWillDismiss: EventEmitter<CustomEvent>;
/**
* Emitted after the popover has dismissed.
*/
ionPopoverDidDismiss: EventEmitter<CustomEvent>;
/**
* Emitted after the popover has presented. Shorthand for ionPopoverDidPresent.
*/
didPresent: EventEmitter<CustomEvent>;
/**
* Emitted before the popover has presented. Shorthand for ionPopoverWillPresent.
*/
willPresent: EventEmitter<CustomEvent>;
/**
* Emitted after the popover has presented. Shorthand for ionPopoverWillDismiss.
*/
willDismiss: EventEmitter<CustomEvent>;
/**
* Emitted after the popover has dismissed. Shorthand for ionPopoverDidDismiss.
*/
didDismiss: EventEmitter<CustomEvent>;
}
const POPOVER_INPUTS = [
'alignment',
'animated',
'arrow',
'keepContentsMounted',
'backdropDismiss',
'cssClass',
'dismissOnSelect',
'enterAnimation',
'event',
'isOpen',
'keyboardClose',
'leaveAnimation',
'mode',
'showBackdrop',
'translucent',
'trigger',
'triggerAction',
'reference',
'size',
'side',
];
const POPOVER_METHODS = ['present', 'dismiss', 'onDidDismiss', 'onWillDismiss'];
@ProxyCmp({
inputs: POPOVER_INPUTS,
methods: POPOVER_METHODS,
})
/**
* @Component extends from @Directive
* so by defining the inputs here we
* do not need to re-define them for the
* lazy loaded popover.
*/
@Directive({
selector: 'ion-popover',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: POPOVER_INPUTS,
})
// eslint-disable-next-line @angular-eslint/directive-class-suffix
export class IonPopover {
// TODO(FW-2827): type
@ContentChild(TemplateRef, { static: false }) template: TemplateRef<any>;
isCmpOpen = false;
protected el: HTMLElement;
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
this.el = r.nativeElement;
this.el.addEventListener('ionMount', () => {
this.isCmpOpen = true;
c.detectChanges();
});
this.el.addEventListener('didDismiss', () => {
this.isCmpOpen = false;
c.detectChanges();
});
proxyOutputs(this, this.el, [
'ionPopoverDidPresent',
'ionPopoverWillPresent',
'ionPopoverWillDismiss',
'ionPopoverDidDismiss',
'didPresent',
'willPresent',
'willDismiss',
'didDismiss',
]);
}
}

View File

@@ -1,7 +1,8 @@
import { Injectable } from '@angular/core';
import { ActionSheetOptions, actionSheetController } from '@ionic/core';
import type { ActionSheetOptions } from '@ionic/core/components';
import { actionSheetController } from '@ionic/core/components';
import { OverlayBaseController } from '../util/overlay';
import { OverlayBaseController } from '../utils/overlay';
@Injectable({
providedIn: 'root',

View File

@@ -1,7 +1,8 @@
import { Injectable } from '@angular/core';
import { AlertOptions, alertController } from '@ionic/core';
import type { AlertOptions } from '@ionic/core/components';
import { alertController } from '@ionic/core/components';
import { OverlayBaseController } from '../util/overlay';
import { OverlayBaseController } from '../utils/overlay';
@Injectable({
providedIn: 'root',

View File

@@ -16,7 +16,7 @@ import {
LIFECYCLE_WILL_ENTER,
LIFECYCLE_WILL_LEAVE,
LIFECYCLE_WILL_UNLOAD,
} from '@ionic/core';
} from '@ionic/core/components';
import { NavParams } from '../directives/navigation/nav-params';

View File

@@ -1,5 +1,6 @@
import { Injectable } from '@angular/core';
import { Animation, createAnimation, getTimeGivenProgression } from '@ionic/core';
import type { Animation } from '@ionic/core/components';
import { createAnimation, getTimeGivenProgression } from '@ionic/core/components';
@Injectable({
providedIn: 'root',

View File

@@ -1,5 +1,5 @@
import { Injectable, InjectionToken } from '@angular/core';
import { Config as CoreConfig, IonicConfig } from '@ionic/core';
import type { Config as CoreConfig, IonicConfig } from '@ionic/core/components';
import { IonicWindow } from '../types/interfaces';

View File

@@ -1,5 +1,6 @@
import { NgZone, Injectable } from '@angular/core';
import { Gesture, GestureConfig, createGesture } from '@ionic/core';
import type { Gesture, GestureConfig } from '@ionic/core/components';
import { createGesture } from '@ionic/core/components';
@Injectable({
providedIn: 'root',

View File

@@ -1,7 +1,8 @@
import { Injectable } from '@angular/core';
import { LoadingOptions, loadingController } from '@ionic/core';
import type { LoadingOptions } from '@ionic/core/components';
import { loadingController } from '@ionic/core/components';
import { OverlayBaseController } from '../util/overlay';
import { OverlayBaseController } from '../utils/overlay';
@Injectable({
providedIn: 'root',

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { menuController } from '@ionic/core';
import { menuController } from '@ionic/core/components';
@Injectable({
providedIn: 'root',

View File

@@ -1,7 +1,8 @@
import { Injector, Injectable, EnvironmentInjector, inject } from '@angular/core';
import { ModalOptions, modalController } from '@ionic/core';
import type { ModalOptions } from '@ionic/core/components';
import { modalController } from '@ionic/core/components';
import { OverlayBaseController } from '../util/overlay';
import { OverlayBaseController } from '../utils/overlay';
import { AngularDelegate } from './angular-delegate';

View File

@@ -1,9 +1,9 @@
import { Location } from '@angular/common';
import { Injectable, Optional } from '@angular/core';
import { NavigationExtras, Router, UrlSerializer, UrlTree, NavigationStart } from '@angular/router';
import { AnimationBuilder, NavDirection, RouterDirection } from '@ionic/core';
import type { AnimationBuilder, NavDirection, RouterDirection } from '@ionic/core/components';
import { IonRouterOutlet } from '../directives/navigation/ion-router-outlet';
import { IonRouterOutlet } from '../directives/navigation/router-outlet';
import { Platform } from './platform';

View File

@@ -1,7 +1,8 @@
import { Injectable } from '@angular/core';
import { PickerOptions, pickerController } from '@ionic/core';
import type { PickerOptions } from '@ionic/core/components';
import { pickerController } from '@ionic/core/components';
import { OverlayBaseController } from '../util/overlay';
import { OverlayBaseController } from '../utils/overlay';
@Injectable({
providedIn: 'root',

View File

@@ -1,6 +1,7 @@
import { DOCUMENT } from '@angular/common';
import { NgZone, Inject, Injectable } from '@angular/core';
import { BackButtonEventDetail, KeyboardEventDetail, Platforms, getPlatforms, isPlatform } from '@ionic/core';
import { getPlatforms, isPlatform } from '@ionic/core/components';
import type { BackButtonEventDetail, KeyboardEventDetail, Platforms } from '@ionic/core/components';
import { Subscription, Subject } from 'rxjs';
// TODO(FW-2827): types

View File

@@ -1,7 +1,8 @@
import { Injector, Injectable, inject, EnvironmentInjector } from '@angular/core';
import { PopoverOptions, popoverController } from '@ionic/core';
import type { PopoverOptions } from '@ionic/core/components';
import { popoverController } from '@ionic/core/components';
import { OverlayBaseController } from '../util/overlay';
import { OverlayBaseController } from '../utils/overlay';
import { AngularDelegate } from './angular-delegate';

View File

@@ -1,7 +1,8 @@
import { Injectable } from '@angular/core';
import { ToastOptions, toastController } from '@ionic/core';
import type { ToastOptions } from '@ionic/core/components';
import { toastController } from '@ionic/core/components';
import { OverlayBaseController } from '../util/overlay';
import { OverlayBaseController } from '../utils/overlay';
@Injectable({
providedIn: 'root',

View File

@@ -7,8 +7,3 @@ export interface IonicWindow extends Window {
Ionic: IonicGlobal;
__zone_symbol__requestAnimationFrame?: (ts: FrameRequestCallback) => number;
}
export interface HTMLStencilElement extends HTMLElement {
componentOnReady?(): Promise<this>;
forceUpdate?(): void;
}

View File

@@ -0,0 +1,53 @@
// TODO: Is there a way we can grab this from angular-component-lib instead?
/* 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)));
};
// 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

@@ -1,9 +1,8 @@
import { NgZone } from '@angular/core';
import type { Config, IonicWindow } from '@ionic/angular/common';
import { setupConfig } from '@ionic/core';
import { applyPolyfills, defineCustomElements } from '@ionic/core/loader';
import { Config } from './providers/config';
import { IonicWindow } from './types/interfaces';
import { raf } from './util/util';
// TODO(FW-2827): types

View File

@@ -1,41 +1,23 @@
import { Directive, HostListener, Input, Optional } from '@angular/core';
import { AnimationBuilder } from '@ionic/core';
import { Config } from '../../providers/config';
import { NavController } from '../../providers/nav-controller';
import { Optional, ElementRef, NgZone, ChangeDetectorRef, Component, ChangeDetectionStrategy } from '@angular/core';
import { IonBackButton as IonBackButtonBase, NavController, Config } from '@ionic/angular/common';
import { IonRouterOutlet } from './ion-router-outlet';
@Directive({
@Component({
selector: 'ion-back-button',
template: '<ng-content></ng-content>',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class IonBackButtonDelegateDirective {
@Input()
defaultHref: string | undefined | null;
@Input()
routerAnimation?: AnimationBuilder;
// eslint-disable-next-line @angular-eslint/directive-class-suffix
export class IonBackButton extends IonBackButtonBase {
constructor(
@Optional() private routerOutlet: IonRouterOutlet,
private navCtrl: NavController,
private config: Config
) {}
/**
* @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();
}
@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,29 @@
import {
ElementRef,
Injector,
EnvironmentInjector,
NgZone,
ChangeDetectorRef,
Component,
ChangeDetectionStrategy,
} from '@angular/core';
import { IonNav as IonNavBase, AngularDelegate } from '@ionic/angular/common';
@Component({
selector: 'ion-nav',
template: '<ng-content></ng-content>',
changeDetection: ChangeDetectionStrategy.OnPush,
})
// eslint-disable-next-line @angular-eslint/directive-class-suffix
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

@@ -1,520 +1,8 @@
import { Location } from '@angular/common';
import {
ComponentRef,
ElementRef,
Injector,
NgZone,
OnDestroy,
OnInit,
ViewContainerRef,
inject,
Attribute,
Directive,
EventEmitter,
Optional,
Output,
SkipSelf,
EnvironmentInjector,
Input,
InjectionToken,
Injectable,
reflectComponentType,
} from '@angular/core';
import { OutletContext, Router, ActivatedRoute, ChildrenOutletContexts, PRIMARY_OUTLET, Data } from '@angular/router';
import { componentOnReady } from '@ionic/core';
import { Observable, BehaviorSubject, Subscription, combineLatest, of } from 'rxjs';
import { distinctUntilChanged, filter, switchMap } from 'rxjs/operators';
import { AnimationBuilder } from '../../ionic-core';
import { Config } from '../../providers/config';
import { NavController } from '../../providers/nav-controller';
import { StackController } from './stack-controller';
import { RouteView, StackDidChangeEvent, StackWillChangeEvent, getUrl, isTabSwitch } from './stack-utils';
// TODO(FW-2827): types
import { Directive } from '@angular/core';
import { IonRouterOutlet as IonRouterOutletBase } from '@ionic/angular/common';
@Directive({
selector: 'ion-router-outlet',
exportAs: 'outlet',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['animated', 'animation', 'mode', 'swipeGesture'],
})
// eslint-disable-next-line @angular-eslint/directive-class-suffix
export class IonRouterOutlet implements OnDestroy, OnInit {
nativeEl: HTMLIonRouterOutletElement;
activatedView: RouteView | null = null;
tabsPrefix: string | undefined;
private _swipeGesture?: boolean;
private stackCtrl: StackController;
// Maintain map of activated route proxies for each component instance
private proxyMap = new WeakMap<any, ActivatedRoute>();
// Keep the latest activated route in a subject for the proxy routes to switch map to
private currentActivatedRoute$ = new BehaviorSubject<{ component: any; activatedRoute: ActivatedRoute } | null>(null);
private activated: ComponentRef<any> | null = null;
/** @internal */
get activatedComponentRef(): ComponentRef<any> | null {
return this.activated;
}
private _activatedRoute: ActivatedRoute | null = null;
/**
* The name of the outlet
*/
@Input() name = PRIMARY_OUTLET;
/** @internal */
@Output() stackWillChange = new EventEmitter<StackWillChangeEvent>();
/** @internal */
@Output() stackDidChange = new EventEmitter<StackDidChangeEvent>();
// eslint-disable-next-line @angular-eslint/no-output-rename
@Output('activate') activateEvents = new EventEmitter<any>();
// eslint-disable-next-line @angular-eslint/no-output-rename
@Output('deactivate') deactivateEvents = new EventEmitter<any>();
private parentContexts = inject(ChildrenOutletContexts);
private location = inject(ViewContainerRef);
private environmentInjector = inject(EnvironmentInjector);
private inputBinder = inject(INPUT_BINDER, { optional: true });
/** @nodoc */
readonly supportsBindingToComponentInputs = true;
// Ionic providers
private config = inject(Config);
private navCtrl = inject(NavController);
set animation(animation: AnimationBuilder) {
this.nativeEl.animation = animation;
}
set animated(animated: boolean) {
this.nativeEl.animated = animated;
}
set swipeGesture(swipe: boolean) {
this._swipeGesture = swipe;
this.nativeEl.swipeHandler = swipe
? {
canStart: () => this.stackCtrl.canGoBack(1) && !this.stackCtrl.hasRunningTask(),
onStart: () => this.stackCtrl.startBackTransition(),
onEnd: (shouldContinue) => this.stackCtrl.endBackTransition(shouldContinue),
}
: undefined;
}
constructor(
@Attribute('name') name: string,
@Optional() @Attribute('tabs') tabs: string,
commonLocation: Location,
elementRef: ElementRef,
router: Router,
zone: NgZone,
activatedRoute: ActivatedRoute,
@SkipSelf() @Optional() readonly parentOutlet?: IonRouterOutlet
) {
this.nativeEl = elementRef.nativeElement;
this.name = name || PRIMARY_OUTLET;
this.tabsPrefix = tabs === 'true' ? getUrl(router, activatedRoute) : undefined;
this.stackCtrl = new StackController(this.tabsPrefix, this.nativeEl, router, this.navCtrl, zone, commonLocation);
this.parentContexts.onChildOutletCreated(this.name, this as any);
}
ngOnDestroy(): void {
this.stackCtrl.destroy();
this.inputBinder?.unsubscribeFromRouteData(this);
}
getContext(): OutletContext | null {
return this.parentContexts.getContext(this.name);
}
ngOnInit(): void {
this.initializeOutletWithName();
}
// Note: Ionic deviates from the Angular Router implementation here
private initializeOutletWithName() {
if (!this.activated) {
// If the outlet was not instantiated at the time the route got activated we need to populate
// the outlet when it is initialized (ie inside a NgIf)
const context = this.getContext();
if (context?.route) {
this.activateWith(context.route, context.injector);
}
}
new Promise((resolve) => componentOnReady(this.nativeEl, resolve)).then(() => {
if (this._swipeGesture === undefined) {
this.swipeGesture = this.config.getBoolean('swipeBackEnabled', (this.nativeEl as any).mode === 'ios');
}
});
}
get isActivated(): boolean {
return !!this.activated;
}
get component(): Record<string, unknown> {
if (!this.activated) {
throw new Error('Outlet is not activated');
}
return this.activated.instance;
}
get activatedRoute(): ActivatedRoute {
if (!this.activated) {
throw new Error('Outlet is not activated');
}
return this._activatedRoute as ActivatedRoute;
}
get activatedRouteData(): Data {
if (this._activatedRoute) {
return this._activatedRoute.snapshot.data;
}
return {};
}
/**
* Called when the `RouteReuseStrategy` instructs to detach the subtree
*/
detach(): ComponentRef<any> {
throw new Error('incompatible reuse strategy');
}
/**
* Called when the `RouteReuseStrategy` instructs to re-attach a previously detached subtree
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
attach(_ref: ComponentRef<any>, _activatedRoute: ActivatedRoute): void {
throw new Error('incompatible reuse strategy');
}
deactivate(): void {
if (this.activated) {
if (this.activatedView) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const context = this.getContext()!;
this.activatedView.savedData = new Map(context.children['contexts']);
/**
* Angular v11.2.10 introduced a change
* where this route context is cleared out when
* a router-outlet is deactivated, However,
* we need this route information in order to
* return a user back to the correct tab when
* leaving and then going back to the tab context.
*/
const primaryOutlet = this.activatedView.savedData.get('primary');
if (primaryOutlet && context.route) {
primaryOutlet.route = { ...context.route };
}
/**
* Ensure we are saving the NavigationExtras
* data otherwise it will be lost
*/
this.activatedView.savedExtras = {};
if (context.route) {
const contextSnapshot = context.route.snapshot;
this.activatedView.savedExtras.queryParams = contextSnapshot.queryParams;
(this.activatedView.savedExtras.fragment as string | null) = contextSnapshot.fragment;
}
}
const c = this.component;
this.activatedView = null;
this.activated = null;
this._activatedRoute = null;
this.deactivateEvents.emit(c);
}
}
activateWith(activatedRoute: ActivatedRoute, environmentInjector: EnvironmentInjector | null): void {
if (this.isActivated) {
throw new Error('Cannot activate an already activated outlet');
}
this._activatedRoute = activatedRoute;
let cmpRef: any;
let enteringView = this.stackCtrl.getExistingView(activatedRoute);
if (enteringView) {
cmpRef = this.activated = enteringView.ref;
const saved = enteringView.savedData;
if (saved) {
// self-restore
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const context = this.getContext()!;
context.children['contexts'] = saved;
}
// Updated activated route proxy for this component
this.updateActivatedRouteProxy(cmpRef.instance, activatedRoute);
} else {
const snapshot = (activatedRoute as any)._futureSnapshot;
/**
* Angular 14 introduces a new `loadComponent` property to the route config.
* This function will assign a `component` property to the route snapshot.
* We check for the presence of this property to determine if the route is
* using standalone components.
*/
const childContexts = this.parentContexts.getOrCreateContext(this.name).children;
// We create an activated route proxy object that will maintain future updates for this component
// over its lifecycle in the stack.
const component$ = new BehaviorSubject<any>(null);
const activatedRouteProxy = this.createActivatedRouteProxy(component$, activatedRoute);
const injector = new OutletInjector(activatedRouteProxy, childContexts, this.location.injector);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const component = snapshot.routeConfig!.component ?? snapshot.component;
cmpRef = this.activated = this.location.createComponent(component, {
index: this.location.length,
injector,
environmentInjector: environmentInjector ?? this.environmentInjector,
});
// Once the component is created we can push it to our local subject supplied to the proxy
component$.next(cmpRef.instance);
// Calling `markForCheck` to make sure we will run the change detection when the
// `RouterOutlet` is inside a `ChangeDetectionStrategy.OnPush` component.
enteringView = this.stackCtrl.createView(this.activated, activatedRoute);
// Store references to the proxy by component
this.proxyMap.set(cmpRef.instance, activatedRouteProxy);
this.currentActivatedRoute$.next({ component: cmpRef.instance, activatedRoute });
}
this.inputBinder?.bindActivatedRouteToOutletComponent(this);
this.activatedView = enteringView;
/**
* The top outlet is set prior to the entering view's transition completing,
* so that when we have nested outlets (e.g. ion-tabs inside an ion-router-outlet),
* the tabs outlet will be assigned as the top outlet when a view inside tabs is
* activated.
*
* In this scenario, activeWith is called for both the tabs and the root router outlet.
* To avoid a race condition, we assign the top outlet synchronously.
*/
this.navCtrl.setTopOutlet(this);
const leavingView = this.stackCtrl.getActiveView();
this.stackWillChange.emit({
enteringView,
tabSwitch: isTabSwitch(enteringView, leavingView),
});
this.stackCtrl.setActive(enteringView).then((data) => {
this.activateEvents.emit(cmpRef.instance);
this.stackDidChange.emit(data);
});
}
/**
* Returns `true` if there are pages in the stack to go back.
*/
canGoBack(deep = 1, stackId?: string): boolean {
return this.stackCtrl.canGoBack(deep, stackId);
}
/**
* Resolves to `true` if it the outlet was able to sucessfully pop the last N pages.
*/
pop(deep = 1, stackId?: string): Promise<boolean> {
return this.stackCtrl.pop(deep, stackId);
}
/**
* Returns the URL of the active page of each stack.
*/
getLastUrl(stackId?: string): string | undefined {
const active = this.stackCtrl.getLastUrl(stackId);
return active ? active.url : undefined;
}
/**
* Returns the RouteView of the active page of each stack.
* @internal
*/
getLastRouteView(stackId?: string): RouteView | undefined {
return this.stackCtrl.getLastUrl(stackId);
}
/**
* Returns the root view in the tab stack.
* @internal
*/
getRootView(stackId?: string): RouteView | undefined {
return this.stackCtrl.getRootUrl(stackId);
}
/**
* Returns the active stack ID. In the context of ion-tabs, it means the active tab.
*/
getActiveStackId(): string | undefined {
return this.stackCtrl.getActiveStackId();
}
/**
* Since the activated route can change over the life time of a component in an ion router outlet, we create
* a proxy so that we can update the values over time as a user navigates back to components already in the stack.
*/
private createActivatedRouteProxy(component$: Observable<any>, activatedRoute: ActivatedRoute): ActivatedRoute {
const proxy: any = new ActivatedRoute();
proxy._futureSnapshot = (activatedRoute as any)._futureSnapshot;
proxy._routerState = (activatedRoute as any)._routerState;
proxy.snapshot = activatedRoute.snapshot;
proxy.outlet = activatedRoute.outlet;
proxy.component = activatedRoute.component;
// Setup wrappers for the observables so consumers don't have to worry about switching to new observables as the state updates
(proxy as any)._paramMap = this.proxyObservable(component$, 'paramMap');
(proxy as any)._queryParamMap = this.proxyObservable(component$, 'queryParamMap');
proxy.url = this.proxyObservable(component$, 'url');
proxy.params = this.proxyObservable(component$, 'params');
proxy.queryParams = this.proxyObservable(component$, 'queryParams');
proxy.fragment = this.proxyObservable(component$, 'fragment');
proxy.data = this.proxyObservable(component$, 'data');
return proxy as ActivatedRoute;
}
/**
* Create a wrapped observable that will switch to the latest activated route matched by the given component
*/
private proxyObservable(component$: Observable<any>, path: string): Observable<any> {
return component$.pipe(
// First wait until the component instance is pushed
filter((component) => !!component),
switchMap((component) =>
this.currentActivatedRoute$.pipe(
filter((current) => current !== null && current.component === component),
switchMap((current) => current && (current.activatedRoute as any)[path]),
distinctUntilChanged()
)
)
);
}
/**
* Updates the activated route proxy for the given component to the new incoming router state
*/
private updateActivatedRouteProxy(component: any, activatedRoute: ActivatedRoute): void {
const proxy = this.proxyMap.get(component);
if (!proxy) {
throw new Error(`Could not find activated route proxy for view`);
}
(proxy as any)._futureSnapshot = (activatedRoute as any)._futureSnapshot;
(proxy as any)._routerState = (activatedRoute as any)._routerState;
proxy.snapshot = activatedRoute.snapshot;
proxy.outlet = activatedRoute.outlet;
proxy.component = activatedRoute.component;
this.currentActivatedRoute$.next({ component, activatedRoute });
}
}
class OutletInjector implements Injector {
constructor(private route: ActivatedRoute, private childContexts: ChildrenOutletContexts, private parent: Injector) {}
get(token: any, notFoundValue?: any): any {
if (token === ActivatedRoute) {
return this.route;
}
if (token === ChildrenOutletContexts) {
return this.childContexts;
}
return this.parent.get(token, notFoundValue);
}
}
// TODO: FW-4785 - Remove this once Angular 15 support is dropped
export const INPUT_BINDER = new InjectionToken<RoutedComponentInputBinder>('');
/**
* Injectable used as a tree-shakable provider for opting in to binding router data to component
* inputs.
*
* The RouterOutlet registers itself with this service when an `ActivatedRoute` is attached or
* activated. When this happens, the service subscribes to the `ActivatedRoute` observables (params,
* queryParams, data) and sets the inputs of the component using `ComponentRef.setInput`.
* Importantly, when an input does not have an item in the route data with a matching key, this
* input is set to `undefined`. If it were not done this way, the previous information would be
* retained if the data got removed from the route (i.e. if a query parameter is removed).
*
* The `RouterOutlet` should unregister itself when destroyed via `unsubscribeFromRouteData` so that
* the subscriptions are cleaned up.
*/
@Injectable()
export class RoutedComponentInputBinder {
private outletDataSubscriptions = new Map<IonRouterOutlet, Subscription>();
bindActivatedRouteToOutletComponent(outlet: IonRouterOutlet): void {
this.unsubscribeFromRouteData(outlet);
this.subscribeToRouteData(outlet);
}
unsubscribeFromRouteData(outlet: IonRouterOutlet): void {
this.outletDataSubscriptions.get(outlet)?.unsubscribe();
this.outletDataSubscriptions.delete(outlet);
}
private subscribeToRouteData(outlet: IonRouterOutlet) {
const { activatedRoute } = outlet;
const dataSubscription = combineLatest([activatedRoute.queryParams, activatedRoute.params, activatedRoute.data])
.pipe(
switchMap(([queryParams, params, data], index) => {
data = { ...queryParams, ...params, ...data };
// Get the first result from the data subscription synchronously so it's available to
// the component as soon as possible (and doesn't require a second change detection).
if (index === 0) {
return of(data);
}
// Promise.resolve is used to avoid synchronously writing the wrong data when
// two of the Observables in the `combineLatest` stream emit one after
// another.
return Promise.resolve(data);
})
)
.subscribe((data) => {
// Outlet may have been deactivated or changed names to be associated with a different
// route
if (
!outlet.isActivated ||
!outlet.activatedComponentRef ||
outlet.activatedRoute !== activatedRoute ||
activatedRoute.component === null
) {
this.unsubscribeFromRouteData(outlet);
return;
}
const mirror = reflectComponentType(activatedRoute.component);
if (!mirror) {
this.unsubscribeFromRouteData(outlet);
return;
}
for (const { templateName } of mirror.inputs) {
outlet.activatedComponentRef.setInput(templateName, data[templateName]);
}
});
this.outletDataSubscriptions.set(outlet, dataSubscription);
}
}
export class IonRouterOutlet extends IonRouterOutletBase {}

View File

@@ -1,22 +1,9 @@
import {
AfterContentChecked,
AfterContentInit,
Component,
ContentChild,
ContentChildren,
ElementRef,
EventEmitter,
HostListener,
Output,
QueryList,
ViewChild,
} from '@angular/core';
import { Component, ContentChild, ContentChildren, ViewChild, QueryList } from '@angular/core';
import { IonTabs as IonTabsBase } from '@ionic/angular/common';
import { NavController } from '../../providers/nav-controller';
import { IonTabBar } from '../proxies';
import { IonRouterOutlet } from './ion-router-outlet';
import { StackDidChangeEvent, StackWillChangeEvent } from './stack-utils';
@Component({
selector: 'ion-tabs',
@@ -60,172 +47,9 @@ import { StackDidChangeEvent, StackWillChangeEvent } from './stack-utils';
],
})
// eslint-disable-next-line @angular-eslint/component-class-suffix
export class IonTabs implements AfterContentInit, AfterContentChecked {
export class IonTabs extends IonTabsBase {
@ViewChild('outlet', { read: IonRouterOutlet, static: false }) outlet: IonRouterOutlet;
@ViewChild('tabsInner', { read: ElementRef, static: true }) tabsInner: ElementRef<HTMLDivElement>;
@ContentChild(IonTabBar, { static: false }) tabBar: IonTabBar | undefined;
@ContentChildren(IonTabBar) tabBars: QueryList<IonTabBar>;
/**
* Emitted before the tab view is changed.
*/
@Output() ionTabsWillChange = new EventEmitter<{ tab: string }>();
/**
* Emitted after the tab view is changed.
*/
@Output() ionTabsDidChange = new EventEmitter<{ tab: string }>();
private tabBarSlot = 'bottom';
constructor(private navCtrl: NavController) {}
ngAfterContentInit(): void {
this.detectSlotChanges();
}
ngAfterContentChecked(): void {
this.detectSlotChanges();
}
/**
* @internal
*/
onStackWillChange({ enteringView, tabSwitch }: StackWillChangeEvent): void {
const stackId = enteringView.stackId;
if (tabSwitch && stackId !== undefined) {
this.ionTabsWillChange.emit({ tab: stackId });
}
}
/**
* @internal
*/
onStackDidChange({ enteringView, tabSwitch }: StackDidChangeEvent): void {
const stackId = enteringView.stackId;
if (tabSwitch && stackId !== undefined) {
if (this.tabBar) {
this.tabBar.selectedTab = stackId;
}
this.ionTabsDidChange.emit({ tab: stackId });
}
}
/**
* When a tab button is clicked, there are several scenarios:
* 1. If the selected tab is currently active (the tab button has been clicked
* again), then it should go to the root view for that tab.
*
* a. Get the saved root view from the router outlet. If the saved root view
* matches the tabRootUrl, set the route view to this view including the
* navigation extras.
* b. If the saved root view from the router outlet does
* not match, navigate to the tabRootUrl. No navigation extras are
* included.
*
* 2. If the current tab tab is not currently selected, get the last route
* view from the router outlet.
*
* a. If the last route view exists, navigate to that view including any
* navigation extras
* b. If the last route view doesn't exist, then navigate
* to the default tabRootUrl
*/
@HostListener('ionTabButtonClick', ['$event'])
select(tabOrEvent: string | CustomEvent): Promise<boolean> | undefined {
const isTabString = typeof tabOrEvent === 'string';
const tab = isTabString ? tabOrEvent : (tabOrEvent as CustomEvent).detail.tab;
const alreadySelected = this.outlet.getActiveStackId() === tab;
const tabRootUrl = `${this.outlet.tabsPrefix}/${tab}`;
/**
* If this is a nested tab, prevent the event
* from bubbling otherwise the outer tabs
* will respond to this event too, causing
* the app to get directed to the wrong place.
*/
if (!isTabString) {
(tabOrEvent as CustomEvent).stopPropagation();
}
if (alreadySelected) {
const activeStackId = this.outlet.getActiveStackId();
const activeView = this.outlet.getLastRouteView(activeStackId);
// If on root tab, do not navigate to root tab again
if (activeView?.url === tabRootUrl) {
return;
}
const rootView = this.outlet.getRootView(tab);
const navigationExtras = rootView && tabRootUrl === rootView.url && rootView.savedExtras;
return this.navCtrl.navigateRoot(tabRootUrl, {
...navigationExtras,
animated: true,
animationDirection: 'back',
});
} else {
const lastRoute = this.outlet.getLastRouteView(tab);
/**
* If there is a lastRoute, goto that, otherwise goto the fallback url of the
* selected tab
*/
const url = lastRoute?.url || tabRootUrl;
const navigationExtras = lastRoute?.savedExtras;
return this.navCtrl.navigateRoot(url, {
...navigationExtras,
animated: true,
animationDirection: 'back',
});
}
}
getSelected(): string | undefined {
return this.outlet.getActiveStackId();
}
/**
* Detects changes to the slot attribute of the tab bar.
*
* If the slot attribute has changed, then the tab bar
* should be relocated to the new slot position.
*/
private detectSlotChanges(): void {
this.tabBars.forEach((tabBar: any) => {
// el is a protected attribute from the generated component wrapper
const currentSlot = tabBar.el.getAttribute('slot');
if (currentSlot !== this.tabBarSlot) {
this.tabBarSlot = currentSlot;
this.relocateTabBar();
}
});
}
/**
* Relocates the tab bar to the new slot position.
*/
private relocateTabBar(): void {
/**
* `el` is a protected attribute from the generated component wrapper.
* To avoid having to manually create the wrapper for tab bar, we
* cast the tab bar to any and access the protected attribute.
*/
const tabBar = (this.tabBar as any).el as HTMLElement;
if (this.tabBarSlot === 'top') {
/**
* A tab bar with a slot of "top" should be inserted
* at the top of the container.
*/
this.tabsInner.nativeElement.before(tabBar);
} else {
/**
* A tab bar with a slot of "bottom" or without a slot
* should be inserted at the end of the container.
*/
this.tabsInner.nativeElement.after(tabBar);
}
}
}

View File

@@ -1,40 +0,0 @@
import { ElementRef, Injector, Directive, EnvironmentInjector } from '@angular/core';
import { AngularDelegate } from '../../providers/angular-delegate';
import { ProxyCmp, proxyOutputs } from '../angular-component-lib/utils';
@ProxyCmp({
inputs: ['animated', 'animation', 'root', 'rootParams', 'swipeGesture'],
methods: [
'push',
'insert',
'insertPages',
'pop',
'popTo',
'popToRoot',
'removeIndex',
'setRoot',
'setPages',
'getActive',
'getByIndex',
'canGoBack',
'getPrevious',
],
})
@Directive({
selector: 'ion-nav',
})
// eslint-disable-next-line @angular-eslint/directive-class-suffix
export class NavDelegate {
protected el: HTMLElement;
constructor(
ref: ElementRef,
environmentInjector: EnvironmentInjector,
injector: Injector,
angularDelegate: AngularDelegate
) {
this.el = ref.nativeElement;
ref.nativeElement.delegate = angularDelegate.create(environmentInjector, injector);
proxyOutputs(this, this.el, ['ionNavDidChange', 'ionNavWillChange']);
}
}

View File

@@ -1,9 +1,8 @@
import { LocationStrategy } from '@angular/common';
import { ElementRef, OnChanges, OnInit, Directive, HostListener, Input, Optional } from '@angular/core';
import { Router, RouterLink } from '@angular/router';
import { AnimationBuilder, RouterDirection } from '@ionic/core';
import { NavController } from '../../providers/nav-controller';
import { Directive } from '@angular/core';
import {
RouterLinkDelegateDirective as RouterLinkDelegateBase,
RouterLinkWithHrefDelegateDirective as RouterLinkHrefDelegateBase,
} from '@ionic/angular/common';
/**
* Adds support for Ionic routing directions and animations to the base Angular router link directive.
@@ -14,93 +13,9 @@ import { NavController } from '../../providers/nav-controller';
@Directive({
selector: ':not(a):not(area)[routerLink]',
})
export class RouterLinkDelegateDirective implements OnInit, OnChanges {
@Input()
routerDirection: RouterDirection = 'forward';
@Input()
routerAnimation?: AnimationBuilder;
constructor(
private locationStrategy: LocationStrategy,
private navCtrl: NavController,
private elementRef: ElementRef,
private router: Router,
@Optional() private routerLink?: RouterLink
) {}
ngOnInit(): void {
this.updateTargetUrlAndHref();
}
ngOnChanges(): void {
this.updateTargetUrlAndHref();
}
private updateTargetUrlAndHref() {
if (this.routerLink?.urlTree) {
const href = this.locationStrategy.prepareExternalUrl(this.router.serializeUrl(this.routerLink.urlTree));
this.elementRef.nativeElement.href = href;
}
}
/**
* @internal
*/
@HostListener('click', ['$event'])
onClick(ev: UIEvent): void {
this.navCtrl.setDirection(this.routerDirection, undefined, undefined, this.routerAnimation);
/**
* This prevents the browser from
* performing a page reload when pressing
* an Ionic component with routerLink.
* The page reload interferes with routing
* and causes ion-back-button to disappear
* since the local history is wiped on reload.
*/
ev.preventDefault();
}
}
export class RouterLinkDelegateDirective extends RouterLinkDelegateBase {}
@Directive({
selector: 'a[routerLink],area[routerLink]',
})
export class RouterLinkWithHrefDelegateDirective implements OnInit, OnChanges {
@Input()
routerDirection: RouterDirection = 'forward';
@Input()
routerAnimation?: AnimationBuilder;
constructor(
private locationStrategy: LocationStrategy,
private navCtrl: NavController,
private elementRef: ElementRef,
private router: Router,
@Optional() private routerLink?: RouterLink
) {}
ngOnInit(): void {
this.updateTargetUrlAndHref();
}
ngOnChanges(): void {
this.updateTargetUrlAndHref();
}
private updateTargetUrlAndHref() {
if (this.routerLink?.urlTree) {
const href = this.locationStrategy.prepareExternalUrl(this.router.serializeUrl(this.routerLink.urlTree));
this.elementRef.nativeElement.href = href;
}
}
/**
* @internal
*/
@HostListener('click')
onClick(): void {
this.navCtrl.setDirection(this.routerDirection, undefined, undefined, this.routerAnimation);
}
}
export class RouterLinkWithHrefDelegateDirective extends RouterLinkHrefDelegateBase {}

View File

@@ -1,139 +1,11 @@
/* eslint-disable */
/* tslint:disable */
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChild,
ElementRef,
EventEmitter,
NgZone,
TemplateRef,
} from '@angular/core';
import { ProxyCmp, proxyOutputs } from '../angular-component-lib/utils';
import { Components, ModalBreakpointChangeEventDetail } from '@ionic/core';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { IonModal as IonModalBase } from '@ionic/angular/common';
export declare interface IonModal extends Components.IonModal {
/**
* Emitted after the modal has presented.
**/
ionModalDidPresent: EventEmitter<CustomEvent>;
/**
* Emitted before the modal has presented.
*/
ionModalWillPresent: EventEmitter<CustomEvent>;
/**
* Emitted before the modal has dismissed.
*/
ionModalWillDismiss: EventEmitter<CustomEvent>;
/**
* Emitted after the modal has dismissed.
*/
ionModalDidDismiss: EventEmitter<CustomEvent>;
/**
* Emitted after the modal breakpoint has changed.
*/
ionBreakpointDidChange: EventEmitter<CustomEvent<ModalBreakpointChangeEventDetail>>;
/**
* Emitted after the modal has presented. Shorthand for ionModalWillDismiss.
*/
didPresent: EventEmitter<CustomEvent>;
/**
* Emitted before the modal has presented. Shorthand for ionModalWillPresent.
*/
willPresent: EventEmitter<CustomEvent>;
/**
* Emitted before the modal has dismissed. Shorthand for ionModalWillDismiss.
*/
willDismiss: EventEmitter<CustomEvent>;
/**
* Emitted after the modal has dismissed. Shorthand for ionModalDidDismiss.
*/
didDismiss: EventEmitter<CustomEvent>;
}
@ProxyCmp({
inputs: [
'animated',
'keepContentsMounted',
'backdropBreakpoint',
'backdropDismiss',
'breakpoints',
'canDismiss',
'cssClass',
'enterAnimation',
'event',
'handle',
'handleBehavior',
'initialBreakpoint',
'isOpen',
'keyboardClose',
'leaveAnimation',
'mode',
'presentingElement',
'showBackdrop',
'translucent',
'trigger',
],
methods: ['present', 'dismiss', 'onDidDismiss', 'onWillDismiss', 'setCurrentBreakpoint', 'getCurrentBreakpoint'],
})
@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>`,
inputs: [
'animated',
'keepContentsMounted',
'backdropBreakpoint',
'backdropDismiss',
'breakpoints',
'canDismiss',
'cssClass',
'enterAnimation',
'event',
'handle',
'handleBehavior',
'initialBreakpoint',
'isOpen',
'keyboardClose',
'leaveAnimation',
'mode',
'presentingElement',
'showBackdrop',
'translucent',
'trigger',
],
})
export class IonModal {
// TODO(FW-2827): type
@ContentChild(TemplateRef, { static: false }) template: TemplateRef<any>;
isCmpOpen: boolean = false;
protected el: HTMLElement;
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
this.el = r.nativeElement;
this.el.addEventListener('ionMount', () => {
this.isCmpOpen = true;
c.detectChanges();
});
this.el.addEventListener('didDismiss', () => {
this.isCmpOpen = false;
c.detectChanges();
});
proxyOutputs(this, this.el, [
'ionModalDidPresent',
'ionModalWillPresent',
'ionModalWillDismiss',
'ionModalDidDismiss',
'ionBreakpointDidChange',
'didPresent',
'willPresent',
'willDismiss',
'didDismiss',
]);
}
}
export class IonModal extends IonModalBase {}

View File

@@ -1,131 +1,9 @@
/* eslint-disable */
/* tslint:disable */
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChild,
ElementRef,
EventEmitter,
NgZone,
TemplateRef,
} from '@angular/core';
import { ProxyCmp, proxyOutputs } from '../angular-component-lib/utils';
import { Components } from '@ionic/core';
export declare interface IonPopover extends Components.IonPopover {
/**
* Emitted after the popover has presented.
*/
ionPopoverDidPresent: EventEmitter<CustomEvent>;
/**
* Emitted before the popover has presented.
*/
ionPopoverWillPresent: EventEmitter<CustomEvent>;
/**
* Emitted after the popover has dismissed.
*/
ionPopoverWillDismiss: EventEmitter<CustomEvent>;
/**
* Emitted after the popover has dismissed.
*/
ionPopoverDidDismiss: EventEmitter<CustomEvent>;
/**
* Emitted after the popover has presented. Shorthand for ionPopoverWillDismiss.
*/
didPresent: EventEmitter<CustomEvent>;
/**
* Emitted before the popover has presented. Shorthand for ionPopoverWillPresent.
*/
willPresent: EventEmitter<CustomEvent>;
/**
* Emitted after the popover has presented. Shorthand for ionPopoverWillDismiss.
*/
willDismiss: EventEmitter<CustomEvent>;
/**
* Emitted after the popover has dismissed. Shorthand for ionPopoverDidDismiss.
*/
didDismiss: EventEmitter<CustomEvent>;
}
@ProxyCmp({
inputs: [
'alignment',
'animated',
'arrow',
'keepContentsMounted',
'backdropDismiss',
'cssClass',
'dismissOnSelect',
'enterAnimation',
'event',
'isOpen',
'keyboardClose',
'leaveAnimation',
'mode',
'showBackdrop',
'translucent',
'trigger',
'triggerAction',
'reference',
'size',
'side',
],
methods: ['present', 'dismiss', 'onDidDismiss', 'onWillDismiss'],
})
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { IonPopover as IonPopoverBase } from '@ionic/angular/common';
@Component({
selector: 'ion-popover',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<ng-container [ngTemplateOutlet]="template" *ngIf="isCmpOpen || keepContentsMounted"></ng-container>`,
inputs: [
'alignment',
'animated',
'arrow',
'keepContentsMounted',
'backdropDismiss',
'cssClass',
'dismissOnSelect',
'enterAnimation',
'event',
'isOpen',
'keyboardClose',
'leaveAnimation',
'mode',
'showBackdrop',
'translucent',
'trigger',
'triggerAction',
'reference',
'size',
'side',
],
})
export class IonPopover {
// TODO(FW-2827): type
@ContentChild(TemplateRef, { static: false }) template: TemplateRef<any>;
isCmpOpen: boolean = false;
protected el: HTMLElement;
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
this.el = r.nativeElement;
this.el.addEventListener('ionMount', () => {
this.isCmpOpen = true;
c.detectChanges();
});
this.el.addEventListener('didDismiss', () => {
this.isCmpOpen = false;
c.detectChanges();
});
proxyOutputs(this, this.el, [
'ionPopoverDidPresent',
'ionPopoverWillPresent',
'ionPopoverWillDismiss',
'ionPopoverDidDismiss',
'didPresent',
'willPresent',
'willDismiss',
'didDismiss',
]);
}
}
export class IonPopover extends IonPopoverBase {}

View File

@@ -8,7 +8,6 @@ export const DIRECTIVES = [
d.IonAlert,
d.IonApp,
d.IonAvatar,
d.IonBackButton,
d.IonBackdrop,
d.IonBadge,
d.IonBreadcrumb,
@@ -50,7 +49,6 @@ export const DIRECTIVES = [
d.IonMenu,
d.IonMenuButton,
d.IonMenuToggle,
d.IonNav,
d.IonNavLink,
d.IonNote,
d.IonPicker,

View File

@@ -230,28 +230,6 @@ export class IonAvatar {
export declare interface IonAvatar extends Components.IonAvatar {}
@ProxyCmp({
inputs: ['color', 'defaultHref', 'disabled', 'icon', 'mode', 'routerAnimation', 'text', 'type']
})
@Component({
selector: 'ion-back-button',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['color', 'defaultHref', 'disabled', 'icon', 'mode', 'routerAnimation', 'text', 'type'],
})
export class IonBackButton {
protected el: HTMLElement;
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
c.detach();
this.el = r.nativeElement;
}
}
export declare interface IonBackButton extends Components.IonBackButton {}
@ProxyCmp({
inputs: ['stopPropagation', 'tappable', 'visible']
})
@@ -1395,39 +1373,6 @@ export class IonMenuToggle {
export declare interface IonMenuToggle extends Components.IonMenuToggle {}
@ProxyCmp({
inputs: ['animated', 'animation', 'root', 'rootParams', 'swipeGesture'],
methods: ['push', 'insert', 'insertPages', 'pop', 'popTo', 'popToRoot', 'removeIndex', 'setRoot', 'setPages', 'getActive', 'getByIndex', 'canGoBack', 'getPrevious']
})
@Component({
selector: 'ion-nav',
changeDetection: ChangeDetectionStrategy.OnPush,
template: '<ng-content></ng-content>',
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
inputs: ['animated', 'animation', 'root', 'rootParams', 'swipeGesture'],
})
export class IonNav {
protected el: HTMLElement;
constructor(c: ChangeDetectorRef, r: ElementRef, protected z: NgZone) {
c.detach();
this.el = r.nativeElement;
proxyOutputs(this, this.el, ['ionNavWillChange', 'ionNavDidChange']);
}
}
export declare interface IonNav extends Components.IonNav {
/**
* Event fired when the nav will change components
*/
ionNavWillChange: EventEmitter<CustomEvent<void>>;
/**
* Event fired when the nav has changed components
*/
ionNavDidChange: EventEmitter<CustomEvent<void>>;
}
@ProxyCmp({
inputs: ['component', 'componentProps', 'routerAnimation', 'routerDirection']
})

View File

@@ -5,39 +5,39 @@ export { RadioValueAccessorDirective as RadioValueAccessor } from './directives/
export { SelectValueAccessorDirective as SelectValueAccessor } from './directives/control-value-accessors/select-value-accessor';
export { TextValueAccessorDirective as TextValueAccessor } from './directives/control-value-accessors/text-value-accessor';
export { IonTabs } from './directives/navigation/ion-tabs';
export { IonBackButtonDelegateDirective as IonBackButtonDelegate } from './directives/navigation/ion-back-button';
export { NavDelegate } from './directives/navigation/nav-delegate';
export { IonBackButton } from './directives/navigation/ion-back-button';
export { IonNav } from './directives/navigation/ion-nav';
export { IonRouterOutlet } from './directives/navigation/ion-router-outlet';
export {
RouterLinkDelegateDirective as RouterLinkDelegate,
RouterLinkWithHrefDelegateDirective as RouterLinkWithHrefDelegate,
} from './directives/navigation/router-link-delegate';
export { NavParams } from './directives/navigation/nav-params';
export { IonModal } from './directives/overlays/modal';
export { IonPopover } from './directives/overlays/popover';
export * from './directives/proxies';
export * from './directives/validators';
// PROVIDERS
export { AngularDelegate } from './providers/angular-delegate';
export { ActionSheetController } from './providers/action-sheet-controller';
export { AlertController } from './providers/alert-controller';
export { LoadingController } from './providers/loading-controller';
export { MenuController } from './providers/menu-controller';
export { PickerController } from './providers/picker-controller';
export { ModalController } from './providers/modal-controller';
export { Platform } from './providers/platform';
export { PopoverController } from './providers/popover-controller';
export { ToastController } from './providers/toast-controller';
export { NavController } from './providers/nav-controller';
export { DomController } from './providers/dom-controller';
export { Config } from './providers/config';
export { AnimationController } from './providers/animation-controller';
export { GestureController } from './providers/gesture-controller';
// ROUTER STRATEGY
export { IonicRouteStrategy } from './util/ionic-router-reuse-strategy';
export {
ActionSheetController,
AlertController,
LoadingController,
MenuController,
ModalController,
PickerController,
PopoverController,
ToastController,
AnimationController,
GestureController,
DomController,
NavController,
Config,
Platform,
AngularDelegate,
NavParams,
IonicRouteStrategy,
} from '@ionic/angular/common';
// TYPES
export * from './types/ionic-lifecycle-hooks';

View File

@@ -1,6 +1,12 @@
import { CommonModule, DOCUMENT } from '@angular/common';
import { ModuleWithProviders, APP_INITIALIZER, NgModule, NgZone } from '@angular/core';
import { Router } from '@angular/router';
import {
ModalController,
PopoverController,
ConfigToken,
AngularDelegate,
provideComponentInputBinding,
} from '@ionic/angular/common';
import { IonicConfig } from '@ionic/core';
import { appInitialize } from './app-initialize';
@@ -11,10 +17,10 @@ import {
SelectValueAccessorDirective,
TextValueAccessorDirective,
} from './directives/control-value-accessors';
import { IonBackButtonDelegateDirective } from './directives/navigation/ion-back-button';
import { INPUT_BINDER, IonRouterOutlet, RoutedComponentInputBinder } from './directives/navigation/ion-router-outlet';
import { IonBackButton } from './directives/navigation/ion-back-button';
import { IonNav } from './directives/navigation/ion-nav';
import { IonRouterOutlet } from './directives/navigation/ion-router-outlet';
import { IonTabs } from './directives/navigation/ion-tabs';
import { NavDelegate } from './directives/navigation/nav-delegate';
import {
RouterLinkDelegateDirective,
RouterLinkWithHrefDelegateDirective,
@@ -23,10 +29,6 @@ import { IonModal } from './directives/overlays/modal';
import { IonPopover } from './directives/overlays/popover';
import { DIRECTIVES } from './directives/proxies-list';
import { IonMaxValidator, IonMinValidator } from './directives/validators';
import { AngularDelegate } from './providers/angular-delegate';
import { ConfigToken } from './providers/config';
import { ModalController } from './providers/modal-controller';
import { PopoverController } from './providers/popover-controller';
const DECLARATIONS = [
// generated proxies
@@ -46,8 +48,8 @@ const DECLARATIONS = [
// navigation
IonTabs,
IonRouterOutlet,
IonBackButtonDelegateDirective,
NavDelegate,
IonBackButton,
IonNav,
RouterLinkDelegateDirective,
RouterLinkWithHrefDelegateDirective,
@@ -77,23 +79,8 @@ export class IonicModule {
multi: true,
deps: [ConfigToken, DOCUMENT, NgZone],
},
{
provide: INPUT_BINDER,
useFactory: componentInputBindingFactory,
deps: [Router],
},
provideComponentInputBinding(),
],
};
}
}
function componentInputBindingFactory(router?: Router) {
/**
* We cast the router to any here, since the componentInputBindingEnabled
* property is not available until Angular v16.
*/
if ((router as any)?.componentInputBindingEnabled) {
return new RoutedComponentInputBinder();
}
return null;
}

View File

@@ -0,0 +1,5 @@
{
"lib": {
"entryFile": "src/index.ts"
},
}

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
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 { IonIcon } from './directives/icon';
export * from './directives/proxies';

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,21 @@
import { Component } from '@angular/core';
import {
RouterLinkDelegateDirective as RouterLinkDelegateBase,
RouterLinkWithHrefDelegateDirective as RouterLinkHrefDelegateBase,
} from '@ionic/angular/common';
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: ':not(a):not(area)[routerLink]',
template: '<ng-content></ng-content>',
standalone: true,
})
export class IonRouterLink extends RouterLinkDelegateBase {}
@Component({
// eslint-disable-next-line @angular-eslint/component-selector
selector: 'a[routerLink],area[routerLink]',
template: '<ng-content></ng-content>',
standalone: true,
})
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);
};
};

View File

@@ -70,6 +70,14 @@ If you want to add a version-specific change, add the change inside of the appro
If you need to add E2E tests that are only run on a specific version of the JS Framework, replicate the `VersionTest` component on each partial application. This ensures that tests for framework version X do not get run for framework version Y.
### Testing Lazy Loaded Ionic Components
Tests for lazy loaded Ionic UI components should only be added under the `/lazy` route. This ensures the `IonicModule` is added.
### Testing Standalone Ionic Components
Tests for standalone Ionic UI components should only be added under the `/standalone` route. This allows for an isolated environment where the lazy loaded `IonicModule` is not initialized. The standalone components use Stencil's custom element bundle instead of the lazy loaded bundle. If `IonicModule` is initialized then the Stencil components will fall back to using the lazy loaded implementation instead of the custom elements bundle implementation.
## Adding New Test Apps
As we add support for new versions of Angular, we will also need to update this directory to test against new applications. The following steps can serve as a guide for adding new apps:

View File

@@ -1,5 +1,5 @@
it("should be on Angular 14", () => {
cy.visit('/');
cy.visit('/lazy');
cy.get('ion-title').contains('Angular 14');
});

View File

@@ -22,7 +22,7 @@
"@ionic/angular-server": "^6.1.15",
"core-js": "^2.6.11",
"express": "^4.15.2",
"ionicons": "^6.0.4",
"ionicons": "^7.1.3-dev.11692630068.1f5f09ee",
"rxjs": "~7.5.0",
"tslib": "^2.3.0",
"typescript-eslint-language-service": "^4.1.5",
@@ -52,6 +52,9 @@
"wait-on": "^5.2.1",
"webpack": "^5.61.0",
"webpack-cli": "^4.9.2"
},
"engines": {
"node": ">= 16"
}
},
"node_modules/@ampproject/remapping": {
@@ -2928,6 +2931,14 @@
"tslib": "^2.1.0"
}
},
"node_modules/@ionic/core/node_modules/ionicons": {
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-6.1.3.tgz",
"integrity": "sha512-ptzz38dd/Yq+PgjhXegh7yhb/SLIk1bvL9vQDtLv1aoSc7alO6mX2DIMgcKYzt9vrNWkRu1f9Jr78zIFFyOXqw==",
"dependencies": {
"@stencil/core": "^2.18.0"
}
},
"node_modules/@istanbuljs/load-nyc-config": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@@ -9968,9 +9979,9 @@
}
},
"node_modules/ionicons": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-6.0.4.tgz",
"integrity": "sha512-uDNOkBo0OVYV+kIhb51g9mb7r3Z0b+78GPZQBsjXuaetNmrB/mNTqN/uFtO+vxL/rQySKjzk8qeKJI5NWL9Ueg==",
"version": "7.1.3-dev.11692630068.1f5f09ee",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.1.3-dev.11692630068.1f5f09ee.tgz",
"integrity": "sha512-UadZRbi50/IFJM5sIa3X7+c1sVsA0AB7uLrLyH7SRTX2MCol7clT1gfw/si28tG0NWScDyvbdC2SgzhN7lkroA==",
"dependencies": {
"@stencil/core": "^2.18.0"
}
@@ -18570,6 +18581,16 @@
"@stencil/core": "^2.16.0",
"ionicons": "^6.0.2",
"tslib": "^2.1.0"
},
"dependencies": {
"ionicons": {
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-6.1.3.tgz",
"integrity": "sha512-ptzz38dd/Yq+PgjhXegh7yhb/SLIk1bvL9vQDtLv1aoSc7alO6mX2DIMgcKYzt9vrNWkRu1f9Jr78zIFFyOXqw==",
"requires": {
"@stencil/core": "^2.18.0"
}
}
}
},
"@istanbuljs/load-nyc-config": {
@@ -23769,9 +23790,9 @@
"dev": true
},
"ionicons": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-6.0.4.tgz",
"integrity": "sha512-uDNOkBo0OVYV+kIhb51g9mb7r3Z0b+78GPZQBsjXuaetNmrB/mNTqN/uFtO+vxL/rQySKjzk8qeKJI5NWL9Ueg==",
"version": "7.1.3-dev.11692630068.1f5f09ee",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.1.3-dev.11692630068.1f5f09ee.tgz",
"integrity": "sha512-UadZRbi50/IFJM5sIa3X7+c1sVsA0AB7uLrLyH7SRTX2MCol7clT1gfw/si28tG0NWScDyvbdC2SgzhN7lkroA==",
"requires": {
"@stencil/core": "^2.18.0"
}

View File

@@ -33,7 +33,7 @@
"@ionic/angular-server": "^6.1.15",
"core-js": "^2.6.11",
"express": "^4.15.2",
"ionicons": "^6.0.4",
"ionicons": "7.1.3-dev.11692630068.1f5f09ee",
"rxjs": "~7.5.0",
"tslib": "^2.3.0",
"typescript-eslint-language-service": "^4.1.5",

View File

@@ -0,0 +1,23 @@
import { importProvidersFrom } from '@angular/core';
import { bootstrapApplication } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';
import { provideIonicAngular, IonicRouteStrategy } from '@ionic/angular/standalone';
import { AppComponentStandalone } from './app/app-standalone.component';
import { AppRoutingModule } from './app/app-routing.module';
import { routes } from './app/app.routes';
export const bootstrapStandalone = () => {
bootstrapApplication(AppComponentStandalone, {
providers: [
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
/**
* provideRouter is not available in Angular 14, so
* we fallback to using AppRoutingModule
*/
importProvidersFrom(AppRoutingModule),
provideIonicAngular({ keyboardHeight: 12345 })
],
});
}

View File

@@ -1,5 +1,5 @@
it("should be on Angular 15", () => {
cy.visit('/');
cy.visit('/lazy');
cy.get('ion-title').contains('Angular 15');
});

View File

@@ -23,7 +23,7 @@
"@nguniversal/express-engine": "^15.0.0",
"core-js": "^2.6.11",
"express": "^4.15.2",
"ionicons": "^6.0.4",
"ionicons": "7.1.3-dev.11692630068.1f5f09ee",
"rxjs": "~7.5.0",
"tslib": "^2.3.0",
"typescript-eslint-language-service": "^4.1.5",
@@ -53,6 +53,9 @@
"wait-on": "^5.2.1",
"webpack": "^5.61.0",
"webpack-cli": "^4.9.2"
},
"engines": {
"node": ">= 16"
}
},
"node_modules/@ampproject/remapping": {
@@ -2653,6 +2656,14 @@
"tslib": "^2.1.0"
}
},
"node_modules/@ionic/core/node_modules/ionicons": {
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-6.1.3.tgz",
"integrity": "sha512-ptzz38dd/Yq+PgjhXegh7yhb/SLIk1bvL9vQDtLv1aoSc7alO6mX2DIMgcKYzt9vrNWkRu1f9Jr78zIFFyOXqw==",
"dependencies": {
"@stencil/core": "^2.18.0"
}
},
"node_modules/@istanbuljs/load-nyc-config": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@@ -9560,9 +9571,9 @@
}
},
"node_modules/ionicons": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-6.0.4.tgz",
"integrity": "sha512-uDNOkBo0OVYV+kIhb51g9mb7r3Z0b+78GPZQBsjXuaetNmrB/mNTqN/uFtO+vxL/rQySKjzk8qeKJI5NWL9Ueg==",
"version": "7.1.3-dev.11692630068.1f5f09ee",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.1.3-dev.11692630068.1f5f09ee.tgz",
"integrity": "sha512-UadZRbi50/IFJM5sIa3X7+c1sVsA0AB7uLrLyH7SRTX2MCol7clT1gfw/si28tG0NWScDyvbdC2SgzhN7lkroA==",
"dependencies": {
"@stencil/core": "^2.18.0"
}
@@ -17575,6 +17586,16 @@
"@stencil/core": "^2.16.0",
"ionicons": "^6.0.2",
"tslib": "^2.1.0"
},
"dependencies": {
"ionicons": {
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-6.1.3.tgz",
"integrity": "sha512-ptzz38dd/Yq+PgjhXegh7yhb/SLIk1bvL9vQDtLv1aoSc7alO6mX2DIMgcKYzt9vrNWkRu1f9Jr78zIFFyOXqw==",
"requires": {
"@stencil/core": "^2.18.0"
}
}
}
},
"@istanbuljs/load-nyc-config": {
@@ -22669,9 +22690,9 @@
"dev": true
},
"ionicons": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-6.0.4.tgz",
"integrity": "sha512-uDNOkBo0OVYV+kIhb51g9mb7r3Z0b+78GPZQBsjXuaetNmrB/mNTqN/uFtO+vxL/rQySKjzk8qeKJI5NWL9Ueg==",
"version": "7.1.3-dev.11692630068.1f5f09ee",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.1.3-dev.11692630068.1f5f09ee.tgz",
"integrity": "sha512-UadZRbi50/IFJM5sIa3X7+c1sVsA0AB7uLrLyH7SRTX2MCol7clT1gfw/si28tG0NWScDyvbdC2SgzhN7lkroA==",
"requires": {
"@stencil/core": "^2.18.0"
}

View File

@@ -34,7 +34,7 @@
"@nguniversal/express-engine": "^15.0.0",
"core-js": "^2.6.11",
"express": "^4.15.2",
"ionicons": "^6.0.4",
"ionicons": "7.1.3-dev.11692630068.1f5f09ee",
"rxjs": "~7.5.0",
"tslib": "^2.3.0",
"typescript-eslint-language-service": "^4.1.5",

View File

@@ -1,5 +1,5 @@
it("should be on Angular 16", () => {
cy.visit('/');
cy.visit('/lazy');
cy.get('ion-title').contains('Angular 16');
});

View File

@@ -1,5 +1,5 @@
it("binding route data to inputs should work", () => {
cy.visit('/version-test/bind-route/test?query=test');
cy.visit('/lazy/version-test/bind-route/test?query=test');
cy.get('#route-params').contains('test');
cy.get('#query-params').contains('test');

View File

@@ -1,7 +1,7 @@
describe('Modal Nav Params', () => {
beforeEach(() => {
cy.visit('/version-test/modal-nav-params');
cy.visit('/lazy/version-test/modal-nav-params');
});
it('should assign the rootParams when presented in a modal multiple times', () => {

View File

@@ -22,7 +22,7 @@
"@nguniversal/express-engine": "^16.0.0",
"core-js": "^2.6.11",
"express": "^4.15.2",
"ionicons": "^7.0.4",
"ionicons": "7.1.3-dev.11692630068.1f5f09ee",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"typescript-eslint-language-service": "^4.1.5",
@@ -3230,6 +3230,14 @@
"zone.js": ">=0.11.0"
}
},
"node_modules/@ionic/angular/node_modules/ionicons": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.1.2.tgz",
"integrity": "sha512-zZ4njAqSP39H8RRvZhJvkHsv7cBjYE/VfInH218Osf2UVxJITSOutTTd25MW+tAXKN5fheYzclUXUsF55JHUDg==",
"dependencies": {
"@stencil/core": "^2.18.0"
}
},
"node_modules/@ionic/core": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-7.0.2.tgz",
@@ -3252,6 +3260,26 @@
"npm": ">=6.0.0"
}
},
"node_modules/@ionic/core/node_modules/ionicons": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.1.2.tgz",
"integrity": "sha512-zZ4njAqSP39H8RRvZhJvkHsv7cBjYE/VfInH218Osf2UVxJITSOutTTd25MW+tAXKN5fheYzclUXUsF55JHUDg==",
"dependencies": {
"@stencil/core": "^2.18.0"
}
},
"node_modules/@ionic/core/node_modules/ionicons/node_modules/@stencil/core": {
"version": "2.22.3",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-2.22.3.tgz",
"integrity": "sha512-kmVA0M/HojwsfkeHsifvHVIYe4l5tin7J5+DLgtl8h6WWfiMClND5K3ifCXXI2ETDNKiEk21p6jql3Fx9o2rng==",
"bin": {
"stencil": "bin/stencil"
},
"engines": {
"node": ">=12.10.0",
"npm": ">=6.0.0"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -10142,9 +10170,9 @@
}
},
"node_modules/ionicons": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.1.0.tgz",
"integrity": "sha512-iE4GuEdEHARJpp0sWL7WJZCzNCf5VxpNRhAjW0fLnZPnNL5qZOJUcfup2Z2Ty7Jk8Q5hacrHfGEB1lCwOdXqGg==",
"version": "7.1.3-dev.11692630068.1f5f09ee",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.1.3-dev.11692630068.1f5f09ee.tgz",
"integrity": "sha512-UadZRbi50/IFJM5sIa3X7+c1sVsA0AB7uLrLyH7SRTX2MCol7clT1gfw/si28tG0NWScDyvbdC2SgzhN7lkroA==",
"dependencies": {
"@stencil/core": "^2.18.0"
}
@@ -19155,6 +19183,16 @@
"ionicons": "^7.0.0",
"jsonc-parser": "^3.0.0",
"tslib": "^2.3.0"
},
"dependencies": {
"ionicons": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.1.2.tgz",
"integrity": "sha512-zZ4njAqSP39H8RRvZhJvkHsv7cBjYE/VfInH218Osf2UVxJITSOutTTd25MW+tAXKN5fheYzclUXUsF55JHUDg==",
"requires": {
"@stencil/core": "^2.18.0"
}
}
}
},
"@ionic/angular-server": {
@@ -19180,6 +19218,21 @@
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-3.2.1.tgz",
"integrity": "sha512-Ybm4NteQBScLq3H0JML/uqo4nWjNpZw1HAAURtR5LlRm7ptzNKO5S8EnHp3m05/uyTzeh9yLpUFHY7bxGNdYLg=="
},
"ionicons": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.1.2.tgz",
"integrity": "sha512-zZ4njAqSP39H8RRvZhJvkHsv7cBjYE/VfInH218Osf2UVxJITSOutTTd25MW+tAXKN5fheYzclUXUsF55JHUDg==",
"requires": {
"@stencil/core": "^2.18.0"
},
"dependencies": {
"@stencil/core": {
"version": "2.22.3",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-2.22.3.tgz",
"integrity": "sha512-kmVA0M/HojwsfkeHsifvHVIYe4l5tin7J5+DLgtl8h6WWfiMClND5K3ifCXXI2ETDNKiEk21p6jql3Fx9o2rng=="
}
}
}
}
},
@@ -24312,9 +24365,9 @@
"dev": true
},
"ionicons": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.1.0.tgz",
"integrity": "sha512-iE4GuEdEHARJpp0sWL7WJZCzNCf5VxpNRhAjW0fLnZPnNL5qZOJUcfup2Z2Ty7Jk8Q5hacrHfGEB1lCwOdXqGg==",
"version": "7.1.3-dev.11692630068.1f5f09ee",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.1.3-dev.11692630068.1f5f09ee.tgz",
"integrity": "sha512-UadZRbi50/IFJM5sIa3X7+c1sVsA0AB7uLrLyH7SRTX2MCol7clT1gfw/si28tG0NWScDyvbdC2SgzhN7lkroA==",
"requires": {
"@stencil/core": "^2.18.0"
}

View File

@@ -33,7 +33,7 @@
"@nguniversal/express-engine": "^16.0.0",
"core-js": "^2.6.11",
"express": "^4.15.2",
"ionicons": "^7.0.4",
"ionicons": "7.1.3-dev.11692630068.1f5f09ee",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"typescript-eslint-language-service": "^4.1.5",

View File

@@ -1,6 +1,6 @@
describe('Accordion', () => {
beforeEach(() => {
cy.visit('/accordions');
cy.visit('/lazy/accordions');
});
it('should correctly expand on multiple modal opens', () => {

View File

@@ -1,7 +1,7 @@
describe('Form Controls: Range', () => {
beforeEach(() => {
cy.visit('/form-controls/range');
cy.visit('/lazy/form-controls/range');
});
it('should have form control initial value', () => {

View File

@@ -1,6 +1,6 @@
describe('Form', () => {
beforeEach(() => {
cy.visit('/form');
cy.visit('/lazy/form');
})
describe('status updates', () => {

View File

@@ -1,6 +1,6 @@
describe('Overlays: Inline', () => {
beforeEach(() => {
cy.visit('/overlays-inline');
cy.visit('/lazy/overlays-inline');
});
describe('Alert', () => {

View File

@@ -1,6 +1,6 @@
describe('Inputs', () => {
beforeEach(() => {
cy.visit('/inputs');
cy.visit('/lazy/inputs');
})
it('should have default value', () => {

View File

@@ -1,19 +1,19 @@
describe('overlays - keepContentsMounted', () => {
describe('modal', () => {
it('should not mount component if false', () => {
cy.visit('/modal-inline');
cy.visit('/lazy/modal-inline');
cy.get('ion-modal ion-content').should('not.exist');
});
it('should mount component if true', () => {
cy.visit('/keep-contents-mounted');
cy.visit('/lazy/keep-contents-mounted');
cy.get('ion-modal ion-content').should('exist');
});
it('should keep component mounted after dismissing if true', () => {
cy.visit('/keep-contents-mounted');
cy.visit('/lazy/keep-contents-mounted');
cy.get('#open-modal').click();
@@ -29,26 +29,26 @@ describe('overlays - keepContentsMounted', () => {
});
it('should has ion-delegate-host on mount', () => {
cy.visit('/keep-contents-mounted');
cy.visit('/lazy/keep-contents-mounted');
cy.get('ion-modal .ion-delegate-host').should('exist');
});
})
describe('popover', () => {
it('should not mount component if false', () => {
cy.visit('/popover-inline');
cy.visit('/lazy/popover-inline');
cy.get('ion-popover ion-content').should('not.exist');
});
it('should mount component if true', () => {
cy.visit('/keep-contents-mounted');
cy.visit('/lazy/keep-contents-mounted');
cy.get('ion-popover ion-content').should('exist');
});
it('should keep component mounted after dismissing if true', () => {
cy.visit('/keep-contents-mounted');
cy.visit('/lazy/keep-contents-mounted');
cy.get('#open-popover').click();

View File

@@ -1,6 +1,6 @@
describe('Modals', () => {
beforeEach(() => {
cy.visit('/modals');
cy.visit('/lazy/modals');
})
it('should open standalone modal and close', () => {
@@ -45,7 +45,7 @@ describe('Modals', () => {
describe('Modals: Inline', () => {
beforeEach(() => {
cy.visit('/modal-inline');
cy.visit('/lazy/modal-inline');
});
it('should initially have no items', () => {
@@ -95,7 +95,7 @@ describe('Modals: Inline', () => {
describe('when in a modal', () => {
beforeEach(() => {
cy.visit('/modals');
cy.visit('/lazy/modals');
cy.get('#action-button').click();
cy.get('#close-modal').click();
cy.get('#action-button').click();

View File

@@ -1,10 +1,10 @@
describe('Navigation', () => {
beforeEach(() => {
cy.visit('/navigation');
cy.visit('/lazy/navigation');
})
it('should navigate correctly', () => {
cy.visit('/navigation/page1');
cy.visit('/lazy/navigation/page1');
cy.wait(2000);
cy.testStack('ion-router-outlet', ['app-navigation-page2', 'app-navigation-page1']);

View File

@@ -1,6 +1,6 @@
describe('Nested Outlet', () => {
beforeEach(() => {
cy.visit('/nested-outlet/page');
cy.visit('/lazy/nested-outlet/page');
})
it('should navigate correctly', () => {

View File

@@ -1,6 +1,6 @@
describe('Popovers: Inline', () => {
beforeEach(() => {
cy.visit('/popover-inline');
cy.visit('/lazy/popover-inline');
});
it('should initially have no items', () => {

View File

@@ -1,6 +1,6 @@
describe('Providers', () => {
beforeEach(() => {
cy.visit('/providers');
cy.visit('/lazy/providers');
})
it('should load all providers', () => {
@@ -17,13 +17,13 @@ describe('Providers', () => {
});
it('should detect testing mode', () => {
cy.visit('/providers?ionic:_testing=true');
cy.visit('/lazy/providers?ionic:_testing=true');
cy.get('#is-testing').should('have.text', 'true');
});
it('should get query params', () => {
cy.visit('/providers?firstParam=abc&secondParam=true');
cy.visit('/lazy/providers?firstParam=abc&secondParam=true');
cy.get('#query-params').should('have.text', 'firstParam: abc, firstParam: true');
})

View File

@@ -1,6 +1,6 @@
describe('Router Link', () => {
beforeEach(() => {
cy.visit('/router-link');
cy.visit('/lazy/router-link');
});
describe('router-link params and fragments', () => {
@@ -9,7 +9,7 @@ describe('Router Link', () => {
const id = 'MyPageID==';
it('should go to a page with properly encoded values', () => {
cy.visit('/router-link?ionic:_testing=true');
cy.visit('/lazy/router-link?ionic:_testing=true');
cy.get('#queryParamsFragment').click();
const expectedPath = `${encodeURIComponent(id)}`;
@@ -24,7 +24,7 @@ describe('Router Link', () => {
});
it('should return to a page with preserved query param and fragment', () => {
cy.visit('/router-link?ionic:_testing=true');
cy.visit('/lazy/router-link?ionic:_testing=true');
cy.get('#queryParamsFragment').click();
cy.get('#goToPage3').click();
@@ -46,7 +46,7 @@ describe('Router Link', () => {
});
it('should preserve query param and fragment with defaultHref string', () => {
cy.visit('/router-link-page3?ionic:_testing=true');
cy.visit('/lazy/router-link-page3?ionic:_testing=true');
cy.get('#goBackFromPage3').click();

View File

@@ -1,6 +1,6 @@
describe('Routing', () => {
beforeEach(() => {
cy.visit('/router-link?ionic:mode=ios');
cy.visit('/lazy/router-link?ionic:mode=ios');
})
it('should swipe and abort', () => {

View File

@@ -1,5 +1,5 @@
describe('Searchbar', () => {
beforeEach(() => cy.visit('/searchbar'));
beforeEach(() => cy.visit('/lazy/searchbar'));
it('should become valid', () => {
cy.get('#status').should('have.text', 'INVALID');

View File

@@ -1,6 +1,6 @@
describe('Routing with Standalone Components', () => {
beforeEach(() => {
cy.visit('/standalone');
cy.visit('/lazy/standalone');
});
it('should render the component', () => {

View File

@@ -1,6 +1,6 @@
describe('Tabs', () => {
beforeEach(() => {
cy.visit('/tabs');
cy.visit('/lazy/tabs');
})
describe('entry url - /tabs', () => {
@@ -214,7 +214,7 @@ describe('Tabs', () => {
describe('entry tab contains navigation extras', () => {
const expectNestedTabUrlToContain = 'search=hello#fragment';
const rootUrlParams = 'test=123#rootFragment';
const rootUrl = `/tabs/account?${rootUrlParams}`;
const rootUrl = `/lazy/tabs/account?${rootUrlParams}`;
beforeEach(() => {
cy.visit(rootUrl);
@@ -288,7 +288,7 @@ describe('Tabs', () => {
describe('entry url - /tabs/account', () => {
beforeEach(() => {
cy.visit('/tabs/account');
cy.visit('/lazy/tabs/account');
});
it('should pop to previous view when leaving tabs outlet', () => {
@@ -322,7 +322,7 @@ describe('Tabs', () => {
describe('entry url - /', () => {
it('should pop to the root outlet from the tabs outlet', () => {
cy.visit('/');
cy.visit('/lazy/');
cy.get('ion-title').should('contain.text', 'Test App');
@@ -356,7 +356,7 @@ describe('Tabs', () => {
describe('entry url - /tabs/account/nested/1', () => {
beforeEach(() => {
cy.visit('/tabs/account/nested/1');
cy.visit('/lazy/tabs/account/nested/1');
})
it('should only display the back-button when there is a page in the stack', () => {
@@ -401,7 +401,7 @@ describe('Tabs', () => {
describe('entry url - /tabs/lazy', () => {
beforeEach(() => {
cy.visit('/tabs/lazy');
cy.visit('/lazy/tabs/lazy');
});
it('should not display the back-button if coming from a different stack', () => {
@@ -419,7 +419,7 @@ describe('Tabs', () => {
describe('enter url - /tabs/contact/one', () => {
beforeEach(() => {
cy.visit('/tabs/contact/one');
cy.visit('/lazy/tabs/contact/one');
});
it('should return to correct tab after going to page in different outlet', () => {
@@ -436,7 +436,7 @@ describe('Tabs', () => {
})
it('Tabs should support conditional slots', () => {
cy.visit('/tabs-slots');
cy.visit('/lazy/tabs-slots');
cy.get('ion-tabs .tabs-inner + ion-tab-bar').should('have.length', 1);

View File

@@ -1,5 +1,5 @@
describe('Textarea', () => {
beforeEach(() => cy.visit('/textarea'));
beforeEach(() => cy.visit('/lazy/textarea'));
it('should become valid', () => {
cy.get('#status').should('have.text', 'INVALID');

View File

@@ -1,6 +1,6 @@
describe('View Child', () => {
beforeEach(() => {
cy.visit('/view-child');
cy.visit('/lazy/view-child');
})
it('should get a reference to all children', () => {

View File

@@ -0,0 +1,14 @@
describe('Back Button', () => {
beforeEach(() => {
cy.visit('/standalone/back-button');
})
it('should be visible and navigate back to page', () => {
cy.ionPageVisible('app-back-button');
cy.get('ion-back-button').click();
cy.ionPageDoesNotExist('app-back-button');
cy.ionPageVisible('app-router-outlet');
});
})

View File

@@ -0,0 +1,20 @@
describe('Icons', () => {
it('should render an icon', () => {
cy.visit('/standalone/icon');
cy.get('ion-icon#icon-string').shadow().find('svg').should('exist');
cy.get('ion-icon#icon-binding').shadow().find('svg').should('exist');
});
it('should render an icon on iOS mode', () => {
cy.visit('/standalone/icon?ionic:mode=ios');
cy.get('ion-icon#icon-mode').shadow().find('svg').should('exist');
});
it('should render an icon on MD mode', () => {
cy.visit('/standalone/icon?ionic:mode=md');
cy.get('ion-icon#icon-mode').shadow().find('svg').should('exist');
});
});

Some files were not shown because too many files have changed in this diff Show More