docs(router): Added section for named outlets and secondary routes (#2842)

This commit is contained in:
Brandon 2016-12-20 17:26:09 -06:00 committed by Ward Bell
parent b47757a0fb
commit 397c337378
15 changed files with 409 additions and 57 deletions

View File

@ -28,21 +28,25 @@ describe('Router', function () {
adminHref: hrefEles.get(2),
adminPreloadList: element.all(by.css('my-app > ng-component > ng-component > ul > li')),
loginHref: hrefEles.get(3),
loginButton: element.all(by.css('my-app > ng-component > p > button')),
sidekicksButton: element.all(by.css('my-app > ng-component > button')),
contactHref: hrefEles.get(4),
contactCancelButton: element.all(by.buttonText('Cancel')),
outletComponents: element.all(by.css('my-app > ng-component'))
};
}
it('should be able to see the start screen', function () {
let page = getPageStruct();
expect(page.hrefs.count()).toEqual(4, 'should be 4 dashboard choices');
expect(page.hrefs.count()).toEqual(5, 'should be 5 dashboard choices');
expect(page.crisisHref.getText()).toEqual('Crisis Center');
expect(page.heroesHref.getText()).toEqual('Heroes');
expect(page.adminHref.getText()).toEqual('Admin');
expect(page.loginHref.getText()).toEqual('Login');
expect(page.contactHref.getText()).toEqual('Contact');
});
it('should be able to see crises center items', function () {
@ -120,12 +124,12 @@ describe('Router', function () {
});
});
it('should be able to handle 404 pages', function () {
it('should be able to see the secondary route', function () {
let page = getPageStruct();
page.heroesHref.click().then(function() {
return page.sidekicksButton.click();
return page.contactHref.click();
}).then(function() {
expect(page.routerTitle.getText()).toContain('Page Not Found');
expect(page.outletComponents.count()).toBe(2, 'should be 2 displayed routes');
});
});

View File

@ -1,11 +1,17 @@
// #docplaster
// #docregion
import { NgModule } from '@angular/core';
// #docregion , v3
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { PageNotFoundComponent }from './not-found.component';
import { ComposeMessageComponent } from './compose-message.component';
const appRoutes: Routes = [
{ path: '**', component: PageNotFoundComponent }
// #enddocregion v3
{
path: 'compose',
component: ComposeMessageComponent,
outlet: 'modal'
}
// #docregion v3
];
@NgModule({

View File

@ -1,13 +1,17 @@
// #docplaster
// #docregion
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { PageNotFoundComponent }from './not-found.component';
import { CanDeactivateGuard } from './can-deactivate-guard.service';
import { ComposeMessageComponent } from './compose-message.component';
import { CanDeactivateGuard } from './can-deactivate-guard.service';
const appRoutes: Routes = [
{ path: '**', component: PageNotFoundComponent }
{
path: 'compose',
component: ComposeMessageComponent,
outlet: 'modal'
}
];
@NgModule({

View File

@ -4,15 +4,21 @@ import { NgModule } from '@angular/core';
// #docregion import-router
import { RouterModule, Routes } from '@angular/router';
// #enddocregion import-router
import { PageNotFoundComponent } from './not-found.component';
import { CanDeactivateGuard } from './can-deactivate-guard.service';
import { ComposeMessageComponent } from './compose-message.component';
import { CanDeactivateGuard } from './can-deactivate-guard.service';
// #docregion can-load-guard
import { AuthGuard } from './auth-guard.service';
import { AuthGuard } from './auth-guard.service';
// #enddocregion can-load-guard
// #docregion lazy-load-admin, can-load-guard
const appRoutes: Routes = [
{
path: 'compose',
component: ComposeMessageComponent,
outlet: 'modal'
},
{
path: 'admin',
loadChildren: 'app/admin/admin.module#AdminModule',

View File

@ -8,11 +8,16 @@ import {
// #docregion preload-v1
} from '@angular/router';
import { PageNotFoundComponent } from './not-found.component';
import { CanDeactivateGuard } from './can-deactivate-guard.service';
import { AuthGuard } from './auth-guard.service';
import { ComposeMessageComponent } from './compose-message.component';
import { CanDeactivateGuard } from './can-deactivate-guard.service';
import { AuthGuard } from './auth-guard.service';
const appRoutes: Routes = [
{
path: 'compose',
component: ComposeMessageComponent,
outlet: 'modal'
},
{
path: 'admin',
loadChildren: 'app/admin/admin.module#AdminModule',

View File

@ -3,12 +3,17 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { PageNotFoundComponent } from './not-found.component';
import { CanDeactivateGuard } from './can-deactivate-guard.service';
import { AuthGuard } from './auth-guard.service';
import { PreloadSelectedModules } from './selective-preload-strategy';
import { ComposeMessageComponent } from './compose-message.component';
import { CanDeactivateGuard } from './can-deactivate-guard.service';
import { AuthGuard } from './auth-guard.service';
import { PreloadSelectedModules } from './selective-preload-strategy';
const appRoutes: Routes = [
{
path: 'compose',
component: ComposeMessageComponent,
outlet: 'modal'
},
{
path: 'admin',
loadChildren: 'app/admin/admin.module#AdminModule',
@ -26,10 +31,6 @@ const appRoutes: Routes = [
data: {
preload: true
}
},
{
path: '**',
component: PageNotFoundComponent
}
// #enddocregion preload-v2
];

View File

@ -12,6 +12,9 @@ import { Component } from '@angular/core';
<a routerLink="/admin" routerLinkActive="active">Admin</a>
</nav>
<router-outlet></router-outlet>
// #enddocregion template
<router-outlet name="modal"></router-outlet>
// #enddocregion template
`
// #enddocregion template
})

View File

@ -12,8 +12,10 @@ import { Component } from '@angular/core';
<a routerLink="/heroes" routerLinkActive="active">Heroes</a>
<a routerLink="/admin" routerLinkActive="active">Admin</a>
<a routerLink="/login" routerLinkActive="active">Login</a>
<a [routerLink]="[{ outlets: { modal: ['compose'] } }]">Contact</a>
</nav>
<router-outlet></router-outlet>
<router-outlet name="modal"></router-outlet>
`
// #enddocregion template
})

View File

@ -5,19 +5,19 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { PageNotFoundComponent }from './not-found.component';
import { AppRoutingModule } from './app-routing.module';
import { HeroesModule } from './heroes/heroes.module';
import { AppComponent } from './app.component';
import { PageNotFoundComponent } from './not-found.component';
import { AppRoutingModule } from './app-routing.module';
import { HeroesModule } from './heroes/heroes.module';
// #docregion crisis-center-module
import { CrisisCenterModule } from './crisis-center/crisis-center.module';
import { CrisisCenterModule } from './crisis-center/crisis-center.module';
// #enddocregion crisis-center-module
import { ComposeMessageComponent } from './compose-message.component';
// #docregion admin-module
import { AdminModule } from './admin/admin.module';
import { AdminModule } from './admin/admin.module';
// #docregion crisis-center-module
import { DialogService } from './dialog.service';
import { DialogService } from './dialog.service';
@NgModule({
imports: [
@ -26,13 +26,16 @@ import { DialogService } from './dialog.service';
HeroesModule,
CrisisCenterModule,
// #enddocregion crisis-center-module
// #enddocregion admin-module
AdminModule,
// #docregion crisis-center-module
AppRoutingModule
],
declarations: [
AppComponent,
PageNotFoundComponent
// #enddocregion admin-module, crisis-center-module
ComposeMessageComponent
// #docregion admin-module, crisis-center-module
],
providers: [
DialogService

View File

@ -1,21 +1,22 @@
// #docplaster
// #docregion
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { PageNotFoundComponent }from './not-found.component';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { HeroesModule } from './heroes/heroes.module';
import { CrisisCenterModule } from './crisis-center/crisis-center.module';
import { AdminModule } from './admin/admin.module';
import { HeroesModule } from './heroes/heroes.module';
import { CrisisCenterModule } from './crisis-center/crisis-center.module';
import { ComposeMessageComponent } from './compose-message.component';
import { DialogService } from './dialog.service';
import { AdminModule } from './admin/admin.module';
import { DialogService } from './dialog.service';
@NgModule({
imports: [
BrowserModule,
CommonModule,
FormsModule,
HeroesModule,
CrisisCenterModule,
@ -24,7 +25,7 @@ import { DialogService } from './dialog.service';
],
declarations: [
AppComponent,
PageNotFoundComponent
ComposeMessageComponent
],
providers: [
DialogService
@ -33,3 +34,4 @@ import { DialogService } from './dialog.service';
})
export class AppModule {
}
// #enddocregion

View File

@ -7,8 +7,10 @@ import { AppComponent } from './app.component';
import { PageNotFoundComponent }from './not-found.component';
import { AppRoutingModule } from './app-routing.module';
import { HeroesModule } from './heroes/heroes.module';
import { CrisisCenterModule } from './crisis-center/crisis-center.module';
import { HeroesModule } from './heroes/heroes.module';
import { CrisisCenterModule } from './crisis-center/crisis-center.module';
import { ComposeMessageComponent } from './compose-message.component';
import { LoginRoutingModule } from './login-routing.module';
import { LoginComponent } from './login.component';
@ -25,7 +27,7 @@ import { DialogService } from './dialog.service';
],
declarations: [
AppComponent,
PageNotFoundComponent,
ComposeMessageComponent,
LoginComponent
],
providers: [

View File

@ -7,9 +7,10 @@ import { AppComponent } from './app.component';
import { PageNotFoundComponent } from './not-found.component';
import { AppRoutingModule } from './app-routing.module';
import { HeroesModule } from './heroes/heroes.module';
import { LoginRoutingModule } from './login-routing.module';
import { LoginComponent } from './login.component';
import { HeroesModule } from './heroes/heroes.module';
import { ComposeMessageComponent } from './compose-message.component';
import { LoginRoutingModule } from './login-routing.module';
import { LoginComponent } from './login.component';
import { DialogService } from './dialog.service';
@ -23,7 +24,7 @@ import { DialogService } from './dialog.service';
],
declarations: [
AppComponent,
PageNotFoundComponent,
ComposeMessageComponent,
LoginComponent
],
providers: [

View File

@ -0,0 +1,108 @@
// #docplaster
// #docregion
// #docregion v1
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/delay';
import 'rxjs/add/operator/do';
import { Component, HostBinding,
trigger, transition,
animate, style, state } from '@angular/core';
import { Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
@Component({
template: `
<h3>Contact Crisis Center</h3>
<div *ngIf="details">
{{ details }}
</div>
<div>
<div>
<label>Message: </label>
</div>
<div>
<textarea [(ngModel)]="message" rows="10" cols="35" [disabled]="sending"></textarea>
</div>
</div>
<p *ngIf="!sending">
<button (click)="send()">Send</button>
// #enddocregion v1
<button (click)="cancel()">Cancel</button>
// #docregion v1
</p>
`,
styles: [
`
:host {
position: relative;
bottom: 10%;
}
`
],
animations: [
trigger('routeAnimation', [
state('*',
style({
opacity: 1,
transform: 'translateX(0)'
})
),
transition(':enter', [
style({
opacity: 0,
transform: 'translateY(100%)'
}),
animate('0.2s ease-in')
]),
transition(':leave', [
animate('0.5s ease-out', style({
opacity: 0,
transform: 'translateY(100%)'
}))
])
])
]
})
export class ComposeMessageComponent {
@HostBinding('@routeAnimation') get routeAnimation() {
return true;
}
@HostBinding('style.display') get display() {
return 'block';
}
@HostBinding('style.position') get position() {
return 'absolute';
}
details: string;
sending: boolean = false;
constructor(private router: Router) {}
send() {
this.sending = true;
this.details = 'Sending Message...';
Observable.of(true)
.delay(1000)
.do(() => {
this.sending = false;
// #enddocregion v1
this.closeModal();
// #docregion v1
}).subscribe();
}
// #enddocregion v1
closeModal() {
this.router.navigate(['/', { outlets: { modal: null }}]);
}
cancel() {
this.closeModal();
}
}
// #enddocregion

View File

@ -0,0 +1,105 @@
// #docregion
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/delay';
import 'rxjs/add/operator/do';
import { Component, HostBinding,
trigger, transition,
animate, style, state } from '@angular/core';
import { Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
@Component({
template: `
<h3>Contact Crisis Center</h3>
<div *ngIf="details">
{{ details }}
</div>
<div>
<div>
<label>Message: </label>
</div>
<div>
<textarea [(ngModel)]="message" rows="10" cols="35" [disabled]="sending"></textarea>
</div>
</div>
<p *ngIf="!sending">
<button (click)="send()">Send</button>
<button (click)="cancel()">Cancel</button>
</p>
`,
styles: [
`
:host {
position: relative;
bottom: 10%;
}
`
],
animations: [
trigger('routeAnimation', [
state('*',
style({
opacity: 1,
transform: 'translateX(0)'
})
),
transition(':enter', [
style({
opacity: 0,
transform: 'translateY(100%)'
}),
animate('0.2s ease-in')
]),
transition(':leave', [
animate('0.5s ease-out', style({
opacity: 0,
transform: 'translateY(100%)'
}))
])
])
]
})
export class ComposeMessageComponent {
@HostBinding('@routeAnimation') get routeAnimation() {
return true;
}
@HostBinding('style.display') get display() {
return 'block';
}
@HostBinding('style.position') get position() {
return 'absolute';
}
details: string;
sending: boolean = false;
constructor(private router: Router) {}
send() {
this.sending = true;
this.details = 'Sending Message...';
Observable.of(true)
.delay(1000)
.do(() => {
this.sending = false;
// Close the modal
this.closeModal();
}).subscribe();
}
closeModal() {
// Providing a `null` value to the named outlet
// clears the contents of the named outlet
this.router.navigate([{ outlets: { modal: null }}]);
}
cancel() {
// Close the modal
this.closeModal();
}
}

View File

@ -47,6 +47,7 @@ include ../../../_includes/_see-addr-bar
* refactoring routing into a [routing module](#routing-module)
* add [child routes](#child-routing-component) under a feature section
* [grouping child routes](#component-less-route) without a component
* displaying [multiple routes](#named-outlets) in separate outlets
* [redirecting](#redirect) from one route to another
* confirming or canceling navigation with [guards](#guards)
* [CanActivate](#can-activate-guard) to prevent navigation to a route
@ -473,7 +474,7 @@ a#router-outlet
.l-sub-section
:marked
A template may hold exactly one ***unnamed*** `<router-outlet>`.
The router supports multiple *named* outlets, a feature we'll cover in future.
The router supports multiple [named outlets](#named-outlets), covered later in the chapter.
a#router-link
:marked
@ -1313,7 +1314,7 @@ h3#merge-hero-routes Import hero module into AppModule
router/ts/app/heroes/hero.service.ts,
router/ts/app/heroes/heroes.module.ts,
router/ts/app/heroes/heroes-routing.module.ts`,
null,
'null,null,v3,null,null,null,null,null',
`app.component.ts,
app.module.ts,
app-routing.module.ts,
@ -1353,6 +1354,8 @@ h3#merge-hero-routes Import hero module into AppModule
* The router should block access to certain features until the user logs-in.
* The application should display multiple routes indepdently of each other.
* Changes to a feature module such as *Crisis Center* shouldn't provoke changes to the `AppModule` or
any other feature's component.
We need to [*separate our concerns*](https://blog.8thlight.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html).
@ -1478,7 +1481,7 @@ h3#import-crisis-module Import crisis center module into the AppModule routes
are now being provided by our `HeroesModule` and our `CrisisCenter` feature modules. We'll keep our `app-routing.module.ts` file
for general routes which we'll cover later in the chapter.
+makeExcerpt('app/app-routing.module.3.ts (v3)', '')
+makeExcerpt('app/app-routing.module.3.ts (v3)', 'v3')
a#redirect
:marked
@ -1603,6 +1606,103 @@ h2#relative-navigation Relative Navigation
+makeExcerpt('app/crisis-center/crisis-list.component.1.ts (relative routerLink)', 'relative-navigation-router-link')
<a id="secondary-routes"></a>
.l-main-section
h3#named-outlets Displaying Multiple Routes: Named Outlets and Secondary Routes
:marked
Up until now, we've used a single outlet and we've nested child routes under that outlet to group routes together.
The `Router` supports one primary `unnamed` outlet, but we can display multiple routes together, each displaying their own component
using one or more **named outlets**.
These named outlets are used to display **secondary routes**, which are configured the same way as a primary route, but
are independent of each other and can work in combination with each other routes. By using named outlets with secondary routes,
we can display multiple route components simultaneously. Secondary routes can also have their own child routes. This allows us to reflect the state of multiple
views in our application using the browser URL.
In our application, we want to give our users a way to contact the `Crisis Center` displayed through a modal. We also want this modal
to be displayed even when switching between pages in our application. We'll use a named outlet to display the modal and control its lifecycle
alongside our currently displayed route.
We'll create a new route component named `ComposeMessageComponent` in `app/compose-message.component.ts` to display our modal view the users will
use to contact the `Crisis Center`. We'll display a simple form to enter a message and a use an `Observable` to simulate a delay to handle the modal after
sending the message.
+makeExcerpt('app/compose-message.component.1.ts (compose message component)', 'v1')
:marked
We'll add a `compose` route to our `AppRoutingModule`, using a route `path` and `component`. In order to use a named outlet, we add an additional property
to our route configuration called **outlet**. This outlet matches the name given to our `RouterOutlet` where we are going to to display the component. The `Router`
will place each route next to its intended `RouterOutlet` during the navigation process.
+makeExcerpt('app/app-routing.module.3.ts (compose route)', '')
:marked
As with our other components, the `ComposeMessageComponent` is imported and added to our `AppModule`'s declarations.
+makeExcerpt('app/app.module.5.ts (compose component)', '')
:marked
As we did in our initial template, we'll add a `RouterOutlet` with an additional **name** attribute and provide our named **modal** outlet. This matches the
name of the `outlet` we used in our `AppRoutingModule`.
+makeExcerpt('app/app.component.4.ts (named outlet)', '')
:marked
Now we have two independent `RouterOutlet`s to display our routes. Routes without a specified `outlet` will use our primary `unnamed` outlet. Our `compose` outlet
will display our `ComposeMessageComponent`. Secondary routes are also reflected in our URL surrounded by parenthesis but are are seamlessly
handled by the `Router` across browsers the same way it handles [route parameters with matrix notation](#optional-route-parameters). We can visit any primary route in our application
and have our `compose` route displayed in combination with the primary route. An example URL displaying the `Crisis Center` list and `Compose` modal is displayed below.
.l-sub-section
:marked
http://localhost:3000/crisis-center(modal:compose)
:marked
If we break down the URL, we see the following parts.
* The primary route with the `crisis-center` path
* The parenthesis where our secondary route grouping starts and ends
* The name of the outlet (`modal`) split by a `colon` and followed with the secondary route path `compose`
Secondary routes are not limited to top level routes. Each level in our route configuration can contain a primary route in an unnamed outlet,
followed by any number of secondary routes displayed in named outlets. Secondary routes have all the same features of a primary route, including
access to their own [activated route](#activated-route).
h3#secondary-route-navigation <i>Secondary Route Navigation</i>: merging routes during navigation
:marked
We learned how the `Router` handles incoming URLs for secondary routes, but we can also navigate declaratively and imperatively using secondary routes
in the [link parameters array](#link-parameters-array). Let's update our application to display our `compose` route using `RouterLink` and navigating
away from a secondary route using the `Router` service.
The `Link Parameters Array` supports secondary routes with an object as the last index in our array. This object uses an **outlets** that lets us build
our links including secondary outlets in the same way we build more dynamic router links. We'll add a `Contact` link to our `Crisis Center` menu to display our
secondary route.
+makeExcerpt('app/app.component.ts (contact)', '')
:marked
The `outlets` property within our object contains the named outlets that will be updated during the navigation process. We'll add a key for the `modal` outlet
and the value for the outlet is the same link parameters array we use with primary routes. When using a `RouterLink` or navigating imperatively, the `Router`
will **merge** the current URL with our provided secondary route. As we navigate around our application, the `Contact` link will be updated with the current URL
and the secondary `(modal:compose)` route, so that we can open our `compose` route independently of our currently displayed route. Once we visit the secondary route,
it will be merged with the current URL across each navigation as long as they're under the same parent path.
.l-sub-section
:marked
When using router's `navigateByUrl` method, secondary routes will **not** be merged into the current URL and will need to be explicitly
provided.
:marked
#### Clearing Secondary Routes
Secondary outlets persist across navigation under the same parent route. We can also remove secondary outlets using a `RouterLink` or imperatively
with the `Router` service using the [link parameters array](#link-parameters-array). We'll update our `ComposeMessageComponent` to remove the
secondary `compose` route when our user cancels the message or its sent successfully.
We'll add a `closeModal` method to our `ComposeMessageComponent` that navigates imperatively using the `router.navigate` method. Since we only want to modify the
secondary route, we only need to provide the secondary route object and `null` out the `modal` outlet. This will remove the secondary route from named `modal` `RouterOutlet`
and update the current URL.
+makeExcerpt('app/compose-message.component.ts (remove secondary route)', '')
<a id="guards"></a>
.l-main-section
h2#guards Route Guards