UNPKG

@ionic/angular

Version:

Angular specific wrappers for @ionic/core

1,311 lines (1,299 loc) 106 kB
import * as i0 from '@angular/core'; import { Injectable, Inject, Optional, InjectionToken, inject, NgZone, ApplicationRef, Injector, createComponent, TemplateRef, Directive, ContentChild, EventEmitter, ViewContainerRef, EnvironmentInjector, Attribute, SkipSelf, Input, Output, reflectComponentType, HostListener, ElementRef, ViewChild } from '@angular/core'; import * as i3 from '@angular/router'; import { NavigationStart, PRIMARY_OUTLET, ChildrenOutletContexts, ActivatedRoute, Router } from '@angular/router'; import * as i1 from '@angular/common'; import { DOCUMENT } from '@angular/common'; import { isPlatform, getPlatforms, LIFECYCLE_WILL_ENTER, LIFECYCLE_DID_ENTER, LIFECYCLE_WILL_LEAVE, LIFECYCLE_DID_LEAVE, LIFECYCLE_WILL_UNLOAD, componentOnReady } from '@ionic/core/components'; import { Subject, fromEvent, BehaviorSubject, combineLatest, of } from 'rxjs'; import { __decorate } from 'tslib'; import { filter, switchMap, distinctUntilChanged } from 'rxjs/operators'; import { NgControl } from '@angular/forms'; class MenuController { menuController; constructor(menuController) { this.menuController = menuController; } /** * Programmatically open the Menu. * @param [menuId] Optionally get the menu by its id, or side. * @return returns a promise when the menu is fully opened */ open(menuId) { return this.menuController.open(menuId); } /** * Programmatically close the Menu. If no `menuId` is given as the first * argument then it'll close any menu which is open. If a `menuId` * is given then it'll close that exact menu. * @param [menuId] Optionally get the menu by its id, or side. * @return returns a promise when the menu is fully closed */ close(menuId) { return this.menuController.close(menuId); } /** * Toggle the menu. If it's closed, it will open, and if opened, it * will close. * @param [menuId] Optionally get the menu by its id, or side. * @return returns a promise when the menu has been toggled */ toggle(menuId) { return this.menuController.toggle(menuId); } /** * Used to enable or disable a menu. For example, there could be multiple * left menus, but only one of them should be able to be opened at the same * time. If there are multiple menus on the same side, then enabling one menu * will also automatically disable all the others that are on the same side. * @param [menuId] Optionally get the menu by its id, or side. * @return Returns the instance of the menu, which is useful for chaining. */ enable(shouldEnable, menuId) { return this.menuController.enable(shouldEnable, menuId); } /** * Used to enable or disable the ability to swipe open the menu. * @param shouldEnable True if it should be swipe-able, false if not. * @param [menuId] Optionally get the menu by its id, or side. * @return Returns the instance of the menu, which is useful for chaining. */ swipeGesture(shouldEnable, menuId) { return this.menuController.swipeGesture(shouldEnable, menuId); } /** * @param [menuId] Optionally get the menu by its id, or side. * @return Returns true if the specified menu is currently open, otherwise false. * If the menuId is not specified, it returns true if ANY menu is currenly open. */ isOpen(menuId) { return this.menuController.isOpen(menuId); } /** * @param [menuId] Optionally get the menu by its id, or side. * @return Returns true if the menu is currently enabled, otherwise false. */ isEnabled(menuId) { return this.menuController.isEnabled(menuId); } /** * Used to get a menu instance. If a `menuId` is not provided then it'll * return the first menu found. If a `menuId` is `left` or `right`, then * it'll return the enabled menu on that side. Otherwise, if a `menuId` is * provided, then it'll try to find the menu using the menu's `id` * property. If a menu is not found then it'll return `null`. * @param [menuId] Optionally get the menu by its id, or side. * @return Returns the instance of the menu if found, otherwise `null`. */ get(menuId) { return this.menuController.get(menuId); } /** * @return Returns the instance of the menu already opened, otherwise `null`. */ getOpen() { return this.menuController.getOpen(); } /** * @return Returns an array of all menu instances. */ getMenus() { return this.menuController.getMenus(); } registerAnimation(name, animation) { return this.menuController.registerAnimation(name, animation); } isAnimating() { return this.menuController.isAnimating(); } _getOpenSync() { return this.menuController._getOpenSync(); } _createAnimation(type, menuCmp) { return this.menuController._createAnimation(type, menuCmp); } _register(menu) { return this.menuController._register(menu); } _unregister(menu) { return this.menuController._unregister(menu); } _setOpen(menu, shouldOpen, animated) { return this.menuController._setOpen(menu, shouldOpen, animated); } } class DomController { /** * Schedules a task to run during the READ phase of the next frame. * This task should only read the DOM, but never modify it. */ read(cb) { getQueue().read(cb); } /** * Schedules a task to run during the WRITE phase of the next frame. * This task should write the DOM, but never READ it. */ write(cb) { getQueue().write(cb); } /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DomController, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); /** @nocollapse */ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DomController, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DomController, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }] }); const getQueue = () => { const win = typeof window !== 'undefined' ? window : null; if (win != null) { const Ionic = win.Ionic; if (Ionic?.queue) { return Ionic.queue; } return { read: (cb) => win.requestAnimationFrame(cb), write: (cb) => win.requestAnimationFrame(cb), }; } return { read: (cb) => cb(), write: (cb) => cb(), }; }; class Platform { doc; _readyPromise; win; /** * @hidden */ backButton = new Subject(); /** * The keyboardDidShow event emits when the * on-screen keyboard is presented. */ keyboardDidShow = new Subject(); /** * The keyboardDidHide event emits when the * on-screen keyboard is hidden. */ keyboardDidHide = new Subject(); /** * The pause event emits when the native platform puts the application * into the background, typically when the user switches to a different * application. This event would emit when a Cordova app is put into * the background, however, it would not fire on a standard web browser. */ pause = new Subject(); /** * The resume event emits when the native platform pulls the application * out from the background. This event would emit when a Cordova app comes * out from the background, however, it would not fire on a standard web browser. */ resume = new Subject(); /** * The resize event emits when the browser window has changed dimensions. This * could be from a browser window being physically resized, or from a device * changing orientation. */ resize = new Subject(); constructor(doc, zone) { this.doc = doc; zone.run(() => { this.win = doc.defaultView; this.backButton.subscribeWithPriority = function (priority, callback) { return this.subscribe((ev) => { return ev.register(priority, (processNextHandler) => zone.run(() => callback(processNextHandler))); }); }; proxyEvent(this.pause, doc, 'pause', zone); proxyEvent(this.resume, doc, 'resume', zone); proxyEvent(this.backButton, doc, 'ionBackButton', zone); proxyEvent(this.resize, this.win, 'resize', zone); proxyEvent(this.keyboardDidShow, this.win, 'ionKeyboardDidShow', zone); proxyEvent(this.keyboardDidHide, this.win, 'ionKeyboardDidHide', zone); let readyResolve; this._readyPromise = new Promise((res) => { readyResolve = res; }); if (this.win?.['cordova']) { doc.addEventListener('deviceready', () => { readyResolve('cordova'); }, { once: true }); } else { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion readyResolve('dom'); } }); } /** * @returns returns true/false based on platform. * @description * Depending on the platform the user is on, `is(platformName)` will * return `true` or `false`. Note that the same app can return `true` * for more than one platform name. For example, an app running from * an iPad would return `true` for the platform names: `mobile`, * `ios`, `ipad`, and `tablet`. Additionally, if the app was running * from Cordova then `cordova` would be true, and if it was running * from a web browser on the iPad then `mobileweb` would be `true`. * * ``` * import { Platform } from 'ionic-angular'; * * @Component({...}) * export MyPage { * constructor(public platform: Platform) { * if (this.platform.is('ios')) { * // This will only print when on iOS * console.log('I am an iOS device!'); * } * } * } * ``` * * | Platform Name | Description | * |-----------------|------------------------------------| * | android | on a device running Android. | * | capacitor | on a device running Capacitor. | * | cordova | on a device running Cordova. | * | ios | on a device running iOS. | * | ipad | on an iPad device. | * | iphone | on an iPhone device. | * | phablet | on a phablet device. | * | tablet | on a tablet device. | * | electron | in Electron on a desktop device. | * | pwa | as a PWA app. | * | mobile | on a mobile device. | * | mobileweb | on a mobile device in a browser. | * | desktop | on a desktop device. | * | hybrid | is a cordova or capacitor app. | * */ is(platformName) { return isPlatform(this.win, platformName); } /** * @returns the array of platforms * @description * Depending on what device you are on, `platforms` can return multiple values. * Each possible value is a hierarchy of platforms. For example, on an iPhone, * it would return `mobile`, `ios`, and `iphone`. * * ``` * import { Platform } from 'ionic-angular'; * * @Component({...}) * export MyPage { * constructor(public platform: Platform) { * // This will print an array of the current platforms * console.log(this.platform.platforms()); * } * } * ``` */ platforms() { return getPlatforms(this.win); } /** * Returns a promise when the platform is ready and native functionality * can be called. If the app is running from within a web browser, then * the promise will resolve when the DOM is ready. When the app is running * from an application engine such as Cordova, then the promise will * resolve when Cordova triggers the `deviceready` event. * * The resolved value is the `readySource`, which states which platform * ready was used. For example, when Cordova is ready, the resolved ready * source is `cordova`. The default ready source value will be `dom`. The * `readySource` is useful if different logic should run depending on the * platform the app is running from. For example, only Cordova can execute * the status bar plugin, so the web should not run status bar plugin logic. * * ``` * import { Component } from '@angular/core'; * import { Platform } from 'ionic-angular'; * * @Component({...}) * export MyApp { * constructor(public platform: Platform) { * this.platform.ready().then((readySource) => { * console.log('Platform ready from', readySource); * // Platform now ready, execute any required native code * }); * } * } * ``` */ ready() { return this._readyPromise; } /** * Returns if this app is using right-to-left language direction or not. * We recommend the app's `index.html` file already has the correct `dir` * attribute value set, such as `<html dir="ltr">` or `<html dir="rtl">`. * [W3C: Structural markup and right-to-left text in HTML](http://www.w3.org/International/questions/qa-html-dir) */ get isRTL() { return this.doc.dir === 'rtl'; } /** * Get the query string parameter */ getQueryParam(key) { return readQueryParam(this.win.location.href, key); } /** * Returns `true` if the app is in landscape mode. */ isLandscape() { return !this.isPortrait(); } /** * Returns `true` if the app is in portrait mode. */ isPortrait() { return this.win.matchMedia?.('(orientation: portrait)').matches; } testUserAgent(expression) { const nav = this.win.navigator; return !!(nav?.userAgent && nav.userAgent.indexOf(expression) >= 0); } /** * Get the current url. */ url() { return this.win.location.href; } /** * Gets the width of the platform's viewport using `window.innerWidth`. */ width() { return this.win.innerWidth; } /** * Gets the height of the platform's viewport using `window.innerHeight`. */ height() { return this.win.innerHeight; } /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: Platform, deps: [{ token: DOCUMENT }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable }); /** @nocollapse */ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: Platform, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: Platform, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }], ctorParameters: function () { return [{ type: undefined, decorators: [{ type: Inject, args: [DOCUMENT] }] }, { type: i0.NgZone }]; } }); const readQueryParam = (url, key) => { key = key.replace(/[[\]\\]/g, '\\$&'); const regex = new RegExp('[\\?&]' + key + '=([^&#]*)'); const results = regex.exec(url); return results ? decodeURIComponent(results[1].replace(/\+/g, ' ')) : null; }; const proxyEvent = (emitter, el, eventName, zone) => { if (el) { el.addEventListener(eventName, (ev) => { /** * `zone.run` is required to make sure that we are running inside the Angular zone * at all times. This is necessary since an app that has Capacitor will * override the `document.addEventListener` with its own implementation. * The override causes the event to no longer be in the Angular zone. */ zone.run(() => { // ?? cordova might emit "null" events const value = ev != null ? ev.detail : undefined; emitter.next(value); }); }); } }; class NavController { location; serializer; router; topOutlet; direction = DEFAULT_DIRECTION; animated = DEFAULT_ANIMATED; animationBuilder; guessDirection = 'forward'; guessAnimation; lastNavId = -1; constructor(platform, location, serializer, router) { this.location = location; this.serializer = serializer; this.router = router; // Subscribe to router events to detect direction if (router) { router.events.subscribe((ev) => { if (ev instanceof NavigationStart) { // restoredState is set if the browser back/forward button is used const id = ev.restoredState ? ev.restoredState.navigationId : ev.id; this.guessDirection = this.guessAnimation = id < this.lastNavId ? 'back' : 'forward'; this.lastNavId = this.guessDirection === 'forward' ? ev.id : id; } }); } // Subscribe to backButton events platform.backButton.subscribeWithPriority(0, (processNextHandler) => { this.pop(); processNextHandler(); }); } /** * This method uses Angular's [Router](https://angular.io/api/router/Router) under the hood, * it's equivalent to calling `this.router.navigateByUrl()`, but it's explicit about the **direction** of the transition. * * Going **forward** means that a new page is going to be pushed to the stack of the outlet (ion-router-outlet), * and that it will show a "forward" animation by default. * * Navigating forward can also be triggered in a declarative manner by using the `[routerDirection]` directive: * * ```html * <a routerLink="/path/to/page" routerDirection="forward">Link</a> * ``` */ navigateForward(url, options = {}) { this.setDirection('forward', options.animated, options.animationDirection, options.animation); return this.navigate(url, options); } /** * This method uses Angular's [Router](https://angular.io/api/router/Router) under the hood, * it's equivalent to calling: * * ```ts * this.navController.setDirection('back'); * this.router.navigateByUrl(path); * ``` * * Going **back** means that all the pages in the stack until the navigated page is found will be popped, * and that it will show a "back" animation by default. * * Navigating back can also be triggered in a declarative manner by using the `[routerDirection]` directive: * * ```html * <a routerLink="/path/to/page" routerDirection="back">Link</a> * ``` */ navigateBack(url, options = {}) { this.setDirection('back', options.animated, options.animationDirection, options.animation); return this.navigate(url, options); } /** * This method uses Angular's [Router](https://angular.io/api/router/Router) under the hood, * it's equivalent to calling: * * ```ts * this.navController.setDirection('root'); * this.router.navigateByUrl(path); * ``` * * Going **root** means that all existing pages in the stack will be removed, * and the navigated page will become the single page in the stack. * * Navigating root can also be triggered in a declarative manner by using the `[routerDirection]` directive: * * ```html * <a routerLink="/path/to/page" routerDirection="root">Link</a> * ``` */ navigateRoot(url, options = {}) { this.setDirection('root', options.animated, options.animationDirection, options.animation); return this.navigate(url, options); } /** * Same as [Location](https://angular.io/api/common/Location)'s back() method. * It will use the standard `window.history.back()` under the hood, but featuring a `back` animation * by default. */ back(options = { animated: true, animationDirection: 'back' }) { this.setDirection('back', options.animated, options.animationDirection, options.animation); return this.location.back(); } /** * This methods goes back in the context of Ionic's stack navigation. * * It recursively finds the top active `ion-router-outlet` and calls `pop()`. * This is the recommended way to go back when you are using `ion-router-outlet`. * * Resolves to `true` if it was able to pop. */ async pop() { let outlet = this.topOutlet; while (outlet) { if (await outlet.pop()) { return true; } else { outlet = outlet.parentOutlet; } } return false; } /** * This methods specifies the direction of the next navigation performed by the Angular router. * * `setDirection()` does not trigger any transition, it just sets some flags to be consumed by `ion-router-outlet`. * * It's recommended to use `navigateForward()`, `navigateBack()` and `navigateRoot()` instead of `setDirection()`. */ setDirection(direction, animated, animationDirection, animationBuilder) { this.direction = direction; this.animated = getAnimation(direction, animated, animationDirection); this.animationBuilder = animationBuilder; } /** * @internal */ setTopOutlet(outlet) { this.topOutlet = outlet; } /** * @internal */ consumeTransition() { let direction = 'root'; let animation; const animationBuilder = this.animationBuilder; if (this.direction === 'auto') { direction = this.guessDirection; animation = this.guessAnimation; } else { animation = this.animated; direction = this.direction; } this.direction = DEFAULT_DIRECTION; this.animated = DEFAULT_ANIMATED; this.animationBuilder = undefined; return { direction, animation, animationBuilder, }; } navigate(url, options) { if (Array.isArray(url)) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return this.router.navigate(url, options); } else { /** * navigateByUrl ignores any properties that * would change the url, so things like queryParams * would be ignored unless we create a url tree * More Info: https://github.com/angular/angular/issues/18798 */ const urlTree = this.serializer.parse(url.toString()); if (options.queryParams !== undefined) { urlTree.queryParams = { ...options.queryParams }; } if (options.fragment !== undefined) { urlTree.fragment = options.fragment; } /** * `navigateByUrl` will still apply `NavigationExtras` properties * that do not modify the url, such as `replaceUrl` which is why * `options` is passed in here. */ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion return this.router.navigateByUrl(urlTree, options); } } /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NavController, deps: [{ token: Platform }, { token: i1.Location }, { token: i3.UrlSerializer }, { token: i3.Router, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); /** @nocollapse */ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NavController, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NavController, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }], ctorParameters: function () { return [{ type: Platform }, { type: i1.Location }, { type: i3.UrlSerializer }, { type: i3.Router, decorators: [{ type: Optional }] }]; } }); const getAnimation = (direction, animated, animationDirection) => { if (animated === false) { return undefined; } if (animationDirection !== undefined) { return animationDirection; } if (direction === 'forward' || direction === 'back') { return direction; } else if (direction === 'root' && animated === true) { return 'forward'; } return undefined; }; const DEFAULT_DIRECTION = 'auto'; const DEFAULT_ANIMATED = undefined; class Config { get(key, fallback) { const c = getConfig(); if (c) { return c.get(key, fallback); } return null; } getBoolean(key, fallback) { const c = getConfig(); if (c) { return c.getBoolean(key, fallback); } return false; } getNumber(key, fallback) { const c = getConfig(); if (c) { return c.getNumber(key, fallback); } return 0; } /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: Config, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); /** @nocollapse */ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: Config, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: Config, decorators: [{ type: Injectable, args: [{ providedIn: 'root', }] }] }); const ConfigToken = new InjectionToken('USERCONFIG'); const getConfig = () => { if (typeof window !== 'undefined') { const Ionic = window.Ionic; if (Ionic?.config) { return Ionic.config; } } return null; }; /** * @description * NavParams are an object that exists on a page and can contain data for that particular view. * Similar to how data was pass to a view in V1 with `$stateParams`, NavParams offer a much more flexible * option with a simple `get` method. * * @usage * ```ts * import { NavParams } from '@ionic/angular'; * * export class MyClass{ * * constructor(navParams: NavParams){ * // userParams is an object we have in our nav-parameters * navParams.get('userParams'); * } * * } * ``` */ class NavParams { data; constructor(data = {}) { this.data = data; console.warn(`[Ionic Warning]: NavParams has been deprecated in favor of using Angular's input API. Developers should migrate to either the @Input decorator or the Signals-based input API.`); } /** * Get the value of a nav-parameter for the current view * * ```ts * import { NavParams } from 'ionic-angular'; * * export class MyClass{ * constructor(public navParams: NavParams){ * // userParams is an object we have in our nav-parameters * this.navParams.get('userParams'); * } * } * ``` * * @param param Which param you want to look up */ get(param) { return this.data[param]; } } // TODO(FW-2827): types class AngularDelegate { zone = inject(NgZone); applicationRef = inject(ApplicationRef); config = inject(ConfigToken); create(environmentInjector, injector, elementReferenceKey) { return new AngularFrameworkDelegate(environmentInjector, injector, this.applicationRef, this.zone, elementReferenceKey, this.config.useSetInputAPI ?? false); } /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: AngularDelegate, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); /** @nocollapse */ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: AngularDelegate }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: AngularDelegate, decorators: [{ type: Injectable }] }); class AngularFrameworkDelegate { environmentInjector; injector; applicationRef; zone; elementReferenceKey; enableSignalsSupport; elRefMap = new WeakMap(); elEventsMap = new WeakMap(); constructor(environmentInjector, injector, applicationRef, zone, elementReferenceKey, enableSignalsSupport) { this.environmentInjector = environmentInjector; this.injector = injector; this.applicationRef = applicationRef; this.zone = zone; this.elementReferenceKey = elementReferenceKey; this.enableSignalsSupport = enableSignalsSupport; } attachViewToDom(container, component, params, cssClasses) { return this.zone.run(() => { return new Promise((resolve) => { const componentProps = { ...params, }; /** * Ionic Angular passes a reference to a modal * or popover that can be accessed using a * variable in the overlay component. If * elementReferenceKey is defined, then we should * pass a reference to the component using * elementReferenceKey as the key. */ if (this.elementReferenceKey !== undefined) { componentProps[this.elementReferenceKey] = container; } const el = attachView(this.zone, this.environmentInjector, this.injector, this.applicationRef, this.elRefMap, this.elEventsMap, container, component, componentProps, cssClasses, this.elementReferenceKey, this.enableSignalsSupport); resolve(el); }); }); } removeViewFromDom(_container, component) { return this.zone.run(() => { return new Promise((resolve) => { const componentRef = this.elRefMap.get(component); if (componentRef) { componentRef.destroy(); this.elRefMap.delete(component); const unbindEvents = this.elEventsMap.get(component); if (unbindEvents) { unbindEvents(); this.elEventsMap.delete(component); } } resolve(); }); }); } } const attachView = (zone, environmentInjector, injector, applicationRef, elRefMap, elEventsMap, container, component, params, cssClasses, elementReferenceKey, enableSignalsSupport) => { /** * Wraps the injector with a custom injector that * provides NavParams to the component. * * NavParams is a legacy feature from Ionic v3 that allows * Angular developers to provide data to a component * and access it by providing NavParams as a dependency * in the constructor. * * The modern approach is to access the data directly * from the component's class instance. */ const childInjector = Injector.create({ providers: getProviders(params), parent: injector, }); const componentRef = createComponent(component, { environmentInjector, elementInjector: childInjector, }); const instance = componentRef.instance; const hostElement = componentRef.location.nativeElement; if (params) { /** * For modals and popovers, a reference to the component is * added to `params` during the call to attachViewToDom. If * a reference using this name is already set, this means * the app is trying to use the name as a component prop, * which will cause collisions. */ if (elementReferenceKey && instance[elementReferenceKey] !== undefined) { console.error(`[Ionic Error]: ${elementReferenceKey} is a reserved property when using ${container.tagName.toLowerCase()}. Rename or remove the "${elementReferenceKey}" property from ${component.name}.`); } /** * Angular 14.1 added support for setInput * so we need to fall back to Object.assign * for Angular 14.0. */ if (enableSignalsSupport === true && componentRef.setInput !== undefined) { const { modal, popover, ...otherParams } = params; /** * Any key/value pairs set in componentProps * must be set as inputs on the component instance. */ for (const key in otherParams) { componentRef.setInput(key, otherParams[key]); } /** * Using setInput will cause an error when * setting modal/popover on a component that * does not define them as an input. For backwards * compatibility purposes we fall back to using * Object.assign for these properties. */ if (modal !== undefined) { Object.assign(instance, { modal }); } if (popover !== undefined) { Object.assign(instance, { popover }); } } else { Object.assign(instance, params); } } if (cssClasses) { for (const cssClass of cssClasses) { hostElement.classList.add(cssClass); } } const unbindEvents = bindLifecycleEvents(zone, instance, hostElement); container.appendChild(hostElement); applicationRef.attachView(componentRef.hostView); elRefMap.set(hostElement, componentRef); elEventsMap.set(hostElement, unbindEvents); return hostElement; }; const LIFECYCLES = [ LIFECYCLE_WILL_ENTER, LIFECYCLE_DID_ENTER, LIFECYCLE_WILL_LEAVE, LIFECYCLE_DID_LEAVE, LIFECYCLE_WILL_UNLOAD, ]; const bindLifecycleEvents = (zone, instance, element) => { return zone.run(() => { const unregisters = LIFECYCLES.filter((eventName) => typeof instance[eventName] === 'function').map((eventName) => { const handler = (ev) => instance[eventName](ev.detail); element.addEventListener(eventName, handler); return () => element.removeEventListener(eventName, handler); }); return () => unregisters.forEach((fn) => fn()); }); }; const NavParamsToken = new InjectionToken('NavParamsToken'); const getProviders = (params) => { return [ { provide: NavParamsToken, useValue: params, }, { provide: NavParams, useFactory: provideNavParamsInjectable, deps: [NavParamsToken], }, ]; }; const provideNavParamsInjectable = (params) => { return new NavParams(params); }; // TODO: Is there a way we can grab this from angular-component-lib instead? /* eslint-disable */ /* tslint:disable */ const proxyInputs = (Cmp, inputs) => { const Prototype = Cmp.prototype; inputs.forEach((item) => { Object.defineProperty(Prototype, item, { get() { return this.el[item]; }, set(val) { this.z.runOutsideAngular(() => (this.el[item] = val)); }, }); }); }; const proxyMethods = (Cmp, methods) => { const Prototype = Cmp.prototype; methods.forEach((methodName) => { Prototype[methodName] = function () { const args = arguments; return this.z.runOutsideAngular(() => this.el[methodName].apply(this.el, args)); }; }); }; const proxyOutputs = (instance, el, events) => { events.forEach((eventName) => (instance[eventName] = fromEvent(el, eventName))); }; // tslint:disable-next-line: only-arrow-functions function ProxyCmp(opts) { const decorator = function (cls) { const { defineCustomElementFn, inputs, methods } = opts; if (defineCustomElementFn !== undefined) { defineCustomElementFn(); } if (inputs) { proxyInputs(cls, inputs); } if (methods) { proxyMethods(cls, methods); } return cls; }; return decorator; } const POPOVER_INPUTS = [ 'alignment', 'animated', 'arrow', 'keepContentsMounted', 'backdropDismiss', 'cssClass', 'dismissOnSelect', 'enterAnimation', 'event', 'focusTrap', 'isOpen', 'keyboardClose', 'leaveAnimation', 'mode', 'showBackdrop', 'translucent', 'trigger', 'triggerAction', 'reference', 'size', 'side', ]; const POPOVER_METHODS = ['present', 'dismiss', 'onDidDismiss', 'onWillDismiss']; let IonPopover = class IonPopover { z; // TODO(FW-2827): type template; isCmpOpen = false; el; constructor(c, r, z) { this.z = z; 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', ]); } /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: IonPopover, deps: [{ token: i0.ChangeDetectorRef }, { token: i0.ElementRef }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Directive }); /** @nocollapse */ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: IonPopover, selector: "ion-popover", inputs: { alignment: "alignment", animated: "animated", arrow: "arrow", keepContentsMounted: "keepContentsMounted", backdropDismiss: "backdropDismiss", cssClass: "cssClass", dismissOnSelect: "dismissOnSelect", enterAnimation: "enterAnimation", event: "event", focusTrap: "focusTrap", isOpen: "isOpen", keyboardClose: "keyboardClose", leaveAnimation: "leaveAnimation", mode: "mode", showBackdrop: "showBackdrop", translucent: "translucent", trigger: "trigger", triggerAction: "triggerAction", reference: "reference", size: "size", side: "side" }, queries: [{ propertyName: "template", first: true, predicate: TemplateRef, descendants: true }], ngImport: i0 }); }; IonPopover = __decorate([ 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. */ ], IonPopover); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: IonPopover, decorators: [{ type: Directive, args: [{ selector: 'ion-popover', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property inputs: POPOVER_INPUTS, }] }], ctorParameters: function () { return [{ type: i0.ChangeDetectorRef }, { type: i0.ElementRef }, { type: i0.NgZone }]; }, propDecorators: { template: [{ type: ContentChild, args: [TemplateRef, { static: false }] }] } }); const MODAL_INPUTS = [ 'animated', 'keepContentsMounted', 'backdropBreakpoint', 'backdropDismiss', 'breakpoints', 'canDismiss', 'cssClass', 'enterAnimation', 'expandToScroll', 'event', 'focusTrap', 'handle', 'handleBehavior', 'initialBreakpoint', 'isOpen', 'keyboardClose', 'leaveAnimation', 'mode', 'presentingElement', 'showBackdrop', 'translucent', 'trigger', ]; const MODAL_METHODS = [ 'present', 'dismiss', 'onDidDismiss', 'onWillDismiss', 'setCurrentBreakpoint', 'getCurrentBreakpoint', ]; let IonModal = class IonModal { z; // TODO(FW-2827): type template; isCmpOpen = false; el; constructor(c, r, z) { this.z = z; 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', ]); } /** @nocollapse */ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: IonModal, deps: [{ token: i0.ChangeDetectorRef }, { token: i0.ElementRef }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Directive }); /** @nocollapse */ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: IonModal, selector: "ion-modal", inputs: { animated: "animated", keepContentsMounted: "keepContentsMounted", backdropBreakpoint: "backdropBreakpoint", backdropDismiss: "backdropDismiss", breakpoints: "breakpoints", canDismiss: "canDismiss", cssClass: "cssClass", enterAnimation: "enterAnimation", expandToScroll: "expandToScroll", event: "event", focusTrap: "focusTrap", handle: "handle", handleBehavior: "handleBehavior", initialBreakpoint: "initialBreakpoint", isOpen: "isOpen", keyboardClose: "keyboardClose", leaveAnimation: "leaveAnimation", mode: "mode", presentingElement: "presentingElement", showBackdrop: "showBackdrop", translucent: "translucent", trigger: "trigger" }, queries: [{ propertyName: "template", first: true, predicate: TemplateRef, descendants: true }], ngImport: i0 }); }; IonModal = __decorate([ 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. */ ], IonModal); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: IonModal, decorators: [{ type: Directive, args: [{ selector: 'ion-modal', // eslint-disable-next-line @angular-eslint/no-inputs-metadata-property inputs: MODAL_INPUTS, }] }], ctorParameters: function () { return [{ type: i0.ChangeDetectorRef }, { type: i0.ElementRef }, { type: i0.NgZone }]; }, propDecorators: { template: [{ type: ContentChild, args: [TemplateRef, { static: false }] }] } }); const insertView = (views, view, direction) => { if (direction === 'root') { return setRoot(views, view); } else if (direction === 'forward') { return setForward(views, view); } else { return setBack(views, view); } }; const setRoot = (views, view) => { views = views.filter((v) => v.stackId !== view.stackId); views.push(view); return views; }; const setForward = (views, view) => { const index = views.indexOf(view); if (index >= 0) { views = views.filter((v) => v.stackId !== view.stackId || v.id <= view.id); } else { views.push(view); } return views; }; const setBack = (views, view) => { const index = views.indexOf(view); if (index >= 0) { return views.filter((v) => v.stackId !== view.stackId || v.id <= view.id); } else { return setRoot(views, view); } }; const getUrl = (router, activatedRoute) => { const urlTree = router.createUrlTree(['.'], { relativeTo: activatedRoute }); return router.serializeUrl(urlTree); }; const isTabSwitch = (enteringView, leavingView) => { if (!leavingView) { return true; } return enteringView.stackId !== leavingView.stackId; }; const computeStackId = (prefixUrl, url) => { if (!prefixUrl) { return undefined; } const segments = toSegments(url); for (let i = 0; i < segments.length; i++) { if (i >= prefixUrl.length) { return segments[i]; } if (segments[i] !== prefixUrl[i]) { return undefined; } } return undefined; }; const toSegments = (path) => { return path .split('/') .map((s) => s.trim()) .filter((s) => s !== ''); }; const destroyView = (view) => { if (view) { view.ref.destroy(); view.unlistenEvents(); } }; // TODO(FW-2827): types class StackController { containerEl; router; navCtrl; zone; location; views = []; runningTask; skipTransition = false; tabsPrefix; activeView; nextId = 0; constructor(tabsPrefix, containerEl, router, navCtrl, zone, location) { this.containerEl = containerEl; this.router = router; this.navCtrl = navCtrl; this.zone = zone; this.location = location; this.tabsPrefix = tabsPrefix !== undefined ? toSegments(tabsPrefix) : undefined; } createView(ref, activatedRoute) { const url = getUrl(this.router, activatedRoute); const element = ref?.location?.nativeElement; const unlistenEvents = bindLifecycleEvents(this.zone, ref.instance, element); return { id: this.nextId++, stackId: computeStackId(this.tabsPrefix, url), unlistenEvents, element, ref, url, }; } getExistingView(activatedRoute) { const activatedUrlKey = getUrl(this.router, activatedRoute); const view = this.views.find((vw) => vw.url === activatedUrlKey); if (view) { view.ref.changeDetectorRef.reattach(); } return view; } setActive(enteringView) { const consumeResult = this.navCtrl.consumeTransition(); let { direction, animation, animationBuilder } = consumeResult; const leavingView = this.activeView; const tabSwitch = isTabSwitch(enteringView, leavingView); if (tabSwitch) { direction = 'back'; animation = undefined; } const viewsSnapshot = this.views.slice(); let currentNavigation; const router = this.router; // Angular >= 7.2.0 if (router.getCurrentNavigation) { currentNavigation = router.getCurrentNavigation(); // Angular < 7.2.0 } else if (router.navigations?.value) { currentNavigation = router.navigations.value; } /** * If the navigation action * sets `replaceUrl: true` * then we need to make sure * we remove the last item * from our views stack */ if (currentNavigation?.extras?.replaceUrl) { if (this.views.length > 0) { this.views.splice(-1, 1); } } const reused = this.views.includes(enteringView); const views = this.insertView(enteringView, direction); // Trigger change detection before transition starts // This will call ngOnInit() the first time too, just after the view // was attached to the dom, but BEFORE the transition starts if (!reused) { enteringView.ref.changeDetectorRef.detectChanges(); } /** * If we are going back from a page that * was presented using a custom animation * we should default to using that * unless the developer explicitly * provided another animation. */ const customAnimation = enteringView.animationBuilder; if (animationBuilder === undefined && direction === 'back' && !tabSwitch && customAnimation !== undefined) { animationBuilder = customAnimation; } /** * Save any custom animation so that navigating * back will use this custom animation by default. */ if (leavingView) { leavingView.animationBuilder = animationBuilder; } // Wait until previous transitions finish return this.zone.runOutsideAngular(() => { return this.wait(() => { // disconnect leaving page from change detection to // reduce jank during the page transition if (leavingView) { leavingView.ref.changeDetectorRef.detach(); } // In case the enteringView is the same as the leavingPage we need to reattach() enteringView.ref.changeDetectorRef.reattach(); return this.transition(enteringView, leavingView, animation, this.canGoBack(1), false,