diff --git a/karma-js.conf.js b/karma-js.conf.js index 35fbdae03f..dffbbf61ac 100644 --- a/karma-js.conf.js +++ b/karma-js.conf.js @@ -58,7 +58,7 @@ module.exports = function(config) { 'dist/all/@angular/compiler/test/aot/**', 'dist/all/@angular/examples/**/e2e_test/*', 'dist/all/@angular/language-service/**', - 'dist/all/@angular/router/**', + 'dist/all/@angular/router/test/**', 'dist/all/@angular/platform-browser/testing/e2e_util.js', 'dist/all/angular1_router.js', 'dist/examples/**/e2e_test/**', diff --git a/packages/core/test/animation/animation_router_integration_spec.ts b/packages/core/test/animation/animation_router_integration_spec.ts new file mode 100644 index 0000000000..f540f3c7af --- /dev/null +++ b/packages/core/test/animation/animation_router_integration_spec.ts @@ -0,0 +1,359 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {animate, animateChild, query, style, transition, trigger, ɵAnimationGroupPlayer as AnimationGroupPlayer} from '@angular/animations'; +import {AnimationDriver, ɵAnimationEngine} from '@angular/animations/browser'; +import {MockAnimationDriver, MockAnimationPlayer} from '@angular/animations/browser/testing'; +import {Component, HostBinding} from '@angular/core'; +import {TestBed, fakeAsync, tick} from '@angular/core/testing'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {Router, RouterOutlet} from '@angular/router'; +import {RouterTestingModule} from '@angular/router/testing'; + +export function main() { + // these tests are only mean't to be run within the DOM (for now) + if (typeof Element == 'undefined') return; + + describe('Animation Router Tests', function() { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [RouterTestingModule, BrowserAnimationsModule], + providers: [{provide: AnimationDriver, useClass: MockAnimationDriver}] + }); + }); + + it('should query the old and new routes via :leave and :enter', fakeAsync(() => { + @Component({ + animations: [ + trigger( + 'routerAnimations', + [ + transition( + 'page1 => page2', + [ + query(':leave', animateChild()), + query(':enter', animateChild()), + ]), + ]), + ], + template: ` +
+ +
+ ` + }) + class ContainerCmp { + constructor(public router: Router) {} + + prepareRouteAnimation(r: RouterOutlet) { + const animation = r.activatedRouteData['animation']; + const value = animation ? animation['value'] : null; + return value; + } + } + + @Component({ + selector: 'page1', + template: `page1`, + animations: [ + trigger( + 'page1Animation', + [ + transition( + ':leave', + [ + style({width: '200px'}), + animate(1000, style({width: '0px'})), + ]), + ]), + ] + }) + class Page1Cmp { + @HostBinding('@page1Animation') public doAnimate = true; + } + + @Component({ + selector: 'page2', + template: `page2`, + animations: [ + trigger( + 'page2Animation', + [ + transition( + ':enter', + [ + style({opacity: 0}), + animate(1000, style({opacity: 1})), + ]), + ]), + ] + }) + class Page2Cmp { + @HostBinding('@page2Animation') public doAnimate = true; + } + + TestBed.configureTestingModule({ + declarations: [Page1Cmp, Page2Cmp, ContainerCmp], + imports: [RouterTestingModule.withRoutes([ + {path: 'page1', component: Page1Cmp, data: makeAnimationData('page1')}, + {path: 'page2', component: Page2Cmp, data: makeAnimationData('page2')} + ])] + }); + + const engine = TestBed.get(ɵAnimationEngine); + const fixture = TestBed.createComponent(ContainerCmp); + const cmp = fixture.componentInstance; + cmp.router.initialNavigation(); + tick(); + fixture.detectChanges(); + engine.flush(); + + cmp.router.navigateByUrl('/page1'); + tick(); + fixture.detectChanges(); + engine.flush(); + + cmp.router.navigateByUrl('/page2'); + tick(); + fixture.detectChanges(); + engine.flush(); + + const player = engine.players[0] !; + const groupPlayer = player.getRealPlayer() as AnimationGroupPlayer; + const players = groupPlayer.players as MockAnimationPlayer[]; + + expect(players.length).toEqual(2); + const [p1, p2] = players; + + expect(p1.duration).toEqual(1000); + expect(p1.keyframes).toEqual([ + {offset: 0, width: '200px'}, + {offset: 1, width: '0px'}, + ]); + + expect(p2.duration).toEqual(2000); + expect(p2.keyframes).toEqual([ + {offset: 0, opacity: '0'}, + {offset: .5, opacity: '0'}, + {offset: 1, opacity: '1'}, + ]); + })); + + it('should allow inner enter animations to be emulated within a routed item', fakeAsync(() => { + @Component({ + animations: [ + trigger( + 'routerAnimations', + [ + transition( + 'page1 => page2', + [ + query(':enter', animateChild()), + ]), + ]), + ], + template: ` +
+ +
+ ` + }) + class ContainerCmp { + constructor(public router: Router) {} + + prepareRouteAnimation(r: RouterOutlet) { + const animation = r.activatedRouteData['animation']; + const value = animation ? animation['value'] : null; + return value; + } + } + + @Component({selector: 'page1', template: `page1`, animations: []}) + class Page1Cmp { + } + + @Component({ + selector: 'page2', + template: ` +

Page 2

+
+
+ `, + animations: [ + trigger( + 'page2Animation', + [ + transition( + ':enter', + [query('.if-one', animateChild()), query('.if-two', animateChild())]), + ]), + trigger( + 'ifAnimation', + [transition( + ':enter', [style({opacity: 0}), animate(1000, style({opacity: 1}))])]) + ] + }) + class Page2Cmp { + @HostBinding('@page2Animation') public doAnimate = true; + + public exp = true; + } + + TestBed.configureTestingModule({ + declarations: [Page1Cmp, Page2Cmp, ContainerCmp], + imports: [RouterTestingModule.withRoutes([ + {path: 'page1', component: Page1Cmp, data: makeAnimationData('page1')}, + {path: 'page2', component: Page2Cmp, data: makeAnimationData('page2')} + ])] + }); + + const engine = TestBed.get(ɵAnimationEngine); + const fixture = TestBed.createComponent(ContainerCmp); + const cmp = fixture.componentInstance; + cmp.router.initialNavigation(); + tick(); + fixture.detectChanges(); + engine.flush(); + + cmp.router.navigateByUrl('/page1'); + tick(); + fixture.detectChanges(); + engine.flush(); + + cmp.router.navigateByUrl('/page2'); + tick(); + fixture.detectChanges(); + engine.flush(); + + const player = engine.players[0] !; + const groupPlayer = player.getRealPlayer() as AnimationGroupPlayer; + const players = groupPlayer.players as MockAnimationPlayer[]; + + expect(players.length).toEqual(2); + const [p1, p2] = players; + + expect(p1.keyframes).toEqual([ + {offset: 0, opacity: '0'}, + {offset: 1, opacity: '1'}, + ]); + + expect(p2.keyframes).toEqual([ + {offset: 0, opacity: '0'}, + {offset: .5, opacity: '0'}, + {offset: 1, opacity: '1'}, + ]); + })); + + it('should allow inner leave animations to be emulated within a routed item', fakeAsync(() => { + @Component({ + animations: [ + trigger( + 'routerAnimations', + [ + transition( + 'page1 => page2', + [ + query(':leave', animateChild()), + ]), + ]), + ], + template: ` +
+ +
+ ` + }) + class ContainerCmp { + constructor(public router: Router) {} + + prepareRouteAnimation(r: RouterOutlet) { + const animation = r.activatedRouteData['animation']; + const value = animation ? animation['value'] : null; + return value; + } + } + + @Component({ + selector: 'page1', + template: ` +

Page 1

+
+
+ `, + animations: [ + trigger( + 'page1Animation', + [ + transition( + ':leave', + [query('.if-one', animateChild()), query('.if-two', animateChild())]), + ]), + trigger( + 'ifAnimation', + [transition(':leave', [style({opacity: 1}), animate(1000, style({opacity: 0}))])]), + ] + }) + class Page1Cmp { + @HostBinding('@page1Animation') public doAnimate = true; + + public exp = true; + } + + @Component({selector: 'page2', template: `page2`, animations: []}) + class Page2Cmp { + } + + TestBed.configureTestingModule({ + declarations: [Page1Cmp, Page2Cmp, ContainerCmp], + imports: [RouterTestingModule.withRoutes([ + {path: 'page1', component: Page1Cmp, data: makeAnimationData('page1')}, + {path: 'page2', component: Page2Cmp, data: makeAnimationData('page2')} + ])] + }); + + const engine = TestBed.get(ɵAnimationEngine); + const fixture = TestBed.createComponent(ContainerCmp); + const cmp = fixture.componentInstance; + cmp.router.initialNavigation(); + tick(); + fixture.detectChanges(); + engine.flush(); + + cmp.router.navigateByUrl('/page1'); + tick(); + fixture.detectChanges(); + engine.flush(); + + cmp.router.navigateByUrl('/page2'); + tick(); + fixture.detectChanges(); + engine.flush(); + + const player = engine.players[0] !; + const groupPlayer = player.getRealPlayer() as AnimationGroupPlayer; + const players = groupPlayer.players as MockAnimationPlayer[]; + + expect(players.length).toEqual(2); + const [p1, p2] = players; + + expect(p1.keyframes).toEqual([ + {offset: 0, opacity: '1'}, + {offset: 1, opacity: '0'}, + ]); + + expect(p2.keyframes).toEqual([ + {offset: 0, opacity: '1'}, + {offset: .5, opacity: '1'}, + {offset: 1, opacity: '0'}, + ]); + })); + }); +} + +function makeAnimationData(value: string, params: {[key: string]: any} = {}): {[key: string]: any} { + return {'animation': {value, params}}; +} diff --git a/packages/router/src/directives/router_outlet.ts b/packages/router/src/directives/router_outlet.ts index 12612f91fc..1bf9a0ef2c 100644 --- a/packages/router/src/directives/router_outlet.ts +++ b/packages/router/src/directives/router_outlet.ts @@ -36,7 +36,7 @@ import {PRIMARY_OUTLET} from '../shared'; * * @stable */ -@Directive({selector: 'router-outlet'}) +@Directive({selector: 'router-outlet', exportAs: 'outlet'}) export class RouterOutlet implements OnDestroy, OnInit { private activated: ComponentRef|null = null; private _activatedRoute: ActivatedRoute|null = null; @@ -89,6 +89,13 @@ export class RouterOutlet implements OnDestroy, OnInit { return this._activatedRoute as ActivatedRoute; } + get activatedRouteData() { + if (this._activatedRoute) { + return this._activatedRoute.snapshot.data; + } + return {}; + } + /** * Called when the `RouteReuseStrategy` instructs to detach the subtree */ @@ -155,4 +162,4 @@ class OutletInjector implements Injector { return this.parent.get(token, notFoundValue); } -} \ No newline at end of file +} diff --git a/tools/public_api_guard/router/router.d.ts b/tools/public_api_guard/router/router.d.ts index e230887802..be4cac7112 100644 --- a/tools/public_api_guard/router/router.d.ts +++ b/tools/public_api_guard/router/router.d.ts @@ -351,6 +351,9 @@ export declare class RouterModule { export declare class RouterOutlet implements OnDestroy, OnInit { activateEvents: EventEmitter; readonly activatedRoute: ActivatedRoute; + readonly activatedRouteData: { + [name: string]: any; + }; readonly component: Object; deactivateEvents: EventEmitter; readonly isActivated: boolean;