mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
Compare commits
22 Commits
patch-test
...
icons-test
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28deb56e9c | ||
|
|
3720ae6a09 | ||
|
|
01c34738a6 | ||
|
|
fd0f25aa72 | ||
|
|
0afa14ed7a | ||
|
|
e5b7f5ff55 | ||
|
|
e57759f4b6 | ||
|
|
ca53682684 | ||
|
|
c65e08dcd1 | ||
|
|
84212acce4 | ||
|
|
104b9547e5 | ||
|
|
e44a02632d | ||
|
|
c52a0972b9 | ||
|
|
8a97e406a0 | ||
|
|
37acdf9711 | ||
|
|
9f20780d66 | ||
|
|
3ea7488b47 | ||
|
|
de48493a16 | ||
|
|
5b31439ca0 | ||
|
|
291b1310a6 | ||
|
|
af68808e69 | ||
|
|
4553425502 |
14
core/package-lock.json
generated
14
core/package-lock.json
generated
@@ -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": {}
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -52,3 +52,23 @@ $ npx schematics @ionic/angular:ng-add
|
||||
|
||||
|
||||
You'll now be able to add ionic components to a vanilla Angular app setup.
|
||||
|
||||
## Project Structure
|
||||
|
||||
**common**
|
||||
|
||||
This is where logic that is shared between lazy loaded and standalone components live. For example, the lazy loaded IonPopover and standalone IonPopover components extend from a base IonPopover implementation that exists in this directory.
|
||||
|
||||
**Note:** This directory exposes internal APIs and is only accessed in the `standalone` and `src` submodules. Ionic developers should never import directly from `@ionic/angular/common`. Instead, they should import from `@ionic/angular` or `@ionic/angular/standalone`.
|
||||
|
||||
**standalone**
|
||||
|
||||
This is where the standalone component implementations live. It was added as a separate entry point to avoid any lazy loaded logic from accidentally being pulled in to the final build. Having a separate directory allows the lazy loaded implementation to remain accessible from `@ionic/angular` for backwards compatibility.
|
||||
|
||||
Ionic developers can access this by importing from `@ionic/angular/standalone`.
|
||||
|
||||
**src**
|
||||
|
||||
This is where the lazy loaded component implementations live.
|
||||
|
||||
Ionic developers can access this by importing from `@ionic/angular`.
|
||||
|
||||
5
packages/angular/common/ng-package.json
Normal file
5
packages/angular/common/ng-package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"lib": {
|
||||
"entryFile": "src/index.ts"
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { HostListener, Input, Optional, ElementRef, NgZone, ChangeDetectorRef, Directive } from '@angular/core';
|
||||
import type { Components } from '@ionic/core';
|
||||
import type { AnimationBuilder } from '@ionic/core/components';
|
||||
|
||||
import { Config } from '../../providers/config';
|
||||
import { NavController } from '../../providers/nav-controller';
|
||||
import { ProxyCmp } from '../../utils/proxy';
|
||||
|
||||
import { IonRouterOutlet } from './router-outlet';
|
||||
|
||||
const BACK_BUTTON_INPUTS = ['color', 'defaultHref', 'disabled', 'icon', 'mode', 'routerAnimation', 'text', 'type'];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export declare interface IonBackButton extends Components.IonBackButton {}
|
||||
|
||||
@ProxyCmp({
|
||||
inputs: BACK_BUTTON_INPUTS,
|
||||
})
|
||||
@Directive({
|
||||
// eslint-disable-next-line @angular-eslint/no-inputs-metadata-property
|
||||
inputs: BACK_BUTTON_INPUTS,
|
||||
})
|
||||
// eslint-disable-next-line @angular-eslint/directive-class-suffix
|
||||
export class IonBackButton {
|
||||
@Input()
|
||||
defaultHref: string | undefined;
|
||||
|
||||
@Input()
|
||||
routerAnimation: AnimationBuilder | undefined;
|
||||
|
||||
protected el: HTMLElement;
|
||||
|
||||
constructor(
|
||||
@Optional() private routerOutlet: IonRouterOutlet,
|
||||
private navCtrl: NavController,
|
||||
private config: Config,
|
||||
private r: ElementRef,
|
||||
protected z: NgZone,
|
||||
c: ChangeDetectorRef
|
||||
) {
|
||||
c.detach();
|
||||
this.el = this.r.nativeElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@HostListener('click', ['$event'])
|
||||
onClick(ev: Event): void {
|
||||
const defaultHref = this.defaultHref || this.config.get('backButtonDefaultHref');
|
||||
|
||||
if (this.routerOutlet?.canGoBack()) {
|
||||
this.navCtrl.setDirection('back', undefined, undefined, this.routerAnimation);
|
||||
this.routerOutlet.pop();
|
||||
ev.preventDefault();
|
||||
} else if (defaultHref != null) {
|
||||
this.navCtrl.navigateBack(defaultHref, { animation: this.routerAnimation });
|
||||
ev.preventDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
52
packages/angular/common/src/directives/navigation/nav.ts
Normal file
52
packages/angular/common/src/directives/navigation/nav.ts
Normal 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']);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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') {
|
||||
192
packages/angular/common/src/directives/navigation/tabs.ts
Normal file
192
packages/angular/common/src/directives/navigation/tabs.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
39
packages/angular/common/src/index.ts
Normal file
39
packages/angular/common/src/index.ts
Normal 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';
|
||||
133
packages/angular/common/src/overlays/modal.ts
Normal file
133
packages/angular/common/src/overlays/modal.ts
Normal 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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
121
packages/angular/common/src/overlays/popover.ts
Normal file
121
packages/angular/common/src/overlays/popover.ts
Normal 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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
@@ -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',
|
||||
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
@@ -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',
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { menuController } from '@ionic/core';
|
||||
import { menuController } from '@ionic/core/components';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
@@ -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
|
||||
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
@@ -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;
|
||||
}
|
||||
53
packages/angular/common/src/utils/proxy.ts
Normal file
53
packages/angular/common/src/utils/proxy.ts
Normal 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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
29
packages/angular/src/directives/navigation/ion-nav.ts
Normal file
29
packages/angular/src/directives/navigation/ion-nav.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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']
|
||||
})
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
5
packages/angular/standalone/ng-package.json
Normal file
5
packages/angular/standalone/ng-package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"lib": {
|
||||
"entryFile": "src/index.ts"
|
||||
},
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
24
packages/angular/standalone/src/directives/icon.ts
Normal file
24
packages/angular/standalone/src/directives/icon.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
3159
packages/angular/standalone/src/directives/proxies.ts
Normal file
3159
packages/angular/standalone/src/directives/proxies.ts
Normal file
File diff suppressed because it is too large
Load Diff
28
packages/angular/standalone/src/index.ts
Normal file
28
packages/angular/standalone/src/index.ts
Normal 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';
|
||||
28
packages/angular/standalone/src/navigation/back-button.ts
Normal file
28
packages/angular/standalone/src/navigation/back-button.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
24
packages/angular/standalone/src/navigation/nav.ts
Normal file
24
packages/angular/standalone/src/navigation/nav.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
13
packages/angular/standalone/src/navigation/router-outlet.ts
Normal file
13
packages/angular/standalone/src/navigation/router-outlet.ts
Normal 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 {}
|
||||
57
packages/angular/standalone/src/navigation/tabs.ts
Normal file
57
packages/angular/standalone/src/navigation/tabs.ts
Normal 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>;
|
||||
}
|
||||
18
packages/angular/standalone/src/overlays/modal.ts
Normal file
18
packages/angular/standalone/src/overlays/modal.ts
Normal 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 {}
|
||||
16
packages/angular/standalone/src/overlays/popover.ts
Normal file
16
packages/angular/standalone/src/overlays/popover.ts
Normal 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 {}
|
||||
51
packages/angular/standalone/src/providers/ionic-angular.ts
Normal file
51
packages/angular/standalone/src/providers/ionic-angular.ts
Normal 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);
|
||||
};
|
||||
};
|
||||
@@ -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:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
it("should be on Angular 14", () => {
|
||||
cy.visit('/');
|
||||
cy.visit('/lazy');
|
||||
|
||||
cy.get('ion-title').contains('Angular 14');
|
||||
});
|
||||
35
packages/angular/test/apps/ng14/package-lock.json
generated
35
packages/angular/test/apps/ng14/package-lock.json
generated
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
23
packages/angular/test/apps/ng14/src/main-standalone.ts
Normal file
23
packages/angular/test/apps/ng14/src/main-standalone.ts
Normal 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 })
|
||||
],
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
it("should be on Angular 15", () => {
|
||||
cy.visit('/');
|
||||
cy.visit('/lazy');
|
||||
|
||||
cy.get('ion-title').contains('Angular 15');
|
||||
});
|
||||
35
packages/angular/test/apps/ng15/package-lock.json
generated
35
packages/angular/test/apps/ng15/package-lock.json
generated
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
it("should be on Angular 16", () => {
|
||||
cy.visit('/');
|
||||
cy.visit('/lazy');
|
||||
|
||||
cy.get('ion-title').contains('Angular 16');
|
||||
});
|
||||
@@ -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');
|
||||
@@ -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', () => {
|
||||
67
packages/angular/test/apps/ng16/package-lock.json
generated
67
packages/angular/test/apps/ng16/package-lock.json
generated
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
describe('Accordion', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/accordions');
|
||||
cy.visit('/lazy/accordions');
|
||||
});
|
||||
|
||||
it('should correctly expand on multiple modal opens', () => {
|
||||
@@ -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', () => {
|
||||
@@ -1,6 +1,6 @@
|
||||
describe('Form', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/form');
|
||||
cy.visit('/lazy/form');
|
||||
})
|
||||
|
||||
describe('status updates', () => {
|
||||
@@ -1,6 +1,6 @@
|
||||
describe('Overlays: Inline', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/overlays-inline');
|
||||
cy.visit('/lazy/overlays-inline');
|
||||
});
|
||||
|
||||
describe('Alert', () => {
|
||||
@@ -1,6 +1,6 @@
|
||||
describe('Inputs', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/inputs');
|
||||
cy.visit('/lazy/inputs');
|
||||
})
|
||||
|
||||
it('should have default value', () => {
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
@@ -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']);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
describe('Nested Outlet', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/nested-outlet/page');
|
||||
cy.visit('/lazy/nested-outlet/page');
|
||||
})
|
||||
|
||||
it('should navigate correctly', () => {
|
||||
@@ -1,6 +1,6 @@
|
||||
describe('Popovers: Inline', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/popover-inline');
|
||||
cy.visit('/lazy/popover-inline');
|
||||
});
|
||||
|
||||
it('should initially have no items', () => {
|
||||
@@ -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');
|
||||
})
|
||||
@@ -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();
|
||||
|
||||
@@ -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', () => {
|
||||
@@ -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');
|
||||
@@ -1,6 +1,6 @@
|
||||
describe('Routing with Standalone Components', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/standalone');
|
||||
cy.visit('/lazy/standalone');
|
||||
});
|
||||
|
||||
it('should render the component', () => {
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
@@ -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', () => {
|
||||
@@ -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');
|
||||
});
|
||||
})
|
||||
20
packages/angular/test/base/e2e/src/standalone/icon.spec.ts
Normal file
20
packages/angular/test/base/e2e/src/standalone/icon.spec.ts
Normal 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
Reference in New Issue
Block a user