diff --git a/modules/@angular/router/src/apply_redirects.ts b/modules/@angular/router/src/apply_redirects.ts index 3154eea0ea..066245d56d 100644 --- a/modules/@angular/router/src/apply_redirects.ts +++ b/modules/@angular/router/src/apply_redirects.ts @@ -10,13 +10,14 @@ import 'rxjs/add/operator/first'; import 'rxjs/add/operator/catch'; import 'rxjs/add/operator/concatAll'; +import {Injector} from '@angular/core'; import {Observable} from 'rxjs/Observable'; import {Observer} from 'rxjs/Observer'; import {of } from 'rxjs/observable/of'; import {EmptyError} from 'rxjs/util/EmptyError'; import {Route, Routes} from './config'; -import {RouterConfigLoader} from './router_config_loader'; +import {LoadedRouterConfig, RouterConfigLoader} from './router_config_loader'; import {PRIMARY_OUTLET} from './shared'; import {UrlPathWithParams, UrlSegment, UrlTree} from './url_tree'; import {merge, waitForMap} from './utils/collection'; @@ -38,8 +39,9 @@ function absoluteRedirect(newPaths: UrlPathWithParams[]): Observable } export function applyRedirects( - configLoader: RouterConfigLoader, urlTree: UrlTree, config: Routes): Observable { - return expandSegment(configLoader, config, urlTree.root, PRIMARY_OUTLET) + injector: Injector, configLoader: RouterConfigLoader, urlTree: UrlTree, + config: Routes): Observable { + return expandSegment(injector, configLoader, config, urlTree.root, PRIMARY_OUTLET) .map(rootSegment => createUrlTree(urlTree, rootSegment)) .catch(e => { if (e instanceof AbsoluteRedirect) { @@ -61,33 +63,33 @@ function createUrlTree(urlTree: UrlTree, rootCandidate: UrlSegment): UrlTree { } function expandSegment( - configLoader: RouterConfigLoader, routes: Route[], segment: UrlSegment, + injector: Injector, configLoader: RouterConfigLoader, routes: Route[], segment: UrlSegment, outlet: string): Observable { if (segment.pathsWithParams.length === 0 && segment.hasChildren()) { - return expandSegmentChildren(configLoader, routes, segment) + return expandSegmentChildren(injector, configLoader, routes, segment) .map(children => new UrlSegment([], children)); } else { return expandPathsWithParams( - configLoader, segment, routes, segment.pathsWithParams, outlet, true); + injector, configLoader, segment, routes, segment.pathsWithParams, outlet, true); } } function expandSegmentChildren( - configLoader: RouterConfigLoader, routes: Route[], + injector: Injector, configLoader: RouterConfigLoader, routes: Route[], segment: UrlSegment): Observable<{[name: string]: UrlSegment}> { return waitForMap( segment.children, - (childOutlet, child) => expandSegment(configLoader, routes, child, childOutlet)); + (childOutlet, child) => expandSegment(injector, configLoader, routes, child, childOutlet)); } function expandPathsWithParams( - configLoader: RouterConfigLoader, segment: UrlSegment, routes: Route[], + injector: Injector, configLoader: RouterConfigLoader, segment: UrlSegment, routes: Route[], paths: UrlPathWithParams[], outlet: string, allowRedirects: boolean): Observable { const processRoutes = of (...routes) .map(r => { return expandPathsWithParamsAgainstRoute( - configLoader, segment, routes, r, paths, outlet, allowRedirects) + injector, configLoader, segment, routes, r, paths, outlet, allowRedirects) .catch((e) => { if (e instanceof NoMatch) return of (null); @@ -107,27 +109,28 @@ function expandPathsWithParams( } function expandPathsWithParamsAgainstRoute( - configLoader: RouterConfigLoader, segment: UrlSegment, routes: Route[], route: Route, - paths: UrlPathWithParams[], outlet: string, allowRedirects: boolean): Observable { + injector: Injector, configLoader: RouterConfigLoader, segment: UrlSegment, routes: Route[], + route: Route, paths: UrlPathWithParams[], outlet: string, + allowRedirects: boolean): Observable { if (getOutlet(route) !== outlet) return noMatch(segment); if (route.redirectTo !== undefined && !allowRedirects) return noMatch(segment); if (route.redirectTo !== undefined) { return expandPathsWithParamsAgainstRouteUsingRedirect( - configLoader, segment, routes, route, paths, outlet); + injector, configLoader, segment, routes, route, paths, outlet); } else { - return matchPathsWithParamsAgainstRoute(configLoader, segment, route, paths); + return matchPathsWithParamsAgainstRoute(injector, configLoader, segment, route, paths); } } function expandPathsWithParamsAgainstRouteUsingRedirect( - configLoader: RouterConfigLoader, segment: UrlSegment, routes: Route[], route: Route, - paths: UrlPathWithParams[], outlet: string): Observable { + injector: Injector, configLoader: RouterConfigLoader, segment: UrlSegment, routes: Route[], + route: Route, paths: UrlPathWithParams[], outlet: string): Observable { if (route.path === '**') { return expandWildCardWithParamsAgainstRouteUsingRedirect(route); } else { return expandRegularPathWithParamsAgainstRouteUsingRedirect( - configLoader, segment, routes, route, paths, outlet); + injector, configLoader, segment, routes, route, paths, outlet); } } @@ -141,8 +144,8 @@ function expandWildCardWithParamsAgainstRouteUsingRedirect(route: Route): Observ } function expandRegularPathWithParamsAgainstRouteUsingRedirect( - configLoader: RouterConfigLoader, segment: UrlSegment, routes: Route[], route: Route, - paths: UrlPathWithParams[], outlet: string): Observable { + injector: Injector, configLoader: RouterConfigLoader, segment: UrlSegment, routes: Route[], + route: Route, paths: UrlPathWithParams[], outlet: string): Observable { const {matched, consumedPaths, lastChild, positionalParamSegments} = match(segment, route, paths); if (!matched) return noMatch(segment); @@ -152,12 +155,13 @@ function expandRegularPathWithParamsAgainstRouteUsingRedirect( return absoluteRedirect(newPaths); } else { return expandPathsWithParams( - configLoader, segment, routes, newPaths.concat(paths.slice(lastChild)), outlet, false); + injector, configLoader, segment, routes, newPaths.concat(paths.slice(lastChild)), outlet, + false); } } function matchPathsWithParamsAgainstRoute( - configLoader: RouterConfigLoader, rawSegment: UrlSegment, route: Route, + injector: Injector, configLoader: RouterConfigLoader, rawSegment: UrlSegment, route: Route, paths: UrlPathWithParams[]): Observable { if (route.path === '**') { return of (new UrlSegment(paths, {})); @@ -168,11 +172,13 @@ function matchPathsWithParamsAgainstRoute( const rawSlicedPath = paths.slice(lastChild); - return getChildConfig(configLoader, route).mergeMap(childConfig => { + return getChildConfig(injector, configLoader, route).mergeMap(routerConfig => { + const childInjector = routerConfig.injector; + const childConfig = routerConfig.routes; const {segment, slicedPath} = split(rawSegment, consumedPaths, rawSlicedPath, childConfig); if (slicedPath.length === 0 && segment.hasChildren()) { - return expandSegmentChildren(configLoader, childConfig, segment) + return expandSegmentChildren(childInjector, configLoader, childConfig, segment) .map(children => new UrlSegment(consumedPaths, children)); } else if (childConfig.length === 0 && slicedPath.length === 0) { @@ -180,23 +186,25 @@ function matchPathsWithParamsAgainstRoute( } else { return expandPathsWithParams( - configLoader, segment, childConfig, slicedPath, PRIMARY_OUTLET, true) + childInjector, configLoader, segment, childConfig, slicedPath, PRIMARY_OUTLET, + true) .map(cs => new UrlSegment(consumedPaths.concat(cs.pathsWithParams), cs.children)); } }); } } -function getChildConfig(configLoader: RouterConfigLoader, route: Route): Observable { +function getChildConfig(injector: Injector, configLoader: RouterConfigLoader, route: Route): + Observable { if (route.children) { - return of (route.children); + return of (new LoadedRouterConfig(route.children, injector, null)); } else if (route.loadChildren) { - return configLoader.load(route.loadChildren).map(r => { + return configLoader.load(injector, route.loadChildren).map(r => { (route)._loadedConfig = r; - return r.routes; + return r; }); } else { - return of ([]); + return of (new LoadedRouterConfig([], injector, null)); } } diff --git a/modules/@angular/router/src/interfaces.ts b/modules/@angular/router/src/interfaces.ts index c9b7fe8e97..1373c8c65c 100644 --- a/modules/@angular/router/src/interfaces.ts +++ b/modules/@angular/router/src/interfaces.ts @@ -53,7 +53,7 @@ import {ActivatedRouteSnapshot, RouterStateSnapshot} from './router_state'; */ export interface CanActivate { canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): - Observable|boolean; + Observable|Promise|boolean; } /** @@ -114,7 +114,7 @@ export interface CanActivate { export interface CanActivateChild { canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): - Observable|boolean; + Observable|Promise|boolean; } /** @@ -161,7 +161,7 @@ export interface CanActivateChild { */ export interface CanDeactivate { canDeactivate(component: T, route: ActivatedRouteSnapshot, state: RouterStateSnapshot): - Observable|boolean; + Observable|Promise|boolean; } /** @@ -197,5 +197,6 @@ export interface CanDeactivate { * @experimental */ export interface Resolve { - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable|any; + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): + Observable|Promise|any; } diff --git a/modules/@angular/router/src/router.ts b/modules/@angular/router/src/router.ts index 94aac7b6c8..f9094a9ca7 100644 --- a/modules/@angular/router/src/router.ts +++ b/modules/@angular/router/src/router.ts @@ -29,7 +29,7 @@ import {createUrlTree} from './create_url_tree'; import {RouterOutlet} from './directives/router_outlet'; import {recognize} from './recognize'; import {resolve} from './resolve'; -import {RouterConfigLoader} from './router_config_loader'; +import {LoadedRouterConfig, RouterConfigLoader} from './router_config_loader'; import {RouterOutletMap} from './router_outlet_map'; import {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState} from './router_state'; import {PRIMARY_OUTLET, Params} from './shared'; @@ -315,7 +315,7 @@ export class Router { const storedState = this.currentRouterState; const storedUrl = this.currentUrlTree; - applyRedirects(this.configLoader, url, this.config) + applyRedirects(this.injector, this.configLoader, url, this.config) .mergeMap(u => { appliedUrl = u; return recognize( @@ -522,7 +522,7 @@ class PreActivation { const canActivate = future._routeConfig ? future._routeConfig.canActivate : null; if (!canActivate || canActivate.length === 0) return Observable.of(true); const obs = Observable.from(canActivate).map(c => { - const guard = this.injector.get(c); + const guard = this.getToken(c, future, this.future); if (guard.canActivate) { return wrapIntoObservable(guard.canActivate(future, this.future)); } else { @@ -535,37 +535,37 @@ class PreActivation { private runCanActivateChild(path: ActivatedRouteSnapshot[]): Observable { const future = path[path.length - 1]; - const canActivateChildGuards = - path.slice(0, path.length - 1) - .reverse() - .map(p => { - const canActivateChild = p._routeConfig ? p._routeConfig.canActivateChild : null; - if (!canActivateChild || canActivateChild.length === 0) return null; - return {snapshot: future, node: p, guards: canActivateChild}; - }) - .filter(_ => _ !== null); + const canActivateChildGuards = path.slice(0, path.length - 1) + .reverse() + .map(p => this.extractCanActivateChild(p)) + .filter(_ => _ !== null); return andObservables(Observable.from(canActivateChildGuards).map(d => { const obs = Observable.from(d.guards).map(c => { - const guard = this.injector.get(c); + const guard = this.getToken(c, c.node, this.future); if (guard.canActivateChild) { - return wrapIntoObservable(guard.canActivateChild(d.snapshot, this.future)); + return wrapIntoObservable(guard.canActivateChild(future, this.future)); } else { - return wrapIntoObservable(guard(d.snapshot, this.future)); + return wrapIntoObservable(guard(future, this.future)); } }); return andObservables(obs); })); } + private extractCanActivateChild(p: ActivatedRouteSnapshot): + {node: ActivatedRouteSnapshot, guards: any[]} { + const canActivateChild = p._routeConfig ? p._routeConfig.canActivateChild : null; + if (!canActivateChild || canActivateChild.length === 0) return null; + return {node: p, guards: canActivateChild}; + } private runCanDeactivate(component: Object, curr: ActivatedRouteSnapshot): Observable { const canDeactivate = curr && curr._routeConfig ? curr._routeConfig.canDeactivate : null; if (!canDeactivate || canDeactivate.length === 0) return Observable.of(true); return Observable.from(canDeactivate) .map(c => { - const guard = this.injector.get(c); - + const guard = this.getToken(c, curr, this.curr); if (guard.canDeactivate) { return wrapIntoObservable(guard.canDeactivate(component, curr, this.curr)); } else { @@ -587,11 +587,17 @@ class PreActivation { private resolveNode(resolve: ResolveData, future: ActivatedRouteSnapshot): Observable { return waitForMap(resolve, (k, v) => { - const resolver = this.injector.get(v); + const resolver = this.getToken(v, future, this.future); return resolver.resolve ? wrapIntoObservable(resolver.resolve(future, this.future)) : wrapIntoObservable(resolver(future, this.future)); }); } + + private getToken(token: any, snapshot: ActivatedRouteSnapshot, state: RouterStateSnapshot): any { + const config = closestLoadedConfig(state, snapshot); + const injector = config ? config.injector : this.injector; + return injector.get(token); + } } function wrapIntoObservable(value: T | Observable): Observable { @@ -685,12 +691,11 @@ class ActivateRoutes { useValue: outletMap }]; - const parentFuture = this.futureState.parent(future); // find the closest parent? - const config = parentFuture ? parentFuture.snapshot._routeConfig : null; + const config = closestLoadedConfig(this.futureState.snapshot, future.snapshot); let loadedFactoryResolver: ComponentFactoryResolver = null; - if (config && (config)._loadedConfig) { - const loadedResolver = (config)._loadedConfig.factoryResolver; + if (config) { + const loadedResolver = config.factoryResolver; loadedFactoryResolver = loadedResolver; resolved.push({provide: ComponentFactoryResolver, useValue: loadedResolver}); }; @@ -710,6 +715,15 @@ class ActivateRoutes { } } +function closestLoadedConfig( + state: RouterStateSnapshot, snapshot: ActivatedRouteSnapshot): LoadedRouterConfig { + const b = state.pathFromRoot(snapshot).filter(s => { + const config = (s)._routeConfig; + return config && config._loadedConfig && s !== snapshot; + }); + return b.length > 0 ? (b[b.length - 1])._routeConfig._loadedConfig : null; +} + function andObservables(observables: Observable>): Observable { return observables.mergeAll().every(result => result === true); } diff --git a/modules/@angular/router/src/router_config_loader.ts b/modules/@angular/router/src/router_config_loader.ts index 9744f85469..632e8de43b 100644 --- a/modules/@angular/router/src/router_config_loader.ts +++ b/modules/@angular/router/src/router_config_loader.ts @@ -6,12 +6,13 @@ * found in the LICENSE file at https://angular.io/license */ -import {AppModuleFactoryLoader, AppModuleRef, ComponentFactoryResolver, OpaqueToken} from '@angular/core'; +import {AppModuleFactoryLoader, ComponentFactoryResolver, Injector, OpaqueToken} from '@angular/core'; import {Observable} from 'rxjs/Observable'; import {fromPromise} from 'rxjs/observable/fromPromise'; import {Route} from './config'; + /** * @deprecated use Routes */ @@ -19,16 +20,19 @@ export const ROUTER_CONFIG = new OpaqueToken('ROUTER_CONFIG'); export const ROUTES = new OpaqueToken('ROUTES'); export class LoadedRouterConfig { - constructor(public routes: Route[], public factoryResolver: ComponentFactoryResolver) {} + constructor( + public routes: Route[], public injector: Injector, + public factoryResolver: ComponentFactoryResolver) {} } export class RouterConfigLoader { constructor(private loader: AppModuleFactoryLoader) {} - load(path: string): Observable { + load(parentInjector: Injector, path: string): Observable { return fromPromise(this.loader.load(path).then(r => { - const ref = r.create(); - return new LoadedRouterConfig(ref.injector.get(ROUTES), ref.componentFactoryResolver); + const ref = r.create(parentInjector); + return new LoadedRouterConfig( + ref.injector.get(ROUTES), ref.injector, ref.componentFactoryResolver); })); } } \ No newline at end of file diff --git a/modules/@angular/router/test/apply_redirects.spec.ts b/modules/@angular/router/test/apply_redirects.spec.ts index 307d9e7f56..d2b8d69705 100644 --- a/modules/@angular/router/test/apply_redirects.spec.ts +++ b/modules/@angular/router/test/apply_redirects.spec.ts @@ -29,7 +29,7 @@ describe('applyRedirects', () => { }); it('should throw when cannot handle a positional parameter', () => { - applyRedirects(null, tree('/a/1'), [ + applyRedirects(null, null, tree('/a/1'), [ {path: 'a/:id', redirectTo: 'a/:other'} ]).subscribe(() => {}, (e) => { expect(e.message).toEqual('Cannot redirect to \'a/:other\'. Cannot find \':other\'.'); @@ -133,12 +133,17 @@ describe('applyRedirects', () => { describe('lazy loading', () => { it('should load config on demand', () => { - const loadedConfig = - new LoadedRouterConfig([{path: 'b', component: ComponentB}], 'stubFactoryResolver'); - const loader = {load: (p: any) => of (loadedConfig)}; + const loadedConfig = new LoadedRouterConfig( + [{path: 'b', component: ComponentB}], 'stubInjector', 'stubFactoryResolver'); + const loader = { + load: (injector: any, p: any) => { + if (injector !== 'providedInjector') throw 'Invalid Injector'; + return of (loadedConfig); + } + }; const config = [{path: 'a', component: ComponentA, loadChildren: 'children'}]; - applyRedirects(loader, tree('a/b'), config).forEach(r => { + applyRedirects('providedInjector', loader, tree('a/b'), config).forEach(r => { compareTrees(r, tree('/a/b')); expect((config[0])._loadedConfig).toBe(loadedConfig); }); @@ -150,7 +155,7 @@ describe('applyRedirects', () => { }; const config = [{path: 'a', component: ComponentA, loadChildren: 'children'}]; - applyRedirects(loader, tree('a/b'), config).subscribe(() => {}, (e) => { + applyRedirects(null, loader, tree('a/b'), config).subscribe(() => {}, (e) => { expect(e.message).toEqual('Loading Error'); }); }); @@ -199,7 +204,7 @@ describe('applyRedirects', () => { {path: '', redirectTo: 'a', pathMatch: 'full'} ]; - applyRedirects(null, tree('b'), config) + applyRedirects(null, null, tree('b'), config) .subscribe( (_) => { throw 'Should not be reached'; }, e => { expect(e.message).toEqual('Cannot match any routes: \'b\''); }); @@ -329,7 +334,7 @@ describe('applyRedirects', () => { ] }]; - applyRedirects(null, tree('a/(d//aux:e)'), config) + applyRedirects(null, null, tree('a/(d//aux:e)'), config) .subscribe( (_) => { throw 'Should not be reached'; }, e => { expect(e.message).toEqual('Cannot match any routes: \'a\''); }); @@ -339,7 +344,7 @@ describe('applyRedirects', () => { }); function checkRedirect(config: Routes, url: string, callback: any): void { - applyRedirects(null, tree(url), config).subscribe(callback, e => { throw e; }); + applyRedirects(null, null, tree(url), config).subscribe(callback, e => { throw e; }); } function tree(url: string): UrlTree { diff --git a/modules/@angular/router/test/router.spec.ts b/modules/@angular/router/test/router.spec.ts index 90a05b6447..0a14d504e0 100644 --- a/modules/@angular/router/test/router.spec.ts +++ b/modules/@angular/router/test/router.spec.ts @@ -907,23 +907,23 @@ describe('Integration', () => { }); - fit('works', - fakeAsync(inject( - [Router, TestComponentBuilder, Location], - (router: Router, tcb: TestComponentBuilder, location: Location) => { - const fixture = createRoot(tcb, router, RootCmp); + it('works', + fakeAsync(inject( + [Router, TestComponentBuilder, Location], + (router: Router, tcb: TestComponentBuilder, location: Location) => { + const fixture = createRoot(tcb, router, RootCmp); - router.resetConfig( - [{path: 'team/:id', component: TeamCmp, canActivate: ['CanActivate']}]); + router.resetConfig( + [{path: 'team/:id', component: TeamCmp, canActivate: ['CanActivate']}]); - router.navigateByUrl('/team/22'); - advance(fixture); - expect(location.path()).toEqual('/team/22'); + router.navigateByUrl('/team/22'); + advance(fixture); + expect(location.path()).toEqual('/team/22'); - router.navigateByUrl('/team/33'); - advance(fixture); - expect(location.path()).toEqual('/team/22'); - }))); + router.navigateByUrl('/team/33'); + advance(fixture); + expect(location.path()).toEqual('/team/22'); + }))); }); }); @@ -1274,6 +1274,42 @@ describe('Integration', () => { .toHaveText('lazy-loaded-parent [lazy-loaded-child]'); }))); + it('should use the injector of the lazily-loaded configuration', + fakeAsync(inject( + [Router, TestComponentBuilder, Location, AppModuleFactoryLoader], + (router: Router, tcb: TestComponentBuilder, location: Location, + loader: SpyAppModuleFactoryLoader) => { + @Component({selector: 'lazy', template: 'lazy-loaded', directives: ROUTER_DIRECTIVES}) + class LazyLoadedComponent { + } + + @AppModule({ + precompile: [LazyLoadedComponent], + providers: [ + provideRoutes([{ + path: '', + canActivate: ['alwaysTrue'], + children: [{path: 'loaded', component: LazyLoadedComponent}] + }]), + {provide: 'alwaysTrue', useValue: () => true} + ] + }) + class LoadedModule { + } + + loader.stubbedModules = {expected: LoadedModule}; + + const fixture = createRoot(tcb, router, RootCmp); + + router.resetConfig([{path: 'lazy', loadChildren: 'expected'}]); + + router.navigateByUrl('/lazy/loaded'); + advance(fixture); + + expect(location.path()).toEqual('/lazy/loaded'); + expect(fixture.debugElement.nativeElement).toHaveText('lazy-loaded'); + }))); + it('error emit an error when cannot load a config', fakeAsync(inject( [Router, TestComponentBuilder, Location, AppModuleFactoryLoader], diff --git a/tools/public_api_guard/router/index.d.ts b/tools/public_api_guard/router/index.d.ts index ef970f01c7..53755d92ee 100644 --- a/tools/public_api_guard/router/index.d.ts +++ b/tools/public_api_guard/router/index.d.ts @@ -21,12 +21,17 @@ export declare class ActivatedRouteSnapshot { /** @stable */ export interface CanActivate { - canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | boolean; + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | Promise | boolean; +} + +/** @stable */ +export interface CanActivateChild { + canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | Promise | boolean; } /** @stable */ export interface CanDeactivate { - canDeactivate(component: T, route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | boolean; + canDeactivate(component: T, route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | Promise | boolean; } /** @stable */ @@ -101,7 +106,7 @@ export declare function provideRoutes(routes: Routes): any; /** @experimental */ export interface Resolve { - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | any; + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | Promise | any; } /** @stable */