feat(router): lifecycle hooks

Closes #2640
This commit is contained in:
Brian Ford
2015-07-07 15:44:29 -07:00
parent f5f85bb528
commit a9a552c112
12 changed files with 823 additions and 106 deletions

View File

@ -24,7 +24,6 @@ export class Instruction {
// "capturedUrl" is the part of the URL captured by this instruction
// "accumulatedUrl" is the part of the URL captured by this instruction and all children
accumulatedUrl: string;
reuse: boolean = false;
specificity: number;
@ -50,23 +49,4 @@ export class Instruction {
}
return this._params;
}
hasChild(): boolean { return isPresent(this.child); }
/**
* Takes a currently active instruction and sets a reuse flag on each of this instruction's
* children
*/
reuseComponentsFrom(oldInstruction: Instruction): void {
var nextInstruction = this;
while (nextInstruction.reuse = shouldReuseComponent(nextInstruction, oldInstruction) &&
isPresent(oldInstruction = oldInstruction.child) &&
isPresent(nextInstruction = nextInstruction.child))
;
}
}
function shouldReuseComponent(instr1: Instruction, instr2: Instruction): boolean {
return instr1.component == instr2.component &&
StringMapWrapper.equals(instr1.params(), instr2.params());
}

View File

@ -0,0 +1,43 @@
import {Instruction} from './instruction';
import {global} from 'angular2/src/facade/lang';
// This is here only so that after TS transpilation the file is not empty.
// TODO(rado): find a better way to fix this, or remove if likely culprit
// https://github.com/systemjs/systemjs/issues/487 gets closed.
var __ignore_me = global;
/**
* Defines route lifecycle method [onActivate]
*/
export interface OnActivate {
onActivate(nextInstruction: Instruction, prevInstruction: Instruction): any;
}
/**
* Defines route lifecycle method [onReuse]
*/
export interface OnReuse {
onReuse(nextInstruction: Instruction, prevInstruction: Instruction): any;
}
/**
* Defines route lifecycle method [onDeactivate]
*/
export interface OnDeactivate {
onDeactivate(nextInstruction: Instruction, prevInstruction: Instruction): any;
}
/**
* Defines route lifecycle method [canReuse]
*/
export interface CanReuse {
canReuse(nextInstruction: Instruction, prevInstruction: Instruction): any;
}
/**
* Defines route lifecycle method [canDeactivate]
*/
export interface CanDeactivate {
canDeactivate(nextInstruction: Instruction, prevInstruction: Instruction): any;
}

View File

@ -0,0 +1,8 @@
/**
* This indirection is needed for TS compilation path.
* See comment in lifecycle_annotations.ts.
*/
library angular2.router.lifecycle_annotations;
export "./lifecycle_annotations_impl.dart";

View File

@ -0,0 +1,17 @@
/**
* This indirection is needed to free up Component, etc symbols in the public API
* to be used by the decorator versions of these annotations.
*/
import {makeDecorator} from 'angular2/src/util/decorators';
import {CanActivate as CanActivateAnnotation} from './lifecycle_annotations_impl';
export {
canReuse,
canDeactivate,
onActivate,
onReuse,
onDeactivate
} from './lifecycle_annotations_impl';
export var CanActivate = makeDecorator(CanActivateAnnotation);

View File

@ -0,0 +1,18 @@
import {CONST, CONST_EXPR} from 'angular2/src/facade/lang';
@CONST()
export class RouteLifecycleHook {
constructor(public name: string) {}
}
@CONST()
export class CanActivate {
constructor(public fn: Function) {}
}
export const canReuse: RouteLifecycleHook = CONST_EXPR(new RouteLifecycleHook("canReuse"));
export const canDeactivate: RouteLifecycleHook =
CONST_EXPR(new RouteLifecycleHook("canDeactivate"));
export const onActivate: RouteLifecycleHook = CONST_EXPR(new RouteLifecycleHook("onActivate"));
export const onReuse: RouteLifecycleHook = CONST_EXPR(new RouteLifecycleHook("onReuse"));
export const onDeactivate: RouteLifecycleHook = CONST_EXPR(new RouteLifecycleHook("onDeactivate"));

View File

@ -0,0 +1,38 @@
library angular.router.route_lifecycle_reflector;
import 'package:angular2/src/router/lifecycle_annotations_impl.dart';
import 'package:angular2/src/router/interfaces.dart';
import 'package:angular2/src/reflection/reflection.dart';
bool hasLifecycleHook(RouteLifecycleHook e, type) {
if (type is! Type) return false;
final List interfaces = reflector.interfaces(type);
var interface;
if (e == onActivate) {
interface = OnActivate;
} else if (e == onDeactivate) {
interface = OnDeactivate;
} else if (e == onReuse) {
interface = OnReuse;
} else if (e == canDeactivate) {
interface = CanDeactivate;
} else if (e == canReuse) {
interface = CanReuse;
}
return interfaces.contains(interface);
}
Function getCanActivateHook(type) {
final List annotations = reflector.annotations(type);
for (var annotation in annotations) {
if (annotation is CanActivate) {
return annotation.fn;
}
}
return null;
}

View File

@ -0,0 +1,20 @@
import {Type, isPresent} from 'angular2/src/facade/lang';
import {RouteLifecycleHook, CanActivate} from './lifecycle_annotations_impl';
import {reflector} from 'angular2/src/reflection/reflection';
export function hasLifecycleHook(e: RouteLifecycleHook, type): boolean {
if (!(type instanceof Type)) return false;
return e.name in(<any>type).prototype;
}
export function getCanActivateHook(type): Function {
var annotations = reflector.annotations(type);
for (let i = 0; i < annotations.length; i += 1) {
let annotation = annotations[i];
if (annotation instanceof CanActivate) {
return annotation.fn;
}
}
return null;
}

View File

@ -126,7 +126,11 @@ export class RouteRegistry {
this.configFromComponent(componentType);
if (partialMatch.unmatchedUrl.length == 0) {
return new Instruction(componentType, partialMatch.matchedUrl, recognizer);
if (recognizer.terminal) {
return new Instruction(componentType, partialMatch.matchedUrl, recognizer);
} else {
return null;
}
}
return this.recognize(partialMatch.unmatchedUrl, componentType)

View File

@ -15,6 +15,7 @@ import {Pipeline} from './pipeline';
import {Instruction} from './instruction';
import {RouterOutlet} from './router_outlet';
import {Location} from './location';
import {getCanActivateHook} from './route_lifecycle_reflector';
let _resolveToTrue = PromiseWrapper.resolve(true);
let _resolveToFalse = PromiseWrapper.resolve(false);
@ -39,7 +40,6 @@ let _resolveToFalse = PromiseWrapper.resolve(false);
export class Router {
navigating: boolean = false;
lastNavigationAttempt: string;
previousUrl: string = null;
private _currentInstruction: Instruction = null;
private _currentNavigation: Promise<any> = _resolveToTrue;
@ -67,7 +67,7 @@ export class Router {
// TODO: sibling routes
this._outlet = outlet;
if (isPresent(this._currentInstruction)) {
return outlet.activate(this._currentInstruction);
return outlet.commit(this._currentInstruction);
}
return _resolveToTrue;
}
@ -109,35 +109,94 @@ export class Router {
* If the given URL does not begin with `/`, the router will navigate relative to this component.
*/
navigate(url: string): Promise<any> {
if (this.navigating) {
return this._currentNavigation;
}
this.lastNavigationAttempt = url;
return this._currentNavigation = this.recognize(url).then((matchedInstruction) => {
if (isBlank(matchedInstruction)) {
return _resolveToFalse;
}
if (isPresent(this._currentInstruction)) {
matchedInstruction.reuseComponentsFrom(this._currentInstruction);
}
return this._currentNavigation = this._currentNavigation.then((_) => {
this.lastNavigationAttempt = url;
this._startNavigating();
var result =
this.commit(matchedInstruction)
.then((_) => {
this._finishNavigating();
ObservableWrapper.callNext(this._subject, matchedInstruction.accumulatedUrl);
});
return PromiseWrapper.catchError(result, (err) => {
this._finishNavigating();
throw err;
});
return this._afterPromiseFinishNavigating(this.recognize(url).then((matchedInstruction) => {
if (isBlank(matchedInstruction)) {
return false;
}
return this._reuse(matchedInstruction)
.then((_) => this._canActivate(matchedInstruction))
.then((result) => {
if (!result) {
return false;
}
return this._canDeactivate(matchedInstruction)
.then((result) => {
if (result) {
return this.commit(matchedInstruction)
.then((_) => {
this._emitNavigationFinish(matchedInstruction.accumulatedUrl);
return true;
});
}
});
});
}));
});
}
private _emitNavigationFinish(url): void { ObservableWrapper.callNext(this._subject, url); }
private _afterPromiseFinishNavigating(promise: Promise<any>): Promise<any> {
return PromiseWrapper.catchError(promise.then((_) => this._finishNavigating()), (err) => {
this._finishNavigating();
throw err;
});
}
_reuse(instruction): Promise<any> {
if (isBlank(this._outlet)) {
return _resolveToFalse;
}
return this._outlet.canReuse(instruction)
.then((result) => {
instruction.reuse = result;
if (isPresent(this._outlet.childRouter) && isPresent(instruction.child)) {
return this._outlet.childRouter._reuse(instruction.child);
}
});
}
private _canActivate(instruction: Instruction): Promise<boolean> {
return canActivateOne(instruction, this._currentInstruction);
}
private _canDeactivate(instruction: Instruction): Promise<boolean> {
if (isBlank(this._outlet)) {
return _resolveToTrue;
}
var next: Promise<boolean>;
if (isPresent(instruction) && instruction.reuse) {
next = _resolveToTrue;
} else {
next = this._outlet.canDeactivate(instruction);
}
return next.then((result) => {
if (result == false) {
return false;
}
if (isPresent(this._outlet.childRouter)) {
return this._outlet.childRouter._canDeactivate(isPresent(instruction) ? instruction.child :
null);
}
return true;
});
}
/**
* Updates this router and all descendant routers according to the given instruction
*/
commit(instruction: Instruction): Promise<any> {
this._currentInstruction = instruction;
if (isPresent(this._outlet)) {
return this._outlet.commit(instruction);
}
return _resolveToTrue;
}
_startNavigating(): void { this.navigating = true; }
_finishNavigating(): void { this.navigating = false; }
@ -149,24 +208,12 @@ export class Router {
subscribe(onNext): void { ObservableWrapper.subscribe(this._subject, onNext); }
/**
* Updates this router and all descendant routers according to the given instruction
*/
commit(instruction: Instruction): Promise<any> {
this._currentInstruction = instruction;
if (isPresent(this._outlet)) {
return this._outlet.activate(instruction);
}
return _resolveToTrue;
}
/**
* Removes the contents of this router's outlet and all descendant outlets
*/
deactivate(): Promise<any> {
deactivate(instruction: Instruction): Promise<any> {
if (isPresent(this._outlet)) {
return this._outlet.deactivate();
return this._outlet.deactivate(instruction);
}
return _resolveToTrue;
}
@ -185,11 +232,10 @@ export class Router {
* router has yet to successfully navigate.
*/
renavigate(): Promise<any> {
var destination = isBlank(this.previousUrl) ? this.lastNavigationAttempt : this.previousUrl;
if (isBlank(destination)) {
if (isBlank(this.lastNavigationAttempt)) {
return this._currentNavigation;
}
return this.navigate(destination);
return this.navigate(this.lastNavigationAttempt);
}
@ -288,3 +334,24 @@ function splitAndFlattenLinkParams(linkParams: List<any>): List<any> {
return accumulation;
}, []);
}
function canActivateOne(nextInstruction, currentInstruction): Promise<boolean> {
var next = _resolveToTrue;
if (isPresent(nextInstruction.child)) {
next = canActivateOne(nextInstruction.child,
isPresent(currentInstruction) ? currentInstruction.child : null);
}
return next.then((res) => {
if (res == false) {
return false;
}
if (nextInstruction.reuse) {
return true;
}
var hook = getCanActivateHook(nextInstruction.component);
if (isPresent(hook)) {
return hook(nextInstruction, currentInstruction);
}
return true;
});
}

View File

@ -1,4 +1,5 @@
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
import {StringMapWrapper} from 'angular2/src/facade/collection';
import {isBlank, isPresent} from 'angular2/src/facade/lang';
import {Directive, Attribute} from 'angular2/src/core/annotations/decorators';
@ -6,8 +7,9 @@ import {DynamicComponentLoader, ComponentRef, ElementRef} from 'angular2/core';
import {Injector, bind, Dependency, undefinedValue} from 'angular2/di';
import * as routerMod from './router';
import {Instruction, RouteParams} from './instruction'
import {Instruction, RouteParams} from './instruction';
import * as hookMod from './lifecycle_annotations';
import {hasLifecycleHook} from './route_lifecycle_reflector';
/**
* A router outlet is a placeholder that Angular dynamically fills based on the application's route.
@ -18,11 +20,10 @@ import {Instruction, RouteParams} from './instruction'
* <router-outlet></router-outlet>
* ```
*/
@Directive({
selector: 'router-outlet'
})
@Directive({selector: 'router-outlet'})
export class RouterOutlet {
private _childRouter: routerMod.Router = null;
childRouter: routerMod.Router = null;
private _componentRef: ComponentRef = null;
private _currentInstruction: Instruction = null;
@ -38,34 +39,100 @@ export class RouterOutlet {
/**
* Given an instruction, update the contents of this outlet.
*/
activate(instruction: Instruction): Promise<any> {
// if we're able to reuse the component, we just have to pass along the instruction to the
// component's router
// so it can propagate changes to its children
if ((instruction == this._currentInstruction || instruction.reuse) &&
isPresent(this._childRouter)) {
return this._childRouter.commit(instruction.child);
commit(instruction: Instruction): Promise<any> {
var next;
if (instruction.reuse) {
next = this._reuse(instruction);
} else {
next = this.deactivate(instruction).then((_) => this._activate(instruction));
}
return next.then((_) => this._commitChild(instruction));
}
private _commitChild(instruction: Instruction): Promise<any> {
if (isPresent(this.childRouter)) {
return this.childRouter.commit(instruction.child);
} else {
return PromiseWrapper.resolve(true);
}
}
private _activate(instruction: Instruction): Promise<any> {
var previousInstruction = this._currentInstruction;
this._currentInstruction = instruction;
this._childRouter = this._parentRouter.childRouter(instruction.component);
var params = new RouteParams(instruction.params());
var bindings = Injector.resolve(
[bind(RouteParams).toValue(params), bind(routerMod.Router).toValue(this._childRouter)]);
this.childRouter = this._parentRouter.childRouter(instruction.component);
return this.deactivate()
.then((_) => this._loader.loadNextToLocation(instruction.component, this._elementRef,
bindings))
var bindings = Injector.resolve([
bind(RouteParams)
.toValue(new RouteParams(instruction.params())),
bind(routerMod.Router).toValue(this.childRouter)
]);
return this._loader.loadNextToLocation(instruction.component, this._elementRef, bindings)
.then((componentRef) => {
this._componentRef = componentRef;
return this._childRouter.commit(instruction.child);
if (hasLifecycleHook(hookMod.onActivate, instruction.component)) {
return this._componentRef.instance.onActivate(instruction, previousInstruction);
}
});
}
deactivate(): Promise<any> {
return (isPresent(this._childRouter) ? this._childRouter.deactivate() :
PromiseWrapper.resolve(true))
/**
* Called by Router during recognition phase
*/
canDeactivate(nextInstruction: Instruction): Promise<boolean> {
if (isBlank(this._currentInstruction)) {
return PromiseWrapper.resolve(true);
}
if (hasLifecycleHook(hookMod.canDeactivate, this._currentInstruction.component)) {
return PromiseWrapper.resolve(
this._componentRef.instance.canDeactivate(nextInstruction, this._currentInstruction));
}
return PromiseWrapper.resolve(true);
}
/**
* Called by Router during recognition phase
*/
canReuse(nextInstruction: Instruction): Promise<boolean> {
var result;
if (isBlank(this._currentInstruction) ||
this._currentInstruction.component != nextInstruction.component) {
result = false;
} else if (hasLifecycleHook(hookMod.canReuse, this._currentInstruction.component)) {
result = this._componentRef.instance.canReuse(nextInstruction, this._currentInstruction);
} else {
result = nextInstruction == this._currentInstruction ||
StringMapWrapper.equals(nextInstruction.params(), this._currentInstruction.params());
}
return PromiseWrapper.resolve(result);
}
private _reuse(instruction): Promise<any> {
var previousInstruction = this._currentInstruction;
this._currentInstruction = instruction;
return PromiseWrapper.resolve(
hasLifecycleHook(hookMod.onReuse, this._currentInstruction.component) ?
this._componentRef.instance.onReuse(instruction, previousInstruction) :
true);
}
deactivate(nextInstruction: Instruction): Promise<any> {
return (isPresent(this.childRouter) ?
this.childRouter.deactivate(isPresent(nextInstruction) ? nextInstruction.child :
null) :
PromiseWrapper.resolve(true))
.then((_) => {
if (isPresent(this._componentRef) && isPresent(this._currentInstruction) &&
hasLifecycleHook(hookMod.onDeactivate, this._currentInstruction.component)) {
return this._componentRef.instance.onDeactivate(nextInstruction,
this._currentInstruction);
}
})
.then((_) => {
if (isPresent(this._componentRef)) {
this._componentRef.dispose();
@ -73,9 +140,4 @@ export class RouterOutlet {
}
});
}
canDeactivate(instruction: Instruction): Promise<boolean> {
// TODO: how to get ahold of the component instance here?
return PromiseWrapper.resolve(true);
}
}