diff --git a/modules/angular2/src/router/directives/router_outlet.ts b/modules/angular2/src/router/directives/router_outlet.ts index 500d1d04b7123..457ceab2643f3 100644 --- a/modules/angular2/src/router/directives/router_outlet.ts +++ b/modules/angular2/src/router/directives/router_outlet.ts @@ -1,7 +1,6 @@ import {PromiseWrapper} from 'angular2/src/facade/async'; import {StringMapWrapper} from 'angular2/src/facade/collection'; import {isBlank, isPresent} from 'angular2/src/facade/lang'; -import {BaseException, WrappedException} from 'angular2/src/facade/exceptions'; import { Directive, @@ -11,7 +10,8 @@ import { ElementRef, Injector, provide, - Dependency + Dependency, + OnDestroy } from 'angular2/core'; import * as routerMod from '../router'; @@ -32,7 +32,7 @@ let _resolveToTrue = PromiseWrapper.resolve(true); * ``` */ @Directive({selector: 'router-outlet'}) -export class RouterOutlet { +export class RouterOutlet implements OnDestroy { name: string = null; private _componentRef: ComponentRef = null; private _currentInstruction: ComponentInstruction = null; @@ -81,8 +81,11 @@ export class RouterOutlet { var previousInstruction = this._currentInstruction; this._currentInstruction = nextInstruction; + // it's possible the component is removed before it can be reactivated (if nested withing + // another dynamically loaded component, for instance). In that case, we simply activate + // a new one. if (isBlank(this._componentRef)) { - throw new BaseException(`Cannot reuse an outlet that does not contain a component.`); + return this.activate(nextInstruction); } return PromiseWrapper.resolve( hasLifecycleHook(hookMod.routerOnReuse, this._currentInstruction.componentType) ? @@ -157,4 +160,6 @@ export class RouterOutlet { } return PromiseWrapper.resolve(result); } + + ngOnDestroy(): void { this._parentRouter.unregisterPrimaryOutlet(this); } } diff --git a/modules/angular2/src/router/router.ts b/modules/angular2/src/router/router.ts index 6fbc004023736..1376785a5062b 100644 --- a/modules/angular2/src/router/router.ts +++ b/modules/angular2/src/router/router.ts @@ -78,6 +78,10 @@ export class Router { throw new BaseException(`registerPrimaryOutlet expects to be called with an unnamed outlet.`); } + if (isPresent(this._outlet)) { + throw new BaseException(`Primary outlet is already registered.`); + } + this._outlet = outlet; if (isPresent(this._currentInstruction)) { return this.commit(this._currentInstruction, false); @@ -85,6 +89,19 @@ export class Router { return _resolveToTrue; } + /** + * Unregister an outlet (because it was destroyed, etc). + * + * You probably don't need to use this unless you're writing a custom outlet implementation. + */ + unregisterPrimaryOutlet(outlet: RouterOutlet): void { + if (isPresent(outlet.name)) { + throw new BaseException(`registerPrimaryOutlet expects to be called with an unnamed outlet.`); + } + this._outlet = null; + } + + /** * Register an outlet to notified of auxiliary route changes. * @@ -198,6 +215,26 @@ export class Router { }); } + /** @internal */ + _settleInstruction(instruction: Instruction): Promise { + return instruction.resolveComponent().then((_) => { + var unsettledInstructions: Array> = []; + + if (isPresent(instruction.component)) { + instruction.component.reuse = false; + } + + if (isPresent(instruction.child)) { + unsettledInstructions.push(this._settleInstruction(instruction.child)); + } + + StringMapWrapper.forEach(instruction.auxInstruction, (instruction, _) => { + unsettledInstructions.push(this._settleInstruction(instruction)); + }); + return PromiseWrapper.all(unsettledInstructions); + }); + } + /** @internal */ _navigate(instruction: Instruction, _skipLocationChange: boolean): Promise { return this._settleInstruction(instruction) @@ -220,26 +257,6 @@ export class Router { }); } - /** @internal */ - _settleInstruction(instruction: Instruction): Promise { - return instruction.resolveComponent().then((_) => { - var unsettledInstructions: Array> = []; - - if (isPresent(instruction.component)) { - instruction.component.reuse = false; - } - - if (isPresent(instruction.child)) { - unsettledInstructions.push(this._settleInstruction(instruction.child)); - } - - StringMapWrapper.forEach(instruction.auxInstruction, (instruction, _) => { - unsettledInstructions.push(this._settleInstruction(instruction)); - }); - return PromiseWrapper.all(unsettledInstructions); - }); - } - private _emitNavigationFinish(url): void { ObservableWrapper.callEmit(this._subject, url); } private _afterPromiseFinishNavigating(promise: Promise): Promise { diff --git a/modules/angular2/test/router/integration/impl/fixture_components.ts b/modules/angular2/test/router/integration/impl/fixture_components.ts index 5777bf163bda1..cc0fdfca5de00 100644 --- a/modules/angular2/test/router/integration/impl/fixture_components.ts +++ b/modules/angular2/test/router/integration/impl/fixture_components.ts @@ -9,6 +9,12 @@ import { ROUTER_DIRECTIVES } from 'angular2/router'; import {PromiseWrapper} from 'angular2/src/facade/async'; +import {isPresent} from 'angular2/src/facade/lang'; +import { + DynamicComponentLoader, + ComponentRef +} from 'angular2/src/core/linker/dynamic_component_loader'; +import {ElementRef} from 'angular2/src/core/linker/element_ref'; @Component({selector: 'goodbye-cmp', template: `{{farewell}}`}) export class GoodbyeCmp { @@ -135,3 +141,31 @@ export function asyncRouteDataCmp() { @RouteConfig([new Redirect({path: '/child-redirect', redirectTo: ['../HelloSib']})]) export class RedirectToParentCmp { } + + +@Component({selector: 'dynamic-loader-cmp', template: `{
}`}) +@RouteConfig([new Route({path: '/', component: HelloCmp})]) +export class DynamicLoaderCmp { + private _componentRef: ComponentRef = null; + constructor(private _dynamicComponentLoader: DynamicComponentLoader, + private _elementRef: ElementRef) {} + + onSomeAction(): Promise { + if (isPresent(this._componentRef)) { + this._componentRef.dispose(); + this._componentRef = null; + } + return this._dynamicComponentLoader.loadIntoLocation(DynamicallyLoadedComponent, + this._elementRef, 'viewport') + .then((cmp) => { this._componentRef = cmp; }); + } +} + + +@Component({ + selector: 'loaded-cmp', + template: '', + directives: [ROUTER_DIRECTIVES] +}) +class DynamicallyLoadedComponent { +} diff --git a/modules/angular2/test/router/integration/impl/sync_route_spec_impl.ts b/modules/angular2/test/router/integration/impl/sync_route_spec_impl.ts index 8556cca4b0dca..71fbfa5a43adf 100644 --- a/modules/angular2/test/router/integration/impl/sync_route_spec_impl.ts +++ b/modules/angular2/test/router/integration/impl/sync_route_spec_impl.ts @@ -17,7 +17,16 @@ import {specs, compile, TEST_ROUTER_PROVIDERS, clickOnElement, getHref} from '.. import {By} from 'angular2/platform/common_dom'; import {Router, Route, Location} from 'angular2/router'; -import {HelloCmp, UserCmp, TeamCmp, ParentCmp, ParentWithDefaultCmp} from './fixture_components'; +import { + HelloCmp, + UserCmp, + TeamCmp, + ParentCmp, + ParentWithDefaultCmp, + DynamicLoaderCmp +} from './fixture_components'; + +import {PromiseWrapper} from 'angular2/src/facade/async'; function getLinkElement(rtc: ComponentFixture) { @@ -420,6 +429,55 @@ function syncRoutesWithSyncChildrenWithDefaultRoutesWithoutParams() { })); } +function syncRoutesWithDynamicComponents() { + var fixture; + var tcb; + var rtr: Router; + + beforeEachProviders(() => TEST_ROUTER_PROVIDERS); + + beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => { + tcb = tcBuilder; + rtr = router; + })); + + + it('should work', + inject([AsyncTestCompleter], + (async) => {tcb.createAsync(DynamicLoaderCmp) + .then((rtc) => {fixture = rtc}) + .then((_) => rtr.config([new Route({path: '/', component: HelloCmp})])) + .then((_) => { + fixture.detectChanges(); + expect(fixture.debugElement.nativeElement).toHaveText('{ }'); + return fixture.componentInstance.onSomeAction(); + }) + .then((_) => { + fixture.detectChanges(); + return rtr.navigateByUrl('/'); + }) + .then((_) => { + fixture.detectChanges(); + expect(fixture.debugElement.nativeElement).toHaveText('{ hello }'); + + return fixture.componentInstance.onSomeAction(); + }) + .then((_) => { + + // TODO(i): This should be rewritten to use NgZone#onStable or + // something + // similar basically the assertion needs to run when the world is + // stable and we don't know when that is, only zones know. + PromiseWrapper.resolve(null).then((_) => { + fixture.detectChanges(); + expect(fixture.debugElement.nativeElement).toHaveText('{ hello }'); + async.done(); + }); + })})); +} + + + export function registerSpecs() { specs['syncRoutesWithoutChildrenWithoutParams'] = syncRoutesWithoutChildrenWithoutParams; specs['syncRoutesWithoutChildrenWithParams'] = syncRoutesWithoutChildrenWithParams; @@ -429,4 +487,5 @@ export function registerSpecs() { syncRoutesWithSyncChildrenWithoutDefaultRoutesWithParams; specs['syncRoutesWithSyncChildrenWithDefaultRoutesWithoutParams'] = syncRoutesWithSyncChildrenWithDefaultRoutesWithoutParams; + specs['syncRoutesWithDynamicComponents'] = syncRoutesWithDynamicComponents; } diff --git a/modules/angular2/test/router/integration/sync_route_spec.ts b/modules/angular2/test/router/integration/sync_route_spec.ts index 12a4d339fea2e..6c70841a071a3 100644 --- a/modules/angular2/test/router/integration/sync_route_spec.ts +++ b/modules/angular2/test/router/integration/sync_route_spec.ts @@ -20,5 +20,7 @@ export function main() { describeWith('default routes', () => { describeWithout('params', itShouldRoute); }); }); + + describeWith('dynamic components', itShouldRoute); }); }