TypeScript Angular4
TypeScript Angular4
with TypeScript
and Angular 4
Building Web Components
with TypeScript
and Angular 4
by Matthew Scarpino
iv
No part of this publication may be reproduced, distributed, or transmitted in any form or by any means,
including photocopying, recording, or other electronic or mechanical methods, without the prior written
permission of the publisher, except in the case of brief quotations embodied in critical reviews and certain
other noncommercial uses permitted by copyright law.
For permission requests, write to the publisher, addressed “Attention – Permissions Coordinator,” at the
following address:
Table of Contents
1.4 Summary....................................................................................................................................................10
2.1 Overview....................................................................................................................................................12
2.2 Node.js.........................................................................................................................................................13
2.6 Summary....................................................................................................................................................24
3.6 Summary...................................................................................................................................................49
4.4 Interfaces...................................................................................................................................................66
4.5 Mixins..........................................................................................................................................................70
4.6 Summary....................................................................................................................................................72
5.5 Summary....................................................................................................................................................95
6.2 Decorators..............................................................................................................................................106
6.3 Summary..................................................................................................................................................115
vii
7.5 Summary.................................................................................................................................................136
8.3 Bootstrapping.......................................................................................................................................141
8.13 Summary...............................................................................................................................................176
viii
Chapter 9 – Directives......................................................................................................................................179
9.4 Summary.................................................................................................................................................197
10.1 Overview...............................................................................................................................................200
10.5 Summary...............................................................................................................................................213
11.1 Promises................................................................................................................................................216
11.5 Summary...............................................................................................................................................239
Chapter 12 – Routing........................................................................................................................................241
12.1 Overview................................................................................................................................................242
12.9 Summary...............................................................................................................................................269
13.5 Summary...............................................................................................................................................282
Chapter 14 – Forms.............................................................................................................................................285
14.1 Overview...............................................................................................................................................286
14.11 Summary.............................................................................................................................................320
x
15.1 Animation.............................................................................................................................................321
15.4 Summary...............................................................................................................................................340
16.1 Introduction.........................................................................................................................................343
16.2 Overview...............................................................................................................................................344
16.4 Summary...............................................................................................................................................362
17.5 Summary...............................................................................................................................................384
18.5 Summary...............................................................................................................................................407
xi
19.3 Summary...............................................................................................................................................432
Index.....................................................................................................................435
Chapter 1
Introducing TypeScript
and Angular
To put it mildly, the development of AngularJS (now just called Angular) has been rocky.
The toolset has undergone many dramatic and startling changes, and as a result, a large
number of developers have thrown up their hands in frustration.
But as I write this in May 2017, Angular 4 appears to have reached a steady state.
Further, the framework provides more capabilities than ever, including routing,
animation, internationalization, lazy loading, and ahead-of-time compilation. These
features make it possible to construct powerful web applications that combine high
performance with rock-solid reliability.
To take advantage of these features, it's important to be familiar with Angular's
principal language, TypeScript. TypeScript is a superset of JavaScript, but has many
advanced characteristics that make it similar to traditional languages like Java or Python.
These features include classes, interfaces, and annotations.
With TypeScript's object-oriented nature, developers can split their applications
into independent software elements called modules. This modularity is central to
Angular, which divides complex applications into special TypeScript modules called web
components.
Despite its power, Angular's learning curve is steep. The goal of this book is to make
both TypeScript and Angular as accessible as possible. The first seven chapters cover the
TypeScript language and its many exciting features. The rest of the book explains how to
build web components using TypeScript and Angular 4.
The goal of this chapter is more modest: to explain what TypeScript and Angular are
and then explain why you should drop whatever you're doing to learn them. To start, we'll
look at the developments that led to the development of Angular.
2 Chapter 1 Introducing TypeScript and Angular
This discussion is important for reasons beyond understanding why Angular and
TypeScript were developed. At many points in this book, it will be helpful to distinguish
between different versions of ECMAScript, particularly ES3, ES5, and ES6.
3 Chapter 1 Introducing TypeScript and Angular
In the early 1990s, Marc Andreessen left academia to found a company called Netscape
Communications. The company released an application for viewing documents written in
a new language called HTML. This browser, called Netscape, became an instant success.
HTML and Netscape were adopted by computer users throughout the world.
Not to be outdone, Microsoft released its own browser called Internet Explorer. This
gained rapid adoption because, unlike Netscape, it was freely available to Windows users.
As a result, the competition between Netscape and Internet Explorer grew so fierce that
reporters referred to it as a browser war.
To gain an advantage, Netscape created a method for adding code to a web page
that could be executed in the user's browser. This client-side scripting language, called
JavaScript, extended the capabilities of web pages beyond basic HTML display. JavaScript
gained a great deal of attention, but there was a problem—the code could only be executed
in Netscape browsers.
Microsoft responded with its own language called JScript. JScript was similar to
JavaScript in many respects, but code written for one browser wouldn't work in another.
This forced web developers to write different code for different browsers, an aspect of web
development that frustrates programmers to this day.
1.1.2 ECMAScript
In 2009, ECMA released the specification for ECMAScript 5. This resolved a number of
issues in ECMAScript 3 and added new features such as getters, setters, and support for
JavaScript Object Notation (JSON). At the time of this writing, ECMAScript 5 is critically
important because it's the latest version of ECMAScript that runs reliably on all modern
browsers.
As mentioned earlier, much of the effort that went into developing ECMAScript 4
was folded into a side project called ECMAScript Harmony. This came to fruition with
the release of ECMAScript 6, which is a major departure from previous releases. This
standard defines many new features including classes, modules, iterators, and generators.
Because of its origin, the specification is frequently referred to as ES6 Harmony.
5 Chapter 1 Introducing TypeScript and Angular
ECMAScript decided to change their naming scheme, and in 2016, they called
their new specification ECMAScript 2016 instead of ECMAScript 7. This provides a **
operator for raising values to a power and makes it easier to search for data in arrays. This
new naming scheme applies to ECMAScript 6 as well, which is officially referred to as
ECMAScript 2015.
In keeping with common usage, this book relies on the old naming scheme. Further,
ECMAScript will be abbreviated to ES whenever possible. Therefore, ECMAScript 5 will
be referred to as ES5 and ECMAScript 6 will be referred to as ES6. And like most of the
world, this book tends to refer to ECMAScript as JavaScript, which is more common
though less accurate.
Of the many success stories in software development, few are as impressive as the rise
of AngularJS. While JavaScript and TypeScript were planned and marketed by large
corporations, AngularJS started as a tiny project that grew popular through word of
mouth. Despite its humble origins, its effect on web development has been so profound as
to gain a worldwide following.
6 Chapter 1 Introducing TypeScript and Angular
AngularJS 1
In 2009, Miško Hevery and Adam Abrons developed a JSON storage framework and
provided a JavaScript toolset for developing client-side applications. The project, called
GetAngular, was intended to simplify front-end and back-end development. But due to
lack of interest, they gave up on their plan and released GetAngular as open source.
After taking a job at Google, Miško Hevery demonstrated GetAngular to his
colleagues, who were impressed by his gains in productivity. His manager, Brad Green,
changed its name to AngularJS and made it an official Google project. As more developers
experimented with the toolset, it became widely used inside and outside the company.
AngularJS provides many advantages over regular JavaScript. Four major strengths
are as follows:
1. Directives — special markers that add behavior to HTML elements
2. Separation of concerns — decoupling of model, view, and controller
3. Two-way data binding — rapid updating of model and view
4. Dependency injection — easy insertion of services and dependencies
<ol>
<li ng-repeat="dir in ['north', 'south', 'east', 'west']">
{{dir}}
</li>
</ol>
AngularJS 2.0
At the ng-europe Conference in 2014, Miško Hevery had every reason to be proud. In
a few short years, his AngularJS project had acquired a worldwide following. He'd risen
from a small-time entrepreneur to one of Google's most prominent web developers.
As he took his place in front of the presentation screen, his audience probably
expected to hear him express pride in AngularJS's phenomenal success. At the very least,
they expected reassurance that the project's development was proceeding normally.
But Miško confounded everyone's expectations. Instead of expressing pride or even
satisfaction, he apologized for AngularJS. He singled out three aspects for which he was
particularly regretful:
1. The complexity of defining custom directives and other structures
2. The difficulty of incorporating capabilities of other toolsets
3. The unintelligibility of untyped variables
After pointing out these shortcomings, Miško presented steps he intended to take to
resolve them:
1. Add type declarations for clearer code and type checking
2. Use metadata (annotations) to explain how data structures should be used
3. Encapsulate data in classes
4. Enable introspection to ensure type contracts and assertions
Miško's presentation implied that AtScript would be the basis of the new language,
but in early 2015, Microsoft announced that the AngularJS 2 framework would be coded
with TypeScript. As shown in Figure 1.2, this provides all of Miško's desired capabilities,
including type declarations, annotations, and classes.
In mid-2015, Google released the first alpha version of AngularJS 2. Many developers
(including myself) jumped on board, and we appreciated the new classes and components.
I was particularly impressed with the router, which makes it possible to insert components
into the page according to the URL. I also appreciated how simple it was to use
dependency injection.
8 Chapter 1 Introducing TypeScript and Angular
As a result of the sweeping changes, Google decided to completely rebrand the toolset.
Instead of following AngularJS 2 conventions, they decided to call the toolset Angular
instead of AngularJS. Because the router version had already reached 3.0, they started
Angular's version at 4.0.
This methodology is called semantic versioning, or semver. Breaking changes are
reserved for major releases and minor releases contain only additive changes. Further,
the Angular team has decided to rely on time-based release cycles of approximately
six months. Angular 4.0 was released in March 2017 so Angular 5.0 is scheduled to be
released around August 2017.
Google wants the community to refer to AngularJS 1.x as AngularJS and anything
beyond that simply as Angular. But developers and hiring managers need to know which
version of the toolset they're dealing with. This book refers to the toolset as Angular, but it
should be understood that the version in question is 4.x.
It will take many chapters to explore Angular's features and capabilities. But there's
one feature I'm eager to discuss here. While AngularJS is focused on developing web
applications, Angular focuses on building web components. This is a big deal.
In time, I expect that web components will have the same impact on web
development that objects and classes had on traditional software development. That is,
instead of coding applications from scratch, web developers will build secure, reliable
applications by connecting prebuilt components. If the impact of object-oriented
programming is any indication, the web component revolution will lead to a dramatic
increase in application performance and a commensurate decrease in labor.
It's likely that corporations will take the lead in providing (selling) web components.
Oracle will sell components that access its databases, Amazon will sell components
that target its Amazon Web Services (AWS), and Google will sell components that
interact with its many online applications. Microsoft will convince developers to use its
development tools by incorporating a library of web components into its toolset.
But in the end, I feel confident that the open-source community will have the last
word. Rather than rely on proprietary solutions, developers will code their own
general-purpose components and distribute them to the community.
Starting with Chapter 11, the topics are important to know but not as crucial.
Chapter 11 discusses asynchronous programming, which arises frequently in Angular
development. If you intend to build single-page applications, I strongly recommend
Chapter 12, which discusses routing, and Chapter 14, which presents form development.
If you're going to use Angular in professional development, Chapters 16 through 19
will be particularly helpful. Chapter 16 introduces the Angular Material library, which
provides prebuilt user interface components with many useful capabilities. Chapter 17
explains how to test components with the Protractor toolset. Chapter 18 presents a full
application that reads and displays REST data and Chapter 19 explains how to design
custom graphics using Scalable Vector Graphics (SVG) and the HTML5 canvas.
1.4 Summary
If we can learn anything from the history of web development, it's this: don't compete with
JavaScript. Dart, GWT, and Java applets have their strengths, but the JavaScript developer
base is huge and passionate. More importantly, JavaScript (ahem, ECMAScript) is the only
language that all modern browsers agree on. As many have learned, it's better to extend
the language than fight it.
Taking this lesson to heart, Microsoft developed TypeScript. TypeScript is a superset
of JavaScript, and provides ES6 features like classes, interfaces, and modules. In addition,
it supports annotations and (optional) type checking. Type checking is unnecessary for
perfect coders, but many developers (including myself) are far from perfect, and prefer to
discover bugs at compile time instead of run time.
TypeScript is the central language for developing Angular applications. Angular
provides the same essential capabilities as AngularJS, including directives, dependency
injection, data binding, and separation of concerns. But because it relies on TypeScript's
classes and annotations, the code is more intelligible and easier to test and debug.
In my opinion, Angular's main advantage is the ability to create web components.
A web component is a modular piece of functionality with its own HTML-formatted
view. This modularity facilitates code reuse, which means you can use your components
in multiple applications. Better still, you can use third-party components with proven
reliability and performance.
I'm excited to be writing about TypeScript and Angular, but before I discuss the
code, I want to present the mechanics of converting TypeScript into JavaScript. Chapter 2
introduces the TypeScript compiler, and shows how to configure the process of converting
TypeScript to JavaScript.
Chapter 2
TypeScript
Development Tools
The best way to learn is by doing, and this book takes a hands-on approach to teaching
TypeScript and Angular. But before you start coding, it's important to be familiar with the
development tools. This chapter explores three topics:
1. Node.js — An open-source framework that provides the packages needed for
TypeScript and Angular development
2. Typescript compiler (tsc) — A command-line utility that converts TypeScript code
into JavaScript code
3. Integrated development environments (IDEs) — Full-featured graphical
environments for TypeScript development
Throughout this book, the underlying goal is to convert TypeScript into JavaScript.
The two languages occupy the same level of abstraction, so the proper term for the
conversion process is transpile. However, the term compile is much more commonly used,
so this book will refer to TypeScript-JavaScript conversion as compilation.
This chapter focuses on TypeScript development and doesn't discuss Angular
development in detail. This is because general Angular development is significantly more
involved. Chapter 7 introduces Angular's CLI (command-line interface), which simplifies
the process of generating, building, and launching Angular projects.
Angular code is based on TypeScript, so it may seem confusing why Angular
development is so different than regular TypeScript development. The first part of this
chapter provides an overview of the two development processes.
12 Chapter 2 TypeScript Development Tools
2.1 Overview
The goal of Angular/TypeScript development is to generate JavaScript that can be
inserted into a web page. Figure 2.1 illustrates the difference between general TypeScript
development and Angular development based on TypeScript.
When regular TypeScript code is compiled, the result is regular JavaScript. But
when Angular code is compiled, the result is one or more JavaScript modules. Modules
are crucial in Angular development, and a full discussion of the topic will have to wait
until Chapter 7. For now, the important point to know is that JavaScript modules require
special processing before they can be inserted into a web page.
13 Chapter 2 TypeScript Development Tools
The utility that performs this special processing is called a module loader or just
a loader. At the time of this writing, Angular's preferred loader is Webpack. Webpack
combines the compiled modules with Angular's modules, and produces JavaScript that
can be accessed in a browser. Chapter 7 discusses the loading process in greater detail.
This chapter and the next four chapters focus on the development process depicted
on the left side of the figure. The primary tool is the TypeScript compiler, which is
provided for free through the Node.js project. The next section discusses Node.js and
explains how to install the TypeScript compiler.
2.2 Node.js
All of the packages and utilities discussed in this book are freely available through the
Node.js ecosystem. This provides a vast number of JavaScript projects and it's been ported
to run on every common operating system and processor. To download the installer, go to
https://nodejs.org and select the option that best suits your development system.
Node.js is provided through the MIT License. This means it can be used in both
open-source and proprietary projects.
Given the variety of operating systems and software versions, I can't provide a
thorough walkthrough of the installation process. But there are two important points to be
aware of:
1. The installation of Node.js includes a command-line utility called npm (this may be
npm.cmd on Windows or just npm on Mac OS/Linux systems).
2. Make sure you can execute npm on a command line. If you can't, find the npm utility
and add its directory to your PATH variable.
No matter what operating system you use, npm is required to install the TypeScript
compiler and the Angular modules. This section explains how npm works and then
discusses the process of installing dependencies needed for this book.
As its name implies, npm manages packages provided by Node.js. This tool performs
a number of operations related to Node.js packages, such as installing, uninstalling,
searching, and updating.
14 Chapter 2 TypeScript Development Tools
The npm tool runs from a command line. For most operations, its usage involves the
same basic syntax:
Table 2.1
Helpful npm Commands
Command Description
install Install a package on the system
uninstall Remove a package from the system
update Update package to the latest version
link Create a symbolic link from a package to the current folder
unlink Remove a symbolic link
ls List installed packages and their dependencies
search Search for a specific package
view Print data about a specific package
help Provide documentation about npm's usage
The install command is the most important. This tells npm to download the files
of the given package and place them in a folder called node_modules. If the -g flag is
used, the installation is global, which means the files will be stored where they can be
globally accessed, such as /usr/local on Mac OS and Linux systems.
Global installation is important if a package contains executables that need to be
accessed from the command line. For example, if a package named pkg contains an
executable that needs to be accessed from many directories, it can be globally installed
with the following command:
If -g isn't used, the installation is local, which means that the package will be installed
into a local node_modules folder. This is preferred over global installation because it
keeps different installations isolated from one another. But if a locally-installed package
contains executables, they will only be accessible inside of node_modules.
If a newer version of a package is available, npm update will download and install the
files for the newer package. It will also install missing packages, if necessary. If -g is used,
npm will update global packages.
The last five commands in the table provide information. The ls command prints
a tree that lists the installed packages and their dependencies. The search command
identifies if a package is available for installation. The view command provides additional
information about an installed package. help provides a great deal of information related
to npm's usage.
2.2.2 package.json
The example archive for this book can be downloaded from http://www.ngbook.io. If you
decompress the zip file, you'll find a series of folders (ch2, ch3, and so on) that contain
example projects for the corresponding chapters.
Until Chapter 7, the example code focuses on general TypeScript development. To
build these projects, you'll need to install a handful of packages. Rather than ask you to
install them individually, I've provided a file called package.json in the archive's top-level
folder.
This file tells npm which dependencies need to be installed. Therefore, after
decompressing the archive, I recommend that you open a command prompt and change
to the folder containing package.json. Then enter the following command:
npm install
When this command is run, npm will perform a number of operations, including the
following:
1. Create a node_modules folder to contain packages and installation files.
2. Install the TypeScript compiler globally. This allows you to execute tsc from a
command line in any directory.
3. Install packages that provide Jasmine and Karma, which facilitate testing of
TypeScript code.
A proper introduction to Jasmine and Karma will have to wait until Chapter 6. The
next two sections discuss the TypeScript compiler and demonstrate how it can be used.
16 Chapter 2 TypeScript Development Tools
tsc -v
tsc accepts a wide range of command-line arguments, but there's usually no need
to learn them. Most developers set compiler options in a file named tsconfig.json. If tsc
is executed in a directory containing tsconfig.json, it will read the file's settings and use
them to configure its compilation.
As its suffix implies, tsconfig.json defines a JSON object. Four important fields of the
object are:
• files — names of files to be compiled
• include — patterns identifying files to be included in the compile
• exclude — patterns identifying files to be excluded from the compile
• compilerOptions — an object whose fields constrain the compilation process
An example will clarify how the files, include, and compilerOptions fields are
used. Consider the following JSON:
{
"files": [ "src/app/app.ts" ],
"include": [ "src/utils/*.ts" ],
"compilerOptions": {
"noEmitOnError": true
}
}
This tells the compiler to process src/app/app.ts and every TypeScript file (*.ts) in the
src/utils directory. The noEmitOnError flag tells the compiler not to produce JavaScript
files if any errors are found.
17 Chapter 2 TypeScript Development Tools
Table 2.2
Compiler Options
Flag Description
target Desired ECMAScript version (es3, es5, es2015, es2016,
es2017, or esNext)
rootDir Root directory of input files
listFiles Print file names processed by the compiler
outDir Directory to contain compiled results
outFile File to contain concatenated results
watch Watch input files
removeComments Remove comments from generated output
noLib Don't include the main library, lib.d.ts, in the compilation
process
alwaysStrict Specifies whether strict mode should be enabled
noEmitOnError Don't generate output if any errors were encountered
noImplicitThis Raise an error on this expressions with implied any type
noUnusedLocals Report errors on unused locals
noUnusedParameters Report errors on unused parameters
noImplicitAny Print a warning for every variable that isn't explicitly
declared
suppressImplicit Suppress noImplicitAny errors for objects without index
AnyIndexErrors signatures
skipLibCheck Suppress type checking of declaration files
experimentalDecorators Enable support for ES7 decorators
declaration Generate declaration files (*.d.ts) for the TypeScript code
declarationDir Place declaration files in the given directory
module The format of the generated module (commonjs, amd,
system, umd, or es2015)
noEmitHelpers Do not insert custom helper functions in generated output
18 Chapter 2 TypeScript Development Tools
{
"compilerOptions": {
"target": "es5",
}
}
By default, the TypeScript compiler places JavaScript code in a *.js file in the same
directory with the same name of the TypeScript file. That is, if src/a.ts and src/b.ts need to
be compiled, the compiler will place the resulting code in src/a.js and src/b.js. The outDir
option identifies another directory to hold the results. If outFile is set, the compiler will
concatenate its results and store the code in the given file.
If alwaysStrict is set to true, the generated JavaScript will contain use script,
which tells browsers to run the script in strict mode. This mode treats mistakes as errors
and prevents the creation of accidental global variables. It also throws an error if a
JavaScript object contains duplicate properties.
In this book's tsconfig.json files, compilerOptions always sets alwaysStrict,
noEmitOnError, noUnusedLocals, and noUnusedParameters to true. This ensures
that errors will be produced if any code appears suspicious or unused.
In the table, the declaration and noLib options relate to declaration files. These
*.d.ts files declare TypeScript data structures, but don't provide code. Chapter 5 explains
how declaration files work.
The emitDecoratorMetadata option tells the compiler to enable support
for decorators. As Chapter 6 will explain, decorators make it possible to alter the
characteristics of TypeScript's data structures. They play an important role in Angular
development, so this option will be set to true in later chapters.
The module, isolatedModules, and moduleResolution flags relate to JavaScript
modules. Modules are important in the world of Angular development, but we won't need
to discuss them until Chapter 7.
19 Chapter 2 TypeScript Development Tools
For example, if you explore the ch2/hello_ts project, you'll see that it has the following
file structure:
hello_ts
|-- tsconfig.json
|-- src
|-- index.html
|-- app
|-- app.ts
When the project is built, the app.js file will be placed in the same folder as app.ts.
The src/index.html file accesses app.js using a <script> tag.
For simple TypeScript projects, this structure may seem unnecessarily complex. But
for sophisticated projects involving Angular web components, these conventions make it
easy to keep everything organized. For this reason, this book employs this structure for
both simple and complex projects.
20 Chapter 2 TypeScript Development Tools
The TypeScript compiler reads configuration settings from a file named tsconfig.json.
Listing 2.1 presents the tsconfig.json file in the ch2/hello_ts project.
{
"files": [ "src/app/app.ts" ],
"compilerOptions": {
"target": "es5",
"removeComments": true,
"alwaysStrict": true,
"noEmitOnError": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
This tells the compiler that the only file to compile is the app.ts file in the project's
src/app directory. When it performs the compilation, the compiler should generate
JavaScript according to the ECMAScript 5 specification. Because alwaysStrict is set,
the ES5 code will execute in strict mode.
The last fields of compilerOptions constrain how the compilation should be
performed. noEmitOnError tells the compiler not to generate JavaScript if an error
occurs. With noUnusedLocals and noUnusedParameters set to true, the compiler
will check for unused declarations.
The code in the project's app.ts file isn't particularly impressive, but it demonstrates an
important feature of TypeScript. Listing 2.2 presents the content of src/app/app.ts.
class HelloTS {
public printMessage() {
document.write("Hello TypeScript!");
}
}
This defines a TypeScript class named HelloTS which has a method named
printMessage. If you're not familiar with classes and methods, don't be concerned.
Chapter 4 discusses classes in detail and explains why they play such an important role in
TypeScript development.
To build this code, go to the project's top-level directory and enter tsc on the
command line. The compiler will convert the TypeScript to JavaScript and place the
resulting code in src/app/app.js. If you open src/index.html in a browser, you'll see
confirmation that the build was performed successfully.
Visual Studio is a popular environment for coding applications. Originally, Visual Studio
focused solely on Windows development and Microsoft-specific technologies. But Visual
Studio has been expanded to support many different languages including TypeScript.
This discussion focuses on the Community Edition of Visual Studio, which is free
for "individual developers, open source projects, academic research, education, and
small professional teams." It can be downloaded from https://www.visualstudio.com/vs/
community.
After downloading and installing Visual Studio, the next step is to install TypeScript
support. Microsoft provides this with an executable that can be downloaded from
TypeScript's main site, http://www.typescriptlang.org. Click the Download link at the
top and then click the version of Visual Studio that you intend to use. The executable
configures Visual Studio to support TypeScript development.
To start the TypeScript development process, launch Visual Studio and go to the
File menu in the upper left. Select the File > New > Project... menu option and the New
Project dialog will appear.
22 Chapter 2 TypeScript Development Tools
The left pane of the dialog lists the different types of projects that can be created.
Selecting the TypeScript option tells Visual Studio that you intend to create an HTML
application with TypeScript.
Toward the bottom of the dialog, input boxes ask for a name for the project and its
solution. In Visual Studio, a project contains all the files needed for a specific output and
a solution contains a set of related projects. Select any name you like for the project and
solution, and press the OK button in the lower right.
After creating the project, Visual Studio will open an editor that displays the project's
example code. To the right of the editor, the Solution Explorer lists the projects that
belong to the solution.
The code in the editor defines a Greeter class that displays the current time. This
code is contained in the project's app.ts file. In addition to app.ts, the project contains
index.html, which defines a web page that accesses the compiled app.js code. The styles
used in the web page are defined in the app.css file.
Every TypeScript project in Visual Studio also contains a web.config file that defines
properties related to ASP.NET. The topic of ASP.NET is far outside the scope of this book,
and it's unfortunate that this file can't be deleted from inside Visual Studio.
In addition to Visual Studio, Microsoft provides a development tool named Visual
Studio Code (VS Code), which can be downloaded from https://code.visualstudio.com.
The process of installing TypeScript support for VS Code is identical to that of installing
support for Visual Studio. A full description of the TypeScript development process can be
found at https://code.visualstudio.com/docs/languages/typescript.
2.5.2 Atom
If you're passionate about JavaScript, Node.js, and open-source technology, you won't find
a better environment than Atom. Developed by GitHub and released under the open-
source MIT license, Atom is the only development environment I know of that is written
in HTML, JavaScript, CSS, and Node.js. It can be freely downloaded from https://atom.io.
The good news is that Atom is "hackable to the core." Every aspect of its appearance
and operation can be customized, and the only language you need to know is JavaScript.
In addition, the Atom package manager (apm) works like the Node package manager
(npm) discussed earlier. As Atom grows in popularity, more and more packages are
becoming available to extend its capabilities.
The downside of relying on JavaScript is that Atom is slower than other text editors,
and its performance degrades significantly for files larger than 10 MB. For the example
code in this book, this won't pose a problem.
23 Chapter 2 TypeScript Development Tools
Introducing Atom
Atom has many helpful features, but because the main menu is hidden by default,
newcomers may find it confusing to work with. Here are five vital points that every Atom
user should be aware of:
1. Pressing Alt makes the main menu visible.
2. To reach the Settings page, go to File > Settings in the main menu.
3. In the Settings page, the environment's color scheme can be changed by clicking
Themes on the left and selecting a different entry in the box labeled UI Theme.
4. In the Settings page, Atom's installed packages can be updated by clicking Updates
on the left and pressing Update All. I recommend doing this on a regular basis.
5. In the Settings page, new packages can be installed by clicking Install on the left and
searching for a package name. Each desired package can be installed by clicking its
Install button.
Atom Projects
A project is a directory containing all the files needed to produce a single output. Visual
Studio organizes projects in a solution and Eclipse organizes projects in a workspace. Both
tools add a special file to a project's directory to store its settings.
Atom doesn't have solutions or workspaces, and it doesn't add special files. Instead,
it expects that all of the directories opened in the environment belong to a single project.
If a user wants to open a directory, he/she is expected to launch Atom from inside the
directory. A directory can also be opened in Atom by right-clicking in the left pane and
selecting Add Project Folder in the context menu.
To view this book's example code in Atom, download the ngbook.zip archive from
http://www.ngbook.io and decompress it to a folder. In Atom, right-click in the left pane
and select Add Project Folder. Select one of the many project directories, such as
ch2/hello_ts, and you'll see its file hierarchy in the environment's left pane.
2.6 Summary
Before you can code web applications with Angular, you need a solid understanding
of the TypeScript language and the development process. This chapter has explained
how to obtain the TypeScript compiler from Node.js and use it to convert TypeScript to
JavaScript.
This chapter also introduced the Angular Style Guide, which will be explored further
in later chapters. This guide provides many conventions for structuring projects and files,
and this book will adhere to its guidelines wherever they apply.
The last part of the chapter introduced two integrated development environments
(IDEs) for TypeScript development. The first is the community version of Visual Studio
2015, which makes it easy to edit and compile TypeScript code. The second is Atom,
which is a full-featured environment based on HTML, JavaScript, CSS, and Node.js.
Chapter 3
TypeScript Basics
If I had to summarize TypeScript as a language, the summary would have three parts:
1. TypeScript is a superset of JavaScript. Valid JavaScript is also valid TypeScript.
2. TypeScript makes it possible to declare variables with types.
3. TypeScript supports many features from ECMAScript 6 and ECMAScript 7,
including classes, interfaces, decorators, and modules.
This chapter focuses on the first two points, and only touches on some of the new
features taken from ES6/ES7. If you've already written JavaScript code, much of this will
look familiar. If you're new to TypeScript and JavaScript, this chapter will provide a solid
foundation from which to understand the rest of the book.
The first section of the chapter introduces TypeScript's data types. In addition
to explaining how to specify a variable's type, the section discusses 12 central types:
boolean, number, string, enum, array, tuple, object/Object, any, undefined,
null, void, and never. For each type, I'll explain the information it represents and how
it can be used in code.
The next part of the chapter discusses functions. With TypeScript, a function
declaration can include both its return type and the types of its arguments. I'll explain how
functions are declared and some of the new features available in TypeScript.
Even if you're a JavaScript expert, there's a great deal in this chapter worthy of
your time. New features in TypeScript include declaring variables with let and const,
enumerated types, tuples, and generic types and functions.
26 Chapter 3 TypeScript Basics
var x = 5;
x = "Hello";
This is acceptable in JavaScript and TypeScript, and won't produce any errors. But
TypeScript makes it possible to specifically identify x as a number. Consider this code:
var x: number;
x = "Hello";
The first line specifies that x is a number. When the code is compiled, the compiler
recognizes that the second line is setting x equal to a string, and it will report an error:
TS2332: Type 'string' is not assignable to type 'number'.
Type checking serves three main purposes:
1. Early error discovery — The compiler reports errors at compile-time so that you
don't have to riddle them out at run-time.
2. Tooling — Editors can analyze text better when variables have types. This affects
features like code completion, error checking, and syntax coloring.
3. Readability — When a variable has a type, it's easier to understand the purpose it
serves.
Setting a variable's type involves following the variable's name with a colon and the
desired type. A general declaration has the following form:
If the variable needs to be initialized, the declaration's form can be given as follows:
When an untyped variable is initialized, the compiler automatically sets the variable's
type to that of the value. Therefore, if a variable is initialized to a value, there's no need to
define its type.
A variable's type can be a predefined TypeScript type or a custom type. Chapter 4
discusses a number of custom types, which include classes, interfaces, and mixins. This
chapter focuses on predefined types, which include boolean, number, string, enum,
array, tuple, Object, any, void, undefined, null, and never.
For experienced JavaScript programmers, much of this will be review. But I'd like to
start with a topic that veterans may not be familiar with: declaring TypeScript variables
with let instead of var.
In JavaScript, every variable declaration must start with var. In TypeScript, variable
declarations can also start with let or const. The difference involves scope:
• A variable declared with var has function scope. When declared in a function, the
variable can be accessed everywhere in the function.
• A variable declared with let or const has block scope. When declared inside a
block, the variable can only be accessed in that block.
function example() {
var i = 22;
for (var i = 0; i < 5; i++) {
...
}
console.log(i); // Prints 5 because of var
}
In this code, i is declared twice: once at the start of the function and once inside the
for loop. Because it's declared with var, i has function scope, which means the variable
inside the function's block is the same variable outside the block. As a result, the for loop
changes the value of i from 22 to 5.
If a variable is declared with let or const, the variable inside a function's block will
be distinct from the variable outside of the block. In the above example, the i in the for
loop will be distinct from the i outside the block. This is shown in the following code.
28 Chapter 3 TypeScript Basics
function example() {
let i = 22;
for (let i = 0; i < 5; i++) {
...
}
console.log(i); // Prints 22 because of let
}
In this case, both declarations of i use let instead of var. As a result, the two
variables are distinct. That is, the variable inside the for loop doesn't affect the value of
the variable declared outside the block.
If you look at the compiled result of the preceding code, you can see how the compiler
keeps the variables separate. The compiler renames the variable in the for loop from i to
i_1, which ensures it won't be confused with the i outside the block.
const is similar to let in that it establishes block scope for a variable. The difference
between the two is that, if a variable is declared with const, its value is expected to
remain constant throughout the block. Any attempt to change the variable's value will
result in a compiler error. If a variable's value is expected to change within a block, it
should be declared with let instead.
3.1.2 Boolean
A boolean variable can be set equal to any operation that returns a boolean value.
These operations include comparison operations (>, <, >=, <=, ==, !==, and ===) and
logical operations (&&, ||, and !). The following examples demonstrate how boolean
variables can be declared and initialized:
• const x: boolean = 5 > 3;
• const y: boolean = "a" !== "b";
• const z: boolean = ((3 < 5) && ("a" != "b"));
JavaScript provides two operators for testing equality: == and ===. The difference is
that x == y returns true if x and y have the same value and x === y returns true if x
and y have the same value and the same type.
29 Chapter 3 TypeScript Basics
3.1.3 Number
In TypeScript, integers and floating-point values belong to the number type, regardless
of their sign or size. This is shown in the following examples, which declare and initialize
three number values:
1. let three: number = 3;
2. let pi: number = 3.14159;
3. let avogadro: number = 6.022e-23;
Mathematical Operations
TypeScript supports all the basic math operations from JavaScript, including +, -, *,
/, and ^. In addition, it supports all the mathematical functions that can be accessed
through Math. Table 3.1 lists the signatures of TypeScript's math functions and provides a
description of each.
30 Chapter 3 TypeScript Basics
Table 3.1
Mathematical Functions
Function Description
abs(num: number) Returns the absolute value of num
acos(num: number) Returns the arccosine of num
asin(num: number) Returns the arcsine of num
atan(num: number) Returns the arctangent of num
atan2(num1: number, Returns the arctangent of num1/num2
num2: number)
ceil(num: number) Rounds num up to the nearest integer
cos(num: number) Returns the cosine of num
exp(num: number) Returns the exponential function of num (enum)
floor(num: number) Rounds num down to the nearest integer
log(num) Returns the natural logarithm of num (loge num)
max(num1, num2, ...) Returns the number with the maximum value
min(num1, num2, ...) Returns the number with the minimum value
pow(x, y) Returns xy
random() Returns a random number between 0 and 1
round(num: number) Rounds num to the nearest integer
sin(num: number) Returns the sine of num
sqrt(num: number) Returns the square root of num
tan(num: number) Returns the tangent of num
When using the trigonometric functions (sin, cos, tan, and so on), keep in mind
that angle values are expected to be in radians, not degrees. As examples, 30° equals π/6
radians and 180° equals π radians. In code, π can be approximated with Math.PI, and the
following code sets x equal to the sine of π/6:
It's important to understand the difference between floor, ceil, and round.
Suppose num lies between two adjacent integers, X and Y, where Y is greater than X.
floor(num) will always return the low integer, X, and ceil(num) will always return the
high integer, Y. round(num) will return whichever value is closer to num. As examples,
Math.floor(0.4) equals 0, Math.ceil(0.4) equals 1, and Math.floor(0.4)
equals 0.
Number-String Conversion
TypeScript also provides routines that convert numbers to strings. These are particularly
helpful when a number requires special formatting.
Table 3.2 lists the routines available for converting numbers to strings. These
routines are accessed in code as methods. A full discussion of methods will have to wait
until Chapter 4, but for now, all you need to know is that a number method is called
by following a number variable with a dot and the method name. For example, if x is a
number, it can be converted to exponential form by calling the x.toExponential()
method.
Table 3.2
Number-String Conversion Methods
Method Description
toExponential() Returns a string containing num's value in
exponential notation
toFixed(length: number) Returns a string containing the value of num
expressed in fixed-point notation with the given
number of places after the decimal
toPrecision(length: number) Returns a string containing the value of num
expressed with the given precision
toString() Returns the string representation of num
Each of these methods returns a string, and the following code demonstrates how
they're used:
3.1.4 String
Strings make it possible to store and operate on text. A string variable can be initialized by
setting it equal to one or more characters surrounded by quotes—double quotes or single
quotes. This is shown in the following examples:
1. let single_char: string = "D";
2. let large: string = 'Single or double, it does not matter';
A string stores its characters in order and each has an index, starting with 0. This is
important to grasp when using the many methods available for string handling. Table 3.3
lists these methods and provides a description of each.
Table 3.3
String Handling Methods
Method Description
charAt(num: number) Returns the character at the given index
charCodeAt(num: number) Returns the Unicode value of the character at the
given index
concat(str1, str2, str3, ...) Returns the concatenation of the given strings
fromCharCode(num: number) Returns the character corresponding to the given
Unicode value
indexOf(str: string) Returns the position of the first occurrence of the
given string
lastIndexOf(str: string) Returns the position of the final occurrence of the
given string
localeCompare(str: string) Compares the string to the given string in the
current locale
match(expr: string) Compares the string to the regular expression and
returns true if there's a match, false otherwise
replace(expr: string, Searches the string for the value or expression
replace: string) and replaces each occurrence with the
replacement string
33 Chapter 3 TypeScript Basics
These methods are simple to understand and use in code. The following examples
show how they can be invoked:
1. let str: string = "Message";
let fifth_char: string = str.charAt(4); // fifth_char = "a"
2. let s1: string = "Type";
let s2: string = "Script";
let str: string = s1.concat(s2); // str = "TypeScript"
3. let str: string = "Message";
let index: number = str.indexOf("s"); // index = 2
4. let str: string = "Message";
let index: number = str.lastIndexOf("s"); // index = 3
5. let str: string = "Message";
let sub: string = str.slice(3); // sub = "sage"
6. let str: string = "Message";
let sub: string = str.substr(3, 3); // sub = "sag"
7. let str: string = "Message";
let sub: string = str.substring(3, 6); // sub = "sag"
34 Chapter 3 TypeScript Basics
It's easy to confuse slice, substr, and substring, which extract and return a
portion of a string. They all accept two number arguments and the first identifies the start
of the substring. For slice, the second argument is optional. For substr, the second
argument sets the length of the returned substring. substring is similar to slice but
the two arguments can be given in any order.
Three methods—match, replace, and search—accept regular expressions as
arguments. Put simply, a regular expression is a string that represents a group of strings.
Entire books have been written about regular expressions, so this discussion will be brief.
In TypeScript, every regular expression takes the following form:
/pattern/modifiers
Here, pattern identifies the characters to search for and modifiers constrains how
the search should be performed. The modifiers portion is optional. This can be set to i,
which specifies that the search should be case-insensitive. It can also be set to g, which
specifies that the search should be performed until all matches are found instead of just
the first. If set to m, the search will be performed across multiple lines.
The expression's pattern can be a regular string, as shown in the following code:
For more general searching, a pattern can contain special characters that represent
sets of characters. The dot (.) can represent any character except a newline or a line
terminator. This means that /tr.p will match trip and trap.
Similarly, square brackets can be employed to identify a range of characters. For
example, [xyz] will match a character that is either x, y, or z. The [a-z] range will
match any lowercase character and [0-9] will match any digit. A character or range can
be negated by preceding it with ^.
For example, suppose you need to match against a three-character string made up of
any character except g followed by two digits. The following code shows how the search
can be made:
In addition to these ranges, a pattern can have metacharacters that represent one or
more characters. Quantifiers make it possible to identify how many characters/ranges
should be matched. Table 3.4 lists the metacharacters and quantifiers available.
35 Chapter 3 TypeScript Basics
Table 3.4
Regular Expression Metacharacters and Quantifiers
Metacharacter/Quantifier Description
\w Matches a word character, short for [_a-zA-Z0-9]
\W Matches a non-word character, short for ^[_a-zA-Z0-9]
\d Matches a digit, short for [0-9]
\D Matches a non-digit, short for ^[0-9]
\s Matches a whitespace character
\S Matches a non-whitespace character
\b Matches at the beginning/end of a word
\B Matches at a position not at the beginning or end of a word
x+ Matches a string that contains at least one occurrence of 'x'
x* Matches a string that contains zero or more occurrences of 'x'
x? Matches a string that contains zero or one occurrences of 'x'
x{NUM} Matches a string that contains a sequence of NUM occurrences
of 'x'
x{NUM1, NUM2} Matches a string that contains a sequence of NUM1 to NUM2
occurrences of 'x'
For example, this code tests the string to see if any word starts or ends with "the":
The following code checks if the string contains at least one digit preceding x, y, or z:
For the last example, the following code checks to see if the string contains three
occurrences of a, b, or c followed by a non-word character:
JavaScript provides more features for matching, and for a more thorough discussion, I
recommend the reference page at http://www.w3schools.com/jsref/jsref_obj_regexp.asp.
36 Chapter 3 TypeScript Basics
Earlier, I mentioned that a variable of type boolean can be assigned one of two values:
true or false. But suppose that a variable needs to be assigned to one of four values,
such as NORTH, SOUTH, EAST, or WEST. The types discussed so far won't be sufficient.
You could make the variable a number and associate NORTH with 0, SOUTH with 1,
and so on. But it's less error-prone to create a data type with specifically-defined values.
This is called an enumerated type, and in TypeScript, it's represented by enum.
To define an enumerated type, the enum keyword must be followed by a name for the
type and each of its possible values. This is shown in the following format:
For example, if variables of the Direction type can be set to NORTH, SOUTH, EAST,
or WEST, the Direction type can be declared in the following way:
When this is compiled into ES5, the result will contain the following code:
Direction[Direction["NORTH"] = 0] = "NORTH";
Direction[Direction["SOUTH"] = 1] = "SOUTH";
Direction[Direction["EAST"] = 2] = "EAST";
Direction[Direction["WEST"] = 3] = "WEST";
As shown, the compiler assigns a number to each value of the enumerated type. These
numeric values can be set in code, as shown in the following declaration:
enum Direction
{NORTH = 7, SOUTH = 2^3, EAST = 3.14159, WEST = 6.02e-23};
After an enumerated type is defined, variables can be declared with the new type.
This is shown in the following code, which declares and initializes a variable called dir:
When assigning a variable to a value of an enumerated type, the value must be given
using dot notation. In this example, the type is Direction and the value is NORTH, so the
variable's value can be set to Direction.NORTH.
37 Chapter 3 TypeScript Basics
3.1.6 Array
An array is an ordered collection of elements of the same type. When declaring an array
in TypeScript, the declaration must identify the type of the array's elements. There are two
general formats for array declarations:
1. let arr1: element_type[];
2. let arr2: Array<element_type>;
An array's length can be obtained by accessing its length property. If you attempt to
read an element beyond the array's length, the compiler won't return an error. But when
you execute the compiled code, the misread value will be undefined.
Array Destructuring
TypeScript recently enabled support for array destructuring, which makes it possible to
extract multiple elements of an array with a single line of code. For example, suppose an
application needs to set the values of three variables (x, y, and z) equal to the first three
elements of a five-element array (five_array). The following code shows how this can
be accomplished with array destructuring:
let x: number;
let y: number;
let z: number;
let five_array = [0, 1, 2, 3, 4];
[x, y, z] = five_array;
38 Chapter 3 TypeScript Basics
Array Iterators
TypeScript supports the same control structures as JavaScript including if, while, and
for. An application can iterate through the elements of an array with code such as the
following:
In addition, TypeScript makes it possible to loop through arrays with the for..in
and for..of iterators. These have the same syntax but the loop variable takes different
values.
In a for..in iterator, the loop variable takes the numeric index of the loop iteration,
starting with 0. For example, consider the following loop:
This code prints 012 because i takes the index values 0, 1, and 2. In contrast, the loop
variable in a for..of iterator takes the corresponding value of the array. The following
loop shows how this works:
This code prints redgreenblue because i takes the array values red, green, and
blue.
TypeScript is a superset of ES6, so you might expect to be able to use ES6 features
like Maps, Sets, and their corresponding iterators. But ES6 collections are only available if
the compiler's target is ES6 or higher. In this book, all the code is intended to run on ES5
browsers, so these collections won't be used.
When I found out that TypeScript won't compile ES6 collections to ES5, I was
disappointed. The closest thing I've found to a justification is that "TypeScript is a
syntactic superset of JavaScript, not a functional superset."
39 Chapter 3 TypeScript Basics
3.1.7 Tuple
Tuples are like arrays but their elements can have different types. In a tuple declaration,
each element's type must be provided in order. The following code declares and initializes
two tuples:
• const t1: [boolean, number] = [true, 6];
• const t2: [number, number, string] = [17, 5.25, "green"];
As with arrays, an element of a tuple can be accessed by its index, which starts with 0
for the first element. This is shown with the following code:
• const x: boolean = t1[0]; // x = true
• const y: number = t2[1]; // y = 5.25
A tuple may seem suitable for storing data elements that are related to one another.
But as Chapter 4 will explain, classes are even better suited for this. Tuples are helpful
when a function needs to return a single data structure that contains multiple values of
different types.
3.1.8 Object/object
The term object gets a lot of mileage in computer programming books, and this book is no
exception. As confusing as it may seem, TypeScript has not one but two types related to
objects: Object and object.
A variable of the Object type is similar to a JavaScript Object. Like a tuple, it
serves as a container whose values can have different types. There are at least three crucial
differences between Objects and tuples:
1. Each value of an Object has an associated string called a key that serves as an
identifier for the value.
2. The order of values in an object isn't important. Object values are accessed by key,
but not by a numeric index.
3. When initializing an object, key-value pairs are surrounded by curly braces instead
of square brackets.
The following code shows how Objects can be declared and initialized:
• let count: Object = {first: "one", second: "two"};
• let house: Object = {num: 31, street: "Main", forSale: true};
40 Chapter 3 TypeScript Basics
As shown, the keys of an object aren't surrounded in quotes, though they can be. If an
object's value is surrounded by quotes, it will be interpreted as a string. Otherwise, it will
be interpreted as another type.
It's frequently necessary to convert objects between string format and JSON format.
This is made possible by two functions:
1. JSON.stringify(Object o) — converts an Object to a string
2. JSON.parse(string s) — converts a string to an Object
To understand what an object (little-o) is, it's important to know that the boolean,
number, string, null, and undefined types are primitive types. If a variable contains a
single value, it's a primitive.
In contrast, arrays, tuples, and Objects are non-primitive types. The object type
serves as a container of all non-primitive types. An Object is a specific type of object.
If a variable's type is unknown, it can be assigned to the any type. This tells the
compiler not to be concerned with type-checking the variable. Using the any type isn't
recommended, but it's particularly helpful when accessing constants and variables in
third-party JavaScript code.
If noImplicityAny isn't set to true, the TypeScript compiler will assign untyped
variables to the any type. But in JavaScript, variables without values are considered
undefined. In TypeScript, every value needs a type, and the undefined value has a type
of undefined. I have never used this type or seen it used in practical code.
The void and null types imply a lack of data instead of a specific type of data. In
particular, the void type represents an absence of data. If a function doesn't return a
value, the type of its return value is set to void. Variables can be declared with the void
type, but they can only be given one of two values: undefined and null.
As with undefined, variables of any type can be set equal to the null value. This
indicates that the variable doesn't contain any data. However, if a variable is assigned to
the null type, it can only be set to the value of null.
It's important to understand the difference between undefined and null. If a
variable is declared but uninitialized, JavaScript will consider it undefined. If a variable
is set to null, it is defined and its value is null.
41 Chapter 3 TypeScript Basics
3.1.11 Never
The never type is new to TypeScript, and represents the data type of any value that can't
occur. For example, if a function never returns or always throws an error, its return value
is assigned to the never type. Similarly, if a variable is assigned in a block of code that will
not be executed, its type is set to never.
In practice, identifying the variable's type and its value is unnecessary. If a variable
is set to a value, TypeScript will be able to determine its type. This is called type inference
and the following variable initializations demonstrate how it's used:
• let x = 3;
• const y = true;
After encountering this code, the TypeScript compiler will infer that x is a number
and y is a boolean despite the lack of types. If x or y is assigned to a value of a different
type, TypeScript will raise an error.
Type inference also applies to arrays, tuples, and objects. In the following code,
TypeScript understands that numArray is an array of numbers, stuffGroup is a tuple
composed of a boolean, string, and number, and address is an object containing
fields named street and streetNumber:
• let numArray = [0, 2, 4];
• let stuffGroup = [true, "Hello", 8];
• let house = {street: "Main", streetNum: 31};
If a variable isn't initialized in its declaration, then it's important to provide its type.
But throughout this book, example code will leave out a variable's type when it can be
inferred.
42 Chapter 3 TypeScript Basics
The typeof operator precedes a variable and provides a string that identifies the
variable's type. The following code shows how it can be used:
const x = 10;
document.write(typeof x); // Prints "number"
This is commonly used when dealing with a variable of an unknown type. For
example, the following code checks to see if unknownVar is a string:
This can also be used to ensure that one variable will have the same type as another.
In the following code, varB is guaranteed to have the same type as varA:
In many cases, code can be simplified by creating types with the same behavior as existing
types but different names. In TypeScript, this can be accomplished with a statement with
the following format:
For example, the following statement specifies that the areaCode type should be
aliased to the number type:
This new type can be used in declaring variables, as in the following code:
Note that the typeof operator will return the original type, not the aliased type. In
this example, typeof myCode will return number.
Variables can be assigned to a union type, which is a combination of data types. In code,
this is accomplished by inserting a | between the names of the different types. For
example, the following code declares weirdVar to be a number or a string:
After this declaration, weirdVar can be set to a number or string value, but not a
value of any other type. This is shown in the following code.
weirdVar = 5; // Acceptable
weirdVar = "Hello"; // Acceptable
weirdVar = false; // Error - "Type is not assignable"
Despite the union type, TypeScript assigns the variable to the same type as its last
successful value assignment. In this case, typeof weirdVar returns number after the
first line of code and returns string after the second line.
3.4.1 Functions
As in regular JavaScript, the definition starts with function followed by a name and
a list of arguments. After the argument list, the function definition contains a block of
code surrounded in curly braces.
Unlike JavaScript functions, this function provides the type of its parameters and the
type of its return value. The type of each parameter is given by following the parameter
name with a colon and the type.
A function can be called by following its name with a list of parameters. For the
preceding example, the function can be called with log2(8). It can also be called by
surrounding the definition with parentheses, followed by parameters in parentheses. For
example, the following code defines and invokes the log2 function:
(function log2(x) {
return Math.log(x) / Math.log(2);
})(8);
In this code, the function doesn't need a name. These types of functions are
anonymous functions. To simplify how these functions are declared, TypeScript supports
arrow function expressions, also known as fat arrow functions. This requires two changes:
45 Chapter 3 TypeScript Basics
For example, the following code shows what the preceding declaration looks like with
proper TypeScript (let instead of var) and arrow function syntax:
It can be hard to recognize the function in the last statement. Just keep in mind that
in TypeScript, the fat arrow (=>) always implies a function. Arguments are placed to the
left of the arrow and its code is placed to the right.
Arrow function expressions are almost equivalent to regular anonymous functions.
One difference involves the this keyword, which will be discussed in Chapter 4. Another
is that function types require arrow function expressions. The following discussion
presents the topic of function types.
If a variable is set equal to a function, TypeScript assigns it the function type. This can
be specified in code by providing the function's signature. The overall format is given as
follows:
For example, the following declaration states that halfRoot's type is that of a
function that receives a number and returns a number.
A function parameter may be made optional by following its name with a question mark.
In the following code, processData can be called with one or two values:
Similarly, a function can assign a default value for a parameter by setting the
parameter to the desired value. In the following code, y's default value is 9:
TypeScript functions can also accept a variable number of arguments. This requires
preceding a parameter's name with ..., as shown in the following code:
This function can accept any number of values and it can access the values through
the nums array. In this code, nums is referred to as a rest parameter.
A function can serve as an argument of another function. This is shown in the following
code:
This is easy to understand, but a function may also accept function types as
parameters. This complicates the declaration because the signature of each function
argument should be provided.
For example, suppose the ex function accepts three parameters: two numbers and a
function that accepts two numbers and returns a number. It can be defined as follows:
3.4.6 Closures
JavaScript Closures
A closure is an inner function that can be called outside of its containing function. The
following code shows how this works:
return increment;
}());
In this code, func contains an inner function called increment. increment is the
return value of func, so each call to func() invokes increment(). When increment
executes, it adds 1 to a variable called private_count, which is a local variable.
48 Chapter 3 TypeScript Basics
After a function executes, its variables are normally deallocated and inaccessible. This
isn't true for closures. In this code, private_count maintains its state between function
calls. The three calls to func() return 1, 2, and 3, respectively. This state is initialized
when the function is first called, and because the function is an IIFE, it's called as soon as
it's defined.
It's important to see that func() returns the current value of private_count, but
the variable private_count can't be read or modified outside of the function. For this
reason, it's referred to as a private variable. Without closures, there is no way to create
private variables.
Because a closure provides privacy and independence, we say that it behaves like a
module. Modularity is the primary advantage of using TypeScript/Angular, and as the next
chapter will explain, this modularity is made possible through closures.
TypeScript Closures
Modules and closures can be coded in TypeScript just as they can in JavaScript. The
only difference is that type information can be added to the different declarations. This
is shown in the following code, which creates the same closure as the JavaScript code
presented earlier:
return increment;
}
());
In this code, using let instead of var makes no difference. This is because the
count variable is only declared once in the function and the function doesn't contain any
blocks.
Despite their utility, it's rare to see closures in TypeScript. This is because it's much
simpler to use classes and let the compiler convert the classes into closures. The next
chapter presents the critical topic of classes.
49 Chapter 3 TypeScript Basics
But suppose the function should accept values of any type. The following declaration
could be used:
This is valid, but it's not specific enough. The declaration needs to make it clear that
the return value must have the same type as that contained in the array. The solution is to
declare the function using type parameters:
This states that middle accepts an array of any type and returns a value of the same
type contained in the array. Because middle is a function that uses type parameters, it's
called a generic function.
3.6 Summary
When I was a Java/GWT programmer, I thought of JavaScript as a wild language, with its
loose typing, prototypal inheritance, and lack of code security. Many JavaScript developers
agree with this assessment and wouldn't have it any other way.
TypeScript lets you decide how wild you want to be. If you want the advantages of
type checking, you can assign types to variables and have the compiler check how they're
used in code. If you'd rather not set types, you can leave your variables untyped.
50 Chapter 3 TypeScript Basics
One of TypeScript's most prominent features is its support for classes. Like a blueprint,
a class identifies properties of a structure, but is not a structure itself. A TypeScript class
tells the system how to construct data structures called objects, and because TypeScript
supports inheritance, polymorphism, and encapsulation, it's referred to as an
object-oriented language.
This chapter's primary purpose is to introduce the fundamental concepts behind
TypeScript's classes and show how they're reflected in code. A class's definition includes
definitions of its variables (called properties) and its functions (called methods). If a class
is designed properly, its methods will be able to perform all of the operations needed to
process its properties.
To understand TypeScript classes, it's important to be familiar with a number of
topics. A constructor is a special method that creates a new object. Another topic is
inheritance—if Class A inherits from Class B, Class A can access all of Class B's
non-private properties and methods. In addition, there are many new keywords to be
familiar with, including this, super, get, and set.
The secondary purpose of this chapter is to discuss interfaces. An interface is similar
to a class, but its properties can't have values and its methods can't have code. In essence,
you can think of an interface as a type for classes. When a class implements an interface, it
must provide values for the interface's properties and code for its methods.
The last part of this chapter presents the strange topic of mixin classes. The mixin
structure makes it possible for a class to acquire features of other classes without
inheriting them. Defining a mixin requires low-level JavaScript code, but mixins can be
useful when you want a class that resembles a set of other classes.
52 Chapter 4 Classes, Interfaces, and Mixins
class Car {
// Properties
make: string;
model: string;
engineType: string;
year: number;
// Methods
park() { ... };
driveForward() { ... };
driveReverse() { ... };
}
After this class is defined, a Car object can be created by calling the constructor with
the new keyword. This is shown in the following TypeScript code:
JavaScript also supports creating objects with the new keyword, but there are many
differences between JavaScript objects and TypeScript objects. One major difference is
that TypeScript objects can't be given new properties or methods.
53 Chapter 4 Classes, Interfaces, and Mixins
To see what this means, suppose we want to add a new property called numDoors.
After myCar is created, the following assignment is acceptable in JavaScript:
myCar.numDoors = 2;
But in TypeScript, this causes an error: Property 'numDoors' does not exist on type
'Car'. If you want to add a new property, you have to define a new class. But you don't
have to copy and paste code from the original class. TypeScript makes it possible to create
subclasses that extend existing classes. These subclasses receive (or inherit) members from
the original class and they can define properties and methods of their own.
The ability to define classes and subclasses is a central feature of TypeScript and other
object-oriented languages, including Java, C++, and Python. An object-oriented language
must have three characteristics:
1. Inheritance — Classes can inherit members of another class
2. Polymorphism — A subclass can be referred to using the type of its parent class
3. Encapsulation — Properties are contained in the same data structure as the methods
that operate on the properties
The rest of this discussion presents these concepts in greater detail. That is, I'll explain
how they're implemented in TypeScript and how they can be used in code.
4.1.1 Inheritance
class A extends B {
...
}
In this manner, inheritance makes it possible to proceed from an abstract type (Car)
to a specific type (SportsCar). By defining further subclasses of Car and SportsCar,
it's possible to create a hierarchy of derived classes that proceed from abstract to specific.
Figure 4.1 gives an idea of what this tree-like hierarchy looks like:
In this hierarchy, the Hatchback subclass can access every property and method
in the EconomyCar and Car classes. But while a TypeScript class can have multiple
subclasses, it can only have one immediate superclass. That is, Class A can extend from
Class B or Class C, but not Class B and Class C. If the compiler finds a TypeScript class
that extends multiple classes, it flags an error: Classes can only extend a single class.
4.1.2 Polymorphism
At first, this may not seem particularly interesting. But polymorphism makes it
possible to declare a variable as an abstract superclass, and later in the code, decide which
specific subclass should be created.
In addition, if a function's parameter is expected to be of a specific class, it can be set
to any object of any derived class. For example, consider the following signature of the
function buyCar:
This function expects a Car as its first parameter. But because TypeScript supports
polymorphism, you can insert an instance of one of its derived types, such as a
Hatchback, Convertible, or FamilyCar.
The public modifier should be clear, but the difference between protected and
private may be hard to grasp. For example, consider the SportsCar class in Figure
4.1, which has three subclasses: MuscleCar, Convertible, and RaceCar. Consider the
following definitions of SportsCar and MuscleCar:
class AddValue {
private value = 5;
This simple class has one property, value, and one method, addVal. When
compiled to JavaScript, the resulting code is given as:
function AddValue() {
this.value = 5;
}
return AddValue;
})();
This relies on the closure mechanism discussed in Chapter 3. When the function
executes, it returns the result of AddValue, which initializes the value property. As
shown in the code, the private modifier for the value property has no effect on the
generated JavaScript.
If a class contains N methods, the top-level JavaScript function will contain N+1
functions. The first function (the constructor) has the same name as the class, and
initializes its properties. For the remaining methods, JavaScript's prototype property is
employed to augment the object with each method.
If a class inherits from another class, the generated code becomes much more difficult
to read. This is because the compiler defines a function called __extends, which iterates
through the superclass's members and accesses the prototype property to add them to
the subclass.
58 Chapter 4 Classes, Interfaces, and Mixins
Once you're familiar with these topics, you'll be well on your way to taking full
advantage of TypeScript's object-oriented capabilities.
4.3.1 Constructors
A TypeScript class can contain a special routine that creates new instances of the class.
This is called a constructor and it's commonly used to initialize an object's properties
when it's created. There are four aspects of constructors that every TypeScript developer
should know:
1. Every constructor must be named constructor.
2. Each class can have only one constructor. If no constructor is defined in code, a
constructor with no arguments (a no-arg constructor) will be created.
3. Constructors are public, so the private or protected modifiers can't be used.
4. A constructor can be invoked by calling new class_name followed by the
constructor's parameter list.
By adding a constructor to the Car class, we can simplify the process of creating and
initializing Car instances. The following code presents a simple example of how a simple
constructor can be coded:
class Car {
make: string;
model: string;
engineType: string;
year: number;
With this constructor in place, a Car object can be created and initialized with the
following code:
let carInstance =
new Car("Honda", "Civic", "240hp,turbocharged", 2006);
class Car {
4.3.2 Overriding
Earlier, I explained how TypeScript makes it possible for one class (the subclass) to inherit
properties and methods from another class (the superclass). A subclass can add methods
that aren't in the superclass and it can redefine methods coded in the superclass.
For example, if a superclass contains a method called addConstant, the subclass can
implement its own addConstant method with different code. When an instance of the
subclass is created, a call to addConstant will invoke the subclass's method.
The process of reimplementing a method in a subclass is called overriding. To
override a method, the subclass's method must have the same parameter list and return
value as the superclass's method. The code in Listing 4.1 demonstrates how this works:
// Superclass
class A {
public addConstant(num: number): number {
return num + 3;
}
}
// Subclass
class B extends A {
public addConstant(num: number): number {
return num + 7;
}
}
If a subclass doesn't have a constructor and a superclass does, TypeScript won't create
a no-arg constructor for the subclass. Instead, the superclass's constructor will be called to
create an instance of the subclass. A subclass can implement its own constructor, but this
isn't considered overriding because constructors aren't considered regular methods.
A subclass can also override properties. That is, if a superclass initializes arg to a
value of 6, a subclass can initialize its value to 19. But the property must have the same
type in the superclass and subclass. That is, if the superclass declares arg as a number, the
subclass must make sure arg is a number. Otherwise, the compiler will return the error:
Types of property 'arg' are incompatible.
61 Chapter 4 Classes, Interfaces, and Mixins
Inside a class definition, the keywords this and super have special meanings. This
discussion explains what their meanings are and demonstrates how they're used in code.
this
It's common for a class's method to access other members of the class. This is particularly
true for constructors that initialize properties. But if a property's name is prop, methods
in the class definition can't access it as prop. Dot notation requires that properties be
accessed through class instances. In a class definition, the instance can be obtained using
the this keyword. Therefore, if prop is a property, it must be accessed as this.prop.
An example will clarify how this works. In Listing 4.2, addOnce uses this to access
the properties x and y. Then addTwice uses this to invoke addOnce.
class Adder {
If this is omitted anywhere in the class definition, the compiler will return an error.
For example, if this.y is replaced with y, the compiler will respond with Cannot find
name 'y'.
this can also be used in function definitions. If a function is defined with regular
syntax, function() {...}, this can refer to the global object, the containing object,
or it can be left undefined. If the function is defined using arrow syntax, () => {...},
this always refers to the enclosing execution context. If the function is global, this
refers to the global object.
62 Chapter 4 Classes, Interfaces, and Mixins
super
Like this, the super keyword can be used in a class definition to access methods of a
class instance. But super provides access to an instance of the class's superclass. That is,
if Class B is a subclass of Class A, super makes it possible for the definition of B to call
methods of A.
An example will make this clear. In Listing 4.3, Class B is a subclass of Class A and
both classes define a method called returnNum. Class B calls its own implementation
with this.returnNum and calls Class A's implementation with super.returnNum.
// Superclass
class A {
// Subclass
class B extends A {
Up to this point, each of the properties and methods we've looked at have been instance
properties and instance methods (collectively called instance members). They're called
instance members because each instance of a class receives its own copy and because
they're accessed through an instance of the class.
For example, suppose that col1 and col2 are instances of the Color class. If
the class has an instance property called rgb, its values must be accessed through the
instances (col1.rgb and col2.rgb) and each value can be changed separately. Similarly,
if blend()is an instance method of Color, it can only be called through instances of the
Color class—col1.blend() and col2.blend().
In some cases, it's better to have properties and methods that can be accessed through
the class itself, not through its instances. These members are called static members, and
their declarations are preceded by the static keyword. This discussion explains why
static members are useful and shows how they can be used.
Static Properties
In general, static properties (also called class properties or class variables) have two main
uses:
1. Constants — if a property's value never changes, it should be declared as static to
ensure that it won't be redefined for each instance.
2. Values that change in the constructor — if a property's value only changes when the
constructor is called and doesn't depend on the instance, it should be made static.
An example will help make this clear. Suppose the Circle class has a property called
pi that always equals 3.14159. As each new Circle instance is created, the constructor
adds the circle's area to a property called totalArea. The following code shows how this
class can be coded:
class Circle {
constructor(radius: number) {
this.area = Circle.pi * radius * radius;
Circle.totalArea += this.area;
}
}
64 Chapter 4 Classes, Interfaces, and Mixins
This definition shows how a class can access its instance properties and static
properties. area is an instance property, so it's accessed as this.area in the constructor.
In contrast, totalArea is a static property, so it's accessed as Circle.area. This is
because static members are accessed through the class, not an instance of the class.
The following code creates two Circle instances and checks the value of
totalArea after each instantiation:
totalArea can be accessed outside of the class because the default visibility is
public. pi can't be read outside of the class because its declaration contains the private
modifier.
Static Methods
Like properties, the methods of a class can be made static by preceding the declaration
with the static keyword. Static methods are invoked through the class name, not
through an instance. This is shown in the following definition of the Circle class, whose
static method computeArea is accessed through the class:
class Circle {
It's important to see that a class's static methods can be invoked without creating any
instances of the class. For this reason, it's common to implement utility routines as static
methods. For example, the methods of the Math class, such as Math.sin and Math.log,
are static so that you don't have to create new instances.
As shown in the example, static methods can access static properties. But static
methods can't access instance properties. In this case, if pi was declared without the
static keyword, the computeArea method wouldn't be able to access it.
65 Chapter 4 Classes, Interfaces, and Mixins
Rather than allow direct access to a class's properties, it's safer to provide indirect access
through special methods called getters and setters. A getter method returns a property's
value and a setter method modifies the value.
ECMAScript 5 simplifies the process of coding getter/setter methods by providing
get and set. If get precedes a method, any attempt to read the property with the
method's name will invoke the method. The following code shows how this can be used:
With this method in place, any attempt to read prop will call prop().
If set precedes a method, attempts to modify the property with the method's name
will invoke the method. A setter method for prop could be coded in the following way:
An attempt to modify prop's value will invoke this method. The code in Listing 4.4
shows how getter/setter methods named foo provide access to a property named num.
class A {
private num: number;
constructor() { this.num = 5; }
// Getter method
get foo(): number { return this.num; }
// Setter method
set foo(f: number) { this.num = f; }
}
Getter methods don't compute a property's value until the property is accessed for the
first time. If a property doesn't change frequently, methods called smart getters store the
property's value in a cache so that it can be accessed quickly.
66 Chapter 4 Classes, Interfaces, and Mixins
4.4 Interfaces
The first part of this chapter showed how a class makes it possible to model an entity's
data (properties) and behavior (methods). An entity's behavior can be split into two parts:
its activities (the list of methods) and manner in which the activities are performed (the
methods' code).
By providing interfaces, TypeScript makes it possible to declare an entity's data and
activities without being specific about the details. To be specific, an interface is a class
whose properties are uninitialized and whose methods don't contain any code.
For example, the following interface represents a soccer player. Each player has
a jersey number and his/her activities include passing the ball, receiving the ball, and
blocking an opponent:
interface SoccerPlayer {
jerseyNum: number;
passBall(...);
receiveBall(...);
blockOpponent(...);
}
This interface doesn't provide a value for jerseyNum or code for its three methods.
If its property is assigned a value or a method is given a body (even {}), the compiler
will respond with an error. Interface properties can be made optional by following the
property name with ?.
Just as classes can inherit from other classes, interfaces can inherit from other
interfaces. This is demonstrated in the following interface, which represents a soccer
player in the CenterForward position:
shootOnGoal();
freeThrow();
}
Just as a class can extend another class, a class can implement an interface. When a class
implements an interface, it doesn't inherit anything. Instead, it receives an obligation to
declare the interface's properties and provide code for its methods. For example, a class
that implements SoccerPlayer must redeclare jerseyNum and provide code for the
passBall, receiveBall, and blockOpponent methods.
The code in Listing 4.5 demonstrates how classes and interfaces work together. The
AddSubtractConstant interface has a property and two methods, addConstant and
subtractConstant. The AddSubtractConstantImpl class implements this interface.
The implementing class doesn't have to assign values to the interface's properties,
but they must at least be redeclared. The class does have to provide code for each of the
interface's methods, even if it's just an empty block, {}.
In addition to uninitialized properties and empty methods, there's one crucial
difference between classes and interfaces: interfaces can't be instantiated. That is,
there's no way to create an object from an interface—you can't create an instance of
AddSubtractConstant with new AddSubtractConstant() and you can't create a
SoccerPlayer instance with new SoccerPlayer().
This raises a question: if interfaces can't be instantiated, what are they good for? Why
would you define an interface when you can define a class instead?
68 Chapter 4 Classes, Interfaces, and Mixins
Before answering this question, I'd like to present an example. Suppose you've written
an application whose behavior can be customized. You want to tell third-party developers
what methods are needed, but you don't want to show them your code. In this case, it's
easier to provide an interface, which lists the methods and their signatures but doesn't
provide any code. Then, if a developer defines a class that implements the interface, the
application knows that it can invoke the required methods.
Put another way, an interface defines a contract. A class that implements an interface
must provide code for a specific set of methods. If a function or method receives an object
from an implementing class, it knows that specific methods can be called. This allows the
interface name to be used as a type.
When dealing with classes and interfaces, there are three rules to keep in mind:
1. A class can only inherit from one other class, but it can implement any number of
interfaces.
2. When a class declares properties from an interface, each property must have the
same accessibility modifiers.
3. When a class implements the methods of an interface, the methods must be instance
methods, not static methods.
There's one point that's interesting but not particularly important. If a class contains
only uninitialized properties, the compiler will allow the class to serve as an interface.
The TypeScript handbook refers to this as type compatibility, which means the compiler
identifies types according to their members.
In the following code, Class A consists of two uninitialized properties. Class B
implements Class A as if it were an interface, and Class C extends Class A as if it were a
class. Class B must redeclare num1 and num2 but Class C does not.
num1 = 4;
num2 = 9;
addNums(k: number): number {
return k + this.num1 + this.num2;
}
}
69 Chapter 4 Classes, Interfaces, and Mixins
If a class contains methods without code blocks, it isn't considered an interface. It's
considered invalid and the compiler flags an error: Function implementation is missing or
not immediately following the declaration.
Up to this point, TypeScript's interfaces closely resemble interfaces in Java and C#. But
unlike Java/C# interfaces, TypeScript interfaces can represent functions and arrays. For
functions, interfaces make it possible to verify that a function's arguments and return
values have the correct type. For arrays, interfaces verify that the index and elements have
the correct type.
Function Interfaces
An interface can define the required types of a function's arguments and return value. The
following code demonstrates how this works:
interface funcDef {
(arg1: number, arg2: boolean): number;
}
This interface defines a contract for a function whose first argument is a number,
whose second argument is a boolean, and whose return value is a number. This interface
can't be implemented by a class, but if a variable is declared with type funcDef, it can
only be assigned to a function with the given arguments and return values.
The interface provides names for the function's arguments, but only their types
matter. This is shown in the following code:
The first line declares newFunc to be of type funcDef. Then newFunc is set equal to
a function whose first argument is a number, whose second argument is a boolean, and
whose return value is a number. If any of the types don't match the interface, the compiler
will return an error: Type '...' is not assignable to type 'funcDef '.
70 Chapter 4 Classes, Interfaces, and Mixins
Array Interfaces
An array interface defines the type of an array's index and the type of its elements. The
array's index must be a number or a string, and its elements can have any defined type.
For example, the following interface represents an array of Thing objects with numeric
indexes:
interface arrayDef {
[i: number]: Thing;
}
The array's elements can be accessed with numeric indexes, such as thingArray[1].
As with function interfaces, the name of the index isn't important, but the type is.
4.5 Mixins
TypeScript's single inheritance requires that a class can only inherit members from one
superclass at most. But TypeScript makes it possible for a class to receive (not inherit)
members from multiple other classes. This can be accomplished using mixins.
In essence, a mixin combines (mixes) the members of multiple classes into another
class. A good way to understand mixins is to look at an example. Suppose you want to
combine the members of Class A and Class B into Class C. This requires five steps:
1. In the definition of Class C, have the class implement Class A and Class B.
2. For each property in Classes A and B, add a declaration for the same property in
Class C with the same type.
3. For each method in Classes A and B, declare the method in Class C using the
appropriate function type.
4. After the class definition, call the function applyMixins. The first argument
should be Class C and the second should be an array containing Classes A and B.
5. Provide code for the applyMixins function, which is defined in the following way:
71 Chapter 4 Classes, Interfaces, and Mixins
The code in Listing 4.6 demonstrates how mixins can be used in practice.
The AddConstant class has a property called num1 and a method called addNum.
The SubtractConstant class has a property called num2 and a method called
subtractNum. The AddSubtract class combines the members of both classes.
// Declare properties
public num1 = 5;
public num2 = 7;
The AddSubtract class declares the addNum and subtractNum methods, but
doesn't provide any code. The behavior of addNum and subtractNum is provided by the
mixed-in classes, AddConstant and SubtractConstant. It's interesting to note that
setting a property's value in AddConstant and SubtractConstant has no effect on the
property in the AddSubtract class.
Members of the mixed-in classes must be public in order to be incorporated into
another class. That is, if AddConstant contains a private property, it can't be mixed into
another class. If the property is protected, it still can't be mixed in because the other class
isn't a subclass.
4.6 Summary
This chapter has discussed classes, interfaces, and mixins, but the emphasis must be
placed on classes. Classes are like cookie cutters. They define the shape and properties of
a cookie, but they aren't cookies. Just as pressing a cookie cutter into dough creates new
cookies, a class can instantiate new objects with code like new Cookie().
Every class can define its data (properties) and behavior (methods). These features
are collectively called members, and by default, every member is publicly accessible. If a
member is declared as protected, it can be accessed by its class and by any subclass. If a
member is private, it can only be accessed in the class in which it's defined.
After introducing classes, this chapter explained a number of related characteristics.
When coding TypeScript classes, it's vital to understand inheritance, constructors, the
this keyword, and method overriding. Other topics, such as getter/setter methods and
static members, are helpful to know but not as critical.
73 Chapter 4 Classes, Interfaces, and Mixins
A class can only extend one other class, but it can implement multiple interfaces.
Interfaces are similar to classes, but their properties must be undeclared and their
methods must not have any code. If a class implements an interface, it is obligated to
declare the interface's properties and provide a code block for each of its methods.
TypeScript also supports interfaces that define specific types of functions and arrays.
If a class's properties aren't assigned to values and its methods have no code, another
class can implement it as if it were an interface. With mixins, a class can implement
multiple classes regardless of whether their methods have code. Mixins are interesting
because they incorporate features from implemented classes into a composite class.
The subject of object-oriented programming is far broader and more interesting
than this chapter has presented. Formal computer scientists take this topic very seriously,
and for a proper discussion, I recommend Object-Oriented Analysis and Design with
Applications by Grady Booch and Robert A. Maksimchuk.
Chapter 5
Declaration Files and the
Document Object Model (DOM)
This chapter discusses two topics that every TypeScript developer should be familiar with:
declaration files and the document object model (DOM). Declaration files are easy to
understand. As the name implies, a declaration file provides declarations of TypeScript
functions and data structures. These files become important when you want to access a
JavaScript file in TypeScript code.
The DOM is more complicated, but it's a crucial (and fascinating) subject to know.
TypeScript provides a series of interfaces and classes that represent aspects of a web page.
These data structures provide methods that make it possible to create, read, modify, and
delete a page's elements.
Most of this chapter is devoted to accessing the DOM in TypeScript, but it's not
important to know every class and method. The primary goal is to become familiar with
the underlying concepts.
In addition to accessing external files, it's important to know how to access external
JavaScript libraries such as jQuery. This can be difficult because we don't want the
compiler to analyze the library's code every time we build an application. To keep the
compiler happy, we need to declare external elements with special declarations called
ambient declarations.
When the compiler encounters an ambient declaration, it accepts the declaration and
doesn't care about the element's actual value or implementation. An ambient declaration
is similar to a regular declaration, but starts with the declare keyword. Ambient
declarations must be placed in special files called declaration files, and every declaration
file has the suffix *.d.ts.
An example will demonstrate how ambient declarations make it possible to access
JavaScript in TypeScript code. Listing 5.1 presents a simple JavaScript function:
To access addNum in TypeScript, the first step is to create a *.d.ts file that declares the
function. By convention, the name of the declaration file is the same as the JavaScript file.
Listing 5.2 shows what addNum.d.ts looks like:
The compiler needs to be informed of the declaration file's name and location,
and this can be accomplished by inserting src/app/addNum.d.ts in the files array of
tsconfig.json. Once this is done, addNum can be called in TypeScript as if it was a regular
function. This is demonstrated in Listing 5.3, which invokes the addNum function
declared in addNum.d.ts.
The JavaScript file being accessed must be included in the web page in addition to
the compiled JavaScript. For this reason, ch5/declaration_demo/src/index.html has two
<script> elements—one for app.js and one for addNum.js.
Many declaration files are freely available on the Internet. One particularly helpful
resource is DefinitelyTyped, which is located at https://github.com/DefinitelyTyped/
DefinitelyTyped. The types directory of this repository contains more than a thousand
declaration files that can be accessed in TypeScript.
Instead of manually copying files from DefinitelyTyped, you can install declaration
files through npm. Declaration files are provided in packages named @types/xyz. As an
example, the following command installs declaration files related to jQuery:
When the installation is finished, the node_modules directory will contain a folder
named @types/jquery. This will contain a declaration file, index.d.ts, which declares the
many functions and structures provided by the jQuery framework.
Unfortunately, jQuery's functions and structures can't be accessed directly. As with
many declaration files provided through DefinitelyTyped, the functions and structures
must be accessed through a container structure called a module. A full discussion of
modules will have to wait until Chapter 7, but for now, there are two important points to
know:
1. For packages obtained through DefinitelyTyped, the name of a module is the same
as that of its package (without @types).
2. The import statement makes it possible to access a module's functions and
structures in TypeScript.
For jQuery, the module's name is jquery and the import statement can be written
as follows:
As a result, $ will be available throughout the TypeScript file. This makes it possible
to perform regular jQuery operations such as selecting elements and performing actions
on them. Because the jQuery package was installed through npm, there's no need to
update tsconfig.json with the name of the declaration file.
78 Chapter 5 Declaration Files and the Document Object Model (DOM)
TypeScript provides a vast assortment of interfaces that represent HTML elements. These
include HTMLBlockElement, HTMLButtonElement, and HTMLParagraphElement.
These data structures form TypeScript's implementation of the document object model,
or DOM. The better you understand these interfaces, the better you'll understand how to
access a document's elements in TypeScript.
Figure 5.1 depicts an abridged inheritance hierarchy of the different interfaces. As
shown, all of the HTML elements are descendants of the EventTarget interface. Every
EventTarget can have an associated event listener to respond to user events, and a later
section will explain how event listeners work.
The first subinterface of EventTarget is Node, and its subinterfaces include
Document, Element, and Attr. Below Element, the HTMLElement interface has a
subinterface for each different type of element in a web page.
79 Chapter 5 Declaration Files and the Document Object Model (DOM)
This discussion presents many, but not all, of the interfaces in Figure 5.1. We'll look
at the Node interface first, followed by the Document, Element, and HTMLElement
interfaces. Then I'll present a handful of element-specific interfaces and show how they
can be used.
NOTE
The DOM manipulation methods discussed in this chapter can be used to read and
modify a web page, but this book's goal is to present web development with Angular.
These methods are fine for analyzing and debugging applications, but for regular web
development, I recommend using Angular instead. As later chapters will show, the
Angular framework provides a great deal of power and elegance with a minimum of
code.
Table 5.1
Members of the Node Interface
Member Description
childNodes The NodeList containing the Node's children
firstChild The first Node in the set of children
lastChild The last Node in the set of children
nextSibling The Node immediately following this Node in the parent's
set of children
previousSibling The Node immediately preceding this Node in the parent's
set of children
parentNode The parent Node
ownerDocument The Node corresponding to the Node's document
nodeName The Node's name
nodeType The Node's type
textContent The Node's text content
appendChild(Node) Adds a Node to the Node's list of children
hasChildNodes() Identifies if the Node has children
insertBefore Makes the Node a child just before the ref child
(Node child, Node ref)
removeChild(Node n) Removes the Node from the list of children
replaceChild Replaces the old child Node with the new Node
(Node new, Node old)
Methods in the first category make it possible to traverse the document's node
structure. Any Node can access the Document node by accessing the ownerDocument
member. Then an application can iterate through the Document's children, and their
children, and so on. In addition, the Node methods make it possible to access a Node's
parent and siblings.
81 Chapter 5 Declaration Files and the Document Object Model (DOM)
Every Node has a name, type, and value. Their values depend on the Node subclass.
For example, if the Node is an Element, its name will equal that of its corresponding
HTML tag. If the Node is an Attr, its value will equal the text in the corresponding
element. This will be demonstrated in the example DOM analysis application presented
later in this section.
Every Node can access its children through a NodeList provided by the
childNodes member. NodeList doesn't implement the Iterable interface, so if you
want to iterate through a Node's children, you can obtain the number of children with
NodeList.length and access the NodeList as an array with each index.
The members listed in the table make it possible to add and remove a Node's
children. This may not seem exciting at first, but they allow you to update a document's
content programmatically. The example code at the end of this section demonstrates how
this can be accomplished.
Every web page has a single Document node that represents the entire document. By
accessing this, an application can create, read, modify, or delete Elements in the page. In
TypeScript, a document can be accessed through the document object. Alternatively, a
Node can access the Document through its ownerDocument property.
In addition to interacting with Elements, the properties and methods of the
Document interface provide information about the web page such as its URL and title.
Table 5.2 lists 20 members of the Document interface.
Table 5.2
Members of the Document Interface (Abridged)
Method Description
URL The Document's URL
activeElement The Element with focus
alinkColor The color of active hyperlinks
all Returns an HTMLCollection containing the
Document's elements
body The HTMLElement corresponding to the body
characterSet The Document's character set
cookie The Document's cookie
documentElement The root node of the Document
82 Chapter 5 Declaration Files and the Document Object Model (DOM)
For the most part, these members are straightforward to understand. Many of the
properties return HTMLCollections, which are essentially arrays of Elements. That is,
this interface provides a length property and an Element can be accessed by following
the collection with [index].
The createElement method makes it possible to create new Elements using
TypeScript's DOM. This accepts the name of a tag and returns the corresponding
HTMLElement. The following code demonstrates how this can be used to create a button
element:
document.body.appendChild(table);
83 Chapter 5 Declaration Files and the Document Object Model (DOM)
The Document's write and writeln methods are also particularly helpful as they
make it possible to print output to the page. This book's TypeScript code examples rely on
these methods to display processing results.
The Document interface doesn't provide any methods for deleting an Element. To
delete an Element, you must access its parent Node and remove it from the parent's list of
children.
Each structural piece of a web page is represented by an Element and each Element
occupies space in the page. Put another way, every <tag></tag> pair in an HTML page
corresponds to an Element.
After obtaining an Element, you can access a wide array of properties and methods.
Table 5.3 lists 16 of them and provides a description of each.
Table 5.3
Members of the Element Interface (Abridged)
Method Description
id The Element's identifier
tagName The Element's tag
className The Element's class
clientHeight, The dimensions of the Element's inner area,
clientWidth including padding but not margin or border
84 Chapter 5 Declaration Files and the Document Object Model (DOM)
Table 5.4
Members of the HTMLElement Interface (Abridged)
Method Description
title The element's title
hidden Whether the element is hidden
children The HTMLCollection containing the element's children
tabIndex The element's index in the tab sequence
innerText The element's text without the tags
outerText The element's text, including the tags
innerHTML The element's HTML-formatted text without the tags
outerHTML The element's HTML-formatted text, including the tags
85 Chapter 5 Declaration Files and the Document Object Model (DOM)
The style property makes it possible to read and modify an HTMLElement's style.
Its data type is a CSSStyleDeclaration, which has a property for each CSS property.
For example, CSS's color property is represented by the color property of the
CSSStyleDeclaration. The following code shows how it can be used to change the text
color of the Document's <code> element to red:
The HTMLElement interface has a subinterface for each type of element in an HTML
page. Figure 5.1 illustrated eight subinterfaces, but there are many more. Each subinterface
has properties and methods specific to an element's type, and this discussion focuses
on four subinterfaces of HTMLElement: HTMLAnchorElement, HTMLInputElement,
HTMLFormElement, and HTMLButtonElement.
For each subinterface, I'll present a handful of the interface's members and show how
the corresponding object can be created in code. For the full list, I recommend looking
through the lib.d.ts declaration file in the lib folder of the TypeScript installation directory.
86 Chapter 5 Declaration Files and the Document Object Model (DOM)
HTMLAnchorElement
HTMLInputElement
The following code demonstrates how to create an <input> element that receives up
to 15 characters:
The HTMLInputElement also provides two properties that simplify the validation
process. If the required property is set to true, the element won't be valid until the user
has provided a value. Similarly, if the pattern property is set, the element won't be valid
until the user's value matches the given pattern.
HTMLFormElement
Chapter 13 discusses the Reactive Forms API, which makes it possible to construct form-
based components with Angular. To create a form in basic TypeScript, you need to add
an HTMLFormElement to the document. This interface provides properties that read and
configure the form's behavior, and five of them are given as:
• name — the value of the element's name attribute
• action — identifies where to send the form's data
• method — the HTTP method used to send a request to the server (get or post)
• elements — an HTMLCollection containing the form's elements
• length — the number of elements contained in the form
HTMLButtonElement
No HTML form is complete without a Submit button, and every button in a web page
is represented by an HTMLButtonElement in the DOM. Each button has a type
attribute that can be set to button, reset, or submit, and many attributes provide
access to the form containing the button. This is reflected by the properties of the
HTMLButtonElement interface, and five of them are listed as follows:
88 Chapter 5 Declaration Files and the Document Object Model (DOM)
Listing 5.4 presents the code in the dom_demo project, which demonstrates how the
TypeScript DOM can be employed to create a form. Figure 5.2 shows what the generated
form looks like.
The project's code uses four of the interfaces presented in this discussion: Document,
HTMLFormElement, HTMLInputElement, and HTMLButtonElement. It also creates
two HTMLLabelElements to serve as labels and two HTMLBRElements to provide line
breaks.
The <input> elements have their required attributes set to true, so the form can't
be submitted until both have values. When the form is submitted, its data is sent to a URL
named recipient.html. This data consists of two strings: a first name (fname) and a last
name (lname).
The ch5/dom_demo/src/recipient.html page receives the form's data and executes the
code in recipient.ts. This code, presented in Listing 5.5, accesses the page's URL through
the Document's URL property. Then it reads the fname and lname parameters from the
URL and prints them to the window.
89 Chapter 5 Declaration Files and the Document Object Model (DOM)
<html>
<head>
<title>DOM Tree Demonstration</title>
</head>
<body>
<p>This is a paragraph</p>
<button>This is a button</button>
<script src="app/app.js"></script>
</body>
</html>
To analyze a page like this, the dom_tree.ts code defines a class called
DomTreeAnalyzer. Its static analyze method iterates through the Document's children
and its children's children. Listing 5.6 presents the code.
class DomTreeAnalyzer {
The analyze function checks the nodeType of each node to make sure only
Element nodes are processed. For each Element, the class prints its name and the name
of its parent. For the example HTML given above, the results printed to the console are
given as follows:
Name: HTML
Parent name: #document
Name: HEAD
Parent name: HTML
Name: TITLE
Parent name: HEAD
Name: BODY
Parent name: HTML
Name: P
Parent name: BODY
Name: BUTTON
Parent name: BODY
Name: SCRIPT
Parent name: BODY
These seven elements form the web page's DOM tree. Figure 5.3 shows what this
hierarchy looks like.
This is much simpler than a tree generated from a professional web page. But this
process of analyzing nodes can be employed for many different types of pages.
92 Chapter 5 Declaration Files and the Document Object Model (DOM)
addEventListener(type: string,
listener: (ev: Event) => any,
useCapture?: boolean)
The first argument sets the type of event to be handled and the second identifies the
function to handle the event. This function receives an object that provides event data
(MouseEvent for mouse clicks, KeyboardEvent for keystrokes, and so on).
The optional third argument of addEventListener identifies how an event
received by a child element should propagate to parent elements:
• If set to true, the event will be processed by parent elements before child events
(outside to inside). This is called event capturing.
• If set to false, the event will be processed by child elements before parent events
(inside to outside). This is called event bubbling, and is the default method of event
propagation.
93 Chapter 5 Declaration Files and the Document Object Model (DOM)
The first argument of addEventListener is a string that identifies the type of event to
be handled. For handling mouse-related events, there are 10 possible values. Table 5.5 lists
them all.
Table 5.5
Mouse Event Strings
Event String Action
click User clicks a mouse button
dblclick User double-clicks a mouse button
mousedown User presses a mouse button down
mouseup User raises a mouse button
mousemove The mouse cursor moves
mouseenter The mouse cursor enters the element
mouseleave The mouse cursor leaves the element
mouseover The mouse cursor passes over the element
mouseout The mouse pointer leaves the element and any child elements
mousewheel The user scrolls the mouse wheel
For example, the following code defines a function to be invoked when the user clicks
the button corresponding to the HTMLElement named btn:
As shown, the event-handling function can access information about the mouse click
through a MouseEvent. This is provided for each of the events in Table 5.5 except for
mouse wheel events, whose handling functions can access a MouseWheelEvent.
94 Chapter 5 Declaration Files and the Document Object Model (DOM)
The first three properties are defined in the Event interface, which is the
superinterface of all event interfaces in the TypeScript DOM. The following code shows
how they can be accessed to provide information about a mouseenter event:
As a result of this listener, three statements will be printed to the log each time the
mouse cursor enters the button's region.
The process of handling keystroke events is similar to that of handling mouse events. One
difference is that keystroke events are identified by another set of strings. Table 5.6 lists
each of them.
Table 5.6
Keystroke Event Strings
Event String Action
keypress User presses and releases a key
keydown User presses a key down
keyup User releases a key
95 Chapter 5 Declaration Files and the Document Object Model (DOM)
To obtain information about the user's action, an event-handling function can access
a KeyboardEvent. This is shown in the following code:
In this code, the event-handling function accesses the Unicode value of the pressed
character through the charCode property of the KeyboardEvent. Other properties of
KeyboardEvent are:
• type — the name of the event
• timeStamp — the time the event occurred
• srcElement— the Element that dispatched the event
• key — a string corresponding to the pressed key
• locale — the keyboard's locale
• shiftKey, ctrlKey, altKey — identifies if Shift/Ctrl/Alt was pressed
The locale property identifies the locale of the user's keyboard. This doesn't
necessarily reflect the user's language.
5.5 Summary
The first part of this chapter introduced the important subject of declaration files. A
declaration file contains TypeScript declarations of functions and data structures in
JavaScript code. These files are vital when you want to access capabilities of an external
JavaScript toolset, such as jQuery or Jasmine. You can find a wide assortment of
declaration files at https://github.com/DefinitelyTyped/DefinitelyTyped.
The rest of this chapter presented the interfaces and methods that TypeScript
provides for accessing the document object model (DOM). At a structural level, every web
page consists of a tree of nodes, with a document node serving as the root. By analyzing
the root and its children, an application can examine a page's structure and create, read, or
modify the elements in the tree.
96 Chapter 5 Declaration Files and the Document Object Model (DOM)
This chapter discusses two TypeScript topics that go beyond the fundamentals of the
language. Neither of them are deep enough to require a separate chapter, but they're both
helpful in TypeScript development:
1. Unit testing — using Jasmine and Karma to test TypeScript code
2. Decorators — examine and replace aspects of decorated code
Unit testing checks the smallest parts of an application, such as variables and objects,
to ensure they're being set to acceptable values. Jasmine is a framework for testing
JavaScript, and Karma makes it possible to automate the testing process with multiple
browsers. By combining Jasmine and Karma, you can configure complex tests and execute
them in an automated fashion. The first part of this chapter explains how this can be done.
The second part explores the topic of decorators. Decorators make it possible to
modify aspects of decorated code. As later chapters will explain, Angular's dependency
injection process uses decorators. For example, the @Injectable decorator tells the
framework to recognize classes as service classes.
This section also introduces Karma, a framework for automating tests with multiple
browsers. Combined, Jasmine and Karma make it possible to fully automate the unit
testing of TypeScript code.
6.1.1 Jasmine
A Jasmine test suite consists of regular JavaScript code that calls a set of nested functions.
At minimum, a test suite combines three functions:
1. The outermost function is describe, which accepts two arguments: a name for the
test suite and a function.
2. Inside describe's second argument, the it function should be called for each unit
test, or spec. it accepts a name for a spec and a function that defines the spec.
3. Inside it's second argument, expect is called to test a JavaScript expression.
An example will clarify how declare, it, and expect work together. Suppose you
want to test the functions in this code:
The following code defines a test suite with two specs. The first tests the result of
getFirstName() and the second tests the result of getLastName().
As shown, the name of the test suite serves as the subject of a sentence that describes
the test. The name of each spec provides a phrase that continues the sentence.
When Jasmine runs, it reports 2 Specs, 0 Failures because both functions execute
successfully. Before I explain how to run Jasmine tests, I want to explain how Jasmine can
be used to test TypeScript.
99 Chapter 6 Unit Testing and Decorators
Testing TypeScript
Jasmine was created to test JavaScript, so it doesn't know anything about TypeScript
features like classes or interfaces. This isn't a problem. Jasmine functions can be coded in a
TypeScript file (*.ts), and when the code is compiled, the specs will test regular JavaScript.
An example will demonstrate how this works. Consider the following class:
class FullName {
constructor(public firstName: string, public lastName: string) {
this.firstName = "James";
this.lastName = "Bond";
}
The following code uses Jasmine's functions to test both methods of the FullName
class:
beforeEach(function() {
fName = new FullName();
});
});
The beforeEach function makes it possible to execute code before any of the specs
are executed. In this case, the fName variable is set equal to a new FullName. When
this code is compiled, Jasmine can perform the test as though it was written with regular
JavaScript. Similarly, the afterEach function makes it possible to execute code after each
of the specs have completed.
100 Chapter 6 Unit Testing and Decorators
Matchers
So far, code testing has been performed by following expect with toEqual. This is
shown in the following code:
expect(fName.getLastName()).toEqual("Bond");
expect accepts a value of any type, and this value is called the actual value. This is
usually a variable or the value returned by a method/function.
The result of expect is chained to a special function called a matcher. A matcher's
purpose is to compare the actual value to an expected value or an expected condition. In
the above example, the matcher is toEqual, but Jasmine provides many more matchers
for performing comparisons. Table 6.1 lists 13 of them.
Table 6.1
Jasmine Matcher Functions
Method Description
toEqual(expected) Returns true if the actual value equals the expected value
(uses == for the equality test)
toBe(expected) Returns true if the actual value equals the expected value
(uses === for the equality test)
toBeGreaterThan Returns true if the actual value is greater than the expected
(expected) value
toBeLessThan(expected) Returns true if the actual value is less than the expected
value
toMatch(pattern) Returns true if the actual value matches the given pattern
101 Chapter 6 Unit Testing and Decorators
Jasmine tests are launched when the test code is loaded in a web page. The ch6/jasmine_
demo project shows how this works. The src/index.html file loads a series of required
Jasmine scripts. In its body, it loads the file to be tested (app/app.js) and the file containing
the tests (test/test.js). Listing 6.1 shows the content of src/index.html.
102 Chapter 6 Unit Testing and Decorators
<html>
<head>
<title>Jasmine Demonstration</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/
ajax/libs/jasmine/2.6.1/jasmine.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/
jasmine/2.6.1/jasmine.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/
jasmine/2.6.1/jasmine-html.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/
jasmine/2.6.1/boot.min.js"></script>
</head>
<body>
<script src="./app/app.js"></script>
<script src="./test/test.js"></script>
</body>
</html>
In the ch6/jasmine_demo project, the test code is in the src/test/test.ts file. The test
suite performs three specs based on the ExampleClass file defined in src/app/app.ts.
Listing 6.2 shows what the test code looks like.
// Initialize variable
let example: ExampleClass;
beforeEach(() => { example = new ExampleClass(); });
// Perform tests
it("should be colored red", () => {
expect(example.color).toBe("red"); });
If you load the web page in a browser, the Jasmine framework will display the results
of the specs in the test suite. Figure 6.1 shows what the result looks like.
103 Chapter 6 Unit Testing and Decorators
A green stripe indicates that each spec in the test suite executed successfully. If a spec
fails (the matcher function returned false), Jasmine will display a red band and identify
which spec failed and how.
The Jasmine web page displays the test results graphically, but this isn't ideal if you
want to run tests on the command line. It's also unsuitable if you want to automate the
testing process with scripts. These issues can be resolved by running Jasmine tests within
the Karma framework.
6.1.2 Karma
While Jasmine displays tests in a single browser, Karma can launch tests in multiple
browsers and return the results on the command line. This makes it possible to examine
test results programmatically. Karma supports a number of different test utilities, but this
discussion focuses on using Karma to automate Jasmine tests.
In the Node.js environment, the Karma toolset is provided in the karma package.
Karma-Jasmine integration is provided in the karma-jasmine package. The install script in
this book's example code installs both.
Configuring Karma
To execute tests, Karma needs a configuration file that identifies which files to test and the
manner in which the tests should be performed. The simplest way to construct this file is
to run karma init at the command line. This asks a series of questions and generates a
configuration file called karma.conf.js.
karma.conf.js defines a function named module.exports that calls another
function named config.set. This accepts a configuration object whose keys should
include the following:
104 Chapter 6 Unit Testing and Decorators
module.exports = function(config) {
config.set({
Karma has more configurable features than those listed above. To see the full list, visit
the main site at http://karma-runner.github.io/0.13/config/configuration-file.html.
105 Chapter 6 Unit Testing and Decorators
Once the configuration file is ready, executing a Karma test requires two simple steps:
1. At a command line, change to a directory containing the configuration file
2. Execute karma start
This output is more convenient to analyze than a Jasmine test, which requires the user
to open a browser to view the test results.
Karma's operation can be improved further by changing its reporter to display the
titles of Jasmine specs instead of simply 1 of 3 and 2 of 3. To configure this, install
the karma-spec-reporter package using npm. Then, in the karma.conf.js file, set the
reporters key equal to an array containing only "spec".
Jasmine and Karma are excellent tools for testing TypeScript code. Another reason
to learn about them involves Protractor. The Protractor framework relies on Jasmine to
test Angular web components. Chapter 17 explains how Protractor makes it possible to
perform end-to-end testing.
106 Chapter 6 Unit Testing and Decorators
6.2 Decorators
Decorators make it possible to alter the characteristics of classes, methods, properties, and
parameters. They play an important role in Angular, which uses parameter decorators as
part of its dependency injection process.
From a developer's point of view, a TypeScript decorator is a special type of function.
Every decorator function has three properties:
1. It belongs to one of four function types: PropertyDecorator, ClassDecorator,
MethodDecorator, or ParameterDecorator.
2. Decorator functions are invoked by preceding a portion of code with the function's
name preceded by @.
3. Decorators are read by the compiler, which modifies the affected code as it generates
JavaScript. In other words, the code in a decorator function executes at runtime, but
the code modification is performed during compilation.
This section discusses the four types of decorator functions. The last part of the
section presents an application that demonstrates how they can be used in practice.
A property decorator makes it possible to access a class's property when it's read or
written. In the following code, the lastName property of the Employee class has a
decorator called nameChange:
class Employee {
@nameChange
public lastName: string;
constructor(...) { ... }
}
The first parameter is the Object containing the property and the second is the
property's key. desc is the property's descriptor, and its fields determine the property's
behavior. If the property is intended to have getter/setter methods, its descriptor must
provide values for the following fields:
• enumerable — identifies if the property should be part of the object's property
enumeration
• configurable — identifies if the property can be changed or deleted
• get — the function to be invoked when the property's value is read
• set — the function to be invoked when the property's value is written
Now let's look at how to code the decorator function. The signature of a
PropertyDecorator function is as follows:
The first parameter is the prototype of the object to be modified. The second is the
name of the property's key. To modify a class's property, the function must perform three
steps:
1. Delete the existing property by calling delete.
2. Create a new property by calling Object.defineProperty with a suitable
descriptor.
3. Provide code for the new property's getter and setter methods.
The following code shows how these steps can be accomplished. The function's name
is propChange, and as specified in Object.defineProperty, the property's getter
method is getFunc and its setter method is setFunc.
In this code, the first two arguments of Object.defineProperty are the same
arguments passed into propChange's parameter list. The third argument of Object.
defineProperty provides a description of the new property, including the names of its
getter method (getFunc) and setter method (setFunc).
This descriptor performs two actions. When the decorated property is read, the
descriptor adds ", Esquire" to the property's value. When the property is written, it
adds ", Jr." to the property's value. This isn't particularly useful, but it's easy to use
similar descriptors to restrict access to data or check for errors.
To understand class decorators, it's important to see how JavaScript constructors relate to
an object's prototype. There are three points to keep in mind:
1. A JavaScript function becomes a constructor when it's called with the new keyword.
2. If Thing() is a function, calling new Thing() returns an object t such that
t.constructor = Thing and t instanceof Thing returns true.
3. Every function has a property called prototype. When new Thing() is called,
every property of Thing's prototype becomes a property of the returned object.
If this makes sense, class decorators won't pose much difficulty. Put simply, a class
decorator receives a constructor and can use it to modify aspects of the class. To be
precise, it accesses the constructor's prototype property to modify objects created by the
constructor.
109 Chapter 6 Unit Testing and Decorators
TFunction is any type that extends the Function type. The function passed to
func_name is the constructor of the decorated class. func_name 's return value is a
replacement function to serve as the new constructor.
This replacement constructor has complete control over how new instances are
created. In coding the constructor, there are three items to consider:
1. A new constructor can invoke the old constructor with the apply method.
2. If the old constructor accepted arguments, the new constructor can access the same
arguments in the apply method.
3. To ensure that instanceof works properly, the new constructor's prototype
property should be set equal to the old constructor's prototype property.
An example will clarify how class decorators work. Consider the following class,
whose constructor accepts two arguments:
@changeColor
class Balloon {
constructor(public color: string, public volume: number) {}
}
The changeColor decorator receives the old constructor and returns a new one that
always sets the color property to blue.
return newFunc;
}
This function accepts the oldFunc constructor and returns the newFunc
constructor. newFunc performs the same operation as oldFunc and sets its prototype
property to that of oldFunc. The only difference is that newFunc changes the first
argument so that the color property is set to blue.
In a JavaScript object, a method is like any other property except that it defines a function.
For this reason, method decorators have a lot in common with property decorators.
While a property decorator returns a replacement property, a method decorator returns a
function definition to replace the old one.
The signature of a method decorator function is given as follows:
The first two arguments are like those of a property decorator—the first provides the
object containing the method and the second identifies the method's name. The third
parameter implements the TypedPropertyDescriptor<T> interface, which is defined
in the following way:
interface TypedPropertyDescriptor<T> {
enumerable?: boolean;
configurable?: boolean;
writable?: boolean;
value?: T;
get?: () => T;
set?: (value: T) => void;
}
class OddPunctuation {
@prependTilde
appendExclamation(arg: string): string {
return arg.concat("!");
}
}
The following code defines a new function that calls the old function
(appendExclamation) and prepends a tilde to its return value.
return desc;
}
This decorator updates desc.value with a new function definition. This function
invokes the original function, which is stored as origMethod. Then it modifies the result
and returns the modified value.
Parameter decorators are unique among the four decorator types because they aren't
intended to replace or modify anything. But a parameter decorator can add data to the
containing class or method using regular operations.
The signature of a parameter decorator function is given as follows:
The first parameter provides the containing object, the second identifies the method's
name, and the last parameter is the position of the decorated parameter in the method's
parameter list. The return value is void, which implies that the function isn't intended to
replace any portion of the decorated code.
I've verified this behavior with my own experiments. That is, I've tried to modify the
decorated parameter's value and its method, but every change is ignored.
Instead of replacing aspects of the decorated code, a parameter decorator can add
metadata to the enclosing object. For example, the following parameter decorator adds a
new property to the prototype of the enclosing object:
The ch6/decorator_demo project demonstrates how all four decorators are used in
practice. It defines two nearly-identical classes: Book and DecoratedBook. The Book
class has no decorators and DecoratedBook has four:
1. changeProp — Appends ", Esquire" to the decorated property when it's read.
2. changeClass — Changes the constructor's third argument to 400.
3. changeMethod — Prepends "more than" to the result of the annotated method.
4. changeParam — Adds a publisher property to the object's prototype.
As shown in Listing 6.4, the app.ts code creates two objects with the same parameters:
@changeProp
public author: string;
public title: string;
public numberOfPages: number;
@methodChange
public displayNumPages(): string {
return this.numberOfPages.toString();
}
}
// Modify a property
function changeProp(target: any, key: string) {
// Modify a method
function methodChange(target: object, key: string, desc: any) {
// Create objects
let b = new Book("Herman Melville", "Moby Dick", 544);
let db = new DecoratedBook("Herman Melville", "Moby Dick", 544);
// Display results
document.writeln("The book ".concat(b.title)
.concat(" was written by ")
.concat(b.author).concat(" and has ")
.concat(b.displayNumPages()).concat(" pages. ")
.concat("The publisher is ").concat(b["publisher"])
.concat(".<br />"));
document.writeln("The book ".concat(db.title)
.concat(" was written by ")
.concat(db.author).concat(" and has ")
.concat(db.displayNumPages()).concat(" pages. ")
.concat("The publisher is ")
.concat(db["publisher"]).concat("."));
In this case, the property decorator modifies the property's value every time its value
is read. This means the decorator will keep appending more text with each access.
6.3 Summary
This chapter has covered the two important topics of unit testing and decorators. These
topics are helpful, but don't be concerned if you don't fully grasp them. You'll still be able
to understand the chapters that follow.
The first topic in this chapter involved unit testing. Jasmine tests JavaScript code
and displays the results in a web page. It can't be used to test TypeScript directly, but if a
TypeScript file is compiled to JavaScript, the Jasmine functions will execute normally. The
Karma framework makes it possible to automate Jasmine testing and display the results of
each test on a command line.
The second topic dealt with decorators. A decorator is a function that reads and/
or replaces aspects of decorated code. A class decorator replaces a class's constructor,
a method decorator replaces a method, and a property decorator replaces a property.
Parameter decorators do not replace parameters, but they can be used to add metadata.
Chapter 7
Modules, Web Components,
and Angular
This section explores the first three points. That is, we'll look at defining modules and
then exporting and importing their features. The section ends with a simple example that
demonstrates how TypeScript modules can be coded.
There are two primary ways to define a TypeScript module. The first involves preceding
a named block with the module keyword. As an example, the following code defines a
module named ExampleMod:
module ExampleMod {
export function addNum(num: number): number {
return num + 17;
}
}
In this case, the ExampleMod module contains one feature: a function called
addNum. It should be clear that a TypeScript file may contain multiple modules similar to
ExampleMod.
This code is easy to understand, but the vast majority of TypeScript modules
don't use the module keyword. Instead, they take advantage of a crucial property: if a
TypeScript file calls import or export at a top level (outside any function, class, or
interface), the compiler will recognize the content of the entire file as one module. The
compiler will name the module according to the file's name without the suffix.
119 Chapter 7 Modules, Web Components, and Angular
If this is the only code in a TypeScript file, the compiler will recognize the code as
a module definition because export is called outside of any code block. The module's
name will be set to the file's name without the suffix. That is, if the code is contained in
AddNum.ts, the name of the module will be set to AddNum.
Chapter 3 explained how the private, protected, and public modifiers make it
possible to set the accessibility of members in a TypeScript class. In a module, every
feature is private (completely inaccessible) unless preceded by export.
As an example, the following code defines a simple module:
const num = 8;
This module has four features, but because of the export keyword, the only feature
that can be accessed by external code is ExampleInterface. Other modules can access
this feature using the import keyword, which will be discussed next.
Modules (and any TypeScript code) can use import to access features from other
modules. TypeScript supports a number of formats for import statements, including the
following:
• import { feature } from 'module_name';
• import { feature1, feature2, ... } from 'module_name';
• import * as name from 'module_name';
120 Chapter 7 Modules, Web Components, and Angular
In these examples, feature is the name of a feature contained in the module named
module_name. As mentioned earlier, module_name is usually the name of the file
containing the module's code without the suffix.
To demonstrate how import is used, suppose that two functions, addNum and
subtractNum, are exported by a module defined in TwoFuncs.ts. Another module can
access the addNum function with the following statement:
As a result of this code, the importing module will be able to invoke addNum as if it
had been defined inside the importing module. Similarly, the following statement accesses
both addNum and subtractNum:
Rather than access a module's features directly, it may be helpful to associate them
with a name and then access them with dot notation. For example, the following statement
imports all the features in TwoFuncs and associates them with the name MyFuncs.
Because of this statement, the module can access addNum as MyFuncs.addNum and
subtractNum as MyFuncs.subtractNum. This dot notation enables the compiler to
distinguish external features from similarly-named features in the module.
If an exported feature is declared as default, it can be imported more simply than other
members. Consider the following code:
Because the exported member is default, an import statement doesn't need curly
braces. So addNum can be imported in the following way:
An import statement can assign an alias to the default feature. The following
statement imports the addNum function and makes it accessible by the alias addNumFunc:
To set an alias for a feature that isn't default, it's necessary to use as alias inside
curly braces. For example, if NotDefault is the name of an exported class, the following
import statement makes it accessible by the alias ND:
As a result of this statement, the importing module will be able to access the ND
feature from module_name using the alias NotDefault.
If you attempt to compile a module as if it were regular TypeScript, the compiler will flag
an error: Cannot compile modules unless the '--module' flag is provided. To resolve this
error, you need to tell the compiler what type of module it should create.
In tsconfig.json, the module format can be set by providing a value for the module
property in the compilerOptions field. This can be set to one of the following values:
1. CommonJS — The CommonJS format (http://www.commonjs.org) makes it
possible to use JavaScript in applications outside the browser. This module format is
employed by the Node.js framework.
2. Amd — The Asynchronous Module Definition (AMD) format was developed as part
of the RequireJS framework (http://requirejs.org).
3. UMD — The Universal Module Definition (UMD) format was developed to provide
compatibility with the CommonJS module format and the AMD module format.
4. System — The System.js loading framework (https://github.com/systemjs/systemjs)
defines a format for bundling modules.
5. es2015/es6 — The ECMAScript 2015 specification (ES6) identifies a module
format similar to that of TypeScript.
6. none — Don't generate code for any of the preceding formats.
If the target property is set to anything other than ES6, the default module format
is CommonJS. If target is set to ES6, the default format is es2015/es6. The following
discussion will show how TypeScript modules can be compiled.
122 Chapter 7 Modules, Web Components, and Angular
The ch7/module_demo project demonstrates how TypeScript modules can be coded and
compiled. The src/app folder contains three TypeScript files:
• AddNumbers.ts — Exports a class named AddNumbers
• SubtractNumbers.ts — Exports a class named SubtractNumbers
• app.ts — Imports the two classes, creates an instance of each class, and then invokes
the printMessage method of each instance.
Listing 7.1 presents the code of app.ts. It identifies modules using the names of their
files without the suffixes.
In the tsconfig.json file, the target property is set to ES6 and the module property
is set to es6. This tells the compiler to generate a module in the ES6 format. If you look at
the compiled code, you'll see that the only difference is the lack of type declarations. This
should make sense. TypeScript is essentially ECMAScript 6 with data types, and an ES6
module is essentially a TypeScript module without types.
Unfortunately, modules can't be loaded into HTML like regular JavaScript code. In
the ch7/module_demo project, this means app.js can't be loaded into a web page using a
<script> element. If you attempt to open src/index.html in a browser, you'll receive an
error.
Instead, modules must be processed using a special utility called a loader. In some
cases, a loader and its modules are combined together into a package called a bundle.
Unlike modules, bundles can be loaded into a page with <script> elements.
At the time of this writing, Angular's preferred loader is Webpack, which is freely
available at https://webpack.github.io. In this book, we won't access Webpack directly.
Instead, we'll access Webpack through a tool called the Angular command line interface
(CLI). A later section of this chapter will discuss the Angular CLI in detail.
123 Chapter 7 Modules, Web Components, and Angular
The goal of this section is to explain these technologies and why they're helpful. The
better you understand them, the more comfortable you'll be with web development.
124 Chapter 7 Modules, Web Components, and Angular
Setting the style and behavior of an HTML element can require a lot of markup. The
HTML structure at stackoverflow.com contains up to nine levels of <div> elements just to
define a list and a set of labels. Understanding the markup is difficult and it's even harder
to upgrade the application and fix errors.
With custom HTML elements, developers can set an element's appearance and
behavior without cluttering up the page. For example, instead of placing a <table> inside
a set of nested <div>s, a custom table could be defined with the following element:
This makes the page easier to read and understand, but a greater benefit is the
ability to add components defined by third parties, such as <google-supertable> or
<oracle-dbform>. Like many pieces of software, these components will cost money
initially, but as the technology matures, they'll be provided freely as open-source.
The official documentation on custom elements can be found at the W3C site at
https://w3c.github.io/webcomponents/spec/custom. This presents criteria for the names
of custom HTML elements:
• May contain letters, digits, dots, hyphens, and underscores
• Can't contain uppercase letters
• Can't conflict with existing elements or CSS properties
• Must contain at least one hyphen
A web component's structure and appearance is defined by its internal HTML. For
example, a web component named <custom-form> will probably contain a <form>
element to define the form, and that element may contain multiple <input> elements and
a <button> element.
125 Chapter 7 Modules, Web Components, and Angular
<template>
<form>
Field1: <input type='text' name='field1'><br />
Field2: <input type='text' name='field2'><br />
<input type='submit' value='Submit'>
</form>
</template>
Unlike regular markup, a browser doesn't render a component's template when the
component's page is loaded. Instead, each HTMLTemplateElement has a content
property that stores the template's content in a DocumentFragment. To render a web
component's template, its content must be specifically integrated into the page.
Chapter 5 explained how the document object model (DOM) structures elements in a web
page. In a regular web page, every element belongs to the same DOM. This means their
styles are governed by the same stylesheet and if one element has its id attribute set, no
other element in the page can have the same id.
When a web component is placed into a document, its appearance, behavior, and
namespace can be isolated from the rest of the document. For example, suppose that
a document specifies that every button should be blue. Does that mean that a web
component's button should be blue? Maybe, maybe not. The component's dependence or
independence should be determined by the developer, not the document.
To understand how this works in practice, it's important to see the relationship
between the elements of a component's template and the DOM tree containing the
component. This is a complex topic, and the official documentation (http://w3c.github.io/
webcomponents/spec/shadow) defines three fundamental terms:
• shadow tree — a tree of nodes associated with a DOM element
• shadow host — an element with an associated shadow tree
• shadow root — the root node of an element's shadow tree
The shadow host is a regular component in the top-level document. Its shadow
root and its children belong to a different node tree. This means the shadow root and its
children aren't descendants of the shadow host. It also means that Document methods
such as getElementById won't return an element in the shadow tree.
126 Chapter 7 Modules, Web Components, and Angular
An example will help make this clear (I hope). Consider the <custom-form>
element discussed earlier. The <custom-form> element is a shadow host, and belongs
to the top-level DOM. But the elements in the <custom-form> template belong to the
component's shadow tree. This shadow tree is commonly referred to as the component's
shadow DOM.
A shadow host can be accessed with traditional DOM methods like document.
getElementById. But these methods can't access the nodes of the shadow tree. The
W3C documentation defines functions that create and access nodes in the shadow DOM,
but this API lies beyond the scope of this book.
The last technology underlying web components may not seem groundbreaking because it
involves importing files into a web page. But HTML imports can play an important role in
a web component's operation. In markup, an HTML import is a <link> element with two
important characteristics:
1. The rel attribute is set to import.
2. The href attribute identifies the URL of an HTML file.
For example, the following HTML import reads in a local file called baz.html:
The difference between an HTML import and an <iframe>'s file inclusion is that the
imported HTML doesn't require a separate frame. The imported HTML is inserted into
the main page, but the document model of the imported HTML is kept distinct from the
main page's DOM.
The imported HTML can be accessed in JavaScript using the import property of
the link element. For example, the following code accesses the <link> element as an
HTMLLinkElement and then reads its content:
With HTML imports, a web page can import web components from a server or a
repository. Then JavaScript code can integrate the web component's template into the
page. HTML imports aren't important for Angular, but they're required by Google's
Polymer toolset.
127 Chapter 7 Modules, Web Components, and Angular
For example, the overall structure of a basic Angular component could be given as
follows:
@Component({
selector: 'basic-comp',
template: '...'
})
export class ClassName {
...
}
Because of the export keyword, the compiler recognizes this as a module. After the
module is compiled, it can be inserted into a page with the <basic-comp> tag. The name
of the tag and the name of the class don't need to be related in any way. However, the tag's
name should meet the four requirements discussed earlier:
• May contain letters, digits, dots, hyphens, and underscores
• Can't contain uppercase letters
• Can't conflict with existing elements or CSS properties
• Must contain at least one hyphen
128 Chapter 7 Modules, Web Components, and Angular
A component's template defines its appearance and internal structure. You can think of it
as a subdocument within the overall document.
Angular provides two ways of defining a component's template. The first involves
adding a template field to the @Component annotation. For example, the following field
sets the component's template to a simple message:
If the template's HTML requires multiple lines, the quotes around the HTML can be
replaced with backticks (``). This is shown in the following template definition:
template:
`<h2>
Hello Angular!
</h2>`
templateUrl: 'template.html'
7.3.2 Directives
template:`
<ol>
<li *ngFor='let prime of prime_numbers'>
Prime numbers: {{ prime }}
</li>
</ol>`
Angular provides a set of core directives that serve a wide range of roles. If these
aren't sufficient, developers can code custom directives that can be associated with
template elements. Chapter 9 discusses Angular's core directives and explains how to code
custom directives.
The installation may take some time. This is because npm installs all of the
dependencies needed to build and launch Angular apps. This includes the Webpack
loader, RxJS, and all the Angular packages, whose names start with @angular.
When the installation is complete, you can access the CLI using ng. This command
can perform a wide range of Angular-related operations, and you can see the full list by
entering the following command:
ng help
If you enter this command, you'll see a wide range of operations that can be
automated through the CLI. Table 7.1 lists the primary categories.
130 Chapter 7 Modules, Web Components, and Angular
Table 7.1
Categories of CLI Operations
Operation Description
build Builds the application and places the compiled code in the dist directory
completion Generates completion scripts
doc Opens Angular documentation
e2e Performs end-to-end testing for the project
eject Ejects the application with Webpack configuration
generate Generates new code
get Obtains a value from the project configuration
help Lists the CLI's commands and operations
lint Checks the project for potential errors
new Creates a new Angular project
serve Builds and launches the application
set Sets a value in the project's configuration
test Performs the project's test suite
version Identifies the CLI version
xi18n Extracts strings for internationalization
In my opinion, CLI's most useful feature is the ability to create an Angular project and
install all the packages needed to build and launch it. To see how this works, I recommend
that you change to a directory that doesn't contain a package.json file. Then execute the
following command:
The -sg flag tells CLI not to create a Git repository for the project. The -st flag tells
CLI not to create spec files. Table 7.2 lists these and other flags used in ng new.
Table 7.2
CLI Project Creation Flags (ng new)
Flag Default Value Description
-d false Run through without making any changes
-v false Print details about the operation
-lc false Automatically link the @angular/cli package
-si false Skip package installation
-sg false Skip initializing a Git repository
-st false Skip creating spec files
-sc false Skip committing the first commit to Git
-dir -- The directory name to create the app in
-sd src The name of the source directory
--style css The default extension of style files
-p app The prefix to use for component selectors
--routing false Generate a routing module
-is false Assign an inline style
-it false Assign an inline template
When ng new completes, you'll see a new directory called FirstProject. This top-level
directory contains three folders:
• node_modules — contains the project's dependencies
• src — contains the project's source code
• e2e — contains files related to end-to-end testing (discussed at length in Chapter 17)
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'app works!';
}
This should look familiar. The @Component decorator identifies the AppComponent
class as a component class. This component can be inserted into an HTML document
with the <app-root> tag and the component's appearance is defined by the template in
app.component.html. The template's style is set by the content of app.component.css.
The component's template is almost trivially simple. Listing 7.3 presents the content
of app.component.html.
<h1>
{{title}}
</h1>
This displays the value of the component's title variable between <h1> tags.
Therefore, the component's appearance simply consists of the app works! string.
The content of app.component.ts may be easy to understand, but other files in the
project will probably be incomprehensible. To see what I mean, look at src/main.ts and
src/app/app.module.ts. Chapter 8 will clarify what's going on and will further discuss the
project's structure.
The process of building a project entails compiling the TypeScript and packaging the
JavaScript into chunks for the loader. In an Angular CLI project, this is accomplished by
changing to the project's top-level directory and entering the following command:
ng build
133 Chapter 7 Modules, Web Components, and Angular
Like ng new, this accepts a number of flags that configure how the operation should
be performed. Table 7.3 lists these flags and provides a description of each.
Table 7.3
CLI Project Build Flags (ng build)
Flag Default Value Description
-t development Identifies the build target (development or
production)
-e -- Defines the build environment
-op dist Sets the output path
--aot false Build using Ahead of Time compilation
-sm true Create source maps
-vc true Create a separate chunk containing vendor libraries
-bh -- Base URL for the application
-d -- URL where files should be deployed
-v false Add more details to output logging
-pr true Log progress to the console as the build progresses
--i18nFile -- Localization file for internationalization
--i18n-format -- Format of the localization file
--locale -- Locale to use for internationalization
-ec -- Extract CSS from global styles into CSS files instead
of JavaScript files
-w false Perform build when files change
-oh -- Define the output filename cache-busting hashing
mode
-poll -- Sets the poll time for watching files
-a -- Sets the desired name for the app
--stats-json false Generates a file that can be used for Webpack analysis
By default, CLI builds Angular projects in development mode. This means that it
will generate source maps that simplify the debugging process. It will also perform extra
checks during Angular's change detection process.
When the build is finished, the compiled files will be placed in the top-level dist
directory. This contains JavaScript files (*.js), source maps (*.js.map), index.html, and a
favicon.ico file.
134 Chapter 7 Modules, Web Components, and Angular
Hash: f00edc02553067b1ab7b
Time: 5965ms
Webpack uses the term chunk instead of bundle for a JavaScript file produced by the
build process. CLI packages the compiled code into different chunks to improve loading
performance. In this example, there are five chunks located in the dist directory:
• polyfills.bundle.js — Provides polyfills, which allow browsers to access advanced
features
• main.bundle.js — Contains code compiled from the main TypeScript files
• styles.bundle.js — Sets the styles used in the application
• vendor.bundle.js — Contains the Angular libraries
• inline.bundle.js — Contains code that Webpack needs to load chunks
ng build -prod
ng build --target=production
135 Chapter 7 Modules, Web Components, and Angular
The Angular CLI installation provides a development server called the NG Live
Development Server. This loads the project's index.html file and deploys the output files
to the URL http://localhost:4200.
To start the server and launch the web application, enter the following command:
ng serve -o
Table 7.4
CLI Project Launching Flags (ng serve)
Flag Default Value Description
-d http://localhost URL where files will be deployed
-p 4200 Port to listen to
-H localhost Name of the host to listen to
-pc -- Proxy configuration file
-ssl false Serve using HTTPS
--sslKey -- SSL key to use for serving HTTPS
--sslCert -- SSL certificate to use for serving HTTPS
-o false Opens the URL in the default browser
-lr true Identifies whether to reload the page on changes
--live -- The URL that the live reload browser client will use
ReloadClient
--hmr false Enable hot module replacement
The last flag in the table enables or disables hot module replacement (HMR). This is a
useful feature of Webpack that replaces old modules with new ones without reloading the
browser. This simplifies the testing process because the browser will immediately display
modules as they change. Note that this is only available in development mode.
136 Chapter 7 Modules, Web Components, and Angular
7.5 Summary
If you jump into Angular development without preparation, you may find the subject
very difficult to grasp. The goal of this chapter has been to gradually introduce the topic
by proceeding from TypeScript modules to the theory of web components. Angular
applications are based on components, and each component is a TypeScript module that
serves as a web component.
The first part of this chapter explained that modules provide encapsulation of
features, which may include classes, interfaces, functions, or variables. Features can be
made accessible using the export keyword and can be accessed from other modules
using the import keyword. You'll encounter export and import throughout this book,
so it's crucial to understand what they accomplish.
To enable modularity in web development, the W3C drafted a standard that presents
the basic characteristics of web components. These characteristics include custom
HTML elements, HTML templates, shadow DOM, and HTML imports. By using these
technologies, web components dramatically simplify the process of web development and
enable a greater amount of code reuse.
Angular serves as a practical implementation of web components. Each web
component is coded as a TypeScript module that exports a specially-annotated class. A
class's annotation defines many aspects of the component, including its custom HTML tag
and its appearance in the page.
Angular projects can be difficult to work with, particularly when it comes to
deployment. The Angular CLI simplifies the development process by performing common
operations with commands starting with ng. The operations discussed in this chapter
include creating projects, building projects, and launching the compiled application.
Chapter 8
Fundamentals of
Angular Development
Looking at these topics, it's difficult to pick one or two that are more important than
the others. All of them are critically important and every Angular developer should have
a solid grasp of each. But before we delve into the code, it's good to be familiar with the
Angular Style Guide and its recommendations.
138 Chapter 8 Fundamentals of Angular Development
When it comes to structuring a project, the style guide's recommendations are simple:
1. The project's code should be placed inside a top-level folder named src.
2. The application's module (a class decorated by @NgModule) should be placed in the
project's root folder, src/app.
3. If a component needs additional files (*.html for the template, *.css for styles, and so
on), the component and its files should be placed in a separate folder named after
the component.
The last point is important. A central principle in the style guide is being able to
locate files quickly. Therefore, if an HTML file defines a component's template, the
component file and the template file should be in the same folder.
The style guide also provides recommendations related to a project's files. The main point
is that each TypeScript file in the root folder (src/app) and its subfolders should have two
suffixes. The first suffix identifies the role served by the code in the Angular framework.
The second suffix identifies the text format (*.ts).
For example, if a file defines a component, its name should be name.component.ts.
If a TypeScript file defines a module, its name should be name.module.ts. Similar double
suffixes include *.directive.ts, *.pipe.ts, and *.service.ts. Directives, pipes, and services will
be discussed in later chapters.
139 Chapter 8 Fundamentals of Angular Development
Just as TypeScript files should be named according to their Angular roles, TypeScript
classes should also be named for their roles. The name of a component class should end
with Component and the name of a module class should end with Module. Therefore,
the WidgetComponent class should be defined in widget.component.ts and AppModule
should be defined in app.module.ts.
The style guide strongly supports the Single Responsibility Principle (SRP), which
states that each file should define one and only one thing. That is, a component file should
only define one component and a template file should only define the template of one
component. For large projects, following this principle can result in a vast number of files,
but each file is easy to find and its purpose is immediately apparent.
Additional code conventions are given as follows:
• Classes should be named using upper camel case (WidgetComponent).
• Properties and methods should be named using lower camel case (addNumbers).
• Put third-party import statements (@angular/core) and application import
statements into separate blocks. Alphabetize the import statements in each block.
The guide also has recommendations for component selectors. Each selector should
be lower case and consist of a prefix followed by a hyphen, as in app-widget.
From this chapter onward, this book assumes that you've installed the Angular CLI.
Each project folder (ch8, ch9, and so on) consists of an app folder, and if you create a CLI
project with ng new, you should be able to replace the app folder of the CLI project with
the app folder of the example project. Then you can build the project with ng build and
launch the application with ng serve.
140 Chapter 8 Fundamentals of Angular Development
An example will clarify how these arrays are set. Listing 8.1 presents the root module
code in the FirstProject project created at the end of Chapter 7:
@NgModule({
declarations: [ AppComponent ],
imports: [
BrowserModule,
FormsModule,
HttpModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
141 Chapter 8 Fundamentals of Angular Development
8.3 Bootstrapping
So far, we've looked at Angular's components and modules, which boil down to decorated
TypeScript classes. But it's not enough to define classes. To serve a purpose, an application
needs to create an instance of a class and start calling methods. In Angular, this launching
process is called bootstrapping.
The Angular Style Guide has three recommendations related to bootstrapping:
• Put bootstrapping and platform logic in a file named main.ts.
• Include error handling in the bootstrapping logic.
• Avoid putting app logic in the main.ts. Place it in a component or service instead.
To understand what "bootstrapping and platform logic" refers to, it helps to look at
actual code. Listing 8.2 presents the main.ts file in the CLI-generated FirstProject.
142 Chapter 8 Fundamentals of Angular Development
The goal of this section is to make this code intelligible. The first part of the section
looks at environment settings and the difference between development and production
mode. The second and third parts explain what platformBrowserDynamic and
platformBrowser accomplish.
By default, every Angular CLI project has a folder in its src directory named
environments. This contains two files: environment.ts and environment.prod.ts. Both
files define modules that export an object, environment, that has a single boolean
field, production. In environment.ts, environment.production is set to false. In
environment.prod.ts, environment.production is set to true.
The main.ts code uses this value to determine whether enableProdMode should be
called. This function disables Angular's development mode, which reduces the number of
checks performed during execution. Also, Angular CLI uses a different set of procedures
to build projects in production mode instead of development mode.
By default, main.ts imports the environment object from environment.ts,
which means default builds are performed in development mode. But if ng build is
executed with the --prod flag, main.ts will access environment.prod.ts instead, which
configures production mode. Note that developers can add fields to both files to provide
environment data.
143 Chapter 8 Fundamentals of Angular Development
By default, Angular templates are compiled at runtime. This means compilation takes
place in the browser as the application loads. This in-browser compilation is called
Just-in-Time (JIT) compiling.
JIT compilation has two main drawbacks. First, the user has to wait for the browser to
compile the code, which may take a significant amount of time. Second, precompiled code
is larger than compiled code.
JIT compiling has been the norm since the first release of Angular 2, so this is the
most common compilation method. It's also very simple. To launch an application with
JIT compilation, the module class needs to call platformBrowserDynamc() to obtain a
PlatformRef and then call bootstrapModule with an Angular module class.
As discussed earlier, an Angular module class is a TypeScript class decorated with
@NgModule. In general, this will be the application's root module, which is commonly
named AppModule.
If you open the FirstProject directory and look in the node_modules/.bin folder, you'll
find an executable named ngc. This runs the Angular compiler, which makes it possible to
compile code before it's loaded. This is called Ahead-of-Time (AOT) compiling.
Compiling Angular code in advance provides a number of advantages over JIT
compiling, including faster rendering, smaller download size, and better security. AOT
compiling is also necessary for lazy loading, which will be discussed in Chapter 12.
Before you can take advantage of AOT compiling, two dependencies are required:
Like tsc, ngc reads settings from tsconfig.json. The settings for AOT compilation are
similar to those of regular compilation, but there are two points to keep in mind:
1. The module field in compilerOptions must be set to es2015
2. A top-level angularCompilerOptions object sets the genDir field to the
name of the directory to contain AOT compilation results. It should also set
skipMetadataEmit to true.
"extends": "../tsconfig.json",
"compilerOptions": {
"module": "es2015"
},
"angularCompilerOptions": {
"genDir": "aot",
"skipMetadataEmit" : true
}
Because genDir is set to aot, the results of the AOT compilation will be placed in the
aot directory. If the tsconfig-aot.json file is in the src directory, the build can be executed
by running the following command from the project's top-level directory:
"node_modules/.bin/ngc" -p src/tsconfig-aot.json
After this finishes, the build files will be stored in the src/aot directory. For each
component file (*.component.ts), you'll find a factory file (*.component.ngfactory.ts) and
a summary file (*.component.ngsummary.ts).
After the AOT build is complete, three changes need to be made to main.ts before the
application can be deployed:
• References to platformBrowserDynamic should be replaced with
platformBrowser
• The bootstrapModule method should be replaced with
bootstrapModuleFactory
• The argument of bootstrapModuleFactory must be AppModuleNgFactory
Listing 8.3 presents main.ts with the changes needed for AOT compilation:
import { AppModuleNgFactory }
from './aot/app/app.module.ngfactory';
The projects in this book rely on JIT compilation. This is because JIT compilation is
easier and requires fewer files. But when you start releasing for production, remember the
advantages of AOT compilation.
145 Chapter 8 Fundamentals of Angular Development
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'app works!';
}
<h1>{{title}}</h1>
The AppComponent class contains a property called title whose value is set
to 'app works!'. The template accesses the property by placing its name in double
curly braces: {{ title }}. As a result, the component displays app works! when
instantiated within the web page. The process of inserting a component's properties into
the component's template is called property interpolation.
If the value of title changes, the value displayed in the template will change
immediately. But the template can't alter the value of title. Because the data transfer is
one-way, this interaction is referred to as one-way data binding.
Template expressions can contain more than just property names. They can hold
constant values (numbers and strings) and the results of simple JavaScript operations. For
example, {{3 * 6 + 2}} will be displayed as 20 and {{'Angular' + 'JS'}} will be
displayed as AngularJS.
An expression can operate on class properties, so if you want to concatenate
firstVal and secondVal, the expression is {{firstVal + secondVal}}. Similarly,
if num equals 5, the expression {{ num + '0' }} will be displayed as 50.
Similar expressions can be inserted in the template wherever strings are appropriate.
For example, the class attribute of a <button> could be set to {{ buttonClass }} and
the id of a <div> could be set to {{ divId }}.
Lastly, an expression can be used to execute JavaScript functions. If returnTwo()
always returns a value of 2, the {{ returnTwo() }} expression will evaluate to 2.
146 Chapter 8 Fundamentals of Angular Development
8.4.1 Pipes
Data displayed in a template can be formatted using pipes. These expressions take the
form {{ data | pipe }}, where pipe specifies how data should be formatted.
An example will make this clear. Suppose the component's class has a string property
called firstName. The uppercase pipe displays text using uppercase characters, so the
following expression configures the template to display firstName in uppercase:
{{ firstName | uppercase }}
Angular provides a number of similar pipes, but they aren't all as easy to use as
uppercase. Most have arguments, separated by colons, that constrain how the data
formatting should be performed. Table 8.1 lists 11 of these pipes and their arguments.
Table 8.1
Angular Pipes
Pipe Name Argument(s) Description
number digitInfo Displays a number in decimal form with the given
number of digits
percent digitInfo Displays a number as a percentage with the given
number of digits
currency code, symbol, Formats a number as a currency. If no symbol is given,
digitInfo the filter uses the currency of the default locale.
date format Formats a date as a string with the given format
json -- Converts an object to a string in JavaScript Object
Notation (JSON)
lowercase -- Converts text to lowercase
uppercase -- Converts text to uppercase
slice start, end Creates a substring or converts an array into a list
async -- Waits for a value from an Observable or Promise
i18nSelect mapping Selects string according to a value
i18nPlural mapping Selects string according to the number of elements
The following discussion explores most of these pipes and demonstrates how they're
used. But i18nPlural and i18nSelect won't be discussed here. They'll be introduced
in Chapter 15, which presents the topic of internationalization (i18n) in detail.
147 Chapter 8 Fundamentals of Angular Development
The number, percent, and currency pipes format numbers in the template. If any of
these are used to format a string, Angular will raise an Invalid Argument exception.
If the number pipe is used without arguments, it won't perform any formatting. That
is, {{25 | number}} displays 25 and {{3.1415 | number}} displays 3.1415.
The number and percent pipes both accept an optional digitInfo argument that
identifies which digits should be displayed. This argument can be given in one of two
forms:
1. x.y, where x is the minimum number of digits before the decimal and y is the
maximum number of digits after the decimal
2. x.y-z, where x is the minimum number of digits before the decimal, y is the
minimum number of digits after the decimal, and z is the maximum number of
digits after the decimal.
The following examples demonstrate how number and percent are used:
• {{25 | number : '2.0'}} displays 25
• {{25 | percent : '2.0'}} displays 2,500%
• {{0.1234 | number : '2.3'}} displays 00.123
• {{0.1234 | percent : '2.3'}} displays 12.340%
• {{123.456789 | number : '2.2-4'}} displays 123.4568
• {{0.333 | percent : '2.2-4'}} displays 33.30%
If the currency pipe is used without arguments, the ISO 4217 code for the local
currency will be prepended. In the United States, {{3 | currency}} will be displayed
as USD3.00. In Europe, {{5.5 | currency}} will be displayed as EUR5.50.
The first optional argument of currency identifies the code to be used. No matter
where you live, if you want to express 17.50 in Canadian dollars, you can display this as
{{17.50 | currency : 'CAD'}}, which will be displayed as CAD17.50.
The second optional argument identifies if the currency's symbol should be used
instead of the ISO 4217 code. The third optional argument is the same digitInfo
argument used by number and percent. The following examples demonstrate this:
• {{ 2.50 | currency : 'USD' : true }} evaluates to $2.50
• {{ 2.50 | currency : 'USD' : true : '2.2' }} evaluates to $02.50
• {{ 4 | currency : 'CAD' : false : '1.2' }} evaluates to CAD4.00
148 Chapter 8 Fundamentals of Angular Development
date
The date pipe expects the date to be provided in an ISO 8601 format or as the number
of milliseconds since Jan 1, 1970. This pipe accepts an optional argument that determines
how the date/time should be formatted. This can contain a wide range of elements, with
y identifying the year with four values, MMM setting the month's three-letter abbreviation,
and dd setting the day of the month using two values. The following examples show how
these and other formatting elements are used.
• {{1440253151182 | date }} evaluates to Aug 22, 2015
• {{1440253151182 | date: 'MMddyy' }} evaluates to 082215
• {{1440253151182 | date: 'MMMMd'}} evaluates to August22
• {{1440253151182 | date: 'EEEEMMMd'}} evaluates to SaturdayAug22
To simplify its usage, date supports format strings that represent common date
formats. These include shortDate, mediumDate, longDate, and fullDate:
• {{1440253151182 | date: 'shortDate' }} evaluates to 8/22/2015
• {{1440253151182 | date: 'mediumDate' }} evaluates to Aug 22, 2015
• {{1440253151182 | date: 'longDate' }} evaluates to August 22, 2015
• {{1440253151182 | date: 'fullDate' }} evaluates to Saturday, August
22, 2015
The date pipe can also display times, with s/ss identifying the second, m/mm
identifying the minute, and h or hh identifying the hour in AM/PM. In addition, special
format strings (shortTime and mediumTime) identify common time formats:
• {{1435252151182 | date: 'hh:mm:ss' }} evaluates to 01:09:11
• {{1435252151182 | date: 'shortTime' }} evaluates to 1:09 PM
• {{1435252151182 | date: 'mediumTime' }} evaluates to 1:09:11 PM
The time zone is determined by the user's locale. If the time format is followed by
Z, this time zone will be printed using the three-letter abbreviation. If the time format is
followed by z, the time zone will be printed in full.
• {{1435252151182 | date: 'hh:mm:ssZ' }} evaluates to 01:09:11EDT
• {{1435252151182 | date: 'hh:mm:ssz' }} evaluates to 01:09:11Eastern
Daylight Time
149 Chapter 8 Fundamentals of Angular Development
json
If a component's class contains an Object, the json pipe will convert it to a string.
For example, consider the following component:
@Component({
selector: 'json-example',
template: '<label>The object is {{ book | json }}.</label>'
})
export class JsonPipeExample {
book: Object;
constructor() {
this.book = {title: 'Quiller', author: 'Adam Hall'};
}
}
When <json_example> is added to a page, its label will display the following text:
The json pipe performs the same operation as JSON.stringify(). It's particularly
helpful for debugging JSON-based data transfers.
The slice pipe is more complex, and can be used in two ways. Its first usage involves
extracting a substring from a string. In this case, it accepts one required argument and one
optional argument. The required argument identifies the starting index of the substring
and the optional argument identifies the final index.
• {{ 'AngularJS' | slice : 2 }} evaluates to gularJS
• {{ 'AngularJS' | slice : 2 : 5 }} evaluates to gul
150 Chapter 8 Fundamentals of Angular Development
If the starting or ending index is negative, the index will be counted from the end of
the string instead of the front.
• {{ 'AngularJS' | slice : -5 }} evaluates to larJS
• {{ 'AngularJS' | slice : -5 : -2 }} evaluates to lar
The second usage of slice extracts a subarray from an array of elements. The
arguments identify the starting and ending elements of the subarray. This usage of slice
becomes helpful when using the NgFor directive, which inserts an array of elements into a
web page. Chapter 9 introduces the NgFor directive and shows how slice can be used to
provide an NgFor loop with a subarray.
async
Unlike the other pipes in Table 8.1, async doesn't format existing data. Instead, it displays
data as it becomes available. To be specific, it subscribes to an Observable or a Promise
and returns the current value.
Because this pipe waits for an indefinite amount of time, we say that it operates
asynchronously. Chapter 11 discusses asynchronous programming in detail and explains
how Observables and Promises are used.
Custom Pipes
Angular makes it possible to define custom pipes with special pipe classes. Chapter 17
explores this topic in detail, and explains how to create a pipe that sorts and displays
data. This custom pipe, called orderBy, is similar to the orderBy filter provided in the
Angular 1.x framework.
<image src='smiley.jpg'</img>
151 Chapter 8 Fundamentals of Angular Development
When a browser reads this, it adds an element to the document object model (DOM).
The element's attributes are collected together and each can be accessed as a property of
the DOM element. Put simply, an element's attributes are external (configured in HTML)
and its properties are internal (set by DOM processing).
You can't access an element's properties in regular HTML, but if an element is part
of a component's template, Angular makes it easy to read and modify its properties.
The notation for this surrounds the property name in square brackets. For example, the
following template definition disables a button by setting its disabled property to true:
Suppose that the button's disabled state should be controlled by a boolean property
called dis. After the discussion in the preceding section, you might expect the assignment
to look like this:
[disabled] = {{ dis }}
But this won't work. To set a DOM property equal to a class property, the class
property must be surrounded in quotes, not curly braces. The full component definition
can be given as follows:
@Component({
selector: 'prop-binding',
template: `
<button [disabled]='dis'>Press me!</button>
`})
This can be confusing. When I see 'dis' in the template, I assume it's a string literal,
not a class property. But this is Angular's syntax for assigning a DOM property to a class
property. Here's a general expression for the assignment:
[DOM_property] = 'class_property'
The official term for this data transfer is property binding. When the class's property
changes, the DOM property changes with it. But keep in mind that the data transfer is
one-way only. When using property binding, a change to the DOM will not change the
class property.
152 Chapter 8 Fundamentals of Angular Development
A DOM property can also be bound to a method in the component's class. For
example, suppose the address of a hyperlink should be set equal to a method's return
value. If the method is getAddr, the following code shows how the binding can be
established:
@Component({
selector: 'prop-method',
template: `
<a [href]='getAddr()'>Angular</a>
`})
Table 8.2 lists the href property and other properties that can be bound. The second
column identifies the expected data type and the third column provides a description.
Table 8.2
Element Properties (Abridged)
Property Data Type Description
hidden boolean Controls the element's visibility
disabled boolean Controls whether the element is enabled or disabled
href string Sets the element's hyperlink address
className string Sets the element's CSS class
classList string Sets the element's CSS class-list
textContent string Sets the element's text (HTML formatting ignored)
innerHTML string Sets the element's text (HTML formatting accepted)
@Component({
selector: 'text-example',
template: `
<label [textContent]='msg'>Old Message</label>
`})
msg: string;
constructor() {
this.msg = 'New Message';
}
}
The original text of the template's label is Old Message. Because the label's
textContent property is bound to msg, the displayed text will be New Message.
If msg equals <b>New Message</b>, the HTML formatting will be ignored and
the label will display <b>New Message</b>. But if the innerHTML property is bound
to msg, the formatting will be recognized and New Message will be displayed in the
browser.
There's one more point that needs to be mentioned. Suppose that, for testing
purposes, you want to set the label's textContent property to a string literal such as
Testing. You might try something like this:
template: `
<label [textContent]='Testing'></label>
`})
template: `
<label [textContent]='"Testing"'></label>
`})
Now the label will display Testing, as desired. Despite working with property
binding for some time, I still find this syntax to be unintuitive. Thankfully, event binding
is easier to work with.
8.6 Event Binding
The last section of Chapter 5 discussed DOM events, which are produced when the user
interacts with a DOM element. When an action occurs, the browser sends a message to
the application. In an Angular component, the process of event handling involves sending
a special event structure from the template to the component's class.
Within a template, events are identified by names (the same names in Tables 5.5 and
5.6) surrounded by parentheses. That is, mouse click events are identified by (click),
mouse entry events are represented by (mouseenter), and keypress events are
represented by (keypress).
To notify the class when an event occurs, the template can associate an event with a
class method. If an event occurs, its associated method will be invoked. For example, the
following template markup associates (click), (mouseenter), and (mouseleave)
with the methods incrNumClicks, setMouseEnter, and setMouseLeave:
<button (click)='incrNumClicks()'
(mouseenter)='setMouseEnter()'
(mouseleave)='setMouseLeave()'>
An event structure provides information about the user's action, such as the location
of the mouse click or the ASCII code of the pressed key. The template can provide this
structure to the class in two main ways: sending it as a method argument or changing the
value of a class property.
An example will make this clear. When a user clicks on an element, the event data
is packaged in an $event structure and the click's x-coordinate equals $event.pageX.
If checkButton is a method that needs to process the x-coordinate, the following code
ensures that checkButton will be invoked when the user clicks the button:
If the class has a property named xcoord, the following markup will set the property
to the click's x-coordinate when the user clicks on the button:
Now that we've seen how general events are handled, it's time to look at specific
types of events. This section divides event handling into three categories: mouse events,
keyboard events, and element-specific events.
155 Chapter 8 Fundamentals of Angular Development
Angular provides eight events that respond to mouse actions. Table 8.3 lists them and
provides a description of each.
Table 8.3
Mouse Events
Event Description
(click) Responds when the element is clicked
(dblclick) Responds when the element is double-clicked
(mousedown)/ Responds when the user's mouse button moves from up to down or
(mouseup) from down to up
(mouseover) Responds when the user's mouse moves over the element
(mousemove) Responds when the user's mouse moves
(mouseenter)/ Responds when the user's mouse enters the element's area or leaves
(mouseleave) the element's area
Each of these can be associated with a method to be called when the corresponding
event occurs. For example, the following code associates double-click events with the
handleDoubleClick method:
<button (dblclick)='handleDoubleClick()'</button>
In many applications, elements alter their appearance when the mouse hovers over
them. In an Angular component, (mouseenter) and (mouseleave) make it possible to
handle hover events. This is shown in the following markup:
<button (mouseenter)='handleMouseEnter()'
(mouseleave)='handleMouseLeave()'
</button>
When an event occurs, the browser provides access to the event's data through the
$event variable. The fields of $event depend on the nature of the event, and for mouse
events, at least five fields are available:
• $event.pageX — the event's x-coordinate (relative to the document's left edge)
• $event.pageY — the event's y-coordinate (relative to the top of the document)
156 Chapter 8 Fundamentals of Angular Development
• $event.type — a string identifying the nature of the event (click for mouse
clicks)
• $event.which — the mouse button that produced the event (1 – left, 2 – middle,
3 – right)
• $event.timeStamp — the number of milliseconds separating the event from
January 1, 1970
@Component({
selector: 'click-handler',
template: `
<p>{{ msg }}</p>
<button (click)='handleClick($event.which)'>Click Me</button>
`})
switch (btn) {
case 1:
this.msg = 'The left button was clicked.';
break;
case 2:
this.msg = 'The middle button was clicked.';
break;
case 3:
this.msg = 'The right button was clicked.';
break;
}
}
}
The class constructor initializes msg with a string stating that no button has been
clicked. When the user clicks on the button, handleClick will be called with a number
that identifies which mouse button was clicked.
Later in this chapter, we'll look at a more interesting example of mouse event
handling. The component counts mouse clicks and changes its message depending on
whether the user's mouse is hovering over it.
157 Chapter 8 Fundamentals of Angular Development
Keyboard events are just as easy to handle as mouse events. Table 8.4 lists the three types
of events.
Table 8.4
Keyboard Events
Event Description
(keydown) Responds when the user presses a key down
(keyup) Responds when the user raises a key
(keypress) Responds when the user presses a key and raises it
Keyboard events can be handled using the same type of code as mouse events. For
example, the following markup displays a message on keyup and keydown events:
An element can't receive events until it has focus. For keystrokes, this means the
element must be selected before keyboard events can be received and handled.
As with mouse events, browsers package event data in an object called $event. For
keystrokes, the object contains the following fields:
• $event.type — a string identifying the nature of the event (keypress for
keystrokes)
• $event.which — the ASCII code of the pressed key
• $event.timeStamp — the number of milliseconds separating the event from
January 1, 1970
The following markup demonstrates how $event can be used with key events.
When the user focuses on the button and presses a key, the template displays how
many milliseconds have elapsed between the event and January 1, 1970.
158 Chapter 8 Fundamentals of Angular Development
Some elements have specific types of events associated with them. This discussion
presents the events for <input> elements and <select> elements, and shows how they
can be received and processed in code.
Input/TextArea Events
For <textarea> elements and <input> elements of text type, the (input) event
responds whenever the user completes a keystroke. The element's full text is provided in
$event.target.value. For example, the following markup associates an <input>
element with a method that receives the element's text:
If the <input> element has checkbox type, the (change) event responds when the
box is checked or unchecked. The element's checked state can be accessed through the
boolean value, $event.target.checked.
The following component demonstrates how a checkbox can be associated with a
method (handleCheck) that receives the box's checked state:
@Component({
selector: 'checkbox-demo',
template: `
<input type='checkbox'
(change)='handleCheck($event.target.checked)'>Checkbox<br>
<p>The checkbox is {{ msg }}.</p>
`})
handleCheck(state: boolean) {
this.msg = (state) ? 'checked' : 'not checked';
}
}
The (change) event is also available for handling actions involving radio buttons.
This event responds when a button is selected or deselected.
159 Chapter 8 Fundamentals of Angular Development
Select Events
A <select> element creates a drop-down list that displays multiple <option> elements,
as shown in the following code:
<select>
<option>red</option>
<option>green</option>
<option>blue</option>
</select>
The (change) event is emitted when the user makes a selection. The name of the
selected option is provided in $event.target.value, which is a string. The following
markup associates the <select> element with a handleSelect method that receives
the selected option:
<select (change)='handleSelect($event.target.value)'>
<option>red</option>
<option>green</option>
<option>blue</option>
</select>
The <option> elements in this drop-down list are static. Chapter 9 explains how the
NgFor directive can be used to dynamically add <option> elements to a list.
<app-root></app-root>
Listing 8.4 shows what the code looks like. The component's first property is a
number named numClicks and the second is a boolean named mouseOver.
@Component({
selector: 'app-root',
template: `
<button type='button' (click) = 'incrNumClicks()'
(mouseenter) = 'setMouseEnter()'
(mouseleave) = 'setMouseLeave()'>
Number of clicks: {{ numClicks }}<br />
Mouseover: {{ mouseOver }}
</button>
`})
As shown, the class handles three different events. As they occur, the class's event
handling methods update the values of the two properties. These values are printed inside
the button using property interpolation.
161 Chapter 8 Fundamentals of Angular Development
<input #inp></input>
<button (click)='processText(inp.value)'>Transfer text</button>
Local variables allow one element to access properties of another element, but
events can't be received through local variables. For example, if you assign #btn to the
<button> and attempt to access(btn.click) in another element, the second element
will not receive the click event.
These three methods are straightforward to use and understand. This section explains
how each of them works and then demonstrates their usage in a complete web component
example.
162 Chapter 8 Fundamentals of Angular Development
Inside @Component, a styles field can be set to an array of CSS rules that will be applied
to the template's elements. For example, the following annotation sets styles equal to an
array containing a rule that associates boldclass with boldface text:
@Component({
template: `...`,
styles: ['.boldclass { font-weight: bold; }']
})
If a component needs many CSS rules, it's more convenient to place them in a
separate file. Within @Component, these files can be identified by the styleUrls field.
For example, the following annotation specifies that the CSS rules in bstyle.css should be
applied to the template.
@Component({
template: `...`,
styleUrls: ['bstyle.css']
})
In addition, CSS styles can be directly inserted into a template. This works in the
same way as a <style> tag in the <head> of an HTML document. The following
annotation demonstrates this. It defines a <style> element that affects elements that
belong to the boldclass class.
@Component({
template:`
<style>
.boldclass button {
font-weight: bold;
}
</style>
<div class='boldclass'>
<label ...></label>
</div>
`})
Ideally, a browser will only apply a component's CSS styling to the component. But
in many browsers, the CSS rules defined for a web component are applied to the entire
document. To be safe, it's a good idea to use unique names for a component's CSS rules.
This will ensure that the rules only affect elements in the component's template.
163 Chapter 8 Fundamentals of Angular Development
An earlier section mentioned that DOM elements have properties named className and
classList. They make it possible to assign CSS classes to an element. By combining the
styles field in @Component with property binding, it's easy for a component class to
assign style classes dynamically.
To demonstrate this, Listing 8.5 presents the code for the component in the ch8/
class_selector project. Its template consists of three elements:
• a <select> element that lists three styles
• a <label> whose style and text is determined by the selected option
• a <button> that applies the selected style/text when pressed
@Component({
selector: 'app-root',
public labelText =
'Please select a class and press the button.';
this.labelClass = choice;
The template assigns the local variable #sel to the <select> element. When the
button is clicked, the click event calls the class's select method with the value
property of the local variable. This is accomplished with the following definition:
<button (click)='select(sel.value)'>
@Component({
selector: 'parent-comp',
template: `
<ol>
<li><child-comp></child-comp></li>
<li><child-comp></child-comp></li>
<li><child-comp></child-comp></li>
</ol>
`})
@Component({
selector: 'toplevel-comp',
template: `
<ol>
<parent-comp>
<child-comp></child-comp>
<child-comp></child-comp>
<child-comp></child-comp>
</parent-comp>
</ol>
`})
Many parent components need to access their children in code. Angular makes this
straightforward by providing specially-decorated properties that return view children and
content children. Children are provided in containers called QueryLists, and before I
discuss how to access them, I'd like to briefly explain what a QueryList is.
8.10.1 QueryLists
for(child in queryList) {
...
}
A QueryList can't be modified, but its contents can be accessed through the
members listed in Table 8.5. T represents the class of the QueryList's elements.
Table 8.5
Members of the QueryList Class
Member Type/Return Type Description
first T The first element in the list
last T The last element in the list
length number The number of elements in the list
changes Observable Provides an observable for waiting
until the children are available
toArray() T[] Returns an array containing the
list's content
toString() string Returns a string representing the
list
map<U>(fn: (item:T) => U) U[] Applies a function to each element
of the list and returns the results
filter<U> T[] Creates an array containing
(fn: (item:T) => boolean) elements that pass the test
reduce<U> U Performs a reduction operation on
(fn:(acc:U,item:T) => U, the elements in the list
init: U)
167 Chapter 8 Fundamentals of Angular Development
A component can access components in its template with two decorated properties:
• @ViewChildren(Class) — returns a QueryList containing all view children of
the class Class
• @ViewChild(Class) — returns the first view child of the class Class
An example will demonstrate how these are used. Consider the following template:
template: `
<p><child1></child1></p>
<p><child1></child1></p>
<p><child2></child2></p>
<p><child2></child2></p>
`})
This component has four view children: two of type Child1 and two of type Child2.
The following properties, declared in the component's class, return a QueryList
containing both Child1 objects and the first Child2 object:
The view children in QueryList are returned in the order in which they were listed
in the template. There's no need to sort the list before processing its items.
In addition to accessing components, @ViewChild can also be used to access regular
HTML elements in the template. This requires four steps:
1. Identify an element of interest with a local variable. (<li #var>...</li>)
2. Decorate a class property with @ViewChild(...) and set the argument to the local
variable. (@ViewChild(var))
168 Chapter 8 Fundamentals of Angular Development
3. Set the type of the decorated property to ElementRef, which represents a general
template element.
4. Access the specific DOM element through the nativeElement property of the
ElementRef.
This is particularly helpful when you want to use a component to draw on an HTML
canvas. For example, suppose a component's template contains the following element:
The component can access the canvas by decorating a property whose type is set to
ElementRef:
Chapter 19 discusses the topic of HTML canvases in detail. It also provides a full
example of how Angular can be used to draw on a canvas.
If possible, the data binding methods discussed earlier should always be employed
instead of accessing the DOM directly. This is because direct access creates a tight
coupling between the component and the page's rendering. Also, a component that
accesses the DOM directly can't be accelerated with web workers.
A component can access its content children with decorated properties similar to those
used for view children:
• @ContentChildren(Class) — returns a QueryList containing all content
children of the given class
• @ContentChild(Class) — returns the first content child with the given class
169 Chapter 8 Fundamentals of Angular Development
<parent>
<child1></child1>
<child1></child1>
<child2></child2>
</parent>
The classes of the three content children are Child1 and Child2. The following
property provides a QueryList containing the two Child1 instances in the order in
which they're inserted into the parent:
Judging from the markup, you might expect that the templates of Child1 and
Child2 would be automatically inserted into the parent's template. This is not the case.
Content children must be specifically inserted into the parent's template, and this is
accomplished by using the <ng-content> placeholder.
This content insertion, commonly called transclusion, is crucial to understand. So let
me present it another way. Suppose that the parent's template is given as follows:
If the parent has content children, then by default, the templates of the content
children will not be displayed. To display the child templates, <ng-content> must be
inserted into the parent's template. This is shown in the following markup:
The <ng-content> tag identifies the point (the transclusion point) where the child's
content should be inserted. This content may contain more than just child components.
Any components, elements, or text between the parent's tags, <parent> and </parent>,
will be inserted.
170 Chapter 8 Fundamentals of Angular Development
An earlier section presented the ch8/class_selector project, which uses the following
markup to set a label's className property:
Now suppose you want to configure a child component to receive property updates
from a parent component. This can be expressed with markup like the following:
<parent>
<child [prop]='newProp'></child>
</parent>
To configure this in code, the child component's class must have a property named
prop annotated with @Input(). This property will be updated whenever the parent's
newProp property changes. The following code shows what this looks like:
class ChildComp {
@Input() prop: string;
}
The @Input() annotation accepts a string that sets the name of the property in
the markup. Therefore, if the property's name in the template is changed from prop to
childProp, the annotation should change from @Input() to @Input(childProp).
But within the class, the property can still be accessed as prop. The example code at the
end of this section demonstrates how this works.
In addition to receiving properties from the parent, a child component can be
configured to produce custom events. Event production is a complex topic, so a thorough
discussion of custom events will have to wait until Chapter 11.
In some cases, processing should be delayed until the class's children are accessible.
This is accomplished by adding code to the appropriate life cycle method of the
component. Table 8.6 lists these methods in order of when they're (usually) invoked.
Table 8.6
Life Cycle Methods of a Component
Life Cycle Method Description
ngOnChanges(changes: Called after data-bound properties have been initialized
{[key: string]: or changed
SimpleChange})
ngOnInit() Called when the component is instantiated
ngDoCheck() Implement custom change detection
ngAfterContentInit() Called after the component's content has been
initialized
ngAfterContentChecked() Called after the component's content has been checked
ngAfterViewInit() Called after the component's view has been initialized
ngAfterViewChecked() Called after the component's view has been checked
ngOnDestroy() Called immediately before the component is destroyed
The life cycle begins when the component checks the values of its properties. After
the component is fully instantiated, it examines its content, which includes its properties
and the elements in the template. This content is initialized and then checked.
An earlier discussion explained how a component can access a QueryList of its
content children. If accessed in the constructor, the list will be empty. It isn't populated
until the component's content has been initialized, so the earliest a component's content
children can be reliably accessed is in ngAfterContentInit.
It may seem odd to have separate methods for a component's content and view. To
see why this is the case, it's important to know about directives. A directive is a component
without a view and Chapter 9 will discuss them in detail.
Each method in Table 8.5 has an associated interface that contains only that method. The
interface's name is almost the same as that of the method, but the initial ng is dropped
and the first letter is capitalized. As examples, the ngOnChanges method is provided
by the OnChanges interface, the ngOnDestroy method is provided by the OnDestroy
interface, and so on.
172 Chapter 8 Fundamentals of Angular Development
If a component class implements one of these eight interfaces, it can add code
to the corresponding method. For example, if a component's class implements the
AfterContentInit interface, its ngAfterContentInit method will be called after
the component's content has been initialized. The following code gives an idea of how this
works:
ngAfterContentInit() {
...
}
}
When using life cycle methods, there's no need to make changes to the component's
@Component annotation. But each interface must be imported from @angular/core.
It's important to note that isFirstChange returns true when the property's value
changes from undefined to its first value. This takes place before the entire component is
initialized. This explains why ngOnChanges is called before ngOnInit.
This component class doesn't do anything. Instead, the template displays the content
assigned to it by its parent. Listing 8.7 presents the code for the parent component.
public ngAfterContentInit() {
alert('The parent component has ' +
this.children.length + ' content children');
}
}
174 Chapter 8 Fundamentals of Angular Development
The template of the parent component has two parts: a label that identifies the
number of children and an ordered list whose content is set by the top-level component.
The component class accesses its content children in a QueryList named children.
This list isn't immediately accessible, but the children will be available when the
ngAfterContentInit life cycle method is called.
Listing 8.8 presents the code in the top-level component. The template contains an
instance of ParentComponent and three instances of ChildComponent.
// Declare properties
@ViewChildren(ParentComponent) public children:
QueryList<ParentComponent>;
public ngAfterViewInit() {
alert('The top-level component has ' +
this.children.length + ' view child');
}
}
<parent-comp [parentProp]='message'>
<child-comp>Content1</child-comp>
<child-comp>Content2</child-comp>
<child-comp>Content3</child-comp>
</parent>
175 Chapter 8 Fundamentals of Angular Development
The template of ParentComp prints two alert messages and creates an ordered list
containing its content children. This template is given as follows:
The ParentComp class declares its msg property with the following code:
The @Input annotation specifies that msg is a property that can be updated by
the top-level component. The argument of @Input() is parentProp, so the top-
level component's template accesses the property as parentProp instead of msg. The
component's class sets parentProp equal to the member variable message, which is set
to Hello.
Input properties are generally accessed through their names, not through aliases. If
you run ng lint for this project, you'll see the following message:
[(ngModel)]='class_property'
When this is placed inside a template element, a change to the element will affect the
property. A change to class_property will affect the template element.
For example, suppose you want to associate a boolean property named checked with
a checkbox in the template. The following markup shows how this can be done.
Similarly, the following markup associates a text entry box with a string property
named inputText:
Though it's simple to configure in code, two-way data binding is not recommended.
If possible, you should always try to use one-way data binding, which doesn't degrade
performance and stability to the same extent. Still, there are times when two-way data
binding is necessary, so it's good to know that NgModel is available.
8.13 Summary
One key difference between AngularJS and Angular is the approach to data binding.
In AngularJS, all data binding is two-way, which means the model and view are always
in sync. This ensures that the application will be responsive. Unfortunately, detecting
changes in the model and view consumes a great deal of resources and can lead to non-
deterministic behavior.
177 Chapter 8 Fundamentals of Angular Development
In contrast, Angular emphasizes one-way binding. This chapter has presented three
mechanisms for one-way binding: property interpolation, property binding, and event
binding. The property interpolation and binding mechanisms update the template when
a class member changes, but don't affect the class. The event binding mechanism updates
the class when the template changes, but doesn't change the template.
After explaining the different one-way binding mechanisms, this chapter explained
how local variables work in the template, how to set styles, and the interaction between
parent and child components. Child components can be view children or content children
depending on where the children are inserted into the parent. A parent can access its
children using decorated properties and can pass data to them using property binding.
Angular provides methods and interfaces that make it possible to execute code at
different stages in a component's life cycle. These methods make it possible to perform
computation when a component's properties change or when its children become
accessible.
Chapter 9
Directives
I still remember the first time I saw an AngularJS directive in action. The directive was
ngRepeat, and when I saw what was happening, my jaw dropped. The directive generates
new list elements without needing JavaScript? Cool!
Since then, my appreciation for directives has grown larger. An Angular directive
is like the magic wand in a fairy tale. But instead of turning pumpkins into carriages,
Angular's directives turn static HTML elements into dynamic controls that can blink in
and out, repeat themselves in a list, or transfer data to the model.
Angular provides fewer directives than AngularJS, but the magic is still there.
Angular's primary directives are called core directives, and they add behavior to template
elements in the same ways that AngularJS directives did. The new NgIf directive
performs the same operation as the old ngIf, the new NgForOf directive performs
essentially the same operation as the old ngRepeat, and so on.
The first part of this chapter presents Angular's core directives and demonstrates how
they're used in code. In addition to being easy to use and understand, they provide a great
deal of power.
The second part of the chapter explains how to write custom directives. The process
of creating a directive is essentially similar to that of coding a component: precede a
regular class with an annotation. In this case, the annotation is @Directive, and it tells
the framework what template elements should receive behavior from the directive and the
nature of the behavior to be added.
The last part of this chapter presents two examples of custom directives. The first
custom directive dynamically adds elements to the document. The second receives click
events from an element and updates its text with each click.
180 Chapter 9 Directives
This section examines each of these directives and shows how it can be used in code.
But first, it's important to be familiar with three points:
• As with a component, every directive has an associated class. Outside of a template,
core directives are referred to by their class names, which take the form of NgXyz,
such as NgIf and NgStyle. Inside a template element, the directive's marker is
written with a lowercase 'n', as in ngIf and ngStyle.
• The core directives and their classes are provided automatically. That is, they don't
need to be imported and they don't need to be added to the declarations array in
the @NgModule annotation.
• With the exception of NgClass and NgStyle, the core directives add/remove
elements from the document. These are called structural directives and their markers
must be preceded by an asterisk, as in *ngXyz. This tells the compiler to place the
directive in a <ng-template> surrounding its element. For example, if <abc>
contains *ngXyz, the equivalent markup is given as follows:
<ng-template [ngXyz]='expression'>
<abc>...</abc>
</ng-template>
The last point is complicated and will be discussed later in the chapter. Just remember
that the markers of structural directives must be preceded by an asterisk, as in *ngIf and
*ngSwitch.
181 Chapter 9 Directives
9.1.1 NgIf
The preceding chapter explained how an element can be hidden by setting its [hidden]
property to true. The operation of NgIf is similar, but instead of hiding an element, it
removes the element from the document.
NgIf accepts an expression and its behavior depends on whether the expression
evaluates to false, 0, "", null, undefined, or NaN. These are referred to as falsy values and
any other values are referred to as truthy.
Basic Usage
<xyz *ngIf='expression'>...</xyz>
If expression evaluates to a truthy value, the xyz element and its subelements will be
added to the document. The following markup demonstrates how this works:
The directive's expression evaluates to false. Therefore, the <div> element and its
subelement won't be part of the document.
If-Then-Else
NgIf supports an if-then-else structure similar to that used in JavaScript. The general
markup is given as follows:
As shown, ngIf associates then with an identifier (trueBlock) and else with an
identifier (falseBlock). The two identifiers are inserted into <ng-template> elements
that contain subelements. If the expression evaluates to a truthy value, the elements inside
the trueBlock element will be inserted into the document. If the expression evaluates to
a falsy value, the elements in the falseBlock element will be inserted into the document.
The following markup demonstrates how this works:
182 Chapter 9 Directives
<ng-template #trueBlock>
<label>This won't be included in the document</label>
</ng-template>
<ng-template #falseBlock>
<label>This will be included in the document</label>
</ng-template>
9.1.2 NgForOf
The NgForOf directive makes it possible to insert multiple elements into a document.
This is particularly useful when an application needs to display dynamic lists and tables.
One confusing aspect of this directive involves the marker. The ngForOf marker is
available, but most applications use the abbreviated ngFor instead. This book will use
ngFor instead of ngForOf throughout its example code.
Basic Usage
In this markup, the directive iterates through elements of iterator, which may be
an array or a string. For each element, NgFor inserts a new xyz element into the DOM.
These new elements are frequently list items, table rows, or options in a <select>. Each
element of iterator can be accessed through the item variable.
An example will clarify how this works. Suppose numArray equals [13, 17, 19]. The
following markup creates a list element for each element in numArray. Each list element
uses the num variable to access the corresponding value of numArray.
<ul>
<li *ngFor='let num of numArray'>
Array element: {{ num }}
</li>
</ul>
183 Chapter 9 Directives
In this example, NgFor generates three list items (<li>). The resulting list is given as
follows:
• Array element: 13
• Array element: 17
• Array element: 19
Accessing Variables
In addition to providing the value of each element in the iterator, NgForOf can provide
other values. These are accessed by appending let localVar = varName to the NgFor
directive. varName can be set to one of the following values:
• index — Provides the index of the current value, starting with 0
• first — A boolean that identifies whether the current value is the first value
• last — A boolean that identifies whether the current value is the last value
• even — A boolean that identifies whether the current value has an even index
• odd — A boolean that identifies whether the current value has an odd index
To clarify how these are used, the following loop creates a paragraph for each value of
numList. The index of each value is accessed as i and l is set to a boolean that identifies
whether the current value is the last value:
The last, even, and odd variables work similarly to the last variable used in the
example. The even and odd variables make it easy to construct a table whose odd rows
have different colors than the even rows.
184 Chapter 9 Directives
TrackBy
Angular monitors the elements of the iterator in NgFor, and if the data changes, the
directive will rebuild the DOM. Therefore, whenever the client reloads data from a server,
the directive will update the DOM even if the data has remained constant.
An application can change this tracking behavior by assigning unique identifiers to
the iterator's elements. If the elements are reloaded, NgFor won't change the DOM if the
elements have identical identifiers.
To assign unique identifiers, NgFor needs to be associated with a tracking function.
This function receives the index of each element and the element itself. The following
code shows how the tracking function can be identified in the template:
The following code shows how this works. NgFor inserts an option element for each
Thing in thingArray. The directive calls trackThing to provide a unique identifier for
each element and trackThing returns the Thing's id field.
class Thing {
public constructor(name: string, id: number) {}
}
@Component({
template: `
<select>
<option *ngFor='let th of thingList; trackBy:trackThing'>
{{ th.name }}
</option>
</select>
`})
9.1.3 NgSwitch/NgSwitchCase/NgSwitchDefault
In JavaScript, the switch statement makes it possible to process one of many blocks of
code according to an expression's value. The overall format is given as follows:
switch(expression) {
case value_1:
...
break;
case value_2:
...
break;
default:
...
}
The NgSwitch directive is similar, but instead of processing one of many blocks of
code, it inserts one of many elements to the DOM. The NgSwitchCase directive serves
the same purpose as a case option in the switch statement. NgSwitchDefault serves
the same purpose as default in the switch statement.
The general usage of NgSwitch, NgSwitchCase, and NgSwitchDefault is given
as follows:
<abc [ngSwitch]='expression'>
<xyz *ngSwitchCase='x'>...</xyz>
<xyz *ngSwitchCase='y'>...</xyz>
<xyz *ngSwitchDefault>...</xyz>
</abc>
Here, abc can be any container element, such as a <div>, <span>, <ul>, or
<ol>. The following example places NgSwitch inside a <div> element, and each
NgSwitchCase corresponds to a label.
<div [ngSwitch]='value'>
<label *ngSwitchCase='3'>Three</label>
<label *ngSwitchCase='5'>Five</label>
<label *ngSwitchDefault>Other</label>
</div>
If value equals 3, the first <label> is added to the document, and if it equals
5, the second label is added. If value is neither 3 nor 5, the label corresponding to
NgSwitchDefault will be added to the document.
186 Chapter 9 Directives
9.1.4 NgClass
Like the className property discussed in Chapter 8, the NgClass directive can change
the CSS styling of an element and its children. The difference is that NgClass can accept
a wider range of values and it can both add and remove CSS classes.
Inside a template element, NgClass can be assigned to one of three types of values:
1. string — adds the given class or space-separated classes
2. array — adds each class in the array
3. object — maps classes to boolean expressions and adds if true
<label [ngClass]='labelClasses'>Message</label>
Similarly, if classObj equals {'classC': true}, the following markup places the
element in classC:
<label [ngClass]='classObj'>Message</label>
9.1.5 NgStyle
The NgStyle directive provides a fourth option, which makes it possible to assign
CSS rules to an element and its children. This directive is assigned to an object that maps
CSS properties to values. These values can be set equal to properties in the component,
thereby enabling the component to dynamically set styles of elements in the template.
For example, suppose the color and size of a paragraph's text should be controlled by
the component. If the component has properties named fontColor and fontSize, the
following markup uses NgStyle to set the color and size.
187 Chapter 9 Directives
According to the Angular documentation, NgStyle can also be set to a property that
defines the full CSS object. I'm sure that this is possible, but despite numerous attempts,
I've never succeeded in getting this to work.
The code in Listing 9.1 presents the component code of the directives_demo project. This
demonstrates how each of the core directives are used in practice.
These examples should be easy to understand, but the NgClass example deserves
additional explanation. To demonstrate the different usages of NgClass, the class defines
two properties: classArray is an array containing two class names and addClass is
an object that maps a class name to true. Assigning NgClass to classArray places the
element in both classes. Assigning NgClass to the object places the element in the given
class.
@Component({
selector: 'app-root',
styles:
['.firstClass { color:red; }',
'.secondClass { font-weight:bold; }',
'.thirdClass { text-decoration: underline; }'],
template: `
The constructor of the AppComponent class declares a series of values that affect the
directives in the template:
• ifVar — because this is set to false, the NgIf directive inserts the label element
that contains the #falseBlock variable
• forArray — the NgFor directive iterates through this array of strings and displays
each string and its index
• switchVar — because this is set to 2, NgSwitch displays the message in the
<template> element whose NgSwitchCase directive equals 2
• classArray and addClass — define CSS classes assigned to paragraphs
• fontColor and fontSize — define CSS properties associated with the style set by
the NgStyle attribute
189 Chapter 9 Directives
@Component({
selector: 'parent-comp',
template:`
<p *ngIf='check'>...</p>
`})
The NgIf directive is contained in the component's template, so it's a view child of
that component. This means it can be accessed through a property annotated with
@ViewChild and it will be available when the component calls ngAfterViewInit. If
the parent's class name is ParentComponent, the following code gives an idea of how the
directive could be accessed:
ngAfterViewInit() {
...
}
}
<parent-comp>
<p [ngStyle]='...'>styled text</p>
</parent-comp>
The class that adds behavior to the selected elements is called the directive class. This
must be decorated with @Directive instead of @Component.
@Directive({selector: '[myDirective]'})
class MyDirective {
...
}
Before a directive can be inserted into a component's template, its class must be added
to the declarations array in the @NgModule annotation of the project's module. The
following code shows what this looks like for the MyDirective class:
@NgModule({
declarations: [ AppComponent, MyDirective ],
imports: [ BrowserModule ],
bootstrap: [ AppComponent ]
})
This section starts by examining the @Directive annotation. The most important
field in this annotation is the selector field, which identifies the directive's marker. This
is required in every @Directive annotation.
191 Chapter 9 Directives
The selector field in @Directive identifies the directive's marker and the manner
in which it can be inserted into template elements. If the directive should be used as an
attribute, the identifier should be placed in square brackets:
@Directive({selector: '[myDirective]'})
The selector value can take other formats that resemble CSS selectors. For
example, the following annotation states that the directive should apply to every element
whose class attribute is set to myClass:
@Directive({selector: '.myClass'})
Not all CSS rule formats are available. Directive selectors can't include wildcards,
hash tags (e.g. #myID), or rules related to descendants. Table 9.1 lists the CSS selector
formats that can be used in a @Directive annotation:
Table 9.1
Selector Formats in Directive Annotations
Selector Format Selected Elements
name Elements with the given name
.class Elements of the given class
[attribute] Elements containing the given attribute
[attribute=value] Elements whose attribute is set to the given value
[attribute*=value] Elements whose attribute contains the given value
name[attribute] Elements containing the given name and whose attribute is set
Multiple values can be OR'ed together by separating them with commas. For example,
the following annotation states that the directive should apply to all paragraphs (p),
elements in the valid class, and elements whose title is set to important:
If a directive is accessed with an attribute, the attribute's value can be set to a string. The
directive class can access this string using the same @Input() annotation discussed in
Chapter 8.
For example, suppose a component's template accesses a directive with the following
markup:
The input variable takes the same name as the directive marker. As discussed in
Chapter 8, an alias can be set by adding text between the parentheses of @Input().
Chapter 8 explained how a component can respond to events from template elements. For
example, the following markup tells Angular to call handleClick whenever the button is
clicked:
<button (click)='handleClick()'</button>
ElementRef is a class with one field: nativeElement. This field provides access
to the HTMLElement corresponding to the directive's element. Chapter 5 discussed
HTMLElements and the other data structures provided by TypeScript for interacting with
the document object model (DOM).
193 Chapter 9 Directives
The following code shows how an ElementRef can be accessed in the constructor of
a directive class:
After the ElementRef is obtained, the class's functions can use it to access the
corresponding DOM element. If a function is preceded with @HostElement(event), it
will be called whenever events of event type occur.
The following code demonstrates how this works. The directive class receives the
ElementRef and the handleClick method uses it to change the inner text of the
corresponding DOM element:
@HostListener('click') handleClick() {
this.ref.nativeElement.innerText = "Hi there!";
}
An earlier discussion explained how structural directives like NgIf and NgFor make it
possible to modify the document's structure. To perform similar operations with a custom
directive, it's important to be familiar with two classes:
• ViewContainerRef — container of host views and embedded views
• TemplateRef — represents an embedded view
class CustomDir {
container.createEmbeddedView(templateRef);
}
}
LoopDirective
The first custom directive, LoopDirective, accepts a number and inserts the given
number of elements to the document. Listing 9.2 presents its code.
constructor(container: ViewContainerRef,
templateRef: TemplateRef<LoopContext>) {
this.container = container;
this.templateRef = templateRef;
}
@Input()
set addElements(num: any) {
this.context.numElements = num;
}
ButtonDirective
@Directive({selector: '[removeChar]'})
export class ButtonDirective {
Component
9.4 Summary
Angular's directives make it possible to add dynamic behavior to a template's elements.
While a programming language has if statements, for loops, and switch-case
statements, Angular makes it possible to add similar functionality with NgIf, NgForOf,
and the three NgSwitch directives. In addition, the NgClass and NgStyle directives
make it possible to dynamically configure an element's appearance.
When the core directives aren't sufficient, you can create a custom directive by
annotating a class with @Directive. Inside this annotation, selector identifies which
elements should be affected by the directive. The directive class can use @Input to access
properties and it can receive events using the @HostListener decorator.
If a directive adds new elements to the document, its marker is preceded by an
asterisk, as in *ngFor. This tells the framework that the directive's element should be
surrounded in a <template> element. The directive can process template elements by
accessing a ViewContainerRef and a TemplateRef through its constructor.
Chapter 10
Dependency Injection
10.1 Overview
Suppose that one object (we'll call it the client) needs to access a second object (we'll call
it the dependency). If the client creates and configures the dependency by itself, the tight
coupling between the two objects will make the client's code hard to test, difficult to
maintain, and nearly impossible to reconfigure.
An example will clarify the problem. Suppose that every instance of the Gizmo class
requires a Widget. A simple way to obtain the Widget is to call its constructor:
class Gizmo {
constructor(...) {
w = new Widget(...);
}
}
This may look fine at first, but when it comes time for testing, there's no way to assign
a MockWidget instance for testing because of the hardcoded Widget class. Also, different
applications may need to access alternatives to the Widget class, such as Doohickey or
Thingamajig.
To improve flexibility and testability, it's better to provide the client with
dependencies that have already been created and configured. For example, if Thing is the
superclass of Widget, Doohickey, and Thingamajig, an external source can provide a
Thing to each Gizmo through Gizmo's constructor. The following code shows how this
can be done:
class Gizmo {
constructor(t: Thing) {
...
}
}
Just as component classes must be preceded with @Component and directive classes
must be preceded with @Directive, service classes must be preceded with
@Injectable. This tells Angular that the class is a dependency that can be injected
into other classes. The parentheses of @Injectable can be left empty, as shown in the
following service class definition:
// Service class
@Injectable()
export class ExampleService {
public provideData() {
return data;
}
}
202 Chapter 10 Dependency Injection
// Component class
@Component({
selector: '...',
providers: [ ExampleService ],
template: `...`})
export class ExampleComponent {
constructor(private service: ExampleService) {
service.someFunction();
}
}
As shown, the @Component decorator has a providers array that accepts the names
of service classes. In this case, the array only contains ExampleService. Note that the
providers array can also be added to the @Directive decorator.
The component accesses the service through the parameter list of its constructor. If a
parameter's type is a service class, Angular will set the parameter to a new instance of the
service class.
Many applications need to inject services into classes that aren't components or directives.
As before, the service class must be preceded with @Injectable(). But the receiving
class doesn't have a providers array.
To make up for this, the name of the service class can be placed in the providers
array of the application's module. The following @NgModule decorator shows what this
looks like for an application containing ExampleComponent and ExampleService:
@NgModule({
declarations: [
ExampleComponent
],
imports: [
BrowserModule
],
providers: [
ExampleService
],
bootstrap: [
ExampleComponent
]
})
Chapter 10 Dependency Injection 203
The code in the ch10/service_demo project demonstrates how services can be accessed by
components and by other services. The app directory contains three important classes:
• BookArrayService — creates an array of Books and makes them available
• BookService — accesses the BookArrayService and provides the array of
Books
• AppComponent — accesses the BookService and displays the content of the Book
array
constructor(bookArray: BookArrayService) {
this.goodBooks = bookArray.getBookArray();
}
public getBooks() {
return this.goodBooks;
}
}
@Component({
selector: 'app-root',
template: `
<ul>
<li *ngFor = 'let book of goodBooks; let i = index'>
Good book {{ i }}: {{ book.title }} by {{ book.author }}
</li>
</ul>
`})
The BookArrayService and BookService classes are both service classes. This
explains why both class definitions are preceded by @Injectable. The services are
accessed through the constructors of the BookService and BookComponent classes.
To serve its function, the BookComponent needs to access the BookService and
the BookService needs to access the BookArrayService. To make sure both service
classes are accessible, the names of both classes are included in the providers array in
the @NgModule decorator of the application's module. The following code shows what
this looks like:
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule
],
providers: [
BookArrayService, BookService
],
bootstrap: [
AppComponent
]
})
When coding services, remember the single responsibility principle. That is, each
service should perform a single operation. If a service's role expands, it's a good idea to
split its responsibilities into separate services.
Services are frequently employed when applications need to read data from a remote
source. Chapter 13 will demonstrate how services are used to transfer data using the
HTTP and JSONP protocols.
NOTE
The following analogy was inspired by an article from Pascal Precht entitled
Dependency Injection in Angular 2. To the best of my knowledge, Pascal was the first
to compare Angular's providers to cooking recipes.
I'm not a particularly good cook, but I understand the basics. Every cooking recipe has
three parts:
• the name of a dish
• a series of ingredients
• a set of instructions for making the dish
As a simple example, Figure 10.1 presents a basic recipe for chocolate-chip cookies.
A professional cook will have hundreds of recipes like the one shown in the figure.
If you go to a restaurant and request a dish, the cook will find the appropriate recipe and
obtain the ingredients needed to make it.
With this in mind, here's a three-part analogy:
1. Just as every restaurant has a cook to prepare dishes, every component has an
injector to provide dependencies.
2. Just as a cook relies on recipes to identify how a dish should be prepared, an injector
relies on providers to identify how dependencies should be constructed.
3. A recipe maps the name of a dish to instructions and ingredients. A provider maps
an object called a token to the construction procedure and dependencies.
Chapter 10 Dependency Injection 207
I hope this gives you a basic idea of how injectors, providers, and tokens are related.
To summarize, every component relies on an injector to inject dependencies. For each
type of dependency, the injector needs a provider to identify the construction process and
which objects are required.
This analogy is fine at a high level, but there are a number of flaws:
• A restaurant may have multiple cooks, but by default, a component has one injector.
• Before an injector can use a provider, the provider must be resolved. I can't think of
any culinary equivalent for resolving a provider.
• A recipe serves one purpose: to define how a dish should be made. A provider can
serve many different purposes.
• An injector can create child injectors that access its providers.
The rest of this section explores the process of using injectors, providers, and tokens
in code. Then we'll look at an example component that demonstrates these concepts.
The primary data structures involved in dependency injection are injectors, providers,
and tokens. Rather than examine these classes individually, I'm going to present the
overall process of injecting dependencies into a component or directive. The process
consists of four steps:
1. Create a Provider that identifies how to create the dependency.
2. Convert the Provider into a ResolvedProvider.
3. Create an Injector from the ResolvedProvider.
4. Call the Injector's get method to obtain the dependency.
ClassProvider
The provide field sets the provider's token, which can have any type. Angular has a
special token type called OpaqueToken that will be encountered in later chapters. But in
most cases, a token is simply the name of a class.
An example will clarify how ClassProviders are used. The following provider can
configure an Injector to produce an instance of a Widget whenever the Injector is
called upon to provide a Thing:
Angular understands that these names serve as tokens and the names of the classes to
be instantiated. This is really a shortened form of the following code:
providers: [
{provide: BookArrayService, useClass: BookArrayService},
{provide: BookService, useClass: BookService}
]
The useClass field is generally set to a class name, but it can be set to anything that
can be instantiated. If useClass is set to a string, that string will be provided whenever a
configured Injector is called with the given token.
FactoryProvider
If the factory function needs additional data, the deps field can be set to an array
of elements. These are the ingredients required to construct the factory's dependency.
The factory function accesses these elements through its parameter list, as shown in the
following code:
Each element in the deps array is a token that will be used by another provider to
obtain the actual value. In the above example, there must be providers available to provide
values for NumArms and NumLegs. Otherwise Angular will produce errors: No provider for
NumArms! No provider for NumLegs!
If a dependency is optional, it can be decorated with @Optional in the function's
parameter list. If a dependency isn't available, the factory function will receive a value of
null.
210 Chapter 10 Dependency Injection
If a class is inserted into resolve's array, it will serve as a provider for instances of
that class. For example, consider the following code:
let resProviders =
ReflectiveInjector.resolve([{provide: Thing, useClass: Thing}]);
After an application obtains an array of resolved providers, the next step is to create
an Injector. This is discussed next.
This code can be simplified by calling the resolveAndCreate method. This accepts
the same argument as the resolve method discussed earlier. But instead of returning a
ResolvedProvider, it returns a configured Injector. This is shown in the following
code:
After an Injector is created, its get method can be called to inject dependencies. This
accepts the name of the token and returns an object of the type to which the token was
associated. As an example, consider the following code:
class TokenA {}
class TokenB {}
class Other {}
class Thing {}
@Component({
selector: 'app-root',
template: `
<ul>
<li>ResultA is an instance of Other: {{ resultA }}</li>
<li>ResultB is an instance of Thing: {{ resultB }}</li>
</ul>
`})
constructor() {
// Class provider
const providerA: ClassProvider = {
provide: TokenA, useClass: Other};
const providerB: FactoryProvider = {
provide: TokenB, useFactory: () => new Thing() };
The component's view displays a list that checks the classes of the resultA and
resultB variables. The elements of this list are given as follows:
• ResultA is an instance of Other: true
• ResultB is an instance of Thing: true
Chapter 10 Dependency Injection 213
10.5 Summary
Most developers only deal with Angular's dependency injection when a component needs
to access a service like HTTP communication or the Form API. In these cases, injection
can be handled by adding an element to the providers array in the @NgModule
annotation or the providers array in the @Component annotation. The provider tells the
component's injector how to obtain the dependencies.
At a low level, Angular's dependency injection process can be difficult to grasp. First,
an application needs to define a class that implements the Provider interface, such as
a ClassProvider or a FactoryProvider. Then the Provider instance needs to be
resolved into a ResolvedProvider. An array of ResolvedProviders can be used to
create an Injector, and the Injector's get method provides the desired dependency.
Each component receives its own injector, and a child component receives a child
injector from its parent. The child injector can access all of the providers of its parent, so
it can inject the same dependencies. This relationship of parent-child injectors forms an
injector hierarchy.
Chapter 11
Asynchronous
Programming
One disadvantage of web development is the constant need to wait—wait for the user to
take an action, wait for the server to respond to a request, wait for the database to sort its
records. Rather than halt the application, it's more efficient to assign a routine to process
results as they become available. This frees the application to work on other tasks instead
of waiting.
The technique of assigning a routine to be executed at an indeterminate time is called
asynchronous programming. This chapter presents two mechanisms for asynchronous
programming: promises and observables. Promises are built-in features of JavaScript and
observables are provided by Reactive Extensions for JavaScript (RxJS), which Angular
requires to operate.
A promise makes it possible to associate two functions with an incomplete operation.
The first function is called if the operation completes successfully. The second is called
if an error prevents the operation from returning successfully. A promise is assumed to
provide a value, and when the value is returned, the promise's processing is complete.
Observables are similar to promises, but provide a series of values instead of just
one. For this reason, observables can be associated with three functions. The first
function executes when the observable provides a value, the second executes if an error
occurs, and the third executes when the observable has finished sending data. A key
difference between observables and promises is that the objects monitoring an observable
(observers) can cancel their monitoring. Promises can't be cancelled.
The last part of this chapter explains how to configure a component to provide
custom events to other components. This configuration requires an EventEmitter,
which behaves like an observable and an observer.
216 Chapter 11 Asynchronous Programming
11.1 Promises
If someone owes you money but can't pay you right now, they may give you an IOU or
other acknowledgement of debt. If an application needs data that isn't available right now,
it may receive a promise. A promise is a data structure that represents an operation that
hasn't completed yet but is expected to provide a result.
For example, if an application wants to access the content of a large file or read
lengthy data from a server, it may receive a promise. The advantage of using promises is
that the application doesn't need to wait for the operation to complete. After receiving a
promise, the application can perform other tasks.
When an application receives a promise, one of two things will eventually happen.
Success means that the operation completed successfully and a result is available. Failure
means that an error occurred and the operation failed.
More formally, we say that a promise has three possible states:
1. Pending — The promise hasn't produced a result because the operation hasn't
completed.
2. Fulfillment — The operation has completed and the promise has provided the result.
When a promise enters the Fulfillment state, we say that it has fulfilled.
3. Rejection — The operation encountered an error and no result is available. When a
promise enters the Rejection state, we say that it has been rejected.
Promises have become popular in JavaScript and they're part of the ES6 specification.
They're also important in Angular development, as we'll see in later chapters. This section
introduces the Promise class and its methods, and then presents an example component
that demonstrates how Promises are used.
Promises begin their operation in the Pending state. Instead of waiting for this state to
change, an application receiving the promise can associate it with two functions. These are
called handlers or callbacks.
The first handler is invoked if the promise fulfills, and for this reason, it's referred to
as a fulfillment handler. The second handler is called if an error occurs and the promise is
rejected. This is called a rejection handler.
The method that assigns handlers to a Promise is called then, and its signature is
given as follows:
then accepts two optional functions. The first function is called if the promise fulfills
and provides a suitable result. The second function is called if the promise is rejected. This
function receives information about the error.
For example, suppose an application receives a Promise named prom. The following
code shows how an application can assign handlers to the Promise:
prom.then(
function(str: string) { console.log("Result: " + str); },
function(err: any) { console.log("Error: " + err); });
}
When the Promise reaches its final state, one of the two functions will write to the
console. The return value of then is the configured Promise, so this method can be
chained with further methods. It's common to see a Promise that calls then multiple
times to define multiple handlers. The following code gives an idea of how this works:
prom.then(function(..){..}, function(..){..})
.then(function(..){..}, function(..){..})
.then(function(..){..}, function(..){..})
218 Chapter 11 Asynchronous Programming
In this code, each call to then executes independently. That is, an error in one
callback won't affect other callbacks. But if one callback modifies the Promise, successive
callbacks will access the altered Promise.
If the asynchronous operation produces an error, the second function of then will
be invoked. But if the first function produces an error, the second function will not be
invoked. For this reason, the Promise class provides the catch method. In essence,
catch is just then with one argument—a function to call if an error occurs. The
following code shows how then and catch can be used together:
prom.then(function(..){..}).catch(function(..){..});
The Promise class provides four helpful static methods that return new Promises:
• resolve(value?: T) — Returns a Promise that is guaranteed to fulfill and
return the given value
• reject(error: any) — Returns a Promise that is guaranteed to be rejected and
provide the given error information
• all(promises: (T)[]) — Accepts an array of Promises and returns a Promise
that only fulfills if every element fulfills.
• race(promises: (T)[]) — Accepts an array of Promises and returns a
Promise that fulfills if any of the elements fulfill.
This discussion presents each of the four methods and shows how they can be
invoked in code.
resolve
The first of the four functions, resolve, returns a Promise that is guaranteed to fulfill. If
resolve is called with a value that isn't a Promise, the value will be provided as the data
object of the returned Promise.
An example will help make this clear. Suppose you want to test your application by
creating a Promise that always provides the value of 5. This can be accomplished with the
following code:
219 Chapter 11 Asynchronous Programming
prom = Promise.resolve(5);
If then is called after the Promise is created, the function that handles fulfillment
will receive the value of 5 when the Promise fulfills.
reject
Just as resolve creates a Promise that always succeeds, reject creates a Promise that
is guaranteed to fail. This accepts a value of any data type, and this value will be provided
to any function that catches the Promise's error.
For example, the following call to reject produces a Promise that enters the
Rejection state. After reaching this state, it provides the error handler with a string value
of Error.
prom = Promise.reject("Error");
The following code shows how an application receiving this Promise can call catch
to process the error:
When the Promise is rejected, the anonymous function will write the message to the
console. A function that handles the Promise's fulfillment will not be invoked.
The all method accepts an array of Promises and returns a Promise that fulfills when
every Promise fulfills. If any Promise in the array is rejected, the returned Promise will
be rejected.
If the returned Promise fulfills, its value will be an array containing the values
provided by each Promise in the original array. This is shown in the following code,
which creates an array of three Promises and uses all to combine them into one.
In this example, prom won't fulfill because the second Promise in its array is
rejected. If the catch method is called, the error handler will receive a single string
containing the Uh-oh message.
220 Chapter 11 Asynchronous Programming
The race method is similar to all in that it receives an array of Promises. But the
returned Promise will fulfill or reject as soon as one of the Promises in the array fulfills
or rejects. The first fulfilled/rejected Promise will set the value or error information of
the Promise. The following code shows how this works:
In this example, it's hard to say whether prom will fulfill or be rejected. If the first
Promise fulfills before the second is rejected, prom will fulfill. If the second Promise is
rejected before the first fulfills, prom will be rejected.
It's more common for applications to receive Promises than create them, but for testing
purposes, it's good to be familiar with the constructor of the Promise class. If T is the
data type of the desired value, the constructor of the Promise class is declared as follows:
constructor(callback: function(
resolve : function(value? : T | Promise<T>),
reject : function(error? : any)) : void);
Similarly, the following code creates a Promise that is rejected and returns the
number 505 as the reason.
For the sake of debugging, the object returned as an error should be an instance of the
Error class instead of a string.
221 Chapter 11 Asynchronous Programming
The code in Listing 11.1 demonstrates how Promises can be created and processed. It
starts by creating two Promises: one that fulfills, returning a number, and one that will be
rejected. The first is processed with then(...).catch(...) and then an array of both
promises is processed with all.
@Component({
selector: 'app-root',
template: `
<p><b>First result</b> - {{ result1 }}</p>
<p><b>Second result</b> - {{ result2 }}</p>
`})
export class AppComponent {
constructor() {
this.promise1 = new Promise((res, rej) => { res(123); });
this.promise2 = Promise.reject('Big problem');
It's important to use arrow function expressions inside the calls to then and catch.
Otherwise, this won't refer to the enclosing AppComponent object.
222 Chapter 11 Asynchronous Programming
It's also possible to import elements individually. For example, the following
command imports the Observable class:
The first approach requires more code because each RxJS class must be accessed
through the Rx namespace. But the second approach is error-prone and frustrating.
Therefore, the example code in this chapter accesses RxJS classes through the Rx
namespace.
223 Chapter 11 Asynchronous Programming
This section focuses on the Observable class, which provides a wide range of static
and instance methods. To present these methods, this section is divided into four topics:
1. Subscribing to an Observable
2. Subscriptions
3. Creating Observables
4. Handling errors
After explaining these topics, this section presents an example web component that
creates an Observable and subscribes two observers.
An example will clarify how this works. If obs is an Observable, the following code
prints different messages to the console depending on whether the Observable produces
data, throws an error, or completes its data transmission:
obs.subscribe(
function(value: number) { console.log("Received: " + value); },
function(exception: any) { console.log("Exception"); },
function() { console.log("Completed"); }
);
In this example, the first function prints a message each time it receives a value.
When the source has finished transmitting its data, the third function will be called. The
second function will only be called if an error occurs.
The Observable class is a generic class, and its name is followed by a parameter
whose type identifies the data provided to the data handler. In the above code, obs is an
Observable<number> because the Observable emits numbers.
224 Chapter 11 Asynchronous Programming
11.2.2 Subscriptions
For testing purposes, it's important to know how to create new Observables. The
Observable class provides many static methods for creating new instances, and most of
them create Observables from known values. Table 11.1 lists six of these methods and
provides a description of each.
225 Chapter 11 Asynchronous Programming
Table 11.1
Static Creation Methods of the Observable Class (Abridged)
Method Description
empty<T>() Creates an empty Observable<T>
range(start: number, Creates an Observable<T> from a range of count values,
count: number) beginning with the starting value
from<T>(iterable: any) Creates an Observable<T> from an iterable or an array-
like object
fromArray<T>(array: T[]) Creates an Observable<T> from an array
The first five methods create Observables from known values. The last,
Observable.create, creates an Observable whose values are determined
programmatically.
The first five methods in Table 11.1 are commonly used for testing. They're also helpful to
newcomers because they clarify what an Observable does. An Observable provides a
sequence of one or more values to its subscribers. For example, the following code creates
an Observable that provides a sequence of five values starting with 3:
The following Observables all transmit the same three values to their subscribers:
obs = Observable.create(
(observer: Rx.Subscriber<string>) => {
observer.next("msg1");
observer.next("msg2");
observer.complete();
}
);
This example could have used the from or fromArray methods, but it's important
to see how Observable.create makes it possible to provide data dynamically. Instead
of providing predefined string values, the inner function could read bytes from a socket or
characters from a file.
The inner function of Observable.create returns an optional function whose
code is invoked when the subscription is terminated. For example, the following
Observable sends a number to its Subscribers and completes the transmission. As
each subscription is terminated, it prints a message to the console:
obs = Observable.create(
(observer: Rx.Subscriber<number>) => {
observer.next(10101);
observer.complete();
return () => {
console.log("Subscription terminated");
}
}
);
When a processing error occurs, an Observable can (and should) catch it and deliver an
Error instance to observers.
The following code shows how this works. The throw statement in the try block
creates a new Error instance. The catch block receives the Error object and calls the
onError method of each subscribed observer.
obs = Observable.create(
(observer: Subscriber<number>) => {
try {
observer.next(1);
observer.next(2);
throw new Error("Major Error");
}
catch(err) {
observer.error(err);
}
}
);
After an Observable catches an error and notifies its observers, each of its
subscriptions will be disposed of. If the error isn't caught, these subscriptions may not be
disposed of. This makes it particularly important to ensure that an Observable's error
handling is performed correctly.
The ch11/rx_demo project demonstrates how Observables and Observers can work
together. Listing 11.2 presents the code for the main component, which declares two
Observer classes, Observer1 and Observer2. Then it creates an Observable that
transmits three numbers and throws an Error.
When the component's button is pressed, the createSubscriptions method
is called. This subscribes two observers (anonymous instances of Observer1 and
Observer2) to the Observable. As the observers receive data, they print messages to
the console.
When a subscription to the Observable terminates, the Observable prints
a message to be displayed under the button. Because there are two observers, two
subscriptions will be canceled, and the Terminate message will be printed twice.
228 Chapter 11 Asynchronous Programming
@Component({
selector: 'app-root',
template: `
<button (click) = 'createSubscriptions()'>
Create Subscriptions</button>
<p [innerHTML]='msg'></p>
`})
export class AppComponent {
private obs: Rx.Observable<number>;
private msg = '';
constructor() {
// Create observable
this.obs = Rx.Observable.create(
(ob: Rx.Subscriber<number>) => {
try {
ob.next(3); ob.next(2); ob.next(1);
throw new Error('Problem');
} catch (err) {
ob.error(err);
}
return () => { this.msg += 'Terminate<br/><br/>'; };
}
);
}
The simplest stream processing operations remove elements of a stream before the
stream's data reaches observers. Table 11.2 lists five of the Observable methods that
perform these operations.
Table 11.2
Filtering Methods of the Observable Class
Method Description
take(count: number) Emit only the first count values of the
sequence
skip(count: number) Emit elements following the first
count elements of the sequence
takeWhile(predicate: Emit values of the sequence while the
function(value: T, index: number): predicate returns true
boolean)
skipWhile(predicate: Prevent emission of values while the
function(value: T, index: number): predicate returns true
boolean)
filter(predicate: Emit values of the sequence for which
function(value: T, index: number): the predicate returns true
boolean)
230 Chapter 11 Asynchronous Programming
The first two methods are particularly easy to use, as shown in the following
examples:
Observable.range(1, 5);
--> 1, 2, 3, 4, 5
Observable.range(1, 5).take(3);
--> 1, 2, 3
Observable.range(1, 5).skip(3);
--> 4, 5
The last three methods in the table use a predicate function to determine which
elements should be removed. These predicate functions all return a boolean and they all
accept the same two parameters:
• val — the data element provided by the Observable
• i — the index of the data element
takeWhile transmits values so long as the predicate function returns true. The
following code emits values while val is less than 4:
Observable.range(1, 5).takeWhile(
(val: number, i: number) => val < 4 );
--> 1, 2, 3
skipWhile prevents values from being transmitted while the predicate returns true.
The following code prevents values from being transmitted as long as the index, i, is less
than 3:
Observable.range(1, 5).skipWhile(
(val: number, i: number) => i < 3 );
--> 4, 5
filter transmits all values for which the predicate returns true. The following code
transmits all odd values:
Observable.range(1, 5).filter(
(val: number) => (val % 2) === 1 );
--> 1, 3, 5
231 Chapter 11 Asynchronous Programming
The Observable class provides methods that fuse multiple data streams into one. Some
combine a stream with itself or with literal values. Other methods convert streams into
new Observables. Table 11.3 lists five of these methods and provides a description of
each.
Table 11.3
Stream Combination Methods of the Observable Class
Method Description
startWith(value: T) Prepend a value to the stream
repeat(count: number) Repeat the data stream the given
number of times
concat(...sources: Observable<T>[]) Append given data streams after the
given stream
merge(obs: Observable<T>) Merge the current stream with the
stream of the given Observable
zip(...sources: Observable<T>, Programmatically merge data streams
(...sources) => {result}} that can hold elements of different types
The startWith method is easy to use. The following code prepends a string to an
Observable's data stream:
The next three methods in the table are more involved. Figure 11.2 gives an idea of
how concat, merge, and zip combine data streams.
232 Chapter 11 Asynchronous Programming
Figure 11.2 Combining Data Streams with concat, merge, and zip
As shown in the figure, concat appends the argument's streams to the original
stream. This is shown in the following code:
Table 11.4
Stream Transformation Methods of the Observable<T> Class (Abridged)
Method Description
map((value: T, index: number) => Uses a function to transform the elements
TResult, thisArg?: any) of a data stream
scan((acc: T, value: T) => T) Provides the preceding result to transform
elements of the data stream
concatMap((value: T, Creates an Observable for each element
index: number) => Observable) and then concatenates them into one
concatMap((value: T, Creates an Observable for each element,
index: number) => Observable, concatenates them into one, and then
(value1: T, value2: T2, processes the elements of the source and
index: number)) combined Observables
flatMap((value: T) => Observable) Creates an Observable for each element
and merges the Observables into one
flatMap((value: T) => TResult[]) Creates an array for each element and
merges the arrays into an Observable
flatMap((value: T) => Observable, Creates an Observable for each element,
(item: T, other: TOther) combines the Observables into one, and
=> TResult) transforms the elements with a function
switchMap((value: T, index: Creates an Observable for each element
number, source: Observable<T>) and merges the Observables into one
=> TResult, thisArg?: any)
NOTE
In RxJS code, you may encounter methods named select, selectConcat,
selectMany, and selectSwitch. These methods are exactly similar to the map,
concatMap, flatMap, and switchMap methods in the table.
234 Chapter 11 Asynchronous Programming
scan is like map, but its function accepts two arguments: the result of the preceding
function call (called the accumulator) and the element's value. The following code shows
how it can be used:
For the first element, the accumulator equals 0, so 0 + 1 = 1. For the second element,
the accumulator is 1, so 1 + 2 = 3. For the third element, the accumulator is 3, so 3 + 3 = 6.
For the last element, the accumulator is 6, so 6 + 4 = 10.
The concatMap method is more complicated. Instead of producing a regular value,
its function returns an Observable for each element. When all the elements have been
processed, it concatenates the Observables' data streams together into one.
An example will clarify how concatMap works. In the following code, the source
Observable has a data stream of three elements. The inner function creates three
Observables and concatenates their sequences together to produce the output
Observable.
Observable.of(5, 6, 7).concatMap(
(val, i) => Observable.of(0, 1, val));
--> 0, 1, 5, 0, 1, 6, 0, 1, 7
concatMap accepts a second function that receives three values: the element of the
source Observable, the element of the combined Observable, and the index. The
following call to concatMap is similar to the first, but inserts a function that adds 2 to
each value in the combined Observable:
235 Chapter 11 Asynchronous Programming
Observable.of(5, 6, 7).concatMap(
(val, i) => Observable.of(0, 1, val),
(val1, val2, index) => val2 + 2);
--> 2, 3, 7, 2, 3, 8, 2, 3, 9
Like concatMap, flatMap computes an Observable for each element in the source
Observable. But instead of concatenating the Observables' streams, flatMap merges
the streams into one. According to the RxJS documentation, this merging "may interleave"
the Observables.
But in my experiments, flatMap produces the same output as concatMap. To
show what I mean, the following code accepts the same input values as in the previous
concatMap example. The output is exactly the same:
Observable.of(5, 6, 7).flatMap(
(val, i) => Observable.of(0, 1, val));
--> 0, 1, 5, 0, 1, 6, 0, 1, 7
Like concatMap, flatMap accepts a function that can access elements of the source
Observable and the combined Observable.
The last method in Table 11.4, switchMap, is similar to flatMap. But when a new
value is emitted from the source Observable, the data stream created by processing
previous values will be halted. This ensures that only current results will be emitted.
This section explains how you can add custom events to web components. The
fundamental class to know is EventEmitter. This is provided by the Angular
framework, but it's a subclass of Subject, which is provided by RxJS. Therefore, this
section starts by introducing the Subject class.
236 Chapter 11 Asynchronous Programming
The Subject class extends the Observable class and also implements the Observer
interface and the empty Subscription class. Figure 11.3 shows what this inheritance
hierarchy looks like.
constructor(isAsync?: boolean)
The isAsync argument is true by default, which means the EventEmitter operates
asynchronously. To see how this works, you need to be familiar with its subscribe
method. This resembles the subscribe method of the Observable class, but its
behavior is slightly different. Its signature is given as follows:
The operation of this method depends on the constructor's isAsync argument. The
following code shows how the generatorOrNext argument is processed:
To test custom events, two components are needed: one to generate the custom event
and one to receive it. In keeping with RxJS terminology, I'll call the first component the
observable component and the second component the observer component.
For example, suppose that a component contains a button that should fire a
custom event when the user focuses on it and presses the 'z' key. I'll call this component
ZButtonComponent, and Listing 11.3 shows how the component can be coded.
@Component({
selector: 'zbutton',
template: `
<button (keypress)='keyhandler($event)'>
Press z to Generate Event
</button>
`})
export class ZButtonComponent {
In this class, the name of the EventEmitter is zpress. It can be accessed outside
the component because of its @Output() decorator.
The component's template associates the button's keystroke events with a method
called keyhandler. When the user presses a key, this method checks to see if 'z' was
pressed. If so, the method calls the EventEmitter's emit method, which sends a value
to observers.
To receive custom events from a child component, a parent component must be specially
configured. This requires two steps:
1. Add the child component to the parent's template. Inside the element, associate the
child's custom event with an event handler in the parent.
2. Inside the parent class, configure the event handler to process the custom event.
239 Chapter 11 Asynchronous Programming
The code in Listing 11.4 shows how a parent component can be configured to receive
custom events from the ZButtonComponent presented earlier.
@Component({
selector: 'app-root',
template: `
<zbutton (zpress)='handler()'></zbutton>
`})
export class AppComponent {
The custom event, zpress, matches the name of the EventEmitter created
in the child component. Also, the @NgModule decorator in the module contains
ZButtonComponent, so the component can be accessed in the parent's template.
It's important to notice that the parent doesn't know how the child processes the
event. It doesn't even know how the event is triggered. It simply handles zpress events
when they occur.
11.5 Summary
When building web components, it's important to know how to receive and handle the
results of incomplete operations. In the Angular framework, asynchronous processing is
made possible by two classes: Promise and Observable.
A Promise represents an ongoing operation that will eventually return a value. The
then method associates the Promise with two functions. The first, called the fulfillment
handler, is called if the operation's value is available. The second, called the rejection
handler, is called if an error prevents the operation from completing.
Observables are more complicated than Promises, but they're also more flexible.
An Observable can provide multiple values to its subscribed observers and each
observer can cancel its subscription. An Observable can be associated with three
functions: one to be called if a value is provided, one to be called if an error occurs, and
one to be called when the data transfer is complete.
240 Chapter 11 Asynchronous Programming
In the example code presented so far, applications have created one or a handful
of components. But large-scale applications may need to display tens or hundreds
of components at a time. In most cases, the arrangement of components should be
determined by the URL.
In theory, you could access the URL with JavaScript and use Angular directives
to insert or remove components. But it's easier and better performing to use Angular's
routing capability. In essence, Angular's routing associates URL patterns with different
Angular components and behaviors.
The goal of this chapter is to explain how Angular routing works and demonstrate
its usage in code. Much of the discussion will be focused on configuring a router module
with one or more routes. Many types of routes are available, but in most cases, a route's
purpose is to associate a URL pattern with an Angular component.
Angular also makes it possible to create special hyperlinks called router links. These
links make it straightforward to update the URL with routes that the router will recognize.
In addition, router links can be associated with Angular components to set link URLs
programmatically.
One major advantage of Angular routing is that it enables lazy loading of application
code. This means that the client will only download the code it needs instead of having to
download all of the code at once.
The last part of the chapter discusses advanced aspects of Angular routing. One
important topic involves configuring routes for child components as well as parent
components. We'll also see how a component can access the router in code and use its
methods to adjust the router's operation or navigate to a new URL.
242 Chapter 12 Routing
12.1 Overview
Angular's routing capability is powerful but complex. Before we get into the details, this
section provides a brief overview of the topic. First, I'd like to explain why routing is so
important to Angular applications. Then I'll present the basic concepts underlying how
routing works in practice.
Chapter 9 introduced the NgSwitch directive, which adds one of many elements to a
document according to a value. Routing is similar, but instead of checking a value, the
router examines the document's URL. Instead of inserting an element into the DOM, a
router inserts a child component into a parent component. In routing applications, the
primary parent component is called the base component.
243 Chapter 12 Routing
Figure 12.1 shows what the application looks like when the URL's path ends in bar,
which results in SecondComponent being inserted into AppComponent.
This application is trivially simple, but the project's code has five aspects that apply to
most applications that take advantage of routing.
244 Chapter 12 Routing
In this project, the routing process is configured in the module class and the base
component. This section looks at the code in both classes.
As mentioned earlier, a route associates a URL with a component and/or state data. An
application's routes must be specified in a Routes, which is an array of Route instances.
At the top of Listing 12.1, the Routes consists of three routes.
The imports array in the @NgModule decorator calls RouterModule.forRoot
with the Routes. This creates and configures the RouterModule that provides the
application with Angular's routing capabilities.
A later section will discuss route definitions in great detail. This discussion looks at
the three routes given in the Routes array. The first is given as follows:
This route is called an index route because it tells the router what to do when the
client visits the base URL. In this case, the route tells the router to redirect the URL to /foo
if the URL's path is empty.
This tells the router to insert FirstComponent into the base component when the
URL's path equals /foo.
Similar to the preceding route definition, this tells the router to insert
SecondComponent into the base component when the URL's path equals /bar.
After the router examines the URL, the selected child component is inserted into the base
component. In the ch12/simple_router project, the base component is AppComponent,
and Listing 12.2 presents its code.
@Component({
selector: 'app-root',
template: `
<p>Select a component:</p>
<ul>
<li><a routerLink='/foo'>First component</a></li>
<li><a routerLink='/bar'>Second component</a></li>
</ul>
<router-outlet></router-outlet>
`})
export class AppComponent {
constructor(private router: Router) {}
}
246 Chapter 12 Routing
The template contains two hyperlinks. Each has a special routerLink property, and
for this reason, they're called router links. The example links are trivially simple, and direct
the browser to a URL whose path is set to /foo or /bar. In both cases, routerLink could
be replaced with the traditional href. But as we'll see shortly, the routerLink property
can do much more than just set static paths.
Below the router links, the component's template contains the following markup:
<router-outlet></router-outlet>
These tags specify where the selected child component should be inserted into the
base component's template. They serve a similar purpose to the <ng-content> tags,
which specify where the templates of content children should be inserted into the parent's
template.
The component's constructor receives a Router instance through dependency
injection. The Router's provider was configured in the module, which created a
RouterModule. A later section will discuss the Router class in detail.
At this point, you should have a basic familiarity with what Angular routing is
intended to accomplish. The next two sections take a closer look at router links and route
definitions.
Earlier, I said that router links update the URL, but that's not necessarily true. When
a router link is clicked, its path will be examined by the component's router, and the URL
will be updated if the router recognizes the value of routerLink. If the router can't
interpret a routerLink, the result will be an error: Cannot match any routes.
The above example sets routerLink equal to a simple string. But routerLink
can also be assigned to an array of values that combine to form a URL path. This will be
discussed next.
247 Chapter 12 Routing
If the routerLink property is set to a string defining an array, the array's elements will
be combined to form the URL path. For example, consider the following router link:
If the user clicks the link, the router will combine the array elements to /abc/xyz. If
the router recognizes the path, the URL's path will be set to /abc/xyz and the appropriate
component will be inserted into the base component.
In addition to strings, the array can also contain numbers and variables from the
component class. The following markup presents an example:
In addition to setting a URL's path, a router link can also set a URL's query string and
fragment. As a quick review, a query string (usually) provides data in name=value pairs
and a fragment links to a portion of the HTML document. Figure 12.2 provides an
example of a URL with a query and fragment:
A router link can specify a query string by assigning a value to the queryParams
property. This is set to a JSON object whose properties will be converted to the parameters
in the query string. To see how this works, consider the following router link.
248 Chapter 12 Routing
If a user clicks on this link, the string ?color=red will be appended to the URL's
path. If the value of queryParams is set to an object containing multiple fields, the query
string will contain multiple parameters, as in ?color=red&num=2.
Suppose that the current URL already contains a query string. By default, this
information will be discarded when a router link is activated. This behavior can be
changed by assigning the queryParamsHandling property to one of two values:
• preserve — make the current query string the query string of the generated URL
• merge — add the current query string to the query string of the generated URL
As an example, the following router link tells the router to merge the query string of
the current URL with the query string defined with queryParams:
A router link can set the URL's fragment by assigning a value to the fragment
property. As an example, the following link sets the fragment to the value of the
component's fragName variable:
If the value of fragName is idx, the string #idx will be appended to the URL's path
when the link is clicked.
By default, router links discard the fragment associated with the current
URL. To tell the router to keep the current fragment, a router link needs to set the
preserveFragment property to true.
As a review, the array from Listing 12.1 is defined with the following code:
This code demonstrates how the path, redirectTo, pathMatch, and component
fields can be used. But many more fields can be set in a route definition and Table 12.1
presents the complete list.
Table 12.1
Fields of a Route Definition
Property Description
path The route URL or pattern
component The type of component to be inserted into the base
component
outlet The name of the routing outlet into which the component
should be inserted
redirectTo URL fragment to replace the current segment
pathMatch Sets the router's URL matching strategy
matcher The name of a UrlMatcher function to be used for URL
matching
children Array of child route definitions
loadChildren A reference to child modules whose bundles can be accessed
through lazy-loading
data Static data to be passed to the inserted child component
through the injected ActivatedRoute
resolve Identifies providers for dynamic data
canActivate Array of tokens used to obtain CanActivate handlers through
dependency injection
canActivateChild Determines if child routes can be activated
canDeactivate Determines if routes can be deactivated
canLoad Determines if children can be loaded
runGuardsAndResolvers Configures how often guards and resolvers are executed
250 Chapter 12 Routing
To explain how route definitions are coded, this discussion divides the table's fields
into five categories:
1. Component routes and secondary routes
2. Index routes and matching
3. Child routes
4. Providing data
5. Routing security
This section discusses the properties in each of these categories. A later section
presents an example project that demonstrates how many of them are used.
The simplest route definitions tell the router to insert a specific component when the URL
matches a given pattern. The URL is set with the path property and the component is set
with the component property. This is shown in the following route definition:
The path field accepts wildcards. The ** wildcard corresponds to any string, so
if path is set to foo/**/bar, the middle URL segment will be matched to any string.
This wildcard is particularly helpful because it allows an application to specify a route
definition to be matched if all other route definitions fail. Consider the following array:
The router processes route definitions in their given order. In this case, if it can't
match the URL to one of the initial paths, it will match the URL to the last route and
insert a new instance of the ErrorComponent.
In addition to **, path accepts variable names given in the form :xyz. These are
called route parameters. The router will match :xyz to any string in the URL's path, and
will pass the corresponding string to the component associated with the URL. As an
example, consider the following route definition:
If the URL's path is /firstComp/red, the router will match the URL and pass the
color=red parameter to the new FirstComponent instance. The component can access
the parameter through the ActivatedRoute injected into its constructor. A later section
will discuss the ActivatedRoute class and its members.
As discussed earlier, a base component uses <router-outlet> tags to control
where a selected component will be inserted into its template. The router-outlet
element accepts an attribute called name that uniquely identifies the outlet. If a route
definition associates the outlet field with a value, the component will be inserted into
the router-outlet element with that value.
A route definition with the outlet field set is called a secondary route. As an
example, suppose that the base component's template contains the following markup:
<router-outlet name='extra'></router-outlet>
To configure a component to be inserted into this outlet, a route definition must have
its outlet property set to extra, as shown in the following code:
You might expect that ExtraComponent will be inserted into the base component if
the URL path starts with extraComp. This is not the case. To be matched with a secondary
route, a URL must satisfy two requirements:
1. It must match a primary route
2. The outlet and the path must be given inside parentheses
As an example, the following URL matches a secondary route whose outlet is set to
extra and whose path is set to extraComp:
http://www.example.com/(extra:extraComp)
A router link can generate a URL for a secondary route if the array associated with
the routerLink property contains an outlets field that associates the outlet with the
route's path. The following markup shows what this looks like:
This may seem confusing, but secondary routes make it possible to insert multiple
independent components into a base component.
252 Chapter 12 Routing
An index route is a definition whose URL path is empty. This is commonly specified by
setting path equal to ''. Instead of identifying a component, it's common for an index
route to redirect the client to another URL. The following definition shows how this can
be accomplished:
If pathMatch isn't specified in an index route, an error will result. This is because,
by default, the router only checks if the URL's path starts with the path. For example, if
path is set to foo, the router will match /foo/bar and /foo/baz because the URLs start
with the path value. But if path is set to '', the router will match every URL because
every URL path starts with the empty string.
To change this behavior, pathMatch needs to be configured. This accepts two values:
• prefix — The router checks to see if the URL's path starts with the path value
• full — The router makes sure the entire URL's path matches the path value
By default, pathMatch is set to prefix. For index routes, it should be set to full.
This ensures that the router will only match the index route if the URL's path is empty.
An application can further configure the router's URL matching by defining a
custom matching strategy. This requires setting the matcher property to a function that
implements the UrlMatcher interface. This interface is defined with the following code:
A route definition can define additional routes called child routes. The router only
examines these routes when the parent route is selected. Child routes are defined
by setting the children property equal to an array containing one or more route
definitions. The following code shows what this looks like:
This last point is important. Lazy loading ensures that the client only downloads the
code it needs. But configuring this behavior is complicated, and will be discussed later in
the chapter.
An earlier discussion explained how URL parameters, such as :id, can be used to send a
URL segment to the new component instance. A route definition can also send data to the
new component by setting the data property to a value of the Data type, which is defined
with the following code:
254 Chapter 12 Routing
The following code shows how data can be configured in a route definition:
In this case, if the router matches the URL, the color parameter will be passed to
the new instance of MyComponent. The component can access the parameter through
the ActivatedRoute injected into its constructor. A later section will discuss the
ActivatedRoute class in full.
While the data property provides static data, the resolve property makes it
possible to provide dynamic data. This property must be set to a ResolveData instance,
which is defined as follows:
The purpose of the resolve property is to identify a class that implements the
Resolve<T> interface, where T is the type of the data to be provided. This interface
declares one method, resolve:
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot):
Observable<T> | Promise<T> | T;
Security is a critical concern in web development, and many applications need to prevent
clients from accessing resources associated with secure URLs. In Angular, this is made
possible through the canActivateChild, canDeactivate, and canLoad properties.
The classes associated with these properties are called guards.
255 Chapter 12 Routing
This discussion explains how these steps are implemented for the
canActivateChild, canDeactivate, and canLoad properties. Afterward, we'll look
at the runGuardsAndResolvers property.
canActivateChild(
childRoute: ActivatedRouteSnapshot,
state: RouterStateSnapshot):
Observable<boolean> | Promise<boolean> | boolean;
The boolean returned by the function identifies whether the child route can be
accessed. If the function returns a value of false, the child route will be inaccessible.
The canDeactivate property controls whether clients can deactivate routes. This
property must be set to a class that implements the CanDeactivate<T> interface,
where T is the component class to be protected. This interface contains one method,
canDeactivate<T>, whose signature is given as follows:
canDeactivate(
component: T,
currentRoute: ActivatedRouteSnapshot,
currentState: RouterStateSnapshot,
nextState?: RouterStateSnapshot):
Observable<boolean> | Promise<boolean> | boolean;
The canLoad property controls a client's ability to load children. This property must be
set to a class that implements the CanLoad interface, whose single method is canLoad:
canLoad(route: Route):
Observable<boolean>|Promise<boolean>|boolean;
The first value identifies the default setting. That is, by default, guards and resolvers
are run only when there's a change to the route's parameters.
To present the code in the ch12/child_routes project, this section presents the code in
the module class (AppModule) and the base component class (AppComponent).
The @NgModule decorator of the project's AppModule class creates the RouterModule
and sets its route definitions. These route definitions are given as follows:
If the base URL is accessed, the first route definition redirects the client to foo/baz.
The pathMatch property is set to full, so the router will only match the URL path if the
entire path is empty.
The second route definition inserts FirstComponent into the base component if
the URL path starts with foo. The children property is assigned to an array containing
two route definitions. If the path starts with foo/baz, the first child route tells the router
to insert ThirdComponent into FirstComponent. If the path starts with foo/qux, the
second child route tells the router to insert FourthComponent into FirstComponent.
The second-to-last route definition assigns the outlet property to extra. This is
a secondary route, and if the URL path contains (extra:extraComp), the router will
insert ExtraComponent into the <router-outlet> element of the base component
whose name attribute is set to extra.
The last component sets path to **, which means any URL path will be matched.
The router accesses route definitions in order, so it will only match this route if none
of the preceding routes have been matched. The ErrorComponent presents a message
stating that the URL path isn't recognized.
258 Chapter 12 Routing
The template of the base component provides router links that activate each of the four
parent/child combinations. The fifth router link activates the secondary route, which
associates the extraComp path with the extra outlet. Listing 12.3 presents its code.
// Base component
@Component({
selector: 'app-root',
template: `
<p>Select a component pair:</p>
<ul>
<li><a routerLink='/foo/baz'>
Parent: First component, Child: Third Component</a></li>
<li><a routerLink='/foo/qux'>
Parent: First component, Child: Fourth Component</a></li>
<li><a routerLink='/bar/baz'>
Parent: Second component, Child: Fourth Component</a></li>
<li><a routerLink='/bar/qux'>
Parent: Second component, Child: Third Component</a></li>
Up to this point, every application has had one module class. But to make use of
lazy loading, an application needs to have a module class for each bundle to be loaded.
A parent module class can load these child module classes by creating route definitions
containing the loadChildren field.
The code of a child module class is similar to that of a parent module class, but there
are a few notable differences. This is shown in the following code:
As shown, a child module class has the same kind of route definitions as a parent. But
in the imports array, the route definitions are accessed by RouterModule.forChild
instead of RouterModule.forRoot. Also, the @NgModule decorator doesn't contain a
bootstrap field.
260 Chapter 12 Routing
According to the Angular Style Guide, child modules should be placed in a separate
directory with the components they access. Suppose that the preceding code was stored in
child.module.ts. Then the app/child directory would contain child.module.ts and the files
defining its components.
In the parent module class, the loadChildren property needs to identify the
TypeScript module containing the child module class and the name of the child module
class. The following code shows how this can be accomplished:
As shown, the # separates the name of the TypeScript module and the name of the
module class. Because of this route definition, the router will use an NgModuleFactory
to create the module when the URL's path starts with foo. Then the child module and its
components can be accessed normally by the parent.
Given the hierarchy of parents and children, it's important to know how to tell
the router where to search for route definitions. In a router link, this is determined by
the initial characters of the value assigned to routerLink. The search location can be
configured in three main ways:
• If the value is preceded by /, the router will search through the root module
• If the value is preceded by ./, the router will search the child route definitions
• If the value is preceded by ../, the router will search the parent route definitions
For example, if the value is preceded by ../../, the router will search through the
grandparent's route definitions. The procedure is similar for all further ancestors.
A later section will present an example application that demonstrates how these
features can be put into practice.
Through dependency injection, components can access data structures related to routing.
Two of the most important structures are the ActivatedRoute and the Router. The
ActivatedRoute is simpler to work with, so we'll look at that first.
As discussed in Chapter 10, components can receive injected dependencies through
their constructors. The following code shows how an ActivatedRoute can be accessed:
Table 12.2
Members of the ActivatedRoute Class
Member Type/Return Value Description
snapshot ActivatedRouteSnapshot Current snapshot of the route
url Observable<UrlSegment[]> The route's URL segments
params Observable<Params> The route's parameters
queryParams Observable<Params> The URL's query parameters
fragment Observable<string> The URL's fragment
data Observable<Data> Data associated with the route
outlet string Name of the route's outlet
component Type<any>|string The route's associated component
routeConfig Route The associated route
root ActivatedRoute The top-most route
262 Chapter 12 Routing
public ngOnInit() {
...
}
}
It's important to understand the difference between query parameters and route
parameters. A query parameter is given in the URL's query string. A route parameter is a
segment of a route definition's path that takes the form :xyz. For example, consider the
following route definition:
If the URL's path contains a segment after foo, that segment will be provided as
a route parameter to the FirstComponent instance. The component can access the
parameter through the params member of the ActivatedRouteSnapshot. The
following code shows how this can be accessed:
263 Chapter 12 Routing
public ngOnInit() {
let msg: string = this.route.snapshot.data['num'];
}
}
As a result of this code, the component will be able to access the route parameter num
inside the ngOnInit method. Accessing data from a route definition uses similar code.
Table 12.3
Properties and Methods of the Router Class (Abridged)
Member Type/Return Value Description
config Routes The router's array of route
definitions
url string The current URL
routerState RouterState The router's state data
navigated boolean Indicates if at least one
navigation took place
events Observable<Event> Router events
errorHandler ErrorHandler Called for navigation errors
urlHandlingStrategy UrlHandlingStrategy Extracts and merges URLs
initialNavigation() void Initializes change listener and
performs initial navigation
navigate( Promise<boolean> Navigate using the given
commands: any[], commands
extras?:
NavigationExtras)
264 Chapter 12 Routing
Many of these are straightforward. config provides the current route definitions in a
Routes object and resetConfig updates the router's route definitions.
The navigate and navigateByUrl methods update the client's URL. The first
argument of navigate accepts an array of commands. This is the same type of array
assigned to the routerLink property in a router link. The following code shows what
this looks like:
The code in the ch12/advanced_router project demonstrates how the Router class
can be used. This project will be discussed shortly.
265 Chapter 12 Routing
In each code example so far, the router has matched route definitions by examining the
URL's path. But the router can be configured to examine the URL's fragment instead of
the path. This is a good idea when the URL's path needs to remain constant.
The method used by the router to examine URLs is called its location strategy. There
are two strategies available and each is represented by a class:
1. PathLocationStrategy — The router matches route definitions according to the
URL's path.
2. HashLocationStrategy — The router matches route definitions according to the
URL's fragment.
providers: [Location,
{provide: LocationStrategy, useClass: HashLocationStrategy}]
Chapter 10 explained how Providers are created. In this code, the useClass
property tells Angular to create a LocationStrategy dependency using the
HashLocationStrategy class.
The HashLocationStrategy becomes important when dealing with older
browsers, which send requests to the server every time a URL's path changes. Older
browsers don't send requests when the URL's fragment changes, so in this case, the
HashLocationStrategy may be preferable to the PathLocationStrategy.
// Route definitions
const routes: Routes = [
{path: '', redirectTo: 'foo', pathMatch: 'full'},
// Base component
@Component({
selector: 'app-root',
template: `
<p>Select a component pair:</p>
<ul>
<!-- First link uses value to set the route parameter -->
<li><a [routerLink]="['/foo', val]">
Parent: First component, path = /foo/11
</a></li>
public handleClick() {
// Navigate programmatically
this.router.navigate(['/foo', 2 * this.val],
{ queryParams: { color: 'red' } });
}
}
Below the router links, the template contains a button that invokes handleClick
when clicked. The handleClick method accesses the Router provided through
dependency injection and calls its navigate method to programmatically redirect the
client to another URL. The second argument of navigate is a NavigateExtras that
assigns query parameters for the URL.
268 Chapter 12 Routing
Listing 12.6 presents the code for the FirstComponent class. This demonstrates
how a component can access a route parameter using the ActivatedRoute. In this case,
the parameter's name is num and the component displays its value in an alert box.
@Component({
selector: 'app-first',
template: `
<h2>I'm the first component!</h2>
`})
export class FirstComponent implements OnInit {
constructor(private route: ActivatedRoute) {}
public ngOnInit() {
Listing 12.7 presents the code for the SecondComponent. This is similar to
FirstComponent, but the component accesses data passed through the route definition.
@Component({
selector: 'app-second',
template: `
<h2>I'm the second component!</h2>
`})
export class SecondComponent implements OnInit {
public ngOnInit() {
12.9 Summary
Angular's routing capability makes it possible to create single-page applications that can
be accessed using multiple URL paths. This enables the user to bookmark an application's
state and change states using the browser's Back and Forward buttons.
At minimum, the router's job is to examine the URL and insert components into
a base component. Its decision-making process is determined by a series of route
definitions provided in the application's module. Most definitions consist of a path field
that identifies the URL path and a component field that identifies the component to be
inserted.
A router link is a regular hyperlink whose routerLink property is set to an
expression. This expression can identify a static URL or an array of URL segments. It can
also contain URL parameters, which provide data to the inserted component. When a user
clicks on a router link, the router assembles the URL and determines whether it matches
one of its route definitions. If it doesn't, the application will produce an error.
If an application's module has been configured properly, its components will be able
to access routing capabilities through dependency injection. The ActivatedRoute
provides information related to the URL and the router's state, and makes it possible to
access data passed from the router. The Router provides methods that change the router's
behavior and navigate to new URLs.
Chapter 13
HTTP and JSONP
Communication
Internet users speak many different languages, but their computers all communicate
using the HyperText Transfer Protocol (HTTP). This protocol has two main parts: the
client asks for data by sending a message called a request. The server provides the data in a
message called a response. The nature of the response depends on the type of request, and
six common request types are GET, POST, PUT, DELETE, PATCH, and HEAD.
Chrome and Firefox support a function called window.fetch, which sends a request
to a URL and receives the response in a Promise. HTTP communication in Angular is
just as easy to use. The Http class provides six methods—one for sending each different
type of HTTP request. Each method returns an Observable that provides the response
for the given URL.
The Response class represents the server's response. This contains a number of
helpful methods that provide information about the response's structure and data. Other
methods transform the response's data to be accessible in code. In particular, its json
method transforms the response's body into a JavaScript object.
The last part of the chapter explains how to transfer data using a mechanism called
JavaScript Object Notation with Padding, or JSONP. This isn't as well known as HTTP,
but it provides a method of accessing data that isn't affected by a browser's cross-domain
restrictions. To be precise, JSONP uses <script> elements to read data from a page that
would normally be inaccessible due to the Same Origin Policy.
Angular's implementation of JSONP communication is as simple to work with as its
HTTP implementation. The Jsonp class provides one method, request, that accesses
data from a URL. The URL's data is provided asynchronously in an Observable.
272 Chapter 13 HTTP and JSONP Communication
@NgModule({
...
imports: [ ... , HttpModule ],
...
})
With this configured, the module's components will be able to access an Http
instance in their constructors. Using the Http instance generally consists of two steps:
1. Call the appropriate Http method to generate and send an HTTP request.
2. Access the Response through the Observable returned by the Http method.
This section begins by explaining how these steps can be performed. Then we'll
look at the Response class in detail. The last part of the section presents an example
component that uses Http methods to read records from a database.
constructor(backend: ConnectionBackend,
defaultOptions: RequestOptions);
Table 13.1
Methods of the Http Class
Method Description
request(url: string | Request, Perform an HTTP request determined by the
options?: RequestOptionsArgs) first string
get(url: string, Send a GET request to the specified URL
options?: RequestOptionsArgs)
post(url: string, body: string, Send a POST request to the specified URL
options?: RequestOptionsArgs)
put(url: string, body: string, Send a PUT request to the specified URL
options?: RequestOptionsArgs)
delete(url: string, Send a DELETE request to the specified URL
options?: RequestOptionsArgs)
patch(url: string, body: string, Send a PATCH request to the specified URL
options?: RequestOptionsArgs)
head(url: string, Send a HEAD request to the specified URL
options?: RequestOptionsArgs)
Each of these methods generates a request and sends it to the given URL. All of them
return an Observable that provides a Response when the server's response is received
by the application.
This discussion will provide a brief review of Rx Observables. But first, it's a good
idea to understand how to customize HTTP requests using a RequestOptions or a
RequestOptionsArgs.
The Http class provides two ways of configuring HTTP requests before they're sent
from the client. The first way involves adding a RequestOptionsArgs parameter to the
appropriate Http method. This configures the request generated by the Http method.
The second way is to set a RequestOptions parameter in the Http constructor.
This configures all requests generated by the Http instance.
RequestOptionsArgs
type RequestOptionsArgs = {
url?: string;
method?: string | RequestMethods;
search?: string | URLSearchParams;
headers?: Headers;
body?: string;
}
The search field makes it possible to set the URL's query string, which consists of
one or more name=value pairs separated by ampersands. This field can be set to a string,
as shown in the following code:
search: "language=TypeScript&api=Angular"
RequestOptions
constructor(
{method, headers, body, url, search}: RequestOptionsArgs);
As discussed in Chapter 11, an Observable's job is to emit data objects to one or more
observers. Each Http method in Table 13.1 returns an Observable that contains
response data. When a component receives this Observable, it performs two steps:
1. Convert the Observable's data to JavaScript object notation (JSON)
2. Set the JSON data equal to a property of the component
The simplest way to transform an Observable's data is to call the map method
with a conversion function. If obs is an Observable and data is the emitted result, the
following code shows how map can be used:
The map method returns an Observable that provides the transformed data. After
the data has been transformed, a component can perform further processing by calling the
subscribe method of the new Observable. This method accepts up to three functions,
and the first is called each time the Observable provides data.
For example, if obs is an Observable and data is its emitted data, the following
code shows how subscribe can be invoked to set this.result equal to the data.
The map and subscribe methods can be chained together to transform and store an
Observable's data. The following code shows what this looks like:
In this code, the map method transforms the Response by calling its json method.
After the transformation, the subscribe method sets the JSON data equal to the
component's data property.
This map-subscribe combination is frequently employed to process the
Observables returned by the methods in Table 13.1. These Observables provide the
response data using instances of the Response class, which will be discussed next.
276 Chapter 13 HTTP and JSONP Communication
Table 13.2
Members of the Response Class
Member Type/Return Value Description
type ResponseTypes Enumerated type: default, basic, cors, error, or
opaque
ok boolean Identifies if an error occurred (status between
200-299)
url string The response's URL
status number The response's status code
statusText string Text corresponding to the response's status code
bytesLoaded number Number of bytes downloaded from the server
totalBytes number Number of bytes expected in the response's body
headers Headers Container of the response's headers
text() string Returns the response's body as a string
json() Object Returns the response's body as an object
Information about the response's status line is provided by the status and
statusText properties. For example, if the HTTP communication proceeded normally,
status will equal 200 and statusText will equal Ok. The following code prints the
response's status text to the console:
277 Chapter 13 HTTP and JSONP Communication
The headers property provides a Headers object that contains the key/value
pairs in the response's headers. Four methods for accessing this information are given as
follows:
• get(key: string) — provides the value for the given key
• has(key: string) — identifies if any of the headers has a value for the specified
key
• keys() — provides an array of strings containing each key in the Headers
• values() — provides an array of string arrays containing each value in the
Headers
As an example, the following code looks at a response's headers and prints the array
of keys:
The two public methods of the Response class, text and json, relate to the
response's optional body. If the body is present, text returns the body as a string. If the
body is provided in JSON format, the json method returns a corresponding JavaScript
object.
For example, suppose the body of the response is given as follows:
The following code calls json to transform the response body into a JavaScript
object. Then it prints the object's red property to the console:
The object is available inside the subscribe method. But because subscribe
executes asynchronously, the object may not be available immediately after the
subscribe method. Therefore, if console.log() is called after subscribe, an error
may result due to the object's undefined value.
278 Chapter 13 HTTP and JSONP Communication
@Component({
selector: 'app-root',
template: `
<p>Authors:</p>
<table>
<tr *ngFor='let author of authors'>
<td>{{ author.first }}</td>
<td>{{ author.last }}</td>
</tr>
</table>
`})
export class AppComponent {
private authors: Object[];
private opts: RequestOptionsArgs;
If you send a GET request to https://unpkg.com using Http.get, the browser's Same
Origin Policy will prevent you from receiving a response. But the site can be accessed by
adding a <script> element to the HTML page. For example, the following markup reads
JavaScript from https://unpkg.com and inserts it into the web page:
<script
src="https://unpkg.com/@angular/http/bundles/http.umd.js">
</script>
An example will help make this clear. Suppose a web page contains the following
markup:
<script src="http://www.xyz.com?callback=processMe"></script>
If www.xyz.com has been configured for JSONP, it will know what data processMe
is looking for. When the <script> executes, www.xyz.com will perform three steps:
1. Read the value of the callback parameter.
2. Determine the desired values of the arguments of processMe.
3. Respond with a string that invokes processMe with suitable arguments.
When the first page receives the response, it will execute the call to processMe like
regular JavaScript code. If processMe wants the latest stock value of Acme, Inc. ($123.45)
and the current minute of the hour (42), the response from the second page might return
the following string:
processMe(123.45, 42);
This may seem straightforward, but there are issues that complicate matters. If
the first page needs to access the second page multiple times, it must create multiple
<script> elements. Also, if the first page sets the URL's query string to the same value as
before, the client's cache may return an old string from the second page.
For this reason, JSONP clients create <script> elements dynamically and insert
them in the document. This can be accomplished with jQuery or the DOM functions
presented in Chapter 5. Another option is to use Angular's JsonpModule, and the next
section explains how it works.
The central class in JSONP is Jsonp, which is a subclass of the Http class discussed
earlier. Because of this relationship, using the Jsonp class is essentially similar to using
the Http class. But there are three major differences:
1. To access JSONP dependencies, the module's @NgModule decorator must add
JsonpModule to its imports array.
2. JSONP supports GET requests only.
3. The response string must be specially formatted.
281 Chapter 13 HTTP and JSONP Communication
The last point requires explanation. The first section of this chapter explained that
each method of the Http class (get, put, and so on) returns an Observable that
provides access to the Response, which can be converted to a JavaScript object.
The get method of the Jsonp class also returns an Observable that emits the
Response text. The following code shows how Jsonp's get method can be called:
jsonp.get("http://www.xyz.com?CALLBACK=processMe")
.subscribe(res => this.result = res.json());
__ng_jsonp__.__req0.finished([{"name": "Matt"}])
When the first site receives this string, Angular executes the function, which creates
an Observable that emits {"name": "Matt"} as part of the response. The component
class can access the response data by calling the subscribe method of the Observable
returned by the get method.
The code in Listing 13.2 shows how JSONP can be used in practice. The component
produces the same table of authors as in the HTTP demonstration, but uses JSONP to
read data from the second site.
As shown, Jsonp's get method accepts the same URL parameter as the get method
of the Http class. But the query string doesn't affect which function is called. Instead, the
second site can use the query string to authenticate the calling site and/or determine what
data should be provided.
For this example, the second site provides the calling site with its list of authors by
returning the following text:
__ng_jsonp__.__req0.finished([
{"first": "Alexandre", "last": "Dumas"},
{"first": "Herman", "last": "Melville"},
{"first": "Mary", "last": "Shelley"}])
282 Chapter 13 HTTP and JSONP Communication
@Component({
selector: 'app-root',
template: `
<p>Authors:</p>
<table>
<tr *ngFor='let author of authors'>
<td>{{ author.first }}</td>
<td>{{ author.last }}</td>
</tr>
</table>
`})
export class AppComponent {
13.5 Summary
HTTP communication is central to the operation of the Internet. Thankfully, Angular
makes it easy to send requests and receive responses. There are three simple steps: inject
an Http instance into a component, call one of its request methods, and process the
Response in the returned Observable. The Response class provides methods for
examining and transforming the content of the HTTP response.
283 Chapter 13 HTTP and JSONP Communication
In an AngularJS 1.x application, a common use of two-way data binding involves form
validation—checking the user's input as it's entered to make sure the input is acceptable.
Two-way data binding ensures that the model will be updated with each user event and
that the view will be updated with every change to the model.
To simplify development, Angular provides a framework for creating, validating, and
monitoring the fields of a form. With this API, each field of interest can be associated
with a control, and each control can be associated with one or more validators. As the
user enters text in the field, the framework automatically triggers the control's validators.
In this manner, Angular provides the responsiveness of two-way data binding without
sacrificing performance and reliability.
Most of this chapter discusses the classes defined by the Reactive Forms API. A
FormControl represents an input element in the form and a FormGroup makes it
possible to manage multiple FormControls. A FormArray is similar to a FormGroup,
but stores FormControls in an array that can be dynamically resized.
Every FormControl can have associated validators. A validator is a function that
examines the FormControl's value and returns an error if the value doesn't meet its
criteria. Angular provides a handful of prebuilt validators, and if these aren't sufficient, it's
easy to define custom validators.
The last part of the chapter discusses the template-driven methodology of form
development. A template-driven form doesn't explicitly create FormControls or
FormGroups. Instead, each field of interest in the form is marked with a directive.
The form uses two-way binding to transfer data between the marked fields and the
component's class.
286 Chapter 14 Forms
<form action='auto_page.jsp'>
Make of automobile:
<input type='text' name='make'><br /><br />
Model of automobile:
<input type='text' name='model'><br /><br />
Year of manufacture:
<input type='number' name='year'><br /><br />
The type attribute of the last <input> element is set to submit. When the user
clicks on it, the form's content is uploaded to the URL given by the action attribute of
the <form> element. This content consists of name/value pairs in which the names are
determined by the name attributes of the <input> elements. The values are set equal to
the text inside each of the corresponding text boxes.
Figure 14.1 depicts the form created by the preceding markup. The appearance of an
<input> element is determined by its type attribute.
If the user clicks the Submit button, the uploaded data will contain three name/value
pairs: make/Dodge, model/Shadow, and year/1993. This data will be sent to the URL
associated with auto_page.jsp.
Angular provides two different ways of adding functionality to forms:
1. Reactive forms — All changes to the form and the component are managed in code
using instances of FormGroup and FormControls.
2. Template-driven forms — Changes to the form and component are configured using
two-way binding.
Most of this chapter focuses on reactive forms. These require more code but provide
greater configurability. A later section will discuss template-driven forms.
The code for this simple form creates one FormGroup for the form and two
FormControls—one for each text box. Listing 14.1 presents the code for the base
component.
288 Chapter 14 Forms
@Component({
selector: 'app-root',
template: `
In the component's template, the <form> element sets the formGroup property to
group. This tells Angular to associate the <form> element with the component's member
named group, which is an instance of the FormGroup class. As we'll see, a FormGroup
serves as a container of other form-related structures.
The component's template also contains two text boxes, and both <input> elements
contain the formControlName property. This property is set to a value that corresponds
to a FormControl member in the component class. The component reads and updates
the element's text by accessing the associated FormControl.
FormGroup and FormControl are both subclasses of AbstractControl. Figure
14.3 presents the inheritance hierarchy.
The following section will discuss the AbstractControl and FormGroup classes.
Later sections will explore the FormControl and FormArray classes.
To use these classes, an application's module needs to import
ReactiveFormsModule from @angular/forms. Also, ReactiveFormsModule must
be included in the imports array of the module's @NgModule decorator.
The AbstractControl class can't be instantiated by itself, but its members can be
accessed by any FormControl, FormGroup, or FormArray. Table 14.1 lists its properties
and methods.
290 Chapter 14 Forms
Table 14.1
Members of the AbstractControl Class
Member Type Description
value any A data value associated with the
control
validator ValidatorFn A synchronous function that
validates the control's data
asyncValidator AsyncValidatorFn A synchronous function that
validates the control's data
errors ValidationErrors Collection of errors related to the
control's data validation
parent FormGroup | The control's container
FormArray
root AbstractControl The control's top-level ancestor
status string A string that identifies whether
the data is valid or invalid
valid/invalid boolean Identifies if the control has errors
(invalid) or no errors (valid)
pending boolean Identifies if the status is pending
enabled/disabled boolean Identifies if the control has been
enabled or disabled
pristine/dirty boolean Identifies if the user has changed
the control's value
touched/untouched boolean Identifies if the user has triggered
a blur event on the control
valueChanges Observable<any> Emits an event when the control's
value changes
statusChanges Observable<any> Emits an event when the control's
validation status changes
get(path: string | AbstractControl Returns the child control with the
Array<string|number>) given path
enable({self, emit}: void Enables the control
{self?: boolean,
emit?: boolean)
disable({self, emit}: void Disables the control
{self?: boolean,
emit?: boolean)
291 Chapter 14 Forms
Every AbstractControl has a value property that can take any type. In the
simple_form example presented earlier, the value of each FormControl was the text in
the associated text box. When the value changes, valueChanges will emit an event to all
observers.
Every AbstractControl can have one or more associated functions that check
whether the control's value is acceptable. These functions are called validators, and can
operate synchronously or asynchronously. A synchronous validator can be assigned by
setting the validator member to a ValidatorFn, which is a function type defined as
follows:
The validator's result determines the control's status, which can take one of four
string values:
1. VALID — no errors have been detected
2. INVALID — at least one error has been detected
3. PENDING — the control's status hasn't been determined
4. DISABLED — the control is exempt from validation
The valid field is true if the form's status is VALID and the invalid field is true if
the status is INVALID. The pending field is true if the control's status is PENDING. Lastly,
disabled is true if the form's status is DISABLED and enabled is true if the form's status
isn't DISABLED. In code, a control's status can be set with the pending, enable, and
disable methods.
The get method makes it possible to access one of the control's children. This accepts
the path of the child to be created. In general, this is the name of the child provided in the
control's constructor.
A control is considered pristine if the user hasn't changed its value. Once the user
makes a change, the control is considered dirty. These states are represented by the
pristine and dirty fields. An application can change a control's pristine/dirty state by
calling its markAsPristine or markAsDirty methods.
The setValidator and setAsyncValidator methods specify a function or
functions to serve as the control's validator. A control's validators can be dissociated by
calling clearValidators or clearAsyncValidators.
293 Chapter 14 Forms
The only required parameter is the first. controls associates names with
AbstractControls. The following code creates a FormGroup with two FormControl
children whose names are name1 and name2:
Table 14.2
Properties and Methods of the FormGroup Class
Member Type/Return Value Description
controls {[key: string], Returns a container that maps
AbstractControl} each control to a name
addControl( void Adds a new control to the group
name: string, and assigns it the given name
c: AbstractControl)
registerControl( AbstractControl Adds a new control to the group
name: string, without updating its value or
c: AbstractControl) validity
removeControl( void Removes the given control from
name: string) the group
setControl( void Replaces an existing control
name: string,
c: AbstractControl)
contains(name: string) boolean Identifies if the named control is
in the group
294 Chapter 14 Forms
Just as the FormGroup constructor accepts a map that associates names with controls,
the controls field provides the group's map. Keep in mind that a FormGroup may
contain other FormGroups in addition to FormControls.
The addControl and registerControl methods both add new
AbstractControls to the group. The difference between the two is that
registerControl doesn't update the control's value or validity. For this reason, it's
generally preferable to use addControl instead.
The setValue, patchValue, and reset methods all update the group's value.
setValue always updates the entire group, while patchValue can update a part of the
group. reset can update the group with a new value, but if called without arguments, it
resets the group's value to null.
@Component({
selector: '...',
template: `
<form [formGroup]='group' (ngSubmit)='handleSubmit()'>
...
<input type='submit' value='Submit'>
</form>
`})
Before this example can work properly, the FormGroup must be initialized with
actual FormControls and corresponding elements must be inserted into the template.
The next section discusses FormControls in detail and shows how they can be associated
with template elements.
constructor(value?: any,
validator?: ValidatorFn | ValidatorFn[],
asyncValidator?: AsyncValidatorFn | AsyncValidatorFn[])
The first argument, value, provides an initial value for the corresponding input
element. For example, if a FormControl is associated with a text box, value will be
written to the box. The second and third arguments identify validation function(s) that
should be used to check the content of the corresponding input element.
I recommend setting an initial value for every FormControl, even if it's ''. In some
cases, if the user submits a form without touching a FormControl, the FormControl's
value will return null, which can lead to errors.
The FormControl class provides five public methods. Table 14.3 lists them and
provides a description of each.
Table 14.3
Members of the FormControl Class
Member Type Description
setValue(value: any, void Sets the value of the control
{self, emit, change}?:
{self?: boolean,
emit?: boolean,
change?: boolean)
patchValue(value: any, void Sets part or all of the control's value
options?: {?: boolean,
?: boolean, ?: boolean,
?: boolean)
reset(formState?: any, void Resets the control's value
{self: emit}?:
{self?: boolean,
emit?: boolean}
registerOnChange( void Assigns a function to be called when the
fn: Function) control's value changes
297 Chapter 14 Forms
The setValue, patchValue, and reset methods are similar to those discussed for
the FormControl class. When one of these methods is called, the FormControl's value
changes and the corresponding data in the template is updated.
The registerOnChange method assigns a function to be called when the
FormControl's value changes. Similarly, registerOnDisabledChange assigns a
function to be called when a disabled event occurs.
The Validators class provides static methods that validate AbstractControls. Table
14.4 lists each of these methods and describes what it accomplishes.
Table 14.4
Methods of the Validators Class
Function Description
nullValidator Returns null, performs no validation
required Checks that the control's value is present
requiredTrue Checks that the control's value is true
email Checks that the control's value is a valid email address
pattern Checks the control's value against a regular expression
minLength Checks that the value is longer than the minimum length
maxLength Checks that the value is shorter than the maximum length
compose Combines errors from multiple synchronous validators
composeAsync Combines errors from multiple asynchronous validators
If fc's value is null or an empty string, cont.valid will return false. The
fc.status property will return INVALID.
The email method checks if the control's value represents a valid email address. The
pattern method checks if the value matches a given pattern. The format of the pattern
value is the same as that of the patterns discussed in Chapter 3.
The minLength and maxLength methods both accept an argument that relates to
the length of an AbstractControl's value. The following code creates a FormControl
that will be invalid if its value is less than three characters long:
Similarly, the following FormControl will be invalid if its length is greater than ten:
This routine returns null if the FormControl's value contains street and returns a
{[key: string]: boolean} if it doesn't. If the FormControl's value is required, the
FormControl's validation can be configured with the following code:
if(!control.valid) {
for name in control.errors {
console.log(name + ': ' + control.errors[name]);
}
}
If the value is set to '', both functions will return {[key: string]: boolean}.
The results printed to the console will be given as follows:
required: true
streetCheck: true
The validation functions are executed in the order that they're given to Validators.
compose.
14.6.1 Subgroups
Rather than place every FormControl in one FormGroup, it helps to divide them
into multiple FormGroups that can be configured and validated individually. These
FormGroups must be inserted into a parent FormGroup that represents the overall form.
Subgroups are associated with elements using the same formGroup directive
discussed earlier. The following markup shows how one FormGroup can contain other
FormGroups:
301 Chapter 14 Forms
As shown, the formGroup directive associates main with the <form> element. Inside
the form, formGroup associates the FormGroups g1 and g2 with <div> elements. The
following code shows how main can be defined in the component class.
this.main =
new FormGroup({
'g1': new FormGroup(...),
'g2': new FormGroup(...)});
It's important to see that main is the variable name of the top-level FormGroup. g1
and g2 are names that main assigns to its children, not the names of the FormGroup
variables.
An important advantage of using subgroups is that they can be validated separately.
Later in this chapter, we'll look at example code that demonstrates how this works.
14.6.2 FormArrays
For example, the following markup associates a FormArray with a <div> element
that creates an <input> for each of its elements.
<div formArrayName='formArray'>
<input *ngFor='let ctrl of formArray.controls; let i = index'
formControlName='{{i}}'>
</div>
302 Chapter 14 Forms
This may be hard to understand at first, but it will become clearer as you become
more familiar with the FormArray class. A good place to start is by looking at its
constructor:
constructor(controls: AbstractControl[],
validator?: function,
asyncValidator?: function))
The members of FormArray are about what you'd expect for an array-based
container of AbstractControls. Table 14.5 lists all of the class's public members.
Table 14.5
Properties and Methods of the FormArray Class
Member Type/Return Value Description
controls Abstract The array of controls managed
Control[] by the FormArray
length number The number of controls managed
by the FormArray
at(index: number) Abstract Returns the control with the
Control given index
push(ctrl: void Adds a control to the end of the
AbstractControl) array
insert(index: number, void Inserts a control into the array at
ctrl: AbstractControl) the given position
removeAt(index: number) void Removes the control at the given
position
setControl(index: number, void Replaces the control at the given
ctrl: AbstractControl) position
303 Chapter 14 Forms
The following markup associates these objects with form elements using the
formGroup and formArrayName directives.
The formGroup directive associates the <form> element with myGroup, which is
the name of the top-level FormGroup. Inside the form, the formArrayName directive
associates the <div> element with array, which is the name of the FormArray assigned
by the FormGroup constructor. In contrast, the ngFor directive accesses myArray, which
is the actual name of the FormArray member. It's important to recognize when to use the
member name versus the name assigned by the parent.
304 Chapter 14 Forms
Inside the FormArray's <div> element, the following markup creates one <input>
element for each element in the FormArray:
14.7.1 Checkboxes
If the type of an <input> is set to checkbox, it will take the form of a checkbox. As with
text elements, this can be associated with a FormControl using the formControlName
directive. For example, the following markup associates a checkbox with a FormControl
named boxControl:
For a checkbox, the FormControl's value is boolean. The box will be initially
checked if the initial value is set to true, and it will be initially unchecked if the value is set
to false. If the FormControl's initial value isn't set and the form is submitted, the value
will equal null.
305 Chapter 14 Forms
Chapter 9 explained how NgFor can set options of a <select> equal to elements of an
array. If you intend to associate a <select> element with a FormControl, the NgFor
directive is required. Otherwise, the FormControl's value will remain at null. For
example, this code defines an array of strings and a FormGroup with one FormControl:
The following markup creates a form that contains a <select> element. The
formControlName directive associates the <select> element with the FormControl.
The NgFor directive creates the options of the <select> element to the strings in the
colors array.
<select formControlName='selectControl'>
<option *ngFor='let color of colors' [value]='color'>
{{color}}
</option>
</select>
</form>
In this case, the FormControl's value property is a string because the <select>
element's options are strings. Therefore, if an initial value is set, it must be set to one of the
strings in the array. If the options are numbers instead of strings, the value property will
be a number and the FormControl's initial value must be set to a number.
// Create a FormGroup
this.group = formBuilder.group({
'input1': this.cntrl1,
'input2': this.cntrl2
});
}
The next section presents a more interesting example of how FormGroups and
FormControls can be used.
Listing 14.2 presents the code for the component. The template markup can be found
in app.component.html and the CSS rules are in app.component.css.
@Component({
selector: 'app-root',
styleUrls: ['app.component.css'],
templateUrl: 'app.component.html',
})
constructor() {
The two topmost controls ask for a name and email address. Both controls
have assigned validators, and in the case of the email address, the validators include
Validators.email, which ensures that the address is structured correctly.
If either field fails validation, the component expands the top portion of the form
and displays a message in red. Further, the Submit button remains disabled until the form
validates successfully.
When the user selects a menu item, the item is added to a list displayed in the form's
lower-left. Each item has a Delete button that allows the user to remove the item from
the order. This behavior is made possible through the NgFor directive, as shown in the
following markup.
310 Chapter 14 Forms
<ul>
<li *ngFor='let order of orders; let i = index'>
{{order}} <input type='button' value='Delete'
(click)='deleteOrderItem(order, i)'>
</li>
</ul>
In this markup, order is a local variable that identifies the menu item and its price.
When the user clicks the item's Delete button, the item is removed from the list and its
price is subtracted from the total.
<input [(ngModel)]='input1'>
Due to two-way binding, the element's value will be updated when input1 changes
and input1 will be updated when the element's value changes.
When using template-driven forms, there are four points to be aware of:
1. The application's module needs to import FormsModule and add FormsModule to
the imports array of the module's @NgModule decorator.
2. The <form> element needs to be associated with an NgForm.
3. Each element to be associated with a control needs to set its name property and set
[(ngModel)] equal to a component property.
4. Validation isn't performed in code, but is accomplished by inserting directives into
elements of interest.
The first point is straightforward to understand, but the other points require
explanation. The following discussion introduces the NgForm and then explains how
validation works in template-driven forms. The end of the section presents example code
that demonstrates how a template-driven form can be created.
311 Chapter 14 Forms
This markup sets the local variable f equal to ngForm. This tells Angular to associate
the form with an NgForm. The local variable can be used throughout the form, and in this
case, the disabled property is set equal to !f.form.valid. The form field corresponds
to the NgForm's FormGroup.
After setting the NgForm, the template needs to identify which elements should be
accessed as form controls. This is set by assigning a name attribute to the elements of
interest. This is shown in the following element.
14.10.2 Validation
In a template-driven form, validation is simple if you use the built-in validators discussed
earlier. Validators are set by adding directives to elements of interest. For example, the
required directive indicates that the element isn't valid until it has a defined value. The
following markup shows how this is used.
This discussion examines these steps in detail. Afterward, I'll present an example that
uses custom validation to check element values in a template-driven form.
Coding a directive to perform custom validation is more difficult than coding a regular
directive. To see why this is the case, consider the following element of a template-driven
form:
This Provider can be associated with a directive through the providers array in
the @Directive decorator. The following code shows what this looks like:
@Directive({
selector: '[validate-street]',
providers: [streetCheckerBinding]
})
class StreetValidator {}
The directive class doesn't need any code. As long as the right Provider is associated
with the directive, elements of interest will be validated by the function bound to
NG_VALIDATORS. In this example, each element containing the marker
validate-street will be validated by the streetChecker function.
In reactive forms, error reporting is easy. Just add an element containing an error message
and control its insertion using NgIf. The following element shows how this works:
Error reporting in a template-driven form is more difficult because it's not as easy
to access the form's controls. Instead of adding NgIf to a basic element, it's necessary to
code a component that performs three operations:
1. Access a control validated by a directive discussed earlier
2. Check the control's errors
3. If the control is invalid, display the appropriate error message
For the first step, the component needs to access the NgForm associated with the
overall <form> element. This can be accomplished using dependency injection, and the
following code shows how a constructor can access the form's NgForm:
As discussed earlier, the NgForm class provides a form method that returns the
form's FormGroup. The FormGroup provides a get method that returns a FormControl
according to its name. Therefore, after an error-reporting component has access to an
NgForm called ngForm, it can obtain a FormControl with code such as the following.
314 Chapter 14 Forms
control = mainForm.form.find(name);
After obtaining the FormControl, a component can access its valid property
to determine its state. If it's invalid, the component can check which errors are present
by calling hasErrors. For example, if an invalid ID value is represented by the badId
string, the following code identifies if control contains an invalid ID value:
idError = control.hasError('badId');
After coding the validation directive and error-reporting component, the last step is to
define the form in a component's template. If a form element requires validation, two
steps are needed:
• Insert the attribute corresponding to the validation directive
• Follow the element with an error-reporting component that contains the name of
the control
The error-reporting component must be given the control's name and the name of
the error or errors to be tested. If the component's selector is error-rep and the error
key is badid, the component's markup could be given as follows:
After accessing the form's NgForm, this component can use the control name,
represented by the control attribute, to find the corresponding FormControl. Then it
can use the name of the error, represented by the error property, to test if the validation
error is present. If so, it will display the message corresponding to the error's name.
To demonstrate how custom validation is accomplished, the following discussion
presents the ch14/template_driven project. This contains two custom validators—one that
checks zip codes and one that checks area codes.
315 Chapter 14 Forms
In America, a zip code is a five-digit value used to identify location in mailing addresses.
An area code is a three-digit value used to identify location in telephone numbers. The
template_driven application displays a form whose input fields ask for a zip code and area
code. Figure 14.x shows what the form looks like:
Each input field has a custom validator and the Submit button won't be enabled until
both fields are valid. The project's code can be found in the ch14/template_driven/app
directory, which contains six TypeScript files:
1. app.module.ts — The application's module
2. app.component.ts — The base component defines the template
3. error-message.component.ts — Displays an error message for invalid elements
4. ac-validator.directive.ts — Directive that validates area codes
5. zip-validator.directive.ts — Directive that validates zip codes
6. error.service.ts — Associates error keys with error messages
To explain how the application works, this discussion looks at the base component
first, the error reporting component second, and then the two validation directives.
Base Component
The app.component.ts file defines the template containing the template-driven form.
Listing 14.3 presents its code.
In the template, the <form> element sets the local variable f equal to ngForm. This
tells Angular to associate the form with an NgForm instance. The NgForm creates the
FormGroup for the form and creates a FormControl for each element with an assigned
name attribute.
316 Chapter 14 Forms
@Component({
selector: 'app-root',
styleUrls: ['app.component.css'],
template: `
<form #f='ngForm' (ngSubmit)='submit()'>
The form consists of two text boxes, two error reporting components, and a Submit
button. The <input> elements for the text boxes have their name attributes set, so
Angular will create a FormControl for each. In addition, both <input> elements have
validation directives. The first <input> contains required and validateZip directives
and the second contains required and validateAreaCode directives.
317 Chapter 14 Forms
Each text box has an error-reporting component to display a message in the event of
an invalid element. In markup, these components are given by <app-error> elements,
and each <app-error> has two properties. The first, controlName, identifies the name
of the element whose validity should be checked. The second, errorChecks, identifies
the nature of the error checking to be performed.
Error Reporting
The component receives two input values from the <app-error> elements in the
form:
• controlName — the name of the FormControl whose validity needs to be
checked
• errorChecks — an array of strings that identify which errors should be checked
To obtain a value for msg, the component uses the NgForm and controlName to
find the FormControl that needs to be checked. If the control is invalid, the component
checks through the list of applicable errors. If it finds a match, msg is set to the appropriate
error message. If the control is valid, msg will be set to null and no error message will be
displayed.
Validation Directives
To validate the text boxes, the application creates two custom validators. The first validates
the zip code, which must consist of five digits. The second validates the area code, which
must consist of three digits.
In code, both validators are implemented with directives. When a directive is
associated with a template element, the directive will check its value to determine the
element's validity.
Listing 14.5 presents the code for the directive that validates zip codes. The directive
class, ZipValidatorDirective, doesn't contain any code. Instead, the providers
array in the @Directive decorator contains a Provider that binds the name of a
function to the NG_VALIDATORS token. This function, zipValidator, accepts a
FormControl and returns an error key (zipcode) if the value doesn't consist of five
digits.
Any element containing the validateZip marker will be accessed by the directive.
Because of the Provider's binding, the element's FormControl will be processed by the
zipValidator function.
319 Chapter 14 Forms
@Directive({
selector: '[validateZip]',
providers: [zipValidatorBinding]
})
export class ZipValidatorDirective {}
@Directive({
selector: '[validateAreaCode]',
providers: [acValidatorBinding]
})
export class AcValidatorDirective {}
320 Chapter 14 Forms
As shown in Listing 14.6, the directive that validates the area code is nearly identical
the directive that validates the zip code. The only significant difference is that the
validation function, acValidator, uses a different pattern to check the FormControl's
value.
14.11 Summary
Angular provides two APIs for building forms: the Reactive Forms API and the Forms
API. Both APIs make it possible to validate a form's inputs automatically, and the only
task left to the developer is to define the validation functions.
Most of this chapter has focused on the Reactive Forms API, which provides classes
that simplify the process of interacting with the form's elements. Inside a form, each field
of interest is associated with a FormControl. Each FormControl has a name, a value,
and one or more optional validators.
FormControls are managed by a FormGroup, which can represent the entire form
or just a portion of it. If the number of FormControls may change dynamically, it's
better to place them in a FormArray. The FormControl class, FormGroup class, and
FormArray class are all subclasses of AbstractControl.
For AngularJS 1.x developers who prefer two-way data binding, the Forms API
supports template-driven forms. A key advantage is that it's unnecessary to deal with
FormControls and FormGroups in code. The main disadvantage is that, without access
to FormControls, it's difficult to associate validators with the form's fields. It's also
difficult to test the validation functions programmatically.
Chapter 15
Animation, i18n,
and Custom Pipes
This chapter explores three topics that are helpful and important but not necessary in
Angular development: animation, internationalization (i18n), and custom pipes. Once
you understand these topics, you'll be able to code applications that appeal to a wide range
of users and provide a professional look-and-feel.
In Angular, animation refers to changing an element's CSS properties over time.
The process is conceptually simple—define CSS states and set the transitions between
them. But implementing animation in code requires calling many nested functions whose
arguments can be difficult to keep track of.
Angular can't translate your application's text into other languages, but it can facilitate
the translation process. To be specific, the goal of Angular's internationalization (i18n)
toolkit is to produce files that clearly identify what translation needs to be performed.
These files can be generated according to the XLIFF (XML Localisation Interchange File
Format) or XMB (XML Message Bundle) formats. These files can be used as input by
computer-aided translators and human translators.
Chapter 8 discussed the topic of pipes and explained how to format template
data with Angular's built-in pipes. This chapter explains how to code custom pipes to
perform custom data formatting. The process involves coding a class that implements the
PipeTransform interface. The transform method of this class will receive the data to
be formatted along with formatting flags, and will return the string to be displayed in the
template.
In AngularJS 1.x, pipes are referred to as filters, and one of the most useful filters is
orderBy, which sorts elements of a collection for display. The last part of this chapter
demonstrates how this capability can be implemented as a custom Angular pipe.
322 Chapter 15 Animation, i18n, and Custom Pipes
15.1 Animation
Animation is one of Angular's newest and most interesting features. With the right
animation, a web application can add a level of polish and delight that static graphics can
never provide.
Technically speaking, Angular animation involves changing an element's CSS
properties over time. For this purpose, Angular provides functions that define sets of CSS
properties (called CSS states) and the transitions between states.
To enable animation, the @angular/animations package must be installed. Then the
application's module needs to import AnimationsModule from @angular/animations.
Lastly, the application needs to define animations by calling functions inside its
@Component decorator. This section explains how these functions can be called.
The @Component decorator can contain many fields, including selector, styleUrls,
and template. It can also contain a field named animations. If this is present, the field
must be set equal to an array whose elements define transitions between CSS states.
Each element of the animations array defines an animation by calling the trigger
function. Therefore, a component that uses animation will have the following structure:
@Component({
selector: '...',
styleUrls: ['...'],
template: `
...
`,
animations: [
trigger('trig1', [...]),
trigger('trig2', [...]),
]
)
The trigger function accepts two arguments—a string identifier and an array of
function calls. The two most important functions that can be called inside the trigger
array are as follows:
• state — Associates a name with a set of CSS properties
• transition — Identifies how a change between states should be performed
323 Chapter 15 Animation, i18n, and Custom Pipes
In general, the array inside trigger will contain two or more state calls followed
by one or more transition calls. Each call to state defines creates a named set of CSS
properties that set an element's appearance in a given state. Each call to transition
identifies how an element should transition from one CSS state to another. The following
discussion presents the state and transition functions in detail.
As with the trigger function, the first argument of the state function is a string
identifier. The second argument calls the style function with one or more CSS rules
contained in curly braces. As an example, the following code demonstrates how state
can be called:
It should make sense that most trigger arrays contain at least two state calls.
These state calls should contain similar CSS properties but assign them different values.
After defining CSS states with state calls, a trigger array should define state
transitions with calls to transition. The first argument of the transition function is
a string with three parts: the name of a starting state, the => symbol, and an ending state.
The second argument of transition is an array containing one or more calls of the
animate function. The following code gives an idea of what this looks like:
animations: [
// Define an animation
trigger('trig1', [
// Starting state
state('start', style({ 'background-color': 'green' })),
// Ending state
state('end', style({ 'background-color': 'blue' })),
In this code, start and end identify different CSS states. The first argument of
transition is 'start => end', which implies that the transition will only be performed
when the element is in the start state. The state transition will continue until the
element's CSS properties match those defined in the end state.
Instead of naming specific states, the transition function can use wildcards. The *
wildcard represents any state of an element. Similarly, the void identifier represents any
state in which the element is not attached to a view.
If set to a string, the first argument of animate can contain one, two, or three values.
The second value sets the time delay before the transition starts. The third value identifies
the rate of the transition's change over time, and can take one of three values:
• ease-in — slow at the beginning, fast at the end
• ease-out — slow at the beginning, fast at the end
• ease-in-out — slow at the beginning and end, fast in the middle
The following code shows how this works. It sets the duration of the transition to 750
milliseconds, the delay to 100 milliseconds, and makes the transition fast at the end:
The second argument of animate is optional, and can be set to a call to the style
function or a call to the keyframes function. If the second argument is set to a call to
style, the CSS properties in the function will be applied during the action represented by
animate. For example, the following code sets the element's background color to yellow
during the 250ms duration of the animate action.
325 Chapter 15 Animation, i18n, and Custom Pipes
The first keyframe state lasts from 0.1 to 0.6, the second lasts from 0.6 to 0.8, and the
third lasts from 0.8 to 1.0. The total time is 1000 ms, so the first keyframe lasts 500 ms, the
second keyframe lasts 200 ms, and the last keyframe lasts 200 ms. The first keyframe state
is delayed by 100 ms.
After the animations array has been populated with trigger calls, the next step is to
associate the animations with template elements. This generally involves two steps:
1. For each element of interest, insert a property whose name identifies the trigger
call (preceded by @). The property's value should be set equal to a member variable
of the component class.
2. In the component class, set the member variable to a string that identifies one of the
state calls in the trigger function.
326 Chapter 15 Animation, i18n, and Custom Pipes
Let's look at a simple example. Suppose you want a button to change color from blue
to green when clicked. The animations array might look like the following:
animations: [
trigger('blueToGreen', [
state('start', style({ 'background-color': 'blue' })),
state('end', style({ 'background-color': 'green' })),
transition('start => end', [ animate(500) ])
])
]
The property's name is @blueToGreen, which is the ID of the trigger call preceded
by the @ symbol. This property is set to stateVar, which is a member variable defined in
the component class.
When the button is clicked, the component's changeState method is called. We
want the button's state to toggle between start and end, so the code for changeState
should look like the following:
public changeState() {
this.stateVar = (this.stateVar === 'start' ? 'end' : 'start');
}
As a result of this method, clicking the button will change stateVar's value from
start to end or from end to start. If the method changes stateVar's value from
start to end, it will trigger the animation identified by blueToGreen, and the button's
background color will change over a duration of 500 milliseconds.
It's important to see that the animation only works in one way—from the start state
to the end state. Using wildcards, we can change the code to perform animation whenever
any state change takes place. This is given as follows:
An application can receive data related to an element's animation state by adding special
events to the element. To be precise, an application can configure an element to emit an
event when its animation starts or ends. For example, the following event calls a method
when animation starts:
(@trigger_name.start)='method_name'
(@trigger_name.done)='method_name'
<button
(@blueToGreen.done)="endState()"
[@blueToGreen]='stateVar'>
</button>
This code doesn't pass any arguments to the event-handling method, but an
application can pass an AnimationTransitionEvent through a variable named
$event. When a method receives an AnimationTransitionEvent, it can obtain
information by accessing its fields:
• element — The element associated with the animation
• triggerName — The name of the trigger
• fromState — The previous animation state
• toState — The current animation state
• phaseName — The animation phase (start or done)
• totalTime — The total duration of the animation
The first three animations demonstrate the different easing methods: ease-in,
ease-out, and ease-in-out. The fourth animation uses keyframes to define how the
text moves during the animation. This animation is defined with the following code:
trigger('text4', [
state('start', style({ 'margin-left': '0px' })),
state('end', style({ 'margin-left': '400px' })),
The total duration of the animation is two seconds. As a result of the keyframes, the
text appears to move quickly from time 0.2s to 0.6s. Then it appears to move slowly from
0.6s to 2s.
329 Chapter 15 Animation, i18n, and Custom Pipes
This section looks at the first three steps and shows how they can be accomplished.
If an element contains text to be translated, it should be marked with the i18n attribute.
This tells the translation utility to include the element in the generated translation file.
The following markup shows how it can be used:
The i18n attribute can be assigned a value that provides the translator with special
instructions. In the following markup, the attribute's value tells the translator that the text
contains an idiom that shouldn't be taken literally:
Now suppose that the value of an element's attribute needs to be translated. For
example, if an <img> element contains an alt attribute, the attribute's value should be
translated. To mark the attribute, i18n should be replaced with i18n-xyz, where xyz is
that attribute's name. The following markup shows how what this looks like:
Here, the i18n-alt tells the utility that the value of alt is intended to be translated.
Another common example is i18n-title.
Chapter 8 introduced Angular's pipes and explained how they can be used to format data
in a component's template. Two of the pipes, i18nSelect and i18nPlural, weren't
discussed. These pipes relate to internationalization, so we'll look at them here.
i18nSelect
In many applications, words may need to change based on user information. For example,
a description may need to include 'her' if the user is female and 'him' if the user is male.
The i18nSelect pipe makes it possible to update the translation file as needed.
This pipe must be accessed with a parameter that identifies an object that maps keys
to display strings. The pipe's input is a key that identifies which string should be displayed.
To see how this works, consider the following element in a component's template:
gender and display are variables defined in the component. The first sets the
user's gender (male or female). The second variable is an object that associates gender
values with display text. The following code shows what this object may look like:
i18nPlural
The i18nPlural pipe is like i18nSelect, but the displayed text changes according
to multiplicity. For example, suppose an application should display item if num is 1 and
items for any other value. The markup could be given as follows:
numMap associates numeric values with output text using conventions established in
the ICU format. This is shown in the following code:
numMap = {'=0': 'no items', '=1': 'one item', 'other': '# items'};
331 Chapter 15 Animation, i18n, and Custom Pipes
After the i18n attributes and pipes have been inserted into a template, the ng-xi18n utility
can be used to generate a translation source file. This utility should be present in the
project's node_modules/.bin directory.
To determine which files need to be translated, ng-xi18n reads tsconfig.json, which
should be present in the current directory. The command for executing ng-xi18n from a
project's top-level directory is given as follows:
./node_modules/.bin/ng-xi18n
When the processing is finished, the utility will produce a file called messages.xlf. The
file's content is structured according to the XML Localisation Interchange File Format,
or XLIFF, which is commonly used to provide data to computer-aided translation (CAT)
systems. For example, suppose that a template consists of the following:
When ng-xi18n processes this, messages.xlf will contain the following text:
<xliff version="1.2"
xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext"
original="ng2.template">
<body>
<trans-unit id="..." datatype="html">
<source>It's not rocket science.</source>
<target/>
<note priority="1" from="description">English idiom</note>
</trans-unit>
<trans-unit id="..." datatype="html">
<source>Have a nice day!</source>
<target/>
</trans-unit>
</body>
</file>
</xliff>
The file format can be changed to the XML Message Bundle (XMB) format by
following the command with --i18nFormat=xmb. If this is used, the resulting file will be
messages.xmb instead of messages.xlf.
332 Chapter 15 Animation, i18n, and Custom Pipes
This section starts by explaining these steps in detail. Then I'll show how to code
a pipe that orders elements of an array. It's called orderBy because I always liked the
orderBy filter from AngularJS.
Unlike component classes, a pipe class must provide code for a specific method. This
method, called transform, is defined in the PipeTransform interface:
The value parameter identifies the primary value directed to the pipe. The args
array contains any colon-separated arguments following the name of the pipe. To clarify
how name, value, and args are related, Figure 15.2 shows how the code in a pipe class
relates to its usage.
333 Chapter 15 Animation, i18n, and Custom Pipes
In this figure, the transform method of MyPipe receives the pipe's var input as its
value parameter. It receives the pipe's x and y arguments in its args array. The return
value of transform is the pipe's output.
In AngularJS 1.x, the orderBy filter sorts elements of an input array. Angular doesn't
have an equivalent pipe, so this discussion explains how an orderBy pipe can be coded.
But before getting into the code, I'd like to explain how orderBy is used.
The two arguments are optional, and if the pipe is used without arguments, it
arranges the array's elements in ascending order. This is shown in the following examples:
• {{ ['b', 'c', 'a'] | orderBy }} evaluates to [ 'a', 'b', 'c' ]
• {{ [9, 22, 6] | orderBy }} evaluates to [ 6, 9, 22 ]
let objArray = [{letter: 'b', num: 3}, {letter: 'c', num: 5},
{letter: 'a', num: 1}, {letter: 'z', num: 5}];
This array can be sorted in descending order of the num property with the following
usage of orderBy:
Now suppose we want to sort objArray using two properties—in descending order
of the num property first, and descending order of the letter property second. In this
case, the expression argument is an array and the usage of orderBy is given as:
The third method of using orderBy provides a function that returns a number to
be used to identify the item's position in the list. For example, the following function
converts each input to a string and returns the length of the string:
stringLengthSort = function(item) {
return JSON.stringify(item).length;
};
In this case, the sort can be performed with the following code:
The pipe's second argument, reverse, is a boolean. This identifies whether the
sorted result should be reversed.
335 Chapter 15 Animation, i18n, and Custom Pipes
Now that you understand how the orderBy pipe works, it's time to look at the code.
Figure 15.3 illustrates the decision-making process that determines how the pipe sorts the
elements of the input array.
To sort the input elements, orderBy relies on JavaScript's sort method for arrays.
If called without an argument, sort rearranges the array's elements in lexicographical
(dictionary) order.
336 Chapter 15 Animation, i18n, and Custom Pipes
sort's optional argument is a function that identifies how the array's elements should
be arranged. This function should accept two values and return a number that identifies
the relative ordering of the two values.
• If the first value is less than the second, the function should return a negative value.
• If the second value is less than the first, the function should return a positive value.
• If the two values should be treated equally, the function should return zero.
As a second example, the following function sorts the objects in objArray according
to their name property:
let objArray = [{name: 'Bill', age: 19}, {name: 'Ken', age: 23},
{name: 'Tom', age: 24}, {name: 'Dave', age: 20}];
primeArray.sort((objA: Object, objB: Object) => {
if (objA.name < objB.name) { return -1; }
if (objA.name > objB.name) { return 1; }
return 0;
});
If you understand the pipe's flowchart and JavaScript's sort method, you should
have no trouble understanding how the custom pipe can be coded. Listing 15.1 presents
the full code of the OrderByPipe class.
This pipe is accessed in AppComponent's template, which combines the pipe with
NgFor to select which array elements should be displayed. For example, the following
code uses orderBy to select elements from stringArray:
<ul>
<li *ngFor="let element of stringArray | orderBy : '-' ">
{{ element }}
</li>
</ul>
This construction makes it possible to read records from a database and sort the
results on the client. This makes it unnecessary to sort data on the server.
337 Chapter 15 Animation, i18n, and Custom Pipes
15.4 Summary
The topics of animation, i18n, and custom pipes aren't sufficiently deep or interesting
to merit chapters of their own. But if an application needs eye-catching graphics and an
international audience, these capabilities are be very useful to know.
In Angular, animation involves changing an component's CSS properties over time.
This is accomplished in code by adding a field named animations to the component's
@Component decorator. This should be set to an array of calls to the trigger function,
whose arguments define different CSS states and transitions between them.
341 Chapter 15 Animation, i18n, and Custom Pipes
The standard set of HTML elements aren't suitable for every application. They're popular
and easy to use, but they're boring, two-dimensional, and don't adhere to the best
practices of graphic design. If you really want to impress your users, you need something
new.
Material Design is Google's best attempt to improve on traditional web design. This
framework describes a set of graphical elements and layouts, and provides a philosophy
for uniting them in a user interface. The first Material library was released for Android,
but Google has also released libraries for Polymer, AngularJS 1.x, and Angular.
The Angular Material library is incomplete, but it's still worth investigating. This
chapter examines many of the library's graphical elements: checkboxes, text boxes,
buttons, radio buttons, cards, lists, toolbars, and progress spinners. For each of these
elements, I'll explain how it works and demonstrate how it can be accessed in code.
16.1 Introduction
At the 2014 Google I/O conference, Google unveiled a new design architecture for
graphical applications called Material Design. Though originally intended for Android
user interfaces, this framework has branched out to encompass web design.
To understand why Google created Material Design, you need to visit the main site,
https://www.google.com/design/spec/material-design. Much of the content is written by
and for graphic designers. The following quote gives an idea:
344 Chapter 16 Material Design
16.2 Overview
The Angular Material Design library consists of graphical elements and themes that
support Google's vision of how an application should appear and behave. In code, each
graphical element corresponds to an Angular component or directive. The goal of this
chapter is to show what these components/directives accomplish and how to access them
in code.
This section starts by explaining how to install the Material Design library. Then we'll
look at the overall process of using the library to builds applications. The last part of the
section explains what themes are and how to specify a theme for an application.
16.2.1 Installation
To take full advantage of Material Design in a project, three packages must be installed.
The following command installs everything:
The Angular Material Design Library is a powerful toolset, but a number of steps must be
performed before its components can be accessed in an application. There are four steps
in total:
1. The application's module must import MaterialModule from the
@angular/material package. MaterialModule must be inserted into the
imports array of the module's @NgModule decorator.
2. Every application must define a core theme that determines the color scheme
employed by the Material Design components. This topic will be discussed shortly.
3. If any components take advantage of animation, the module must import
BrowserAnimationsModule from @angular/animations and add
BrowserAnimationsModule to the imports array.
4. If any components require gesture support, make sure the module imports the
HammerJS library with import 'hammerjs'.
Without question, the second step is the most annoying. The next section explores
the topic of Material Design Themes.
An application's theme is the set of colors used by Material Design components in the
application. To simplify color selection, Material Design provides four predefined themes
whose names identify the primary color and accent color. Each theme is given as a CSS file
in the project's node_modules/@angular/material/prebuilt-themes directory.
346 Chapter 16 Material Design
At the time of this writing, the @angular/material package provides the following
built-in themes:
• deeppurple-amber.css — Primary color is deep purple, accent color is amber
• indigo-pink.css — Primary color is indigo, accent color is pink
• pink-bluegrey.css — Primary color is pink, accent color is blue-gray
• purple-green.css — Primary color is purple, accent color is green
In an ideal world, we'd be able to configure the theme by adding the theme file to the
component's styleUrls array. But that's not sufficient. One way to include the CSS file
at the top level is add a statement to the src/styles.css file, such as the following:
@import '~@angular/material/prebuilt-themes/deeppurple-amber.css';
For the second step, we need to set the CSS class of an element surrounding the
application's element. This can be accomplished by modifying the <body> element in the
src/index.html file:
<body class='mat-app-background'>
The example projects in this file provide code for the project's src/app directory, but
don't provide CSS files or HTML files. Therefore, before you run any of these projects, be
sure to update the src/styles.css file and the <body> element in the src/index.html file.
Table 16.1
Material Design Components
Class Selector
MdCheckBox md-checkbox
MdInputContainer md-input-container
MdButton/MdAnchor md-button
md-raised-button
md-icon-button
md-fab
md-mini-fab
MdRadioGroup md-radio-group
MdRadioButton md-radio-button
MdCard md-card
MdCardHeader md-card-header
MdCardTitleGroup md-card-title-group
MdList md-list
MdListItem md-list-item
MdListAvatar md-list-avatar
MdToolbar md-toolbar
MdProgressSpinner md-progress-spinner
The selectors in the right column accept attributes and properties that configure the
component's appearance and behavior. The following discussion explores many of the
available features, but if you're interested in a complete presentation, it's a good idea to
download the source code from https://github.com/angular/material2.
16.3.1 Checkbox
Of the components in the Material Design library, the checkbox is probably the simplest.
It behaves like an <input> element of type checkbox, but provides features beyond
those of the common HTML element. The most noticeable features are the colored
background, the flash when the box is checked or unchecked, and the ability to control the
box's state with the Space key. Figure 16.1 shows what it looks like.
348 Chapter 16 Material Design
Checkboxes are easy to integrate into web components. Listing 16.1 presents the code
for the ch16/checkbox_test project. As shown, the checkbox's label is set by inserting text
between <md-checkbox> and </md-checkbox>.
@Component({
selector: 'app-root',
template: `
<md-checkbox [checked]='true'>
Initially checked
</md-checkbox><br /><br />
private handleClick() {
this.isChecked = !this.isChecked;
this.msg = this.isChecked ?
'Uncheck second button' : 'Check second button';
}
}
349 Chapter 16 Material Design
The component's template contains three checkboxes. It checks the first by setting
checked to true. The second box is to the right of its label because the align attribute
is set to end. The third checkbox is checked and disabled because its checked and
disabled attributes are set to true.
The component uses two-way binding to associate the state of the second checkbox
with a property named isChecked. When the button is clicked, the component changes
the value of isChecked, thereby checking or unchecking the second checkbox and
changing the button's text.
<md-input-container>
<input mdInput placeholder="Placeholder text" required>
</md-input-container>
As shown, the input element can display text below the entry line that provides
instructions or feedback. This is made possible by adding an <md-hint> element. The
base component in the ch16/input_test project demonstrates how hints can be used to
perform basic validation. Listing 16.2 presents the code.
350 Chapter 16 Material Design
@Component({
selector: 'app-root',
styles: ['md-hint { font-weight: bold; font-size: 80%; }'],
template: `
<md-input-container>
<md-hint>
{{ inp.value.length < 7 ?
'More characters needed' : 'Hooray!' }}
</md-hint>
</md-input-container>
`})
In this code, the <md-hint> element accesses the <input> element through the inp
local variable. Then it determines how many characters have been entered by checking the
inp.value.length.
When the <input> element receives focus, the placeholder text and the baseline
are both set to the accent color. This is because the dividerColor attribute is set to
accent. As discussed earlier in the chapter, accent refers to the color that accents the
application's primary color. If dividerColor is set to primary, the baseline would be
set to the primary color.
The Material Design library for Angular provides two components that represent button
elements: MdButton and MdAnchor. Unlike other Material Design elements, these
aren't added to templates using separate elements. Instead, they're instantiated by adding
attributes to <button> and <a> elements.
MdButtons and MdAnchors have the same graphical appearance and the same
attribute names. The major difference is that MdAnchor attributes are attached to
hyperlinks and MdButton attributes are attached to buttons. Another difference is that
MdAnchor is a subclass of MdButton, and provides additional properties and event
processing.
351 Chapter 16 Material Design
The FAB plays an important role in Material Design. It represents the primary action
to be taken in an application. Unless it's disabled, it takes the application's accent color by
default. This is shown in Figure 16.3, which depicts five types of buttons.
In the first row, the first button is defined without any atrributes set and the second
button has its disabled attribute set to true. This is why the button's outline is practically
invisible. In the second row, the second raised button has its color attribute set to warn.
This displays the button using the application's warn color, which is orange by default.
color can also be set to primary or accent.
Many of the buttons in the figure display icons. This can be configured by placing an
<img> element inside the <button>. The following markup shows what this looks like.
352 Chapter 16 Material Design
The code in Listing 16.3 creates the buttons illustrated in Figure 16.3. The
component's template contains a table with five rows, one for each button type. In keeping
with Google's convention, FAB buttons are displayed with plus signs.
@Component({
selector: 'app-root',
styles: ['td { padding: 15px; }'],
template: `
<table>
<tr>
<td>Buttons:</td>
<td><button md-button>Regular</button></td>
<td><button md-button disabled='true'>Disabled</button></td>
</tr>
<tr>
<td>Raised Buttons:</td>
<td><button md-raised-button>Regular</button></td>
<td><button md-raised-button color='warn'>Warn</button></td>
</tr>
<tr>
<td>Icon Buttons:</td>
<td><button md-icon-button>
<img src='assets/images/cross.png'></button></td>
<td><button md-icon-button>
<img src='assets/images/error.png'></button></td>
</tr>
<tr>
<td>Floating Action Button (FAB):</td>
<td><button md-fab>
<img src='assets/images/fab.png'></button></td>
</tr>
<tr>
<td>Mini-FAB:</td>
<td><button md-mini-fab>
<img src='assets/images/fab.png'></button></td>
</tr>
</table>
`})
export class AppComponent {}
353 Chapter 16 Material Design
The ch16/button_test directory contains a subfolder named images. This contains the
PNGs that define the button's images. To make these images available, this folder must be
copied to the project's src/assets folder. The content of the assets folder will be copied to
the directory containing the compiled JavaScript files. A similar copy must be performed
for all projects containing images.
The icons used in this example were taken from Google's icon repository for Material
Design. Google provides these simple icons for free, and the primary download site is
https://design.google.com/icons. They're divided into categories that include Action,
Alert, Communication, and Navigation.
In this example, the component class doesn't do anything interesting. But the process
of handling events for Material Design buttons is exactly similar to that for regular
buttons. That is, (click) events in the template can be received by event-handling
methods in the component class.
To use MdAnchor elements instead of MdButton elements, replace <button>
elements with <a> elements. The MdAnchor hyperlinks can be configured like regular
hyperlinks. These are frequently used to create router links, which were discussed in
Chapter 12.
In the early days of electronics, radios had special buttons for selecting a station. When
one button was pushed in, the other buttons popped out. This is because only one station
could be selected at a time.
The concept behind radio buttons is similar. If a user interface contains a group of
radio buttons, only one can be selected at a time. When one button is selected, the others
are deselected. This is one of the few instances where the state of a graphical element
depends on the state of another.
To support radio buttons, the Material Design library for Angular declares two
component classes:
• MdRadioButton represents one option with a radio button
• MdRadioGroup represents a group of options with radio buttons
The code in the ch16/radio_test project demonstrates how radio buttons and radio
groups work together to provide single-selection behavior. The project's base component
creates a radio group with five radio buttons, each representing a character from
commedia dell'arte. Figure 16.4 shows what the radio group looks like.
354 Chapter 16 Material Design
The code in Listing 16.3 shows how the MdRadioButton and MdRadioGroup are
used. In the template, the radio group is represented by an <md-radio-group> element
and each radio button is represented by an <md-radio-button>.
@Component({
selector: 'app-root',
styles: ['md-radio-button {
display: block; margin-top: 0px; margin-bottom: 10px; }
'],
template: `
<label>
Favorite Commedia dell'Arte Character: {{ selection }}
</label>
</md-radio-group></p>
// Characters
public chars = ['Scaramouche', 'Pulcinella',
'Pantalone', 'Columbine', 'il Dottore'];
This component uses the NgFor directive to add a radio button for each element in a
string array. The value property of each button is set equal to the corresponding string in
the array.
In the md-radio-group element, two-way binding is used to associate the selected
button with the selection variable. This is accomplished using NgModel, as shown in
the following markup:
<md-radio-group [(ngModel)]='selection'>
It's important to note that NgModel must be imported from the FormsModule
discussed in Chapter 14. That is, the application's module needs to import FormsModule
from @angular/forms and add FormsModule to the imports array of the @NgModule
decorator.
Because of the two-way binding, the radio group's selection will change whenever
the component's selection variable changes. In this example, the first option will be
selected when the Select Scaramouche button is pressed.
16.3.4 Cards
Material Design has a great deal to say about cards and their usage. Here are five
important points:
• A card's width should remain constant, but its height may increase to display
additional content. Cards only scroll vertically.
• A card may have a title and subtitle, and both should be placed at the top.
• An optional overflow menu may be positioned in the card's upper-right corner or
the lower-right corner.
• In addition to a main action, such as playing a video, a card may have supplemental
actions. These may be activated through text, icons, or controls at the bottom of the
card.
• A card's internal text should not contain hyperlinks. But the card's action area may
contain links that provide entry points to more detailed information.
The Material Design library for Angular makes it possible to add cards to a web
application by providing three components:
1. MdCard — the overall card, accessed with <md-card>
2. MdCardHeader — the card's header region, accessed with <md-card-header>
3. MdCardTitleGroup — a second type of header, accessed with
<md-card-title-group>
The <md-card> serves as the top-level element. The simplest possible card can be
defined by placing text inside its tags:
<md-card>Simple content</md-card>
This creates a white, raised, rectangular region that displays the given text in its
center. By default, this region takes all the available width, but this can be configured by
setting the card's width property.
The <md-card> element accepts a number of subelements that define content for
different portions of the card. Five subelements are given as follows:
• <md-card-title> — Sets the card's title
• <md-card-subtitle> — Sets the card's subtitle
• <md-card-content> — Defines the content of the card
• <md-card-actions> — Provides actions of the card
• <md-card-footer> — Place content on the card's bottom edge
357 Chapter 16 Material Design
<md-card>
<md-card-title>Important Card</md-card-title>
<md-card-content>
In the history of humanity, no card has ever hoped to be as
important as this one. And surely, none will ever come close.
</md-card-content>
<md-card-actions>
<a href="https://angular.io">Go Angular!</a>
</md-card-actions>
</md-card>
Figure 16.8 shows what the resulting card looks like with the width of md-card set
to 300 pixels. The title is large and black and the subtitle is small and gray.
Most of the cards on Google+ have the same structure toward the top—a circular
image to the left, a boldfaced title that identifies the poster, and a subtitle that identifies
the date/time of the post. This region is called the header, and by default, it's 40 pixels in
height. A header can be added to a page by inserting an <md-card-header> element
inside the <md-card>.
The image to the header's left is called an avatar. This can be added to a header by
inserting an <img> element with the md-card-avatar attribute set. Figure 16.6 shows
what a card looks like with a header and an avatar image.
358 Chapter 16 Material Design
This figure depicts the base component created in the ch16/card_test project. Listing
16.4 presents the component's code.
@Component({
selector: 'app-root',
styles: ['md-card { width: 300px; }'],
template: `
<md-card>
<md-card-header>
<img md-card-avatar src='assets/images/smiley.jpg'>
<md-card-title>Don Diego de la Vega</md-card-title>
<md-card-subtitle>Posted at midnight</md-card-subtitle>
</md-card-header>
<md-card-content>
Out of the night, when the full moon is bright,
comes a horseman known as Zorro.
This bold renegade carves a Z with his blade,
a Z that stands for Zorro!
</md-card-content>
<md-card-actions>
<button md-raised-button (click)='handleClick()'>
Like</button>
</md-card-actions>
</md-card>
`})
This card contains a raised button in its action area. This is accomplished by creating
a <button> and adding the md-raised-button attribute discussed earlier.
16.3.5 Lists
A list is a vertical container of elements called list items or tiles. Tiles in a list have the
same width and usually all display the same kind of data. This data could be text, images,
simple actions, or combinations thereof.
The Material Design library provides four entities (two components and two
directives) that make it possible to add lists to Angular applications:
1. MdList — the list container, accessed with <md-list>
2. MdListItem — an item in the list, accessed with <md-list-item>
3. MdListAvatar — an image in a list item, accessed with md-list-avatar
4. MdLine — an distinct line of text in a list item, accessed with md-line
<md-list> and <md-list-item> work like the <ol> and <li> elements in regular
HTML. That is, each item is surrounded by <md-list-item> and </md-list-item>,
and the entire set of list items is surrounded by <md-list> and </md-list>. This is
shown in the following markup:
<md-list>
<md-list-item>First Item</md-list-item>
<md-list-item>Second Item</md-list-item>
<md-list-item>Third Item</md-list-item>
</md-list>
Suppose that an list item's text has supporting text similar to a card's subtitle. The text
can be separated using text tags containing the md-line attribute. The following markup
shows how this attribute can be used:
<md-list>
<md-list-item>
<p md-line>First Item</p>
<p md-line>This is the first item</p>
</md-list-item>
<md-list-item>
<p md-line>Second Item</p>
<p md-line>This is the second item</p>
</md-list-item>
</md-list>
360 Chapter 16 Material Design
This markup inserts md-line in <p> elements, but the attribute can be added to any
regular text element, such as <h1>, <h2>, and so on.
It's common to precede the content of list items with small images called avatars.
These can be added to a list item with <img> elements that contain the md-list-avatar
attribute. The code in Listing 16.5 demonstrates how the four entities in the list module
(MdList, MdListItem, MdListAvatar, and MdLine) are used.
</md-list>
`})
Figure 16.7 depicts the configured list. As shown, the first line of text for each item is
printed larger than the second.
361 Chapter 16 Material Design
16.3.6 Toolbars
Just as a list displays of list items in a vertical column, a toolbar displays toolbar rows in a
vertical column. The difference is that toolbars are intended to provide actions to the user
and they're frequently employed to display the application's name.
A toolbar is represented by the MdToolbar component, which can be accessed in
markup as <md-toolbar>. Each toolbar row is represented by an <md-toolbar-row>
section. This is shown in the following markup:
<md-toolbar color="primary">
<span>Application Title</span>
<md-toolbar-row>
<span>First Row</span>
</md-toolbar-row>
<md-toolbar-row>
<span>Second Row</span>
</md-toolbar-row>
</md-toolbar>
It's common to place one or more actions on the far right of the toolbar. These might
open an overflow menu, change display properties, or delete the current document. This
can be accomplished by inserting <input> elements of type image. The code in Listing
16.6 shows how this can be implemented.
362 Chapter 16 Material Design
@Component({
selector: 'app-root',
styles: ['.fill_horizontal { flex: 1 1 auto; }'],
template: `
<md-toolbar color='primary'>
Application Title
<span class='fill_horizontal'></span>
</md-toolbar>
`})
Figure 16.8 shows what the resulting toolbar looks like. The overall color is
configured by setting the color attribute of the <md-toolbar> to primary.
The <md-toolbar> separates the menu image from the application title by inserting
a <span> element whose style is configured to occupy the available horizontal space.
363 Chapter 16 Material Design
16.3.7 Spinners
<md-progress-circle mode="indeterminate"></md-progress-circle>
The code in Listing 16.7 presents a more involved example. In this component,
pressing a button changes the degree of completion of the determinate spinner.
@Component({
selector: 'app-root',
styles: ['td { padding: 15px; }'],
template: `
<table>
<tr>
<th>Determinate</th>
<th>Indeterminate</th>
</tr>
<tr>
<!-- Determinate spinner -->
<td>
<md-progress-spinner mode='determinate'
color='accent' [value]='progress'>
</md-progress-spinner>
</td>
Listing 16.7: ch16/spinner_test/app/app.component.ts (Continued)
Chapter 6 explained how Jasmine can be used to test JavaScript functions and how Karma
makes it possible to automate testing with multiple browsers. Protractor is similar to
Jasmine/Karma in that its tests rely on Jasmine's syntax and they can be automated to
execute in different browsers.
The primary difference is that Jasmine is strictly used for unit testing. That is, a
Jasmine test executes one or more functions and checks for suitable return values. These
functions run in isolation of one another and aren't concerned with the user's actions.
In contrast, Protractor makes it possible to perform end-to-end (e2e) testing.
Protractor is deeply concerned with the user's experience and how he/she interacts with
the browser from the start of the application to its completion.
In practice, the difference between Protractor and Jasmine is that Protractor
provides a wealth of capabilities for interacting with the browser. With Protractor, a
test can simulate a user's actions and check to make sure that the application responds
appropriately.
The goal of this section is to explain how Protractor can be configured and launched
in a CLI project. Later sections will explain how to write Protractor tests for advanced
applications.
If you want to know how Protractor is configured for a CLI project, the first place to look
is the protractor.conf.js file in the top-level directory. This defines an exports.config
object whose fields specify how Protractor should execute. Table 17.1 lists 16 possible
fields of exports.config.
Table 17.1
Protractor Configuration Fields
Configuration Field Description
allScriptsTimeout Milliseconds to wait for test
specs Array of the test files to be executed
capabilities Configuration for a browser instance
multiCapabilities Configuration for multiple browser instances
directConnect Whether Protractor should access the browser directly
baseUrl The base location for resolving relative URLs
framework Test framework
jasmineNodeOpts Configuration options for Jasmine
exclude Files to exclude from testing
maxSessions Maximum number of sessions that can be executed
params Parameters that can be accessed in browser.params
beforeLaunch Function to execute before the test starts
afterLaunch Function to execute before the program exits
onPrepare Function to execute before specs are executed
onComplete Function to execute after tests are finished
onCleanUp Function to execute after the instance has been shut down
The specs field identifies the file or files that define the desired test. In a CLI project,
this field is set to *.e2e-spec.ts in the top-level e2e folder. I'll discuss Protractor spec files
shortly.
369 Chapter 17 Testing Components with Protractor
capabilities: {
browserName: "safari"
}
The capabilities object may include other configuration fields, such as:
• specs — JavaScript code to execute for the specific browser
• count — number of browser instances to launch
• maxInstances — the maximum number of browser instances that should be
launched
multiCapabilities: [{
"browserName": "chrome"
}, {
"browserName": "firefox"
}]
If the target browser is Chrome or Firefox, Protractor can connect to the browser
using its driver instead of using a Selenium server. This direct access can be configured by
setting directConnect to true.
The framework field identifies the framework that Protractor should employ to
perform tests. This can be set to jasmine, mocha, or custom. In this chapter, all of the
tests are based on Jasmine, and the jasmineNodeOpts field identifies configuration
settings for the framework.
The last five fields in the table identify functions to execute at various stages in the
test's execution. beforeLaunch is called before the browser object is available, and
onPrepare is called after browser is available, but before Protractor executes its first
test. onPrepare is called once for each browser to be launched.
370 Chapter 17 Testing Components with Protractor
By default, the framework field of the configuration file is set to jasmine and the specs
field is set to ./e2e/**/*.e2e-spec.ts. This tells Protractor to execute Jasmine tests
in the top-level e2e folder. By default, this folder contains three files:
• tsconfig.e2e.json — Settings for compiling the Protractor code
• app.po.ts — Obtains data from the main application
• app.e2e-spec.ts — Tests the data provided by app.po.ts
The app.po.ts file defines a class called TemplatePage. The app.e2e-spec.ts file
instantiates the class and calls its methods using Jasmine functions. Its code is as follows:
beforeEach(() => {
page = new TemplatePage();
});
If you followed the discussion in Chapter 6, this should look familiar. The central
functions are describe, it, and expect:
• declare — identifies the overall test suite. declare accepts two arguments: a
name for the test suite and a function that contains the test suite.
• it — called inside declare's function to define a spec. it accepts two arguments: a
name for the spec and a function that defines the spec.
• expect — called inside it's function to validate a JavaScript expression. expect
accepts a value (the actual value) and its result is chained to a method (the matcher)
that compares the actual value to the desired result.
The code in the app.po.ts file is not as straightforward. The navigateTo method
calls browser.get and the getParagraphText method calls a function called
element. These features are provided by the protractor package, and the next section
explains what they are and how they can be used.
371 Chapter 17 Testing Components with Protractor
Table 17.2
Methods of the Browser Object
Method Description
get(url: string) Navigates to the given URL
setLocation(url: string) Navigates to the in-page URL (after the '#')
getLocationAbsUrl() Returns the browser's absolute URL
refresh() Reloads the current page
restart() Restart the browser instance
close() Closes the current browser window
quit() Terminates the current session
getTitle() Returns the title of the current page
actions() Defines a sequence of user actions
touchActions() Defines a sequence of touch actions
findElement(Locator) Returns the element selected by the given locator
findElements(Locator[]) Returns the elements selected by the given locators
isElementPresent(Locator) Identifies if the element is present
executeScript Executes JavaScript in the current frame or window
(string | Function)
executeAsyncScript Executes asynchronous JavaScript in the current frame
(string | Function) or window
call(fn: function, Call a JavaScript function to execute in the given scope
scope: Object, with the given arguments
var_args: ...)
372 Chapter 17 Testing Components with Protractor
For example, the following code tells the browser to open Google's homepage:
browser.get("http://www.google.com");
When testing CLI applications, the URL is commonly set to the root, so CLI tests
generally start with the following:
browser.get("/");
expect(browser.getLocationAbsUrl())
.toEqual("http://www.google.com");
Many of these methods return Promises. For example, if you want to print the
title of the current page, console.log(browser.getTitle()) won't do the job.
getTitle returns a Promise, so then must be called to access the Promise's result. The
following code shows how this works:
browser.getTitle().then(function(title) {
console.log(title);
});
The methods actions and touchActions make it possible to define how the test
interacts with elements in the web page. A later discussion will explain how to execute
sequences of actions in the browser.
To access an element in the page, findElement returns a WebElement representing
a DOM element. findElements returns an array of WebElements. In both cases,
elements are selected by a Locator or array of Locators, and the following discussion
explains how Locators work.
373 Chapter 17 Testing Components with Protractor
In a Protractor test, it's common to select elements in a document and then read or change
their state. Accessing elements requires two steps:
1. Call a method of the by object to obtain a Locator.
2. Use the Locator in a method that provides an element or elements. These methods
include browser.findElement and browser.findElements.
The by object makes it possible to select elements based on a wide range of criteria.
Table 17.3 lists 13 of its methods.
Table 17.3
Selection Methods of the by Object
Selection Method Description
tagName(tag: string) Selects elements by their tag name
id(id: string) Selects an element by its ID
css(sel: string) Selects elements by their CSS selector
className(class: string) Selects elements by their CSS class
name(name: string) Selects elements by their name attribute
linkText(text: string) Selects link elements by their visible text
partialLinkText Selects link elements by a substring of
(text: string) their visible text
js(string | Selects elements by evaluating a JavaScript
Function, args) expression
xpath(sel: string) Selects elements with an XPath selector
buttonText(text: string) Selects a button by its visible text
partialButtonText Selects buttons by a substring of their visible text
(text: string)
cssContainingText(class: Finds elements by CSS class whose visible text
string, name: string) contains the given string
addLocator(name: string, Defines a custom Locator for selecting elements
script: Function | string)
It's important to see how the by object and the browser methods work together. For
example, to find an element whose ID is submitButton, you'd call by.id to obtain a
Locator and then call browser.findElement to obtain the WebElement.
374 Chapter 17 Testing Components with Protractor
browser.findElement(by.id("submitButton"));
Similarly, the following code obtains every element in the CSS class named caption:
browser.findElements(by.className("caption"));
This code returns an array containing every anchor element whose displayed text
contains the string click:
browser.findElements(by.partialLinkText("click"));
17.2.3 ElementFinders
element(by.tagName("button")).getText().then( function(text) {
console.log("Element text:" + text);
});
element.all(by.css(".navbar"));
If elements are selected according to a CSS selector, the code can be simplified using a
notation similar to jQuery's:
• element(by.css("xyz")) can be replaced with $("xyz"). For example,
element(by.css("#box")) can be replaced with $("#box").
• To select multiple elements, element.all(by.css("xyz")) can be replaced with
$$("xyz"). For example, element.all(by.css(".navbar")) can be replaced
with $$(".navbar").
375 Chapter 17 Testing Components with Protractor
After an ElementFinder has been obtained, its methods make it possible to read or
change the state of the corresponding DOM element. Table 17.4 presents fourteen of these
methods.
Table 17.4
ElementFinder Methods
Method Description
click() Sends a click event to the selected element(s)
sendKeys(var_args ...) Sends a sequence of keystrokes to the selected
element(s)
submit() Submits the form corresponding to the given element
clear() Clears the value of the element (input or textarea)
getText() The element's displayed text
getInnerHtml() The element's HTML content, not including the tags
getOuterHtml() The element's HTML content, including the tags
getSize() Returns the element's dimensions in pixels
getLocation() Returns the element's location in pixels
getTagName() Returns the element's tag name
getCssValue(prop: string) Returns the element's CSS value for the given property
getAttribute Returns the value corresponding to the given attribute
(attr: string)
isEnabled() Identifies if the element's state is enabled
isSelected() Identifies if the element has been enabled
isDisplayed() Identifies if the element is displayed in the page
The first four methods perform an action on the corresponding DOM element. The
easiest to understand is click. The following code delivers a click event to the element
whose ID is sendData:
$("#sendData").click();
Table 17.5
Special Key Codes
protractor.Key.ENTER protractor.Key.ESCAPE
protractor.Key.SPACE protractor.Key.TAB
protractor.Key.SHIFT protractor.Key.UP
protractor.Key.ALT protractor.Key.DOWN
protractor.Key.META protractor.Key.LEFT
protractor.Key.CONTROL protractor.Key.RIGHT
protractor.Key.COMMAND protractor.Key.NULL
The following code uses sendKeys to transfer the characters N, a, m, and e followed
by the Enter key. This text is delivered to the element whose ID is storeName:
$(#storeName).sendKeys("Name", protractor.Key.ENTER);
If this element is an <input> element, its text can be checked by reading its value
attribute. The following code shows how this can be done:
$("#storeName").getAttribute("value").then( function(value) {
console.log("Input value:" + text);
});
If a modifier key (Shift, Control, Alt, Meta) is sent, it will remain active until
protractor.Key.NULL is sent. To see how this works, consider the following code:
17.2.4 ElementArrayFinders
$$("a").then(function(elementArray) {
expect(elementArray[0].getText()).toBe("Click Me");
});
Table 17.6
ElementArrayFinder Methods
Method Description
filter(func: function) Returns an ElementArrayFinder whose elements meet the
criteria given by the filter function
get(index: number) Returns the ElementFinder at the given index
first() Returns the first ElementFinder in the array
last() Returns the last ElementFinder in the array
count() Returns the number of ElementFinders in the array
locator() Returns the most recently-used Locator
each(func: function) Applies a function to each element in the array
map(func: function) Applies a map function to each element in the array
reduce(func: function, Applies a reduce function to all the elements in the array
init: any)
evaluate(in: string) Evaluates the input string as if it was an Angular expression
allowAnimations() Identifies if animation is allowed on the array's elements
378 Chapter 17 Testing Components with Protractor
$$("button").filter(function(element, index) {
return element.getText().then(function(text) {
return text.indexOf("Click") > -1;
});
}).each(function(element, index) {
element.click();
});
map is almost identical to each, and both methods accept a function that receives
an ElementFinder and the ElementFinder's index. The difference between them is
that each returns void and map returns a promise that resolves to an array of values. Each
iteration of map provides an element in this returned array.
For example, the following code creates an ElementArrayFinder containing all
the buttons in the page and uses map to place each button's displayed text in an array. The
join method combines these strings into one, which is printed to the console.
$$("button").map(function(element, index) {
return element.getText();
}).then(function(array) {
console.log(array.join(""));
});
$$("button").reduce(function(acc, element) {
return element.getText().then(function(text) {
return acc + text;
});
}, "").then(function(res) {
console.log(res);
});
17.2.5 Actions
Table 17.7
Protractor Action Methods
Method Description
click(b: button) Performs a mouse click at the current location
click(el: ElementFinder, Performs a mouse click on the given element
b: button)
doubleClick(b: button) Performs a double-click at the current location
doubleClick Performs a double-click on the given element
(el: ElementFinder,
b: button)
dragAndDrop Performs a drag-and-drop from one element to
(src: ElementFinder, another
dst: ElementFinder)
dragAndDropBy Performs a drag-and-drop from an element to a
(src: ElementFinder, location given by (x, y)
x: number, y: number)
keyDown(k: key) Presses the given modifier key
keyUp(k: key) Releases the given modifier key
mouseDown(b: button) Performs a mouse click without release
380 Chapter 17 Testing Components with Protractor
When using these methods, there are two important points to understand:
1. The methods can be chained together to define a sequence of actions.
2. The chain starts with browser.actions() and ends with perform(), which
executes the actions.
For the actions involving mouse buttons, such as click and doubleClick, the
button value can take one of three values: protractor.Button.LEFT,
protractor.Button.RIGHT, and protractor.Button.MIDDLE. As a simple
example, the following sequence right-clicks on the button whose ID is browse, and then
double-clicks on the same button:
browser.actions().click($("#browse"), protractor.Button.RIGHT)
.doubleclick().perform();
For every action sequence, the initial position of the mouse pointer is (0, 0) and its
position is updated after mouse-related events. This is why the doubleclick() action in
the example code doesn't need to identify the target—the pointer is still over the button.
Protractor doesn't provide any methods that interact with <select> elements and
their options. I spent hours trying to use mouseMove and click to select an option
programmatically, but there's an easier way. If an option is named GreenOption, the
following code will select it:
element(by.cssContainingText("option", "GreenOption")).click();
381 Chapter 17 Testing Components with Protractor
The keyDown and keyUp methods only accept modifier keys, such as protractor.
Key.SHIFT. For other keys, the sendKeys method in the table accepts the same
arguments as the ElementFinder's sendKeys method discussed earlier. There's no way
to specifically identify the target—the keystrokes will be sent to the element that received
focus last.
For example, the following action sequence sends Ctrl-C to the current element:
browser.actions().sendKeys(protractor.Key.CONTROL, "c",
protractor.Key.NULL).perform();
Table 17.8
CLI Test Execution Flags (ng e2e)
Flag Description
-t/-dev/-prod Identifies the build target
-e Sets the build environment
-op Specifies the output path
-aot Builds using Ahead of Time compilation
-sm Output sourcemaps
-vc Split vendor libraries into a separate bundle
-bh Base URL for the application
-d URL where files will be deployed
-v Sets verbose messaging
-pr Logs progress to the console
--i18n-file Sets the localization file
--i18n-format Sets the format of the localization file
--locale The locale to use for localization
382 Chapter 17 Testing Components with Protractor
By default, all tests are based on the development build target. The following
command executes the end-to-end test using the production target:
ng e2e -prod
The -aot flag enables Ahead of Time compilation, which was discussed in Chapter 8.
If the test is run for the production build target, this will be enabled by default.
// Initialize test
beforeEach(() => {
browser.get('/');
});
browser.actions().click($('#demo_button')).click(
$('#demo_button')).perform();
expect($('#demo_button').getText()).toEqual('2');
});
$('#demo_input').sendKeys(protractor.Key.SHIFT,
'hello', protractor.Key.NULL, 'hello');
expect($('#demo_input').getAttribute('value')).
toEqual('HELLOhello');
});
});
Spec started
expect($("#demo_button").getText()).toEqual("2");
$("#demo_input").sendKeys(protractor.Key.SHIFT,
"hello", protractor.Key.NULL, "hello");
The sendKeys method delivers the hello string twice, once before the Shift key
is pressed and once afterward. As a result, the text in the <input> element is given as
HELLOhello.
17.5 Summary
The Jasmine toolset is fine for checking functions and variables, but it's unsuitable for
testing the usage of real-world applications. In contrast, end-to-end testing directs user
actions to an application and checks the application's state to ensure proper behavior.
Protractor excels at this type of testing, and the goal of this chapter has been to explain
how it can be used.
At first glance, a Protractor test suite looks like a Jasmine test suite, using define,
it, and expect to check actual values against expected values. But Protractor's advantage
is that it provides a wealth of objects and functions for interacting with a browser. For
example, the browser object makes it possible to read and test the browser's state as it
runs an application.
At its simplest, an end-to-end test involves directing actions to a browser and
verifying changes to the application's state. In Protractor, a sequence of actions can be
delivered to an element by calling the actions method of the browser object. This can
be chained with methods representing simulated user actions, such as click(). The
perform method executes the actions.
To access elements in the document, Protractor provides Locators, which select
elements according to criteria including attributes and CSS styles. Locators make it
possible to obtain ElementFinders and ElementArrayFinders, which can send
actions to browser elements and read their state information.
Chapter 18
Displaying REST Data
with Dynamic Tables
From GMail to Amazon, today's most popular web sites all perform the same basic
operations. They read records from a database and display them in a dynamic table. These
tables provide a common set of features, which include the following:
• Clicking a record's title provides more information
• Records can be sorted in ascending or descending order
• Records can be filtered using search criteria
• Records can be selected for individual operations, such as deletion
• Users can select how many rows should be displayed at once
The goal of this chapter is to show how similar tables can be constructed using
Angular. The project discussed in this chapter ties together many of Angular's advanced
features, including routing, HTTP access, and the Reactive Forms API.
The table's data is provided in raw form instead of HTML, so the client-server
application is more accurately called a web service. A popular architecture for web service
communication is Representational State Transfer, or REST. REST provides guidelines
for HTTP data transfer, especially with regard to the names and hierarchy of uniform
resource identifiers, or URIs. This chapter explains what these guidelines are and show
how they can be used in an Angular application.
The majority of this chapter is concerned with code. The discussion topics include
implementing REST's guidelines using Angular's HTTP service, validating user data with
the Reactive Forms API, and selecting child components by URL using Angular's routing
capability.
388 Chapter 18 Displaying REST Data with Dynamic Tables
Each record of the table identifies the title, artist, and year of a popular song. Users
can sort the records by year, mark rows for deletion, search for text, and specify how many
rows should be displayed.
Clicking on a title brings up a second page with a simple form. As shown in Figure
18.2, this form has three text boxes, one for each field of the record:
18.2.1 Philosophy
There's no specification that defines how a REST-based (or RESTful) web service should
behave. Instead, REST defines a set of design rules, or contraints, that RESTful web
services are expected to follow. Roy Fielding introduced these constraints in his 2000
dissertation, Architectural Styles and the Design of Network-based Software Architectures:
1. Client-server — In REST's separation-of-concerns methodology, the client is
concerned with one set of issues, such as the user interface, and the server is
concerned with another set of issues, such as data storage. These elements are called
components.
2. Stateless — A REST request contains all the information needed to understand it.
The server doesn't keep track of sessions but the client may.
3. Cacheable — Data in a REST response can be marked as non-cacheable or
cacheable. This can improve performance, but it may also reduce reliability due to
stale entries in the cache.
4. Layered system — The architecture consists of independent, hierarchical layers.
5. Uniform interface — Components have a consistent contract that enables
communication with many different types of components.
6. (Optional) Code on demand — REST supports extensibility by allowing applets or
scripts to provide additional functionality.
390 Chapter 18 Displaying REST Data with Dynamic Tables
The REST methodology presents clear guidelines for accessing resources. REST
recognizes two types of uniform resource identifiers (URIs):
• Collection URIs — Identify a collection of elements, such as
http://example.edu/professors
• Element URIs — Identify a single element in a collection, such as
http://example.edu/professors/smith
A URI's type determines how it should be accessed using HTTP requests. For a
Collection URI, a GET request lists the elements in the collection, a PUT request replaces
the collection with another, a POST request adds an element to the collection, and a
DELETE request deletes the collection.
For an Element URI, the different methods have different purposes. A GET request
retrieves a representation of the element, a PUT request replaces the element, and a
DELETE request removes the element from the collection. POST requests generally aren't
used with Element URIs.
In RESTful applications, URIs identify things, not actions. Therefore, a URI like
http://example.edu/get_office_hours isn't acceptable because 'get_office_hours' implies
an action. A URI like http://example.edu/professors/update?name=smith is also frowned
upon because 'update' identifies an action.
The REST architecture doesn't define a format for requests and responses, but
metadata should be kept in the header, not the body. The only data in a response's body
should be the resource requested by the client. The format of the response data is usually
XML or JSON, and the request can specify the format by adding a suffix to the request,
such as http://example.edu/professors/smith.json. It's also acceptable to specify the desired
resource format in the request's header. The process of determining which format to
return is called content negotiation.
To keep applications stateless, resources should provide links that identify what
operations are available for interacting with the application. The following markup
provides an example:
<student>
<student_number>123</student_number>
<link rel="pass" href="http://ex.edu/students/123/pass" />
<link rel="fail" href="http://ex.edu/students/123/fail" />
</student>
In REST literature, this concept is denoted HATEOAS, which stands for Hypermedia
as the Engine of Application State.
391 Chapter 18 Displaying REST Data with Dynamic Tables
http://www.ngbook.io/songs?page=1&songsperpage=10
this.http.get("http://www.ngbook.io/songs", opts)
.map((res: Response) => res.json())
.subscribe((songs: Array<Song>) => this.songs = songs);
The get method provides access to the server's response in the form of an
Observable. As explained in Chapter 11, the map method transforms each of the
Observable's elements. In this code, json transforms the body of the response data into
JSON format. Afterward, the subscribe method receives the JSON data as an array of
Song objects.
The application presented in this chapter doesn't support sending PUT, POST, or
DELETE requests to the collection URI, but it does support sending requests to element
URIs. Each song has an ID between 00 and 39, and the following URI accesses Song 27:
http://www.ngbook.io/songs/song27
392 Chapter 18 Displaying REST Data with Dynamic Tables
The application can send GET, PUT, and DELETE requests to element URIs. The user
interface makes this possible with the following actions:
• Checking a record in the song table and pressing Delete sends a DELETE request to
the element URI corresponding to the song.
• Clicking on a title in the song table opens the edit form for editing. As the form
opens, it sends a GET request to obtain data for the selected song.
• When the edit form is submitted with valid data, it sends a PUT request to update
the song's data.
To show how REST works in practice, I've written back end code that runs on a server
and provides data through www.ngbook.io. The server receives and responds to HTTP
requests, but in the interest of security, it doesn't allow any operation to modify the song
data. Instead, for DELETE and PUT requests, the server returns a message stating that the
HTTP request was received.
The SongView application doesn't provide information through HATEOAS. This is
because the API is sufficiently simple that no additional metadata is needed.
The module class, AppModule, is defined in the app/app.module.ts file. To meet the
application's requirements, the module imports capabilities for routing, HTTP data
transfer, and forms. Listing 18.1 presents its code.
Chapter 12 explained how modules can define a Routes array whose elements
associate URLs with components. In this case, the base URL redirects to the /songtable
URL, which inserts the SongTableComponent into the base component. When the
client accesses the /editpage URL, the router inserts an EditPageComponent into the
base component.
393 Chapter 18 Displaying REST Data with Dynamic Tables
The SongTableComponent provides the main visual component of the project. This
reads song records from the server and displays them as rows of a table. By default, the
records are unsorted and the table displays ten records at once.
Listing 18.2 presents the component's template and Listing 18.3 presents the
component's class. There's a great deal of code, and rather than discuss all of it in detail,
this section explores five aspects of the table's operation:
1. Linking to the edit page
2. Pagination
3. Row selection and deletion
4. Row sorting
5. Text search
394 Chapter 18 Displaying REST Data with Dynamic Tables
<div id='song_view'>
<p id='title'>Popular Songs</p>
<div class='top'>
<span><button (click)='handleDelete()'>Delete</button></span>
<span>Songs Per Page:
<select (change)='handleSelect($event.target.value)'>
<option>5</option>
<option selected>10</option>
<option>20</option>
</select></span>
<input class='search_box' type='search'
(search)='handleSearch($event.target.value)'
onfocus="if (this.value=='search') this.value = ''"
value='search' size='25'>
</div>
<table class='song_table'>
<tr>
<th class='row-checkBox'>
<input type='checkbox' [checked]='allChecked'
(change)='handleChecks($event.target.checked)'></th>
<th class='row-title'>Title</th>
<th class='row-artist'>Artist</th>
<th class='row-year' (click)='handleSort()'>Year
<img class='sort_img' [src]='sortImage'></th>
</tr>
<tr *ngFor='let song of songs; let i = index'>
<td class='center'><input type='checkbox'
[(ngModel)]='checks[i]'></td>
<td class='indent'><a [routerLink]="['../editpage']"
[queryParams]="{songid: song.id}">
{{song.title}}</a></td>
<td class='indent'>{{song.artist}}</td>
<td class='center'>{{song.year}}</td>
</tr>
</table>
<div class='bottom'>
<span><img src='assets/images/left_arrow.png'
[className]="(page == 0) ? 'hide' : ''"
(click)='handlePrev()'></span>
<span class='bottom_center'>
Page {{ page + 1 }} of {{ numPages }}</span>
<span><img src='assets/images/right_arrow.png'
[className]="(page == numPages-1) ? 'hide' : ''"
(click)='handleNext()'></span>
</div>
</div>
395 Chapter 18 Displaying REST Data with Dynamic Tables
@Component({
selector: 'app-songtable',
templateUrl: 'songtable.component.html',
styleUrls: ['songtable.component.css']})
export class SongTableComponent {
private songs = [];
private checks = [];
private allChecked = false;
private page = 0;
private songsPerPage = 10;
private numPages = 4;
private sortDirection = SortDirection.None;
private sortImage = 'assets/images/no_sort.png';
private url = 'http://www.ngbook.io/songs';
// Update URL
if (text) {
query = 'search='.concat(text);
} else if (text === '') {
query = 'page='.concat(this.page.toString()).
concat('&songsPerPage=').
concat(this.songsPerPage.toString());
}
opts = {search: query};
this.http.get(this.url, opts)
.map((res: Response) => res.json())
.subscribe((songs: Array<Song>) => this.songs = songs);
}
// No direction
case SortDirection.None:
this.sortDirection = SortDirection.Desc;
this.sortImage = 'assets/images/desc_sort.png';
break;
// Descending direction
case SortDirection.Desc:
this.sortDirection = SortDirection.Asc;
this.sortImage = 'assets/images/asc_sort.png';
break;
// Ascending direction
case SortDirection.Asc:
this.sortDirection = SortDirection.None;
this.sortImage = 'assets/images/no_sort.png';
break;
}
this.sendRequest();
}
As depicted in Figure 18.1, each song's title serves as a hyperlink. If a title is clicked,
the application opens a page for editing the song's data. In the SongTableComponent
template, song titles are defined with the following markup:
The second route configuration associates the songtable path with the
SongTableComponent. This component is displayed when the application starts. The
third route configuration associates the editpage path with the EditPageComponent.
The router link for each song title points to editpage, so the EditPageComponent will
be displayed when the link is clicked.
The router link sets a query when clicked. This contains the ID of the selected song,
which will be appended to the URL path when the link is activated. For example, if the
user clicks on a link for Song 17, the query will be set to ?songid=17.
It's important to understand the .. in the routerLink value (../editpage).
This tells the router to access the route definitions of the root module instead of
the SongTableComponent's route definitions. This makes sense because the
SongTableComponent doesn't have any route definitions of its own.
399 Chapter 18 Displaying REST Data with Dynamic Tables
Pagination
For nontrivial web applications, the database will hold more records than a table can
display at once. For this reason, the collection of records must be split into groups that
can be displayed in the table. These groups are called pages, and the process of managing
pages is called pagination.
The SongTableComponent class has three properties related to pagination:
• page identifies the page currently displayed in the table
• songsPerPage identifies the number of records in each page
• numPages identifies the number of pages
When the SongTableComponent needs to retrieve data, it tells the server which
records it wants by providing the values of page and songsPerPage as parameters of the
GET request. The following code shows how this is accomplished:
The user can change which page is displayed by clicking one of the arrows below
the table. If the current page is the first page, the left (previous) arrow will be hidden. If
the current page is the last page, the right (next) arrow will be hidden. This behavior is
configured with the following markup:
<img src='assets/images/left_arrow.png'
[className]='(page == 0) ? 'hide' : '''
(click)='handlePrev()'>
...
<img src='assets/images/right_arrow.png'
[className]='(page == numPages - 1) ? 'hide' : '''
(click)='handleNext()'>
The handlePrev method is called when the left arrow is clicked and handleNext
is called when the right arrow is clicked. These methods update page and send a GET
request to the server with the updated value.
400 Chapter 18 Displaying REST Data with Dynamic Tables
To hide the arrow image, the className property changes according to the current
page number. For example, if the first page is displayed, page will equal 0 and the left
arrow's className will be set to hide. It may seem odd to update className instead of
the hidden property, but it was necessary for esthetic reasons. That is, I wanted the arrow
to occupy the same amount of space when not displayed.
Above the table, a <select> element allows the user to change the number of songs
per page (the options are 5, 10, and 20). The element's markup is given below:
<select (change)="handleSelect($event.target.value)">
<option>5</option>
<option selected>10</option>
<option>20</option>
</select>
When the user makes a selection, the (change) event fires and the handleSelect
method is called with $event.target.value, which identifies how many records
should be displayed at once. The handleSelect method updates songsPerPage and
numPages, and sends a new GET request to the server.
Along the left side of the table, check boxes allow the user to select one or more rows of
the table. The SongTable class needs to be informed when the user checks/unchecks
a box, and on occasion, the class needs to check/uncheck boxes without the user. This
requires two-way data binding, and the following template markup shows how this can be
accomplished using NgModel:
When the user checks or unchecks the upper-left box, the handleChecks method
checks or unchecks each box in the table. Also, the element's checked property is
associated with the class's allChecked property. If the upper-left box needs to be
unchecked (such as when the user turns to a different page), the class can uncheck this
box by setting allChecked to false.
In this table, the only operation that can be performed on selected rows is deletion.
This is made possible by the Delete button, whose markup is given as follows:
<button (click)="handleDelete()">Delete</button>
The handleDelete method iterates through the values of checks and sends a
DELETE request to the server for each selected row. This is accomplished by calling the
delete method of the Http object, and the following code shows how this works:
For example, suppose the user checks the box for Songs 37 and 39, and then presses
the Delete button. The handleDelete method will call http.delete twice. The first
time, the URL will be set to http://www.ngbook.io/songs/song/37. The second time, the
URL will be set to http://www.ngbook.io/songs/song/39.
As mentioned earlier, security reasons prevent the server from actually modifing the
data. However, the server acknowledges receipt of the DELETE request by returning a
message in the body of its response.
The handleDelete method accesses the server's response by calling the subscribe
method of the Observable returned by http.delete. Then it obtains the message text
by calling the text method of the Response object.
402 Chapter 18 Displaying REST Data with Dynamic Tables
Row Sorting
The SongTableComponent makes it possible to sort records according to the year the
song was released. The user sets the direction of the sort (ascending or descending) by
clicking on the header cell entitled Year. The following markup shows how this header cell
is defined in the template:
When the user clicks on the header cell, the handleSort method changes the sort
direction (sortDirection) and the image displayed in the header cell (sortImage).
This image can be an arrow pointing up (ascending sort), an arrow pointing down
(descending sort), or two arrows (no sort). The following code shows how handleSort
accomplishes this:
switch (this.sortDirection) {
case SortDirection.None:
this.sortDirection = SortDirection.Desc;
this.sortImage = 'assets/images/desc_sort.png';
break;
case SortDirection.Desc:
this.sortDirection = SortDirection.Asc;
this.sortImage = 'assets/images/asc_sort.png';
break;
case SortDirection.Asc:
this.sortDirection = SortDirection.None;
this.sortImage = 'assets/images/no_sort.png';
break;
}
this.sendRequest();
}
The sendRequest method creates a GET request and sends it to the server, which
performs the actual sorting. If the sort direction is ascending, the string &sort=asc will
be appended to the request's query string. If the sort direction is descending, the string
&sort=desc will be appended.
403 Chapter 18 Displaying REST Data with Dynamic Tables
Text Search
The search box allows the user to filter the table's content for records containing specific
text. In the SongTable's template, this <input> element is defined with the following
markup:
The search event fires when the user enters text in the search box and presses Enter.
This calls the handleSearch method, whose behavior changes depending on whether
the user's text is an empty string or not. This is shown in the following code:
If the search text is empty, the method generates a basic GET request for the current
page. If the search text isn't empty, it appends a query string to the GET request that sets
search equal to the user's text. The Http.get method sends the request and returns
an Observable for the response. As the response is received, the Observable's map
method converts its body to JSON and the subscribe method interprets the JSON as an
array of Song objects.
If this was a professional application, handleSearch would check the user's search
data more thoroughly. It would also obtain the number of search results from the server
and update the table accordingly.
404 Chapter 18 Displaying REST Data with Dynamic Tables
When the user clicks on a song's title, the application displays a form that allows the user
to update the song's fields. Figure 18.2 showed what this form looks like.
This form is created by the EditPageComponent, which uses the Reactive Forms
API discussed in Chapter 14. Listing 18.4 presents the code for the component's template
and Listing 18.5 presents the code for the component's class.
@Component({
selector: 'app-editpage',
templateUrl: 'editpage.component.html',
styleUrls: ['editpage.component.css']})
export class EditPageComponent implements OnInit {
This assigns a name to each FormControl. The template uses these names to
associate the FormControls with <input> elements. This is shown in the following
markup:
Before the form can be submitted, each <input> must be non-empty. This is why the
second argument of each FormControl constructor contains Validators.required.
The third FormControl, which receives the year of a song's release, requires extra
validation, which is performed a special function called yearValidator. This function
receives a FormControl and returns a non-null value if its value isn't a four-digit
number. The following code shows how this validation is performed.
When the form is submitted, the component sends a PUT request to the song's URL
with the new text. The server won't update the song's data, but it will return a message
stating that it received the request.
407 Chapter 18 Displaying REST Data with Dynamic Tables
18.5 Summary
This chapter's SongView application isn't going to win any programming awards, but it
ties together a number of capabilities discussed in earlier chapters. It relies a great deal
on the HTTP service and the Forms API. It also uses routing to switch between the song
table and the edit view.
The SongView application reads raw data from a server and displays the data in a
web page. The project's code follows the REST guidelines for client-server data transfers
in a web service. On the back end, this means assigning specific URIs to provide different
types of data. A Collection URI, such as http://www.ngbook.io/songs, provides
information about multiple songs. In contrast, an Element URI, such as http://www.
ngbook.io/songs/song32, provides information about a specific song.
The hard part of coding applications like SongView isn't transfering data between the
client and server, but handling operations related to the table. These operations include
switching pages, sorting records, deleting records, and changing the number of records to
be displayed. Most of these operations can be accomplished with property/event binding,
but sometimes two-way binding is the only option available.
Chapter 19
Custom Graphics with
SVG and the HTML5 Canvas
Up to this point, all of the graphical elements in a component's template have been based
on standard HTML elements or the Material Design elements discussed in Chapter 16.
But a component's appearance can also be set using SVG (Scalable Vector Graphics)
or the HTML5 canvas. Both toolsets are ideal for developers who want to make their
applications memorable, and this chapter discusses both in detail.
SVG stands for Scalable Vector Graphics, a toolset for defining shapes and text with
XML. All modern browsers support SVG, so <svg></svg> elements can be inserted
directly inside a component's template. Graphics can be defined by adding subelements
to these tags. For example, circles can be drawn by adding <circle> elements and
rectangles can be drawn by adding <rect> elements.
Most, but not all browsers support <canvas> elements, which are defined in the
HTML5 specification. Unlike SVG, which adds elements to the template's DOM, canvas
elements make it possible to define graphics programmatically. This is accomplished by
calling methods of the CanvasRenderingContext2D associated with the template's
<canvas> element. Most of these methods are straightforward to understand and use,
such as drawImage and strokeText. But the process of drawing paths in a canvas is
more difficult.
SVG and the HTML5 canvas both make it possible to reposition graphics using linear
transformations. The three types of linear transformations are translations, rotations, and
scalings. A translation shifts a graphic's location by adding values to the graphic's (x, y)
coordinates. A rotation turns a graphic about an axis through a given angle and scaling
increases or decreases a graphic's size. The last part of this chapter demonstrates how a
graphic can be animated by changing its transformation properties in different animation
frames.
410 Chapter 19 Custom Graphics with SVG and the HTML5 Canvas
@Component({
selector: "basic-circle",
template: `
<svg height="200" width="200">
<circle cx="75" cy="75" r="75" />
</svg>
`})
Table 19.1 lists the six basic shapes that can be drawn with SVG. The second column
presents attributes of the corresponding elements.
Table 19.1
SVG Basic Shapes
Shape Element Geometric Attributes Description
line x1, y1, x2, y2 Draws a line from (x1, y1) to (x2, y2)
polyline points Draws lines that connect the given points
polygon points Draws a polygon bounded by the given points
rect x, y, width, Draws a rectangle
height, rx, ry
circle cx, cy, r Draws a circle with the given center and radius
ellipse cx, cy, rx, ry Draws an ellipse with the given center,
horizontal radius, and vertical radius
411 Chapter 19 Custom Graphics with SVG and the HTML5 Canvas
There are three points that need to be mentioned about SVG shape elements:
1. Shape elements, like all SVG elements, are subelements of a top-level <svg>
element. The combination of <svg> and its subelements is called an SVG fragment.
2. By default, the solid shapes (polygons, rectangles, circles, and ellipses) are filled and
the default fill color is black.
3. In addition to the geometric attributes listed in the table, every shape element can
have a class attribute that defines its CSS class and a style attribute that defines
a particular style rule. It can also have a transform attribute that identfies a linear
transformation.
As shown in the preceding example, the root element of an SVG fragment is <svg>. This
accepts attributes that set properties for elements defined between <svg> and </svg>.
Table 19.2 lists these attributes and provides a description of each.
Table 19.2
Attributes of the <svg> Element
Attributes Description
version SVG version to which the fragment conforms
width, height Overall dimensions of the fragment
x, y Origin of the fragment
viewBox Rectangle to which the fragment's content should be stretched
preserveAspectRatio Identifies if the fragment can be stretched
contentScriptType The script language for the fragment
contentStyleType The style sheet language for the fragment
The width and height attributes set the dimensions of the overall fragment. If
these dimensions are given without units, they're assumed to be in pixels. But width and
height can also be given in em, ex, px, pt, pc, cm, mm, and in.
The x and y attributes set the fragment's upper-left corner if the fragment is
embedded inside another fragment. If a fragment isn't contained in another fragment, x
and y will have no effect.
412 Chapter 19 Custom Graphics with SVG and the HTML5 Canvas
The viewBox attribute creates a custom coordinate system inside the fragment. This
accepts four values: new coordinates for the fragment's upper-left corner and new
coordinates for the fragment's lower-right corner. These new coordinates will be used to
place content inside the fragment.
An example will clarify how viewBox is used. The following markup draws a simple
circle in a 200-by-200 pixel fragment:
The circle has a radius of 75 and is centered at (75, 75). The resulting graphic was
depicted in Figure 19.1.
Now suppose that the viewBox attribute sets the upper-left corner of the fragment to
(0, 0) and sets the lower-right corner to (300, 300). The following markup shows how this
can be configured:
The fragment still occupies 200-by-200 pixels, but its internal coordinates only run
from (0, 0) to (100, 100). Figure 19.2 shows what the resulting circle looks like:
The term aspect ratio refers to the ratio of width to its height. If the aspect ratio of a view
box is different than that of its container, the content will be scaled so that its aspect ratio
matches that of the container. This behavior can be configured by setting values of the
preserveAspectRatio attribute.
preserveAspectRatio accepts two values and the first identifies how the
viewbox's dimensions should be stretched. This accepts one of ten constants:
• none — scale the graphic so that the view box matches the viewport rectangle
• xMinYMin — align the min x-coordinate of the view box with the min x-coordinate
of the viewport, align the min y-coordinate of the view box with the min
y-coordinate of the viewport
• xMidYMin — align the mid x-coordinate of the view box with the mid x-coordinate
of the viewport, align the min y-coordinate of the view box with the min
y-coordinate of the viewport
• xMaxYMin — align the max x-coordinate of the view box with the max x-coordinate
of the viewport, align the min y-coordinate of the view box with the min
y-coordinate of the viewport
• xMinYMid — align the min x-coordinate of the view box with the min x-coordinate
of the viewport, align the mid y-coordinate of the view box with the mid
y-coordinate of the viewport
• xMidYMid — align the mid x-coordinate of the view box with the mid x-coordinate
of the viewport, align the mid y-coordinate of the view box with the mid
y-coordinate of the viewport
• xMaxYMid — align the max x-coordinate of the view box with the max x-coordinate
of the viewport, align the mid y-coordinate of the view box with the mid
y-coordinate of the viewport
• xMinYMax — align the min x-coordinate of the view box with the min x-coordinate
of the viewport, align the max y-coordinate of the view box with the max
y-coordinate of the viewport
• xMidYMax — align the mid x-coordinate of the view box with the mid x-coordinate
of the viewport, align the max y-coordinate of the view box with the max
y-coordinate of the viewport
• xMaxYMax — align the max x-coordinate of the view box with the max x-coordinate
of the viewport, align the max y-coordinate of the view box with the max
y-coordinate of the viewport
414 Chapter 19 Custom Graphics with SVG and the HTML5 Canvas
If the first value of preserveAspectRatio isn't set to none, the second value
identifies if and how the view box should be scaled. This attribute, meetOrSlice, can be
set to one of two values:
• meet — the view box is scaled to the maximum size needed to fit inside the
container while still preserving its aspect ratio
• slice — the view box is scaled to the minimum size needed to cover the container's
area while still preserving its aspect ratio
19.1.2 Lines
The <svg> element can contain one or more subelements that define shapes inside the
container. The simplest of these is the <line> subelement, which draws a line inside the
fragment. This accepts four attributes that identify the line's starting and ending points:
x1, y1, x2, y2.
For example, the following markup draws a line from (50, 50) to (100, 100):
Unlike circles and other solid shapes, lines are not drawn in black by default. This
means the line's color must be specifically set. The above example sets the line's color
using the style attribute, but the class attribute can also be used to assign the <line>
to a CSS class. Then the class's properties can be configured in a CSS file.
19.1.3 Polylines
The points attribute of the <polyline> element accepts a series of (x, y) pairs and
draws a line from each point to the next. If there are N points, N–1 lines will be drawn.
In (x, y) pair, the x and y values must be separated by a comma. For example, the
following markup draws a polyline containing three lines: one from (0, 0) to (0, 50), one
from (0, 50) to (50, 100), and one from (50, 100) to (100, 100).
415 Chapter 19 Custom Graphics with SVG and the HTML5 Canvas
In addition to setting the lines' color, this markup sets the element's fill property to
none. This is because, by default, the triangles formed by the polyline are filled in. If the
shape has N points, N–2 triangles will be drawn.
19.1.4 Polygons
The <polygon> element accepts the same points attribute as the <polyline> element.
The difference is that each polygon has a final line that connects the last point to the first.
If there are N points in the polygon, the shape will be drawn with N lines.
As with the polyline element, polygons are filled by default. If fill is set to none,
only the polygon's outline will be drawn.
19.1.5 Rectangles
To draw a rectangle, the <rect> element accepts a total of six attributes: four required
and two optional. The required attributes are x, y, width, and height, which set the
location and size of the rectangle to be drawn. For example, the following markup draws a
80x60 rectangle whose upper-left corner is at (50, 50).
Like the polyline and polygon shapes, SVG rectangles are filled by default. The default
color is black.
The two optional attributes of <rect> make it possible to create rectangles with
rounded corners. The rounded corners are partial ellipses whose radius in the x-axis is
given by rx and whose radius in the y-axis is given by ry.
416 Chapter 19 Custom Graphics with SVG and the HTML5 Canvas
The maximum value of rx is one-half the rectangle's width and the maximum value
of ry is one-half the rectangle's height. If rx and ry are set to their maximum values, the
rectangle will be drawn as an ellipse. This is configured in the following markup:
The minimum values of rx and ry is 0. If rx and ry are set to 0, the rectangle will be
drawn without rounded corners.
19.1.6 Circles
Circles are particularly easy to work with. The <circle> element can define its shape
with only three attributes: cx, cy, and r. cx and cy set the center of the circle and r sets
its radius. For example, the following markup defines a circle whose center is at (100, 100)
and whose radius is 40 pixels.
As depicted in Figure 19.1, circles are filled by default. The default fill color is black.
19.1.7 Ellipses
An ellipse can be thought of as a circle with two radii—one in the x-axis and one in the
y-axis. This is reflected in the <ellipse> element, which accepts four attributes. The cx
and cy attributes set the center of the ellipse, while rx is the radius in the x-axis and ry is
the radius in the y-axis.
For example, the following markup creates an ellipse centered at (75, 75). The radius
in the x-axis is 100 and the radius of the y-axis is 80.
19.1.8 Text
Any text inside of the <svg> won't be displayed unless it's provided in a <text>
subelement. The position of the text is set with two attributes, x and y, and the text to be
printed is contained in the body of the <text> and </text> elements. This is shown in
the following markup:
The <text> element accepts <tspan> subelements that represent text in different
locations. Each <tspan> has its own x and y attributes, and the following markup uses
<tspan> to print four lines of text:
The coordinates of the <text> element don't affect the placement of the text in
the <tspan> subelements. But any styling applied to the <text> will be applied to the
<tspan> elements. This may include setting the color with the fill property, setting
the size with font-size, or setting the font with font-family. The following markup
shows how these properties can be configured:
By default, the x and y coordinates identify the lower-left corner of the box
containing the text. But the text-anchor attribute makes it possible to change how the
coordinates affect the text placement. If text-anchor is set to middle, the coordinates
will set the center of the text. If text-anchor is set to end, the coordinates will set the
end point of the text.
418 Chapter 19 Custom Graphics with SVG and the HTML5 Canvas
19.1.9 Transformation
Each SVG shape and text element can have an attribute called transform. This applies
a linear transformation to the element before displaying it in the page. This may involve
performing one or more of the following operations:
• translation — shifting the shape's position
• rotation — turning the shape through an angle around an origin
• scaling — increasing or decreasing the size of the shape
An example will clarify how the transform attribute can be used. Figure 19.3
depicts a rectangle before and after transformation.
In this case, the transform attribute performs two transformations before drawing
the shape. The first, denoted by translate(50, 20), shifts the rectangle right by 50
pixels and down by 20 pixels. The second transformation, denoted by rotate(30),
rotates the rectangle by 30°.
This rotation doesn't specify an origin with origx or origy. Therefore, the rectangle
is rotated around the origin, (0, 0). A positive angle specifies a counterclockwise rotation
and a negative angle specifies a clockwise rotation.
The order of transformations given in the transform attribute is important. In
the example, performing the rotation before the translation will place the rectangle in a
different position.
HTML's buttons are rectangular, and pages that want to display rounded buttons
frequently use images. But as discussed earlier, SVG makes it possible to define rounded
elements by inserting a <rect> element between <svg> and </svg>. The code in Listing
19.1 demonstrates how a rounded button can be defined in Angular.
@Component({
selector: 'app-root',
styles: ['rect:hover { fill: whitesmoke !important; }'],
template: `
<svg width='100' height='40' (click)='handleClick()'>
</svg>
`})
export class AppComponent {
public handleClick() {
alert('The button has been pressed');
}
}
The <svg> element associates click events with the component's handleClick
method. When the graphic is clicked, the component writes a message to an alert box.
420 Chapter 19 Custom Graphics with SVG and the HTML5 Canvas
@Component({
selector: "canvas-demo",
template: `<canvas id="mycnvs" width="..." height="..."></canvas>`
})
public ngAfterViewInit() {
this.ctx = this.canvas.nativeElement.getContext("2d");
...perform initial drawing...
}
}
The easiest shape to draw in a canvas is a rectangle. This is made possible by calling one of
two methods of the CanvasRenderingContext2D:
• strokeRect(x, y, width, height) — draws the outline of a rectangle at the
coordinates (x, y) with dimensions given by width and height
• fillRect(x, y, width, height) — draws a filled rectangle at the coordinates
(x, y) with dimensions given by width and height
The coordinates are given in pixels relative to the canvas's upper-left corner.
Therefore, if the first two arguments are 100 and 200, the upper-left corner of the
rectangle will be placed 100 pixels to the right of the canvas's upper-left corner and 200
pixels down.
If ctx is a CanvasRenderingContext2D, the following code demonstrates how
strokeRect can be used.
By default, each line in a stroked rectangle will be one pixel wide. Unlike SVG, line
width can't be set with CSS properties. Instead, the CanvasRenderingContext2D
provides properties and methods for styling stroked shapes. Table 19.3 lists them and
provides a description of each.
Table 19.3
Properties/Methods for Stroke Styling
Property/Method Description
lineWidth Sets/returns the line width
strokeStyle Sets/returns the line color
lineCap Shape at the end of a line (butt, round, or square)
lineJoin Shape of corners where lines meet (miter, round, or bevel)
miterLimit Maximum line extension for mitered joins
lineDashOffset Sets/returns the space between the start of a line and the
first dash
setLineDash(array)/ Sets/returns the dash pattern
getLineDash()
422 Chapter 19 Custom Graphics with SVG and the HTML5 Canvas
In code, the application needs to set the stroke style before calling the draw function.
For example, the following code draws the outline of a rectangle with lines ten pixels thick
and rounded corners.
this.ctx.lineWidth = 10;
this.ctx.lineJoin = "round";
this.ctx.strokeRect(100, 200, 50, 50);
By default, lineJoin is set to miter, which means connected lines are extended
until they're joined at a point. The maximum extension is determined by miterLimit.
The setLineDash method accepts an array that sets the pixel lengths of the line's
dashes and spaces. If this argument is set to [x, y], each dash will be x pixels long and
the spacing between dashes will be y pixels.
Figure 19.4 clarifies how line joins and dashes can be set. In each case, the rectangle's
size is set to 100-by-100 and the line width is set to 20.
This discussion has focused on setting styles for stroked rectangles, but the settings in
Table 19.3 apply to all stroked shapes. This includes the following topics: paths and text.
19.2.2 Paths
Table 19.4 lists the different path functions available. The second column describes
the shape added to the path.
Table 19.4
Path Methods
Method Description
beginPath() Starts a path
moveTo(number x, number y) Changes the current point to (x, y)
lineTo(number x, number y) Adds a line from the current point to (x, y)
arcTo(number ctrlx, Adds an arc from the current point to (x, y)
number ctrly, number x,
number y, number radius)
quadraticCurveTo( Adds a quadratic Bézier curve from the current
number ctrlx, number ctrly, point to (x, y) using the control point (ctrlx, ctrly)
number x, number y)
bezierCurveTo( Adds a cubic Bézier curve from the current point to
number ctrlx_1, (x, y) using the control points (ctrlx_1, ctrly_1) and
number ctrly_1, (ctrlx_2, ctrly_2)
number ctrlx_2,
number ctrly_2,
number x, number y)
closePath() Adds a straight line from the current point to the
path's initial point
stroke() Draws an outline of the shapes that make up the
path
fill() Fills the path with the current fill color
The moveTo method must be called to set the path's initial point. As a simple
example, the following code sets the initial point to (100, 100). Then it calls lineTo to
add a line from e from the initial point to (200, 200).
this.ctx.beginPath();
this.ctx.lineTo(200, 200);
this.ctx.stroke();
Just as lineTo adds a straight line to the path, arcTo adds a circular arc from the
current point to the destination. quadraticCurveTo adds a quadratic Bézier curve
to the path and bezierCurveTo adds a cubic Bézier curve. Though fascinating, the
mathematics underlying Bézier curves lie beyond the scope of this book.
424 Chapter 19 Custom Graphics with SVG and the HTML5 Canvas
The following code demonstrates how many of these methods can be used. The code
adds a straight line, an arc, another straight line, and a quadratic Bézier curve to the path.
Then it calls closePath to draw a line from the final point to the starting point.
ctx.beginPath();
ctx.moveTo(50, 300);
ctx.lineTo(250, 200);
ctx.arcTo(350, 150, 400, 275, 25);
ctx.lineTo(440, 400);
ctx.quadraticCurveTo(460, 480, 410, 460);
ctx.closePath();
ctx.stroke();
Figure 19.5 shows what the resulting shape looks like. In addition to the points on
the path, this figure displays the control points for the arc and the quadratic Bézier curve.
Dashed lines indicate how the control points are positioned relative to the path's lines.
Because the path is stroked, its appearance can be customized with the methods listed
in in Table 19.4. If ctx.fill() is called instead of ctx.stroke(), the path will be filled
in. A later discussion will explain the different fill styles provided by the Canvas API.
19.2.3 Text
Like the other shapes discussed so far, text can be drawn in outline (stroked) or filled in.
The CanvasRenderingContext2D provides two methods for drawing text:
1. strokeText(str, x, y [, maxWidth]) — draws text at the location (x, y)
with the optional maximum width
2. fillText(str, x, y [, int maxWidth]) — draws and fills in text at the
location (x, y) with the optional maximum width
As an example, the following code draws the string Hello at the location (50, 50).
By default, canvas text is printed in a 10-pixel sans-serif font. This can't be changed
with CSS, but the CanvasRenderingContext2D provides four properties for styling
text. Table 19.5 lists them and provides a description of each.
Table 19.5
Text Styling Methods
Method Description
font Sets the font size and font family
textAlign Text alignment - start , end, left, right or center
textBaseline Text position relative to the baseline - top, hanging, middle, alphabetic,
ideographic, or bottom
direction Arrangement of characters in the text - ltr (left-to-right), rtl, or inherit
The font property accepts a string that identifies the size and family of the desired
font. This string is parsed as a CSS font value, so the size can be given in units like px, em,
ex, cm, mm, in, pt, or pc. The font family can also take any value acceptable for the CSS
font-family property. This is demonstrated in the following code:
Drawn text defines a set of horizontal lines called baselines. The top baseline is
positioned at the height of the highest character and the bottom baseline is positioned at
the lowest point of the lowest character. The textBaseline property makes it possible to
change which of these baselines should be used to vertically position text.
19.2.4 Images
ctx.drawImage(img, x, y)
Another usage of drawImage inserts dimensions that set the size of the image to be
displayed in the canvas:
Instead of displaying the entire image, drawImage can clip the image first. To
identify where the image should be clipped, the method accepts additional parameters:
the upper-left corner of the clipped region (sx, sy) and the region's dimensions (swidth,
sheight). Adding these parameters, the method' full declaration is given as:
After this code executes, the image data in smiley.jpg hasn't necessarily been loaded
into the HTMLImageElement. Rather than call drawImage immediately, it's better to
wait until the image has been loaded. The following code shows how to accomplish this by
assigning the image's onload property to an anonymous function:
So far, the example code has called strokeRect to draw rectangles, strokeText to
draw text, and stroke to draw paths. The Canvas API also provides fill methods, such
as fillRect, fillText, and fill. The fill color is black by default, but this can be
configured by setting the fillStyle property of the CanvasRenderingContext2D.
The simplest way to use fillStyle is to set it equal to a color. The color must be
given as a CSS string, so any of the following lines will set the fill color to blue:
ctx.fillStyle = "blue";
ctx.fillStyle = "rgb(0, 0, 255)";
ctx.fillStyle = "#0000ff";
ctx.fillStyle = "#00f";
In addition to being set to a constant color, the fill style can be set to a change in
color called a gradient. To be specific, the fillStyle property can be set equal to a
CanvasGradient. The CanvasRenderingContext2D provides two methods that
return CanvasGradients:
• createLinearGradient(x0, y0, x1, y1) — Returns a linear gradient whose
color changes along a line from (x0, y0) to (x1, y1)
• createRadialGradient(x0, y0, r0, x1, y1, r1) — Returns a radial
gradient whose color starts at a circle centered at (x0, y0) with a radius of r0 and
ends at a circle centered at (x1, y1) with a radius at r1
The first call to addColorStop sets the initial color to black and the second call sets
the final color to white. Therefore, when the rectangle is drawn, the color will progress
linearly from black to white.
428 Chapter 19 Custom Graphics with SVG and the HTML5 Canvas
The first argument identifies the image to be repeated in the filled shape. The image
must be given as a CanvasImageSource, which can be provided in a number of forms,
including as an HTMLCanvasElement and another CanvasRenderingContext2D.
A common way to obtain a CanvasImageSource is to create or obtain an
HTMLImageElement. As discussed earlier can be created with the new Image
constructor or by calling document.createElement("img").
For example, the following code creates a new CanvasImageSource whose source is
set to smiley.jpg. When the image is loaded, the code creates a CanvasPattern from the
CanvasImageSource and makes it the current fill style.
19.2.6 Transformations
As mentioned earlier in this chapter, a linear transformation may involve one of three
operations or a combination thereof:
• translation — shifting the shape's position
• rotation — turning the shape through an angle around an origin
• scaling — increasing or decreasing the size of the shape
Every canvas has a data structure that determines what operation or operations
should be performed on its shapes. This is called the current matrix, and it's represented
by a SVGMatrix object, which consists of a series of numbers that form a mathematical
structure called a matrix. Matrix theory is beyond the scope of this appendix, so this
appendix won't discuss the SVGMatrix or the transform matrix.
Instead, this discussion focuses on the three CanvasRenderingContext2D
methods that transform shapes in the canvas:
429 Chapter 19 Custom Graphics with SVG and the HTML5 Canvas
The rotate method is different than the rotate used in SVG. First, the rotation
is always performed around the canvas's origin. Second, the argument is assumed to be
given in radians. To convert degrees to radians, a value must be multiplied by π/180. this
is shown in the following code, which rotates elements by 45°:
ctx.rotate(45 * Math.PI/180);
ctx.translate(75, 50);
ctx.rotate(30 * Math.PI/180);
The translate, rotate, and scale methods alter the canvas's current matrix, so
the transformations will be applied to every shape drawn afterward. To reset the current
matrix back to its original value, the individual values of the matrix must be changed. This
can be accomplished with the following code:
ctx.setTransform(1, 0, 0, 1, 0, 0);
The example application at the end of this chapter demonstrates how this is used in
practice.
19.2.7 Animation
The shapes in an HTML5 canvas can't be styled with CSS, so the animation procedure
discussed in Chapter 15 won't be suitable for canvas animation. Instead, the application
needs to configure when its canvas should be redrawn. JavaScript provides three functions
that can serve this purpose:
430 Chapter 19 Custom Graphics with SVG and the HTML5 Canvas
public ngAfterViewInit() {
this.ctx = this.canvas.nativeElement.getContext("2d");
this.drawFrame();
}
public drawFrame() {
window.requestAnimationFrame(() => { this.drawFrame(); });
this.ctx.clearRect(0, 0, 200, 200);
this.ctx.translate(100, 100);
this.ctx.rotate(6 * this.count++ * Math.PI/180);
this.ctx.beginPath();
this.ctx.moveTo(0, 0);
this.ctx.lineTo(50, 0);
this.ctx.stroke();
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
}
In this code, drawFrame calls clearRect before drawing the line. This accepts the
same arguments as the strokeRect/fillRect methods discussed earlier, and it clears
the canvas for redrawing.
The window.requestAnimationFrame method accepts a callback function to
be called before the next repaint operation. When this function is called, it receives an
argument that identifies the time when the callback was called for the first time. With this
information, an application can keep track of elapsed time between iterations.
The code in Listing 19.2 shows how an application can use the elapsed time to
control the animation. In this case, the line completes a rotation every five seconds. The
component also contains a button that halts the animation if pressed.
431 Chapter 19 Custom Graphics with SVG and the HTML5 Canvas
@Component({
selector: 'app-root',
styles: ['canvas { border: 1px solid; }'],
template: `
<canvas #test [width]='width' [height]='height'></canvas><br />
<button (click)='onClick()'>
{{ keepDrawing ? 'Halt Animation' : 'Start Animation' }}
</button>
`})
// Continue animation
if (this.keepDrawing) {
window.requestAnimationFrame(t => this.drawFrame(t) );
}
}
private onClick() {
this.keepDrawing = !this.keepDrawing;
if (this.keepDrawing) {
window.requestAnimationFrame(t => this.drawFrame(t) );
}
}
}
19.3 Summary
SVG and the HTML5 canvas make it possible to create custom graphics in a component's
template. They provide essentially the same capabilities, but SVG graphics are defined
using markup and canvas graphics are defined by calling JavaScript functions.
To access SVG in an Angular application, <svg></svg> tags must be inserted into
a component's template. Inside these tags, graphics can be defined using subelements like
<circle>, <rect>, and <line>. Text can be added using <text> subelements. These
graphics can be configured by assigning their attributes to suitable values.
Configuring an HTML5 canvas in an Angular application is more involved.
First, a <canvas> element must be inserted into a component's template. Then the
component needs to access the element as a view child, which means adding code to the
component's ngAfterViewInit method. This code needs to access the view child's
CanvasRenderingContext2D object, which provides functions for creating graphics.
433 Chapter 19 Custom Graphics with SVG and the HTML5 Canvas
J
H
N
L
Netscape 3
never type 41
lazy loading 141, 242, 259–260 NgClass directive 180, 186
let 27, 48–50 NgForOf directive 182–184
life cycle methods 171–172 NgIf directive 180–182, 189
loaders 122, 134 NgModel 176
local variables 161 NgStyle directive 180, 186–187
location strategies 265 NgSwitchCase directive 180, 185
loop NgSwitchDefault directive 180, 185
for..in 38 NgSwitch directive 180, 185
for..of 38 Node interface 78–82
lowercase pipe 146, 149 Node.js 13–15, 103
noImplicityAny 40
non-primitive types 40
npm (Node package manager) 13–15
M null type 40
number pipe 146–147
number-string conversion 31
matchers 100–101 number type 29
material design
buttons and anchors 350–352
cards 355–358
checkboxes 347–348
input container 349–350
lists 359–360
Index 439
O Protractor
actions 379–381
browser object 317–372
configuration 368–370
object-oriented programming (OOP) 52–56, 73
ElementFinders 374–378
object prototypes 108–110
executing tests 381–382
object type 39, 40
using locators 373–374
observables 222–237, 272, 275
providers 205–210, 213, 312, 313
Observers 224, 227–229
providers array 141
optional parameters 46
public access modifier 59
outerHTML property 85
public modifier 55–56
outerText property 85
overriding members 60
Q
P
quantifiers 34–35
query configuration 247–248
package.json 15
QueryList class 166–167, 222
pagination 399, 400
parameter decorators 111–112
parent-child injectors 213
pending state 216 R
percent pipe 146–147
pipes 146–147
PipeTransform interface 332–333 Reactive-Extensions 222
platform logic 141 Reactive Forms API 285
polymorphism 53–54 ReflectiveInjector class 211
primitive types 40 regular expressions 34–35
private access modifier 59 rejection handler 218
private modifier 55–56 rejection handlers 217–220
production mode 134, 142 rejection state 216
projects 19 RequestOptions parameter 273–274
project structure 19 Response class 272, 276–277
promises 216–221 rest parameters 46
property binding 150–153 REST (Representational State Transfer) 389–392
property decorators 106–108 route matching 252
property interpolation 145 Router class 263–264
protected access modifier 59 router links 246–247, 251
protected modifier 55–56 router module 243
440 Index
U
XLIFF (XML Localisation Interchange File Format)
321, 331
XMB (XML Message Bundle) 321, 331
undefined type 40
XMLHttpRequests 272
union type 43
unit testing 97
Universal Module Definition (UMD) 121
uppercase pipe 146, 149
useClass field 208
useFactory function 209