Skip to content

Commit ac80df0

Browse files
committed
fix(router): reuse common parent components
1 parent aff85b5 commit ac80df0

File tree

3 files changed

+104
-14
lines changed

3 files changed

+104
-14
lines changed

modules/angular2/src/router/instruction.js

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ export class Instruction {
1919
router:any;
2020
matchedUrl:string;
2121
params:Map<string, string>;
22+
reuse:boolean;
2223

2324
constructor({params, component, children, matchedUrl}:{params:StringMap, component:any, children:Map, matchedUrl:string} = {}) {
25+
this.reuse = false;
2426
this.matchedUrl = matchedUrl;
2527
if (isPresent(children)) {
2628
this._children = children;
@@ -51,13 +53,42 @@ export class Instruction {
5153
}
5254

5355
/**
54-
* Takes a function:
56+
* Does a synchronous, breadth-first traversal of the graph of instructions.
57+
* Takes a function with signature:
5558
* (parent:Instruction, child:Instruction) => {}
5659
*/
5760
traverseSync(fn:Function) {
5861
this.forEachChild((childInstruction, _) => fn(this, childInstruction));
5962
this.forEachChild((childInstruction, _) => childInstruction.traverseSync(fn));
6063
}
64+
65+
/**
66+
* Does an asynchronous, breadth-first traversal of the graph of instructions.
67+
* Takes a function with signature:
68+
* (child:Instruction, parentOutletName:string) => {}
69+
*/
70+
traverseAsync(fn:Function) {
71+
return this.mapChildrenAsync(fn)
72+
.then((_) => this.mapChildrenAsync((childInstruction, _) => childInstruction.traverseAsync(fn)));
73+
}
74+
75+
76+
/**
77+
* Takes a currently active instruction and sets a reuse flag on this instruction
78+
*/
79+
reuseComponentsFrom(oldInstruction:Instruction) {
80+
this.forEachChild((childInstruction, outletName) => {
81+
var oldInstructionChild = oldInstruction.getChildInstruction(outletName);
82+
if (shouldReuseComponent(childInstruction, oldInstructionChild)) {
83+
childInstruction.reuse = true;
84+
}
85+
});
86+
}
87+
}
88+
89+
function shouldReuseComponent(instr1:Instruction, instr2:Instruction) {
90+
return instr1.component == instr2.component &&
91+
StringMapWrapper.equals(instr1.params, instr2.params);
6192
}
6293

6394
function mapObjAsync(obj:StringMap, fn) {

modules/angular2/src/router/router.js

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {Promise, PromiseWrapper, EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
22
import {Map, MapWrapper, List, ListWrapper} from 'angular2/src/facade/collection';
3-
import {isBlank, Type} from 'angular2/src/facade/lang';
3+
import {isBlank, isPresent, Type} from 'angular2/src/facade/lang';
44

55
import {RouteRegistry} from './route_registry';
66
import {Pipeline} from './pipeline';
@@ -24,6 +24,8 @@ export class Router {
2424
lastNavigationAttempt: string;
2525
previousUrl:string;
2626

27+
_currentInstruction:Instruction;
28+
2729
_pipeline:Pipeline;
2830
_registry:RouteRegistry;
2931
_outlets:Map<any, RouterOutlet>;
@@ -42,6 +44,7 @@ export class Router {
4244
this._registry = registry;
4345
this._pipeline = pipeline;
4446
this._subject = new EventEmitter();
47+
this._currentInstruction = null;
4548
}
4649

4750

@@ -61,7 +64,11 @@ export class Router {
6164
*/
6265
registerOutlet(outlet:RouterOutlet, name = 'default'):Promise {
6366
MapWrapper.set(this._outlets, name, outlet);
64-
return this.renavigate();
67+
if (isPresent(this._currentInstruction)) {
68+
var childInstruction = this._currentInstruction.getChildInstruction(name);
69+
return outlet.activate(childInstruction);
70+
}
71+
return PromiseWrapper.resolve(true);
6572
}
6673

6774

@@ -107,23 +114,26 @@ export class Router {
107114

108115
this.lastNavigationAttempt = url;
109116

110-
var instruction = this.recognize(url);
117+
var matchedInstruction = this.recognize(url);
111118

112-
if (isBlank(instruction)) {
119+
if (isBlank(matchedInstruction)) {
113120
return PromiseWrapper.resolve(false);
114121
}
115122

116-
instruction.router = this;
123+
if(isPresent(this._currentInstruction)) {
124+
matchedInstruction.reuseComponentsFrom(this._currentInstruction);
125+
}
126+
127+
matchedInstruction.router = this;
117128
this._startNavigating();
118129

119-
var result = this._pipeline.process(instruction)
120-
.then((_) => {
121-
this._location.go(instruction.matchedUrl);
122-
})
130+
var result = this._pipeline.process(matchedInstruction)
123131
.then((_) => {
124-
ObservableWrapper.callNext(this._subject, instruction.matchedUrl);
125-
})
126-
.then((_) => this._finishNavigating());
132+
this._location.go(matchedInstruction.matchedUrl);
133+
ObservableWrapper.callNext(this._subject, matchedInstruction.matchedUrl);
134+
this._finishNavigating();
135+
this._currentInstruction = matchedInstruction;
136+
});
127137

128138
PromiseWrapper.catchError(result, (_) => this._finishNavigating());
129139

@@ -148,7 +158,11 @@ export class Router {
148158

149159
activateOutlets(instruction:Instruction):Promise {
150160
return this._queryOutlets((outlet, name) => {
151-
return outlet.activate(instruction.getChildInstruction(name));
161+
var childInstruction = instruction.getChildInstruction(name);
162+
if (childInstruction.reuse) {
163+
return PromiseWrapper.resolve(true);
164+
}
165+
return outlet.activate(childInstruction);
152166
})
153167
.then((_) => instruction.mapChildrenAsync((instruction, _) => {
154168
return instruction.router.activateOutlets(instruction);

modules/angular2/test/router/outlet_spec.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import {Location} from 'angular2/src/router/location';
3131
import {RouteRegistry} from 'angular2/src/router/route_registry';
3232
import {DirectiveMetadataReader} from 'angular2/src/core/compiler/directive_metadata_reader';
3333

34+
var teamCmpCount;
35+
3436
export function main() {
3537
describe('Outlet Directive', () => {
3638

@@ -51,6 +53,7 @@ export function main() {
5153
ctx = new MyComp();
5254
rtr = router;
5355
location = loc;
56+
teamCmpCount = 0;
5457
}));
5558

5659
function compile(template:string = "<router-outlet></router-outlet>") {
@@ -132,6 +135,7 @@ export function main() {
132135
});
133136
}));
134137

138+
135139
it('should generate link hrefs without params', inject([AsyncTestCompleter], (async) => {
136140
compile('<a href="hello" router-link="user"></a>')
137141
.then((_) => rtr.config({'path': '/user', 'component': UserCmp, 'as': 'user'}))
@@ -143,6 +147,26 @@ export function main() {
143147
});
144148
}));
145149

150+
151+
it('should reuse common parent components', inject([AsyncTestCompleter, Location], (async, location) => {
152+
compile()
153+
.then((_) => rtr.config({'path': '/team/:id', 'component': TeamCmp }))
154+
.then((_) => rtr.navigate('/team/angular/user/rado'))
155+
.then((_) => {
156+
view.detectChanges();
157+
expect(teamCmpCount).toBe(1);
158+
expect(view.rootNodes).toHaveText('team angular { hello rado }');
159+
})
160+
.then((_) => rtr.navigate('/team/angular/user/victor'))
161+
.then((_) => {
162+
view.detectChanges();
163+
expect(teamCmpCount).toBe(1);
164+
expect(view.rootNodes).toHaveText('team angular { hello victor }');
165+
async.done();
166+
});
167+
}));
168+
169+
146170
it('should generate link hrefs with params', inject([AsyncTestCompleter], (async) => {
147171
ctx.name = 'brian';
148172
compile('<a href="hello" router-link="user" [router-params]="{name: name}">{{name}}</a>')
@@ -242,6 +266,27 @@ class ParentCmp {
242266
constructor() {}
243267
}
244268

269+
270+
@Component({
271+
selector: 'team-cmp'
272+
})
273+
@View({
274+
template: "team {{id}} { <router-outlet></router-outlet> }",
275+
directives: [RouterOutlet]
276+
})
277+
@RouteConfig([{
278+
path: '/user/:name',
279+
component: UserCmp
280+
}])
281+
class TeamCmp {
282+
id:string;
283+
constructor(params:RouteParams) {
284+
this.id = params.get('id');
285+
teamCmpCount += 1;
286+
}
287+
}
288+
289+
245290
@Component({
246291
selector: 'my-comp'
247292
})

0 commit comments

Comments
 (0)