Skip to content

fix(router-outlet): support relative router links #17888

New issue

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

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

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 91 additions & 3 deletions angular/src/directives/navigation/ion-router-outlet.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
import { Attribute, ChangeDetectorRef, ComponentFactoryResolver, ComponentRef, Directive, ElementRef, EventEmitter, Injector, NgZone, OnDestroy, OnInit, Optional, Output, SkipSelf, ViewContainerRef } from '@angular/core';
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';
Expand All @@ -13,7 +31,6 @@ import { RouteView, getUrl } from './stack-utils';
inputs: ['animated', 'swipeGesture']
})
export class IonRouterOutlet implements OnDestroy, OnInit {

private activated: ComponentRef<any> | null = null;
private activatedView: RouteView | null = null;

Expand All @@ -23,6 +40,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<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);

tabsPrefix: string | undefined;

@Output() stackEvents = new EventEmitter<any>();
Expand Down Expand Up @@ -159,20 +182,31 @@ 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;
resolver = resolver || this.resolver;

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

Expand Down Expand Up @@ -212,6 +246,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<any> {
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 {
Expand Down