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;