© Adam Freeman 2019
A. FreemanEssential TypeScripthttps://doi.org/10.1007/978-1-4842-4979-6_17

17. Creating an Angular App, Part 1

Adam Freeman1 
(1)
London, UK
 
In this chapter, I start the process of creating an Angular web application that has the same set of features as the example in Chapters 15 and 16. Unlike other frameworks, where using TypeScript is an option, Angular puts TypeScript at the heart of web application development and relies on its features, especially decorators. For quick reference, Table 17-1 lists the TypeScript compiler options used in this chapter.
Table 17-1.

The TypeScript Compiler Options Used in This Chapter

Name

Description

baseUrl

This option specifies the root location used to resolve module dependencies.

declaration

This option produces type declaration files when enabled, which describe the types for use in other projects.

emitDecoratorMetadata

This option determines whether decorator metadata is produced in the JavaScript code emitted by the compiler.

experimentalDecorators

This option determines whether decorators are enabled.

importHelpers

This option determines whether helper code is added to the JavaScript to reduce the amount of code that is produced overall.

lib

This option selects the type declaration files the compiler uses.

module

This option determines the style of module that is used.

moduleResolution

This option specifies how modules are resolved.

outDir

This option specifies the directory in which the JavaScript files will be placed.

sourceMap

This option determines whether the compiler generates source maps for debugging.

target

This option specifies the version of the JavaScript language that the compiler will target in its output.

typeRoots

This option specifies the root location that the compiler uses to look for declaration files.

Preparing for This Chapter

Angular projects are most easily created using the angular-cli package. Open a command prompt and run the command shown in Listing 17-1 to install the angular-cli package.

Tip

You can download the example project for this chapter—and for all the other chapters in this book—from https://github.com/Apress/essential-typescript .

npm install --global @angular/[email protected]
Listing 17-1.

Installing the Project Creation Package

The Angular package names are prefixed with @. Once you have installed the package, navigate to a convenient location and run the command shown in Listing 17-2 to create a new Angular project.
ng new angularapp
Listing 17-2.

Creating a New Project

The Angular development tools are used through the ng command, and ng new creates a new project. During the setup process, you will be asked to make choices about the way the new project is configured. Use the answers from Table 17-2 to prepare the example project for this chapter.
Table 17-2.

The Project Setup Questions and Answers

Question

Answer

Would you like to add Angular routing?

Yes

Which stylesheet format would you like to use?

CSS

It can take a few minutes for the project to be created because a large number of JavaScript packages must be downloaded.

Configuring the Web Service

Once the creation process is complete, run the commands shown in Listing 17-3 to navigate to the project folder and add the packages that will provide the web service and allow multiple packages to be started with a single command.
cd angularapp
npm install --save-dev [email protected]
npm install --save-dev [email protected]
Listing 17-3.

Adding Packages to the Project

To provide the data for the web service, add a file called data.js to the angularapp folder with the content shown in Listing 17-4.
module.exports = function () {
    return {
        products: [
            { id: 1, name: "Kayak", category: "Watersports",
                description: "A boat for one person", price: 275 },
            { id: 2, name: "Lifejacket", category: "Watersports",
                description: "Protective and fashionable", price: 48.95 },
            { id: 3, name: "Soccer Ball", category: "Soccer",
                description: "FIFA-approved size and weight", price: 19.50 },
            { id: 4, name: "Corner Flags", category: "Soccer",
                description: "Give your playing field a professional touch",
                price: 34.95 },
            { id: 5, name: "Stadium", category: "Soccer",
                description: "Flat-packed 35,000-seat stadium", price: 79500 },
            { id: 6, name: "Thinking Cap", category: "Chess",
                description: "Improve brain efficiency by 75%", price: 16 },
            { id: 7, name: "Unsteady Chair", category: "Chess",
                description: "Secretly give your opponent a disadvantage",
                price: 29.95 },
            { id: 8, name: "Human Chess Board", category: "Chess",
                description: "A fun game for the family", price: 75 },
            { id: 9, name: "Bling Bling King", category: "Chess",
                description: "Gold-plated, diamond-studded King", price: 1200 }
        ],
        orders: []
    }
}
Listing 17-4.

The Contents of the data.js File in the angularapp Folder

Update the scripts section of the package.json file to configure the development tools so that the Angular toolchain and the web service are started at the same time, as shown in Listing 17-5.
...
"scripts": {
  "ng": "ng",
  "json": "json-server data.js -p 4600",
  "serve": "ng serve",
  "start": "npm-run-all -p serve json",
  "build": "ng build",
  "test": "ng test",
  "lint": "ng lint",
  "e2e": "ng e2e"
},
...
Listing 17-5.

Configuring Tools in the package.json File in the angularapp Folder

These entries allow both the web service that will provide the data and the Angular development tools to be started with a single command.

Configuring the Bootstrap CSS Package

Use the command prompt to run the command shown in Listing 17-6 in the angularapp folder to add the Bootstrap CSS framework to the project.
npm install [email protected]
Listing 17-6.

Adding the CSS Package

The Angular development tools require a configuration change to incorporate the Bootstrap CSS stylesheet in the application. Open the angular.json file in the angularapp folder and add the item shown in Listing 17-7 to the build/styles section.
...
"build": {
    "builder": "@angular-devkit/build-angular:browser",
    "options": {
    "outputPath": "dist/angularapp",
    "index": "src/index.html",
    "main": "src/main.ts",
    "polyfills": "src/polyfills.ts",
    "tsConfig": "src/tsconfig.app.json",
    "assets": [
        "src/favicon.ico",
        "src/assets"
    ],
    "styles": [
        "src/styles.css",
         "node_modules/bootstrap/dist/css/bootstrap.min.css"
    ],
    "scripts": [],
    "es5BrowserSupport": true
    },
...
Listing 17-7.

Adding a Stylesheet in the angular.json File in the angularapp Folder

Caution

There are two styles settings in the angular.json file, and you must take care to change the one in the build section and not the test section. If you don’t see styled content when you run the example application, the likely cause is that you have edited the wrong section.

Starting the Example Application

Use the command prompt to run the command shown in Listing 17-8 in the angularapp folder
npm start
Listing 17-8.

Starting the Development Tools

The Angular development tools take a moment to start and perform the initial compilation, producing output like this:
...
** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **
 12% building 18/19 modules 1 active ...bpackhot sync nonrecu
Hash: 86ec9c0a3ba22f1ee55c
Time: 11370ms
chunk {es2015-polyfills} es2015-polyfills.js, es2015-polyfills.js.map (es2015-polyfills) 285 kB [initial] [rendered]
chunk {main} main.js, main.js.map (main) 12.1 kB [initial] [rendered]
chunk {polyfills} polyfills.js, polyfills.js.map (polyfills) 236 kB [initial] [rendered]
chunk {runtime} runtime.js, runtime.js.map (runtime) 6.08 kB [entry] [rendered]
chunk {styles} styles.js, styles.js.map (styles) 1.13 MB [initial] [rendered]
chunk {vendor} vendor.js, vendor.js.map (vendor) 4.56 MB [initial] [rendered]
wdm: Compiled successfully.
...
Once the initial compilation has been completed, open a browser window and navigate to http://localhost:4200 to see the placeholder content created by the command in Listing 17-2 and which is shown in Figure 17-1.
../images/481342_1_En_17_Chapter/481342_1_En_17_Fig1_HTML.jpg
Figure 17-1.

Running the example application

Understanding TypeScript in Angular Development

Angular depends on TypeScript decorators, shown in Chapter 15, to describe the different building blocks used to create web applications. Look at the contents of the app.module.ts file in the src/app folder, and you will see one of the modules that Angular relies on:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, AppRoutingModule],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
Decorators are so important in Angular development that they are applied to classes that contain few or even no members, just to help define or configure the application. This is the NgModule decorator, and it is used to describe a group of related features in the Angular application (Angular modules exist alongside conventional JavaScript modules, which is why this file contains both import statements and the NgModule decorator). Another example can be seen in the app.component.ts file in the src/app folder.
import { Component } from '@angular/core';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'angularapp';
}

This is the Component decorator, which describes a class that will generate HTML content, similar in purpose to the JSX classes I created in the stand-alone web app in Chapters 15 and 16.

Understanding the TypeScript Angular Toolchain

The toolchain for Angular is similar to the one I used in Chapters 15 and 16 and relies on webpack and the Webpack Development Server, with customizations specific to Angular. You can see traces of webpack in some of the messages that are omitted by the Angular development tools, but the details—and the configuration file—are not exposed directly. You can see and change the configuration used for the TypeScript compiler because the project is created with a tsconfig.json file, which is created with the following settings:
{
  "compileOnSave": false,
  "compilerOptions": {
    "baseUrl": "./",
    "outDir": "./dist/out-tsc",
    "sourceMap": true,
    "declaration": false,
    "module": "esnext",
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "importHelpers": true,
    "target": "es2015",
    "typeRoots": ["node_modules/@types"],
    "lib": ["es2018", "dom"]
  }
}

The configuration writes the compiled JavaScript files to the dist/out-tsc folder, although you won’t see that folder in the project because webpack is used to create a bundle automatically.

The most important settings are experimentalDecorators and emitDecoratorMetadata, which enable decorators and decorator metadata in the JavaScript files produced by the compiler. This feature—more than any other feature provided by TypeScript—is essential for Angular development.

Caution

Care is required when making changes to the tsconfig.json file because they can break the rest of the Angular toolchain. Most changes in an Angular project are applied through the angular.json File.

Understanding the Two Angular Compilers

There are two compilation stages in an Angular application. The first is the one you have seen throughout this book, where the TypeScript compiler processes TypeScript files and emits pure JavaScript code. During development, this compilation stage is performed each time a file change is detected, just as it was when I used webpack directly. To trigger the first Angular compilation stage, make the change shown in Listing 17-9 to the app.component.ts file in the src/app folder.
import { Component } from '@angular/core';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'angularapp';
  names: string[] = ["Bob", "Alice", "Dora"];
}
Listing 17-9.

Making a Change in the app.component.ts File in the src/app Folder

When the change is saved, the TypeScript compiler will run, and a new bundle will be created. After the slow initial compilation when the development tools are started, subsequent changes are quick and require only the changed files to be processed. During this compilation phase, you will see messages from the Angular development tools.
Date: 6:05:44.284Z - Hash: e5dae6a935157698bc33 - Time: 239ms
5 unchanged chunks
chunk {main} main.js, main.js.map (main) 12.1 kB [initial] [rendered]
wdm: Compiled successfully.
The TypeScript classes responsible for presenting content to the user have decorators that specify the HTML and CSS files they depend on, like these dependencies from the app.component.ts file:
...
import { Component } from '@angular/core';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'angularapp';
  names: string[] = ["Bob", "Alice", "Dora"];
}
...
These dependencies are resolved during the bundling process, and the files are included in the bundles, with their contents encoded as JavaScript strings. The HTML files contain a mix of regular HTML elements and annotations, known as directives, that describe how dynamic content should be generated using the data defined in the corresponding class. Replace the contents in the app.component.html file in the src/app folder with those shown in Listing 17-10 to add a directive that generates HTML elements using the values in the names array defined in Listing 17-9.
<h4 class="bg-primary text-white text-center p-2">Names</h4>
<ul>
    <li *ngFor="let name of names">
        {{ name }}
    </li>
</ul>
Listing 17-10.

Replacing the Contents of the app.component.html File in the src/app Folder

An Angular directive has been applied to the li element in Listing 17-10. The directive is ngFor, and it is responsible for repeating a section of content for each data value in a sequence. In this case, the ngFor directive will generate a li item for each value in the names array defined in Listing 17-9. When you save the changes to Listing 17-10, a new bundle will be generated, and the browser will be automatically reloaded, producing the result shown in Figure 17-2.
../images/481342_1_En_17_Chapter/481342_1_En_17_Fig2_HTML.jpg
Figure 17-2.

Using a directive

The second compilation stage is performed when the bundle has been received by the browser and the JavaScript code it contains is executed. During the application startup phase, the HTML files are extracted from the bundle and compiled so that directives are translated into JavaScript statements that can be executed by the browser, producing the results shown in the figure. Figure 17-3 shows the relationship between the two stages of compilation.
../images/481342_1_En_17_Chapter/481342_1_En_17_Fig3_HTML.jpg
Figure 17-3.

The two Angular compilation stages

The second compiler is included in the bundle and doesn’t rely on TypeScript or the TypeScript compiler.

Understanding Ahead-of-time Compilation

The second Angular compilation stage is performed by the browser every time the application starts, which can introduce a delay before the user is presented with content, especially when the browser is running on a slow device. An alternative approach is ahead-of-time (AOT) compilation, which performs the second compilation phase during the bundling process.

With AOT enabled, both compilers are used to create the contents of the bundle, which means that both the TypeScript and HTML files are compiled into pure JavaScript and no further compilation is required when the bundle is received by the browser.

The advantages of AOT compilation are that the application startup is quicker and the bundle file can be smaller because the code for the compiler is not required. But AOT is not a good choice for all projects because it places restrictions on the TypeScript/JavaScript that can be used, requiring adherence to a subset of language features. See https://angular.io/guide/aot-compiler for details of the restrictions.

If you do want to enable AOT, then you can start the Angular development tools with the --aot argument. For the example project, this means making the following change to the package.json file:
...
"scripts": {
"json": "json-server data.js -p 4600",
"ng": "ng",
"serve": "ng serve --aot",
"start": "npm-run-all -p serve json",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
...

Once you have changed the package.json file, use Control+C to stop the development tools, and run the npm start command to launch them again.

Creating the Data Model

To start the data model, create the src/app/data folder and add to it a file called entities.ts, with the code shown in Listing 17-11.
export type Product = {
    id: number,
    name: string,
    description: string,
    category: string,
    price: number
};
export class OrderLine {
    constructor(public product: Product, public quantity: number) {
        // no statements required
    }
    get total(): number {
        return this.product.price * this.quantity;
    }
}
export class Order {
    private lines = new Map<number, OrderLine>();
    constructor(initialLines?: OrderLine[]) {
        if (initialLines) {
            initialLines.forEach(ol => this.lines.set(ol.product.id, ol));
        }
    }
    public addProduct(prod: Product, quantity: number) {
        if (this.lines.has(prod.id)) {
            if (quantity === 0) {
                this.removeProduct(prod.id);
            } else {
                this.lines.get(prod.id)!.quantity += quantity;
            }
        } else {
            this.lines.set(prod.id, new OrderLine(prod, quantity));
        }
    }
    public removeProduct(id: number) {
        this.lines.delete(id);
    }
    get orderLines(): OrderLine[] {
        return [...this.lines.values()];
    }
    get productCount(): number {
        return [...this.lines.values()]
            .reduce((total, ol) => total += ol.quantity, 0);
    }
    get total(): number {
        return [...this.lines.values()].reduce((total, ol) => total += ol.total, 0);
    }
}
Listing 17-11.

The Contents of the entities.ts File in the src/app/data Folder

This is the same code used in Chapter 15 and requires no changes because Angular uses regular TypeScript classes for its data model entities.

Creating the Data Source

To create the data source, add a file named dataSource.ts to the src/app/data folder with the code shown in Listing 17-12.
import { Observable } from "rxjs";
import { Injectable } from '@angular/core';
import { Product, Order } from "./entities";
export type ProductProp = keyof Product;
export abstract class DataSourceImpl {
    abstract loadProducts(): Observable<Product[]>;
    abstract storeOrder(order: Order): Observable<number>;
}
@Injectable()
export class DataSource {
    private _products: Product[];
    private _categories: Set<string>;
    public order: Order;
    constructor(private impl: DataSourceImpl) {
        this._products = [];
        this._categories = new Set<string>();
        this.order = new Order();
        this.getData();
    }
    getProducts(sortProp: ProductProp = "id", category? : string): Product[] {
        return this.selectProducts(this._products, sortProp, category);
    }
    protected getData(): void {
        this._products = [];
        this._categories.clear();
        this.impl.loadProducts().subscribe(rawData => {
            rawData.forEach(p => {
                this._products.push(p);
                this._categories.add(p.category);
            });
        });
    }
    protected selectProducts(prods: Product[], sortProp: ProductProp,
            category?: string): Product[] {
        return prods.filter(p => category === undefined || p.category === category)
                .sort((p1, p2) => p1[sortProp] < p2[sortProp]
                    ? -1 : p1[sortProp] > p2[sortProp] ? 1: 0);
    }
    getCategories(): string[] {
        return [...this._categories.values()];
    }
    storeOrder(): Observable<number> {
        return this.impl.storeOrder(this.order);
    }
}
Listing 17-12.

The Contents of the dataSource.ts File in the src/app/data Folder

Services are one of the key features in Angular development; they allow classes to declare dependencies in their constructors that are resolved at runtime, a technique known as dependency injection. The DataSource class declares a dependency on a DataSourceImpl object in its constructor, like this:
...
constructor(private impl: DataSourceImpl) {
...

When a new DataSource object is needed, Angular will inspect the constructor, create a DataSourceImpl object, and use it to invoke the constructor to create the new object, a process known as injection. The Injectable decorator tells Angular that other classes can declare dependencies on the DataSource class. The DataSourceImpl class is abstract, and the DataSource class has no idea which concrete implementation class will be used to resolve its constructor dependency. The selection of the implementation class is made in the application’s configuration, as shown in Listing 17-14.

One of the key advantages of using a framework for web application development is that updates are handled automatically. Angular uses the Reactive Extensions library, known as RxJS, to manage updates, allowing changes in data to be handled automatically. The RxJS Observable class is used to describe a sequence of values that will be generated over time, including asynchronous activities like requesting data from a web service. The loadProducts method defined by the DataSourceImpl class returns an Observable<Product[]> object, like this:
...
abstract loadProducts(): Observable<Product[]>;
...
A TypeScript generic type argument is used to specify that the result of the loadProducts method is an Observable object that will generate a sequence of Product array objects. The values generated by an Observable object are received using the subscribe method, like this:
...
this.impl.loadProducts().subscribe(rawData => {
    rawData.forEach(p => {
        this._products.push(p);
        this._categories.add(p.category);
    });
});
...

In this situation, I am using the Observable class as a direct replacement for the standard JavaScript Promise. The Observable class provides sophisticated features for dealing with complex sequences, but the advantage here is that Angular will update the content presented to the user when the Observable produces a result, which means that the rest of the DataSource class can be written without needing to deal with asynchronous tasks.

Creating the Data Source Implementation Class

To extend the abstract DataSourceImpl class to work with the web service, I added a file named remoteDataSource.ts to the src/app/data folder and added the code shown in Listing 17-13.
import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Observable } from "rxjs";
import { map } from "rxjs/operators";
import { DataSourceImpl } from "./dataSource";
import { Product, Order } from "./entities";
const protocol = "http";
const hostname = "localhost";
const port = 4600;
const urls = {
    products: `${protocol}://${hostname}:${port}/products`,
    orders: `${protocol}://${hostname}:${port}/orders`
};
@Injectable()
export class RemoteDataSource extends DataSourceImpl {
    constructor(private http: HttpClient) {
        super();
    }
    loadProducts(): Observable<Product[]> {
        return this.http.get<Product[]>(urls.products);
    }
    storeOrder(order: Order): Observable<number> {
        let orderData = {
            lines: [...order.orderLines.values()].map(ol => ({
                productId: ol.product.id,
                productName: ol.product.name,
                quantity: ol.quantity
            }))
        }
        return this.http.post<{ id: number}>(urls.orders, orderData)
            .pipe<number>(map(val => val.id));
    }
}
Listing 17-13.

The Contents of the remoteDataSource.ts File in the src/app/data Folder

The RemoteDataSource constructor declares a dependency on an instance of the HttpClient class, which is the built-in Angular class for making HTTP requests. The HttpClient class defines get and post methods that are used to send HTTP requests with the GET and POST verbs. The data type that is expected is specified as a type argument, like this:
...
loadProducts(): Observable<Product[]> {
    return this.http.get<Product[]>(urls.products);
}
...

The type argument is used for the result from the get method, which is an Observable that will generate a sequence of the specified type, which is Product[] in this case.

Tip

The generic type arguments for the HttpClient methods are standard TypeScript. There is no Angular magic happening behind the scenes, and the developer remains responsible for specifying a type that will correspond to the data received from the server.

The RxJS library contains features that can be used to manipulate the values generated by an Observable object, some of which are used in Listing 17-13.
...
return this.http.post<{ id: number}>(urls.orders, orderData)
    .pipe<number>(map(val => val.id));
...

The pipe method is used with the map function to create an Observable that generates values based on those from another Observable. This allows me to receive the result from the HTTP POST request and extract just the id property from the result.

Note

In the stand-alone web application, I created an abstract data source class and created subclasses that provided local or web service data, which was loaded by a method called in the abstract class constructor. This is an approach that doesn’t work well in Angular because the HttpClient is not assigned to an instance property until after the abstract class constructor is invoked with the super keyword, which means the subclass is asked to get data before it has been properly set up. To avoid this problem, I separated just the part of the data source that deals with the data into the abstract class.

Configuring the Data Source

The last step of creating the data source is to create an Angular module, which will make the data source available for use in the rest of the application and select the implementation of the abstract DataSourceImpl class that will be used. Add a file called data.module.ts to the src/app/data folder and add the code shown in Listing 17-14.
import { NgModule } from "@angular/core";
import { HttpClientModule } from "@angular/common/http";
import { DataSource, DataSourceImpl } from './dataSource';
import { RemoteDataSource } from './remoteDataSource';
@NgModule({
  imports: [HttpClientModule],
  providers: [DataSource, { provide: DataSourceImpl, useClass: RemoteDataSource}]
})
export class DataModelModule { }
Listing 17-14.

The Contents of the data.module.ts File in the src/app/data Folder

The DataModelModule class is defined just so that the NgModule decorator can be applied. The decorator’s imports property defines the dependencies that the data model classes require, and the providers property defines the classes in the Angular module that can be injected into the constructors of other classes in the application. For this module, the imports property tells Angular that the module that contains the HttpClient class is required, and the providers property tells Angular that the DataSource class can be used for dependency injection and that dependencies on the DataSourceImpl class should be resolved using the RemoteDataSource class.

Displaying a Filtered List of Products

Angular splits the generation of HTML content into two files: a TypeScript class to which the Component decorator is applied and an HTML template that is annotated with directives that direct the generation of dynamic content. When the application is executed, the HTML template is compiled, and the directives are executed using the methods and properties provided by the TypeScript class.

Classes to which the Component decorator is applied are known, logically enough, as components. The convention in Angular development is to include the role of the class in the file name, so to create the component responsible for the details of a single product to the user, I added a file named productItem.component.ts in the src/app folder with the code shown in Listing 17-15.
import { Component, Input, Output, EventEmitter } from "@angular/core";
import { Product } from './data/entities';
export type productSelection = {
    product: Product,
    quantity: number
}
@Component({
    selector: "product-item",
    templateUrl: "./productItem.component.html"
})
export class ProductItem {
    quantity: number = 1;
    @Input()
    product: Product;
    @Output()
    addToCart = new EventEmitter<productSelection>();
    handleAddToCart() {
        this.addToCart.emit({ product: this.product,
            quantity: Number(this.quantity)});
    }
}
Listing 17-15.

The Contents of the productItem.component.ts File in the src/app Folder

The Component decorator configures the component. The selector property specifies the CSS selector that Angular will use to apply the component to the application’s HTML, and the templateUrl property specifies the component’s HTML template. For the ProductItem class, the selector property tells Angular to apply this component when it encounters the product-item element and that the component’s HTML template can be found in a file called productItem.component.html in the same directory as the TypeScript file.

Angular uses the Input decorator to denote the properties that allow components to receive data values through HTML element attributes. The Output decorator is used to denote the flow of data out from the component through a custom event. The ProductItem class receives a Product object, whose details it displays to the user, and triggers a custom event when the user clicks a button, accessible through the addToCart property.

To create the component’s template, create a file called productItem.component.html in the src/app folder and add the elements shown in Listing 17-16.
<div class="card m-1 p-1 bg-light">
    <h4>
        {{ product.name }}
        <span class="badge badge-pill badge-primary float-right">
            ${{ product.price.toFixed(2) }}
        </span>
    </h4>
    <div class="card-text bg-white p-1">
        {{ product.description }}
        <button class="btn btn-success btn-sm float-right"
                (click)="handleAddToCart()">
            Add To Cart
        </button>
        <select class="form-control-inline float-right m-1" [(ngModel)]="quantity">
            <option>1</option>
            <option>2</option>
            <option>3</option>
        </select>
    </div>
</div>
Listing 17-16.

The Contents of the productItem.component.html File in the src/app Folder

Angular templates use double curly braces to display the results of JavaScript expressions, such as this one:
...
<span class="badge badge-pill badge-primary float-right">
    ${{ product.price.toFixed(2) }}
</span>
...

Expressions are evaluated in the context of the component, so this fragment reads the value of the product.price property, invokes the toFixed method, and inserts the result into the enclosing span element.

Event handling is done using parentheses around the event name, like this:
...
<button class="btn btn-success btn-sm float-right" (click)="handleAddToCart()">
...
This tells Angular that when the button element emits the click event, the component’s handleAddToCart method should be invoked. Form elements have special support in Angular, which you can see on the select element.
...
<select class="form-control-inline float-right m-1" [(ngModel)]="quantity">
...

The ngModel directly is applied with square brackets and parentheses and creates a two-way binding between the select element and the component’s quantity property. Changes to the quantity property will be reflected by the select element, and values picked using the select element are used to update the quantity property.

Displaying the Category Buttons

To create the component that will display the list of category buttons, add a file called categoryList.component.ts to the src/app folder and add the code shown in Listing 17-17.
import { Component, Input, Output, EventEmitter } from "@angular/core";
@Component({
    selector: "category-list",
    templateUrl: "./categoryList.component.html"
})
export class CategegoryList {
    @Input()
    selected: string
    @Input()
    categories: string[];
    @Output()
    selectCategory = new EventEmitter<string>();
    getBtnClass(category: string): string {
        return  "btn btn-block " +
            (category === this.selected ? "btn-primary" : "btn-secondary");
    }
}
Listing 17-17.

The Contents of the categoryList.component.ts File in the src/app Folder

The CategoryList component has Input properties that receive the currently selected category and the list of categories to display. The Output decorator has been applied to the selectCategory property to define a custom event that will be triggered when the user makes a selection. The getBtnClass method is a helper that returns the list of Bootstrap classes that a button element should be assigned to and helps keep the component’s template free of complex expressions. To create the template for the component, create a file named categoryList.component.html in the src/app folder with the content shown in Listing 17-18.
<button *ngFor="let cat of categories" [class]="getBtnClass(cat)"
        (click)="selectCategory.emit(cat)">
    {{ cat }}
</button>
Listing 17-18.

The Contents of the categoryList.component.html File in the src/app Folder

This template uses the ngFor directive to generate a button element for each of the values returned by the categories property. The asterisk (the * character) that prefixes ngFor indicates a concise syntax that allows the ngFor directive to be applied directly to the element that will be generated.

Angular templates use square brackets to create a one-way binding between an attribute and a data value, like this:
...
<button *ngFor="let cat of categories" [class]="getBtnClass(cat)"
    (click)="selectCategory.emit(cat)">
...

The square brackets allow the value of the class attribute to be set using a JavaScript expression, which is the result of calling the component’s getBtnClass method.

Creating the Header Display

To create the component that will display the summary of the user’s product selections and provide the means to navigate to the order summary, add a file called header.component.ts in the src/app folder with the code shown in Listing 17-19.
import { Component, Input, Output, EventEmitter } from "@angular/core";
import { Order } from './data/entities';
@Component({
    selector: "header",
    templateUrl: "./header.component.html"
})
export class Header {
    @Input()
    order: Order;
    @Output()
    submit = new EventEmitter<void>();
    get headerText(): string {
        let count = this.order.productCount;
        return count === 0 ? "(No Selection)"
            : `${ count } product(s), $${ this.order.total.toFixed(2)}`
    }
}
Listing 17-19.

The Contents of the header.component.ts File in the src/app Folder

To create the component’s template, add a file named header.component.html to the src/app folder with the content shown in Listing 17-20.
<div class="p-1 bg-secondary text-white text-right">
    {{ headerText }}
    <button class="btn btn-sm btn-primary m-1" (click)="submit.emit()">
        Submit Order
    </button>
</div>
Listing 17-20.

The Contents of the header.component.html File in the src/app Folder

Combining the Product, Category, and Header Components

To define the component that presents the ProductItem, CategoryList, and Header components to the user, add a file named productList.component.ts to the src/app folder with the code shown in Listing 17-21.
import { Component } from "@angular/core";
import { DataSource } from './data/dataSource';
import { Product } from './data/entities';
@Component({
    selector: "product-list",
    templateUrl: "./productList.component.html"
})
export class ProductList {
    selectedCategory = "All";
    constructor(public dataSource: DataSource) {}
    get products(): Product[] {
        return this.dataSource.getProducts("id",
            this.selectedCategory === "All" ? undefined : this.selectedCategory);
    }
    get categories(): string[] {
        return ["All", ...this.dataSource.getCategories()];
    }
    handleCategorySelect(category: string) {
        this.selectedCategory = category;
    }
    handleAdd(data: {product: Product, quantity: number}) {
        this.dataSource.order.addProduct(data.product, data.quantity);
    }
    handleSubmit() {
        console.log("SUBMIT");
    }
}
Listing 17-21.

The Contents of the productList.component.ts File in the src/app Folder

The ProductList class declares a dependency on the DataSource class and defines products and categories methods that return data from the DataSource. There are three methods that will respond to user interaction: handleCategorySelect will be invoked when the user clicks a category button, handleAdd will be invoked when the user adds a product to the order, and handleSubmit will be called when the user wants to move on to the order summary. The handleSubmit method writes out a message to the console and will be fully implemented in Chapter 18.

To create the component’s template, add a file named productList.component.html to the src/app folder with the content shown in Listing 17-22.
<header [order]="dataSource.order" (submit)="handleSubmit()"></header>
<div class="container-fluid">
    <div class="row">
        <div class="col-3 p-2">
            <category-list [selected]="selectedCategory" [categories]="categories"
                (selectCategory)="handleCategorySelect($event)"></category-list>
        </div>
        <div class="col-9 p-2">
            <product-item *ngFor="let p of products" [product]="p"
                (addToCart)="handleAdd($event)"></product-item>
        </div>
    </div>
</div>
Listing 17-22.

The Contents of the productList.component.html File in the src/app Folder

This template shows how components are combined to present content to the user. Custom HTML elements whose tags correspond to the selector properties in the Component decorators are applied to the classes defined in earlier listings, like this:
...
<header [order]="dataSource.order" (submit)="handleSubmit()"></header>
...

The header tag corresponds to the selector setting for the Component decorator applied to the Header class in Listing 17-19. The order attribute is used to provide a value for the Input property of the same name defined by the Header class and allows ProductList to provide Header with the data it requires. The submit attribute corresponds to the Output property defined by the Header class and allows ProductList to receive notifications. The ProductList template uses header, category-list, and product-item elements to display the Header, CategoryList, and ProductItem components.

Configuring the Application

The application module is used to register the components the application uses as well as any additional modules that have been defined, such as the one I created for the data model earlier in the chapter. Listing 17-23 shows the changes to the application module, which is defined in the app.module.ts file.
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { FormsModule } from "@angular/forms";
import { DataModelModule } from "./data/data.module";
import { ProductItem } from './productItem.component';
import { CategegoryList } from "./categoryList.component";
import { Header } from "./header.component";
import { ProductList } from "./productList.component";
@NgModule({
    declarations: [AppComponent, ProductItem, CategegoryList, Header, ProductList],
    imports: [BrowserModule, AppRoutingModule, FormsModule, DataModelModule],
    providers: [],
    bootstrap: [AppComponent]
})
export class AppModule { }
Listing 17-23.

Configuring the Module in the app.module.ts File in the src/app Folder

The NgModule decorator’s declarations property is used to declare the components that the application requires and is used to add the classes defined in the previous sections. The imports property is used to list the other modules the application requires and has been updated to include the data model module defined in Listing 17-14.

To display the new components to the user, replace the content in the app.component.html file with the single element shown in Listing 17-24.
<product-list></product-list>
Listing 17-24.

Replacing the Contents of the app.component.html File in the src/app Folder

When the application runs, Angular will encounter the product-list element and compare it to the selector properties of the Component decorators configured through the Angular module. The product-list tag corresponds to the selector property of the Component decorator applied to the ProductList class in Listing 17-21. Angular creates a new ProductList object, renders its template content, and inserts it into the product-list element defined in Listing 17-24. The HTML that the ProductList component generates is inspected, and the header, category-list, and product-item elements are discovered, leading to those components being instantiated and their content inserted into each element. The process is repeated until all of the elements that correspond to components have been resolved and the content can be presented to the user, as shown in Figure 17-4.
../images/481342_1_En_17_Chapter/481342_1_En_17_Fig4_HTML.jpg
Figure 17-4.

Displaying content to the user

The user can filter the list of products and add products to the order. Clicking Submit Order only writes a message to the browser’s JavaScript console, but I’ll add support for the rest of the application’s workflow in the next chapter.

Summary

In this chapter, I explained the role that TypeScript has in Angular development. I explained that TypeScript decorators are used to describe the different building blocks that can be used in an Angular application. I also explained that Angular HTML templates are compiled when the browser executes the application, which means that TypeScript features have already been removed and cannot be used in templates. In the next chapter, I complete the application and prepare it for deployment.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.191.169