fix(router): reuse common parent components
This commit is contained in:
parent
aff85b5037
commit
ac80df0959
|
@ -19,8 +19,10 @@ export class Instruction {
|
||||||
router:any;
|
router:any;
|
||||||
matchedUrl:string;
|
matchedUrl:string;
|
||||||
params:Map<string, string>;
|
params:Map<string, string>;
|
||||||
|
reuse:boolean;
|
||||||
|
|
||||||
constructor({params, component, children, matchedUrl}:{params:StringMap, component:any, children:Map, matchedUrl:string} = {}) {
|
constructor({params, component, children, matchedUrl}:{params:StringMap, component:any, children:Map, matchedUrl:string} = {}) {
|
||||||
|
this.reuse = false;
|
||||||
this.matchedUrl = matchedUrl;
|
this.matchedUrl = matchedUrl;
|
||||||
if (isPresent(children)) {
|
if (isPresent(children)) {
|
||||||
this._children = children;
|
this._children = children;
|
||||||
|
@ -51,13 +53,42 @@ export class Instruction {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes a function:
|
* Does a synchronous, breadth-first traversal of the graph of instructions.
|
||||||
|
* Takes a function with signature:
|
||||||
* (parent:Instruction, child:Instruction) => {}
|
* (parent:Instruction, child:Instruction) => {}
|
||||||
*/
|
*/
|
||||||
traverseSync(fn:Function) {
|
traverseSync(fn:Function) {
|
||||||
this.forEachChild((childInstruction, _) => fn(this, childInstruction));
|
this.forEachChild((childInstruction, _) => fn(this, childInstruction));
|
||||||
this.forEachChild((childInstruction, _) => childInstruction.traverseSync(fn));
|
this.forEachChild((childInstruction, _) => childInstruction.traverseSync(fn));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does an asynchronous, breadth-first traversal of the graph of instructions.
|
||||||
|
* Takes a function with signature:
|
||||||
|
* (child:Instruction, parentOutletName:string) => {}
|
||||||
|
*/
|
||||||
|
traverseAsync(fn:Function) {
|
||||||
|
return this.mapChildrenAsync(fn)
|
||||||
|
.then((_) => this.mapChildrenAsync((childInstruction, _) => childInstruction.traverseAsync(fn)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes a currently active instruction and sets a reuse flag on this instruction
|
||||||
|
*/
|
||||||
|
reuseComponentsFrom(oldInstruction:Instruction) {
|
||||||
|
this.forEachChild((childInstruction, outletName) => {
|
||||||
|
var oldInstructionChild = oldInstruction.getChildInstruction(outletName);
|
||||||
|
if (shouldReuseComponent(childInstruction, oldInstructionChild)) {
|
||||||
|
childInstruction.reuse = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldReuseComponent(instr1:Instruction, instr2:Instruction) {
|
||||||
|
return instr1.component == instr2.component &&
|
||||||
|
StringMapWrapper.equals(instr1.params, instr2.params);
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapObjAsync(obj:StringMap, fn) {
|
function mapObjAsync(obj:StringMap, fn) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import {Promise, PromiseWrapper, EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
|
import {Promise, PromiseWrapper, EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
|
||||||
import {Map, MapWrapper, List, ListWrapper} from 'angular2/src/facade/collection';
|
import {Map, MapWrapper, List, ListWrapper} from 'angular2/src/facade/collection';
|
||||||
import {isBlank, Type} from 'angular2/src/facade/lang';
|
import {isBlank, isPresent, Type} from 'angular2/src/facade/lang';
|
||||||
|
|
||||||
import {RouteRegistry} from './route_registry';
|
import {RouteRegistry} from './route_registry';
|
||||||
import {Pipeline} from './pipeline';
|
import {Pipeline} from './pipeline';
|
||||||
|
@ -24,6 +24,8 @@ export class Router {
|
||||||
lastNavigationAttempt: string;
|
lastNavigationAttempt: string;
|
||||||
previousUrl:string;
|
previousUrl:string;
|
||||||
|
|
||||||
|
_currentInstruction:Instruction;
|
||||||
|
|
||||||
_pipeline:Pipeline;
|
_pipeline:Pipeline;
|
||||||
_registry:RouteRegistry;
|
_registry:RouteRegistry;
|
||||||
_outlets:Map<any, RouterOutlet>;
|
_outlets:Map<any, RouterOutlet>;
|
||||||
|
@ -42,6 +44,7 @@ export class Router {
|
||||||
this._registry = registry;
|
this._registry = registry;
|
||||||
this._pipeline = pipeline;
|
this._pipeline = pipeline;
|
||||||
this._subject = new EventEmitter();
|
this._subject = new EventEmitter();
|
||||||
|
this._currentInstruction = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -61,7 +64,11 @@ export class Router {
|
||||||
*/
|
*/
|
||||||
registerOutlet(outlet:RouterOutlet, name = 'default'):Promise {
|
registerOutlet(outlet:RouterOutlet, name = 'default'):Promise {
|
||||||
MapWrapper.set(this._outlets, name, outlet);
|
MapWrapper.set(this._outlets, name, outlet);
|
||||||
return this.renavigate();
|
if (isPresent(this._currentInstruction)) {
|
||||||
|
var childInstruction = this._currentInstruction.getChildInstruction(name);
|
||||||
|
return outlet.activate(childInstruction);
|
||||||
|
}
|
||||||
|
return PromiseWrapper.resolve(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -107,23 +114,26 @@ export class Router {
|
||||||
|
|
||||||
this.lastNavigationAttempt = url;
|
this.lastNavigationAttempt = url;
|
||||||
|
|
||||||
var instruction = this.recognize(url);
|
var matchedInstruction = this.recognize(url);
|
||||||
|
|
||||||
if (isBlank(instruction)) {
|
if (isBlank(matchedInstruction)) {
|
||||||
return PromiseWrapper.resolve(false);
|
return PromiseWrapper.resolve(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
instruction.router = this;
|
if(isPresent(this._currentInstruction)) {
|
||||||
|
matchedInstruction.reuseComponentsFrom(this._currentInstruction);
|
||||||
|
}
|
||||||
|
|
||||||
|
matchedInstruction.router = this;
|
||||||
this._startNavigating();
|
this._startNavigating();
|
||||||
|
|
||||||
var result = this._pipeline.process(instruction)
|
var result = this._pipeline.process(matchedInstruction)
|
||||||
.then((_) => {
|
.then((_) => {
|
||||||
this._location.go(instruction.matchedUrl);
|
this._location.go(matchedInstruction.matchedUrl);
|
||||||
})
|
ObservableWrapper.callNext(this._subject, matchedInstruction.matchedUrl);
|
||||||
.then((_) => {
|
this._finishNavigating();
|
||||||
ObservableWrapper.callNext(this._subject, instruction.matchedUrl);
|
this._currentInstruction = matchedInstruction;
|
||||||
})
|
});
|
||||||
.then((_) => this._finishNavigating());
|
|
||||||
|
|
||||||
PromiseWrapper.catchError(result, (_) => this._finishNavigating());
|
PromiseWrapper.catchError(result, (_) => this._finishNavigating());
|
||||||
|
|
||||||
|
@ -148,7 +158,11 @@ export class Router {
|
||||||
|
|
||||||
activateOutlets(instruction:Instruction):Promise {
|
activateOutlets(instruction:Instruction):Promise {
|
||||||
return this._queryOutlets((outlet, name) => {
|
return this._queryOutlets((outlet, name) => {
|
||||||
return outlet.activate(instruction.getChildInstruction(name));
|
var childInstruction = instruction.getChildInstruction(name);
|
||||||
|
if (childInstruction.reuse) {
|
||||||
|
return PromiseWrapper.resolve(true);
|
||||||
|
}
|
||||||
|
return outlet.activate(childInstruction);
|
||||||
})
|
})
|
||||||
.then((_) => instruction.mapChildrenAsync((instruction, _) => {
|
.then((_) => instruction.mapChildrenAsync((instruction, _) => {
|
||||||
return instruction.router.activateOutlets(instruction);
|
return instruction.router.activateOutlets(instruction);
|
||||||
|
|
|
@ -31,6 +31,8 @@ import {Location} from 'angular2/src/router/location';
|
||||||
import {RouteRegistry} from 'angular2/src/router/route_registry';
|
import {RouteRegistry} from 'angular2/src/router/route_registry';
|
||||||
import {DirectiveMetadataReader} from 'angular2/src/core/compiler/directive_metadata_reader';
|
import {DirectiveMetadataReader} from 'angular2/src/core/compiler/directive_metadata_reader';
|
||||||
|
|
||||||
|
var teamCmpCount;
|
||||||
|
|
||||||
export function main() {
|
export function main() {
|
||||||
describe('Outlet Directive', () => {
|
describe('Outlet Directive', () => {
|
||||||
|
|
||||||
|
@ -51,6 +53,7 @@ export function main() {
|
||||||
ctx = new MyComp();
|
ctx = new MyComp();
|
||||||
rtr = router;
|
rtr = router;
|
||||||
location = loc;
|
location = loc;
|
||||||
|
teamCmpCount = 0;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function compile(template:string = "<router-outlet></router-outlet>") {
|
function compile(template:string = "<router-outlet></router-outlet>") {
|
||||||
|
@ -132,6 +135,7 @@ export function main() {
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
it('should generate link hrefs without params', inject([AsyncTestCompleter], (async) => {
|
it('should generate link hrefs without params', inject([AsyncTestCompleter], (async) => {
|
||||||
compile('<a href="hello" router-link="user"></a>')
|
compile('<a href="hello" router-link="user"></a>')
|
||||||
.then((_) => rtr.config({'path': '/user', 'component': UserCmp, 'as': 'user'}))
|
.then((_) => rtr.config({'path': '/user', 'component': UserCmp, 'as': 'user'}))
|
||||||
|
@ -143,6 +147,26 @@ export function main() {
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
it('should reuse common parent components', inject([AsyncTestCompleter, Location], (async, location) => {
|
||||||
|
compile()
|
||||||
|
.then((_) => rtr.config({'path': '/team/:id', 'component': TeamCmp }))
|
||||||
|
.then((_) => rtr.navigate('/team/angular/user/rado'))
|
||||||
|
.then((_) => {
|
||||||
|
view.detectChanges();
|
||||||
|
expect(teamCmpCount).toBe(1);
|
||||||
|
expect(view.rootNodes).toHaveText('team angular { hello rado }');
|
||||||
|
})
|
||||||
|
.then((_) => rtr.navigate('/team/angular/user/victor'))
|
||||||
|
.then((_) => {
|
||||||
|
view.detectChanges();
|
||||||
|
expect(teamCmpCount).toBe(1);
|
||||||
|
expect(view.rootNodes).toHaveText('team angular { hello victor }');
|
||||||
|
async.done();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
it('should generate link hrefs with params', inject([AsyncTestCompleter], (async) => {
|
it('should generate link hrefs with params', inject([AsyncTestCompleter], (async) => {
|
||||||
ctx.name = 'brian';
|
ctx.name = 'brian';
|
||||||
compile('<a href="hello" router-link="user" [router-params]="{name: name}">{{name}}</a>')
|
compile('<a href="hello" router-link="user" [router-params]="{name: name}">{{name}}</a>')
|
||||||
|
@ -242,6 +266,27 @@ class ParentCmp {
|
||||||
constructor() {}
|
constructor() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'team-cmp'
|
||||||
|
})
|
||||||
|
@View({
|
||||||
|
template: "team {{id}} { <router-outlet></router-outlet> }",
|
||||||
|
directives: [RouterOutlet]
|
||||||
|
})
|
||||||
|
@RouteConfig([{
|
||||||
|
path: '/user/:name',
|
||||||
|
component: UserCmp
|
||||||
|
}])
|
||||||
|
class TeamCmp {
|
||||||
|
id:string;
|
||||||
|
constructor(params:RouteParams) {
|
||||||
|
this.id = params.get('id');
|
||||||
|
teamCmpCount += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-comp'
|
selector: 'my-comp'
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue