I assume that route
is the injected ActivatedRoute
.
Each ActivatedRoute
is bound to a routed component and when a route changes occurs, the current component being displayed is going to be destroyed, as well as its bound ActivatedRoute
, so that's why you won't get a complete
notification.
Here's how an ActivatedRoute
is created:
function createActivatedRoute(c: ActivatedRouteSnapshot) {
return new ActivatedRoute(
new BehaviorSubject(c.url), new BehaviorSubject(c.params), new BehaviorSubject(c.queryParams),
new BehaviorSubject(c.fragment), new BehaviorSubject(c.data), c.outlet, c.component, c);
}
Now, how are ActivatedRoute
s bound to routed component?
Assuming you have a configuration that looks like this:
{
path: 'a/:id',
component: AComponent,
children: [
{
path: 'b',
component: BComponent,
},
{
path: 'c',
component: CComponent,
},
]
}
and an issued URL like a/123/b
you'd end up having a tree of ActivatedRoute
s:
APP
|
A
|
B
Whenever you schedule a navigation(e.g router.navigateToUrl()
), it has to go through some important phases:
- apply redirects: checking for redirects; loading lazy-loaded modules; finding
NoMatch
errors
- recognize: creating the
ActivatedRouteSnapshot
tree
- preactivation: comparing the resulted tree with the current one; this phase also collects
canActivate
and canDeactivate
guards, based on the found differences
- running guards
- create router state: where
ActivatedRoute
tree is created
activating the routes: this is the cherry on the cake and the place where the ActivatedRoute
tree is leveraged
It is also important to mention the role that router-outlet
plays.
Angular keeps track of the router-outlet
s with the help of a Map
object.
Here's what happens when you have a <router-outlet></router-outlet>
in your app:
@Directive({selector: 'router-outlet', exportAs: 'outlet'})
export class RouterOutlet implements OnDestroy, OnInit {
private activated: ComponentRef<any>|null = null;
private _activatedRoute: ActivatedRoute|null = null;
private name: string;
@Output('activate') activateEvents = new EventEmitter<any>();
@Output('deactivate') deactivateEvents = new EventEmitter<any>();
constructor(
private parentContexts: ChildrenOutletContexts, private location: ViewContainerRef,
private resolver: ComponentFactoryResolver, @Attribute('name') name: string,
private changeDetector: ChangeDetectorRef) {
this.name = name || PRIMARY_OUTLET;
parentContexts.onChildOutletCreated(this.name, this);
}
}
Note the presence of activated
(which is a component) and _activatedRoute
!.
And here are the relevant bits of ChildrenOutletContexts
:
export class ChildrenOutletContexts {
// contexts for child outlets, by name.
private contexts = new Map<string, OutletContext>();
/** Called when a `RouterOutlet` directive is instantiated */
onChildOutletCreated(childName: string, outlet: RouterOutlet): void {
const context = this.getOrCreateContext(childName);
context.outlet = outlet;
this.contexts.set(childName, context);
}
}
where childName
is by default 'primary'
. For now, focus your attention only on the context.outlet
part.
So, for our route configuration:
{
path: 'a/:id',
component: AComponent,
children: [
{
path: 'b',
component: BComponent,
},
{
path: 'c',
component: CComponent,
},
]
}
the router-outlet
Map
would look like this(roughly):
{
primary: { // Where `AComponent` resides [1]
children: {
// Here `AComponent`'s children reside [2]
primary: { children: { } }
}
}
}
Now, let's see how a route is activated:
// This block of code will be run for [1] and [2] (in this order!)
const context = parentContexts.getOrCreateContext(future.outlet);
/* ... */
const config = parentLoadedConfig(future.snapshot);
const cmpFactoryResolver = config ? config.module.componentFactoryResolver : null;
context.attachRef = null;
context.route = future;
context.resolver = cmpFactoryResolver;
if (context.outlet) {
context.outlet.activateWith(future, cmpFactoryResolver);
}
this.activateChildRoutes(futureNode, null, context.children);
context.outlet.activateWith(future, cmpFactoryResolver);
is what what we're looking for(where outlet
is the RouterOutlet
directive instance):
activateWith(activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver|null) {
if (this.isActivated) {
throw new Error('Cannot activate an already activated outlet');
}
this._activatedRoute = activatedRoute;
const snapshot = activatedRoute._futureSnapshot;
const component = <any>snapshot.routeConfig!.component;
resolver = resolver || this.resolver;
const factory = resolver.resolveComponentFactory(component);
const childContexts = this.parentContexts.getOrCreateContext(this.name).children;
const injector = new OutletInjector(activatedRoute, childContexts, this.location.injector);
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.
this.changeDetector.markForCheck();
this.activateEvents.emit(this.activated.instance);
}
Note that this.activated
holds the routed component(e.g AComponent
) and this._activatedRoute
holds the ActivatedRoute
for this component.
Let's see now what happens when we're navigating to another route and this current view is destroyed:
deactivateRouteAndOutlet(
route: TreeNode<ActivatedRoute>, parentContexts: ChildrenOutletContexts): void {
const context = parentContexts.getContext(route.value.outlet);
if (context) {
const children: {[outletName: string]: any} = nodeChildrenAsMap(route);
const contexts = route.value.component ? context.children : parentContexts;
// Deactivate children first
forEach(children, (v: any, k: string) => this.deactivateRouteAndItsChildren(v, contexts));
if (context.outlet) {
// Destroy the component
context.outlet.deactivate();
// Destroy the contexts for all the outlets that were in the component
context.children.onOutletDeactivated();
}
}
}
where RouterOutlet.deactivate()
looks like this:
deactivate(): void {
if (this.activated) {
const c = this.component;
this.activated.destroy(); // Destroying the current component
this.activated = null;
// Nulling out the activated route - so no `complete` notification
this._activatedRoute = null;
this.deactivateEvents.emit(c);
}
}