fix(router): support outlets within dynamic components
Fixes internal b/27294172
This commit is contained in:
parent
75343eb340
commit
7d44b8230e
|
@ -1,7 +1,6 @@
|
||||||
import {PromiseWrapper} from 'angular2/src/facade/async';
|
import {PromiseWrapper} from 'angular2/src/facade/async';
|
||||||
import {StringMapWrapper} from 'angular2/src/facade/collection';
|
import {StringMapWrapper} from 'angular2/src/facade/collection';
|
||||||
import {isBlank, isPresent} from 'angular2/src/facade/lang';
|
import {isBlank, isPresent} from 'angular2/src/facade/lang';
|
||||||
import {BaseException, WrappedException} from 'angular2/src/facade/exceptions';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Directive,
|
Directive,
|
||||||
|
@ -11,7 +10,8 @@ import {
|
||||||
ElementRef,
|
ElementRef,
|
||||||
Injector,
|
Injector,
|
||||||
provide,
|
provide,
|
||||||
Dependency
|
Dependency,
|
||||||
|
OnDestroy
|
||||||
} from 'angular2/core';
|
} from 'angular2/core';
|
||||||
|
|
||||||
import * as routerMod from '../router';
|
import * as routerMod from '../router';
|
||||||
|
@ -32,7 +32,7 @@ let _resolveToTrue = PromiseWrapper.resolve(true);
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
@Directive({selector: 'router-outlet'})
|
@Directive({selector: 'router-outlet'})
|
||||||
export class RouterOutlet {
|
export class RouterOutlet implements OnDestroy {
|
||||||
name: string = null;
|
name: string = null;
|
||||||
private _componentRef: ComponentRef = null;
|
private _componentRef: ComponentRef = null;
|
||||||
private _currentInstruction: ComponentInstruction = null;
|
private _currentInstruction: ComponentInstruction = null;
|
||||||
|
@ -81,8 +81,11 @@ export class RouterOutlet {
|
||||||
var previousInstruction = this._currentInstruction;
|
var previousInstruction = this._currentInstruction;
|
||||||
this._currentInstruction = nextInstruction;
|
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)) {
|
if (isBlank(this._componentRef)) {
|
||||||
throw new BaseException(`Cannot reuse an outlet that does not contain a component.`);
|
return this.activate(nextInstruction);
|
||||||
}
|
}
|
||||||
return PromiseWrapper.resolve(
|
return PromiseWrapper.resolve(
|
||||||
hasLifecycleHook(hookMod.routerOnReuse, this._currentInstruction.componentType) ?
|
hasLifecycleHook(hookMod.routerOnReuse, this._currentInstruction.componentType) ?
|
||||||
|
@ -157,4 +160,6 @@ export class RouterOutlet {
|
||||||
}
|
}
|
||||||
return PromiseWrapper.resolve(result);
|
return PromiseWrapper.resolve(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void { this._parentRouter.unregisterPrimaryOutlet(this); }
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,6 +78,10 @@ export class Router {
|
||||||
throw new BaseException(`registerPrimaryOutlet expects to be called with an unnamed outlet.`);
|
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;
|
this._outlet = outlet;
|
||||||
if (isPresent(this._currentInstruction)) {
|
if (isPresent(this._currentInstruction)) {
|
||||||
return this.commit(this._currentInstruction, false);
|
return this.commit(this._currentInstruction, false);
|
||||||
|
@ -85,6 +89,19 @@ export class Router {
|
||||||
return _resolveToTrue;
|
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.
|
* Register an outlet to notified of auxiliary route changes.
|
||||||
*
|
*
|
||||||
|
@ -198,6 +215,26 @@ export class Router {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
_settleInstruction(instruction: Instruction): Promise<any> {
|
||||||
|
return instruction.resolveComponent().then((_) => {
|
||||||
|
var unsettledInstructions: Array<Promise<any>> = [];
|
||||||
|
|
||||||
|
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 */
|
/** @internal */
|
||||||
_navigate(instruction: Instruction, _skipLocationChange: boolean): Promise<any> {
|
_navigate(instruction: Instruction, _skipLocationChange: boolean): Promise<any> {
|
||||||
return this._settleInstruction(instruction)
|
return this._settleInstruction(instruction)
|
||||||
|
@ -220,26 +257,6 @@ export class Router {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
_settleInstruction(instruction: Instruction): Promise<any> {
|
|
||||||
return instruction.resolveComponent().then((_) => {
|
|
||||||
var unsettledInstructions: Array<Promise<any>> = [];
|
|
||||||
|
|
||||||
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 _emitNavigationFinish(url): void { ObservableWrapper.callEmit(this._subject, url); }
|
||||||
|
|
||||||
private _afterPromiseFinishNavigating(promise: Promise<any>): Promise<any> {
|
private _afterPromiseFinishNavigating(promise: Promise<any>): Promise<any> {
|
||||||
|
|
|
@ -9,6 +9,12 @@ import {
|
||||||
ROUTER_DIRECTIVES
|
ROUTER_DIRECTIVES
|
||||||
} from 'angular2/router';
|
} from 'angular2/router';
|
||||||
import {PromiseWrapper} from 'angular2/src/facade/async';
|
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}}`})
|
@Component({selector: 'goodbye-cmp', template: `{{farewell}}`})
|
||||||
export class GoodbyeCmp {
|
export class GoodbyeCmp {
|
||||||
|
@ -135,3 +141,31 @@ export function asyncRouteDataCmp() {
|
||||||
@RouteConfig([new Redirect({path: '/child-redirect', redirectTo: ['../HelloSib']})])
|
@RouteConfig([new Redirect({path: '/child-redirect', redirectTo: ['../HelloSib']})])
|
||||||
export class RedirectToParentCmp {
|
export class RedirectToParentCmp {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Component({selector: 'dynamic-loader-cmp', template: `{ <div #viewport></div> }`})
|
||||||
|
@RouteConfig([new Route({path: '/', component: HelloCmp})])
|
||||||
|
export class DynamicLoaderCmp {
|
||||||
|
private _componentRef: ComponentRef = null;
|
||||||
|
constructor(private _dynamicComponentLoader: DynamicComponentLoader,
|
||||||
|
private _elementRef: ElementRef) {}
|
||||||
|
|
||||||
|
onSomeAction(): Promise<any> {
|
||||||
|
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: '<router-outlet></router-outlet>',
|
||||||
|
directives: [ROUTER_DIRECTIVES]
|
||||||
|
})
|
||||||
|
class DynamicallyLoadedComponent {
|
||||||
|
}
|
||||||
|
|
|
@ -17,7 +17,16 @@ import {specs, compile, TEST_ROUTER_PROVIDERS, clickOnElement, getHref} from '..
|
||||||
import {By} from 'angular2/platform/common_dom';
|
import {By} from 'angular2/platform/common_dom';
|
||||||
import {Router, Route, Location} from 'angular2/router';
|
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) {
|
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() {
|
export function registerSpecs() {
|
||||||
specs['syncRoutesWithoutChildrenWithoutParams'] = syncRoutesWithoutChildrenWithoutParams;
|
specs['syncRoutesWithoutChildrenWithoutParams'] = syncRoutesWithoutChildrenWithoutParams;
|
||||||
specs['syncRoutesWithoutChildrenWithParams'] = syncRoutesWithoutChildrenWithParams;
|
specs['syncRoutesWithoutChildrenWithParams'] = syncRoutesWithoutChildrenWithParams;
|
||||||
|
@ -429,4 +487,5 @@ export function registerSpecs() {
|
||||||
syncRoutesWithSyncChildrenWithoutDefaultRoutesWithParams;
|
syncRoutesWithSyncChildrenWithoutDefaultRoutesWithParams;
|
||||||
specs['syncRoutesWithSyncChildrenWithDefaultRoutesWithoutParams'] =
|
specs['syncRoutesWithSyncChildrenWithDefaultRoutesWithoutParams'] =
|
||||||
syncRoutesWithSyncChildrenWithDefaultRoutesWithoutParams;
|
syncRoutesWithSyncChildrenWithDefaultRoutesWithoutParams;
|
||||||
|
specs['syncRoutesWithDynamicComponents'] = syncRoutesWithDynamicComponents;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,5 +20,7 @@ export function main() {
|
||||||
describeWith('default routes', () => { describeWithout('params', itShouldRoute); });
|
describeWith('default routes', () => { describeWithout('params', itShouldRoute); });
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describeWith('dynamic components', itShouldRoute);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue