diff --git a/10_lazy_loading.md b/10_lazy_loading.md new file mode 100644 index 0000000..aa38117 --- /dev/null +++ b/10_lazy_loading.md @@ -0,0 +1,337 @@ +# Chapter 8: Lazy Loading + +Angular is built with the focus on mobile. That's why we put a lot of effort into making compiled and bundled Angular 2 applications small. One of the techniques we use extensively is dead code elimination, which helped drop the size of a hello world application to only 20K. This is half the size of an analogous Angular 1 application--an impressive result! + +At some point, however, our application will be big enough, that even with this technique, the application file will be too large to be loaded at once. That's where lazy loading comes into play. + +Lazy loading speeds up our application load time by splitting it into multiple bundles, and loading them on demand. We designed the router to make lazy loading simple and easy. + +## Example + +We are going to continue using the mail app example, but this time we will add a new section, contacts, to our application. At launch, our application displays messages. Click the contacts button and it shows the contacts. + +Let's start by sketching out our application. + + main.ts: + +```javascript +import {Component, NgModule} from '@angular/core'; +import {RouterModule} from '@angular/router'; +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; + +@Component({...}) class MailAppCmp {} +@Component({...}) class ConversationsCmp {} +@Component({...}) class ConversationCmp {} + +@Component({...}) class ContactsCmp {} +@Component({...}) class ContactCmp {} + +const ROUTES = [ + { + path: 'contacts', + children: [ + { path: '', component: ContactsCmp }, + { path: ':id', component: ContactCmp } + ] + }, + { + path: ':folder', + children: [ + { path: '', component: ConversationsCmp }, + { path: ':id', component: ConversationCmp, children: [...]} + ] + } +]; + + +@NgModule({ + //... + bootstrap: [MailAppCmp], + imports: [RouterModule.forRoot(ROUTES)] +}) +class MailModule {} + +platformBrowserDynamic().bootstrapModule(MailModule); +``` + +The button showing the contacts UI can look like this: + +```html + +``` + +In addition, we can also support linking to individual contacts, as follows: + +```html +Show Contact +``` + +In the code sample above all the routes and components are defined together, in the same file. This is done for the simplicity sake. How the components are arranged does not really matter, as long as after the compilation we will have a single bundle file `'main.bundle.js'`, which will contain the whole application. + +{width=40%} +![](images/10_lazy_loading/lazy_loading_1.png) + +### Just One Problem + +There is one problem with this setup: even though `ContactsCmp` and `ContactCmp` are not displayed on load, they are still bundled up with the main part of the application. As a result, the initial bundle is larger than it could have been. + +Two extra components may not seem like a big deal, but in a real application the contacts module can include dozens or even hundreds of components, together with all the services and helper functions they need. + +A better setup would be to extract the contacts-related code into a separate module and load it on-demand. Let's see how we can do that. + +## Lazy Loading + +We start with extracting all the contacts-related components and routes into a separate file. + + contacts.ts: + +```javascript +import {NgModule, Component} from '@angular/core'; +import {RouterModule} from '@angular/router'; + +@Component({...}) class ContactsComponent {} +@Component({...}) class ContactComponent {} + +const ROUTES = [ + { path: '', component: ContactsComponent }, + { path: ':id', component: ContactComponent } +]; + +@NgModule({ + imports: [RouterModule.forChild(ROUTES)] +}) +class ContactsModule {} +``` + +In Angular an ng module is part of an application that can be bundled and loaded independently. So we have defined one in the code above. + +### Referring to Lazily-Loaded Module + +Now, after extracting the contacts module, we need to update the main module to refer to the newly extracted one. + +```javascript +const ROUTES = [ + { + path: 'contacts', + loadChildren: 'contacts.bundle.js', + }, + { + path: ':folder', + children: [ + { + path: '', + component: ConversationsCmp + }, + { + path: ':id', + component: ConversationCmp, + children: [...] + } + ] + } +]; + +@NgModule({ + //... + bootstrap: [MailAppCmp], + imports: [RouterModule.forRoot(ROUTES)] +}) +class MailModule {} + +platformBrowserDynamic().bootstrapModule(MailModule); +``` + +The `loadChildren` properly tells the router to fetch the `'contacts.bundle.js'` when and only when the user navigates to 'contacts', then merge the two router configurations, and, finally, activate the needed components. + +By doing so we split the single bundle into two. + +{width=80%} +![](images/10_lazy_loading/lazy_loading_2.png) + +The bootstrap loads just the main bundle. The router won't load the contacts bundle until it is needed. + +```html + + +Show Contact +``` + +Note that apart from the router configuration we don't have to change anything in the application after splitting it into multiple bundles: existing links and navigations are unchanged. + +## Deep Linking + +But it gets better! The router also supports deep linking into lazily-loaded modules. + +To see what I mean imagine that the contacts module lazy loads another one. + + contacts.ts: + +```javascript +import {Component, NgModule} from '@angular/core'; +import {RouterModule} from '@angular/router'; + +@Component({...}) class AllContactsComponent {} +@Component({...}) class ContactComponent {} + +const ROUTES = [ + { path: '', component: ContactsComponent }, + { path: ':id', component: ContactComponent, loadChildren: 'details.bundle.js' } +]; + +@NgModule({ + imports: [RouterModule.forChild(ROUTES)] +}) +class ContactsModule {} + +details.ts: + +@Component({...}) class BriefComponent {} +@Component({...}) class DetailComponent {} + +const ROUTES = [ + { path: '', component: BriefDetailComponent }, + { path: 'detail', component: DetailComponent }, +]; + +@NgModule({ + imports: [RouterModule.forChild(ROUTES)] +}) +class DetailModule {} +``` + +![](images/10_lazy_loading/lazy_loading_3.png) + +Imagine we have the following link in the main section or our application. + +```html + + Show Contact Detail + +``` + +When clicking on the link, the router will first fetch the contacts module, then the details module. After that it will merge all the configurations and instantiate the needed components. Once again, from the link's perspective it makes no difference how we bundle our application. It just works. + +## Sync Link Generation + +The `RouterLink` directive does more than handle clicks. It also sets the `` tag's href attribute, so the user can right-click and "Open link in a new tab". + +For instance, the directive above will set the anchor's href attribute to `'/contacts/13/detail;full=true'`. And it will do it synchronously, without loading the configurations from the contacts or details bundles. Only when the user actually clicks on the link, the router will load all the needed configurations to perform the navigation. + +## Navigation is URL-Based + +Deep linking into lazily-loaded modules and synchronous link generation are possible only because the router's navigation is URL-based. Because the router does not have the notion of route names, it does not have to use any configuration to generate links. What we pass to routerLink (e.g., `['/contacts', id, 'detail', {full: true}]`) is just an array of URL segments. In other words, link generation is purely mechanical and application independent. + +This is an important design decision we have made early on because we knew that lazy loading is a key use case for using the router. + + +## Customizing Module Loader + +The built-in application module loader uses SystemJS. But we can provide our own implementation of the loader as follows: + +```javascript +@NgModule({ + //... + bootstrap: [MailAppCmp], + imports: [RouterModule.forRoot(ROUTES)], + providers: [{provide: NgModuleFactoryLoader, useClass: MyCustomLoader}] +}) +class MailModule {} + +platformBrowserDynamic().bootstrapModule(MailModule); +``` + +You can look at [`SystemJsNgModuleLoader`](https://github.com/angular/angular/blob/master/modules/@angular/core/src/linker/system_js_ng_module_factory_loader.ts) to see an example of a module loader. + +Finally, you don't have to use the loader at all. Instead, you can provide a callback the route will use to fetch the module. + +```javascript +{ + path: 'contacts', + loadChildren: () => System.import('somePath'), +} +``` + +## Preloading Modules + +Lazy loading speeds up our application load time by splitting it into multiple bundles, and loading them on demand. We designed the router to make lazy loading transparent, so you can opt in and opt out of lazy loading with ease. + +The issue with lazy loading, of course, is that when the user navigates to the lazy-loadable section of the application, the router will have to fetch the required modules from the server, which can take time. + +To fix this problem we have added support for preloading. Now the router can preload lazy-loadable modules in the background while the user is interacting with our application. + +This is how it works. + +First, we load the initial bundle, which contains only the components we have to have to bootstrap our application. So it is as fast as it can be. + +![](images/10_lazy_loading/preloading_1.png) + +Then, we bootstrap the application using this small bundle. + +![](images/10_lazy_loading/preloading_2.png) + +At this point the application is running, so the user can start interacting with it. While she is doing it, we, in the background, preload other modules. + +![](images/10_lazy_loading/preloading_3.png) + +Finally, when she clicks on a link going to a lazy-loadable module, the navigation is instant. + +![](images/10_lazy_loading/preloading_4.png) + +We got the best of both worlds: the initial load time is as small as it can ben, and subsequent navigations are instant. + +### Enabling Preloading + +To enable preloading we need to pass a preloading strategy into forRoot. + +```js +@NgModule({ + bootstrap: [MailAppCmp], + imports: [RouterModule.forRoot(ROUTES, + {preloadingStrategy: PreloadAllModules})] +}) +class MailModule {} +``` + +The latest version of the router ships with two strategies: preload nothing and preload all modules, but you can provide you own. And it is actually a lot simpler that it may seem. + +### Custom Preloading Strategy + +Say we don't want to preload all the modules. Rather, we would like to say explicitly, in the router configuration, what should be preloaded. + +```js +[ + { + path: 'moduleA', + loadChildren: './moduleA.module', + data: {preload: true} + }, + { + path: 'moduleB', + loadChildren: './moduleB.module' + } +] +``` + +We start with creating a custom preloading strategy. + +```js +export class PreloadSelectedModulesList implements PreloadingStrategy { + preload(route: Route, load: Function): Observable { + return route.data && route.data.preload ? load() : of(null); + } +} +``` + +The preload method takes two parameters: a route and the function that actually does the preloading. In it we check if the preload property is set to true. And if it is, we call the load function. + +Finally, we need to enable the strategy by listing it as a provider and passing it to RouterModule.forRoot. + +```js +@NgModule({ + bootstrap: [MailAppCmp], + providers: [CustomPreloadingStrategy], + imports: [RouterModule.forRoot(ROUTES, + {preloadingStrategy: CustomPreloadingStrategy})] +}) +class MailModule {} +``` \ No newline at end of file diff --git a/11_guards.md b/11_guards.md new file mode 100644 index 0000000..9e85c6d --- /dev/null +++ b/11_guards.md @@ -0,0 +1,242 @@ +# Chapter 9: Guards + +![](images/4_angular_router_overview/cycle_running_guards.png) + +The router uses guards to make sure that navigation is permitted, which can be useful for security, authorization, monitoring purposes. + +There are four types of guards: `canLoad`, `canActivate`, `canActivateChild`, and `canDeactivate`. In this chapter we will look at each of them in detail. + +## CanLoad + +Sometimes, for security reasons, we do not want the user to be able to even see the source code of the lazily loaded bundle if she does not have the right permissions. That's what the `canLoad` guard is for. If a `canLoad` guard returns false, the router will not load the bundle. + +Let's take this configuration from the previous chapter and set up a `canLoad` guard to restrict access to contacts: + +```javascript +const ROUTES = [ + { + path: ':folder', + children: [ + { + path: '', + component: ConversationsCmp + }, + { + path: ':id', + component: ConversationCmp, + children: [...] + } + ] + }, + { + path: 'contacts', + canLoad: [CanLoadContacts], + loadChildren: 'contacts.bundle.js' + } +]; +``` + +Where `CanLoadContacts` is defined like this: + +```javascript +@Injectable() +class CanLoadContacts implements CanLoad { + constructor(private permissions: Permissions, + private currentUser: UserToken) {} + + canLoad(route: Route): boolean { + if (route.path === "contacts") { + return this.permissions.canLoadContacts(this.currentUser); + } else { + return false; + } + } +} +``` + +Note that in the configuration above the line `"canLoad: [CanLoadContacts]"` does not tell the router to instantiate the guard. It instructs the router to fetch `CanLoadContacts` using dependency injection. This means that we have to register `CanLoadContacts` in the list of providers somewhere (e.g., when bootstrapping the application). + +```javascript +@NgModule({ + //... + providers: [CanLoadContacts], + //... +}) +class MailModule { +} +platformBrowserDynamic().bootstrapModule(MailModule); +``` + +The router will use dependency injection to get an instance of `CanLoadContacts`. After that, the route will call the `canLoad` method, which can return a promise, an observable, or a boolean. If the returned value is a promise or an observable, the router will wait for that promise or observable to complete before proceeding with the navigation. If the returned value is false, the navigation will fail. + +We could also use a function with the same signature instead of the class. + +```javascript +{ + path: 'contacts', + canLoad: [canLoad], + loadChildren: 'contacts.bundle.js' +} + +function canLoad(route: Route): boolean { + // ... +} + +@NgModule({ + //... + providers: [{provide: canLoad, useValue: canLoad}], + //... +}) +class MailModule { +} +platformBrowserDynamic().bootstrapModule(MailModule); +``` + +Finally, the router will call the `canLoad` guard during any navigation loading contacts, not just the first time, even though the bundle itself will be loaded only once. + +## CanActivate + +The `canActivate` guard is the default mechanism of adding permissions to the application. To see how we can use it, let's take the example from above and remove lazy loading. + +```javascript +const ROUTES = [ + { + path: ':folder', + children: [ + { + path: '', + component: ConversationsCmp + }, + { + path: ':id', + component: ConversationCmp, + children: [...] + } + ] + }, + { + path: 'contacts', + canActivate: [CanActivateContacts], + children: [ + { path: '', component: ContactsCmp }, + { path: ':id', component: ContactCmp } + ] + } +]; +``` + +Where `CanActivateContacts` is defined like this: + +```javascript +@Injectable() +class CanActivateContacts implements CanActivate { + constructor(private permissions: Permissions, + private currentUser: UserToken) {} + + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): + boolean { + if (route.routerConfig.path === "contacts") { + return this.permissions.canActivate(this.currentUser); + } else { + return false; + } + } +} +``` + +As with canLoad, we need to add `CanActivateContacts` to a list of providers, as follows: + +```javascript +@NgModule({ + //... + providers: [CanActivateContacts], + //... +}) +class MailModule { +} +platformBrowserDynamic().bootstrapModule(MailModule); +``` + +Note, the signatures of the `canLoad` and `canActivate` guards are different. Since `canLoad` is called *during* the construction of the router state, and `canActivate` is called after, the `canActivate` guard gets more information. It gets its activated route and the whole router state, whereas the `canLoad` guard only gets the route. + +As with `canLoad`, the router will call `canActivate` any time an activation happens, which includes any parameters' changes. + +## CanActivateChild + +The `canActivateChild` guard is similar to `canActivate`, except that it is called when a child of the route is activated, and not the route itself. + +Imagine a function that takes a URL and decides if the current user should be able to navigate to that URL, i.e., we would like to check that any navigation is permitted. This is how we can accomplish this by using `canActivateChild`. + +```javascript +{ + path: '', + canActivateChild: [AllowUrl], + children: [ + { + path: ':folder', + children: [ + { path: '', component: ConversationsCmp }, + { path: ':id', component: ConversationCmp, children: [...]} + ] + }, + { + path: 'contacts', + children: [ + { path: '', component: ContactsCmp }, + { path: ':id', component: ContactCmp } + ] + } + ] +} +``` + +Where `AllowUrl` is defined like this: + +```javascript +@Injectable() +class AllowUrl implements CanActivateChild { + constructor(private permissions: Permissions, + private currentUser: UserToken) {} + + canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): + boolean { + return this.permissions.allowUrl(this.currentUser, state.url); + } +} +``` + +Since we placed the guard at the very root of the router configuration, it will be called during any navigation. + +## CanDeactivate + +The `canDeactivate` guard is different from the rest. Its main purpose is not to check permissions, but to ask for confirmation. To illustrate this let's change the application to ask for confirmation when the user closes the compose dialog with unsaved changes. + +```javascript +[ + { + path: 'compose', + component: ComposeCmp, + canDeactivate: [SaveChangesGuard] + outlet: 'popup' + } +] +``` + +Where `SaveChangesGuard` is defined as follows: + +```javascript +class SaveChangesGuard implements CanDeactivate { + constructor(private dialogs: Dialogs) {} + + canDeactivate(component: ComposeCmp, route: ActivatedRouteSnapshot, + state: RouterStateSnapshot): Promise { + if (component.unsavedChanges) { + return this.dialogs.unsavedChangesConfirmationDialog(); + } else { + return Promise.resolve(true); + } + } +} +``` + +`SaveChangesGuard` asks the user to confirm the navigation because all the unsaved changes would be lost. If she confirms, the `unsavedChangesConfirmationDialog` will return false, and the navigation will be canceled. \ No newline at end of file diff --git a/12_events.md b/12_events.md new file mode 100644 index 0000000..956e10d --- /dev/null +++ b/12_events.md @@ -0,0 +1,130 @@ +# Chapter 10: Events + +The router provides an observable of navigation events. Any time the user navigates somewhere, or an error is thrown, a new event is emitted. This can be useful for setting up monitoring, troubleshooting issues, implementing error handling, etc. + +## Enable Tracing + +The very first thing we can do during development to start troubleshoot router-related issues is to enable tracing, which will print out every single event in the console. + +```javascript +@NgModule({ + import: [RouterModule.forRoot(routes, {enableTracing: true})] +}) +class MailModule { +} +platformBrowserDynamic().bootstrapModule(MailModule); +``` + +## Listening to Events + +To listen to events, inject the router service and subscribe to the events observable. + +```javascript +class MailAppCmp { + constructor(r: Router) { + r.events.subscribe(e => { + console.log("event", e); + }); + } +} +``` + +For instance, let's say we want to update the title any time the user succesfully navigates. An easy way to do that would be to listen to all `NavigationEnd` events: + +```javascript +class MailAppCmp { + constructor(r: Router, titleService: TitleService) { + r.events.filter(e => e instanceof NavigationEnd).subscribe(e => { + titleService.updateTitleForUrl(e.url); + }); + } +} +``` + +## Grouping by Navigation ID + +The router assigns a unique id to every navigation, which we can use to correlate events. + +Let start with defining a few helpers used for identifying the start and the end of a particular navigation. + +```javascript +function isStart(e: Event): boolean { + return e instanceof NavigationStart; +} + +function isEnd(e: Event): boolean { + return e instanceof NavigationEnd || + e instanceof NavigationCancel || + e instanceof NavigationError; +} +``` + + Next, let's define a combinator that will take an observable of all the events related to a navigation and reduce them into an array. + +```javascript +function collectAllEventsForNavigation(obs: Observable): + Observable { + let observer: Observer; + const events = []; + const sub = obs.subscribe(e => { + events.push(e); + if (isEnd(e)) { + observer.next(events); + observer.complete(); + } + }); + return new Observable(o => observer = o); +} +``` + +Now equipped with these helpers, we can implement the desired functionality. + +```javascript +class MailAppCmp { + constructor(r: Router) { + r.events. + + // Groups all events by id and returns Observable>. + groupBy(e => e.id). + + // Reduces events and returns Observable>. + // The inner observable has only one element. + map(collectAllEventsForNavigation). + + // Returns Observable. + mergeAll(). + + subscribe((es:Event[]) => { + console.log("navigation events", es); + }); + } +} +``` + +## Showing Spinner + +In the last example let's use the events observable to show the spinner during navigation. + +```javascript +class MailAppCmp { + constructor(r: Router, spinner: SpinnerService) { + r.events. + // Fitlers only starts and ends. + filter(e => isStart(e) || isEnd(e)). + + // Returns Observable. + map(e => isStart(e)). + + // Skips duplicates, so two 'true' values are never emitted in a row. + distinctUntilChanged(). + + subscribe(showSpinner => { + if (showSpinner) { + spinner.show(); + } else { + spinner.hide(); + } + }); + } +} +``` \ No newline at end of file diff --git a/13_testing.md b/13_testing.md new file mode 100644 index 0000000..402da17 --- /dev/null +++ b/13_testing.md @@ -0,0 +1,283 @@ +# Chapter 11: Testing Router + +Everything in Angular is testable, and the router isn't an exception. In this chapter we will look at three ways to test routable components: isolated tests, shallow tests, and integration tests. + +## Isolated Tests + +It is often useful to test complex components without rendering them. To see how it can be done, let's write a test for the following component: + +```javascript +@Component({moduleId: module.id, templateUrl: 'compose.html'}) +class ComposeCmp { + form = new FormGroup({ + title: new FormControl('', Validators.required), + body: new FormControl('') + }); + + constructor(private route: ActivatedRoute, + private currentTime: CurrentTime, + private actions: Actions) {} + + onSubmit() { + const routerStateRoot = this.route.snapshot.root; + const conversationRoute = routerStateRoot.firstChild; + const conversationId = +conversationRoute.params['id']; + + const payload = Object.assign({}, + this.form.value, + {createdAt: this.currentTime()}); + + this.actions.next({ + type: 'reply', + conversationId: conversationId, + payload: payload + }); + } +} +``` + +compose.html + +```html +
+
+ Title: + + required + +
+
+ Body: +
+ +
+``` + +There a few things in this example worth noting: + +1. We are using reactive forms in the template of this component. This require us to manually create a form object in the component class, which has a nice consequence: we can test input handling without rendering the template. + +2. Instead of modifying any state directly, `ComposeCmp` emits an action, which is processed elsewhere. Thus the isolated test will have to only check that the action has been emitted. + +3. `this.route.snapshot.root` returns the root of the router state, and `routerStateRoot.firstChild` gives us the conversation route to read the id parameter from. + +Now, let's look at the test. + +```javascript +describe('ComposeCmp', () => { + let actions: BehaviorSubject; + let time: CurrentTime; + + beforeEach(() => { + // this subject acts as a "spy" + actions = new BehaviorSubject(null); + + // dummy implementation of CurrentTime + time = () => '2016-08-19 9:10AM'; + }); + + it('emits a reply action on submit', () => { + // a fake activated route + const route = { + snapshot: { + root: { + firstChild: { params: { id: 11 } } + } + } + }; + const c = new ComposeCmp(route, time, actions); + + // performing an action + c.form.setValue({ + title: 'Categorical Imperative vs Utilitarianism', + body: 'What is more practical in day-to-day life?' + }); + c.onSubmit(); + + // reading the emitted value from the subject + // to make sure it matches our expectations + expect(actions.value.conversationId).toEqual(11); + expect(actions.value.payload).toEqual({ + title: 'Categorical Imperative vs Utilitarianism', + body: 'What is more practical in day-to-day life?', + createdAt: '2016-08-19 9:10AM' + }); + }); +}); +``` + +As you can see, testing routable Angular components in isolation is no different from testing any other JavaScript object. + + + +## Shallow Testing +Testing component classes without rendering their templates works in certain scenarios, but not in all of them. Sometimes we can write a meaningful test only if we render a component's template. We can do that and still keep the test isolated. We just need to render the template without rendering the component's children. This is what is colloquially known as shallow testing. + +Let's see this approach in action. + +```javascript +@Component( + {moduleId: module.id, templateUrl: 'conversations.html'}) +export class ConversationsCmp { + folder: Observable; + conversations: Observable; + + constructor(route: ActivatedRoute) { + this.folder = route.params.pluck('folder'); + this.conversations = route.data.pluck('conversations'); + } +} +``` + +This constructor, although short, may look a bit funky if you are not familiar with RxJS. So let's step through it. First, we pluck `folder` out of the params object, which is equivalent to `route.params.map(p => p['folder'])`. Second, we pluck out `conversations`. + +In the template we use the async pipe to bind the two observables. The async pipe always returns the latest value emitted by the observable. + +```html +{{folder|async}} + + +

+ {{c.title}} +

+

+ {{c.user.name}} [{{c.user.email}}] +

+ +``` + +Now let's look at the test. + +```javascript +describe('ConversationsCmp', () => { + let params: BehaviorSubject; + let data: BehaviorSubject; + + beforeEach(async(() => { + params = of({ + folder: 'inbox' + }); + + data = of({ + conversations: [ + { + id: 1, + title: 'On the Genealogy of Morals by Nietzsche', + user: {name: 'Kate', email: 'katez@example.com'} + }, + { + id: 2, + title: 'Ethics by Spinoza', + user: {name: 'Corin', email: 'corin@example.com'} + } + ] + }); + + TestBed.configureTestingModule({ + declarations: [ConversationsCmp], + providers: [ + { provide: ActivatedRoute, useValue: {params, data} } + ] + }); + TestBed.compileComponents(); + })); + + it('updates the list of conversations', () => { + const f = TestBed.createComponent(ConversationsCmp); + f.detectChanges(); + + expect(f.debugElement.nativeElement).toHaveText('inbox'); + expect(f.debugElement.nativeElement).toHaveText('On the Genealogy of Morals'); + expect(f.debugElement.nativeElement).toHaveText('Ethics'); + + params.next({ + folder: 'drafts' + }); + + data.next({ + conversations: [ + { id: 3, title: 'Fear and Trembling by Kierkegaard', user: {name: 'Someone Else', email: 'someonelse@example.com'} } + ] + }); + f.detectChanges(); + + expect(f.debugElement.nativeElement).toHaveText('drafts'); + expect(f.debugElement.nativeElement).toHaveText('Fear and Trembling'); + }); +}); +``` + +First, look at how we configured our testing module. We only declared `ConversationsCmp`, nothing else. This means that all the elements in the template will be treated as simple DOM nodes, and only common directives (e.g., ngIf and ngFor) will be applied. This is exactly what we want. Second, instead of using a real activated route, we are using a fake one, which is just an object with the params and data properties. + +## Integration Testing + +Finally, we can always write an integration test that will exercise the whole application. + +```javascript +describe('integration specs', () => { + const initialData = { + conversations: [ + {id: 1, title: 'The Myth of Sisyphus'}, + {id: 2, title: 'The Nicomachean Ethics'} + ], + messages: [ + {id: 1, conversationId: 1, text: 'The Path of the Absurd Man'} + ] + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + // MailModule is an NgModule that contains all application + // components and the router configuration + + // RouterTestingModule overrides the router and location providers + // to make them test-friendly. + imports: [MailModule, RouterTestingModule], + + providers: [ + { provide: 'initialData', useValue: initialData} + ] + }); + TestBed.compileComponents(); + })); + + it('should navigate to a conversation', fakeAsync(() => { + // get the router from the testing NgModule + const router = TestBed.get(Router); + + // get the location from the testing NgModule, + // which is a SpyLocation that comes from RouterTestingModule + const location = TestBed.get(Location); + + // compile the root component of the app + const f = TestBed.createComponent(MailAppCmp); + + router.navigateByUrl("/inbox"); + advance(f); + + expect(f.debugElement.nativeElement).toHaveText('The Myth of Sisyphus'); + expect(f.debugElement.nativeElement).toHaveText('The Nicomachean Ethics'); + + // find the link + const c = f.debugElement.query(e => e.nativeElement.textContent === "The Myth of Sisyphus"); + c.nativeElement.click(); + advance(f); + + expect(location.path()).toEqual("/inbox/0"); + expect(f.nativeElement).toHaveText('The Path of the Absurd Man'); + })); +}); + +function advance(f: ComponentFixture) { + tick(); + f.detectChanges(); +} +``` + +Even though both the shallow and integration tests render components, these tests are very different in nature. In the shallow test we mocked up every single dependency of a component. In the integration one we did it only with the location service. Shallow tests are isolated, and, as a result, can be used to drive the design of our components. Integration tests are only used to check the correctness. + + +## Summary + +In this chapter we looked at three ways to test Angular components: isolated tests, shallow tests, and integration tests. Each of them have their time and place: isolated tests are a great way to test drive your components and test complex logic. Shallow tests are isolated tests on steroids, and they should be used when writing a meaningful test requires to render a component's template. Finally, integration tests verify that a group of components and services (e.g., the router) work together. diff --git a/14_config.md b/14_config.md new file mode 100644 index 0000000..392d6ff --- /dev/null +++ b/14_config.md @@ -0,0 +1,156 @@ +# Chapter 12: Configuration + +In this last chapter we will look at configuring the router. + +## Importing RouterModule + +We configure the router by importing `RouterModule`, and there are two ways to do it: `RouterModule.forRoot` and `RouterModule.forChild`. + +`RouterModule.forRoot` creates a module that contains all the router directives, the given routes, and the router service itself. And `RouterModule.forChild` creates a module that contains all the directives and the given routes, but does not include the service. + +The router library provides two ways to configure the module because it deals with a shared mutable resource--location. That is why we cannot have more than one router service active--they would clobber each other. Therefore we can use `forChild` to configure every lazy-loaded child module, and `forRoot` at the root of the application. `forChild` can be called multiple times, whereas `forRoot` can be called only once. + +```javascript +@NgModule({ + imports: [RouterModule.forRoot(ROUTES)] +}) +class MailModule {} + +@NgModule({ + imports: [RouterModule.forChild(ROUTES)] +}) +class ContactsModule {} +``` + +## Configuring Router Service + +We can configure the router service by passing the following options to `RouterModule.forRoot`: + +* `enableTracing` makes the router log all its internal events to the console. +* `useHash` enables the location strategy that uses the URL fragment instead of the history API. +* `initialNavigation` disables the initial navigation. +* `errorHandler` provides a custom error handler. + +Let's look at each of them in detail. + +### Enable Tracing + +Setting `enableTracing` to true is a great way to learn how the router works. + +```javascript +@NgModule({ + imports: [RouterModule.forRoot(ROUTES, {enableTracing: true})] +}) +class MailModule {} +``` + +With this option set, the router will log every internal event to the your console. You'll see something like this: + +``` +Router Event: NavigationStart +NavigationStart(id: 1, url: '/inbox') + +Router Event: RoutesRecognized +RoutesRecognized(id: 1, url: '/inbox', urlAfterRedirects: '/inbox', state: + Route(url:'', path:'') { + Route(url:'inbox', path:':folder') { + Route(url:'', path:'') + } + } +) + +Router Event: NavigationEnd +NavigationEnd(id: 1, url: '/inbox', urlAfterRedirects: '/inbox') + + +Router Event: NavigationStart +NavigationStart(id: 2, url: '/inbox/0') + +Router Event: RoutesRecognized +RoutesRecognized(id: 2, url: '/inbox/0', urlAfterRedirects: '/inbox/0', state: + Route(url:'', path:'') { + Route(url:'inbox', path:':folder') { + Route(url:'0', path:':id') { + Route(url:'', path:'') + } + } + } +) + +Router Event: NavigationEnd +NavigationEnd(id: 2, url: '/inbox/0', urlAfterRedirects: '/inbox/0') +``` + +You can right click on any of the events and store them as global variables. This allows you to interact with them: inspect router state snapshots, URLs, etc.. + +### Use Hash + +The router supports two location strategies out of the box: the first one uses the browser history API, and the second one uses the URL fragment or hash. To enable the hash strategy, do the following: + +```javascript +@NgModule({ + imports: [RouterModule.forRoot(ROUTES, {useHash: true})] +}) +class MailModule {} +``` + +You can also provide your own custom strategy as follows: + +```javascript +@NgModule({ + imports: [RouterModule.forRoot(ROUTES)], + providers: [{provide: LocationStrategy, useClass: MyCustomLocationStrategy}] +}) +class MailModule {} +``` + +## Disable Initial Navigation + +By default, `RouterModule.forRoot` will trigger the initial navigation: the router will read the current URL and will navigate to it. We can disable this behavior to have more control. + +```javascript +@NgModule({ + imports: [RouterModule.forRoot(ROUTES, {initialNavigation: false})], +}) +class MailModule { + constructor(router: Router) { + router.navigateByUrl("/fixedUrl"); + } +} +``` + +## Custom Error Handler + +Every navigation will either succeed, will be canceled, or will error. There are two ways to observe this. + +The `router.events` observable will emit: + +* `NavigationStart` when navigation stars. +* `NavigationEnd` when navigation succeeds. +* `NavigationCancel` when navigation is canceled. +* `NavigationError` when navigation fails. + +All of them contain the `id` property we can use to group the events associated with a particular navigation. + +If we call `router.navigate` or `router.navigateByUrl` directly, we will get a promise that: + +* will be resolved with `true` if the navigation succeeds. +* will be resolved with `false` if the navigation gets canceled. +* will be rejected if the navigation fails. + +Navigation fails when the router cannot match the URL or an exception is thrown during the navigation. Usually this indicates a bug in the application, so failing is the right strategy, but not always. We can provide a custom error handler to recover from certain errors. + +```javascript +function treatCertainErrorsAsCancelations(error) { + if (error isntanceof CancelException) { + return false; //cancelation + } else { + throw error; + } +} + +@NgModule({ + imports: [RouterModule.forRoot(ROUTES, {errorHandler: treatCertainErrorsAsCancelations})] +}) +class MailModule {} +``` diff --git a/15_last.md b/15_last.md new file mode 100644 index 0000000..c766305 --- /dev/null +++ b/15_last.md @@ -0,0 +1,11 @@ +# Fin + +This is the end of this short book on the Angular Router. We have learned a lot. We looked at what routers do in general: they are responsible for manage state transitions. Then we looked at the Angular2 router: its mental model, its API, and the design principles behind it. We also learned how to test applications using the router, and how to configure it. + +## Bug Reports + +If you find any typos, or have suggestions on how to improve the book, please, email me at avix1000@gmail.com. + +### Example App + +Throughout the book I used the same application in all the examples. You can find the source code of this application here: [MailApp](https://github.com/vsavkin/router_mailapp). diff --git a/2_example.md b/2_example.md new file mode 100644 index 0000000..830e893 --- /dev/null +++ b/2_example.md @@ -0,0 +1,5 @@ +# Example + +For all the examples in this book we will use MailApp, which is an application akin to Inbox or Gmail. At launch, the application displays a list of conversations, which we can browse through. Once we click on a conversation, we can see all its messages. We can also compose a new message, or view an existing message in a popup. + +You can find the source code of the MailApp application here: [MailApp](https://github.com/vsavkin/router_mailapp). \ No newline at end of file diff --git a/3_routing_is_all_about.md b/3_routing_is_all_about.md new file mode 100644 index 0000000..2959380 --- /dev/null +++ b/3_routing_is_all_about.md @@ -0,0 +1,81 @@ +# Chapter 1: What Do Routers Do? + +Before we jump into the specifics of the Angular router, let's talk about what routers do in general. + +As you know, an Angular application is a tree of components. Some of these components are reusable UI components (e.g., list, table), and some are application components, which represent screens or some logical parts of the application. The router cares about application components, or, to be more specific, about their arrangements. Let's call such component arrangements router states. So a router state defines what is visible on the screen. + + +I> A router state is an arrangement of application components that defines what is visible on the screen. + +### Router Configuration + +The router configuration defines all the potential router states of the application. Let's look at an example. + +```javascript +[ + { + path: ':folder', + children: [ + { + path: '', + component: ConversationsCmp + }, + { + path: ':id', + component: ConversationCmp, + children: [ + { path: 'messages', component: MessagesCmp }, + { path: 'messages/:id', component: MessageCmp } + ] + } + ] + }, + { + path: 'compose', + component: ComposeCmp, + outlet: 'popup' + }, + { + path: 'message/:id', + component: PopupMessageCmp, + outlet: 'popup' + } +] +``` + +Don't worry about understanding all the details. I will cover them in later chapters. For now, let's depict the configuration as follows: + +{width=80%} +![](images/3_routing_is_all_about/router_config.png) + +As you can see the router configuration is a tree, with every node representing a route. Some nodes have components associated with them, some do not. We also use color to designate different outlets, where an outlet is a location in the component tree where a component is placed. + +### Router State + +A router state is a subtree of the configuration tree. For instance, the example below has `ConversationsCmp` activated. We say *activated* instead of *instantiated* as a component can be instantiated only once but activated multiple times (any time its route's parameters change). + +{width=80%} +![](images/3_routing_is_all_about/activated_1.png) + +Not all subtrees of the configuration tree are valid router states. If a node has multiple children of the same color, i.e., of the same outlet name, only one of them can be active at a time. For instance, `ComposeCmp` and `PopupMessageCmp` cannot be displayed together, but `ConversationsCmp` and `PopupMessageCmp` can. Stands to reason, an outlet is nothing but a location in the DOM where a component is placed. So we cannot place more than one component into the same location at the same time. + +### Navigation + +The router's primary job is to manage navigation between states, which includes updating the component tree. + +I> Navigation is the act of transitioning from one router state to another. + +To see how it works, let's look at the following example. Say we perform a navigation from the state above to this one: + +{width=80%} +![](images/3_routing_is_all_about/activated_2.png) + +Because `ConversationsCmp` is no longer active, the router will remove it. Then, it will instantiate `ConversationCmp` with `MessagesCmp` in it, with `ComposeCmp` displayed as a popup. + +### Summary + +That's it. The router simply allows us to express all the potential states which our application can be in, and provides a mechanism for navigating from one state to another. The devil, of course, is in the implementation details, but understanding this mental model is crucial for understanding the implementation. + +### Isn't it all about the URL? + +The URL bar provides a huge advantage for web applications over native ones. It allows us to reference states, bookmark them, and share them with our friends. In a well-behaved web application, any application state transition results in a URL change, and any URL change results in a state transition. In other words, a URL is nothing but a serialized router state. The Angular router takes care of managing the URL to make sure that it is always in-sync with the router state. \ No newline at end of file diff --git a/4_angular_router_overview.md b/4_angular_router_overview.md new file mode 100644 index 0000000..d2fbee1 --- /dev/null +++ b/4_angular_router_overview.md @@ -0,0 +1,327 @@ +# Chapter 2: Overview + +Now that we have learned what routers do in general, it is time to talk about the Angular router. + +![](images/4_angular_router_overview/cycle_full.png) + +The Angular router takes a URL, then: + +1. Applies redirects +2. Recognizes router states +3. Runs guards and resolves data, +4. Activates all the needed components +5. Manages navigation + +Most of it happens behind the scenes, and, usually, we do not need to worry about it. But remember, the purpose of this book is to teach you how to configure the router to handle any crazy requirement your application might have. So let's get on it! + +### URL Format + +Since I will use a lot of URLs in the examples below, let's quickly look at the URL format. + + /inbox/33(popup:compose) + + /inbox/33;open=true/messages/44 + +As you can see, the router uses parentheses to serialize secondary segments (e.g., popup:compose), the colon syntax to specify the outlet, and the ';parameter=value' syntax (e.g., open=true) to specify route specific parameters. + +--- + +In the examples below we assume that we have given the following configuration to the router, and we are navigating to `'/inbox/33/messages/44'`. + +```javascript +[ + { path: '', pathMatch: 'full', redirectTo: '/inbox' }, + { + path: ':folder', + children: [ + { + path: '', + component: ConversationsCmp + }, + { + path: ':id', + component: ConversationCmp, + children: [ + { path: 'messages', component: MessagesCmp }, + { path: 'messages/:id', component: MessageCmp } + ] + } + ] + }, + { + path: 'compose', + component: ComposeCmp, + outlet: 'popup' + }, + { + path: 'message/:id', + component: PopupMessageCmp, + outlet: 'popup' + } +] +``` + +## Applying Redirects + +![](images/4_angular_router_overview/cycle_applying_redirects.png) + +The router gets a URL from the user, either when she clicks on a link or updates the location bar directly. The first thing that router does with this URL is it will apply any redirects. + +What is a redirect? + +I> A redirect is a substitution of a URL segment. Redirects can either be local or absolute. Local redirects replace a single segment with a different one. Absolute redirects replace the whole URL. Redirects are local unless you prefix the url with a slash. + +The provided configuration has only one redirect rule: `{ path: '', pathMatch: 'full', redirectTo: '/inbox' }`, i.e., replace `'/'` with `'/inbox'`. This redirect is absolute because the redirectTo value starts with a slash. + +Since we are navigating to `'/inbox/33/messages/44'` and not `'/'`, the router will not apply any redirects, and the URL will stay as is. + + +## Recognizing States + +![](images/4_angular_router_overview/cycle_recognizing.png) + +Next, the router will derive a router state from the URL. To understand how this phase works, we need to learn a bit about how the router matches the URL. + +The router goes through the array of routes, one by one, checking if the URL starts with a route's path. Here it will check that `'/inbox/33/messages/44'` starts with ':folder'. It does, since ‘:folder’ is what is called a ‘variable segment’. It is a parameter where normally you’d expect to find a constant string. Since it is a variable, virtually any string will match it. In our case ‘inbox’ will match it. So the router will set the folder parameter to 'inbox', then it will take the children configuration items, the rest of the URL `'33/messages/44'`, and will carry on matching. As a result, the id parameter will be set to '33', and, finally, the `'messages/:id'` route will be matched with the second id parameter set to '44'. + +If the taken path through the configuration does not "consume" the whole URL, the router backtracks to try an alternative path. If it is impossible to match the whole URL, the navigation fails. But if it works, the router state representing the future state of the application will be constructed. + +{width=80%} +![](images/4_angular_router_overview/router_state.png) + +A router state consists of activated routes. And each activated route can be associated with a component. Also, note that we always have an activated route associated with the root component of the application. + + +## Running Guards + +![](images/4_angular_router_overview/cycle_running_guards.png) + +At this stage we have a future router state. Next, the router will check that transitioning to the new state is permitted. It will do this by running guards. We will cover guards in detail in next chapters. For now, it is sufficient to say that a guard is a function that the router runs to make sure that a navigation to a certain URL is permitted. + + +## Resolving Data + +After the router has run the guards, it will resolve the data. To see how it works, let's tweak our configuration from above. + +```javascript +[ + { + path: ':folder', + children: [ + { + path: '', + component: ConversationsCmp, + resolve: { + conversations: ConversationsResolver + } + } + ] + } +] +``` + +Where `ConversationsResolver` is defined as follows: + +```javascript +@Injectable() +class ConversationsResolver implements Resolve { + constructor(private repo: ConversationsRepo, private currentUser: User) {} + + resolve(route: ActivatedRouteSnapshot, state: RouteStateSnapshot): + Promise { + return this.repo.fetchAll(route.params['folder'], this.currentUser); + } +} +``` + +Finally, we need to register `ConversationsResolver` when bootstrapping our application. + +```javascript +@NgModule({ + //... + providers: [ConversationsResolver], + bootstrap: [MailAppCmp] +}) +class MailModule { +} + +platformBrowserDynamic().bootstrapModule(MailModule); +``` + +Now when navigating to `'/inbox'`, the router will create a router state, with an activated route for the conversations component. That route will have the folder parameter set to 'inbox'. Using this parameter with the current user, we can fetch all the inbox conversations for that user. + +We can access the resolved data by injecting the activated route object into the conversations component. + +```javascript +@Component({ + template: ` + + ` +}) +class ConversationsCmp { + conversations: Observable; + constructor(route: ActivatedRoute) { + this.conversations = route.data.pluck('conversations'); + } +} +``` + +## Activating Components + +![](images/4_angular_router_overview/cycle_activation.png) + +At this point, we have a router state. The router can now activate this state by instantiating all the needed components, and placing them into appropriate router outlets. + +To understand how it works, let's take a look at how we use router outlets in a component template. + +The root component of the application has two outlets: primary and popup. + +```javascript +@Component({ + template: ` + ... + + + ... + + ` +}) +class MailAppCmp { +} +``` + +Other components, such as `ConversationCmp`, have only one. + +```javascript +@Component({ + template: ` + ... + + ... + ` +}) +class ConversationCmp { +} +``` + + +Now imagine we are navigating to `'/inbox/33/messages/44(popup:compose)'`. + +That's what the router will do. First, it will instantiate `ConversationCmp` and place it into the primary outlet of the root component. Then, it will place a new instance of `ComposeCmp` into the 'popup' outlet. Finally, it will instantiate a new instance of `MessageCmp` and place it in the primary outlet of the just created conversation component. + + +### Using Parameters + +Often components rely on parameters or resolved data. For instance, the conversation component probably need to access the conversation object. We can get the parameters and the data by injecting `ActivatedRoute`. + +```javascript +@Component({...}) +class ConversationCmp { + conversation: Observable; + id: Observable; + + constructor(r: ActivatedRoute) { + // r.data is an observable + this.conversation = r.data.map(d => d.conversation); + + // r.params is an observable + this.id = r.params.map(p => p.id); + } +} +``` + +If we navigate from `'/inbox/33/messages/44(popup:compose)'` to + +`'/inbox/34/messages/45(popup:compose)'`, the data observable will emit a new 'map' with the new object, and the conversation component will display the information about Conversation 34. + +As you can see the router exposes parameters and data as observables, which is convenient most of the time, but not always. Sometimes what we want is a snapshot of the state that we can examine at once. + +```javascript +@Component({...}) +class ConversationCmp { + conversation: Conversation; + constructor(r: ActivatedRoute) { + const s: ActivatedRouteSnapshot = r.snapshot; + this.conversation = s.data['conversation']; // Conversation + } +} +``` + + +## Navigation + +![](images/4_angular_router_overview/cycle_navigation.png) + +So at this point the router has created a router state and instantiated the components. Next, we need to be able to navigate from this router state to another one. There are two ways to accomplish this: imperatively, by calling `router.navigate`, or declaratively, by using the `RouterLink` directive. + +### Imperative Navigation + +To navigate imperatively, inject the `Router` service and call navigate. + +```javascript +@Component({...}) +class MessageCmp { + public id: string; + constructor(private route: ActivatedRoute, private router: Router) { + route.params.subscribe(_ => this.id = _.id); + } + + openPopup(e) { + this.router.navigate([{outlets: {popup: ['message', this.id]}}]).then(_ => { + // navigation is done + }); + } +} +``` + +### RouterLink + +Another way to navigate around is by using the `RouterLink` directive. + +```javascript +@Component({ + template: ` + Edit + ` +}) +class MessageCmp { + public id: string; + constructor(private route: ActivatedRoute) { + route.params.subscribe(_ => this.id = _.id); + } +} +``` + +This directive will also update the href attribute when applied to an `` link element, so it is SEO friendly and the right-click open-in-new-browser-tab behavior we expect from regular links will work. + +## Summary + +Let's look at all the operations of the Angular router one more time. + +![](images/4_angular_router_overview/cycle_full.png) + +When the browser is loading `'/inbox/33/messages/44(popup:compose)'`, the router will do the following. First, it will apply redirects. In this example, none of them will be applied, and the URL will stay as is. Then the router will use this URL to construct a new router state. + +{width=80%} +![](images/4_angular_router_overview/router_state.png) + +Next, the router will instantiate the conversation and message components. + +{width=80%} +![](images/4_angular_router_overview/component_tree.png) + +Now, let's say the message component has the following link in its template: + +```html +Edit +``` + +The router link directive will take the array and will set the href attribute to + + `'/inbox/33/messages/44(popup:message/44)'`. + +Now, the user triggers a navigation by clicking on the link. The router will take the constructed URL and start the process all over again: it will find that the conversation and message components are already in place. So no work is needed there. But it will create an instance of `PopupMessageCmp` and place it into the popup outlet. Once this is done, the router will update the location property with the new URL. + +That was intense--a lot of information! But we learned quite a few things. We learned about the core operations of the Angular router: applying redirects, state recognition, running guards and resolving data, component activation, and navigation. Finally, we looked at an e2e example showing the router in action. + +In the rest of this book we will discuss the same operations one more time in much greater depth. \ No newline at end of file diff --git a/5_url.md b/5_url.md new file mode 100644 index 0000000..11858e3 --- /dev/null +++ b/5_url.md @@ -0,0 +1,103 @@ +# Chapter 3: URLs + +When using the Angular router, a URL is just a serialized router state. Any state transition results in a URL change, and any URL change results in a state transition. Consequently, any link or navigation creates a URL. + +## Simple URL + +Let's start with this simple URL `'/inbox/33'`. + +This is how the router will encode the information about this URL. + +```javascript +const url: UrlSegment[] = [ + {path: 'inbox', params: {}}, + {path: '33', params: {}} +]; +``` + +Where UrlSegment is defined as follows: + +```javascript +interface UrlSegment { + path: string; + params: {[name:string]:string}; +} +``` + +We can use the ActivatedRoute object to get the URL segments consumed by the route. + +```javascript +class MessageCmp { + constructor(r: ActivatedRoute) { + r.url.forEach((u: UrlSegment[]) => { + //... + }); + } +} +``` + +## Params + +Let's soup it up a little by adding matrix or route-specific parameters, so the result URL looks like this: `'/inbox;a=v1/33;b1=v1;b2=v2'`. + +```javascript +[ + {path: 'inbox', params: {a: 'v1'}}, + {path: '33', params: {b1: 'v1', b2: 'v2'}} +] +``` + +Matrix parameters are scoped to a particular URL segment. Because of this, there is no risk of name collisions. + +## Query Params + +Sometimes, however, you want to share some parameters across many activated routes, and that's what query params are for. For instance, given this URL `'/inbox/33?token=23756'`, we can access 'token' in any component: + +```javascript +class ConversationCmp { + constructor(r: ActivateRoute) { + r.queryParams.forEach((p) => { + const token = p['token'] + }); + } +} +``` + +Since query parameters are not scoped, they should not be used to store route-specific information. + +The fragment (e.g., `'/inbox/33#fragment'`) is similar to query params. + +```javascript +class ConversationCmp { + constructor(r: ActivatedRoute) { + r.fragment.forEach((f:string) => { + + }); + } +} +``` + +## Secondary Segments + +Since a router state is a tree, and the URL is nothing but a serialized state, the URL is a serialized tree. In all the examples so far every segment had only one child. For instance in `'/inbox/33'` the '33' segment is a child of 'inbox', and 'inbox' is a child of the '/' root segment. We called such children 'primary'. Now look at this example: + + /inbox/33(popup:message/44) + +Here the root has two children 'inbox' and 'message'. + +{width=60%} +![](images/5_url/url_1.png) + +The router encodes multiple secondary children using a '//'. + + /inbox/33(popup:message/44//help:overview) + +{width=80%} +![](images/5_url/url_2.png) + +If some other segment, not the root, has multiple children, the router will encode it as follows: + + /inbox/33/(messages/44//side:help) + +{width=60%} +![](images/5_url/url_3.png) \ No newline at end of file diff --git a/6_matching.md b/6_matching.md new file mode 100644 index 0000000..1417ac6 --- /dev/null +++ b/6_matching.md @@ -0,0 +1,350 @@ +# Chapter 4: URL Matching + +At the core of the Angular router lies a powerful URL matching engine, which transforms URLs and converts them into router states. Understanding how this engine works is important for implementing advanced use cases. + +Once again, let's use this configuration. + +```javascript +[ + { path: '', pathMatch: 'full', redirectTo: '/inbox' }, + { + path: ':folder', + children: [ + { + path: '', + component: ConversationsCmp + }, + { + path: ':id', + component: ConversationCmp, + children: [ + { path: 'messages', component: MessagesCmp }, + { path: 'messages/:id', component: MessageCmp } + ] + } + ] + }, + { + path: 'compose', + component: ComposeCmp, + outlet: 'popup' + }, + { + path: 'message/:id', + component: PopupMessageCmp, + outlet: 'popup' + } +] +``` + + +First, note that every route is defined by two key parts: + +* How it matches the URL. +* What it does once the URL is matched. + +*Is is important that the second concern, the action, does not affect the matching.* + +And let's say we are navigating to `'/inbox/33/messages/44'`. + +This is how matching works: + +The router goes through the provided array of routes, one by one, checking if the unconsumed part of the URL starts with a route's path. + +Here it checks that `'/inbox/33/messages/44'` starts with `:folder`. It does. So the router sets the folder parameter to 'inbox', then it takes the children of the matched route, the rest of the URL, which is `'33/messages/44'`, and carries on matching. + +The router will check that `'33/messages/44'` starts with '', and it does, since we interpret every string to begin with the empty string. Unfortunately, the route does not have any children and we haven't consumed the whole URL. So the router will backtrack to try the next route `path: ':id'`. + +This one will work. The id parameter will be set to '33', and finally the `messages/:id` route will be matched, and the second id parameter will be set to '44'. + +## Backtracking + +Let's illustrate backtracking one more time. If the taken path through the configuration does not “consume” the whole url, the router backtracks to try an alternative path. + +Say we have this configuration: + +```javascript +[ + { + path: 'a', + children: [ + { + path: 'b', + component: ComponentB + } + ] + }, + { + path: ':folder', + children: [ + { + path: 'c', + component: ComponentC + } + ] + } +] +``` + +When navigating to `'/a/c'`, the router will start with the first route. The `'/a/c'` URL starts with `"path: 'a'"`, so the router will try to match `/c` with `b`. Because it is unable to do that, it will backtrack and will match `'a'` with `":folder"`, and then `'c'` with `"c"`. + +## Depth-First + +The router doesn't try to find the best match, i.e. it does not have any notion of specificity. It is satisfied with the first one that consumes the whole URL. + +```javascript +[ + { + path: ':folder', + children: [ + { + path: 'b', + component: ComponentB1 + } + ] + }, + { + path: 'a', + children: [ + { + path: 'b', + component: ComponentB2 + } + ] + } +] +``` + +When navigating to `'/a/b'`, the first route will be matched even though the second one appears to be 'specific'. + +## Wildcards + +We have seen that path expressions can contain two types of segments: + +* constant segments (e.g., `path: 'messages'`) +* variable segments (e.g., `path: ':folder'`) + +Using just these two we can handle most use cases. Sometimes, however, what we want is the "otherwise" route. The route that will match against any provided URL. That's what wildcard routes are. In the example below we have a wildcard route `{ path: '**', redirectTo: '/notfound' }` that will match any URL that we were not able to match otherwise and will activate `NotFoundCmp`. + +```javascript +[ + { + path: ':folder', + children: [ + { + path: '', + component: ConversationsCmp + }, + { + path: ':id', + component: ConversationCmp, + children: [ + { path: 'messages', component: MessagesCmp }, + { path: 'messages/:id', component: MessageCmp } + ] + } + ] + } + { path: '**', component: NotFoundCmp } +] +``` + +The wildcard route will "consume" all the URL segments, so `NotFoundCmp` can access those via the injected `ActivatedRoute`. + +## Empty-Path Routes + +If you look at our configuration once again, you will see that some routes have the path set to an empty string. What does it mean? + +```javascript +[ + { + path: ':folder', + children: [ + { + path: '', + component: ConversationsCmp + } + ] + } +] +``` + +By setting 'path' to an empty string, we create a route that instantiates a component but does not “consume” any URL segments. This means that if we navigate to `'/inbox'`, the router will do the following: + +First, it will check that `/inbox` starts with `:folder`, which it does. So it will take what is left of the URL, which is '', and the children of the route. Next, it will check that '' starts with '', which it does! So the result of all this is the following router state: + +{width=40%} +![](images/6_matching/router_state.png) + +Empty path routes can have children, and, in general, behave like normal routes. The only special thing about them is that they inherit matrix parameters of their parents. This means that this URL `/inbox;expand=true` will result in the router state where two activated routes have the expand parameter set to true. + +{width=40%} +![](images/6_matching/router_state_merged_params.png) + +## Matching Strategies + +By default the router checks if the URL starts with the path property of a route, i.e., it checks if the URL is prefixed with the path. This is an implicit default, but we can set this strategy explicitly, as follows: + +```javascript +// identical to {path: 'a', component: ComponentA} +{path: 'a', pathMatch: 'prefix', component: ComponentA} +``` + +The router supports a second matching strategy--full, which checks that the path is "equal" to what is left in the URL. This is mostly important for redirects. To see why, let's look at this example: + +```javascript +[ + { path: '', redirectTo: '/inbox' }, + { + path: ':folder', + children: [ + ... + ] + } +] +``` + +Because the default matching strategy is prefix, and any URL starts with an empty string, the router will always match the first route. Even if we navigate to `'/inbox'`, the router will apply the first redirect. Our intent, however, is to match the second route when navigating to `'/inbox'`, and redirect to `'/inbox'` when navigating to `'/'`. Now, if we change the matching strategy to 'full', the router will apply the redirect only when navigating to `'/'`. + +## Componentless Routes + +Most of the routes in the configuration have either the redirectTo or component properties set, but some have neither. For instance, look at `"path: ':folder'"` route in the configuration below. + +```javascript +{ + path: ':folder', + children: [ + { + path: '', + component: ConversationsCmp + }, + { + path: ':id', + component: ConversationCmp, + children: [ + { path: 'messages', component: MessagesCmp }, + { path: 'messages/:id', component: MessageCmp } + ] + } + ] +} +``` + +We called such routes 'componentless' routes. Their main purpose is to consume URL segments, provide some data to its children, and do it without instantiating any components. + +The parameters captured by a componentless route will be merged into their children's parameters. The data resolved by a componentless route will be merged as well. In this example, both the child routes will have the folder parameter in their parameters maps. + +This particular example could have been easily rewritten as follows: + +```javascript +[ + { + path: ':folder', + component: ConversationsCmp + }, + { + path: ':folder/:id', + component: ConversationCmp, + children: [ + { path: 'messages', component: MessagesCmp }, + { path: 'messages/:id', component: MessageCmp } + ] + } +] +``` + +We have to duplicate the `:folder` parameter, but overall it works. Sometimes, however, there is no other good option but to use a componentless route. + +### Sibling Components Using Same Data + +For instance, it is useful to share parameters between sibling components. + +In the following example we have two components--`MessageListCmp` and `MessageDetailsCmp`--that we want to put next to each other, and both of them require the message id parameter. `MessageListCmp` uses the id to highlight the selected message, and MessageDetailsCmp uses it to show the information about the message. + +One way to model that would be to create a bogus parent component, which both `MessageListCmp` and `MessageDetailsCmp` can get the id parameter from, i.e. we can model this solution with the following configuration: + +```javascript +[ + { + path: 'messages/:id', + component: MessagesParentCmp, + children: [ + { + path: '', + component: MessageListCmp + }, + { + path: '', + component: MessageDetailsCmp, + outlet: 'details' + } + ] + } +] +``` + +With this configuration in place, navigating to `'/messages/11'` will result in this component tree: + +{width=80%} +![](images/6_matching/component_tree.png) + +This solution has a problem--we need to create the bogus component, which serves no real purpose. That's where componentless routes are a good solution: + +```javascript +[ + { + path: 'messages/:id', + children: [ + { + path: '', + component: MessageListCmp + }, + { + path: '', + component: MessageDetailsCmp, + outlet: 'details' + } + ] + } +] +``` + +Now, when navigating to `'/messages/11'`, the router will create the following component tree: + +{width=80%} +![](images/6_matching/component_tree2.png) + +## Composing Componentless and Empty-path Routes + +What is really exciting about all these features is that they compose very nicely. So we can use them together to handle advanced use cases in just a few lines of code. + +Let me give you an example. We have learned that we can use empty-path routes to instantiate components without consuming any URL segments, and we can use componentless routes to consume URL segments without instantiating components. What about combining them? + +```javascript +[ + { + path: '', + canActivate: [CanActivateMessagesAndContacts], + resolve: { + token: TokenNeededForBothMessagsAndContacts + }, + + children: [ + { + path: 'messages', + component: MesssagesCmp + }, + { + path: 'contacts', + component: ContactsCmp + } + ] + } +] +``` + +Here we have defined a route that neither consumes any URL segments nor creates any components, but used merely for running guards and fetching data that will be used by both `MesssagesCmp` and `ContactsCmp`. Duplicating these in the children is not an option as both the guard and the data resolver can be expensive asynchronous operations and we want to run them only once. + +## Summary + +We've learned a lot! First, we talked about how the router does matching. It goes through the provided routes, one by one, checking if the URL starts with a route's path. Then, we learned that the router does not have any notion of specificity. It just traverses the configuration in the depth-first order, and it stops after finding the path matching the whole URL, i.e., the order of routes in the configuration matters. After that, we talked about empty-path routes that do not consume any URL segments, and about componentless routes that do not instantiate any components. We showed how we can use them to handle advanced use cases. \ No newline at end of file diff --git a/7_redirects.md b/7_redirects.md new file mode 100644 index 0000000..6e6004a --- /dev/null +++ b/7_redirects.md @@ -0,0 +1,140 @@ +# Chapter 5: Redirects + +![](images/4_angular_router_overview/cycle_applying_redirects.png) + +Using redirects we can transform the URL before the router creates a router state out of it. This is useful for normalizing URLs and large scale refactorings. + +## Local and Absolute Redirects + +Redirects can be local and absolute. Local redirects replace a single URL segment with a different one. Absolute redirects replace the whole URL. + +If the 'redirectTo' value starts with a '/', then it is an absolute redirect. The next example shows the difference between relative and absolute redirects. + +```javascript +[ + { + path: ':folder/:id', + component: ConversationCmp, + children: [ + { + path: 'contacts/:name', + redirectTo: '/contacts/:name' + }, + { + path: 'legacy/messages/:id', + redirectTo: 'messages/:id' + }, + { + path: 'messages/:id', + component: MessageCmp + } + ] + }, + { + path: 'contacts/:name', + component: ContactCmp + } +] +``` + +When navigating to `'/inbox/33/legacy/messages/44'`, the router will apply the second redirect and will change the URL to `'/inbox/33/messages/44'`. In other words, the part of the URL corresponding to the matched segment will be replaced. But navigating to `'/inbox/33/contacts/jim'` will replace the whole URL with `'/contacts/jim'`. + +Note that a redirectTo value can contain variable segments captured by the path expression (e.g., ':name', ':id'). All the matrix parameters of the corresponding segments matched by the path expression will be preserved as well. + +## One Redirect at a Time + +You can set up redirects at different levels of your router configuration. Let's modify the example from above to illustrate this. + +```javascript +[ + { + path: 'legacy/:folder/:id', + redirectTo: ':folder/:id' + }, + { + path: ':folder/:id', + component: ConversationCmp, + children: [ + { + path: 'legacy/messages/:id', + redirectTo: 'messages/:id' + }, + { + path: 'messages/:id', + component: MessageCmp + } + ] + } +] +``` + +When navigating to `'/legacy/inbox/33/legacy/messages/44'`, the router will first apply the outer redirect, transforming the URL to `'/inbox/33/legacy/messages/44'`. After that the router will start processing the children of the second route and will apply the inner redirect, resulting in this URL: `'/inbox/33/messages/44'`. + +One constraint the router imposes is at any level of the configuration the router applies only one redirect, i.e., redirects cannot be chained. + +For instance, say we have this configuration. + +```javascript +[ + { + path: 'legacy/messages/:id', + redirectTo: 'messages/:id' + }, + { + path: 'messages/:id', + redirectTo: 'new/messages/:id' + }, + { + path: 'new/messages/:id', + component: MessageCmp + } +] +``` + +When navigating to `'legacy/messages/:id'`, the router will replace the URL with `'messages/:id'` and will stop there. It won't redirect to `'new/messages/:id'`. A similar constraint is applied to absolute redirects: once an absolute redirect is matched, the redirect phase stops. + +## Using Redirects to Normalize URLs + +We often use redirects for URL normalization. Say we want both `mail-app.vsavkin.com` and `mail-app.vsavkin.com/inbox` render the same UI. We can use a redirect to achieve that: + +```javascript +[ + { path: '', pathMatch: 'full', redirectTo: '/inbox' }, + { + path: ':folder', + children: [ + ... + ] + } +] +``` + +We can also use redirects to implement a not-found page. + +```javascript +[ + { + path: ':folder', + children: [ + { + path: '', + component: ConversationsCmp + }, + { + path: ':id', + component: ConversationCmp, + children: [ + { path: 'messages', component: MessagesCmp }, + { path: 'messages/:id', component: MessageCmp } + ] + } + { path: '**', redirectTo: '/notfound/conversation' } + ] + } + { path: 'notfound/:objectType', component: NotFoundCmp } +] +``` + +## Using Redirects to Enable Refactoring + +Another big use case for using redirects is to enable large scale refactorings. Such refactorings can take months to complete, and, consequently, we will not be able to update all the URLs in the whole application in one go. We will need to do in phases. By using redirects we can keep the old URLs working while migrating to the new implementation. \ No newline at end of file diff --git a/8_routerstate.md b/8_routerstate.md new file mode 100644 index 0000000..22163db --- /dev/null +++ b/8_routerstate.md @@ -0,0 +1,281 @@ +# Chapter 6: Router State + +![](images/4_angular_router_overview/cycle_recognizing.png) + +During a navigation, after redirects have been applied, the router creates a `RouterStateSnapshot`. + +## What is RouterStateSnapshot? + +```javascript +interface RouterStateSnapshot { + root: ActivatedRouteSnapshot; +} + +interface ActivatedRouteSnapshot { + url: UrlSegment[]; + params: {[name:string]:string}; + data: {[name:string]:any}; + + queryParams: {[name:string]:string}; + fragment: string; + + root: ActivatedRouteSnapshot; + parent: ActivatedRouteSnapshot; + firstchild: ActivatedRouteSnapshot; + children: ActivatedRouteSnapshot[]; +} +``` + +As you can see `RouterStateSnapshot` is a tree of activated route snapshots. Every node in this tree knows about the "consumed" URL segments, the extracted parameters, and the resolved data. To make it clearer, let's look at this example: + +```javascript +[ + { + path: ':folder', + children: [ + { + path: '', + component: ConversationsCmp + }, + { + path: ':id', + component: ConversationCmp, + children: [ + { + path: 'messages', + component: MessagesCmp + }, + { + path: 'messages/:id', + component: MessageCmp, + resolve: { + message: MessageResolver + } + } + ] + } + ] + } +] +``` + +When we are navigating to `'/inbox/33/messages/44'`, the router will look at the URL and will construct the following `RouterStateSnapshot`: + +{width=40%} +![](images/8_routerstate/router_state.png) + +After that the router will instantiate `ConversationCmp` with `MessageCmp` in it. + +Now imagine we are navigating to a different URL: `'/inbox/33/messages/45'`, which will result in the following snapshot: + +{width=40%} +![](images/8_routerstate/router_state2.png) + +To avoid unnecessary DOM modifications, the router will reuse the components when the parameters of the corresponding routes change. In this example, the id parameter of the message component has changed from 44 to 45. This means that we cannot just inject an `ActivatedRouteSnapshot` into `MessageCmp` because the snapshot will always have the id parameter set to 44, i.e., it will get stale. + +The router state snapshot represents the state of the application at a moment in time, hence the name 'snapshot'. But components can stay active for hours, and the data they show can change. So having only snapshots won't cut it--we need a data structure that allows us to deal with changes. + +Introducing RouterState! + +```javascript +interface RouterState { + snapshot: RouterStateSnapshot; //returns current snapshot + + root: ActivatedRoute; +} + +interface ActivatedRoute { + snapshot: ActivatedRouteSnapshot; //returns current snapshot + + url: Observable; + params: Observable<{[name:string]:string}>; + data: Observable<{[name:string]:any}>; + + queryParams: Observable<{[name:string]:string}>; + fragment: Observable; + + root: ActivatedRout; + parent: ActivatedRout; + firstchild: ActivatedRout; + children: ActivatedRout[]; +} +``` + +`RouterState` and `ActivatedRoute` are similar to their snapshot counterparts except that they expose all the values as observables, which are great for dealing with values changing over time. + +Any component instantiated by the router can inject its `ActivatedRoute`. + +```javascript +@Component({ + template: ` + Title: {{(message|async).title}} + ... + ` +}) +class MessageCmp { + message: Observable; + constructor(r: ActivatedRoute) { + this.message = r.data.map(d => d.message); + } +} +``` + +If we navigate from `'/inbox/33/messages/44'` to `'/inbox/33/messages/45'`, the data observable will emit a new set of data with the new message object, and the component will display Message 45. + +## Accessing Snapshots + +The router exposes parameters and data as observables, which is convenient most of the time, but not always. Sometimes what we want is a snapshot of the state that we can examine at once. + +```javascript +@Component({...}) +class MessageCmp { + constructor(r: ActivatedRoute) { + r.url.subscribe(() => { + r.snapshot; // any time url changes, this callback is fired + }); + } +} +``` + +## ActivatedRoute + +`ActivatedRoute` provides access to the url, params, data, queryParams, and fragment observables. We will look at each of them in detail, but first let's examine the relationships between them. + +URL changes are the source of any changes in a route. And it has to be this way as the user has the ability to modify the location directly. + +Any time the URL changes, the router derives a new set of parameters from it: the router takes the positional parameters (e.g., ':id') of the matched URL segments and the matrix parameters of the last matched URL segment and combines those. This operation is pure: the URL has to change for the parameters to change. Or in other words, the same URL will always result in the same set of parameters. + +Next, the router invokes the route's data resolvers and combines the result with the provided static data. Since data resolvers are arbitrary functions, the router cannot guarantee that you will get the same object when given the same URL. Even more, often this cannot be the case! The URL contains the id of a resource, which is fixed, and data resolvers fetch the content of that resource, which often varies over time. + +Finally, the activated route provides the queryParams and fragment observables. In opposite to other observables, that are scoped to a particular route, query parameters and fragment are shared across multiple routes. + +### URL + +Given the following: + +```javascript +@Component({...}) +class ConversationCmp { + constructor(r: ActivatedRoute) { + r.url.subscribe((s:UrlSegment[]) => { + console.log("url", s); + }); + } +} +``` + +And navigating first to `'/inbox/33/messages/44'` and then to `'/inbox/33/messages/45'`, we will see: + +``` +url [{path: 'messages', params: {}}, {path: '44', params: {}}] +url [{path: 'messages', params: {}}, {path: '45', params: {}}] +``` + +We do not often listen to URL changes as those are too low level. One use case where it can be practical is when a component is activated by a wildcard route. Since in this case the array of URL segments is not fixed, it might be useful to examine it to show different data to the user. + + +### Params + +Given the following: + +```javascript +@Component({...}) +class MessageCmp { + constructor(r: ActivatedRoute) { + r.params.subscribe((p => { + console.log("params", params); + }); + } +} +``` + +And when navigating first to `'/inbox/33/messages;a=1/44;b=1'` and then to `'/inbox/33/messages;a=2/45;b=2'`, we will see + +``` +params {id: '44', b: '1'} +params {id: '45', b: '2'} +``` + +First thing to note is that the id parameter is a string (when dealing with URLs we always work with strings). Second, the route gets only the matrix parameters of its last URL segment. That is why the 'a' parameter is not present. + +### Data + +Let's tweak the configuration from above to see how the data observable works. + +```javascript +{ + path: 'messages/:id', + component: MessageCmp, + data: { + allowReplyAll: true + }, + resolve: { + message: MessageResolver + } +} +``` + +Where `MessageResolver` is defined as follows: + +```javascript +class MessageResolver implements Resolve { + constructor(private repo: ConversationsRepo, private currentUser: User) {} + + resolve(route: ActivatedRouteSnapshot, state: RouteStateSnapshot): + Promise { + return this.repo.fetchMessage(route.params['id'], this.currentUser); + } +} +``` + +The data property is used for passing a fixed object to an activated route. It does not change throughout the lifetime of the application. The resolve property is used for dynamic data. + +Note that in the configuration above the line `"message: MessageResolver"` does not tell the router to instantiate the resolver. It instructs the router to fetch one using dependency injection. This means that you have to register `MessageResolver` in the list of providers somewhere. + +Once the router has fetched the resolver, it will call the 'resolve' method on it. The method can return a promise, an observable, or any other object. If the return value is a promise or an observable, the router will wait for that promise or observable to complete before proceeding with the activation. + +The resolver does not have to be a class implementing the `Resolve` interface. It can also be a function: + +```javascript +function resolver(route: ActivatedRouteSnapshot, state: RouteStateSnapshot): + Promise { + return repo.fetchMessage(route.params['id'], this.currentUser); +} +``` + +The router combines the resolved and static data into a single property, which you can access, as follows: + +```javascript +@Component({...}) +class MessageCmp { + constructor(r: ActivatedRoute) { + r.data.subscribe((d => { + console.log('data', d); + }); + } +} +``` + +When navigating first to `'/inbox/33/message/44'` and then to `'/inbox/33/messages/45'`, we will see + +``` +data {allowReplyAll: true, message: {id: 44, title: 'Rx Rocks', ...}} +data {allowReplyAll: true, message: {id: 45, title: 'Angular Rocks', ...}} +``` + +## Query Params and Fragment + +In opposite to other observables, that are scoped to a particular route, query parameters and fragment are shared across multiple routes. + +```javascript +@Component({...}) +class MessageCmp { + debug: Observable; + fragment: Observable; + + constructor(route: ActivatedRoute) { + this.debug = route.queryParams.map(p => p.debug); + this.fragment = route.fragment; + } +} +``` \ No newline at end of file diff --git a/9_navigation.md b/9_navigation.md new file mode 100644 index 0000000..2cb460a --- /dev/null +++ b/9_navigation.md @@ -0,0 +1,324 @@ +# Chapter 7: Links and Navigation + +![](images/4_angular_router_overview/cycle_navigation.png) + +The primary job of the router is to manage navigation between different router states. There are two ways to accomplish this: imperatively, by calling `router.navigate`, or declaratively, by using the `RouterLink` directive. + +As before, let's assume the following configuration: + +```javascript +[ + { path: '', pathMatch: 'full', redirectTo: '/inbox' }, + { + path: ':folder', + children: [ + { + path: '', + component: ConversationsCmp + }, + { + path: ':id', + component: ConversationCmp, + children: [ + { path: 'messages', component: MessagesCmp }, + { path: 'messages/:id', component: MessageCmp } + ] + } + ] + }, + { + path: 'compose', + component: ComposeCmp, + outlet: 'popup' + }, + { + path: 'message/:id', + component: PopupMessageCmp, + outlet: 'popup' + } +] +``` + +## Imperative Navigation + +To navigate imperatively, inject the Router service and call `navigate` or `navigateByUrl` on it. Why two methods and not one? + +Using `router.navigateByUrl` is similar to changing the location bar directly--we are providing the "whole" new URL. Whereas `router.navigate` creates a new URL by applying a series of passed-in commands, a patch, to the current URL. + +To see the difference clearly, imagine that the current URL is `'/inbox/11/messages/22(popup:compose)'`. + +With this URL, calling `router.navigateByUrl('/inbox/33/messages/44')` will result in +`'/inbox/33/messages/44'`, and calling `router.navigate('/inbox/33/messages/44')` will result in + `'/inbox/33/messages/44(popup:compose)'`. + +### Router.navigate + +Let's see what we can do with `router.navigate`. + + +####Passing an array or a string + +Passing a string is sugar for passing an array with a single element. + +```javascript +router.navigate('/inbox/33/messages/44') +``` + +is sugar for + +```javascript +router.navigate(['/inbox/33/messages/44']) +``` + +which itself is sugar for + +```javascript +router.navigate(['/inbox', 33, 'messages', 44]) +``` + +####Passing matrix params + +```javascript +router.navigate([ + '/inbox', 33, {details: true}, 'messages', 44, {mode: 'preview'} +]) +``` + +navigates to + +```javascript +'/inbox/33;details=true/messages/44;mode=preview(popup:compose)' +``` + +#### Updating secondary segments + +```javascript +router.navigate([{outlets: {popup: 'message/22']) +``` +navigates to + +```javascript +'/inbox/11/messages/22(popup:message/22)' +``` + +We can also update multiple segments at once, as follows: + +```javascript +router.navigate([ + {outlets: {primary: 'inbox/33/messages/44', popup: 'message/44'}} +]) +``` + +navigates to + +```javascript +'/inbox/33/messages/44(popup:message/44)' +``` + +And, of course, this works for any segment, not just the root one. + +```javascript +router.navigate([ + '/inbox/33', {outlets: {primary: 'messages/44', help: 'message/123'}} +]) +``` + +navigates to + +```javascript +'/inbox/33/(messages/44//help:messages/123)(popup:message/44)' +``` + +We can also remove segments by setting them to 'null'. + +```javascript +router.navigate([{outlets: {popup: null]) +``` + +navigates to + +```javascript +'/inbox/11/messages/22' +``` + +####Relative Navigation + +By default, the navigate method applies the provided commands starting from the root URL segment, i.e., the navigation is absolute. We can make it relative by providing the starting route, like this: + +```javascript +@Component({...}) +class MessageCmp { + constructor(private route: ActivatedRoute, private router: Router) {} + + goToConversation() { + this.router.navigate('../../', {relativeTo: this.route}); + } +} +``` + +Let's look at a few examples, given we are providing the `"path: 'message/:id'"` route. + +```javascript +router.navigate('details', {relativeTo: this.route}) +``` + +navigates to + +```javascript +'/inbox/33/messages/44/details(popup:compose)' +``` + +The '../' allows us to skip one URL segment. + +```javascript +router.navigate('../55', {relativeTo: this.route}) +``` + +navigates to + +```javascript +'/inbox/33/messages/55(popup:compose)' +``` + +```javascript +router.navigate('../../', {relativeTo: this.route}) +``` + +navigates to + +```javascript +'/inbox/33(popup:compose)' +``` + + +####Forcing Absolute Navigation + +If the first command starts with a slash, the navigation is absolute regardless if we provide a route or not. + + +####Navigation is URL-Based + +Using '../' does not skip a route, but skips a URL segment. More generally, the router configuration has no affect on URL-generation. The router only looks at the current URL and the provided commands to generate a new URL. + + +####Passing Query Params and Fragment + +By default the router resets query params during navigation. If this is the current URL: + +```javascript +'/inbox/11/messages/22?debug=true#section2' +``` + +Then calling + +```javascript +router.navigate('/inbox/33/message/44') +``` + +will navigate to + +```javascript +'/inbox/33/messages/44' +``` + +If we want to preserve the current query params and fragment, we can do the following: + +```javascript +router.navigate('/inbox/33/message/44', + {preserveQueryParams: true, preserveFragment: true}) +``` + +Or we always can provide new params and fragment, like this: + +```javascript +router.navigate('/inbox/33/message/44', + {queryParams: {debug: false}, fragment: 'section3'}) +``` + + +### RouterLink + +Another way to navigate around is by using the `RouterLink` directive. + +```javascript +@Component({ + template: ` + Open Message 44 + ` +}) +class SomeComponent {} +``` + +Behind the scenes `RouterLink` just calls `router.navigate` with the provided commands. If we do not start the first command with a slash, the navigation is relative to the route of the component. + +Everything that we can pass to `router.navigate`, we can pass to `routerLink`. For instance: + +```html + + Open Message 44 + +``` + +As with `router.navigate`, we can set or preserve query params and fragment. + +```html + + Open Message 44 + + + + Open Message 44 + +``` + +This directive will also update the href attribute when applied to an `` link element, so it is SEO friendly and the right-click open-in-new-browser-tab behavior we expect from regular links will work. + +### Active Links + +By using the `RouterLinkActive` directive, we can add a CSS class to an element when the link's route becomes active. + +```html +Inbox +``` + +When the URL is either `'/inbox'` or `'/inbox/33'`, the active-link class will be added to the `a` tag. If the url changes, the class will be removed. + +We can set more than one class, as follows: + +```html +Inbox + +Inbox +``` + + +####Exact Matching + +We can make the matching exact by passing `"{exact: true}"`. This will add the classes only when the URL matches the link exactly. For instance, in the following example the 'active-link' class will be added only when the URL is `'/inbox'`, not `'/inbox/33'`. + +```html + + Inbox + +``` + +####Adding Classes to Ancestors + +Finally, we can apply this directive to an ancestor of a `RouterLink`. + +```html +
+ Inbox + Drafts +
+``` + +This will set the 'active-link' class on the div tag if the URL is either '/inbox' or '/drafts'. + +## Summary + +That's a lot of information! So let's recap. + +First, we established that the primary job of the router is to manage navigation. And there are two ways to do it: imperatively, by calling `router.navigate`, or declaratively, by using the RouterLink directive. We learned about the difference between `router.navigateByUrl` and `router.navigate`: one takes the whole URL, and the other one takes a patch it applies to the current URL. Then we talked about absolute and relative navigation. Finally, we saw how to use `[routerLink]` and `[routerLinkActive]` to set up navigation in the template. \ No newline at end of file