diff --git a/.pullapprove.yml b/.pullapprove.yml index 29ae27c355..53b8aab37e 100644 --- a/.pullapprove.yml +++ b/.pullapprove.yml @@ -494,6 +494,8 @@ groups: 'packages/router/**', 'packages/examples/router/**', 'aio/content/guide/router.md', + 'aio/content/guide/router-tutorial.md', + 'aio/content/examples/router-tutorial/**', 'aio/content/examples/router/**', 'aio/content/images/guide/router/**' ]) diff --git a/aio/content/examples/router-tutorial/e2e/src/app.e2e-spec.ts b/aio/content/examples/router-tutorial/e2e/src/app.e2e-spec.ts new file mode 100644 index 0000000000..ac01714126 --- /dev/null +++ b/aio/content/examples/router-tutorial/e2e/src/app.e2e-spec.ts @@ -0,0 +1,42 @@ +import { browser, element, by } from 'protractor'; + +describe('Router basic tutorial e2e tests', () => { + + beforeEach(() => { + browser.get(''); + }); + + it('should display Angular Router Sample', () => { + expect(element(by.css('h1')).getText()).toBe('Angular Router Sample'); + }); + + it('should display Crisis Center button', () => { + expect(element.all(by.css('a')).get(0).getText()).toBe('Crisis Center'); + }); + + it('should display Heroes button', () => { + expect(element.all(by.css('a')).get(1).getText()).toBe('Heroes'); + }); + + it('should display HEROES', () => { + expect(element(by.css('h3')).getText()).toBe('HEROES'); + }); + + it('should change to display crisis list component', async () => { + const crisisButton = element.all(by.css('a')).get(0); + await crisisButton.click(); + expect(element(by.css('h3')).getText()).toBe('CRISIS CENTER'); + }); + + it('should change to display heroes component', async () => { + const heroesButton = element.all(by.css('a')).get(1); + await heroesButton.click(); + expect(element(by.css('h3')).getText()).toBe('HEROES'); + }); + + it('should use wildcard route', async () => { + browser.get('/fake-page'); + expect(browser.getCurrentUrl()).toContain('fake-page'); + expect(element(by.css('h2')).getText()).toBe('Page Not Found'); + }); +}); diff --git a/aio/content/examples/router-tutorial/example-config.json b/aio/content/examples/router-tutorial/example-config.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/router-tutorial/src/app/app.component.css b/aio/content/examples/router-tutorial/src/app/app.component.css new file mode 100644 index 0000000000..fcefcc4011 --- /dev/null +++ b/aio/content/examples/router-tutorial/src/app/app.component.css @@ -0,0 +1,34 @@ +.button { + box-shadow: inset 0px 1px 0px 0px #ffffff; + background: linear-gradient(to bottom, #ffffff 5%, #f6f6f6 100%); + background-color: #ffffff; + border-radius: 6px; + border: 1px solid #dcdcdc; + display: inline-block; + cursor: pointer; + color: #666666; + font-family: Arial; + font-size: 15px; + font-weight: bold; + padding: 6px 24px; + text-decoration: none; + text-shadow: 0px 1px 0px #ffffff; + outline: 0; +} +.activebutton { + box-shadow: inset 0px 1px 0px 0px #dcecfb; + background: linear-gradient(to bottom, #bddbfa 5%, #80b5ea 100%); + background-color: #bddbfa; + border-radius: 6px; + border: 1px solid #84bbf3; + display: inline-block; + cursor: pointer; + color: #ffffff; + font-family: Arial; + font-size: 15px; + font-weight: bold; + padding: 6px 24px; + text-decoration: none; + text-shadow: 0px 1px 0px #528ecc; + outline: 0; +} \ No newline at end of file diff --git a/aio/content/examples/router-tutorial/src/app/app.component.html b/aio/content/examples/router-tutorial/src/app/app.component.html new file mode 100644 index 0000000000..ecf364870b --- /dev/null +++ b/aio/content/examples/router-tutorial/src/app/app.component.html @@ -0,0 +1,31 @@ + + +

Angular Router Sample

+ + + + + + + + +
+ + + + + + + + + + + + +
\ No newline at end of file diff --git a/aio/content/examples/router-tutorial/src/app/app.component.ts b/aio/content/examples/router-tutorial/src/app/app.component.ts new file mode 100644 index 0000000000..db6605650c --- /dev/null +++ b/aio/content/examples/router-tutorial/src/app/app.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] +}) +export class AppComponent { + title = 'angular-router-sample'; +} diff --git a/aio/content/examples/router-tutorial/src/app/app.module.ts b/aio/content/examples/router-tutorial/src/app/app.module.ts new file mode 100644 index 0000000000..519b7f173d --- /dev/null +++ b/aio/content/examples/router-tutorial/src/app/app.module.ts @@ -0,0 +1,38 @@ +// #docplaster +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +// #docregion router-import +import { RouterModule } from '@angular/router'; +// #enddocregion router-import +import { AppComponent } from './app.component'; +import { CrisisListComponent } from './crisis-list/crisis-list.component'; +import { HeroesListComponent } from './heroes-list/heroes-list.component'; +import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; + +@NgModule({ + declarations: [ + AppComponent, + CrisisListComponent, + HeroesListComponent, + PageNotFoundComponent + ], + // #docplaster + // #docregion import-basic, import-redirect, import-wildcard + imports: [ + BrowserModule, + RouterModule.forRoot([ + {path: 'crisis-list', component: CrisisListComponent}, + {path: 'heroes-list', component: HeroesListComponent}, + // #enddocregion import-basic + {path: '', redirectTo: '/heroes-list', pathMatch: 'full'}, + // #enddocregion import-redirect + {path: '**', component: PageNotFoundComponent} + // #enddocregion import-wildcard + // #docregion import-basic, import-redirect, import-wildcard + ]), + ], + // #enddocregion import-basic, import-redirect, import-wildcard + providers: [], + bootstrap: [AppComponent] +}) +export class AppModule { } diff --git a/aio/content/examples/router-tutorial/src/app/crisis-list/crisis-list.component.css b/aio/content/examples/router-tutorial/src/app/crisis-list/crisis-list.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/router-tutorial/src/app/crisis-list/crisis-list.component.html b/aio/content/examples/router-tutorial/src/app/crisis-list/crisis-list.component.html new file mode 100644 index 0000000000..76ebcaf88f --- /dev/null +++ b/aio/content/examples/router-tutorial/src/app/crisis-list/crisis-list.component.html @@ -0,0 +1,2 @@ +

CRISIS CENTER

+

Get your crisis here

diff --git a/aio/content/examples/router-tutorial/src/app/crisis-list/crisis-list.component.ts b/aio/content/examples/router-tutorial/src/app/crisis-list/crisis-list.component.ts new file mode 100644 index 0000000000..24ccdace70 --- /dev/null +++ b/aio/content/examples/router-tutorial/src/app/crisis-list/crisis-list.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-crisis-list', + templateUrl: './crisis-list.component.html', + styleUrls: ['./crisis-list.component.css'] +}) +export class CrisisListComponent { + +} diff --git a/aio/content/examples/router-tutorial/src/app/heroes-list/heroes-list.component.css b/aio/content/examples/router-tutorial/src/app/heroes-list/heroes-list.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/router-tutorial/src/app/heroes-list/heroes-list.component.html b/aio/content/examples/router-tutorial/src/app/heroes-list/heroes-list.component.html new file mode 100644 index 0000000000..563d7e4478 --- /dev/null +++ b/aio/content/examples/router-tutorial/src/app/heroes-list/heroes-list.component.html @@ -0,0 +1,2 @@ +

HEROES

+

Get your heroes here

diff --git a/aio/content/examples/router-tutorial/src/app/heroes-list/heroes-list.component.ts b/aio/content/examples/router-tutorial/src/app/heroes-list/heroes-list.component.ts new file mode 100644 index 0000000000..d526b189bf --- /dev/null +++ b/aio/content/examples/router-tutorial/src/app/heroes-list/heroes-list.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-heroes-list', + templateUrl: './heroes-list.component.html', + styleUrls: ['./heroes-list.component.css'] +}) +export class HeroesListComponent { + +} diff --git a/aio/content/examples/router-tutorial/src/app/page-not-found/page-not-found.component.css b/aio/content/examples/router-tutorial/src/app/page-not-found/page-not-found.component.css new file mode 100644 index 0000000000..e69de29bb2 diff --git a/aio/content/examples/router-tutorial/src/app/page-not-found/page-not-found.component.html b/aio/content/examples/router-tutorial/src/app/page-not-found/page-not-found.component.html new file mode 100644 index 0000000000..cd89fb7689 --- /dev/null +++ b/aio/content/examples/router-tutorial/src/app/page-not-found/page-not-found.component.html @@ -0,0 +1,2 @@ +

Page Not Found

+

We couldn't find that page! Not even with x-ray vision.

diff --git a/aio/content/examples/router-tutorial/src/app/page-not-found/page-not-found.component.ts b/aio/content/examples/router-tutorial/src/app/page-not-found/page-not-found.component.ts new file mode 100644 index 0000000000..3f5686d096 --- /dev/null +++ b/aio/content/examples/router-tutorial/src/app/page-not-found/page-not-found.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-page-not-found', + templateUrl: './page-not-found.component.html', + styleUrls: ['./page-not-found.component.css'] +}) +export class PageNotFoundComponent { + +} diff --git a/aio/content/examples/router-tutorial/src/index.html b/aio/content/examples/router-tutorial/src/index.html new file mode 100644 index 0000000000..2f1d7fa27c --- /dev/null +++ b/aio/content/examples/router-tutorial/src/index.html @@ -0,0 +1,13 @@ + + + + + Angular Router Sample + + + + + + + + diff --git a/aio/content/examples/router-tutorial/src/main.ts b/aio/content/examples/router-tutorial/src/main.ts new file mode 100644 index 0000000000..c7b673cf44 --- /dev/null +++ b/aio/content/examples/router-tutorial/src/main.ts @@ -0,0 +1,12 @@ +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic().bootstrapModule(AppModule) + .catch(err => console.error(err)); diff --git a/aio/content/examples/router-tutorial/stackblitz.json b/aio/content/examples/router-tutorial/stackblitz.json new file mode 100644 index 0000000000..7e88374d3f --- /dev/null +++ b/aio/content/examples/router-tutorial/stackblitz.json @@ -0,0 +1,9 @@ +{ + "description": "Router", + "files":[ + "!**/*.d.ts", + "!**/*.js", + "!**/*.[0-9].*" + ], + "tags": ["router-tutorial"] +} diff --git a/aio/content/guide/router-tutorial.md b/aio/content/guide/router-tutorial.md new file mode 100644 index 0000000000..94f837c4e9 --- /dev/null +++ b/aio/content/guide/router-tutorial.md @@ -0,0 +1,262 @@ +# Using Angular routes in a single-page application + +This tutorial describes how you can build a single-page application, SPA that uses multiple Angular routes. + + +In an SPA, all of your application's functions exist in a single HTML page. +As users access your application's features, the browser needs to render only the parts that matter to the user, instead of loading a new page. This pattern can significantly improve your application's user exprience. + +To define how users navigate through your application, you use routes. You can add routes to define how users navigate from one part of your application to another. +You can also configure routes to guard against unexpected or unauthorized behavior. + +To explore a sample app featuring the contents of this tutorial, see the . + +## Objectives + +* Organize a sample application's features into modules. +* Define how to navigate to a component. +* Pass information to a component using a parameter. +* Structure routes by nesting several routes. +* Check whether users can access a route. +* Control whether the application can discard unsaved changes. +* Improve performance by pre-fetching route data and lazy loading feature modules. +* Require specific criteria to load components. + +## Prerequisites + +To complete this tutorial, you should have a basic understanding of the following concepts: + +* JavaScript +* HTML +* CSS +* [Angular CLI](/cli) + +You might find the [Tour of Heroes tutorial](/tutorial) helpful, but it is not required. + +## Create a sample application + +Using the Angular CLI, create a new application, _angular-router-sample_. This application will have two components: _crisis-list_ and _heroes-list_. + +1. Create a new Angular project, _angular-router-sample_. + + + ng new angular-router-sample + + + When prompted with `Would you like to add Angular routing?`, select `N`. + + When prompted with `Which stylesheet format would you like to use?`, select `CSS`. + + After a few moments, a new project, `angular-router-sample`, is ready. + +1. From your terminal, navigate to the `angular-router-sample` directory. + +1. Create a component, _crisis-list_. + + + ng generate component crisis-list + + +1. In your code editor, locate the file, `crisis-list.component.html` and replace + the placeholder content with the following HTML. + + + +1. Create a second component, _heroes-list_. + + + ng generate component heroes-list + + +1. In your code editor, locate the file, `heroes-list.component.html` and replace the placeholder content with the following HTML. + + + +1. In your code editor, open the file, `app.component.html` and replace its contents with the following HTML. + + + +1. Verify that your new application runs as expected by running the `ng serve` command. + + + ng serve + + +1. Open a browser to `http://localhost:4200`. + + You should see a single web page, consisting of a title and the HTML of your two components. + +## Import `RouterModule` from `@angular/router` + +Routing allows you to display specific views of your application depending on the URL path. +To add this functionality to your sample application, you need to update the `app.module.ts` file to use the module, `RouterModule`. +You import this module from `@angular/router`. + +1. From your code editor, open the `app.module.ts` file. + +1. Add the following `import` statement. + + + +## Define your routes + +In this section, you'll define two routes: + +* The route `/crisis-center` opens the `crisis-center` component. +* The route `/heroes-list` opens the `heroes-list` component. + +A route definition is a JavaScript object. Each route typically has two propteries. The first property, `path`, is a string +that specifies the URL path for the route. The second property, `component`, is a string that specifies +what component your application should display for that path. + +1. From your code editor, open the `app.module.ts` file. + +1. Locate the `@NgModule()` section. + +1. Replace the `imports` array in that section with the following. + + + +This code adds the `RouterModule` to the `imports` array. Next, the code uses the `forRoot()` method of the `RouterModule` to +define your two routes. This method takes an array of JavaScript objects, with each object defining the proprties of a route. +The `forRoot()` method ensures that your application only instantiates one `RouterModule`. For more information, see +[Singleton Services](/guide/singleton-services#forroot-and-the-router). + +## Update your component with `router-outlet` + +At this point, you have defined two routes for your application. However, your application +still has both the `crisis-list` and `heroes-list` components hard-coded in your `app.component.html` template. For your routes to +work, you need to update your template to dynamically load a component based on the URL path. + +To implement this functionality, you add the `router-outlet` directive to your template file. + +1. From your code editor, open the `app.component.html` file. + +1. Delete the following lines. + + + +1. Add the `router-outlet` directive. + + + +View your updated application in your browser. You should see only the application title. To +view the `crisis-list` component, add `crisis-list` to the end of the path in your browser's +address bar. For example: + + +http://localhost:4200/crisis-list + + +Notice that the `crisis-list` component displays. Angular is using the route you defined to dynamically load the +component. You can load the `heroes-list` component the same way: + + +http://localhost:4200/heroes-list + + +## Control navigation with UI elements + +Currently, your application supports two routes. However, the only way to use those routes +is for the user to manually type the path in the browser's address bar. In this section, you'll +add two links that users can click to navigate between the `heroes-list` and `crisis-list` +components. You'll also add some CSS styles. While these styles are not required, they make +it easier to identify the link for the currently-displayed component. You'll add that functionality +in the next section. + +1. Open the `app.component.html` file and add the following HTML below the title. + + + + This HTML uses an Angular directive, `routerLink`. This directive connects the routes + you defined to your template files. + +1. Open the `app.component.css` file and add the following styles. + + + + +If you view your application in the browser, you should see these two links. When you click +on a link, the corresponding component appears. + +## Identify the active route + +While users can navigate your application using the links you added in the previous section, +they don't have an easy way to identify what the active route is. You can add this functionality +using Angular's `routerLinkActive` directive. + +1. From your code editor, open the `app.component.html` file. + +1. Update the anchor tags to include the `routerLinkActive` directive. + + + +View your application again. As you click one of the buttons, the style for that button updates +automatically, identifying the active component to the user. By adding the `routerLinkActive` +directive, you inform your application to apply a specific CSS class to the active route. In this +tutorial, that CSS class is `activebutton`, but you could use any class that you want. + +## Adding a redirect + +In this step of the tutorial, you add a route that redirects the user to display the `/heroes-list` component. + +1. From your code editor, open the `app.module.ts` file. + +1. In the `imports` array, update the `RouterModule` section as follows. + + + + Notice that this new route uses an empty string as its path. In addition, it replaces the `component` property with two new ones: + + * `redirectTo`. This property instructs Angular to redirect from an empty path to the + `heroes-list` path. + * `pathMatch`. This property instructs Angular on how much of the URL to match. For this + tutorial, you should set this property to `full`. This strategy is recommended when + you have an empty string for a path. For more information about this property, + see the [Route API documentation](/api/router/Route). + +Now when you open your application, it displays the `heroes-list` component by default. + +## Adding a 404 page + +It is possible for a user to try to access a route that you have not defined. To account for +this behavior, a best practice is to display a 404 page. In this section, you'll create a 404 page and +update your route configuration to show that page for any unspecified routes. + +1. From the terminal, create a new component, `PageNotFound`. + + + ng generate component page-not-found + + +1. From your code editor, open the `page-not-found.component.html` file and replace its contents + with the following HTML. + + + +1. Open the `app.module.ts` file. In the `imports` array, update the `RouterModule` section as follows. + + + + The new route uses a path, `**`. This path is how Angular identifies a wildcard route. Any route + that does not match an existing route in your configuration will use this route. + +
+ Notice that the wildcard route is placed at the end of the array. The order of your + routes is important, as Angular applies routes in order and uses the first match it finds. +
+ +Try navigating to a non-existing route on your application, such as `http://localhost:4200/powers`. +This route doesn't match anything defined in your `app.module.ts` file. However, because you +defined a wildcard route, the application automatically displays your `PageNotFound` component. + +## Next steps + +At this point, you have a basic application that uses Angular's routing feature to change +what components the user can see based on the URL address. You have extended these features +to include a redirect, as well as a wildcard route to display a custom 404 page. + +For more information about routing, see the following topics: + +* [In-app Routing and Navigation](/guide/router) +* [Router API](/api/router) diff --git a/aio/content/navigation.json b/aio/content/navigation.json index 7164537ef8..7dcce9328e 100644 --- a/aio/content/navigation.json +++ b/aio/content/navigation.json @@ -746,6 +746,17 @@ } ] }, + { + "title": "Tutorials", + "tooltop": "End-to-end tutorials for learning Angular concepts and patterns.", + "children": [ + { + "url": "guide/router-tutorial", + "title": "Using Angular Routes in a Single-page Application", + "tooltip": "A tutorial that covers many patterns associated with Angular routing." + } + ] + }, { "title": "Release Information", "tooltip": "Angular release practices, updating, and upgrading.",