diff --git a/angular/src/directives/navigation/ion-router-outlet.ts b/angular/src/directives/navigation/ion-router-outlet.ts index e9f9a18f090..61e0db526fd 100644 --- a/angular/src/directives/navigation/ion-router-outlet.ts +++ b/angular/src/directives/navigation/ion-router-outlet.ts @@ -1,11 +1,11 @@ import { Attribute, ChangeDetectorRef, ComponentFactoryResolver, ComponentRef, Directive, ElementRef, EventEmitter, Injector, NgZone, OnDestroy, OnInit, Optional, Output, SkipSelf, ViewContainerRef } from '@angular/core'; import { ActivatedRoute, ChildrenOutletContexts, OutletContext, PRIMARY_OUTLET, Router } from '@angular/router'; - +import { BehaviorSubject, Observable } 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, getUrl } from './stack-utils'; +import { getUrl, RouteView } from './stack-utils'; @Directive({ selector: 'ion-router-outlet', @@ -13,7 +13,6 @@ import { RouteView, getUrl } from './stack-utils'; inputs: ['animated', 'swipeGesture'] }) export class IonRouterOutlet implements OnDestroy, OnInit { - private activated: ComponentRef | null = null; private activatedView: RouteView | null = null; @@ -23,6 +22,12 @@ export class IonRouterOutlet implements OnDestroy, OnInit { private stackCtrl: StackController; private nativeEl: HTMLIonRouterOutletElement; + // Maintain map of activated route proxies for each component instance + private proxyMap = new WeakMap(); + + // 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); + tabsPrefix: string | undefined; @Output() stackEvents = new EventEmitter(); @@ -159,6 +164,8 @@ export class IonRouterOutlet implements OnDestroy, OnInit { 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; const component = snapshot.routeConfig!.component as any; @@ -166,13 +173,22 @@ export class IonRouterOutlet implements OnDestroy, OnInit { const factory = resolver.resolveComponentFactory(component); const childContexts = this.parentContexts.getOrCreateContext(this.name).children; + const activatedRouteProxy = this.createActivatedRouteProxy(activatedRoute); - const injector = new OutletInjector(activatedRoute, childContexts, this.location.injector); + const injector = new OutletInjector(activatedRouteProxy, childContexts, this.location.injector); cmpRef = this.activated = this.location.createComponent(factory, this.location.length, injector); // 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); + + // Once the component is created, use the component instance to setup observables + this.setupProxyObservables(activatedRouteProxy, cmpRef.instance); + + // Store references to the proxy by component + this.proxyMap.set(cmpRef.instance, activatedRouteProxy); + this.currentActivatedRoute$.next({ component: cmpRef.instance, activatedRoute }); + this.changeDetector.markForCheck(); } @@ -212,6 +228,60 @@ export class IonRouterOutlet implements OnDestroy, OnInit { getActiveStackId(): string | undefined { return this.stackCtrl.getActiveStackId(); } + + /** + * Creates a proxy object that we can use to update activated route properties without losing reference + * in the component injector + */ + private createActivatedRouteProxy(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; + + return proxy as ActivatedRoute; + } + + private setupProxyObservables(proxy: ActivatedRoute, component: any): void { + (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'); + } + + /** + * Create a wrapped observable that will switch to the latest activated route matched by the given view id + */ + private proxyObservable(component: any, path: string): Observable { + return this.currentActivatedRoute$.pipe( + filter(current => current !== null && current.component === component), + switchMap(current => current && (current.activatedRoute as any)[path]), + distinctUntilChanged() + ); + } + + /** + * Updates the given proxy route with data from the new incoming route + */ + 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 {