Fain - Angular 2 Development with TypeScript

Fain Angular 2 Development with TypeScript Playlists Copyright history For online information and ordering of this and other Manning books,...

0 downloads 1 Views 22MB Size

Recommend Documents

Palmer test ing Angular Applications Jesse Palmer Corinna Cohn Mike Giambalvo Craig Nishina Foreword by Brad Green, Google MANNING Anatomy of a Basic Component Unit test import { async, ComponentFixture, test Bed } from '@angular/core/t

Surge a hea d With Bla ckberry Development Surge a hea d With Bla ckberry Development the re has been a huge ba ttle between the iPhone, a ndroid and the BlackBerry. (...)

Web development company 2 [Ebizz Infotech] Best Website Development Company in India By [5th Floor Gand hi Palace, Timaliyawad, Nanpura] [SURAT, GUJARAT] WEBSITE DEVELOPMENT COMPANY Website is critical hotspot for giving the data on

All rights are reserved by the Publisher, whether the whole or part of the material is concerned, specifically the rights of translation, reprinting, reuse of illustrations, recitation, broadcasting, reproduction on microfilms or in any other physi

Min and max attributes are used to set the max imun and minimum value. (...)

This is essentially the way that normal HTML elements behave, which makes Angular easy to understand and learn.  The figure below shows the structure of an example application used in chapter 5: (...)

Name of Unit Respondents / Capacity Administration and HR (AH)  2/3 Customer Service and IT (CI)  3/4 Finance (FI)  8 /8 Land Contracts Legal (LCL)  2/4 Rationale For Measures Used Embedded within the OPA are twenty me

Angular Material Side Nav | Side Menu Example md-sidenav Directive and $mdSidenav service is used to create Nav, let us first create simple Side Nav to understand how it works Source :- http://tutorialsplane.com/angular-material-side-nav/

Angular Material Tooltip Angular Material Tooltip Angular Material Tooltip: (...)

Angular Material Nav bar Example Let us first create a simple example of nav bar – If you run the above example it will produce output something like this – Thank you (...)



You’ll be using multiple custom HTML elements that represent your components: , and . You’ll add them the same way as  in index.html. The most interesting part in this file is how you can display the list of products. Each product will be represented by the same HTML fragment on the web page. Because there are multiple products, you need to render the same HTML multiple times. The NgFor directive is used inside a component’s template to loop through the list of items in the data collection and render HTML markup for each item. You can use the shorthand syntax *ngFor to represent the NgFor directive.

  


Because *ngFor is in 
, each loop iteration will render a 
 with the content of the corresponding  inside. To pass an instance of a product to ProductComponent, you use the square brackets for property binding: [product]="prod", where [product] refers to the property­named product inside the component represented by , and prod is a local template variable declared on the fly in the *ngFor directive as let prod. We’ll discuss property bindings in detail in chapter 5. The col­sm­4 col­lg­4 col­md­4 styles come from Twitter’s Bootstrap library, where the window’s width is divided into 12 invisible columns. In this example, you want to allocate 4 columns (one third of the 
’s width) if a device has small (sm means 768 pixels or more), large (lg is for 1200 px or more), and medium (md is for 992 px or more) screen sizes. Because this doesn’t specify any columns for extra­small devices (xs is for screens under 768 px), the entire width of a 
 will be allocated for a single 
product>. To see how the page layout changes for different screen sizes, narrow your browser’s window to make it less than 768 pixels wide. You can read more about the Bootstrap grid system in the Bootstrap documentation at http://getbootstrap.com/css/#grid.

Note

ApplicationComponent relies on the existence of other components (such as ProductItemComponent) that you’ll create in the subsequent steps. If you try to run the auction now, you’ll see errors in the Developer Console of your browser.

The product item component

In the product­item directory, create a product­item.ts file that declares a Product­ ItemComponent representing a single product item from the auction. The source code of product­item.ts is structured much like the code of application.ts: the import statements go on top, and then comes the component class declaration annotated with @Component. Listing 2.20. product-item.ts

import {Component, Input} from '@angular/core'; import StarsComponent from 'app/components/stars/stars'; import {Product} from 'app/services/product­service'; @Component({   selector: 'auction­product­item',   templateUrl: 'app/components/product­item/product­item.html' }) export default class ProductItemComponent {   @Input() product: Product; }

The component’s product property is annotated with @Input(). This means the value for this property will be exposed to the parent component, which can bind a value to it. We’ll discuss input properties in detail in chapter 6. Create a product­item.html file to contain the following template of the product component (which will be represented by its price, title, and description). Listing 2.21. product-item.html

          {{ product.price }}     

{{ product.title }}

    

{{ product.description }}

  
  
       


Here you use another type of data binding: an expression within double curly braces. Angular evaluates the value of the expression within the braces, turns the result into a string, and replaces this expression in the template with the resulting string. Internally this process is implemented using string interpolation. Note the  tag that represents the StarsComponent and was declared in AppModule. You bind the value of product.rating to the rating property of the StarsComponent. For this to work, rating must be declared as an input property in the StarsComponent that you’ll create next. The stars component

The stars component will display the rating of a product. In figure 2.5, you can see that it displays an average rating number of 4.3 as well as star icons representing the rating. Figure 2.5. Stars component

Angular provides component lifecycle hooks (see chapter 6) that allow you to define the callback methods that will be invoked at certain moments of the component’s lifecycle. In this component, you’ll use the ngOnInit() callback, which will be invoked as soon as an instance of the component is created and its properties are initialized. Create the stars.ts file in the stars directory with the following content. Listing 2.22. stars.ts

The count property specifies the total number of stars to be rendered. If this property isn’t initialized by the parent, the component renders five stars by default. The rating property stores the average rating that determines how many stars should be filled with the color and how many should remain empty. In the stars array, the elements with the false value represent empty stars, and those with true represent stars filled with color. You initialize the stars array in the ngOnInit() lifecycle callback, which will be used in the template to render stars. ngOnInit() is called only once, right after the component’s data­bound properties have been checked for the first time, and before any of its children have been checked. When ngOnInit() is invoked, all properties passed from the parent view are already initialized, so you can use the rating value to compute the values in the stars array. Alternatively, you could turn stars into a getter to compute it on the fly, but the getter would be invoked each time Angular synchronizes the model with the view. Exactly the same array would be computed multiple times. Create the template of the StarsComponent in the stars.html file, as shown next. Listing 2.23. stars.html

        {{ rating }} stars



You already used the NgFor directive and curly braces data­binding expression in ApplicationComponent. Here you bind a CSS class name to an expression: [class.glyphicon­star­empty]="star". If an expression within the double quotes on the right evaluates to true, the glyphicon­star­empty CSS class will be added to the class attribute of the  element. Copying the rest of the code

To finish this project, copy the missing components from the chapter2/auction directory to the corresponding directories of your project: The services directory contains the product­service.ts file that declares two classes: Product and ProductService. This is where the data for the auction comes from. We’ll provide more details about the content of this file in the hands­on section of chapter 3. The navbar directory contains the code for the top navigation bar. The footer directory contains the code for the footer of the page. The search directory contains the initial code for the SearchComponent, which is a form that you’ll develop in chapter 7. The carousel directory contains the code that implements a Bootstrap slider in the top portion of the home page.

2.5.3. Launching the online auction application To launch the auction application, open a command window and start live­server in your project directory. You can do it by running the npm start command, which is configured in the package.json file to start the live­server. It’ll open the browser, and you should be able to see the home page as shown in figure 2.4. The product details page isn’t implemented yet, so the product title links won’t work. We recommend that you use the Chrome browser for development, because it has the best tools for debugging your code. Keep the Developer Tools panel open while running all code samples. If you see unexpected results, check the Console tab for error messages. Also, there’s a great Chrome browser extension called Augury, which is a convenient debugging tool for Angular apps. After installing this extension, you’ll see an additional Augury tab in the Chrome development tools panel (see figure 2.6), which allows you to see and modify the values of your app components at runtime. Figure 2.6. The Augury panel

Figure 2.6. The Augury panel

2.6. SUMMARY In this chapter, you’ve had your first experience writing an Angular application. We briefly covered the main principles and most important building blocks of an Angular application. In future chapters, we’ll discuss them in detail. You’ve also created an initial version of the online auction application. This has shown you how to set up a development environment and structure an Angular project. An Angular application is represented by a hierarchy of components that are packaged into modules. Each Angular component contains a template for the UI rendering and an annotated class implementing this component’s functionality. Templates and styles can be either inlined or stored in separate files.

The SystemJS module loader allows you to split the application into ES6 modules and dynamically assembles everything together at runtime. Configuration parameters for SystemJS can be specified in a separate configuration file. Using npm for managing dependencies is the simplest way of configuring a new Angular project.

laylists

Chapter 3. Navigation with the Angular router

History

This chapter covers opics

Configuring routes utorials

Passing data while navigating from one route to another

Offers &Having more than one area for navigation (a.k.a. outlet) on the same page using Deals

auxiliary routes Highlights

Lazy­loading modules with the router

ettings In chapter 2, you built the home page of the online auction with the intent to create a

single­page application (SPA): the main page won’t be reloaded, but its parts may Support change. You now want to add navigation to this application so it’ll change the content

area of the page (we’ll define that a bit later) based on the user’s actions. Imagine that

Sign Out

the user needs to be able to see product details, bid on products, and chat with sellers. The Angular router allows you to configure and implement such navigation without performing a page reload. Not only do you want to be able to change the view inside the page, but you may also want to bookmark its URL so you can get to specific product details faster. To do this, you need to assign a unique URL for each view. In general, you can think of a router as an object responsible for the view state of the application. Every application has one router, and you need to configure the router to make it work. We’ll first cover the main features of the router, and then you’ll add a second view (Product Details) to the online auction so that if the user clicks a particular product on the home page, the page’s content will change to display the details of the selected product.

3.1. ROUTING BASICS You can think of a SPA as a collection of states, such as Home state, Product Details

state, and Shipping state. Each state represents a different view of the same SPA. So far, the online auction has only one view state: the home page. The online auction (see figure 2.4) has a navigation bar (a component) on top, a search form (another component) on the left, and a footer (yet another component) at the bottom, and you want these components to remain visible all the time. The rest of the page consists of a content area that displays the  and several  components. You’ll reuse this content area (the outlet) for displaying different views based on the user’s actions. To do this, you’ll need to configure the router so it can display different views in the outlet, replacing one view with another. This content area is represented by the tag . Figure 3.1 shows the area you’ll use for displaying different views. Figure 3.1. Allocating the area for changing views

Note

There can be more than one outlet on the page. We’ll cover that in section 3.5.

You’ll be assigning a component for each view that you want to display in this area. In chapter 2, you didn’t create a parent component that would encapsulate the carousel and auction products, but by the end of this chapter you’ll refactor the code to create a HomeComponent to serve as a parent for the carousel and products. You’ll also create ProductDetailComponent to represent each product’s details. At any given time, the user will see either HomeComponent or ProductDetailComponent in the  area.

The router is responsible for managing client­side navigation, and in section 3.1.2 we’ll provide a high­level overview of what the router is made up of. In the non­SPA world, site navigation is implemented as a series of requests to a server, which refreshes the entire page by sending the appropriate HTML documents to the browser. With SPA, the code for rendering components is already on the client (except for the lazy­loading scenarios), and you just need to replace one view with another. As the user navigates the application, it can still make requests to the server to retrieve or send data. Sometimes a view (the combination of the UI code and the data) has everything it needs already downloaded to the browser, but other times a view will communicate with the server by issuing AJAX requests or via WebSockets. Each view will have a unique URL shown in the location bar of the browser, and we’ll discuss that next.

3.1.1. Location strategies At any given time, the browser’s location bar displays the URL of the current view. A URL can contain different parts (segments). It starts with a protocol followed by a domain name, and it may include a port number. Parameters that need to be passed to the server may follow a question mark (this is true for HTTP GET requests), like this: http://mysite.com:8080/auction?someParam=123 Changing any character in the preceding URL results in a new request to the server. In SPAs, you need the ability to modify the URL without making a server­side request so the application can locate the proper view on the client. Angular offers two location strategies for implementing client­side navigation: HashLocationStrategy—A hash sign (#) is added to the URL, and the URL segment after the hash uniquely identifies the route to be used as a web page fragment. This strategy works with all browsers, including the old ones. PathLocationStrategy—This History API–based strategy is supported only in browsers that support HTML5. This is the default location strategy in Angular. Hash-based navigation

A sample URL that uses hash­based navigation is shown in figure 3.2. Changing any character to the right of the hash sign doesn’t cause a direct server­side request, but navigates to the view represented by the path (with or without parameters) after the hash. The hash sign serves as a separator between the base URL and the client­side

locations of the required content. Figure 3.2. Dissecting the URL

Try to navigate a SPA like Gmail, and watch the URL. For the Inbox, it looks like this: https://mail.google.com/mail/u/0/#inbox. Now go to the Sent folder, and the hash portion of the URL will change from inbox to sent. The client­side JavaScript code invokes the necessary functions to display the Sent view. But why does the Gmail app still shows you the “Loading...” message when you switch to the Sent box? The JavaScript code of the Sent view can still make AJAX requests to the server to get the new data, but it doesn’t load any additional code, markup, or CSS from the server. In this book, we’ll use hash­based navigation, and @NgModule will include the following providers value (providers are explained in chapter 4):

providers:[{provide: LocationStrategy, useClass: HashLocationStrategy}]

History API-based navigationx

The browser’s History API allows you to move back and forth through the user’s navigation history as well as programmatically manipulate the history stack (see “Manipulating the browser history” in the Mozilla Developer Network, http://mng.bz/i64G). In particular, the pushState() method is used to attach a segment to the base URL as the user navigates your SPA. Consider the following URL: http://mysite.com:8080/products/page/3. The products/page/3 URL segment can be pushed (attached) to the base URL programmatically without using the hash tag. If the user navigates from page 3 to  4 , the application’s code will push products/page/4, saving the previous products/page/3 state in the browser

history. Angular spares you from invoking pushState() explicitly—you just need to configure the URL segments and map them to the corresponding components. With the History API–based location strategy, you need to tell Angular what to use as a base URL in your application so it can properly append the client­side URL segments. You can do it in one of two ways: Add the  tag to the header of index.html, such as . Assign a value for the APP_BASE_HREF Angular constant in the root module, and use it as the providers value. The following code snippet uses / as a base URL, but it can be any URL segment that denotes the end of the base URL: import { APP_BASE_HREF } from '@angular/common'; ... @NgModule({ ...   providers:[{provide: APP_BASE_HREF, useValue: '/'}] }) class AppModule { }

3.1.2. The building blocks of client-side navigation Let’s get familiar with the main concepts of implementing client­side navigation using the Angular router. In the Angular framework, the implementation of routing functionality is implemented in a separate RouterModule module. If your application needs routing, make sure your package.json file includes the dependency @angular/router. Our package.json includes the following line: "@angular/router": "3.0.0". Remember, the goal of this chapter is to explain how to navigate between the different views of a SPA, so the first thing we need to focus on is how to configure the router and add it to the module declaration. Angular offers the following main players for implementing routing in your application: Router—An object that represents the router in the runtime. You can use its navigate() and navigateByUrl() methods to navigate to a route either by the configured route path or by the URL segment, respectively. RouterOutlet—A directive that serves as a placeholder within your web page () where the router should render the component.

Routes—An array of routes that map URLs to components to be rendered inside the . RouterLink—A directive for declaring a link to a route if the navigation is done using HTML anchor tags. RouterLink may contain parameters to be passed to the route’s component. ActivatedRoute—An object that represents the route or routes that are currently active. You configure routes in a separate array of objects of type Route. Here’s an example:

const routes: Routes = [     {path: '',        component: HomeComponent},     {path: 'product', component: ProductDetailComponent} ];

Because route configuration is done on the module level, you need to import routes in the @NgModule decorator. If you declare routes for the root modules, you should use the forRoot() method, like this:

import { BrowserModule } from '@angular/platform­browser'; import { RouterModule } from '@angular/router'; ... @NgModule({   imports: [ BrowserModule, RouterModule.forRoot(routes)],     ... })

If you’re configuring routes for a feature module (not for the root one), use the forChild() method:

import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; ... @NgModule({   imports: [ CommonModule, RouterModule.forChild(routes)],     ... })

Note that in feature modules you import CommonModule instead of BrowserModule. In a typical scenario, you’ll be implementing navigation by performing the following steps:

1.  Configure your app routes to map the URL segments to the corresponding components, and pass the configuration object to either RouterModule.forRoot() or RouterModule.forChild() as an argument. If some of the components expect to receive input values, you can use route parameters. 2.  Import the returned value of forRoot() or forChild() in the @NgModule decorator. 3.  Define the outlet where the router will render components by using the  tag. 4.  Add HTML anchor tags with bounded [routerLink] properties (square brackets denote property binding), so that when the user clicks the link, the router will render the corresponding component. Think of a [routerLink] as a client­side replacement for the href attribute of the HTML anchor tag. Invoking the router’s navigate() method is an alternative to using [routerLink] for navigating to a route. In either case, the router will find a match to the provided path, will create (or find) the instance of the specified component, and will update the URL accordingly. Let’s illustrate these steps in a sample application (see the router_samples folder in the code samples). Say you want to create a root component that has two links, Home and Product Details, at the top of the page. The application should render either HomeComponent or ProductDetailComponent, depending on which link the user clicks. HomeComponent will render the text “Home Component” on a red background, and ProductDetailComponent will render “Product Details Component” on cyan. Initially the web page should display HomeComponent, as shown in figure 3.3. After the user clicks the Product Details link, the router should display Product­ DetailComponent, as shown in figure 3.4. Figure 3.3. The Home route of the basic_routing sample

The main goal of this exercise is to get familiar with the router, so the components will be very simple. Here is the code of HomeComponent. Listing 3.1. HomeComponent

import {Component} from '@angular/core'; @Component({     selector: 'home',     template: 'Home Component',     styles: ['.home {background: red}']}) export class HomeComponent {}

The code of ProductDetailComponent looks similar, but instead of red it uses a cyan background. Listing 3.2. ProductDetailComponent

import {Component} from '@angular/core'; @Component({     selector: 'product',     template: 'Product Details Component',     styles: ['.product {background: cyan}']}) export class ProductDetailComponent {}

Configure the routes in a separate file called app.routing.ts. Listing 3.3. app.routing.ts

HomeComponent is mapped to a path containing an empty string, which implicitly makes it a default route. The Routes type is just a collection of the objects with properties declared in the Route interface, as shown here:

export interface Route {     path?: string;     pathMatch?: string;     component?: Type | string;     redirectTo?: string;     outlet?: string;     canActivate?: any[];     canActivateChild?: any[];     canDeactivate?: any[];     canLoad?: any[];     data?: Data;     resolve?: ResolveData;     children?: Route[];     loadChildren?: string; }

TypeScript interfaces are described in appendix B, but we’d like to remind you that the question mark after the property name means that this property is optional. You can pass to the function forRoot() or forChild() a configuration object that only has a couple of properties filled in. In the basic app, you use just two properties of Route: path and component. The next step is to create a root component that will contain the links for navigating between the Home and Product Details views. The root AppComponent will be located in the app.component.ts file. Listing 3.4. app.component.ts

Note the use of brackets in the  tags. The square brackets around routerLink denote property binding, and the brackets on the right represent an array with one element (for example, ['/']). We’ll show you examples of an array with two or more elements later in this chapter. The second anchor tag has the routerLink property bound to the component configured for the /product path. The matched components will be rendered in the area marked with , which in this app is located below the anchor tags. None of the components are aware of the router configuration, because it’s the module’s business. Let’s declare and bootstrap the root module. For simplicity, implement these two actions in the same main.ts file. Listing 3.5. main.ts

The module’s providers property is an array of registered providers (there’s just one in this example) for dependency injection, which will be covered in chapter 4. At this point, you just need to know that although the default location strategy is PathLocationStrategy, you want Angular to use the HashLocationStrategy

class for routing (note the hash sign in the URL in figure 3.4). Figure 3.4. The Product Details route of the basic_routing sample

Note

Angular removes trailing slashes from URLs. You can see how the URLs for these routes look in figures 3.3 and 3.4. Child components may have their own route configurations, as we’ll discuss later in this chapter.

Running the sample applications in this book Typically the code that comes with each chapter has several sample applications. To run a particular application, you’ll need to make a one­line change in the configuration file of SystemJS to specify the name of the main script that you want to run. To run this application using the code that comes with the book, make sure that the main script that bootstraps your root module is properly mapped in systemjs.config.js. For example, this is how to specify that the main script is located in the main­param.ts file:

packages: {   'app': {main: 'main­param', defaultExtension: 'ts'} }

The same applies to the other sample applications in this and other chapters.

The main script of this application is located in the main.ts file in the directory samples.

To run this app, make sure that the systemjs.config.js file lists main.ts in the app package, and then start the live­server from the project root directory.

SystemJS and on­the­fly transpiling TypeScript offers an elegant declarative syntax for implementing many Angular features, including routing. We use SystemJS in our code samples, and the transpiling of TypeScript into JavaScript is done on the fly when the application code is loaded into the browser. But what if the application doesn’t work as expected? If you open the Developer Tools panel in your browser, you’ll see that each file with the extension .ts has a corresponding .ts!transpiled file. This is a transpiled version of the code that may be handy if you need to see the actual JavaScript code that runs in the browser. The following figure shows the Chrome Developer Tools panel with the source code of product.ts!transpiled.

Monitoring the SystemJS­transpiled code

Note

Angular comes with a Location class that allows you to navigate to an absolute URL by invoking its go(), forward(), and back() methods, along with some others. Location should be used only if you need to interact with the URL outside of the

Angular router. You’ll see an example of using Location in chapter 9, where you’ll write scripts for unit testing.

3.1.3. Navigating to routes with navigate() In the basic_routing code example in the previous section, you arranged the navigation using routerLink in HTML anchor tags. But what if you need to arrange navigation programmatically without asking the user to click a link? Let’s modify that code sample to navigate by using the navigate() method. You’ll add a button that will also navigate to the ProductDetailComponent, but this time no HTML anchors will be used. Listing 3.6 (main­navigate.ts) will invoke the navigate() method on the Router instance that will be injected into the RootComponent via its constructor. For simplicity, you place the module and routes declaration, the bootstrap, and the AppComponent in the same file, but in real­world projects you should keep them separate as you did in the previous section. Listing 3.6. main-navigate.ts

This example uses a button to navigate to the product route, but this can be done programmatically without requiring user actions. Just invoke the navigate() method (or navigateByUrl()) from your application code when necessary. You’ll see another example of using this API in chapter 9, where we’ll explain how to unit­test the router.

Tip

Having a reference to the Router instance allows you to check if a particular route is active by calling isRouteActive().

Handling 404 errors If the user enters a nonexistent URL in your application, the router won’t be able to find a matching route and will print an error message on the browser’s console, leaving the user to wonder why no navigation is happening. Consider creating an application component that will be displayed whenever the application can’t find the matching component. For example, you could create a component named _404Component and configure it

with the wildcard path **:

[   {path: '',        component: HomeComponent},   {path: 'product', component: ProductDetailComponent},   {path: '**', component: _404Component} ])

Now whenever the router can’t match the URL to any component, it’ll render the content of _404Component instead. You can see it in action by running the application main­with­404.ts that comes with the book. Just enter a nonexistent URL in the browser, such as http://localhost:8080/#/wrong. The wildcard route configuration has to be the last element in the array of routes. The router always treats the wildcard route as a match, so any routes listed after the wildcard one won’t be considered.

3.2. PASSING DATA TO ROUTES The basic routing application showed how you can display different components in a predefined outlet on the window, but you often need not only to display a component, but also to pass some data to it. For example, if you navigate from the Home to the Product Details route, you need to pass the product ID to the component that represents the destination route, such as ProductDetailComponent. The component that represents the destination route can receive passed parameters via its constructor argument of type ActivatedRoute. Besides the passed parameters, ActivatedRoute stores the route’s URL segment, the outlet. We’ll show you how to extract route parameters from an ActivatedRoute object in this section.

3.2.1. Extracting parameters from ActivatedRoute When the user navigates to the Product Details route, you need to pass the product ID to this route to display details for the particular product. Let’s modify the code of the application in the previous section so RootComponent can pass the product ID to ProductDetailComponent. The new version of this component will be called ProductDetailComponentParam, and Angular will inject an object of type ActivatedRoute into it. The ActivatedRoute object will contain the information about the component loaded

into the outlet. Listing 3.7. ProductDetailComponentParam

The ActivatedRoute object will contain all the parameters that are being passed to the component. You just need to declare the constructor’s argument, specifying its type, and Angular will know how to instantiate and inject this object. We’ll cover dependency injection in detail in chapter 4. In listing 3.8, you’ll change the configuration of the product route and routerLink to ensure that the value of the product ID will be passed to the ProductDetailComponentParam component if the user choses to go this route. The new version of the app is called main­param.ts. Listing 3.8. main-param.ts

The routerLink property for the Product Details link is initialized with a two­element array. The elements of the array build up the path specified in the routes configuration given to the RouterModule.forRoot() method. The first element of the array represents the static part of the route’s path: product. The second element represents the variable part of the path: /:id. For simplicity, you hardcode the ID to be 1234, but if the RootComponent class had a productID variable pointing at the appropriate object, you could write { productID} instead of 1234. For the Product Details route, Angular will construct the URL segment /product/1234. Figure 3.5 shows how the Product Details view will be rendered in the browser. Note the URL: the router replaced the product/:id path with /product/1234. Figure 3.5. The Product Details route received the product ID 1234.

Let’s review the steps that Angular performed under the hood to render the main page

of the application: 1.  Check the content of each routerLink to find the corresponding route configurations. 2.  Parse the URLs, and replace the parameter names with actual values where specified. 3.  Build the  tags that the browser understands. Figure 3.6 shows a snapshot of the home page of the application with the Chrome Developer Tools panel open. Because the path property of the configured Home route had an empty string, Angular didn’t add anything to the base URL of the page. But the anchor under the Product Details link has already been converted into a regular HTML tag. When the user clicks the Product Details link, the router will attach a hash sign and add /product/1234 to the base URL so that the absolute URL of the Product Details view will become http://localhost:8080/#/product/1234. Figure 3.6. The Product Details anchor tag is ready.

3.2.2. Passing static data to a route Parent components will usually pass data to their children, but Angular also offers a mechanism to pass arbitrary data to components at the time of route configuration. For example, besides dynamic data like a product ID, you may need to pass a flag indicating whether the application is running in a production environment. This can be done by using the data property of your route configuration. The route for the product details can be configured as follows:

{path: 'product/:id', component: ProductDetailComponentParam , data:

 [{isProd: true}]}

The data property can contain an array of arbitrary key­value pairs. When the router opens ProductDetailComponentParam, the data value will be located in the data property of the ActivatedRoute.snapshot:

export class ProductDetailComponentParam {   productID: string;   isProdEnvironment: string;   constructor(route: ActivatedRoute) {     this.productID = route.snapshot.params['id'];     this.isProdEnvironment = route.snapshot.data[0]['isProd'];     console.log("this.isProdEnvironment = " + this.isProdEnvironment);   } }

Passing data to a route via the data property isn’t an alternative to configuring parameters in the path property, as in path: 'product/:id', but it can come in handy when you need to pass some data to a route during the configuration phase, such as whether it’s a production or QA environment. The application that implements this functionality is located in the main­param­data.ts file.

3.3. CHILD ROUTES An Angular application is a tree of components that have parent­child relations. Each component is well encapsulated, and you have full control over what you expose to the rest of the application’s scripts and what you keep private within the component. Any component can have its own styles that won’t mix with the parent’s styles. A component can also have its own dependency injectors. A child component can have its own routes, but all routes are configured outside of any component. In the previous section, you configured the routes to show the content of either HomeComponent or ProductDetailComponent in the router­outlet of AppComponent. Imagine now that you want to enable ProductDetailComponent (the child) to show either the product description or the seller’s info. This means that you want to add the configuration of child routes for Product­ DetailComponent. You’ll use the children property of the Route interface for this:

[ {path: '',            component: HomeComponent},   {path: 'product/:id', component: ProductDetailComponent,     children: [       {path: '', component: ProductDescriptionComponent},

      {path: 'seller/:id', component: SellerInfoComponent}     ]} ]

Figure 3.7 shows how the application will look once the user clicks the Product Details link on the root component, which renders ProductDetailComponent (the child) showing ProductDescription. This is the default route of the child, because its path property has an empty string. Figure 3.8 shows the application after the user clicks the Product Details link and then on Seller Info.

Note

If you read the electronic version of this book, you’ll see that the seller’s info is shown on a yellow background. We did this on purpose to discuss the styling of components a bit later in this chapter.

To implement the views shown in figures 3.7 and 3.8, you’ll modify ProductDetailComponent so it also has two children, SellerInfoComponent and ProductDescriptionComponent, and its own . Figure 3.9 shows the hierarchy of components that you’re going to implement. Figure 3.7. The Product Description route

Figure 3.8. The child route renders SellerInfo.

Figure 3.9. The routes hierarchy in the basic_routing app

The entire code of this example with the child routes is located in main­child.ts, shown next. Listing 3.9. main-child.ts

import {Component} from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform­browser­dynamic'; import { NgModule }      from '@angular/core'; import { BrowserModule } from '@angular/platform­browser'; import {LocationStrategy, HashLocationStrategy} from '@angular/common'; import { Routes, RouterModule } from '@angular/router'; import {HomeComponent} from "./components/home"; import {ProductDetailComponent} from './components/product­child'; import {ProductDescriptionComponent} from './components/product­description'; import {SellerInfoComponent} from './components/seller'; const routes: Routes = [     {path: '',            component: HomeComponent},     {path: 'product/:id', component: ProductDetailComponent,         children: [           {path: '',           component: ProductDescriptionComponent},           {path: 'seller/:id', component: SellerInfoComponent}         ]} ]; @Component({

    selector: 'app',     template: `         Home
        Product Details              ` }) class AppComponent {} @NgModule({     imports:      [ BrowserModule, RouterModule.forRoot(routes)],     declarations: [ AppComponent, HomeComponent, ProductDetailComponent,                     ProductDescriptionComponent, SellerInfoComponent],     providers:[{provide: LocationStrategy, useClass: HashLocationStrategy}],     bootstrap:    [ AppComponent ] }) class AppModule { } platformBrowserDynamic().bootstrapModule(AppModule);

Take another look at the URL in figure 3.8. When the user clicks the Product Details link, the product/1234 segment is added to the URL. The router finds a match to this path in the configuration object and renders ProductDetailComponent in the outlet. The new version of ProductDetailComponent (product­child.ts) has its own outlet, where it can display either ProductDescriptionComponent (the default) or Seller­InfoComponent. Listing 3.10. ProductDetailComponent

Note

Child components don’t need to be imported. Neither Product­ DescriptionComponent nor SellerInfoComponent is explicitly mentioned in the template of ProductDetailComponent, and there’s no need to list them in the directives property. They’re included in AppModule.

By looking at the route configuration for SellerInfoComponent, you’ll see that it expects to receive the seller’s ID as a parameter. You’ll be passing a hard­coded value of 5678 as the seller’s ID. When the user clicks the Seller Info link, the URL will include the product/1234/seller/5678 segment (see figure 3.8). The router will find a match in the configuration object and will display SellerInfoComponent.

Note

This version of ProductDetailComponent has only one link to open the seller’s info. To navigate from the seller route back to /product, the user can just click the web browser’s Back button.

ProductDescriptionComponent is trivial. Listing 3.11. ProductDescriptionComponent

import {Component} from '@angular/core'; @Component({     selector: 'product­description',     template: '

This is a great product!

' }) export class ProductDescriptionComponent {}

Because SellerInfoComponent expects to receive the seller’s ID, its constructor needs an argument of type ActivatedRoute to get the seller ID, as you did in Product­DetailComponent. Listing 3.12. SellerInfoComponent

Listing 3.12. SellerInfoComponent

import {Component} from '@angular/core'; import {ActivatedRoute} from '@angular/router'; @Component({     selector: 'seller',     template: 'The seller of this product is Mary Lou (98%)',     styles: [':host {background: yellow}'] }) export class SellerInfoComponent {    sellerID: string;    constructor(route: ActivatedRoute){      this.sellerID = route.snapshot.params['id'];      console.log(`The SellerInfoComponent got the seller id{this.sellerID}`);    } }

You use a pseudoclass :host to display the content of this component on a yellow background. This will serve as a good segue to a brief discussion of Shadow DOM. The :host pseudoclass selector can be used with elements that are created using Shadow DOM, which provides better encapsulation for components (see the “Shadow DOM Support in Angular” sidebar). Although not all web browsers support Shadow DOM yet, Angular emulates Shadow DOM by default and creates a shadow root. The HTML element associated with this shadow root is called the shadow host. In listing 3.12, you use :host to apply the yellow background color to SellerInfoComponent, which serves as a shadow host. Shadow DOM styles of the components aren’t merged with the styles of the global DOM, and the IDs of the component’s HTML tags won’t overlap with the IDs of the DOM.

Deep linking Deep linking is the ability to create a link to specific content inside a web page rather than to the entire page. In the basic routing applications, you’ve seen examples of deep linking: The URL http://localhost:8080/#/product/1234 links not just to the Product Details page, but to a specific view representing the product with an ID of 1234. The URL http://localhost:8080/#/product/1234/seller/5678 links even deeper. It

shows the information about the seller with an ID of 5678 who sells the product whose ID is 1234. You can easily see deep linking in action by copying the link http://localhost:8080/#/product/1234/seller/5678 from the application running in Chrome and pasting it into Firefox or Safari.

Shadow DOM support in Angular Shadow DOM is a part of the Web Components standards. Every web page is represented by a tree of DOM objects, but Shadow DOM allows you to encapsulate a subtree of HTML elements to create a boundary between one component and another. Such a subtree is rendered as part of the HTML document, but its elements aren’t attached to the main DOM tree. In other words, the Shadow DOM places a wall between the DOM content and the internals of the HTML component. When you add a custom tag to a web page, it includes an HTML fragment, and with Shadow DOM this fragment is scoped to the component without merging with the DOM of the web page. With Shadow DOM, the CSS styles of the custom component won’t be merged with the main DOM CSS, preventing possible conflicts in rendering styles. Open any YouTube video in the Chrome browser, which natively supports Shadow DOM. At the time of this writing, the video player is represented by the tag video, which you can find by opening the Developer Tools and browsing the content under the Elements tab as shown in the following figure.

The 
     
Add review

 
       


readonly mode is turned off. Note that you use two­way binding in two places: [(rating)] and [(ngModel)]. Earlier in this chapter, we discussed the use of the ngModel directive for two­way binding; but if you have an input property (such as rating) and an output property that has the same name plus the suffix Change (such as ratingChange), you’re allowed to use the [()] syntax with such properties. The Leave a Review button toggles the visibility of the preceding 
. This is how it can be implemented: Leave a Review

Replace the content of the product­detail.html file with the following. Listing 6.14. product-detail.html

         
        {{ product.price }}         

{{ product.title }}

        

{{ product.description }}

    
             {{ reviews.length }} reviews

        

    


             
    
             Leave a Review                   
        
        
Add review         




             
                                  {{ review.user }}             {{ review.timestamp | date: 'shortDate'              

{{ review.comment }}

             

After typing a review and giving stars to a product, the user clicks the Add Review button, which invokes addReview() on the component. Let’s implement it in TypeScript. 5.  Modify the product­detail.ts file. Adding a review should do two things: send the newly entered review to the server and recalculate the average product rating on the UI. You’ll recalculate the average on the UI, but you won’t implement the communication with the server; you’ll log the review on the browser’s console. Then you’ll add the new review to the array of existing reviews. The following code fragment from ProductDetailComponent implements this functionality: addReview() {   let review = new Review(0, this.product.id, new Date(), 'Anonymous',       this.newRating, this.newComment);   console.log("Adding review " + JSON.stringify(review));   this.reviews = [...this.reviews, review];   this.product.rating = this.averageRating(this.reviews);   this.resetForm(); }

averageRating(reviews: Review[]) {   let sum = reviews.reduce((average, review) => average + review.rating, 0);   return sum / reviews.length; }

After creating a new instance of the Review object, you need to add it to the reviews array. The spread operator lets you write it in an elegant way: this.reviews = [...this.reviews, review];

The reviews array gets the values of all existing elements (...this.reviews) plus the new one (review). The recalculated average is assigned to the rating property, which is propagated to the UI via binding. What’s left? Replace the content of the product­detail.ts file with the following code, and this hands­on exercise is over! Listing 6.15. product-detail.ts

import {Component} from '@angular/core'; import {ActivatedRoute} from '@angular/router'; import {Product, Review, ProductService} from

 '../../services/product­service'; import StarsComponent from '../stars/stars'; @Component({   selector: 'auction­product­page',   styles: ['auction­stars.large {font­size: 24px;}'],   templateUrl: 'app/components/product­detail/product­detail.html' }) export default class ProductDetailComponent {   product: Product;   reviews: Review[];   newComment: string;   newRating: number;   isReviewHidden: boolean = true;   constructor(route: ActivatedRoute, productService: ProductService) {     let prodId: number = parseInt(route.snapshot.params['productId']);     this.product = productService.getProductById(prodId);     this.reviews = productService.getReviewsForProduct(this.product.id);   }   addReview() {

    let review = new Review(0, this.product.id, new Date(), 'Anonymous',         this.newRating, this.newComment);     console.log("Adding review " + JSON.stringify(review));     this.reviews = [...this.reviews, review];     this.product.rating = this.averageRating(this.reviews);     this.resetForm();   }

  averageRating(reviews: Review[]) {     let sum = reviews.reduce((average, review) => average + review.rating,0);     return sum / reviews.length;   }   resetForm() {     this.newRating = 0;     this.newComment = null;     this.isReviewHidden = true;   } }

6.6. SUMMARY Any Angular application is a hierarchy of components that need to communicate with each other. This chapter was dedicated to covering different ways of arranging such communication. Binding to the component’s input properties and dispatching events via the output properties allow you to create loosely coupled components. By means of its change­detection mechanism, Angular intercepts changes in components’ properties to ensure that their bindings are updated. Each component goes through a certain set of events during its lifecycle. Angular provides several lifecycle hooks where you can write code to intercept these events and apply custom logic there. These are the main takeaways for this chapter: Parent and child components should avoid direct access to each other’s internals but should communicate via input and output properties. A component can emit custom events via its output properties, and these events can carry an application­specific payload. Communications between unrelated components can be arranged by using the Mediator design pattern. A parent component can pass one or more template fragments to a child at runtime.

Each Angular component lets you intercept major lifecycle events of a component and insert application­specific code there. The Angular change­detection mechanism automatically monitors changes to components’ properties and updates the UI accordingly.

You can mark selected branches of your app component tree to be excluded from the change­detection process.

Playlists

Chapter 7. Working with forms

History

This chapter covers Topics

Understanding the Angular Forms API (NgModel, FormControl, FormGroup, form Tutorialsdirectives, FormBuilder)

Working with template­driven forms

Offers & Deals

Working with reactive forms Highlights

Understanding form validation

Settings Angular offers rich support for handling forms. It goes beyond regular data­binding by

treating form fields as first­class citizens and providing fine­grained control over form Support data. Sign Out

This chapter will start by demonstrating how you can implement a sample user registration form in pure HTML. While working on this form, we’ll briefly discuss the standard HTML forms and their shortcomings. Then you’ll see what the Angular Forms API brings to the table, and we’ll cover the template­driven and reactive approaches to creating forms in Angular. After covering the basics, you’ll refactor the original version of the user registration form to use the template­driven approach, and we’ll discuss its pros and cons. Then we’ll do the same with the reactive approach. After that, we’ll discuss form validation. At the end of the chapter, you’ll apply this new knowledge to the online auction application and start implementing its search form component.

Template­driven vs. reactive approaches In a template­driven approach, forms are fully programmed in the component’s template. The template defines the structure of the form, the format of its fields, and the validation rules. In contrast, in a reactive approach, you create the form model programmatically in the

code (in TypeScript, in this case). The template can be either statically defined and bound to an existing form model or dynamically generated based on the model.

By the end of the chapter, you’ll be familiar with the Angular Forms API and the various ways of working with forms and applying data validation.

7.1. OVERVIEW OF HTML FORMS HTML provides basic features for displaying forms, validating entered values, and submitting the data to the server. But HTML forms may not be good enough for real­ world business applications, which need a way to programmatically process the entered data, apply custom validation rules, display user­friendly error messages, transform the format of the entered data, and choose the way data is submitted to the server. For business applications, one of the most important considerations when choosing a web framework is how well it handles forms. In this section, we’ll evaluate standard HTML form features using a sample user registration form, and we’ll define a set of requirements that a modern web application needs to fulfill users’ expectations. We’ll also look at the form features provided by Angular.

7.1.1. Standard browser features You may be wondering what you need from an application framework other than data­ binding, if HTML already allows you to validate and submit forms. To answer this question, let’s review an HTML form that uses only standard browser features. Listing 7.1. Plain HTML user registration form

  
Username:         
  
SSN:              
  
Password:         
  
Confirm password: 
  Submit

The form contains a button and four input fields: username, SSN, password, and password confirmation. Users can enter whatever values they want: no input validation is applied here. When the user clicks the Submit button, the form’s values are submitted to the server’s /register endpoint using HTTP POST, and the page is

refreshed. The default HTML form behavior isn’t a good fit for a SPA, which typically needs the following functionality: Validation rules should be applied to individual input fields. Error messages should be displayed next to the input fields that cause the problems. Dependent fields should be validated all together. This form has password and password­confirmation fields, so whenever either of them is changed, both fields should be revalidated. The application should be in control of the values submitted to the server. When the user clicks the Submit button, the application should invoke an event­handler function to pass the form values. The application can validate the values or change their format before sending the submit request. The application should decide how the data is submitted to the server, whether it’s a regular HTTP request, an AJAX request, or a WebSocket message. HTML’s validation attributes and semantic input types partially satisfy the first two requirements. HTML validation attributes

Several standard validation attributes allow you to validate individual input fields: required, pattern, maxlength, min, max, step, and so on. For example, you can request that username be a required field and its value should contain only letters and numbers:



Here you use a regular expression, [a­zA­Z0­9]+, to restrict what can be entered in this field. When the user clicks the Submit button, the form will be validated before the submit request is sent. In figure 7.1, you can see the default error message displayed in the Chrome browser when username doesn’t conform to the specified pattern. Figure 7.1. Validation error message

There are a number of problems with this message: It’s too vague and doesn’t help the user identify and fix the problem. As soon as the input field loses focus, the error message disappears. This message format likely won’t match other styles in the application. This input field prevents users from submitting invalid values, but it doesn’t let you provide a decent user experience by helping the user with friendly client­side validation. Semantic input types

HTML supports multiple types of input elements: text, number, url, email, and so on. Choosing the right type for a form field may prevent users from entering an invalid value. But although this provides a better user experience, it’s still not enough to satisfy application­specific validation needs. Let’s consider a ZIP code field (a U.S. postal code). It might be tempting to use the number input element, because a ZIP code is represented by a numeric value (at least, in the United States). To keep the values within a certain range, you could use the min and max attributes. For example, for a five­digit ZIP code, you could use following markup:



But not every five­digit number is a valid ZIP code. In a more complex example, you might also want to allow only a subset of ZIP codes from the user’s state. To support all these real­world scenarios, you need more advanced form support, and

this is what an application framework can provide. Let’s see what Angular has to offer.

7.1.2. Angular’s Forms API There are two approaches to working with forms in Angular: template­driven and reactive. These two approaches are exposed as two different APIs (sets of directives and TypeScript classes) in Angular. With the template­driven approach, the form model is defined in the component’s template using directives. Because you’re limited to the HTML syntax while defining the form model, the template­driven approach suits only simple scenarios. For complex forms, the reactive approach is a better option. With the reactive approach, you create an underlying data structure in the code (not in the template). After the model is created, you link the HTML template elements to the model using special directives prefixed with form*. Unlike template­driven forms, reactive forms can be tested without a web browser. Let’s highlight several important concepts to further clarify the difference between template­driven and reactive forms: Both types of forms have a model, which is an underlying data structure that stores the form’s data. In the template­driven approach, the model is created implicitly by Angular based on the directives you attach to the template’s elements. In the reactive approach, you create the model explicitly and then link the HTML template elements to that model. The model is not an arbitrary object. It’s an object constructed using classes defined in the @angular/forms module: FormControl, FormGroup, and Form­Array. In the template­driven approach, you don’t access these classes directly, whereas in the reactive approach you explicitly create instances of these classes. The reactive approach doesn’t spare you from writing an HTML template. The view won’t be generated for you by Angular.

Enabling Forms API support Both types of forms—template­driven and reactive—need to be explicitly enabled before you start using them. To enable template­driven forms, add FormsModule from @angular/forms to the imports list of the NgModule that uses the Forms API. For reactive forms, use ReactiveFormsModule. Here’s how to do it:

We won’t repeat this code for each example in this chapter, but all of them assume the modules are imported. All the downloadable code samples for the book import the modules in AppModule.

7.2. TEMPLATE-DRIVEN FORMS As we mentioned earlier, you can use only directives to define a model in the template­ driven approach. But what directives can you use? These directives come with FormsModule: NgModel, NgModelGroup, and NgForm. In chapter 5, we discussed how the NgModel directive can be used for two­way data binding. But in the Forms API, it plays a different role: it marks the HTML element that should become a part of the form model. Although these two roles are separate, they don’t conflict and can be safely used at the same time on a single HTML element. You’ll see examples later in this section. Let’s briefly look at these directives and then apply the template­driven approach to the sample registration form.

7.2.1. Directives overview Here we’ll briefly describe the three main directives from FormsModule: NgModel, NgModelGroup, and NgForm. We’ll show how they can be used in the template and highlight their most important features. NgForm

NgForm is the directive that represents the entire form. It’s automatically attached to every 
 element. NgForm implicitly creates an instance of the FormGroup class that represents the model and stores the form’s data (more on FormGroup later in this chapter). NgForm automatically discovers all child HTML elements marked with the

NgModel directive and adds their values to the form model. The NgForm directive has multiple selectors that you can use to attach NgForm to non­  elements:

This syntax comes in handy if you’re using a CSS framework that requires a certain structure for the HTML elements, and you can’t use the  element. If you want to exclude a particular  from being handled by Angular, use the ngNoForm attribute:



The ngNoForm attribute prevents Angular from creating an instance of NgForm and attaching it to the 
 element. NgForm has an exportAs property declared on its @Directive annotation, which allows you to use the value of this property to create a local template variable that references the instance of NgForm:

{{ f.value | json }}


First, you specify ngForm as the value of the exportAs property of NgForm; f points at the instance of NgForm attached to the 
. Then you can use the f variable to access instance members of the NgForm object. One of them is value, which represents the current value of all form fields as a JavaScript object. You can pass it through the standard json pipe to display the form’s value on the page. NgForm intercepts the standard HTML form’s submit event and prevents automatic form submission. Instead, it emits the custom ngSubmit event:



The code subscribes to the ngSubmit event using the event­binding syntax. onSubmit is an arbitrary name for the method defined in the component, and it’s invoked when

the ngSubmit event is fired. To pass all of the form’s values as an argument to this method, use the f variable to access NgForm’s value property. NgModel

In the context of the Forms API, NgModel represents a single field on the form. It implicitly creates an instance of the FormControl class that represents the model and stores the fields’ data (more on FormControl later in this chapter). You attach the FormControl object to an HTML element using the ngModel attribute. Note that the Forms API doesn’t require either a value assigned to ngModel or any kind of brackets around the attribute:

The NgForm.value property points at the JavaScript object that holds the values of all form fields. The value of the field’s name attribute becomes the property name of the corresponding property in the JavaScript object in NgForm.value. Like NgForm, the NgModel directive has an exportAs property, so you can create a variable in the template that will reference an instance of NgModel and its value property:

NgModelGroup

NgModelGroup represents a part of the form and allows you to group form fields together. Like NgForm, it implicitly creates an instance of the FormGroup class. Basically, NgModelGroup creates a nested object in the object stored in NgForm.value. All the child fields of NgModelGroup become properties on the nested object. Here’s how you can use it:

7.2.2. Enriching the HTML form Let’s refactor the sample user registration form from listing 7.1. There, it was a plain HTML form that didn’t use any Angular features. Now you’ll wrap it into an Angular component, add validation logic, and enable programmatic handling of the submit event. Let’s start by refactoring the template and then move on to the TypeScript part. First, modify the 
 element. Listing 7.2. Angular-aware form

        

You declare a local template variable f that points at the NgForm object attached to the
 element. You need this variable to access the form’s properties, such as value and valid, and to check whether the form has errors of a specific type. You also configure the event handler for the ngSubmit event emitted by NgForm. You don’t want to listen to the standard submit event, and NgForm intercepts the submit event and stops its propagation. This prevents the form from being automatically submitted to the server, resulting in a page reload. Instead, NgForm emits its own ngSubmit event. The onSubmit() method is the event handler. It’s defined as the component’s instance method. In template­driven forms, onSubmit() takes one argument: the form’s value, which is a plain JavaScript object that keeps the values of all the fields on the form. Next, you change the username and ssn fields. Listing 7.3. Modified username and ssn fields

Now let’s change the password fields. Because these fields are related and represent the same value, it’s natural to combine them into a group. It will also be convenient to deal with both passwords as a single object when you implement form validation later in this chapter. Listing 7.4. Modified password fields

The Submit button is the only HTML element left in the template, but it remains the same as in the plain HTML version of the form:

Submit

Now that you’re done with the template refactoring, let’s wrap it into a component. Here’s the code of the component: Listing 7.5. HTML form component

@Component({   selector: 'app',   template: `...` }) class AppComponent {   onSubmit(formValue: any) {     console.log(formValue);   } }

We haven’t included the content of the template, to keep the listing terse, but the refactored version described earlier should be inlined here. The onSubmit() event handler takes a single argument: the form’s value. As you can see, the handler doesn’t use any Angular­specific API. Depending on the validity flag, you can decide whether to post the formValue to the server. In this example, you print it to the console. Figure 7.2 displays the sample registration form with the form directives applied to it. Each form directive is circled so you can see what the form is made up of. The complete running application that illustrates how to use form directives is located in the 01_template­driven.ts file, in the code that comes with the book. Figure 7.2. Form directives on the registration form

7.3. REACTIVE FORMS Unlike in the template­driven approach, creating a reactive form is a two­step process. First you need to create a model programmatically in the code, and then you link HTML elements to that model using directives in the template. Let’s start with the first step, creating a model.

7.3.1. Form model The form model is an underlying data structure that keeps the form’s data. It’s constructed out of special classes defined in the @angular/forms module: FormControl, FormGroup, and FormArray. FormControl

FormControl is an atomic form unit. Usually it corresponds to a single  element, but it can also represent a more complex UI component like a calendar or a slider. FormControl keeps the current value of the HTML element it corresponds to, the element’s validity status, and whether it’s been modified. Here’s how you can create a control:

FormGroup

FormGroup usually represents a part of the form and is a collection of FormControls. FormGroup aggregates the values and the statuses of each FormControl in the group. If one of the controls in a group is invalid, the entire group becomes invalid. It’s convenient for managing related fields on the form. FormGroup is also used to represent the entire form. For example, if a date range is represented by two date input fields, they can be combined into a single group to obtain the date range as a single value and display an error if either of the entered dates is invalid. Here’s how you can create a control group combining the from and to controls:

let formModel = new FormGroup({   from: new FormControl(),   to  : new FormControl() });

FormArray

FormArray is similar to FormGroup, but it has a variable length. Whereas FormGroup represents an entire form or a fixed subset of a form’s fields, FormArray usually represents a growable collection of fields. For example, you could use FormArray to allow users to enter an arbitrary number of emails. Here’s a model that would back such a form:

7.3.2. Form directives

The reactive approach uses a completely different set of directives than template­driven forms. The directives for reactive forms come with ReactiveFormsModule (see section 7.2). All reactive directives are prefixed with the form* string, so you can easily distinguish the reactive from the template­driven approach just by looking at the template. Reactive directives aren’t exportable, which means you can’t create a variable in the template that references an instance of a directive. This is done intentionally to clearly separate the two approaches. In template­driven forms, you don’t access the model classes; and in reactive forms, you can’t operate the model in the template. Table 7.1 shows how model classes correspond to form directives. The first column lists the model classes covered in the previous section. In the second column are the directives that bind a DOM element to an instance of a model class using the property­ binding syntax. As you can see, FormArray can’t be used with the property binding. The third column lists directives that link a DOM element to a model class by name. They must only be used in the formGroup directive. Table 7.1. Correspondence of model classes to form directives Model class

Form directives

 

Bind

Link

FormGroup

formGroup

formGroupName

FormControl

formControl

formControlName

FormArray



formArrayName

Let’s look at the form directives. formGroup

formGroup often binds an instance of the FormGroup class that represents the entire form model to a top­level form’s DOM element, usually a . All directives attached to the child DOM elements will be in the scope of formGroup and can link model instances by name. To use the formGroup directive, first create a FormGroup in the component:

@Component(...) class FormComponent {   formModel: FormGroup = new FormGroup({}); }

Then add the formGroup attribute to an HTML element. The value of the formGroup attribute references a component’s property that keeps an instance of the FormGroup class:



formGroupName

formGroupName can be used to link nested groups in a form. It needs to be in the scope of a parent formGroup directive to link one of its child FormGroup instances. Here’s how you’d define a form model that can be used with formGroupName. Listing 7.6. Form model to use with formGroupName

Now let’s look at the template. Listing 7.7. formGroup template

In the formGroup scope, you can use formGroupName to link child model classes by names defined in the parent FormGroup. The value you assign to the formGroupName attribute must match the name you chose for the child FormGroup in listing 7.7 (in this case, it’s dateRange).

Property­binding shorthand syntax Because the value you assign to the *Name directive is a string literal, you can use a shorthand syntax and omit the square brackets around the attribute name. The long version would look like this:

...

Note the square brackets around the attribute name and single quotes around the attribute value.

formControlName

formControlName must be used in the scope of the formGroup directive. It links one of its child FormControl instances to a DOM element. Let’s continue the example of the date­range model introduced when we explained the formGroupName directive. The component and form model remain the same. You only need to complete the template. Listing 7.8. Completed formGroup template

               

As in the formGroupName directive, you just specify the name of a FormControl you want to link to the DOM element. Again, these are the names you chose while defining the form model. formControl

formControl can be used for single­field forms, when you don’t want to create a form model with FormGroup but still want to use Forms API features like validation and the reactive behavior provided by the FormControl.valueChanges property. You saw an example in chapter 5 when we discussed observables. Here’s the essence of that example. Listing 7.9. FormControl

You could use ngModel to sync the value entered by user with the component’s property; but because you’re using the Forms API, you can use its reactive features. In the preceding example, you apply several RxJS operators to the observable returned by the valueChanges property to improve the user experience. More details on this example can be found in chapter 5. Here’s the template of the FormComponent from listing 7.9:



Because you’re working with a standalone FormControl that’s not a part of a FormGroup, you can’t use the formControlName directive to link it by name. Instead you use formControl with the property­binding syntax. formArrayName

formArrayName must be used in the scope of a formGroup directive. It links one of its child FormArray instances to a DOM element. Because form controls in FormArray don’t have names, you can link them to DOM elements only by index. Usually you render them in a loop, using the ngFor directive. Let’s look at an example that allows users to enter an arbitrary number of emails. We’ll highlight the key parts of the code here, but you can find the full runnable example in 02_growable­items­form.ts in the code distributed with the book. First you define the model. Listing 7.10. 02_growable-items-form.ts file: defining the model

In the template, email fields are rendered in the loop using the ngFor directive. Listing 7.11. 02_growable-items-form.ts file: template

The let i notation in the *ngFor loop allows you to automatically bind the value of the array’s index to the i variable available in the loop. The formControlName directive links the FormControl in FormArray to a DOM element; but instead of specifying a name, it uses the i variable that references the index of the current control. When users click the Add Email button, you push a new FormControl instance to the FormArray: this.formModel.get('emails').push(new FormControl()). Figure 7.3 shows the form with two email fields; an animated version, available at https://www.manning.com/books/angular­2­development­with­typescript, shows how it works. Every time the user clicks the Add Email button, a new FormControl instance is pushed to the emails FormArray, and through data­binding a new input field is rendered on the page. The form’s value is updated below the form in real time via data­ binding as well. Figure 7.3. Form with a growable email collection

7.3.3. Refactoring the sample form Now let’s refactor the sample registration form from listing 7.1. Originally it was a plain HTML form, and then you applied a template­driven approach. Now it’s time for a reactive version. You start reactive forms by defining a form model. Listing 7.12. Defining a form model

The formModel property keeps an instance of the FormGroup type that defines the structure of the form. You’ll use this property in the template to bind the model to the DOM element with the formGroup directive. It’s initialized programmatically in the constructor by instantiating model classes. The names you give to form controls in the parent FormGroup are used in the template to link the model to the DOM elements with the formControlName and formGroupName directives. passwordsGroup is a nested FormGroup to group the password and password­ confirmation fields. It will be convenient to manage their values as a single object when you add form validation. Because reactive form directives aren’t exportable, you can’t access them in the template and pass the form’s value directly to the onSubmit() method as an argument. Instead, you access the value using the component’s property that holds the form’s model. Now that the model is defined, you can write the HTML markup that binds to the model. Listing 7.13. HTML binding to the model

The behavior of this reactive version of the registration form is identical to the template­driven version, but the internal implementation differs. The complete application that illustrates how to create reactive forms is located in the 03_reactive.ts file, in the code that comes with the book.

7.3.4. Using FormBuilder

7.3.4. Using FormBuilder FormBuilder simplifies the creation of reactive forms. It doesn’t provide any unique features compared to the direct use of the FormControl, FormGroup, and FormArray classes, but its API is more terse and saves you from the repetitive typing of class names. Let’s refactor the component class from the previous section to use FormBuilder. The template will remain exactly the same, but you’ll change the way the formModel is constructed. Here’s what it should look like. Listing 7.14. Refactoring formModel with FormBuilder

Unlike FormGroup, FormBuilder allows you to instantiate FormControls using an array. Each item of the array has a special meaning. The first item is FormControl’s initial value. The second is a validator function. It can also accept a third argument, which is an async validator function. The rest of the array’s items are ignored. As you can see, configuring a form model with FormBuilder is less verbose and is based on the configuration objects rather than requiring explicit instantiation of the control’s classes. The complete application that illustrates how to use FormBuilder is located in the 04_form­builder.ts file in the code that comes with the book.

7.4. FORM VALIDATION One of the advantages of using the Forms API, compared to regular data binding, is that forms have validation capabilities. Validation is available for both types of forms: template­driven and reactive. You create validators as plain TypeScript functions. In the reactive approach, you use functions directly, and in the template­driven approach you wrap them into custom directives.

Let’s start by validating reactive forms and then move to template­driven ones. We’ll cover the basics and apply validation to the sample registration form.

7.4.1. Validating reactive forms Validators are just functions that conform to the following interface:

The validator function should declare a single parameter of type AbstractControl and return an object literal. There are no restrictions on the implementation of the function—it’s up to the validator’s author. AbstractControl is the superclass for FormControl, FormGroup, and FormArray, which means validators can be created for all model classes. A number of predefined validators ship with Angular: required, minLength, maxLength, and pattern. They’re defined as static methods of the Validators class declared in the @angular/forms module, and they match standard HTML5 validation attributes. Once you have a validator, you need to configure the model to use it. In the reactive approach, you provide validators as arguments to the constructors of the model classes. Here’s an example:

You can also provide a list of validators as the second argument:

let usernameControl = new FormControl('', [Validators.required,

 Validators.minLength(5)]);

To test the control’s validity, use the valid property, which returns either true or false:

If any of the validation rules fails, you can get error objects generated by the validator functions:

let errors: {[key: string]: any} = usernameControl.errors;

The error object The error returned by a validator is represented by a JavaScript object that has a property with the same name as the validator. Whether it’s an object literal or an object with a complex prototypal chain doesn’t matter for the validator. The property’s value can be of any type and may provide additional error details. For example, the standard Validators.minLength() validator returns the following error object:

{   minlength: {     requiredLength: 7,     actualLength: 5   } }

The object has a top­level property that matches the validator’s name, minlength. Its value is also an object with two fields: requiredLength and actualLength. These error details can be used to display a user­friendly error message. Not all validators provide the error details. Sometimes the top­level property just indicates that the error has occurred. In this case, the property is initialized with the value true. Here’s an example of the standard Validators.required() error object:

{   required: true }

Custom validators

Standard validators are good for validating basic data types, like strings and numbers. If you need to validate a more complex data type or application­specific logic, you may

need to create a custom validator. Because validators in Angular are just functions with a certain signature, they’re fairly easy to create. You need to declare a function that accepts an instance of one of the control types—FormControl, FormGroup, or FormArray—and returns an object that represents the validation error (see the sidebar “The error object”). Here’s an example of a custom validator that checks whether the control’s value is a valid Social Security number (SSN), which is a unique ID given to each U.S. citizen:

Custom validators are used the same way as the standard ones:

let ssnControl = new FormControl('', ssnValidator);

The complete running application that illustrates how to create custom validators is located in the 05_custom­validator.ts file in the code that comes with the book. Group validators

You may want to validate not only individual fields but also groups of fields. Angular allows you to define validator functions for FormGroups as well. Let’s create a validator that will make sure the password and password­confirmation fields on the sample registration form have the same value. Here’s one possible implementation:

The signature of the function conforms to the ValidatorFn interface : the first

parameter is of type FormGroup, which is a subclass of AbstractControl, and the return type is an object literal. Note that you use an ECMAScript feature called destructuring (see the “Destructuring” section in appendix A). You extract the value property from the instance of the FormGroup class that will be passed as an argument. This makes sense here because you never access any other FormGroup property in the validator’s code. Next you get the names of all properties in the value object and save them in two variables, first and rest. first is the name of a property that will be used as the reference value—values of all other properties must be equal to it to make validation pass. rest stores the names of all the other properties. Again, you’re using the destructuring feature to extract references to the array items (see the section “Destructuring of arrays” in appendix A). Finally, you return either null if the values in the group are valid or an object that indicates the error state otherwise. Validating the sample registration form

Now that we’ve covered the basics, let’s add validation to the sample registration form. You’ll use the ssnValidator and equalValidator implemented earlier in this section. Here’s the modified form model. Listing 7.15. Modified form model

Before printing the form’s model to the console in the onSubmit() method, you check whether the form is valid:

onSubmit() {   if (this.formModel.valid) {     console.log(this.formModel.value);   } }

In the model­driven approach, configuring validators requires changes only in the code, but you still want to make some changes in the template. You want to display validation errors when the user enters an invalid value. Here’s the modified version of the template. Listing 7.16. Modified template

Note how you access the hasError() method available on the form model when you conditionally show error messages. It takes two parameters: the name of the validation error you want to check, and the path to the field you’re interested in in the form model. In the case of username, it’s a direct child of the top­level FormGroup that represents the form model, so you specify just the name of the control. But the password field is a child of the nested FormGroup, so the path to the control is specified as an array of strings. The first element is the name of the nested group, and the second element is the name of the password field itself. Like the username field, passwordsGroup specifies the path as a string because it’s a direct child of the top­level FormGroup. The complete running application that illustrates how to use validator functions with reactive forms is located in the 09_reactive­with­validation.ts file in the code that comes with the book. In this example, you hardcoded the error messages in the

template, but they can be provided by the validators. For the example that dynamically provides error messages, see the 07_custom­validator­error­message.ts file.

Configuring validators with FormBuilder Validators can also be configured when you’re using FormBuilder to define form models. Here’s a modified version of the model for the sample registration form that uses FormBuilder:

Asynchronous validators

The Forms API supports asynchronous validators. Async validators can be used to check form values against a remote server, which involves sending an HTTP request. Like regular validators, async validators are functions. The only difference is that async validators should return either an Observable or a Promise object. Here’s an async version of the SSN validator. Listing 7.17. Async SSN validator

Async validators are passed as the third argument to constructors of model classes:

let ssnControl = new FormControl('', null, asyncSsnValidator);

The complete running application that illustrates how to use async validators is located in the 08_async­validator.ts file in the code that comes with the book. Checking a field’s status and validity

You’re already familiar with control properties such as valid, invalid, and errors for checking field statuses. In this section, we’ll look at a number of other properties that help improve the user experience: Touched and untouched fields— In addition to checking a control’s validity, you can also use the touched and untouched properties to check whether a field was visited by the user. If the user puts the focus into a field using the keyboard or mouse, the field becomes touched; otherwise it’s untouched. This can be useful when displaying error messages—if the value in a field is invalid but it was never visited by the user, you can choose not to highlight it with red, because it’s not a user mistake. Here’s an example:

Note

All the properties discussed here are available for the model classes FormControl, FormGroup, and FormArray, as well as for the template­driven directives NgModel, NgModelGroup, and NgForm.

Note the CSS class binding example on the last line. It conditionally applies the hasError CSS class to the element if the expression on the right side is true. If you used only c.invalid, the border would be highlighted as soon as the page was

rendered; but this can confuse users, especially if the page has a lot of fields. Instead, you add one more condition: the field must be touched. Now the field is highlighted only after a user visits this field. Pristine and dirty fields— Another useful pair of properties are pristine and its counterpart dirty. dirty indicates that the field was modified after it was initialized with its original value. These properties can be used to prompt the user to save changed data before leaving the page or closing the dialog window.

Note

All of the preceding properties have corresponding CSS classes (ng­touched and ng­untouched, ng­dirty and ng­pristine, ng­valid and ng­invalid) that are automatically added to HTML elements when the property is true. These can be useful to style elements in a certain state.

Pending fields— If you have async validators configured for a control, you may also find the Boolean property pending to be useful. It indicates whether the validity status is currently unknown. This happens when an async validator is still in progress and you need to wait for the results. This property can be used for displaying a progress indicator. For reactive forms, the statusChanges property of type Observable can be more convenient. It emits one of three values: VALID, INVALID, and PENDING. Validating template-driven forms

Directives are all you can use when you create template­driven forms, so you can wrap validator functions into directives to use them in the template. Let’s create a directive that wraps the SSN validator implemented in listing 7.17. Listing 7.18. SsnValidatorDirective

The square brackets around the ssn selector denote that the directive can be used as an

attribute. This is convenient, because you can add the attribute to any  element or to an Angular component represented as a custom HTML element. In this example, you register the validator function using the predefined NG_VALIDATORS Angular token. This token is in turn injected by the NgModel directive, and NgModel gets the list of all validators attached to the HTML element. Then NgModel passes validators to the FormControl instance it implicitly creates internally. The same mechanism is responsible for running validators; directives are just a different way to configure them. The multi property lets you associate multiple values with the same token. When the token is injected into the NgModel directive, NgModel gets a list of values instead of a single value. This enables you to pass multiple validators. Here’s how you can use SsnValidatorDirective:



You can find the complete running application that illustrates directive validators in the 06_custom­validator­directive.ts file in the code that comes with the book. Validating the sample registration form

Now you can add form validation to the sample registration form. Let’s start with the template. Listing 7.19. Registration form validation template

In the template­driven approach, you don’t have a model in the component. Only the template can inform the form’s handler whether the form is valid, and that’s why you pass the form’s value and validity status as arguments to the onSubmit() method. You also add the novalidate attribute to prevent standard browser validation from interfering with the Angular validation. Validation directives are added as attributes. The required directive is provided by Angular and is available once you register Forms API support with FormsModule. Similarly, you can use the minlength directive to validate the password field. To conditionally show and hide validation errors, you use the same hasError() method you used in the reactive version. But to access this method, you need to use a form property of type FormGroup, available on the f variable that references an instance of the formGroup directive. In the onSubmit() method, you check whether the form is valid before printing the value to the console. Listing 7.20. Checking form validation

Now for the last step: you need to add custom validator directives to the declarations list of the NgModule where you define AppComponent. Listing 7.21. Adding validator directives

Listing 7.21. Adding validator directives

The complete running application that illustrates how to use validator directives with template­driven forms is located in the 10_template­driven­with­validation.ts file in the code that comes with the book.

7.5. HANDS-ON: ADDING VALIDATION TO THE SEARCH FORM This hands­on exercise will start where you left off in chapter 6. You’ll need to modify the code of the SearchComponent to enable form validation and collect the data entered in the form. When the search form is submitted, you’ll print the form’s value on the browser’s console. Chapter 8 is about communication with the server, and in that chapter you’ll refactor the code so the search form will make a real HTTP request. In this section you’ll perform the following steps: 1.  Add a new method to the ProductService class that returns an array of all available product categories. 2.  Create a model representing the search form using FormBuilder. 3.  Configure validation rules for the model. 4.  Refactor the template to properly bind to the model created in the previous step. 5.  Implement the onSearch() method to handle the form’s submit event. Figure 7.4 shows what the search form will look like after you complete this hands­on exercise. It illustrates the validators in action. Figure 7.4. Search form with validators

If you prefer to see the final version of this project, browse the source code in the auction folder from chapter 7. Otherwise, copy the auction folder from chapter 6 to a separate location, and follow the instructions in this section.

7.5.1. Modifying the root module to add Forms API support Update the app.module.ts file to enable reactive forms support for the application. Import ReactiveFormsModule from @angular/forms, and add it to the list of imported modules in the main application NgModule. Listing 7.22. Updated app.module.ts file

import { ReactiveFormsModule } from '@angular/forms'; @NgModule({   imports: [     BrowserModule,     FormsModule,     ReactiveFormsModule,     RouterModule.forRoot([ ... ])   ],

7.5.2. Adding a list of categories to the SearchComponent Each product has the categories property, represented by an array of strings, and a single product can relate to multiple categories. The form should allow users to select a category while searching for products; you need a way to provide a list of all available categories to the form so it can display them to users. In a real­world application, the categories would likely come from the server. In this online auction example, you’ll add a method to the ProductService class that will return hardcoded categories:

1.  Open the app/services/product­service.ts file, and add a getAllCategories() method that accepts no parameters and returns a list of strings: getAllCategories(): string[] {   return ['Books', 'Electronics', 'Hardware']; }

2.  Open the app/components/search/search.ts file, and add an import statement for ProductService: import {ProductService} from '../../services/product­service';

3.  Configure this service as a provider for SearchComponent: @Component({   selector: 'auction­search',   providers: [ProductService],   //... })

4.  Declare a categories: string[] class property as a reference to the list of categories. You’ll use it for the data binding: export default class SearchComponent {   categories: string[]; }

5.  Declare a constructor() with one parameter: ProductService. Angular will inject it when the component is instantiated. Initialize the categories property using the getAllCategories() method:

7.5.3. Creating a form model Now let’s define the model that will handle the search form: 1.  Open the app/components/search/search.ts file, and add the Forms API­related imports. The import statement at the beginning of the file should look like this: import {Component} from '@angular/core';

import {FormControl, FormGroup, FormBuilder, Validators} from

 '@angular/forms';

2.  Declare a formModel class property of the FormGroup type: export default class SearchComponent {   formModel: FormGroup;   //... }

3.  In the constructor, define the formModel using the FormBuilder class: const fb = new FormBuilder(); this.formModel = fb.group({   'title': [null, Validators.minLength(3)],   'price': [null, positiveNumberValidator],   'category': [­1] })

4.  Add a positiveNumberValidator function: function positiveNumberValidator(control: FormControl): any {   if (!control.value) return null;   const price = parseInt(control.value);   return price === null || typeof price === 'number' && price > 0       ? null : {positivenumber: true}; }

positiveNumberValidator() attempts to parse an integer value from the FormControl’s value using the standard parseInt() function. If the parsed value is a valid positive number, the function returns null, meaning there are no errors. Otherwise the function returns an error object.

7.5.4. Refactoring the template Let’s add form directives to the template to bind the model defined in the previous step to the HTML elements: 1.  You defined the form model in the code implementing the reactive approach, so in the template you should attach the NgFormModel directive to the 
 element:

2.  Define the validation rules, and conditionally display error messages for the title field:   Product title:           Type at least 3 characters   

Here you use the form­group, form­control, has­error, and help­block CSS classes defined in the Twitter Bootstrap library. They’re required to properly render the form and highlight the field with the red border in the case of a validation error. You can read more about these classes in the Bootstrap documentation, in the “Forms” section: http://getbootstrap.com/css/#forms. 3.  Do the same for the product price field:   Product price:           Price is not a positive number   

4.  Add validation rules and an error message for the product category field:   Product category:        All categories     {{c}}   

The Submit button remains unchanged.

7.5.5. Implementing the onSearch() method Add the following onSearch() method:

onSearch() {   if (this.formModel.valid) {     console.log(this.formModel.value);   } }

7.5.6. Launching the online auction To launch the application, open a command window and start http­server in the root directory of the project. Enter http://localhost:8080 in a web browser, and you should see a Home page that includes the search form shown in 7.4. This version of the application illustrates form creation and validation without performing a search. You’ll implement the search functionality in chapter 8, when we discuss communication with servers.

7.6. SUMMARY In this chapter, you’ve learned how to work with forms in Angular. These are the main takeaways from this chapter: There are two approaches to working with forms: template­driven and reactive. The template­driven approach is easier and quicker to configure, but the reactive one is easier to test, enables more flexibility, and provides more control over the form. The reactive approach offers advantages for applications that use not only the DOM

renderer but another one (such as one from NativeScript) targeting non­browser environments. Reactive forms are programmed once and can be reused by more than one renderer. A number of standard validators ship with Angular, but you can also create custom ones. You should validate the user’s input, but client­side validation isn’t a replacement for performing additional validation on the server. Consider client­side validation as a way to provide instant feedback to the user, minimizing server requests involving invalid data.

Playlists

Chapter 8. Interacting with servers using HTTP and WebSockets

History

Topics This chapter covers TutorialsCreating a simple web server using the Node and Express frameworks

Making server requests from Angular using the Http object API

Offers & Deals

Communicating with the Node server from Angular clients using the HTTP protocol Highlights

Wrapping a WebSocket client into an Angular service that generates an observable stream

Settings

Broadcasting data from the server to multiple clients via WebSockets Support

Angular applications can communicate with any web server supporting HTTP or

SignWebSocket protocols, regardless of what server­side platform is used. So far, we’ve Out

been covering mostly the client side of Angular applications, with the weather service example in chapter 5 being the only exception. In this chapter, you’ll learn how to communicate with web servers in more detail. We’ll first give you a brief overview of Angular’s Http object, and then you’ll create a web server using TypeScript and Node.js. This server will provide the data required for all of the code samples, including the online auction. Then you’ll learn how the client code can make HTTP requests to web servers and consume the responses using observables, which we introduced in chapter 5. We’ll also show you how to communicate with the server via WebSockets, focusing on the server­side data push. In the hands­on section, you’ll implement a product search function in which the data about auction products and reviews will come from the server via HTTP requests. You’ll also implement product bid notifications, which will be sent by the server using the WebSocket protocol.

8.1. A BRIEF OVERVIEW OF THE HTTP OBJECT’S API Web applications run HTTP requests asynchronously so the UI remains responsive and

the user can continue working with the application while the HTTP requests are being processed by the server. Asynchronous HTTP requests can be implemented using callbacks, promises, or observables. Although promises eliminate the callback hell (see appendix A), they have the following shortcomings: There’s no way to cancel a pending request made with a promise. When a promise resolves or rejects, the client receives either the data or an error message, but in either case it’ll just be a single piece of data. A promise doesn’t offer a way to handle a continuous stream of chunks of data delivered over time. Observables don’t have these shortcomings. In section 5.2.2, we looked at a promise­ based scenario that resulted in multiple unnecessary requests to get a price quote for a stock, generating unnecessary network traffic. Then, in the example with the weather services in section 5.2.3, we demonstrated how you can cancel HTTP requests made with observables. Let’s look at Angular’s implementation of the Http class, which is included in the @angular/http package. This package includes several classes and interfaces, as described in the Angular HTTP client documentation at http://mng.bz/87C3. If you peek inside the @angular/http/src/http.d.ts type definition file, you’ll see the following APIs in the Http class:

import {Observable} from 'rxjs/Observable'; ... export declare class Http { ...    constructor(_backend: ConnectionBackend, _defaultOptions: RequestOptions);    request(url: string | Request, options?: RequestOptionsArgs):    

 Observable;

   get(url: string, options?: RequestOptionsArgs): Observable;    post(url: string, body: string, options?: RequestOptionsArgs):    

 Observable;

   put(url: string, body: string, options?: RequestOptionsArgs):    

 Observable;

  delete(url: string, options?: RequestOptionsArgs): Observable;   patch(url: string, body: string, options?: RequestOptionsArgs):   

 Observable;

  head(url: string, options?: RequestOptionsArgs): Observable;

}

This code is written in TypeScript, and each of the Http object’s methods has url as a mandatory argument, which can be either a string or a Request object. You can also pass an optional object of type RequestOptionArgs. Each method returns an Observable that wraps an object of type Response. The following code snippet illustrates one of the ways of using the get() API of the Http object, passing a URL as a string:

constructor(private http: Http) {   this.http.get('/products').subscribe(...); }

We haven’t specified the full URL here (such as http://localhost:8000/products), assuming that the Angular application makes a request to the server where it was deployed, so the base portion of the URL can be omitted. The subscribe() method has to receive an observer object with the code for handling the received data and errors. The Request object offers a more generic API, where you can separately create a Request instance, specify an HTTP method, and include the search parameters and a Header:

let myHeaders:Headers  = new Headers(); myHeaders.append('Authorization', 'Basic QWxhZGRpb'); this.http     .request(new Request({       headers: myHeaders,       method: RequestMethod.Get,       url: '/products',       search: 'zipcode=10001'     }))     .subscribe(...);

RequestOptionsArgs is declared as a TypeScript interface:

export interface RequestOptionsArgs {     url?: string;     method?: string | RequestMethod;     search?: string | URLSearchParams;

    headers?: Headers;     body?: any;     withCredentials?: boolean;     responseType?: ResponseContentType; }

All members of this interface are optional, but if you decide to use them, the TypeScript compiler will ensure that you provide values of the proper data types:

var myRequest: RequestOptionsArgs = {     url: '/products',     method: 'Get' }; this.http     .request(new Request(myRequest))     .subscribe(...);

In the hands­on section, you’ll see an example of using the search property of RequestOptionsArgs to make HTTP requests that have query string parameters.

What’s the Fetch API? There’s an effort under way to unify the process of fetching resources on the web. The Fetch API (https://fetch.spec.whatwg.org/) can be used as a replacement for the XMLHttpRequest object. It defines generic Request and Response objects, which can be used not only with HTTP, but also with other emerging web technologies like Service Workers and the Cache API. With the Fetch API, HTTP requests are made using the global function fetch():

To extract the body’s content from the response, you need to use one of the methods in the Response object. Each method expects the body to be in a certain format. You read the body as plain text using the text() method, which in turn returns a Promise.

Unlike Angular’s observable­based Http service, the Fetch API is promise­based. The Fetch API is mentioned in Angular documentation because several of Angular’s classes and interfaces are inspired by it (such as Request, Response, and RequestOptionsArgs).

Later in this chapter, you’ll see how to make requests using the Http object’s API and how to handle HTTP responses by subscribing to observable streams. In chapter 5, you used the public weather server, but here you’ll create your own web server using the Node.js framework.

8.2. CREATING A WEB SERVER WITH NODE AND TYPESCRIPT Many platforms allow you to develop and deploy web servers. In this book, we decided to use Node.js for the following reasons: There’s no need to learn a new programming language to understand the code. Node allows you to create standalone applications (such as servers). Node does a great job in the area of communications using HTTP or WebSockets. Using Node lets you continue writing code in TypeScript, so we don’t have to explain how to create a web server in Java, .NET, or Python. In Node, a simple web server can be written with a few lines of code, and you’ll start with a very basic one. Then you’ll write a web server that can serve JSON data (product details, of course) using the HTTP protocol. A bit later, you’ll create yet another version of the server that will communicate with the client over a WebSocket connection. Finally, in the hands­on project, we’ll teach you how to write the client portion of the auction so it communicates with your web server.

8.2.1. Creating a simple web server In this section, you’ll create a standalone Node application that will run as a server supporting all the Angular code examples. When both the server and client sides are ready, the project’s directory will have the structure shown in figure 8.1. Figure 8.1. The project structure of an Angular-Node application

Note

If you’ve run the code samples from appendix B, you already have the TypeScript compiler installed on your computer. Otherwise, do that now.

Let’s start by creating a directory named http_websocket_samples with a server subdirectory. Configure a new Node project there by running the following command:

npm init ­y

As you learned in chapter 2, the ­y option instructs npm to create the package.json configuration file with default settings, without prompting you for any options. Next, create the hello_server.ts file with the following content: Listing 8.1. hello_server.ts

Listing 8.1 needs to be transpiled, so create the tsconfig.json file in the http_websocket_samples directory to configure the tsc compiler. Listing 8.2. tsconfig.json

Listing 8.2. tsconfig.json

After running the npm run tsc command, the transpiled hello_server.js file will be saved in the build directory, and you can start your web server:

node build/hello_server.js

Node will start the V8 JavaScript engine that will run the script from hello_server.js; it will create a web server and print the following message on the console: “Listening on http://localhost:8000”. If you open your browser at this URL, you’ll see a web page with the text “Hello World!”

TypeScript 2.0 and @types In this project, you use a locally installed tsc compiler version 2.0, which uses the @types packages to install type­definition files. That’s because older versions of tsc didn’t support the types compiler option, and if you have an older version of tsc installed globally, running tsc will use that version, causing compilation errors. To ensure that you use the local version of tsc, configure it as a command ("tsc": "tsc") in the scripts section of package.json, and start the compiler by entering the npm run tsc command to transpile the server’s files. Run this command from the same directory where the tsconfig.json file is located (the project root in the code samples for this chapter).

You need to have Node’s type­definition files (see appendix B) to prevent TypeScript compilation errors. To install Node’s type definitions for another project, run the following command from the root directory of your project:

npm i @types/node ­­save

If you use the code samples that come with this chapter, you can run the command npm install, because the package.json file includes the @types/node dependency for Node:

"@types/node": "^4.0.30"

8.2.2. Serving JSON In all the auction code samples so far, the data about products and reviews has been hardcoded in the product­service.ts file as arrays of JSON­formatted objects. In the hands­on section, you’ll move this data to the server, so the Node web server needs to know how to serve JSON. To send JSON to the browser, you need to modify the header to specify a MIME type of application/json:

const server = http.createServer((request, response) => {     response.writeHead(200, {'Content­Type': 'application/json'});     response.end('{"message": "Hello Json!"}\n'); });

This snippet suffices as an illustration of sending JSON, but real­world servers perform more functions, such as reading files, routing, and handling various HTTP requests (GET, POST, and so on). Later, in the auction example, you’ll need to respond with either product or review data depending on the request. To minimize manual coding, let’s install Express (http://expressjs.com), a Node framework that provides a set of features required by all web applications. You won’t be using all of its functionality, but it will help with creating a RESTful web service that will return JSON­formatted data. To install Express, run the following command from the http_websocket_samples directory:

npm install express ­­save

This downloads Express into the node_modules folder and updates the

dependencies section in package.json. Because this project’s file has the entry "@types/express": "^4.0.31", you already have all the type definitions for Express in your node_modules directory. But if you want to install them in any other project, run the following command:

npm i @types/express ­­save

Now you can import Express into your application and start using its API while writing code in TypeScript. The following listing shows the my­express­server.ts file that implements the server­side routing for HTTP GET requests. Listing 8.3. my-express-server.ts

If you used the ES5 syntax instead of destructuring, you’d need to write two lines instead of one:

var address = server.address().address; var port = server.address().port;

Transpile my­express­server.ts by running npm run tsc, and start this server (node build/my­express­server.js). You can request either products or services, depending on which URL you enter in the browser, as shown in figure 8.2. Figure 8.2. Server-side routing with Express

Note

To debug Node applications, refer to your preferred IDE documentation. You can also use the node­inspector command­line tool (https://github.com/node­inspector/node­ inspector).

8.2.3. Live TypeScript recompilation and code reload The server­side examples are written in TypeScript, so you need to use tsc to transpile the code to JavaScript prior to deploying it in Node. In section B.3.1, in appendix B, we’ll discuss the compilation option ­w that runs tsc in watch mode; whenever a TypeScript file changes, it gets recompiled automatically. To set the auto­compilation mode for your code, open a separate command window in the directory with the sources, and run the following command there:

tsc ­w

When no filenames are specified, tsc uses the tsconfig.json file for compilation options. Now, whenever you make a change in the TypeScript code and save the file, it’ll generate a corresponding .js file in the build directory as specified in tsconfig.json. Accordingly, to start your web server with Node, you could use the following command:

node build/my­express­server.js

Live recompilation of the TypeScript code helps, but the Node server won’t automatically pick up code changes after it has started. So that you don’t need to manually restart the Node server to see your code changes in action, you can use a handy utility: Nodemon (http://nodemon.io). It will monitor for any changes in your

source and, when it detects changes, will automatically restart your server and reload the code. You can install Nodemon either globally or locally. For a global install, use the following command:

npm install ­g nodemon

The following command will start your server in monitoring mode:

nodemon build/my­express­server.js

Install Nodemon locally (npm install nodemon ­­save­dev) and introduce npm scripts (https://docs.npmjs.com/misc/scripts) in the package.json file. Listing 8.4. package.json

"scripts": {   "tsc": "tsc",   "start": "node build/my­express­server.js",   "dev": "nodemon build/my­express­server.js" }, "devDependencies": {   "nodemon": "^1.8.1" }

With this setup, you can start the server in development mode with npm run dev (auto restart/reload) or npm start in production (no restart/reload). We gave the name dev to the command that starts nodemon, but you can name it anything you want, such as startNodemon.

8.2.4. Adding the RESTful API for serving products Your ultimate goal is to serve products and reviews for the auction application. In this section, we’ll illustrate how to prepare a Node server with REST endpoints to serve products in JSON format when HTTP GET requests are received. You’ll modify the code in the my­express­server.ts file to serve either all products or a specific one (by ID). The modified version of this application, shown next, is located in the auction­rest­server.ts file. Listing 8.5. auction-rest-server.ts

Now you can start the auction­rest­server.ts application in Node (run nodemon build/auction­rest­server.js) and see if the browser receives all products or a selected product. Figure 8.3 shows the browser window after we entered the URL http://localhost:8000/products. Our server returned all the products in JSON format. Figure 8.3. Node server response to http://localhost:8000/products

Figure 8.4 shows the browser window after we entered the URL

http://localhost:8000/products/1. This time, our server returned only data about the product that has an id with the value of 1. Figure 8.4. Node server response to http://localhost:8000/products/1

The server is ready. Now you can learn how to initiate HTTP requests and handle responses in Angular applications.

8.3. BRINGING ANGULAR AND NODE TOGETHER Earlier in this chapter, you created the http_websocket_samples folder containing the auction­rest­server.ts file, which is a Node application that responds to HTTP GET requests and supplies product details. In this section, you’ll write an Angular client that will issue HTTP requests and treat the product’s data as an Observable object returned by your server. The code of the Angular application will be located in the client subdirectory (see figure 8.1).

8.3.1. Static resources on the server A typical web application deployed on the server includes static resources (such as HTML, images, CSS, and JavaScript code) that have to be loaded in the browser when the user enters the application’s URL. Because we’re using SystemJS, which does on­ the­fly transpiling, the TypeScript files are static resources as well. From Node’s perspective, the Angular portion of this application is considered static resources. Because Angular apps load dependencies from node_modules, this directory also belongs to the static resources required by the browser. The Express framework has a special API to specify the directories with static resources, and you’ll make slight modifications in the auction­rest­server.ts file shown in listing 8.5. In that file, you didn’t specify the directory with static resources, because no client’s app was deployed there. The new version of this file will be called auction­ rest­server­angular.ts. First, add the following lines:

import * as path from "path"; app.use('/', express.static(path.join(__dirname, '..', 'client')));

app.use('/node_modules', express.static(path.join(__dirname, '..',

 'node_modules')));

When the browser requests static resources, Node will look for them in the client and node_modules directories. Here you use Node’s path.join API to ensure that the file path is created in a cross­platform way. You can use path.join when you need to build an absolute path for a specific file; you’ll see examples later. Let’s keep the same REST endpoints on the server: / serves main.html, which is the landing page of the application. /products gets all products. /products/:id gets a product by its ID. Unlike in the my_express_server.ts application, you don’t want Node to handle the base URL; you want Node to send the main.html file to the browser. In the auction­ rest­server­angular.ts file, change the route for the base URL / to look like this:

app.get('/', (req, res) => {   res.sendFile(path.join(__dirname, '../client/main.html')); });

Now, when the user enters the URL of the Node server in the browser, the main.html file will be served first. Then it’ll load your Angular application with all dependencies. The common npm configuration file

The new version of the package.json file will combine all dependencies required for both the Node­related code and your Angular application. Note that you declare several commands in the script section. The first command is for running the locally installed tsc, and the others are to start Node servers for the code samples included in this chapter. Listing 8.6. Modified package.json file

{  "private": true,  "scripts": {    "tsc": "tsc",    "start": "node build/my­express­server.js",    "dev": "nodemon build/my­express­server.js",    "devRest": "nodemon build/auction­rest­server.js",    "restServer": "nodemon build/auction­rest­server­angular.js",

   "simpleWsServer": "node build/simple­websocket­server.js",    "twowayWsServer": "nodemon build/two­way­websocket­server.js",    "bidServer": "nodemon build/bids/bid­server.js"  },  "dependencies": {    "@angular/common": "^2.0.0",    "@angular/compiler": "^2.0.0",    "@angular/core": "^2.0.0",    "@angular/forms": "^2.0.0",    "@angular/http": "^2.0.0",    "@angular/platform­browser": "^2.0.0",    "@angular/platform­browser­dynamic": "^2.0.0",    "@angular/router": "^3.0.0",    "core­js": "^2.4.0",    "rxjs": "5.0.0­beta.12",    "systemjs": "0.19.37",    "zone.js": "0.6.21",    "@types/express": "^4.0.31",    "@types/node": "^4.0.30",    "express": "^4.14.0",    "ws": "^1.1.1"  },  "devDependencies": {    "@types/es6­shim": "0.0.30",    "@types/ws": "0.0.29",    "nodemon": "^1.8.1",    "typescript": "^2.0.0"  } }

Note that you include the @angular/http package here, which includes Angular’s support for the HTTP protocol. You also include ws and @types/ws—you’ll need them for WebSocket support later in the chapter.

npm scripts npm supports the scripts property in package.json with more than a dozen scripts available right out of the box (see the npm­scripts documentation for details, https://docs.npmjs.com/misc/scripts). You can also add new commands specific to your development and deployment workflow. Some of these scripts need to be run manually (such as npm start), and some are invoked automatically (such as postinstall). In general, if any command in the scripts section starts with the post prefix, it’ll run automatically after the command specified after this prefix. For example, if you define the command "postinstall":

"myCustomIstall.js", each time you run npm install, the myCustomIstall.js script will run as well. Similarly, if a command has a pre prefix, the command will run before the command named after this prefix. For example, in section 10.3.2, you’ll see the following commands in the package.json file:

"prebuild": "npm run clean && npm run test",  "build": "webpack ­­config webpack.prod.config.js ­­progress ­­profile

 ­­colors"

If you run the build command, npm will first run the script defined in prebuild; then it’ll run the script defined in build. So far, you’ve been using only two commands: npm start and npm run dev. But you can add any commands you like to the scripts section of your package.json file. For example, both the build and prebuild commands in the preceding example are custom commands.

Common vs. separate configuration files In this chapter, all code samples for the client and server belong to a single npm project and share the same package.json file. All dependencies and typings are shared by the client and server applications. This setup may reduce the time for installing dependencies and save space on disk because some of the dependencies may be shared between the client and server. But keeping the code for the client and server in a single project tends to complicate the build automation process for two reasons: Client and server may require conflicting versions of a particular dependency. You use build automation tools, which may require different configurations for client and server, and their node_modules directories won’t be located in the root directory of the project. In chapter 10, you’ll separate the client and server portions of the online auction into two independent npm projects.

The next step is to add an Angular app to the client directory.

8.3.2. Making GET requests with the Http object When Angular’s Http object makes a request, the response comes back as Observable, and the client’s code will handle it by using the subscribe() method. Let’s start with a simple application (client/app/main.ts) that retrieves all products from the Node server and renders them using an HTML unordered list. Listing 8.7. client/app/main.ts

To see the error callback in action, change the endpoint from '/products' to something else. Your Angular application will print the following on the console: “Can’t get products. Error code: 404, URL: http://localhost:8000/products”.

Note

The HTTP GET request is sent to the server only when you invoke the subscribe() method and not when you call the get() method.

You’re ready to start the server and enter its URL in the browser to see the Angular app served. You can start your Node server either by running the long command

node build/auction­rest­server­angular.js

or by using the npm script that you defined in the package.json file:

npm run restServer

Open the browser to http://localhost:8000, and you’ll see the Angular app shown in figure 8.5. Figure 8.5. Retrieving all products from the Node server

Note

Make sure the client/systemjs.config.js file maps the app package to main.ts.

Tip

You can make HTTP GET requests that pass parameters in the URL after the question mark (such as myserver.com?param1=val1¶m2=val2). The Http.get() method can accept a second parameter, which is an object that implements RequestOptionsArgs. The search field of RequestOptionsArgs can be used to set either a string or a URLSearchParams object. You’ll see an example of using URLSearchParams in the hands­on section.

8.3.3. Unwrapping observables in templates with AsyncPipe In the previous section, you handled the observable stream of products in the TypeScript code by invoking the subscribe() method. Angular offers an alternative syntax that lets you handle observables right in the template of a component with

pipes; it’s covered in chapter 5. Angular includes AsyncPipe (or async if used in templates), which can receive a Promise or Observable as input and subscribe to it automatically. To see this in action, let’s make the following changes in the code from the previous section: Change the type of the products variable from Array to Observable. Remove the declaration of the theDataSource variable. Remove the invocation of subscribe() in the code. You’ll assign the Observable returned by http.get().map() to products. Add the async pipe to the *ngFor loop in the template. The following code (main­asyncpipe.ts) implements these changes. Listing 8.8. main-asyncpipe.ts

Running this application will produce the same output you saw in figure 8.5.

Note

This version of AppComponent with async is shorter than the version in listing 8.7. But the code that explicitly invokes subscribe() is easier to test.

8.3.4. Injecting HTTP into a service In this section, you’ll see an example of an injectable ProductService class that will encapsulate HTTP communications with the server. You’ll create a small application in which the user can enter the product ID and have the application make a request to the server’s /products/:id endpoint. The user enters the product ID and clicks the button, which starts a subscription to the Observable property productDetails on the ProductService object. Figure 8.6 shows the injectable objects of the application you’re going to build. Figure 8.6. The client-server workflow

In chapter 7, you became familiar with the Forms API. Here you’ll create an App­ Component with a simple form that has an input field and a Find Product button. This application will communicate with the Node web server you created earlier, and you’ll implement the client portion in two iterations. In the first version (main­form.ts), you won’t use the ProductService class. The AppComponent will get the Http object injected and will make requests to the server. Listing 8.9. main-form.ts

Figure 8.7 shows a screenshot taken after we entered 2 as a product ID and clicked the Find Product button, which sent a request to the URL http://localhost:8000/products/2. The Node Express server matched /products/2 with the corresponding REST endpoint and routed this request to the method defined as app.get('/products/:id'). Figure 8.7. Getting product details by ID

Injecting an Http object into a service

Now let’s introduce the ProductService class (product­service.ts). In listing 8.9, you injected Http into the constructor of the AppComponent; now you’ll move the code that uses Http into ProductService so the code reflects the architecture in figure 8.6. Listing 8.10. product-service.ts

The ProductService class uses DI. The @Injectable() decorator instructs the TypeScript compiler to generate the metadata for ProductService, and using this decorator is required here. When you were injecting Http into the component that had another decorator (@Component), that was a signal to the TypeScript compiler to generate the metadata for the component required for DI. If the class ProductService didn’t have any decorators, the TypeScript compiler wouldn’t generate any metadata for it, and the Angular DI mechanism wouldn’t know that it had some injection to do into Product­Service. The mere existence of the @Injectable() decorator is required for classes that represent services, and you

shouldn’t forget to include "emitDecoratorMetadata": true in the tsconfig.json file. The new version of AppComponent (main­with­service.ts) will become a subscriber of the observable stream produced by ProductService. Listing 8.11. main-with-service.ts

ProductService isn’t a component, but a class, and Angular doesn’t allow you to specify providers for classes. As a result, you specify the provider for Http in the AppComponent by including the providers property in the @Component decorator. The other choice would be to declare providers in @NgModule. In this particular application, it wouldn’t make a difference.

In chapter 4, while discussing DI, we mentioned that Angular can inject objects, and, if they have their own dependencies, Angular will inject them as well. Listing 8.11 proves that Angular’s DI module works as expected.

8.4. CLIENT-SERVER COMMUNICATION VIA WEBSOCKETS WebSocket is a low­overhead binary protocol supported by all modern web browsers. With request­based HTTP, a client sends a request over a connection and waits for a response to come back (half­duplex), as shown in figure 8.8. On the other hand, the WebSocket protocol allows data to travel in both directions simultaneously (full­ duplex) over the same connection, as shown in figure 8.9. The WebSocket connection is kept alive, which has an additional benefit: low latency in the interaction between the server and the client. Figure 8.8. Half-duplex communication

Figure 8.9. Full-duplex communication

Whereas a typical HTTP request/response adds several hundred bytes (the headers) to

the application data, with WebSockets the overhead is as low as a couple of bytes. If you’re not familiar with WebSockets, refer to www.websocket.org or one of the many tutorials available online.

8.4.1. Pushing data from a Node server WebSockets are supported by most server­side platforms (Java, .NET, Python, and others), but you’ll continue using Node to implement your WebSocket­based server. You’ll implement one particular use case: the server will push data to a browser­based client as soon as the client connects to the socket. We purposely won’t have you send a request for data from the client, to illustrate that WebSockets aren’t about request­ response communication. Either party can start sending the data over the WebSocket connection. Several Node packages implement the WebSocket protocol; here you’ll use the npm package called ws (https://www.npmjs.com/package/ws). Install it by entering the following command in your project’s directory:

npm install ws ­­save

Then install the type definition file for ws:

npm install @types/ws ­­save­dev

Now the TypeScript compiler won’t complain when you use the API from the ws package. Besides, this file is handy for seeing the APIs and types available. Your first WebSocket server will be pretty simple: it’ll push the text “This message was pushed by the WebSocket server” to the client as soon as the connection is established. You purposely don’t want the client to send any data request to the server, to illustrate that a socket is a two­way street and that the server can push the data without any request ceremony. The application in listing 8.12 (simple­websocket­server.ts) creates two servers. The HTTP server will run on port 8000 and will be responsible for sending the initial HTML file to the client. The WebSocket server will run on port 8085 and will communicate with all connected clients through this port. Listing 8.12. simple-websocket-server.ts

Note

In listing 8.12, you import only the Server module from ws. If you used other exported members, you could write import * as ws from "ws";.

In listing 8.12, HTTP and WebSocket servers are running on different ports, but you could reuse the same port by providing the newly created httpServer instance to the constructor of WsServer:

const httpServer = app.listen(8000, "localhost", () => {...}); const wsServer: WsServer = new WsServer({server: httpServer});.

In the hands­on section, you’ll reuse port 8000 for both HTTP and WebSocket communications (see the server/auction.ts file).

Note

Note

As soon as the new client connects to the server, the reference to this connection is added to the wsServer.clients array so you can broadcast messages to all connected clients if needed: wsServer.clients.forEach (client => client.send('...'));.

The content of the client’s simple­websocket­client.html file is shown in listing 8.13. This client doesn’t use either Angular or TypeScript. As soon as this file is downloaded to the browser, its script connects to your WebSocket server at ws://localhost:8085. Note that the protocol is ws and not http. For a secure socket connection, use the wss protocol. Listing 8.13. simple-websocket-client.html

To run the server that pushes data to the clients, start the Node server (node build/simple­websocket­server.js or npm simpleWsServer). It will print the following messages on the console:

WebSocket server is listening on port 8085 HTTP Server is listening on 8000

Note

If you’ll be modifying the code located in the server directory, don’t forget to run npm run tsc in the root directory of your project to create a fresh version of your JavaScript code in the build directory. Otherwise the node command will load the old JavaScript file.

To receive the message pushed from the server, open the browser to http://localhost:8000. You’ll see the message, as shown in figure 8.10. Figure 8.10. Getting the message from the socket

In this example, the HTTP protocol is used only to initially load the HTML file. Then the client requests the protocol upgrade to WebSocket (status code 101), and from then on this application won’t use HTTP.

Tip

You can monitor messages going over the socket by using the Frames tab in Chrome Developer Tools.

8.4.2. Turning a WebSocket into an observable In the previous section, you wrote a client in JavaScript (no Angular) using the browser’s WebSocket object. Now we’ll show you how to create a service that will wrap

the browser’s WebSocket object in an observable stream so Angular components can subscribe to messages coming from the server over the socket connection. Earlier, in section 8.3.2, the code that received the product data was structured as follows (in pseudocode):

this.http.get('/products')   .subscribe(       data => handleNextDataElement(),       err => handleErrors(),       () => handleStreamCompletion() );

Basically, your goal was to write the application code that would consume the observable stream provided by Angular’s Http service. But Angular has no service that will produce an observable from a WebSocket connection, so you’ll have to write such a service. This way, the Angular client will be able to subscribe to messages coming from the WebSocket the same way it did with the Http object. Wrapping any service in an observable stream

Now you’ll create a small Angular application that won’t use a WebSocket server but will illustrate how to wrap business logic into an Angular service that emits data via an observable stream. Let’s start by creating an observable service that will emit hard­ coded values without actually connecting to a socket. The following code creates a service that emits the current time every second. Listing 8.14. custom-observable-service.ts

import {Observable} from 'rxjs/Rx'; export class CustomObservableService{   createObservableService(): Observable{       return new Observable(           observer => {               setInterval(() =>                   observer.next(new Date())               , 1000);           }       );   } }

In this code, you create an observable, assuming that the subscriber will provide an Observer object that knows what to do with the data pushed by the observable. Whenever the observable invokes the next() method on the observer, the subscriber will receive the value given as an argument (new Date() in this example). The data stream never throws an error and never completes.

Note

You can also create a subscriber for an observable by explicitly invoking Subscriber.create(). You’ll see such an example in the hands­on section.

The AppComponent in listing 8.15 gets the CustomObservableService injected, invokes the createObservableService() method that returns Observable, and subscribes to it, creating an observer that knows what to do with the data. The observer in this application assigns the received time to the currentTime variable. Listing 8.15. custom-observable-service-subscriber.ts

import { platformBrowserDynamic } from '@angular/platform­browser­dynamic'; import { NgModule, Component }      from '@angular/core'; import { BrowserModule } from '@angular/platform­browser'; import 'rxjs/add/operator/map'; import {CustomObservableService} from "./custom­observable­service"; @Component({   selector: 'app',   providers: [ CustomObservableService ],   template: `

Simple subscriber to a service

       Current time: {{currentTime | date: 'jms'}}   `}) class AppComponent {   currentTime: Date;   constructor(private sampleService: CustomObservableService) {       this.sampleService.createObservableService()         .subscribe( data => this.currentTime = data );   } } @NgModule({

  imports:      [ BrowserModule],   declarations: [ AppComponent],   bootstrap:    [ AppComponent ] }) class AppModule { } platformBrowserDynamic().bootstrapModule(AppModule);

For this app, you create the index.html file in the root directory of the project. This app doesn’t use any servers, and you can run it by entering the command live­server in the terminal window. In the browser’s window, the current time will be updated every second. You use DatePipe here with the format 'jms', which displays only hours, minutes, and seconds (all date formats are described in the Angular DatePipe documentation at http://mng.bz/78lD). This is a simple example, but it demonstrates a basic technique for wrapping any application logic in an observable stream and subscribing to it. In this case, you use setInterval(), but you could replace it with any application­specific code that generates one or more values and sends them as a stream. Don’t forget about error handling and completing the stream if need be. The following code snippet shows a sample observable that sends one element to the observer, may throw an error, and tells the observer that the streaming is complete:

return new Observable(     observer => {       try {         observer.next('Hello from observable');         //throw ("Got an error");       } catch(err) {          observer.error(err);       } finally{          observer.complete();       }     } );

If you uncomment the line with throw, observer.error() is invoked, which results in the invocation of the error handler on the subscriber, if there is one. Now let’s teach the Angular service to communicate with the WebSocket server. Angular talking to a WebSocket server

Let’s create a small Angular application with a WebSocket service (on the client) that interacts with the Node WebSocket server. The server­side tier can be implemented with any technology that supports WebSockets. Figure 8.11 illustrates the architecture of such an application. Figure 8.11. Angular interacting with a server via a socket

The code in listing 8.16 wraps the browser’s WebSocket object into an observable stream. This service creates an instance of WebSocket that’s connected to the server based on the provided URL, and this instance handles messages received from the server. The WebSocketService also has a sendMessage() method so the client can send messages to the server. Listing 8.16. websocket-observable-service.ts

import {Observable} from 'rxjs/Rx'; export class WebSocketService{     ws: WebSocket;     createObservableSocket(url:string):Observable{         this.ws = new WebSocket(url);         return new Observable(           observer => {             this.ws.onmessage = (event) =>                       observer.next(event.data);             this.ws.onerror = (event) => observer.error(event);             this.ws.onclose = (event) => observer.complete();         }      );     }

    sendMessage(message: any){         this.ws.send(message);     } }

Note

Listing 8.16 shows one way to create an observable from WebSocket. As an alternative, you can use the Observable.webSocket() method to do this.

Listing 8.17 shows the code of the AppComponent that subscribes to WebSocketService, which is injected into the AppComponent from figure 8.11. This component can also send messages to the server when the user clicks the Send Msg to Server button. Listing 8.17. websocket-observable-service-subscriber.ts

import { platformBrowserDynamic } from '@angular/platform­browser­dynamic'; import { NgModule, Component } from '@angular/core'; import { BrowserModule } from '@angular/platform­browser'; import {WebSocketService} from "./websocket­observable­service"; @Component({   selector: 'app',   providers: [ WebSocketService ],   template: `

Angular subscriber to WebSocket service

       {{messageFromServer}}
       Send msg to Server   `}) class AppComponent {   messageFromServer: string;   constructor(private wsService: WebSocketService) {       this.wsService.createObservableSocket("ws://localhost:8085")         .subscribe(             data => {               this.messageFromServer = data;             },             err => console.log( err),             () =>  console.log( 'The observable stream is complete')         );   }

    sendMessageToServer(){         console.log("Sending message to WebSocket server");         this.wsService.sendMessage("Hello from client");     } } @NgModule({     imports:      [ BrowserModule],     declarations: [ AppComponent],     bootstrap:    [ AppComponent ] }) class AppModule { } platformBrowserDynamic().bootstrapModule(AppModule);

The HTML file that renders this component is called two­way­websocket­client.html. You need to make sure websocket­observable­service­subscriber is configured as the main app script in systemjs.config.js. Listing 8.18. two-way-websocket-client.html

  Http samples

                        Loading...

Finally, you’ll create another version of simple­websocket­server.ts to serve an HTML file with a different Angular client. This server will be implemented in the two­way­ websocket­server.ts file and will have almost the same code, with two small changes:

1.  When the server receives a request to the base URL, it needs to serve the preceding HTML to the client: app.get('/', (req, res) => { res.sendFile(path.join(__dirname, '..',

 'client/two­way­websocket­client.html')); });

2.  You need to add the on('message') handler to process messages arriving from the client: wsServer.on('connection',    websocket => {        websocket.send('This message was pushed by the WebSocket server');        websocket.on('message',                       message => console.log("Server received: %s",                           });

 message));

To see this application in action, run nodemon build/two­way­ websocket_server.js (or use the npm run twowayWsServer command that’s configured in package.json), and open your browser to localhost:8000. You’ll see the window with the message pushed from Node, and if you click the button, a “Hello from client” message will be sent to the server. We took the screenshot in figure 8.12 after clicking the button once (Chrome Developer Tools was opened to the Frames tab under Network). Figure 8.12. Getting the message in Angular from Node

Now that you know how to communicate with a server via the HTTP and WebSocket protocols, let’s teach the online auction to interact with the Node server.

8.5. HANDS-ON: IMPLEMENTING PRODUCT SEARCH AND BID NOTIFICATIONS The amount of code added to this chapter’s version of the auction is pretty substantial, so we decided to spare you from typing it all. In this hands­on exercise, we’ll just review the new and modified code fragments in the new version of the auction app that comes with this chapter. This version of the application accomplishes two main goals: Implements product search functionality. The SearchComponent will connect the auction to the Node server via HTTP, and the data about products and reviews will come from the server. Adds server­pushed bid notifications using the WebSocket protocol, so the user can subscribe and watch bid prices for a selected product. Figure 8.13 shows the main players involved in the product search implementation. Figure 8.13. Product search implementation

The DI in the figure stands for dependency injection. Angular injects the Http object into ProductService, which in turn is injected into three components: HomeComponent, SearchComponent, and ProductDetailComponent. The ProductService object is responsible for all communications with the server.

Note

You use the Node server in this project, but you can use any technology that supports the HTTP and WebSocket protocols, such as Java, .NET, Python, Ruby, and so on.

As we mentioned, we’ll provide brief explanations about the code changes made in various scripts, but you should perform a detailed code review of the auction on your own. In this chapter’s version of the auction, the scripts section looks like this:

"scripts": {     "tsc": "tsc",     "start": "node build/auction.js",     "dev": "nodemon build/auction.js"   }

Running npm start will start your Node server loading the auction.js script. In this project, the tsconfig.json file specifies the build directory as an output for the TypeScript compiler; two files, auction.js and model.js, are created there when you run npm run tsc in the project root directory. If you have version 2.0 or later of the TypeScript compiler installed globally, you can just run the tsc command. The TypeScript auction.ts source contains the code implementing the HTTP and WebSocket servers, and model.ts contains the data that now resides on the server. Running npm run dev will start your Node server in live reload mode.

8.5.1. Implementing product search using HTTP

8.5.1. Implementing product search using HTTP The auction’s home page has a Search form on the left side; the user can enter search criteria, click the button Search, and get matching products from the server. As shown in figure 8.13, ProductService is responsible for all HTTP communications with the server, including the initial load of product information or finding products that meet certain criteria. Moving product and review data to the server

So far, data about products and reviews has been hardcoded in the client­side ProductService class; when the application starts, it shows all the hardcoded products in HomeComponent. When the user clicks a product, the router navigates to ProductDetailComponent, which shows product details and reviews, also hardcoded in ProductService. Now you want the data about products and reviews to be located on the server. The server/auction.ts and server/model.ts files contain the code that will run as a Node application (the web server). The auction.ts file implements HTTP and WebSocket functionality, and the model.ts file declares the Product and Review classes and the products and reviews arrays with the data. These arrays were also removed from the client/app/services/product­service.ts file.

Note

The Product class has a new categories property, which will be used in the SearchComponent.

The ProductService class

The ProductService class will get the Http object injected, and most of the methods of this class will return observable streams generated by HTTP requests. The following code fragment shows the new version of the getProducts() method:

getProducts(): Observable {   return this.http.get('/products')     .map(response => response.json()); }

As you’ll recall, the preceding method won’t issue the HTTP GET request until some

object subscribes to getProducts() or a component’s template uses an AsyncPipe with the data returned by this method (you can find an example in HomeComponent). The getProductById() method looks similar:

getProductById(productId: number): Observable {   return this.http.get(`/products/${productId}`)     .map(response => response.json()); }

The getReviewsForProduct() method also returns an Observable:

getReviewsForProduct(productId: number): Observable {   return this.http     .get(`/products/${productId}/reviews`)     .map(response => response.json())     .map(reviews => reviews.map(       (r: any) => new Review(r.id, r.productId, new Date(r.timestamp),        }

r.user, r.rating, r.comment)));

The new ProductService.search() method is used when the user clicks the Search button in SearchComponent:

search(params: ProductSearchParams): Observable {     return this.http       .get('/products', {search: encodeParams(params)})       .map(response => response.json());   }

The preceding Http.get() method uses a second argument, which is an object with the search property for storing the query string parameters. As you saw in the RequestOptionsArgs interface earlier, the search property can hold either a string or an instance of URLSearchParams. Following is the code of the ProductService.encodeParams() method, which turns a JavaScript object into an instance of URLSearchParams:

function encodeParams(params: any): URLSearchParams {   return Object.keys(params)     .filter(key => params[key])     .reduce((accum: URLSearchParams, key: string) => {       accum.append(key, params[key]);

      return accum;     }, new URLSearchParams()); }

The new ProductService.getAllCategories() method is used to populate the Categories dropdown in the SearchComponent:

getAllCategories(): string[] {   return ['Books', 'Electronics', 'Hardware']; }

The ProductService class also defines a new searchEvent variable of type EventEmitter. We’ll explain its use in the next section when we discuss how to pass the search results to the HomeComponent. Providing search results to HomeComponent

Initially, HomeComponent displays all products by invoking the ProductService.getProducts() method. But if the user performs a search by some criteria, you need to make a request to the server, which may return a subset of products or an empty data set if none of the products meet the search criteria. SearchComponent receives a result, which has to be passed to HomeComponent. If both of these components were children of the same parent (such as AppComponent), you could use the parent as a mediator (see chapter 6) and input/output parameters of the children for the data. But HomeComponent is added to AppComponent dynamically by the router, and currently Angular doesn’t support cross­route input/output parameters. You need another mediator, and the ProductService object can become one, because it’s injected into both SearchComponent and HomeComponent. The ProductService class has a searchEvent variable that’s declared as follows:

searchEvent: EventEmitter = new EventEmitter();

SearchComponent uses this variable to emit the searchEvent that carries the object with search parameters as the payload. HomeComponent subscribes to this event, as shown in figure 8.14. Figure 8.14. Component communication via events

SearchComponent is a form, and when the user clicks the Search button, it has to notify the world about which search parameters were entered. Product­Service does this by emitting the event with the search parameters:

onSearch() {   if (this.formModel.valid) {     this.productService.searchEvent.emit(this.formModel.value);   } }

HomeComponent is subscribed to the searchEvent that may arrive from SearchComponent with a payload of search parameters. As soon as that happens, the ProductService.search() method is invoked:

this.productService.searchEvent   .subscribe(     params => this.products = this.productService.search(params),     console.error.bind(console),     () => console.log('DONE')   );

Search limitations Our search solution assumes that HomeComponent is displayed on the screen when the user performs the product search. But if the user navigates to the Product Detail view, HomeComponent is removed from the DOM, and there are no listeners for the search­Event. This isn’t a serious shortcoming for an example in a book, and an easy fix would be to disable the search button if the user navigated from the Home route. You can also inject the Router object into SearchComponent and, when the user

clicks the Search button while the home route isn’t active (if (!router.isActive(url))), navigate to it programmatically by invoking router.navigate('home'), which returns a Promise object. When the promise is resolved, you can emit the searchEvent from there.

Handling product search on the server

The following code fragment is from the auction.ts file that handles the product search request sent from the client. When the client hits the server’s endpoint with query string parameters, you pass the received parameters as req.query to the getProducts() function, which performs a sequence of filters (as specified by the parameters) on the products array to filter out the non­matching products:

app.get('/products', (req, res) => {   res.json(getProducts(req.query)); }); ... function getProducts(params): Product[] {   let result = products;   if (params.title) {     result = result.filter(       p => p.title.toLowerCase().indexOf(params.title.toLowerCase()) !== ­1);   }   if ( result.length > 0 && parseInt(params.price)) {     result = result.filter(       p => p.price <= parseInt(params.price));   }   if ( result.length > 0 && params.category) {     result = result.filter(       p => p.categories.indexOf(params.category.toLowerCase()) !== ­1);   }   return result; }

Testing the product search functionality

Now that we’ve done a brief code review of the product search implementation, you can start the Node server using the command npm run dev and open the browser to localhost:8000. When the auction app loads, enter your search criteria in the form on the left, and see how HomeComponent re­renders its children

(ProductItemComponent) that meet the search criteria.

8.5.2. Broadcasting auction bids using WebSockets In real auctions, multiple users can bid on products. When the server receives a bid from a user, the bid server should broadcast the latest bid to all users who are interested in receiving such notifications (those who subscribed for notifications). You’ve emulated the bidding process by generating random bids from random users. When users open the Product Details view, they should be able to subscribe to bid notifications made by other users on the selected product. You implement this functionality with a server­side push via WebSockets. Figure 8.15 shows the Product Details view with the Watch toggle button that starts and stops the current bid notifications pushed by the server over the socket. Next, we’ll briefly highlight the changes in the auction app related to bid notifications. Figure 8.15. Toggle button for watching bids

The client side

Two new services are located in the client/app/services directory: BidService and WebSocketService. WebSocketService is an Observable wrapper for the WebSocket object. It’s similar to the one you created earlier, in section 8.4.2. BidService gets the WebSocketService injected:

@Injectable() export class BidService {   constructor(private webSocket: WebSocketService) {}   watchProduct(productId: number): Observable {     let openSubscriber = Subscriber.create(       () => this.webSocket.send({productId: productId}));

    return this.webSocket.createObservableSocket('ws://localhost:8000', openSubs       .map(message => JSON.parse(message));   } }

BidService is injected into ProductDetailComponent. When the user clicks the Watch toggle button, the BidService.watchProduct() method sends the product ID to the server, indicating that this user wants to start or stop watching the selected product:

toggleWatchProduct() {   if (this.subscription) {     this.subscription.unsubscribe();     this.subscription = null;     this.isWatching = false;   } else {     this.isWatching = true;     this.subscription = this.bidService.watchProduct(this.product.id)       .subscribe(         products => this.currentBid = products.find((p: any) => p.productId          === this.product.id).bid,         error => console.log(error));   } }

The template of the ProductDetailComponent has the Watch toggle button, and the latest bid received from the server is rendered as HTML label:

    {{ isWatching ? 'Stop watching' : 'Watch' }}      

There’s also a small new client/app/services/services.ts script, in which you declare all the import statements and the array of services used for dependency injection:

import {BidService} from './bid­service'; import {ProductService} from './product­service'; import {WebSocketService} from './websocket­service'; export const ONLINE_AUCTION_SERVICES = [   BidService,   ProductService,   WebSocketService ];

The providers declared in the ONLINE_AUCTION_SERVICES constant are used in the main.ts file that bootstraps the Angular portion of the auction:

@NgModule({ ...   providers:[ProductService,              ONLINE_AUCTION_SERVICES,              {provide: LocationStrategy, useClass: HashLocationStrategy}],   bootstrap:[ ApplicationComponent ] })

The server side

The server/auction.ts script includes the code that maintains subscribed clients and generates random bids. Each generated bid can be up to five dollars higher than the last one. As soon as the new bid is generated, it’s broadcast to all subscribed clients. The following code from the server/auction.ts file handles bid notification requests and broadcasting bids to all subscribed clients:

Here you test the readyState property of the WebSocket object to make sure the client is still connected. For example, if the user closed the auction window, there would be no need to send bid notifications, so this socket connection is removed from the subscriptions map.

Note

Note the use of the spread operator (...) in the subscribeToProductBids() method. You use it to copy an existing array of product IDs and add a new one.

We’ve covered the WebSocket­related code of the auction, and we encourage you to review the rest of the code on your own. To test the bid­notification functionality, you’ll need to start the application, click a product title, and, on the product detail view, click the Watch button. You should see the new bids for this product that are pushed from the server. Open the auction application in more than one browser to test that each browser properly turns bid notifications on and off.

8.6. SUMMARY The main subject of this chapter was enabling client­server interaction, which is the reason web frameworks exist. Angular, combined with the RxJS extensions library, offers a unified approach for consuming data from the server: the client’s code subscribes to the data stream coming from the server, whether it’s an HTTP­ or WebSocket­based interaction. The programming model is changed: instead of requesting the data as in AJAX­style applications, Angular consumes data that’s pushed by observable streams. These are the main takeaways from this chapter: Angular comes with the Http object that supports HTTP communications with web servers.

Providers for HTTP services are located in the HttpModule module. If your app uses HTTP, don’t forget to include it in the @NgModule decorator. Public methods of HttpObject return Observable, and only when the client subscribes to it is the request to the server made. The WebSocket protocol is more efficient and concise than HTTP. It’s bidirectional, and both client and server can initiate communication. Creating a web server with NodeJS and Express is relatively simple, but an Angular client can communicate with web servers implemented in different technologies.

y

Chapter 9. Unit-testing Angular applications

History

This chapter covers Topics

The basics of unit testing with the Jasmine framework Tutorials

The main artifacts from the Angular testing library

Offers &Testing the main players of an Angular app: services, components, and the router Deals

Running unit tests against web browsers with the Karma test runner

Highlights

Implementing unit testing in the online auction example Settings

To ensure that your software has no bugs, you need to test it. Even if your application has no bugs today, it may have them tomorrow, after you modify the existing code or

Support

introduce new code. Even if you don’t change the code in a particular module, it may

Signstop working properly as a result of changes in another module. Your application code Out

has to be retested regularly, and this process should be automated. You need to prepare test scripts and start running them as early as possible in your development cycle. There are two main types of testing for the front end of web applications:

Unit testing asserts that a small unit of code (such as a component or function) accepts the expected input data and returns the expected result. Unit testing is about testing isolated pieces of code, especially public interfaces. That’s what we’ll discuss in this chapter. End­to­end testing asserts that the entire application works as end users expect and that all units properly interact with each other. For end­to­end testing of Angular 2 applications, you can use the Protractor library (see https://angular.github.io/protractor).

Note

Load or stress testing shows how many concurrent users can work with a web application while it maintains the expected response time. Load­testing tools are

mainly about testing the server side of web applications.

Unit tests are for testing the business logic of separate units of code, and typically you’ll be running unit tests a lot more often than end­to­end tests. End­to­end testing can emulate a user’s actions (such as button clicks) and check the behavior of your application. During end­to­end testing, you shouldn’t run the unit­testing scripts. This chapter is about unit­testing Angular applications. Several frameworks have been specifically created for implementing and running unit tests, and our framework of choice is Jasmine. Actually, it’s not only our choice—as we write this, Angular’s testing library only works with Jasmine for unit testing. This is described in the “Jasmine Testing 101” section of the Angular documentation (http://mng.bz/0nv3). We’ll start by covering the basics of unit testing with Jasmine; toward the end of the chapter, you’ll write and run test scripts to unit­test selected components in the online auction. We’ll give you a brief overview of Jasmine so you can quickly start writing unit tests; for more details, see the Jasmine documentation (http://jasmine.github.io). For running tests, you’ll use a test runner called Karma (https://karma­runner.github.io), which is an independent command­line utility that can run tests written in different test frameworks.

9.1. GETTING TO KNOW JASMINE Jasmine allows you to implement a behavior­driven development (BDD) process, which suggests that tests of any unit of software should be specified in terms of the desired behavior of the unit. With BDD, you use natural language constructs to describe what you think your code should be doing. You write unit­test specifications in the form of short sentences, such as “ApplicationComponent is successfully instantiated” or “StarsComponent emits the rating change event.” Because it’s so easy to understand the meaning of the tests, they can serve as your program documentation. If other developers need to become familiar with your code, they can start by reading the code of the unit tests to understand your intentions. Using natural language to describe tests has another advantage: it’s easy to reason about the test results, as shown in figure 9.1. Figure 9.1. Running tests using Jasmine’s test runner

Tip

As much as we’d like all of our tests to pass, make a habit of ensuring that your tests fail first, and see if the test results are easy to understand.

In Jasmine terminology, a test is called a spec, and a combination of one or more specs is called a suite. A test suite is defined with the describe() function—this is where you describe what you’re testing. Each test spec in the suite is programmed as an it() function, which defines the expected behavior of the code under test and how to test it. Here’s an example:

describe('MyCalculator', () => {   it('should know how to multiply', () => {     // The code that tests multiplication goes here   });   it('should not divide by zero', () => {     // The code that tests division by zero goes here   }); });

Testing frameworks have the notion of an assertion, which is a way of questioning whether an expression is true or false. If the assertion is false, the framework throws an error. In Jasmine, assertions are specified using the function expect(), followed by matchers: toBe(), toEqual(), and so on. It’s as if you’re writing a sentence. “I expect 2+2 to equal 4” looks like this:

expect(2 + 2).toEqual(4);

Matchers implement a Boolean comparison between the actual and expected values. If

the matcher returns true, the spec passes. If you expect a test result not to have a certain value, just add the keyword not before the matcher:

expect(2 + 2).not.toEqual(5);

You can find the complete list of matchers at Jamie Mason’s Jasmine­Matchers page on GitHub: https://github.com/JamieMason/Jasmine­Matchers. We’ve given our test suites the same names as the files under test, adding the suffix .spec to the name, which is a standard practice; for example, application.spec.ts contains the test script for application.ts. The following test suite is from the file application.spec.ts; it tests that the instance of ApplicationComponent is created:

import AppComponent from './app'; describe('AppComponent', () => {   it('is successfully instantiated', () => {     const app = new AppComponent();     expect(app instanceof AppComponent).toEqual(true);   }); });

This is a test suite containing a single test. If you extract the texts from describe() and it() and put them together, you’ll get a sentence that clearly indicates what you’re testing here: “ApplicationComponent is successfully instantiated.”

Note

If other developers need to know what your spec tests, they can read the texts in describe() and it(). Each test should be self­descriptive and serve as program documentation.

The preceding code instantiates AppComponent and expects the expression app instanceof AppComponent to be evaluated to true. From the import statement, you can guess that this test script is located in the same directory as AppComponent.

Where to store test files The Jasmine framework is used to unit test JavaScript applications written in different frameworks or in pure JavaScript. One of the approaches for storing test files is to create a separate test directory and keep only test scripts there, so they aren’t mixed up with the application code. In Angular applications, we prefer to keep each test script in the same directory as the component or service under test. This is convenient for two reasons: All component­related files are located together in the same directory. Typically, we create a directory for storing the component’s .ts, .html, and .css files; adding a .spec file there won’t clutter the directory content. There’s no need to change the configuration of the SystemJS loader, which already knows where the application files are located. It will load the tests from the same locations.

If you want some code to be executed before each test (such as to prepare test dependencies), you can specify it in the setup functions beforeAll() and beforeEach(), which will run before the suite or each spec, respectively. If you want to execute some code right after the suite or each spec is finished, use the teardown functions afterAll() and afterEach().

Tip

If you have a spec with multiple it() tests, and you want the runner to skip some tests, change them from it() to xit().

9.1.1. What to test Now that you have an understanding of how to test, the question is what to test. In Angular applications written in TypeScript, you can test functions, classes, and components: Test functions— Say you have a function that converts the passed string to uppercase. You can write multiple tests just for this function, for cases where the argument is null, an empty string, undefined, a lowercase word, an uppercase word,

a mixed­case word, a number, and so on. Test classes— If you have a class containing several methods (like Product­ Service), you can write a test suite that includes all the tests needed to ensure that each of the class methods functions properly. Test components— You can test the public API of your services or components. In addition to testing them for correctness, we’ll show you code samples that use publicly exposed properties or methods.

9.1.2. How to install Jasmine You can get Jasmine by downloading its standalone distribution, but you’ll install it using npm, as you’ve done for all other packages in this book. The npm repository has several Jasmine­related packages, but you just need jasmine­core. Open the command window in the root of your project, and run the following command:

npm install jasmine­core ­­save­dev

To make sure the TypeScript compiler knows about the Jasmine types, run the following command to install the Jasmine type­definition file:

npm i @types/jasmine ­­save­dev

When your tests are written, you need a test­runner application to run them. Jasmine comes with two runners: one is for the command line (see the npm package jasmine), and the other is HTML­based. You’ll start by using the HTML­based runner, but to run tests from the command line, you’ll use another test runner called Karma. Although Jasmine comes with a preconfigured HTML­based runner as a sample app, you need to create an HTML file to test yours. This HTML file should include the following script tags that load Jasmine:


 jasmine.css">               

Using the standalone Jasmine distribution If you want to see the running Jasmine tests quickly, download the zip file with the standalone version of Jasmine from https://github.com/jasmine/jasmine/releases. Unzip this file, and open SpecRunner.html in your web browser. You’ll see the window shown here.

Testing the sample app that comes with Jasmine

You’ll also need to add all required Angular dependencies, as you did in every index.html file in all the code samples in the book, plus the Angular testing library. You’ll keep using the SystemJS loader, but this time you’ll load the code of the unit tests (the .spec files), which will load the application code via import statements. In this chapter, we’ll show you how to write unit tests. You’ll run them manually using the HTML­based runner first. Then we’ll show you how to use Karma, which can run command­line tests that report possible errors in different browsers. In chapter 10, you’ll integrate Karma into the application build process, so the unit tests will run automatically as part of the build.

9.2. WHAT COMES WITH ANGULAR’S TESTING LIBRARY Angular comes with a testing library that includes the wrappers for Jasmine’s describe(), it(), and xit() functions and also adds such functions as

beforeEach(), async(), fakeAsync(), and others. Because you don’t configure and bootstrap the application during test runs, Angular offers a TestBed helper class that allows you to declare modules, components, providers, and so on. TestBed includes such functions as configureTestingModule(), createComponent(), inject(), and others. For example, the syntax for configuring a testing module looks similar to configuring @NgModule:

beforeEach(() => {     TestBed.configureTestingModule({       imports: [ ReactiveFormsModule, RouterTestingModule,                  RouterTestingModule.withRoutes(routes)],       declarations: [AppComponent, HomeComponent, WeatherComponent],       providers: [{provide: WeatherService, useValue: {} }       ]     })   });

The beforeEach() function is used in test suites during the setup phase. It allows you to specify the required modules, components, and providers that may be needed by each test. The inject() function creates an injector and injects the specified objects into tests, according to the app’s providers configured for Angular DI:

inject([Router, Location], (router: Router, location: Location) => {   // Do something })

The async() function runs in the Zone and may be used with asynchronous services. async() doesn’t complete the test until all of its asynchronous operations have been completed or the specified timeout has passed:

it(' does something', async(inject([AClass], object => {   myPromise.then(() => { expect(true).toEqual(true); }); }), 3000));

The fakeAsync() function lets you speed up the testing of asynchronous services by simulating the passage of time:

The Angular testing library has an NgMatchers interface that includes the following matchers: toBePromise()—Expects the value to be a promise toBeAnInstanceOf()—Expects the value to be an instance of a class toHaveText()—Expects the element to have exactly the given text toHaveCssClass()—Expects the element to have the given CSS class toHaveCssStyle()—Expects the element to have the given CSS styles toImplement()—Expects a class to implement the interface of the given class toContainError()—Expects an exception to contain the given error text toThrowErrorWith()—Expects a function to throw an error with the given error text when executed The Angular Testing API for TypeScript is documented at http://mng.bz/ym8N. We’ll show you how to test services, routers, event emitters, and components later in this chapter, but first let’s go over some basics.

9.2.1. Testing services Typically, Angular services are injected into components; to set up the injectors, you need to define providers for an it() block. Angular offers the beforeEach() setup method, which runs before each it() call. You can inject the service into it() using inject() to test synchronous functions in the service. Real services may need some time to complete, and this may slow your tests. There are two ways to speed up tests: Create a class that implements a mock service by extending a class of the real service that returns hardcoded data quickly. For example, you can create a mock service for WeatherService that returns immediately without making any requests to the remote server that returns actual weather data:

class MockWeatherService implement WeatherService {   getWeather() {     return Observable.empty();   } }

Use the fakeAsync() function, which automatically identifies asynchronous calls and replaces timeouts, callbacks, and promises with immediately executed functions. The tick() function allows you to fast­forward the time, so there’s no need to wait until the timeout expires. You’ll see examples of using fakeAsync() later in this chapter.

9.2.2. Testing navigation with the router To test the router, your spec scripts can invoke such router methods as navigate() and navigateByUrl(). The navigate() method takes an array of configured routes (commands) that will construct the route as an argument, whereas navigateByUrl() takes a string representing the segment of the URL you want to navigate to. If you use navigate(), you specify the configured path and route params, if any. If the router is properly configured, it should update the URL in the address bar of the browser. The next code snippet shows how to programmatically navigate to the product route, pass 0 as a route param, and ensure that after the navigation, the URL (represented by the Location object) has a segment of /product/0:

it('should be able to navigate to product details using commands API',     fakeAsync(inject([Router, Location], (router: Router, location:     

 Location) => {

      TestBed.createComponent(AppComponent);       router.navigate(['/products', 0]);       tick();       expect(location.path()).toBe('/product/0');     })   ));

When you provide an array of values to the router, it’s called a commands API. For the preceding code fragment to work, the route with parameter /products/:productId has to be configured, as explained in chapter 3. The it() function invokes the callback provided as the second argument. fake­

Async() wraps a function provided as an argument (inject() in the previous example) and executes it in the Zone. The tick() function lets you manually fast­ forward the time and advance tasks in the microtasks queue in the browser’s event loop. In other words, you can emulate the time that asynchronous tasks take, and execute asynchronous code synchronously, which simplifies and speeds up execution of the unit tests. With TestBed.createComponent() (explained in the next section), you create an instance of the component. Then you invoke the router’s navigate() method, advance the async tasks that perform the navigation with tick(), and check whether the current location is the same as the expected one. The navigateByUrl() function takes a specific URL segment and should properly build the Location.path that represents the client’s portion in the address bar of the browser. This is what you’ll test:

router.navigateByUrl('/products'); ... expect(location.path()).toBe('/products');

You’ll see how to use navigateByUrl() in section 9.3. While testing the router, you can use SpyLocation, which is a mock for the Location provider. It allows tests to fire simulated location events. For example, you can prepare a specific URL and simulate the change of the hash portion, the browser’s Back and Forward buttons, and more.

9.2.3. Testing components Components are classes with templates. If your class contains methods implementing the application’s logic, you can test them as you would any other functions; but more often you’ll be testing the templates. In particular, you’re interested in testing that the bindings work properly and that they display the expected data. Angular offers the TestBed.createComponent() method, which returns a ComponentFixture object that will be used to work with the component when it’s created. This fixture gives you access to both the component and the native HTML element’s instances, so you can assign values to the component’s properties as well as find specific HTML elements in the component’s template. You can also trigger the change­detection cycle on the component by invoking the

detectChanges() method on the fixture. After the change detection has updated the UI, you can run the expect() function to check the rendered values. The following code snippet illustrates these actions using a ProductComponent that has a product property, assuming that it’s bound to the template element 

:

let fixture = TestBed.createComponent(ProductDetailComponent); let element = fixture.nativeElement; let component = fixture.componentInstance; component.product = {title: 'iPhone 7', price: 700}; fixture.detectChanges(); expect(element.querySelector('h4').innerHTML).toBe('iPhone 7');

Now let’s write a sample application in which you’ll implement unit tests for a component, a router, and a service.

9.3. TESTING A SAMPLE WEATHER APPLICATION Let’s try testing Angular components and services using an application that has a main page with two links: Home and Weather. You’ll use a router to navigate to the Weather page, which is a refactored version of the weather app you created in chapter 5 (observable­events­http.ts). In chapter 5, a large chunk of the code was placed in the constructor of AppComponent, which complicates testing because you can’t invoke the code of the constructor after an object is created. Now WeatherComponent will get the Weather­Service injected, and WeatherService will use the remote server from chapter 5 to get the weather information. Figure 9.2 shows what the window looked like when we ran this application, navigated to the Weather route, and entered New York in the input field. Figure 9.2. Checking the weather component in the test_samples project

Figure 9.3 shows the structure of this project (see the test_weather directory). Note the .spec files, which contain the code for unit­testing the components and the weather service. Figure 9.3. The structure of the test_samples project

To run these tests, create the following test.html file that loads all the spec.ts files

circled in figure 9.3 Listing 9.1. test.html

9.3.1. Configuring SystemJS To use an HTML­based test runner, you need to add Angular testing modules to your SystemJS configuration. Here’s the fragment from the systemjs.config.js file that comes with this project. Listing 9.2. systemjs.config.js fragment

'@angular/common/testing'                  : 'ng:common/bundles/

 common­testing.umd.js',     '@angular/compiler/testing'                : 'ng:compiler/bundles/       compiler­testing.umd.js',     '@angular/core/testing'                    : 'ng:core/bundles/

 core­testing.umd.js',          '@angular/router/testing'                   : 'ng:router/bundles/       router­testing.umd.js',     '@angular/http/testing'                    : 'ng:http/bundles/

      http­testing.umd.js',     '@angular/platform­browser/testing'        : 'ng:platform­browser/

 bundles/platform­browser­testing.umd.js',          '@angular/platform­browser­dynamic/testing': 'ng:platform­browser­dynamic/bundles/platform­browser­dynamic­testing.umd.js',   },   paths: {     'ng:': 'node_modules/@angular/'   },

9.3.2. Testing the weather router The router for this application is configured in the app.routing.ts file. Listing 9.3. app.routing.ts

import { Routes, RouterModule } from '@angular/router'; import { HomeComponent } from './components/home'; import { WeatherComponent } from './components/weather'; export const routes: Routes = [   { path: '',        component: HomeComponent },   { path: 'weather', component: WeatherComponent } ]; export const routing = RouterModule.forRoot(routes);

Although you can configure the routes either in your app module or in a separate file, having the routes configured in a separate file is a best practice. Doing so allows you to reuse the route configuration to run both the application and the test scripts. The script in app.module.ts of the weather app uses the routes constant in the declaration of @NgModule. Listing 9.4. app.module.ts

@NgModule({   imports: [BrowserModule, HttpModule, ReactiveFormsModule, routing],

  declarations: [AppComponent, HomeComponent, WeatherComponent],   bootstrap: [AppComponent],   providers: [     { provide: LocationStrategy,   useClass: HashLocationStrategy },     { provide: WEATHER_URL_BASE,   useValue: 'http://api.openweathermap.org/       data/2.5/weather?q=' },     { provide: WEATHER_URL_SUFFIX, useValue:

 '&units=imperial&appid=ca3f6d6ca3973a518834983d0b318f73' },          WeatherService   ] })

The test script for the routes is located in the app.spec.ts file, and it reuses the same routes constant. Listing 9.5. app.spec.ts

Note that you import ReactiveFormsModule because WeatherComponent uses the Forms API.

Note

Don’t unit­test third­party code in your app. In listing 9.5, you use an empty object as a provider for WeatherService, which in the real app makes calls to a remote weather server. What if that remote server is down when you run your test specs? Unit tests should assert that your scripts work fine, not someone else’s software. That’s why you

don’t use an actual WebService and use an empty object instead.

When testing the client­side navigation of your application, you’ll use the Router class and its navigate() and navigateByUrl() methods. Listing 9.5 illustrates the use of both the navigate() and the navigateByUrl() methods for testing that the programmatic navigation properly updates the address bar of the application. But because you don’t run that app during the test, there’s no browser address bar, so it has to be emulated. That’s why instead of RouterModule, you use RouterTestingModule, which knows how to check the expected content of the address bar using the Location class. Now let’s look at testing the injection of services. As a matter of fact, you’ve already been injecting services while testing routes:

fakeAsync(inject([Router, Location],...))

But in the next section, we’ll show you a different way to initialize the required services: you’ll get the Injector object and invoke its get() method.

9.3.3. Testing the weather service The WeatherService class encapsulates communications with the weather server. Listing 9.6. weather.service.ts

import {Inject, Injectable, OpaqueToken} from '@angular/core'; import {Http, Response} from '@angular/http'; import {Observable} from 'rxjs/Observable'; import 'rxjs/add/operator/filter'; import 'rxjs/add/operator/map'; export const WEATHER_URL_BASE = new OpaqueToken('WeatherUrlBase'); export const WEATHER_URL_SUFFIX = new OpaqueToken('WeatherUrlSuffix'); export interface WeatherResult {   place: string;   temperature: number;   humidity: number; } @Injectable() export class WeatherService {

  constructor(       private http: Http,       @Inject(WEATHER_URL_BASE) private urlBase: string,       @Inject(WEATHER_URL_SUFFIX) private urlSuffix: string) {   }   getWeather(city: string): Observable {     return this.http         .get(this.urlBase + city + this.urlSuffix)         .map((response: Response) => response.json())         .filter(this._hasResult)         .map(this._parseData);   }   private _hasResult(data): boolean {     return data['cod'] !== '404' && data.main;   }   private _parseData(data): WeatherResult {     let [first,] = data.list;     return {      place: data.name || 'unknown',       temperature: data.main.temp,       humidity: data.main.humidity     };   } }

Note the use of the OpaqueToken type mentioned in chapter 4. You use it twice to inject into urlBase and urlSuffix the values provided in the @NgModule decorator. Using dependency injection for urlBase and urlSuffix makes it simpler to replace the real weather server with a mock, if need be. The getWeather() method in listing 9.6 forms the URL for the HTTP get() by concatenating urlBase, city, and urlSuffix. The result is processed by map(), filter(), and another map() so the observable will emit objects of type WeatherResult.

Note

You aren’t testing the _hasResult() and _parseData() methods because private methods can’t be unit­tested. Should you decide to test them, change their access level to public.

To test WeatherService, you’ll use the MockBackend class, which is one of Angular’s implementations of the Http object. MockBackend doesn’t make any HTTP requests but intercepts them and allows you to create and return hardcoded data in the format of the expected result. Before each test, you’ll get a reference to the Injector object, which will get you the new instances of MockBackend and WeatherService. The testing code for the WeatherService is located in the weather.service.spec.ts file. Listing 9.7. weather.service.spec.ts

These are the main takeaways from testing services injection: Prepare the providers. If you’re using services that make requests to external servers, mock them up. We’ve shown you how to test the navigation and services; now let’s look at how you can test an Angular component.

9.3.4. Testing the weather component WeatherService is injected into WeatherComponent (weather.ts) via constructor, where you subscribe to the observable’s messages coming from the WeatherService. When the user starts entering the name of a city in the UI, the getWeather() method is invoked, and the returned weather data is displayed in the template via binding. Listing 9.8. weather.ts

Listing 9.8. weather.ts

import {Component} from '@angular/core'; import {FormControl} from '@angular/forms'; import 'rxjs/add/operator/debounceTime'; import 'rxjs/add/operator/switchMap'; import {WeatherService, WeatherResult} from '../services/weather.service'; @Component({   selector: 'my­weather',   template: `     

Weather

         

Current weather in {{weather?.place}}:

    
          
  • Temperature: {{weather?.temperature}}F
  •       
  • Humidity: {{weather?.humidity}}%
  •     
  `, }) export class WeatherComponent {   searchInput: FormControl;   weather: WeatherResult;   constructor(weatherService: WeatherService) {     this.searchInput = new FormControl('');     this.searchInput.valueChanges         .debounceTime(300)         .switchMap((place: string) => weatherService.getWeather(place))         .subscribe(             (wthr: WeatherResult) => this.weather = wthr,             error => console.error(error),             () => console.log('Weather is retrieved'));   } }

You want to write a test to check that when the weather property gets the values, the template is properly updated via bindings. You also want to test that when the value of the searchInput object changes, the observable emits data via its valueChanges property.

The Elvis operator The template of WeatherComponent includes expressions with question marks, such as weather?.place. The question mark in this context is called the Elvis operator.

The weather property is populated asynchronously, and if weather is null by the time the expression is evaluated, the weather.place expression would throw an error. To suppress null dereferencing, you use the Elvis operator to short­circuit further evaluation if weather is null. The Elvis operator offers an explicit notation to show which values could be null.

The test suite will contain one test to check that data bindings work as expected. Test­ Bed.createComponent(WeatherComponent); will create a ComponentFixture that contains references to the WeatherComponent as well as the DOM object that represents this component. In listing 9.8, you use the weather property for bindings; you’ll initialize this property with the object literal containing hardcoded values for place, humidity, and temperature. After that, you’ll force change­detection by invoking the detectChanges() method on the instance of ComponentFixture. You expect to see the values from weather in one 

 and two 
  •  tags in the component’s template. The code for this test is located in the weather.spec.ts file. Listing 9.9. weather.spec.ts

    Tip

    Listing 9.9 uses an empty object to mock WeatherService because you don’t plan to invoke any methods on it. You could define the mock service as class MockWeatherService implements WeatherService and provide implementations for the real methods, but they would return hardcoded values. When defining a mock service in real­world applications, it’s advisable to create classes that implement the interfaces of the real services.

    Note

    In chapter 7, you learned two approaches for creating forms in Angular. Although the template­driven approach requires little coding, using reactive forms makes them more testable without requiring the DOM object.

    Running tests in the HTML-based runner

    Let’s run the test suite for the weather application in the HTML­based runner. Just run the live­server, and enter the URL http://localhost:8080/test.html in the browser. All the tests should pass, and the browser window should look like figure 9.4. Figure 9.4. The test suite passed.

    When you write tests, you want to see how they fail. Let’s make one of the tests fail to see how it’s reported. Change the temperature to 58 degrees on the line where you initialize the weather property:

    component.weather = {place: 'New York', humidity: 44, temperature: 58};

    The test still expects the UI to render the temperature as 57 degrees:

    expect(element.querySelector('li:nth­of­type(1)').innerHTML)

     .toBe('Temperature: 57F');

    The test output shown in figure 9.5 reports the failure of one of five tests. Figure 9.5. The test suite failed.

    Manually running tests in the web browser isn’t the best way to unit­test your code. You want a testing process that can be run as a script from a command line, so it can be integrated into the automated build process. Jasmine has a runner that can be used from a command prompt, but we prefer to use an independent test runner, Karma, that can work with a variety of unit­test frameworks. We’ll show you how to use the Karma runner next.

    9.4. RUNNING TESTS WITH KARMA Karma (https://karma­runner.github.io) is a test runner that was originally created by the AngularJS team, but it’s being used to test JavaScript code written with or without frameworks. Karma is built using Node.js, and although it doesn’t run in a web browser, it can run tests to check whether your application will work in multiple browsers (you’ll run tests for Chrome and Firefox). For the Weather application, you’ll install Karma and the plugins for Jasmine, Chrome, and Firefox, and save them in the devDependencies section of package.json:

    npm install karma karma­jasmine karma­chrome­launcher karma­firefox­

     launcher ­­save­dev

    To run Karma, configure the npm test command of the project as follows:

    "scripts": {   "test": "karma start karma.conf.js" }

    Note

    Note

    The karma executable is a binary file located in the node_modules/.bin directory.

    You’ll also create a small karma.conf.js configuration file to let Karma know about the project. This file is located in the root directory of the project and includes paths for Angular files as well as configuration options for the Karma runner. Listing 9.10. karma.conf.js

    Most of listing 9.10 lists the paths where required files are located. Karma generates a temporary HTML page that includes the files listed with included: true. Files listed with included: false will be dynamically loaded at runtime. All the Angular files,

    including the testing ones, are loaded dynamically by SystemJS. You’ll need to add one more file to the project: karma­test­runner.js. It’s the script that actually runs the tests. Listing 9.11. karma-test-runner.js

    Now you’re ready to run your tests using the command npm test from the command line. During the run, Karma will open and close each configured browser and print the test results, as shown in figure 9.6. Figure 9.6. Testing the weather application with Karma

    Developers tend to use the latest versions of the browser that has the best dev tools, which is Google Chrome. We’ve seen real­world projects in which a developer demos the application running perfectly in Chrome, and then users complain about an error in Safari. Make sure the development process uses Karma and tests the application against all browsers. Before giving your application to the QA team or showing it to your manager, make sure your Karma runner doesn’t report any errors in all the required browsers. Now that we’ve given you an overview of writing and running unit tests, let’s implement tests for the online auction.

    9.5. HANDS-ON: UNIT-TESTING THE ONLINE AUCTION The goal of this exercise is to show you how to unit­test selected modules of the online auction application. In particular, you’ll add unit tests for ApplicationComponent, StarsComponent, and ProductService. You’ll run the tests using Jasmine’s HTML­based runner and then Karma.

    Note

    We’re going to use the auction application from chapter 8 as a starting point, so copy it to a separate directory and follow the instructions in this section. If you prefer to review the code instead of typing it, use the code that comes with chapter 9, and run the tests.

    Install Jasmine, Karma, the type­definition files for Jasmine, and all Angular dependencies by running the following commands:

    npm install jasmine­core karma karma­jasmine karma­chrome­launcher karma­

     firefox­launcher ­­save­dev npm install @types/jasmine ­­save­dev npm install

    In the client directory, create a new auction­unit­tests.html file for loading Jasmine tests. Listing 9.12. auction-unit-tests.html

      [TEST] Online Auction                  
     jasmine.css">         

     

      

                                            



    The content of this file is similar to test.html from the weather application. The only difference is that you’re loading different spec files here: application.spec, stars.spec, and product­service.spec.

    9.5.1. Testing ApplicationComponent Create an application.spec.ts file in the client/app/components/application directory to test that ApplicationComponent is successfully instantiated. This isn’t an overly useful test, but it can serve as an illustration of testing whether an instance of a TypeScript class (not even one related to Angular) was successfully created. Listing 9.13. application.spec.ts

    import ApplicationComponent from './application'; describe('ApplicationComponent', () => {   it('is successfully instantiated', () => {     const app = new ApplicationComponent();

        expect(app instanceof ApplicationComponent).toEqual(true);   }); });

    9.5.2. Testing ProductService To test ProductService, create a product­service.spec.ts file in the app/services directory. In this spec, you’ll test the HTTP service, and although the it() function is rather small, there will be lots of preparation before the test runs. Listing 9.14. product-service.spec.ts

    First you create an object literal, mockProduct = {id: 1}, which is used to emulate the data that could come from the server as an HTTP response. You want mockBackend to mock and return an object with hardcoded values for each HTTP request. You could have created an instance of Product with more properties, but for this simple test, having just the ID suffices.

    9.5.3. Testing StarsComponent For the last test, we picked StarsComponent because it demonstrates how you can test a component’s properties and the event emitter. StarsComponent loads its HTML from a file that requires special processing during testing. Angular loads the files specified in templateUrl asynchronously and performs just­in­time compilation on them. You’ll need to do the same in the test spec by invoking TestBed.compileComponents(). This step is required for any component that uses the templateUrl property. In the client/app/components/stars directory, create a stars.spec.ts file with the following content. Listing 9.15. stars.spec.ts

    TestBed creates a new instance of StarsComponent (you don’t use the injected one here) and gives you a fixture with references to the component and the native element. To test that all the stars are empty, you assign 0 to the input rating property on the component instance. The rating property is actually a setter on StarsComponent that modifies both rating and the stars array:

    set rating(value: number) {   this._rating = value || 0;   this.stars = Array(this.maxStars).fill(true, 0, this.rating); }

    Then you start the change­detection cycle, which forces the *ngFor loop to re­render the star images in the template of the StarsComponent:

      
            class="starrating glyphicon glyphicon­star"         [class.glyphicon­star­empty]="!star"         (click)="fillStarsWithColor(i)">      {{rating | number:'.0­2'}} stars



    The CSS for the filled stars is starrating glyphicon glyphicon­star. The empty stars have an additional CSS class, glyphicon­star­empty. The test 'all stars are empty' uses the glyphicon­star­empty selector and expects that there are exactly five native elements with this class. The test 'all stars are filled' assigns the rating 5. It uses the CSS selector .glyphicon­star:not(.glyphicon­star­empty), which uses the not operator to ensure that the stars aren’t empty. The test 'emits rating change event when readonly is false' uses the injected component. There you subscribe to the ratingChange event, expecting the values of the rating to be 3. When a user wants to change the rating, they click the third star (while leaving a review), which invokes the fillStarsWithColor method on the component, passing 3 as the index argument:

    fillStarsWithColor(index) {   if (!this.readonly) {     this.rating = index + 1; // to prevent zero rating     this.ratingChange.emit(this.rating);   } }

    Because there’s no user to do the clicking during the unit testing, you invoke this method programmatically:

    component.readonly = false;  component.fillStarsWithColor(2);

    If you want to see how this test fails, change the argument of fillStarsWithColor() to any number other than 2.

    Order of operations in testing events The code of the test 'emits rating change event when readonly is false' may

    make you wonder why you write the preceding two lines at the end of the test after subscribe() is invoked. The subscription to Observable is lazy, and it’ll receive the next element only after the fillStarsWithColor(2) method is invoked, emitting the event. If you move the subscribe() method down, the event will be emitted before the subscriber is created, and the test will fail on timeout because the done() method will never be invoked.

    9.5.4. Running the tests To run the tests, first recompile the server’s code by running npm run tsc. Then start the auction application by entering npm start on the console. It’ll start the Node server on port 8000. Enter http://localhost:8000/auction­unit­ tests.html in the browser, and the tests should run, producing the output shown in figure 9.7. Figure 9.7. Testing the online auction with the HTML-based runner

    To run the same tests using Karma, copy the files karma.conf and karma­test­runner from chapter 9’s auction directory into the root directory of your project. (These files were explained in section 9.4.) Run npm test, and you should see the output shown in figure 9.8. Figure 9.8. Testing the auction with Karma

    9.6. SUMMARY We can’t overstate the importance of running unit tests in your Angular applications. Unit tests let you ensure that each component or service of your application works as expected. In this chapter, we’ve demonstrated how to write unit tests using the Jasmine framework and how to run them with either Jasmine or Karma. These are the main takeaways for this chapter:

    A component or a service is a good candidate for writing a test suite.

    Although you can keep all test files separate from your application, it’s more convenient to keep them next to the components they test. Unit tests run quickly, and most application business logic should be tested with unit tests. While you’re writing tests, make them fail to see that their failure report is easy to understand. If you decide to implement end­to­end testing, don’t rerun unit tests during this process.

    Running unit tests should be part of your automated build process. We’ll show you how to do that in chapter 10.

    Playlists

    Chapter 10. Bundling and deploying applications with Webpack

    History

    Topics This chapter covers TutorialsBundling apps for deployment using Webpack

    Configuring Webpack for bundling Angular apps in dev and prod

    Offers & Deals

    Integrating the Karma test runner into the automated build process Highlights

    Creating a prod build for the online auction

    SettingsAutomating project generation and bundling with Anguar CLI

    Over the course of this book, you’ve written and deployed multiple versions of the

    Support

    online auction and lots of smaller applications. Web servers properly served your

    Signapplications to the user. So why not just copy all the application files to the production Out

    server, run npm install, and be done with deployment? No matter which programming language or framework you use, you’ll want to achieve two goals: The deployed web application should be small in size (so it can load faster).

    The browser should make a minimal number of requests to the server on startup (so it can load faster). When a browser makes requests to the server, it gets HTML documents, which may include additional files like CSS, images, videos, and so on. Let’s take the online auction application as an example. On startup, it makes hundreds of requests to the server just to load Angular with its dependencies and the TypeScript compiler, which weigh 5.5 MB combined. Add to this the code you wrote, which is a couple of dozen HTML, TypeScript, and CSS files, let alone images! It’s a lot of code to download, and way too many server requests for such a small application. Look at figure 10.1, which shows the content of the Network tab in Chrome Developer Tools after the auction was loaded into our browser: there are lots of network requests, and the app size is huge. Figure 10.1. Monitoring the development version of the online auction application

    Figure 10.1. Monitoring the development version of the online auction application

    Real­world applications consist of hundreds or even thousands of files, and you want to minimize, optimize, and bundle them together during deployment. In addition, for production, you can precompile the code into JavaScript, so you don’t need to load the 3 MB TypeScript compiler in the browser. Several popular tools are used to deploy JavaScript web applications. All of them run using Node and are available as npm packages. These tools fit into two main categories: Task runners Module loaders and bundlers Grunt (http://gruntjs.com) and Gulp (http://gulpjs.com) are widely used general­purpose task runners. They know nothing about JavaScript applications, but they allow you to configure and run the tasks required for deploying the applications. Grunt and Gulp aren’t easy to maintain for build processes, because their configuration files are several hundred lines long. In this book, you used npm scripts for running tasks, and a task is a script or a binary file that can be executed from a command line. Configuring npm scripts is simpler than using Grunt and Gulp, and you’ll continue using them in this chapter. If the complexity of your project increases and the number of npm scripts becomes unmanageable, consider using Grunt or Gulp to run builds. So far you’ve used SystemJS to load modules. Browserify (http://browserify.org),

    Webpack (http://webpack.github.io), Broccoli (www.npmjs.com/package/broccoli­system­ builder), and Rollup (http://rollupjs.org) are all popular bundlers. Each of them can create code bundles to be consumed by the browsers. The simplest is Webpack, which allows you to convert and combine all your application assets into bundles with minimal configuration. A concise comparison of various bundlers is available on the Webpack site: http://mng.bz/136m. The Webpack bundler was created specifically for web applications running in a browser, and many typical tasks required for preparing web application builds are supported out of the box with minimal configuration and without the need to install additional plugins. This chapter starts by introducing Webpack, and then you’ll prepare two separate builds (dev and prod) for the online auction. Finally, you’ll run an optimized version of the online auction and compare the size of the application with what’s shown in figure 10.1. You won’t use SystemJS in this chapter—Webpack will invoke the TypeScript compiler while bundling the apps. The compilation will be controlled by a special loader that uses tsc internally to transpile TypeScript into JavaScript.

    Note

    The Angular team has created Angular CLI (https://github.com/angular/angular­cli), which is a command­line interface for automating an application’s creation, testing, and deployment. Angular CLI uses the Webpack bundler internally. We’ll introduce Angular CLI later in this chapter.

    10.1. GETTING TO KNOW WEBPACK While preparing for a trip, you may pack dozens of items into a couple of suitcases. Savvy travelers use special vacuum­seal bags that allow them to squeeze even more clothes into the same suitcase. Webpack is an equivalent tool. It’s a module loader and a bundler that lets you group your application files into bundles; you can also optimize their sizes to fit more into the same bundle. For example, you can prepare two bundles for deployment: all your application files are merged into one bundle, and all required third­party frameworks and libraries are in another. With Webpack, you can prepare separate bundles for development and production deployment, as shown in figure 10.2. In dev mode, you’ll create the bundles

    in memory, whereas in production mode Webpack will generate actual files on disk. Figure 10.2. Dev and prod deployments

    It’s convenient to write an application as a set of small modules where one file is one module, but for deployment you’ll need a tool to pack all of these files into a small number of bundles. This tool should know how to build the module dependencies tree, sparing you from manually maintaining the order of the loaded modules. Webpack is such a tool, and in the Webpack philosophy, everything can be a module, including CSS, images, and HTML. The process of deployment with Webpack consists of two major steps: 1.  Build the bundles (this step can include code optimization). 2.  Copy the bundles to the desired server. Webpack is distributed as an npm package, and, like all tools, you can install it either globally or locally. Let’s start by installing Webpack globally:

    npm install webpack ­g

    Note

    This chapter uses Webpack 2.1.0, which at the time of this writing is in beta. To install it globally, we used the command npm i [email protected]­beta.25 ­g.

    A bit later, you’ll install Webpack locally by adding it into the devDependencies section of the package.json file. But installing it globally will let you quickly see the

    process of turning an application into a bundle.

    Tip

    A curated list of Webpack resources (documentation, videos, libraries, and so on) is available on GitHub. Take a look at awesome­webpack: https://github.com/d3viant0ne/awesome­webpack.

    10.1.1. Hello World with Webpack Let’s get familiar with Webpack via a very basic Hello World example consisting of two files: index.html and main.js. Here’s the index.html file. Listing 10.1. index.html

      

    The main.js file is even shorter. Listing 10.2. main.js

    document.write('Hello World!');

    Open a command­prompt window in the directory where these file are located, and run the following command:

    webpack main.js bundle.js

    The main.js file is a source file, and bundle.js is the output file in the same directory. We usually include the word bundle in the output filename. Figure 10.3 shows the result of running the preceding command. Figure 10.3. Creating the first bundle

    Note that the size of the generated bundle.js is larger than that of main.js because Webpack didn’t just copy one file into another but added other code required by this bundler. Creating a bundle from a single file isn’t overly useful, because it increases the file size; but in a multi­file application, bundling files together makes sense. You’ll see this as you read this chapter. Now you need to modify the 

    The main.js JavaScript file remains the same:

    document.write('Hello World!');

    The package.json file in the hello­world­devserver project looks like this. Listing 10.5. hello-world-devserver/package.json

    {   "name": "first­project",   "version": "1.0.0",   "description": "",   "main": "main.js",   "scripts": {     "start": "webpack­dev­server"   },   "keywords": [],   "author": "",   "license": "ISC",   "devDependencies": {     "webpack": "^2.1.0­beta.25",     "webpack­dev­server": "^2.1.0­beta.0"

      } }

    Note that you’ve configured the npm start command for running the local webpack­ dev­server.

    Note

    When you serve your application with webpack­dev­server, it’ll run on the default port 8080 and will generate the bundles in memory without saving them in a file. Then webpack­dev­server will recompile and serve the new versions of the bundles every time you modify the code.

    You can add the configuration section of webpack­dev­server in the devServer section of the webpack.config.js file. There you can put any options that webpack­dev­server allows on the command line (see the Webpack product documentation at http://mng.bz/gn4r). This is how you could specify that the files should be served from the current directory:

    devServer: {   contentBase: '.' }

    The complete configuration file for the hello­world­devserver project is shown here and can be reused by both the webpack and webpack­dev­server commands. Listing 10.6. hello-world-devserver/webpack.config.js

    const path = require('path'); module.exports = {   entry: {     'main': './main.js'   },   output: {     path: './dist',     filename: 'bundle.js'   },   watch: true,   devServer: {     contentBase: '.'   }



    In listing 10.6, two of the options are needed only if you’re planning to run the webpack command in watch mode and generate output files on disk: The Node module path resolves relative paths in your project (in this case, it specifies the ./dist directory). watch: true starts Webpack in watch mode. If you run the webpack­dev­server command, the preceding two options aren’t used. webpack­dev­server always runs in watch mode, doesn’t output files on disk, and builds bundles in memory. The contentBase property lets webpack­dev­ server know where your index.html file is located. Let’s try to run the Hello World application by serving the application with webpack­ dev­server. In the command window, run npm start to start webpack­dev­server. On the console, webpack­dev­server will log the output, which starts with the URL you can use with the browser, which by default is http://localhost:8080. Open your browser to this URL, and you’ll see a window that displays the message “Hello World”. Modify the text of the message in main.js: Webpack will automatically rebuild the bundle, and the server will reload the fresh content. Resolving filenames

    This is all good, but you’ve been writing code in TypeScript, which means you need to let Webpack know that your modules can be located not only in .js files, but in .ts files as well. In the webpack.config.js file in listing 10.6, you specified the filename with the extension: main.js. But you can specify just the filenames without any extensions as long webpack.config.js has the resolve section. The following code snippet shows how you can let Webpack know that your modules can be located in files with extension .js or .ts:

    resolve: {   extensions: ['.js', '.ts'] }

    The TypeScript files also have to be preprocessed (transpiled). You need to tell Webpack to transpile your application’s .ts files into .js files before creating bundles; you’ll see how to do that in the next section.

    Usually, build­automation tools provide you with a way to specify additional tasks that need to be performed during the build process. Webpack offers loaders and plugins that allow you to customize builds.

    10.1.2. How to use loaders Loaders are transformers that take a source file as input and produce another file as output (in memory or on disk), one at a time. A loader is a small JavaScript module with an exported function that performs a certain transformation. For example, json­ loader takes an input file and parses it as JSON. base64­loader converts its input into a base64­encoded string. Loaders play a role similar to tasks in other build tools. Some loaders are included with Webpack so you don’t need to install them separately; others can be installed from public repositories. Check the list of loaders in the Webpack docs on GitHub (http://mng.bz/U0Yv) to see how to install and use the loaders you need. In essence, a loader is a function written in Node­compatible JavaScript. To use a loader that’s not included with the Webpack distribution, you’ll need to install it using npm and include it in your project’s package.json file. You can either manually add the required loader to the devDependencies section of package.json or run the npm install command with the ­­save­dev option. In the case of ts­loader, the command would look like this:

    npm install ts­loader ­­save­dev

    Loaders are listed in the webpack.config.js file in the module section. For example, you can add the ts­loader as follows:

    module: {   loaders: [     {       test: /\.ts$/,       exclude: /node_modules/,       loader: 'ts­loader'     },   ] }

    This configuration tells Webpack to check (test) each filename and, if it matches the regular expression \.ts$, to preprocess it with ts­loader. In the syntax of regular expressions, the dollar sign at the end indicates that you’re interested only in files

    having names that end with .ts. Because you don’t want to include Angular’s .ts files in the bundle, you exclude the node_modules directory. You can either reference loaders by their full name (such as ts­loader), or by their shorthand name, omitting the ­ loader suffix (for example, ts). If you use relative paths in your templates (for example, template: "./home.html"), you need to use angular2­template­ loader.

    Note

    The SystemJS loader isn’t used in any of the projects presented in this chapter. Webpack loads and transforms all project files using one or more loaders configured in webpack.config.js based on the file type.

    Using loaders for HTML and CSS files

    In the previous chapters, Angular components that stored HTML and CSS in separate files were specified in the @Component annotation as templateUrl and styleUrls, respectively. Here’s an example:

    @Component({   selector: 'my­home',   styleUrls: ['app/components/home.css')],   templateUrl: 'app/components/home.html' })

    We usually keep the HTML and CSS files in the same directory where the component code is located. Can you specify the path relative to the current directory? Webpack allows you to do this:

    @Component({   selector: 'my­home',   styles: [home.css'],   templateUrl: 'home.html' })

    While creating bundles, Webpack automatically adds the require() statements for loading CSS and HTML files, replacing the preceding code with the following:

    @Component({   selector: 'my­home',   styles: [require('./home.css')],   templateUrl: require('./home.html') })

    Then Webpack checks every require() statement and replaces it with the content of the required file, applying the loaders specified for the respective file types. The require() statement used here isn’t the one from CommonJS: it’s the internal Webpack function that makes Webpack aware that these files are dependencies. Webpack’s require() not only loads the files, but also can reload them when modified (if you run it in watch mode or use webpack­dev­server).

    Relative paths in templates with SystemJS It’s great that Webpack supports relative paths. But what if you want to be able to load the same app with either SystemJS or Webpack? By default, in Angular you have to use the full path to external files, starting from the app root directory. This would require code changes if you decided to move the component into a different directory. But if you use SystemJS and keep the component’s code and its HTML and CSS files in the same directory, you can use a special moduleId property. If you assign to this property a special __moduleName binding, SystemJS will load files relative to the current module without the need to specify the full path:

    declare var __moduleName: string; @Component({   selector: 'my­home',   moduleId:__moduleName,   templateUrl: './home.html',   styleUrls: ['./home.css'] })

    You can read more about relative paths in the Angular documentation in the “Component­Relative Paths” section at http://mng.bz/47w0.

    In dev mode, for HTML processing, you’ll use raw­loader, which transforms .html

    files into strings. To install this loader and save it in the devDependencies section of package.json, run the following command:

    npm install raw­loader ­­save­dev

    In prod, you’ll use html­loader, which removes extra spaces, newline characters, and comments from HTML files:

    npm install html­loader ­­save­dev

    For CSS processing, you use the loaders css­loader and style­loader; during the building process, all related CSS files will be inlined. css­loader parses CSS files and minifies the styles. style­loader inserts CSS as a