X Tutup
Skip to content

Commit 7d44b82

Browse files
btfordvikerman
authored andcommitted
fix(router): support outlets within dynamic components
Fixes internal b/27294172
1 parent 75343eb commit 7d44b82

File tree

5 files changed

+142
-25
lines changed

5 files changed

+142
-25
lines changed

modules/angular2/src/router/directives/router_outlet.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import {PromiseWrapper} from 'angular2/src/facade/async';
22
import {StringMapWrapper} from 'angular2/src/facade/collection';
33
import {isBlank, isPresent} from 'angular2/src/facade/lang';
4-
import {BaseException, WrappedException} from 'angular2/src/facade/exceptions';
54

65
import {
76
Directive,
@@ -11,7 +10,8 @@ import {
1110
ElementRef,
1211
Injector,
1312
provide,
14-
Dependency
13+
Dependency,
14+
OnDestroy
1515
} from 'angular2/core';
1616

1717
import * as routerMod from '../router';
@@ -32,7 +32,7 @@ let _resolveToTrue = PromiseWrapper.resolve(true);
3232
* ```
3333
*/
3434
@Directive({selector: 'router-outlet'})
35-
export class RouterOutlet {
35+
export class RouterOutlet implements OnDestroy {
3636
name: string = null;
3737
private _componentRef: ComponentRef = null;
3838
private _currentInstruction: ComponentInstruction = null;
@@ -81,8 +81,11 @@ export class RouterOutlet {
8181
var previousInstruction = this._currentInstruction;
8282
this._currentInstruction = nextInstruction;
8383

84+
// it's possible the component is removed before it can be reactivated (if nested withing
85+
// another dynamically loaded component, for instance). In that case, we simply activate
86+
// a new one.
8487
if (isBlank(this._componentRef)) {
85-
throw new BaseException(`Cannot reuse an outlet that does not contain a component.`);
88+
return this.activate(nextInstruction);
8689
}
8790
return PromiseWrapper.resolve(
8891
hasLifecycleHook(hookMod.routerOnReuse, this._currentInstruction.componentType) ?
@@ -157,4 +160,6 @@ export class RouterOutlet {
157160
}
158161
return PromiseWrapper.resolve(result);
159162
}
163+
164+
ngOnDestroy(): void { this._parentRouter.unregisterPrimaryOutlet(this); }
160165
}

modules/angular2/src/router/router.ts

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,30 @@ export class Router {
7878
throw new BaseException(`registerPrimaryOutlet expects to be called with an unnamed outlet.`);
7979
}
8080

81+
if (isPresent(this._outlet)) {
82+
throw new BaseException(`Primary outlet is already registered.`);
83+
}
84+
8185
this._outlet = outlet;
8286
if (isPresent(this._currentInstruction)) {
8387
return this.commit(this._currentInstruction, false);
8488
}
8589
return _resolveToTrue;
8690
}
8791

92+
/**
93+
* Unregister an outlet (because it was destroyed, etc).
94+
*
95+
* You probably don't need to use this unless you're writing a custom outlet implementation.
96+
*/
97+
unregisterPrimaryOutlet(outlet: RouterOutlet): void {
98+
if (isPresent(outlet.name)) {
99+
throw new BaseException(`registerPrimaryOutlet expects to be called with an unnamed outlet.`);
100+
}
101+
this._outlet = null;
102+
}
103+
104+
88105
/**
89106
* Register an outlet to notified of auxiliary route changes.
90107
*
@@ -198,6 +215,26 @@ export class Router {
198215
});
199216
}
200217

218+
/** @internal */
219+
_settleInstruction(instruction: Instruction): Promise<any> {
220+
return instruction.resolveComponent().then((_) => {
221+
var unsettledInstructions: Array<Promise<any>> = [];
222+
223+
if (isPresent(instruction.component)) {
224+
instruction.component.reuse = false;
225+
}
226+
227+
if (isPresent(instruction.child)) {
228+
unsettledInstructions.push(this._settleInstruction(instruction.child));
229+
}
230+
231+
StringMapWrapper.forEach(instruction.auxInstruction, (instruction, _) => {
232+
unsettledInstructions.push(this._settleInstruction(instruction));
233+
});
234+
return PromiseWrapper.all(unsettledInstructions);
235+
});
236+
}
237+
201238
/** @internal */
202239
_navigate(instruction: Instruction, _skipLocationChange: boolean): Promise<any> {
203240
return this._settleInstruction(instruction)
@@ -220,26 +257,6 @@ export class Router {
220257
});
221258
}
222259

223-
/** @internal */
224-
_settleInstruction(instruction: Instruction): Promise<any> {
225-
return instruction.resolveComponent().then((_) => {
226-
var unsettledInstructions: Array<Promise<any>> = [];
227-
228-
if (isPresent(instruction.component)) {
229-
instruction.component.reuse = false;
230-
}
231-
232-
if (isPresent(instruction.child)) {
233-
unsettledInstructions.push(this._settleInstruction(instruction.child));
234-
}
235-
236-
StringMapWrapper.forEach(instruction.auxInstruction, (instruction, _) => {
237-
unsettledInstructions.push(this._settleInstruction(instruction));
238-
});
239-
return PromiseWrapper.all(unsettledInstructions);
240-
});
241-
}
242-
243260
private _emitNavigationFinish(url): void { ObservableWrapper.callEmit(this._subject, url); }
244261

245262
private _afterPromiseFinishNavigating(promise: Promise<any>): Promise<any> {

modules/angular2/test/router/integration/impl/fixture_components.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ import {
99
ROUTER_DIRECTIVES
1010
} from 'angular2/router';
1111
import {PromiseWrapper} from 'angular2/src/facade/async';
12+
import {isPresent} from 'angular2/src/facade/lang';
13+
import {
14+
DynamicComponentLoader,
15+
ComponentRef
16+
} from 'angular2/src/core/linker/dynamic_component_loader';
17+
import {ElementRef} from 'angular2/src/core/linker/element_ref';
1218

1319
@Component({selector: 'goodbye-cmp', template: `{{farewell}}`})
1420
export class GoodbyeCmp {
@@ -135,3 +141,31 @@ export function asyncRouteDataCmp() {
135141
@RouteConfig([new Redirect({path: '/child-redirect', redirectTo: ['../HelloSib']})])
136142
export class RedirectToParentCmp {
137143
}
144+
145+
146+
@Component({selector: 'dynamic-loader-cmp', template: `{ <div #viewport></div> }`})
147+
@RouteConfig([new Route({path: '/', component: HelloCmp})])
148+
export class DynamicLoaderCmp {
149+
private _componentRef: ComponentRef = null;
150+
constructor(private _dynamicComponentLoader: DynamicComponentLoader,
151+
private _elementRef: ElementRef) {}
152+
153+
onSomeAction(): Promise<any> {
154+
if (isPresent(this._componentRef)) {
155+
this._componentRef.dispose();
156+
this._componentRef = null;
157+
}
158+
return this._dynamicComponentLoader.loadIntoLocation(DynamicallyLoadedComponent,
159+
this._elementRef, 'viewport')
160+
.then((cmp) => { this._componentRef = cmp; });
161+
}
162+
}
163+
164+
165+
@Component({
166+
selector: 'loaded-cmp',
167+
template: '<router-outlet></router-outlet>',
168+
directives: [ROUTER_DIRECTIVES]
169+
})
170+
class DynamicallyLoadedComponent {
171+
}

modules/angular2/test/router/integration/impl/sync_route_spec_impl.ts

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,16 @@ import {specs, compile, TEST_ROUTER_PROVIDERS, clickOnElement, getHref} from '..
1717
import {By} from 'angular2/platform/common_dom';
1818
import {Router, Route, Location} from 'angular2/router';
1919

20-
import {HelloCmp, UserCmp, TeamCmp, ParentCmp, ParentWithDefaultCmp} from './fixture_components';
20+
import {
21+
HelloCmp,
22+
UserCmp,
23+
TeamCmp,
24+
ParentCmp,
25+
ParentWithDefaultCmp,
26+
DynamicLoaderCmp
27+
} from './fixture_components';
28+
29+
import {PromiseWrapper} from 'angular2/src/facade/async';
2130

2231

2332
function getLinkElement(rtc: ComponentFixture) {
@@ -420,6 +429,55 @@ function syncRoutesWithSyncChildrenWithDefaultRoutesWithoutParams() {
420429
}));
421430
}
422431

432+
function syncRoutesWithDynamicComponents() {
433+
var fixture;
434+
var tcb;
435+
var rtr: Router;
436+
437+
beforeEachProviders(() => TEST_ROUTER_PROVIDERS);
438+
439+
beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => {
440+
tcb = tcBuilder;
441+
rtr = router;
442+
}));
443+
444+
445+
it('should work',
446+
inject([AsyncTestCompleter],
447+
(async) => {tcb.createAsync(DynamicLoaderCmp)
448+
.then((rtc) => {fixture = rtc})
449+
.then((_) => rtr.config([new Route({path: '/', component: HelloCmp})]))
450+
.then((_) => {
451+
fixture.detectChanges();
452+
expect(fixture.debugElement.nativeElement).toHaveText('{ }');
453+
return fixture.componentInstance.onSomeAction();
454+
})
455+
.then((_) => {
456+
fixture.detectChanges();
457+
return rtr.navigateByUrl('/');
458+
})
459+
.then((_) => {
460+
fixture.detectChanges();
461+
expect(fixture.debugElement.nativeElement).toHaveText('{ hello }');
462+
463+
return fixture.componentInstance.onSomeAction();
464+
})
465+
.then((_) => {
466+
467+
// TODO(i): This should be rewritten to use NgZone#onStable or
468+
// something
469+
// similar basically the assertion needs to run when the world is
470+
// stable and we don't know when that is, only zones know.
471+
PromiseWrapper.resolve(null).then((_) => {
472+
fixture.detectChanges();
473+
expect(fixture.debugElement.nativeElement).toHaveText('{ hello }');
474+
async.done();
475+
});
476+
})}));
477+
}
478+
479+
480+
423481
export function registerSpecs() {
424482
specs['syncRoutesWithoutChildrenWithoutParams'] = syncRoutesWithoutChildrenWithoutParams;
425483
specs['syncRoutesWithoutChildrenWithParams'] = syncRoutesWithoutChildrenWithParams;
@@ -429,4 +487,5 @@ export function registerSpecs() {
429487
syncRoutesWithSyncChildrenWithoutDefaultRoutesWithParams;
430488
specs['syncRoutesWithSyncChildrenWithDefaultRoutesWithoutParams'] =
431489
syncRoutesWithSyncChildrenWithDefaultRoutesWithoutParams;
490+
specs['syncRoutesWithDynamicComponents'] = syncRoutesWithDynamicComponents;
432491
}

modules/angular2/test/router/integration/sync_route_spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,7 @@ export function main() {
2020
describeWith('default routes', () => { describeWithout('params', itShouldRoute); });
2121

2222
});
23+
24+
describeWith('dynamic components', itShouldRoute);
2325
});
2426
}

0 commit comments

Comments
 (0)
X Tutup