From de37729823077bf6cecbcc5e8369e1c6b01d8b80 Mon Sep 17 00:00:00 2001 From: Brian Ford Date: Sun, 30 Aug 2015 21:25:46 -0700 Subject: [PATCH] feat(router): implement Router.isRouteActive --- modules/angular1_router/src/ng_outlet.js | 59 ++---- modules/angular2/src/router/router.ts | 145 +++++++++++--- modules/angular2/src/router/router_outlet.ts | 184 +++++++----------- .../angular2/test/router/router_link_spec.ts | 1 + modules/angular2/test/router/router_spec.ts | 44 +++-- 5 files changed, 241 insertions(+), 192 deletions(-) diff --git a/modules/angular1_router/src/ng_outlet.js b/modules/angular1_router/src/ng_outlet.js index 3d170d5952..92848f5339 100644 --- a/modules/angular1_router/src/ng_outlet.js +++ b/modules/angular1_router/src/ng_outlet.js @@ -132,62 +132,41 @@ function ngOutletDirective($animate, $injector, $q, $router, $componentMapper, $ } } - router.registerOutlet({ - commit: function (instruction) { + router.registerPrimaryOutlet({ + reuse: function (instruction) { var next = $q.when(true); - var componentInstruction = instruction.component; - if (componentInstruction.reuse) { - var previousInstruction = currentInstruction; - currentInstruction = componentInstruction; - if (currentController.onReuse) { - next = $q.when(currentController.onReuse(currentInstruction, previousInstruction)); - } - } else { - var self = this; - next = this.deactivate(instruction).then(function () { - return self.activate(componentInstruction); - }); + var previousInstruction = currentInstruction; + currentInstruction = instruction; + if (currentController.onReuse) { + next = $q.when(currentController.onReuse(currentInstruction, previousInstruction)); } - return next.then(function () { - if (childRouter) { - return childRouter.commit(instruction.child); - } else { - return $q.when(true); - } - }); + + return next; }, canReuse: function (nextInstruction) { var result; - var componentInstruction = nextInstruction.component; if (!currentInstruction || - currentInstruction.componentType !== componentInstruction.componentType) { + currentInstruction.componentType !== nextInstruction.componentType) { result = false; } else if (currentController.canReuse) { - result = currentController.canReuse(componentInstruction, currentInstruction); + result = currentController.canReuse(nextInstruction, currentInstruction); } else { - result = componentInstruction === currentInstruction || - angular.equals(componentInstruction.params, currentInstruction.params); + result = nextInstruction === currentInstruction || + angular.equals(nextInstruction.params, currentInstruction.params); } - return $q.when(result).then(function (result) { - // TODO: this is a hack - componentInstruction.reuse = result; - return result; - }); + return $q.when(result); }, canDeactivate: function (instruction) { if (currentInstruction && currentController && currentController.canDeactivate) { - return $q.when(currentController.canDeactivate(instruction && instruction.component, currentInstruction)); + return $q.when(currentController.canDeactivate(instruction, currentInstruction)); } return $q.when(true); }, deactivate: function (instruction) { - // todo(shahata): childRouter.dectivate, dispose component? - var result = $q.when(); - return result.then(function () { - if (currentController && currentController.onDeactivate) { - return currentController.onDeactivate(instruction && instruction.component, currentInstruction); - } - }); + if (currentController && currentController.onDeactivate) { + return $q.when(currentController.onDeactivate(instruction, currentInstruction)); + } + return $q.when(); }, activate: function (instruction) { var previousInstruction = currentInstruction; @@ -228,7 +207,7 @@ function ngOutletDirective($animate, $injector, $q, $router, $componentMapper, $ } }); } - }, outletName); + }); } } diff --git a/modules/angular2/src/router/router.ts b/modules/angular2/src/router/router.ts index b5157e50c1..a413660878 100644 --- a/modules/angular2/src/router/router.ts +++ b/modules/angular2/src/router/router.ts @@ -54,11 +54,16 @@ export class Router { lastNavigationAttempt: string; private _currentInstruction: Instruction = null; + private _currentNavigation: Promise = _resolveToTrue; private _outlet: RouterOutlet = null; - private _auxOutlets: Map = new Map(); + + private _auxRouters: Map = new Map(); + private _childRouter: Router; + private _subject: EventEmitter = new EventEmitter(); + constructor(public registry: RouteRegistry, public _pipeline: Pipeline, public parent: Router, public hostComponent: any) {} @@ -67,25 +72,74 @@ export class Router { * Constructs a child router. You probably don't need to use this unless you're writing a reusable * component. */ - childRouter(hostComponent: any): Router { return new ChildRouter(this, hostComponent); } + childRouter(hostComponent: any): Router { + return this._childRouter = new ChildRouter(this, hostComponent); + } /** - * Register an object to notify of route changes. You probably don't need to use this unless - * you're writing a reusable component. + * Constructs a child router. You probably don't need to use this unless you're writing a reusable + * component. */ - registerOutlet(outlet: RouterOutlet): Promise { + auxRouter(hostComponent: any): Router { return new ChildRouter(this, hostComponent); } + + /** + * Register an outlet to notified of primary route changes. + * + * You probably don't need to use this unless you're writing a reusable component. + */ + registerPrimaryOutlet(outlet: RouterOutlet): Promise { if (isPresent(outlet.name)) { - this._auxOutlets.set(outlet.name, outlet); - } else { - this._outlet = outlet; + throw new BaseException(`registerAuxOutlet expects to be called with an unnamed outlet.`); } + + this._outlet = outlet; if (isPresent(this._currentInstruction)) { - return outlet.commit(this._currentInstruction); + return this.commit(this._currentInstruction, false); } return _resolveToTrue; } + /** + * Register an outlet to notified of auxiliary route changes. + * + * You probably don't need to use this unless you're writing a reusable component. + */ + registerAuxOutlet(outlet: RouterOutlet): Promise { + var outletName = outlet.name; + if (isBlank(outletName)) { + throw new BaseException(`registerAuxOutlet expects to be called with an outlet with a name.`); + } + + // TODO... + // what is the host of an aux route??? + var router = this.auxRouter(this.hostComponent); + + this._auxRouters.set(outletName, router); + router._outlet = outlet; + + var auxInstruction; + if (isPresent(this._currentInstruction) && + isPresent(auxInstruction = this._currentInstruction.auxInstruction[outletName])) { + return router.commit(auxInstruction); + } + return _resolveToTrue; + } + + + /** + * Given an instruction, returns `true` if the instruction is currently active, + * otherwise `false`. + */ + isRouteActive(instruction: Instruction): boolean { + var router = this; + while (isPresent(router.parent) && isPresent(instruction.child)) { + router = router.parent; + instruction = instruction.child; + } + return isPresent(this._currentInstruction) && + this._currentInstruction.component == instruction.component; + } /** * Dynamically update the routing configuration and trigger a navigation. @@ -143,7 +197,7 @@ export class Router { _navigate(instruction: Instruction, _skipLocationChange: boolean): Promise { return this._settleInstruction(instruction) - .then((_) => this._reuse(instruction)) + .then((_) => this._canReuse(instruction)) .then((_) => this._canActivate(instruction)) .then((result) => { if (!result) { @@ -190,14 +244,17 @@ export class Router { }); } - _reuse(instruction: Instruction): Promise { + /* + * Recursively set reuse flags + */ + _canReuse(instruction: Instruction): Promise { if (isBlank(this._outlet)) { return _resolveToFalse; } - return this._outlet.canReuse(instruction) + return this._outlet.canReuse(instruction.component) .then((result) => { - if (isPresent(this._outlet.childRouter) && isPresent(instruction.child)) { - return this._outlet.childRouter._reuse(instruction.child); + if (isPresent(this._childRouter) && isPresent(instruction.child)) { + return this._childRouter._canReuse(instruction.child); } }); } @@ -211,19 +268,26 @@ export class Router { return _resolveToTrue; } var next: Promise; - if (isPresent(instruction) && instruction.component.reuse) { + var childInstruction: Instruction = null; + var reuse: boolean = false; + var componentInstruction: ComponentInstruction = null; + if (isPresent(instruction)) { + childInstruction = instruction.child; + componentInstruction = instruction.component; + reuse = instruction.component.reuse; + } + if (reuse) { next = _resolveToTrue; } else { - next = this._outlet.canDeactivate(instruction); + next = this._outlet.canDeactivate(componentInstruction); } // TODO: aux route lifecycle hooks return next.then((result) => { if (result == false) { return false; } - if (isPresent(this._outlet.childRouter)) { - return this._outlet.childRouter._canDeactivate(isPresent(instruction) ? instruction.child : - null); + if (isPresent(this._childRouter)) { + return this._childRouter._canDeactivate(childInstruction); } return true; }); @@ -234,13 +298,29 @@ export class Router { */ commit(instruction: Instruction, _skipLocationChange: boolean = false): Promise { this._currentInstruction = instruction; - var next = _resolveToTrue; + var next: Promise = _resolveToTrue; if (isPresent(this._outlet)) { - next = this._outlet.commit(instruction); + var componentInstruction = instruction.component; + if (componentInstruction.reuse) { + next = this._outlet.reuse(componentInstruction); + } else { + next = + this.deactivate(instruction).then((_) => this._outlet.activate(componentInstruction)); + } + if (isPresent(instruction.child)) { + next = next.then((_) => { + if (isPresent(this._childRouter)) { + return this._childRouter.commit(instruction.child); + } + }); + } } + var promises = []; - MapWrapper.forEach(this._auxOutlets, - (outlet, _) => { promises.push(outlet.commit(instruction)); }); + MapWrapper.forEach(this._auxRouters, (router, name) => { + promises.push(router.commit(instruction.auxInstruction[name])); + }); + return next.then((_) => PromiseWrapper.all(promises)); } @@ -262,10 +342,23 @@ export class Router { * Removes the contents of this router's outlet and all descendant outlets */ deactivate(instruction: Instruction): Promise { - if (isPresent(this._outlet)) { - return this._outlet.deactivate(instruction); + var childInstruction: Instruction = null; + var componentInstruction: ComponentInstruction = null; + if (isPresent(instruction)) { + childInstruction = instruction.child; + componentInstruction = instruction.component; } - return _resolveToTrue; + var next: Promise = _resolveToTrue; + if (isPresent(this._childRouter)) { + next = this._childRouter.deactivate(childInstruction); + } + if (isPresent(this._outlet)) { + next = next.then((_) => this._outlet.deactivate(componentInstruction)); + } + + // TODO: handle aux routes + + return next; } diff --git a/modules/angular2/src/router/router_outlet.ts b/modules/angular2/src/router/router_outlet.ts index 7ea11ed8d4..01cc15f312 100644 --- a/modules/angular2/src/router/router_outlet.ts +++ b/modules/angular2/src/router/router_outlet.ts @@ -1,17 +1,19 @@ import {Promise, PromiseWrapper} from 'angular2/src/core/facade/async'; import {StringMapWrapper} from 'angular2/src/core/facade/collection'; -import {isBlank, isPresent} from 'angular2/src/core/facade/lang'; +import {isBlank, isPresent, BaseException} from 'angular2/src/core/facade/lang'; import {Directive, Attribute} from '../core/metadata'; import {DynamicComponentLoader, ComponentRef, ElementRef} from 'angular2/core'; import {Injector, bind, Dependency, UNDEFINED} from 'angular2/di'; import * as routerMod from './router'; -import {Instruction, ComponentInstruction, RouteParams} from './instruction'; +import {ComponentInstruction, RouteParams} from './instruction'; import {ROUTE_DATA} from './route_data'; import * as hookMod from './lifecycle_annotations'; import {hasLifecycleHook} from './route_lifecycle_reflector'; +let _resolveToTrue = PromiseWrapper.resolve(true); + /** * A router outlet is a placeholder that Angular dynamically fills based on the application's route. * @@ -23,7 +25,6 @@ import {hasLifecycleHook} from './route_lifecycle_reflector'; */ @Directive({selector: 'router-outlet'}) export class RouterOutlet { - childRouter: routerMod.Router = null; name: string = null; private _componentRef: ComponentRef = null; @@ -33,141 +34,96 @@ export class RouterOutlet { private _parentRouter: routerMod.Router, @Attribute('name') nameAttr: string) { if (isPresent(nameAttr)) { this.name = nameAttr; - } - this._parentRouter.registerOutlet(this); - } - - /** - * Given an instruction, update the contents of this outlet. - */ - commit(instruction: Instruction): Promise { - instruction = this._getInstruction(instruction); - var componentInstruction = instruction.component; - if (isBlank(componentInstruction)) { - return PromiseWrapper.resolve(true); - } - var next; - if (componentInstruction.reuse) { - next = this._reuse(componentInstruction); + this._parentRouter.registerAuxOutlet(this); } else { - next = this.deactivate(instruction).then((_) => this._activate(componentInstruction)); - } - return next.then((_) => this._commitChild(instruction)); - } - - private _getInstruction(instruction: Instruction): Instruction { - if (isPresent(this.name)) { - return instruction.auxInstruction[this.name]; - } else { - return instruction; + this._parentRouter.registerPrimaryOutlet(this); } } - private _commitChild(instruction: Instruction): Promise { - if (isPresent(this.childRouter)) { - return this.childRouter.commit(instruction.child); - } else { - return PromiseWrapper.resolve(true); - } - } - - private _activate(instruction: ComponentInstruction): Promise { + activate(nextInstruction: ComponentInstruction): Promise { var previousInstruction = this._currentInstruction; - this._currentInstruction = instruction; - var componentType = instruction.componentType; - this.childRouter = this._parentRouter.childRouter(componentType); + this._currentInstruction = nextInstruction; + var componentType = nextInstruction.componentType; + var childRouter = this._parentRouter.childRouter(componentType); var bindings = Injector.resolve([ bind(ROUTE_DATA) - .toValue(instruction.routeData()), - bind(RouteParams).toValue(new RouteParams(instruction.params)), - bind(routerMod.Router).toValue(this.childRouter) + .toValue(nextInstruction.routeData()), + bind(RouteParams).toValue(new RouteParams(nextInstruction.params)), + bind(routerMod.Router).toValue(childRouter) ]); return this._loader.loadNextToLocation(componentType, this._elementRef, bindings) .then((componentRef) => { this._componentRef = componentRef; if (hasLifecycleHook(hookMod.onActivate, componentType)) { - return this._componentRef.instance.onActivate(instruction, previousInstruction); + return this._componentRef.instance.onActivate(nextInstruction, previousInstruction); } }); } - - /** - * Called by Router during recognition phase - */ - canDeactivate(nextInstruction: Instruction): Promise { - if (isBlank(this._currentInstruction)) { - return PromiseWrapper.resolve(true); - } - var outletInstruction = this._getInstruction(nextInstruction); - if (hasLifecycleHook(hookMod.canDeactivate, this._currentInstruction.componentType)) { - return PromiseWrapper.resolve(this._componentRef.instance.canDeactivate( - isPresent(outletInstruction) ? outletInstruction.component : null, - this._currentInstruction)); - } - return PromiseWrapper.resolve(true); - } - - - /** - * Called by Router during recognition phase - */ - canReuse(nextInstruction: Instruction): Promise { - var result; - - var outletInstruction = this._getInstruction(nextInstruction); - var componentInstruction = outletInstruction.component; - - if (isBlank(this._currentInstruction) || - this._currentInstruction.componentType != componentInstruction.componentType) { - result = false; - } else if (hasLifecycleHook(hookMod.canReuse, this._currentInstruction.componentType)) { - result = this._componentRef.instance.canReuse(componentInstruction, this._currentInstruction); - } else { - result = - componentInstruction == this._currentInstruction || - (isPresent(componentInstruction.params) && isPresent(this._currentInstruction.params) && - StringMapWrapper.equals(componentInstruction.params, this._currentInstruction.params)); - } - return PromiseWrapper.resolve(result).then((result) => { - // TODO: this is a hack - componentInstruction.reuse = result; - return result; - }); - } - - - private _reuse(instruction: ComponentInstruction): Promise { + reuse(nextInstruction: ComponentInstruction): Promise { var previousInstruction = this._currentInstruction; - this._currentInstruction = instruction; + this._currentInstruction = nextInstruction; + + if (isBlank(this._componentRef)) { + throw new BaseException(`Cannot reuse an outlet that does not contain a component.`); + } return PromiseWrapper.resolve( hasLifecycleHook(hookMod.onReuse, this._currentInstruction.componentType) ? - this._componentRef.instance.onReuse(instruction, previousInstruction) : + this._componentRef.instance.onReuse(nextInstruction, previousInstruction) : true); } + deactivate(nextInstruction: ComponentInstruction): Promise { + var next = _resolveToTrue; + if (isPresent(this._componentRef) && isPresent(this._currentInstruction) && + hasLifecycleHook(hookMod.onDeactivate, this._currentInstruction.componentType)) { + next = PromiseWrapper.resolve( + this._componentRef.instance.onDeactivate(nextInstruction, this._currentInstruction)); + } + return next.then((_) => { + if (isPresent(this._componentRef)) { + this._componentRef.dispose(); + this._componentRef = null; + } + }); + } + + /** + * Called by Router during recognition phase + */ + canDeactivate(nextInstruction: ComponentInstruction): Promise { + if (isBlank(this._currentInstruction)) { + return _resolveToTrue; + } + if (hasLifecycleHook(hookMod.canDeactivate, this._currentInstruction.componentType)) { + return PromiseWrapper.resolve( + this._componentRef.instance.canDeactivate(nextInstruction, this._currentInstruction)); + } + return _resolveToTrue; + } - deactivate(nextInstruction: Instruction): Promise { - var outletInstruction = this._getInstruction(nextInstruction); - var componentInstruction = isPresent(outletInstruction) ? outletInstruction.component : null; - return (isPresent(this.childRouter) ? - this.childRouter.deactivate(isPresent(outletInstruction) ? outletInstruction.child : - null) : - PromiseWrapper.resolve(true)) - .then((_) => { - if (isPresent(this._componentRef) && isPresent(this._currentInstruction) && - hasLifecycleHook(hookMod.onDeactivate, this._currentInstruction.componentType)) { - return this._componentRef.instance.onDeactivate(componentInstruction, - this._currentInstruction); - } - }) - .then((_) => { - if (isPresent(this._componentRef)) { - this._componentRef.dispose(); - this._componentRef = null; - } - }); + /** + * Called by Router during recognition phase + */ + canReuse(nextInstruction: ComponentInstruction): Promise { + var result; + + if (isBlank(this._currentInstruction) || + this._currentInstruction.componentType != nextInstruction.componentType) { + result = false; + } else if (hasLifecycleHook(hookMod.canReuse, this._currentInstruction.componentType)) { + result = this._componentRef.instance.canReuse(nextInstruction, this._currentInstruction); + } else { + result = nextInstruction == this._currentInstruction || + (isPresent(nextInstruction.params) && isPresent(this._currentInstruction.params) && + StringMapWrapper.equals(nextInstruction.params, this._currentInstruction.params)); + } + return PromiseWrapper.resolve(result).then((result) => { + // TODO: this is a hack + nextInstruction.reuse = result; + return result; + }); } } diff --git a/modules/angular2/test/router/router_link_spec.ts b/modules/angular2/test/router/router_link_spec.ts index dc5d6fc487..26423b299a 100644 --- a/modules/angular2/test/router/router_link_spec.ts +++ b/modules/angular2/test/router/router_link_spec.ts @@ -105,6 +105,7 @@ function makeDummyLocation() { function makeDummyRouter() { var dr = new SpyRouter(); dr.spy('generate').andCallFake((routeParams) => dummyInstruction); + dr.spy('isRouteActive').andCallFake((_) => false); dr.spy('navigateInstruction'); return dr; } diff --git a/modules/angular2/test/router/router_spec.ts b/modules/angular2/test/router/router_spec.ts index b06451cf84..1419cad5b8 100644 --- a/modules/angular2/test/router/router_spec.ts +++ b/modules/angular2/test/router/router_spec.ts @@ -53,9 +53,9 @@ export function main() { var outlet = makeDummyOutlet(); router.config([new Route({path: '/', component: DummyComponent})]) - .then((_) => router.registerOutlet(outlet)) + .then((_) => router.registerPrimaryOutlet(outlet)) .then((_) => { - expect(outlet.spy('commit')).toHaveBeenCalled(); + expect(outlet.spy('activate')).toHaveBeenCalled(); expect(location.urlChanges).toEqual([]); async.done(); }); @@ -66,11 +66,11 @@ export function main() { inject([AsyncTestCompleter], (async) => { var outlet = makeDummyOutlet(); - router.registerOutlet(outlet) + router.registerPrimaryOutlet(outlet) .then((_) => router.config([new Route({path: '/a', component: DummyComponent})])) .then((_) => router.navigate('/a')) .then((_) => { - expect(outlet.spy('commit')).toHaveBeenCalled(); + expect(outlet.spy('activate')).toHaveBeenCalled(); expect(location.urlChanges).toEqual(['/a']); async.done(); }); @@ -80,11 +80,11 @@ export function main() { inject([AsyncTestCompleter], (async) => { var outlet = makeDummyOutlet(); - router.registerOutlet(outlet) + router.registerPrimaryOutlet(outlet) .then((_) => router.config([new Route({path: '/b', component: DummyComponent})])) .then((_) => router.navigate('/b', true)) .then((_) => { - expect(outlet.spy('commit')).toHaveBeenCalled(); + expect(outlet.spy('activate')).toHaveBeenCalled(); expect(location.urlChanges).toEqual([]); async.done(); }); @@ -94,14 +94,14 @@ export function main() { it('should navigate after being configured', inject([AsyncTestCompleter], (async) => { var outlet = makeDummyOutlet(); - router.registerOutlet(outlet) + router.registerPrimaryOutlet(outlet) .then((_) => router.navigate('/a')) .then((_) => { - expect(outlet.spy('commit')).not.toHaveBeenCalled(); + expect(outlet.spy('activate')).not.toHaveBeenCalled(); return router.config([new Route({path: '/a', component: DummyComponent})]); }) .then((_) => { - expect(outlet.spy('commit')).toHaveBeenCalled(); + expect(outlet.spy('activate')).toHaveBeenCalled(); async.done(); }); })); @@ -142,13 +142,33 @@ export function main() { inject([AsyncTestCompleter], (async) => { var outlet = makeDummyOutlet(); - router.registerOutlet(outlet); + router.registerPrimaryOutlet(outlet); router.config([new AsyncRoute({path: '/first', loader: loader, as: 'FirstCmp'})]); var instruction = router.generate(['/FirstCmp']); router.navigateInstruction(instruction) .then((_) => { - expect(outlet.spy('commit')).toHaveBeenCalled(); + expect(outlet.spy('activate')).toHaveBeenCalled(); + async.done(); + }); + })); + + it('should return whether a given instruction is active with isRouteActive', + inject([AsyncTestCompleter], (async) => { + var outlet = makeDummyOutlet(); + + router.registerPrimaryOutlet(outlet) + .then((_) => router.config([ + new Route({path: '/a', component: DummyComponent, as: 'A'}), + new Route({path: '/b', component: DummyComponent, as: 'B'}) + ])) + .then((_) => router.navigate('/a')) + .then((_) => { + var instruction = router.generate(['/A']); + var otherInstruction = router.generate(['/B']); + + expect(router.isRouteActive(instruction)).toEqual(true); + expect(router.isRouteActive(otherInstruction)).toEqual(false); async.done(); }); })); @@ -213,7 +233,7 @@ function makeDummyOutlet() { ref.spy('canActivate').andCallFake((_) => PromiseWrapper.resolve(true)); ref.spy('canReuse').andCallFake((_) => PromiseWrapper.resolve(false)); ref.spy('canDeactivate').andCallFake((_) => PromiseWrapper.resolve(true)); - ref.spy('commit').andCallFake((_) => PromiseWrapper.resolve(true)); + ref.spy('activate').andCallFake((_) => PromiseWrapper.resolve(true)); return ref; }