IMPORTANT:
Some of the content here is a personal summary/abbreviation of contents on the official documentation and the tutorial on Scrimba. Feel free to refer to the official site if you think some of the sections written here are not clear.
Installing Angular
To install angular, it is recommended that you first install nvm
and node
: https://github.com/nvm-sh/nvm
Once you have those two installed, you can install theAngular Cli
: https://angular.io/guide/setup-local
- With
Angular Cli
, it allows you to generate much content with some simple commands. For example, theng generate
/ng g
command. - For more information on
Angular Cli
, please visit https://cli.angular.io/
Introduction to Angular
Angular is a platform and framework for building single-page client applications using HTML and TypeScript. Angular is written in TypeScript. It
implements core and optional functionality as a set of TypeScript libraries that you import into your apps.
Creating an Angular Project
To create a new angular project, you can use ng new <projectName>
, which will generate a new folder with name <projectName>
under your current directory, in which your project source code would go.
Once you have created your first project with ng new <projectName>
, you will see that it generated many files as shown below:
A high level overview of the usages of those file would be:
File | Usage |
---|---|
tslint.js |
Used to control/standardize certain code/project properties across the team |
tsconfig.json |
Controls the compilation settings for ts (type-script) files |
package.json |
Controls the dependencies that our project has |
package-lock.json |
Controls the dependencies that should be immutable |
karma.conf.js |
Used for unit testing your code |
.gitignore |
Controls which files would be ignored for your git version control |
.editorconfig |
Used for customizing coding format in your editors (some editors don’t use it) |
.angular-cli.json |
Controls packaging/deployment properties of your entire project |
src |
Directory where your source code should live |
Basic Building Blocks of an Angular App
On a very high level, your application would use the following building blocks:
- Components
- UI code, how your web application looks
- Services
- Frontend/Backend service, which does the computing
- Modules
- A container assembling the above two
An example would be:
Component
A component
in angular is made up of some html templates and some source codes (these two combined is also called a view), which can also include/use certain services.
- Components define views, which are sets of screen elements that Angular can choose among and modify according to your program logic and data.
- Components use services, which provide specific functionality not directly related to views. Service providers can be injected into components as dependencies, making your code modular, reusable, and efficient.
Every Angular application has at least one component, the root component that connects a component hierarchy with the page document object model (DOM). Each component defines a class that contains application data and logic, and is associated with an HTML template that defines a view to be displayed in a target environment.
A component
typically has a selector, which would look like <app-customer></app-customer>
(in this case, app-customzer
would be your component name). This is used for Angular in its html to know which component to go to.
Component Code
Codes in a component typically contains the following elements:
- Imports
- imports necessary dependencies for your code
- Decorators
- Metadata about your component
- Class
- Actual code for your UI
For example:
Your imports could be:
1 | import { Component } from `@angular/code`; /** This is provided by Angular */ |
Your decorators could be:
1 | ({ |
You define a component’s application logic - what it does to support the view - inside a class. The class interacts with the view through an API of properties and methods:
1 | export class CustomersComponent{ // export makes this component available for other components |
A simple working example could be:
1 | // in your app.component.ts |
And of course, there needs to be a module to contain/register that component:
1 | // in your app.module.ts |
Now, if you run ng serve -o
, you will see your server up and running.
Note:
ng serve
compiles and serves up your application.ng serve -open
orng serve -o
will dong serve
and then also opens the web browser with your project’s URL address.
Basic Mechanism
How doesAngular
node where to look for when it finds <app-root>
tag in the main.html
?
- Angular looks at the
main.ts
file:
1 | import { enableProdMode } from '@angular/core'; |
- Now, in that module,
Angular
will be told to loadAppComponent
1 | import { BrowserModule } from '@angular/platform-browser'; |
Creating a Component
To create a component, you can use the command ng g component <component-name>
. This will generate a folder with name <component-name>
along with four files:
<component-name>.component.css
- the
css
for the below html page
- the
<component-name>.component.html
- you can imagine this as a sub-
html
page that could be injected later into other html
- you can imagine this as a sub-
<component-name>.component.spec.ts
<component-name>.component.ts
- where your component code goes
It is recommended that for each new feature, you use a module
to manage those components. Therefore, it is recommended that you add:
<component-name>.module.ts
For example, you can populate your <component-name>.component.ts
with:
1 | import { Component, OnInit } from '@angular/core'; |
The template html could be as simple as:
1 | <h1>{{ title }}</h1> |
Now, importantly, your <component-Name>.module.ts
could be:
1 | import { NgModule } from '@angular/core'; |
Now, to make this available, we would need to include this module in our root module - app.module.ts
in order for Angular to see our work:
1 | import { NgModule } from '@angular/core'; |
Lastly, your app.component.ts
for the displayed web page would look like:
1 | import { Component, OnInit } from '@angular/core'; |
Component Metadata
The metadata for a component tells Angular where to get the major building blocks that it needs to create and present the component and its view. In particular, it associates a template with the component, either directly with inline code, or by reference. Together, the component and its template describe a view.
In addition to containing or pointing to the template, the @Component
metadata configures, for example, how the component can be referenced in HTML and what services it requires.
For example:
1 | ({ |
This example shows some of the most useful @Component
configuration options:
selector
: A CSS selector that tells Angular to create and insert an instance of this component wherever it finds the corresponding tag in template HTML.- For example, if an app’s HTML contains
<app-hero-list></app-hero-list>
, then Angular inserts an instance of theHeroListComponent
view between those tags.
- For example, if an app’s HTML contains
templateUrl
: The module-relative address of this component’s HTML template. Alternatively, you can provide the HTML template inline, as the value of thetemplate
property. This template defines the component’s host view.providers
: An array of providers for services that the component requires.- In the example, this tells Angular how to provide the
HeroService
instance that the component’s constructor uses to get the list of heroes to display.
- In the example, this tells Angular how to provide the
Template Syntax
A template looks like regular HTML, except that it also contains Angular template syntax, which alters the HTML based on your app’s logic and the state of app and DOM data.
For example, here is a template for the Tutorial’s HeroListComponent
.
1 | <h2>Hero List</h2> |
This template uses typical HTML elements like <h2>
and <p>
, and also includes Angular template-syntax elements, *ngFor
, ,
(click)
, [hero]
, and <app-hero-detail>
. The template-syntax elements tell Angular how to render the HTML to the screen, using program logic and data.
All those Angular HTML elements above will be explored below.
Data Binding
One of the most important and convenient feature of Angular is its data binding (a feature also common to most Template Engines). However, Angular provides more advanced data bindings as well, which you will find out shortly below.
Note:
- Without a framework like Angular, you would be responsible for pushing data values into the HTML controls and turning user responses into actions and value updates. Writing such push and pull logic by hand is tedious, error-prone, and a nightmare to read, as any experienced front-end JavaScript programmer can attest.
- One advanced feature of Angular is its two-way data binding, a mechanism for coordinating the parts of a template with the parts of a component. Add binding markup to the template HTML to tell Angular how to connect both sides.
The following diagram shows the four forms of data binding markup. Each form has a direction: to the DOM, from the DOM, or both.
The example from the HeroListComponent
template above uses three of these forms.
1 | <li>{{hero.name}}</li> |
- The
(inline) interpolation displays the component’s
hero.name
property value within the<li>
element. - The
[hero]
property binding passes the value ofselectedHero
from the parentHeroListComponent
to thehero
property of the childHeroDetailComponent
. - The
(click)
event binding calls the component’sselectHero
method when the user clicks a hero’s name.
Note:
- If you use interpolation, you can only supply it with a
string
. If you use property bindings, you can supply it with objects.- For property binding, it might look confusing, but it is that the
"value"
gets rendered from the host component (maps to field with the namevalue
), and then that rendered"value"
gets passed into the property with nameproperty
for later uses.
- If you did
property="value"
without brackets, the"value"
will not be passed from the component as Angular does not know it is a property binding.
Two-way data binding (used mainly in template-driven forms) combines property and event binding in a single notation. Here’s an example from the HeroDetailComponent
template that uses two-way data binding with the ngModel
directive.
1 | <input [(ngModel)]="hero.name"> |
In two-way binding:
- First, a data property value flows to the input box from the component as with property binding. (Why it starts with a
[]
) - Then, if the user’s changes the field, it also flow back to the component, resetting the property to the latest value, as with event binding.
Class Binding
You can also add and remove CSS class from an element’s class
attribute with a class binding.
Here’s how to set the class
attribute without a binding in plain HTML:
1 | <!-- standard class attribute setting --> |
- To create a single class binding, start with the prefix
class
followed by a dot (.
) and the name of the CSS class (for example,[class.foo]="hasFoo"
). Angular adds the class when the bound expression is true, and it removes the class when the expression is false:
1 | <div [class.fooBar]="<some expression>"></div> |
- If
<some expression>
evaluates totrue
, then the CSS classfoorBar
will be added - If
<some expression>
evaluates tofalse/undefined/null
, then the CSS classfoorBar
will not be added or be removed (if added)
- To create a binding to multiple classes, use a generic
[class]
binding without the dot (for example,[class]="classExpr"
). The expression can be a space-delimited string of class names, or you can format it as an object with class names as the keys and true/false expressions as the values. With object format, Angular will add a class only if its associated value is truthy.
Binding Type | Syntax | Input Type | Example Input Values |
---|---|---|---|
Single class binding | [class.foo]="hasFoo" |
`boolean | undefined |
Multi-class binding | [class]="classExpr" |
string |
"my-class-1 my-class-2 my-class-3" |
`{[key: string]: boolean | undefined | ||
Array<string> |
['foo', 'bar'] |
Pipes
Angular pipes let you declare display-value-transformations in your template HTML. A class with the @Pipe
decorator defines a function that transforms input values to output values for display in a view.
Angular defines various pipes, such as the date pipe and currency pipe; for a complete list, see the Pipes API list. You can also define new pipes.
To specify a value transformation in an HTML template, use the pipe operator |
.
1 | {{interpolated_value | pipe_name}} |
You can chain pipes, sending the output of one pipe function to be transformed by another pipe function. A pipe can also take arguments that control how it performs its transformation. For example, you can pass the desired format to the date
pipe.
1 | <!-- Default format: output 'Jun 15, 2015'--> |
Directives
Angular templates are dynamic. When Angular renders them, it transforms the DOM according to the instructions given by directives. A directive is a class with a @Directive()
decorator.
Note:
- A component is technically a directive. However, components are so distinctive and central to Angular applications that Angular defines the
@Component()
decorator, which extends the@Directive()
decorator with template-oriented features.
In addition to components, there are two other kinds of directives: structural and attribute. Angular defines a number of directives of both kinds, and you can define your own using the @Directive()
decorator.
Just as for components, the metadata for a directive associates the decorated class with a selector
element that you use to insert it into HTML. In templates, directives typically appear within an element tag as attributes, either by name or as the target of an assignment or a binding.
Structural Directives
Structural directives alter layout by adding, removing, and replacing elements in the DOM (hence changing parts of its structure). The example template uses two built-in structural directives to add application logic to how the view is rendered.
1 | <li *ngFor="let hero of heroes"></li> |
*ngFor
is an iterative; it tells Angular to stamp out one<li>
per hero in theheroes
list.*ngIf
is a conditional; it includes theHeroDetail
component only if a selected hero exists.
Attribute Directives
Attribute directives alter the appearance or behavior of an existing element (hence not changing the overall DOM structure). In templates they look like regular HTML attributes, hence the name.
The ngModel
directive, which implements two-way data binding, is an example of an attribute directive. ngModel
modifies the behavior of an existing element (typically <input>
) by setting its display value property and responding to change events.
1 | <input [(ngModel)]="hero.name"> |
For more information on directives, learn more in the Attribute Directives and Structural Directives guides.
Module
A Module in Angular is called a NgModule
, which can be interpreted as a container assembling some of your components and services for a specific feature.
An
NgModule
declares a compilation context for a set of components that is dedicated to an application domain, a workflow, or a closely related set of capabilities. AnNgModule
can associate its components with related code, such as services, to form functional units.Every Angular app has a root module, conventionally named
AppModule
, which provides the bootstrap mechanism that launches the application. An app typically contains many functional modules.Like JavaScript modules,
NgModules
can import functionality from otherNgModules
, and allow their own functionality to be exported and used by otherNgModules
. For example, to use the router service in your app, you import theRouter
NgModule
.
NgModule
Metadata
An NgModule
is defined by a class decorated with @NgModule()
. The @NgModule()
decorator is a function that takes a single metadata object (why it is surrounded by a {}
), whose properties describe the module. The most important properties are as follows.
declarations
: The Component, directives, and pipes that belong to thisNgModule
.exports
: The subset of declarations that should be visible and usable in the component templates of otherNgModules
(when imported).imports
: Other modules whose exported classes are needed by component templates declared in thisNgModule
.providers
: Creators of Service that thisNgModule
contributes to the global collection of services; they become accessible in all parts of the app. (You can also specify providers at the component level, which is often preferred.)bootstrap
: The main application view, called the root component, which hosts all other app views. Only the rootNgModule
(of a specific view) should set thebootstrap
property.
An example would be:
1 | import { NgModule } from '@angular/core'; |
Note:
- A component and its template together define a view. You can imagine a view as a sub-html page that is rendered by Angular.
NgModule
and Component
The aim of a component is to create a view with a html template. When you create a component, it is associated directly with a single view, called the host view (for this component). The host view can be the root of a view hierarchy, which can contain embedded views, which are in turn the host views of other components.
Those components can be in the same NgModule
, or can be imported from other NgModules
. Views in the tree can be nested to any depth.
A simple example of a view hierarchy could be:
The aim of a Module/NgModule
is to define a compilation context - the context where Angular should be able to find all necessary components for rendering a provided html template.
A root NgModule
always has a root component that is created during bootstrap, but any NgModule
can include any number of additional components, which can be loaded through the router or created through the template. The components that belong to an NgModule
share a compilation context.
Angular Library Modules
Angular loads as a collection of JavaScript modules. You can think of them as library modules. Each Angular library name begins with the @angular
prefix. Install them with the node package manager npm
and import parts of them with JavaScript import
statements.
NgModules
from Angular libraries using JavaScript import statements. For example, the following code imports the BrowserModule
NgModule
from the platform-browser
library.
1 | import { BrowserModule } from '@angular/platform-browser'; |
In the example of the simple root module above, the application module needs material from within BrowserModule
. To access that material, add it to the @NgModule
metadata imports
like this.
1 | ({ |
Service
A Service is usually pieces of code (function, data, feature) that are used across different components.
A service class definition is immediately preceded by the
@Injectable()
decorator. The decorator provides the metadata that allows other providers to be injected as dependencies into your class.For example: The Angular
Router
NgModule
provides a service that lets you define a navigation path among the different application states and view hierarchies in your app. (More on Routers will be covered later)
Angular distinguishes components from services to increase modularity and reusability. By separating a component’s view-related functionality from other kinds of processing, you can make your component classes clean and efficient.
Example of a Service
Here would be an example of a service (class) - src/app/logger.service.ts
:
1 | export class Logger { |
(The above is just a simple example, an actual service usually has the @Injectable()
annotation, in order to be registered with an Angular dependency injector. More on this later.)
Services can depend on other services (made available by dependency injection). For example, here’s a HeroService
that depends on the Logger
service, and also uses BackendService
to get heroes. That service in turn might depend on the HttpClient
service to fetch heroes asynchronously from a server.
1 | export class HeroService { |
Dependency Injection (DI)
DI is wired into the Angular framework and used everywhere to provide new components with the services or other things they need.
Components consume services; that is, you can inject a service into a component, giving the component access to that service class.
To define a class as a service in Angular, use the @Injectable()
decorator to provide the metadata that allows Angular to inject it into a component as a dependency. Similarly, use the @Injectable()
decorator to indicate that a component or other class (such as another service, a pipe, or an NgModule
) has a dependency.
Dependency Injection Mechanism
In general, there are three components involved:
- The injector is the main mechanism. Angular creates an application-wide injector for you during the bootstrap process, and additional injectors as needed. You don’t have to create injectors.
- An injector creates dependencies, and maintains a container of dependency instances that it reuses if possible.
- All of those are done behind the scene.
- A provider is an object that tells an injector how to obtain or create a dependency.
Angular does dependency injection by the following three steps:
When Angular creates a new instance of a component class, it determines which services or other dependencies that component needs by looking at the constructor parameter types.
For example, the
HeroService
dependency will be injected in the example below:1
constructor(private service: HeroService) { }
When Angular discovers that a component depends on a service, it first checks if the injector has any existing instances of that service. If a requested service instance doesn’t yet exist, the injector makes one using the registered provider, and adds it to the injector before returning the service to Angular.
When all requested services have been resolved and returned, Angular can call the component’s constructor with those services as arguments.
Using Dependency Injection
For any using dependency that you need in your app, you must:
- Register a provider for the dependency (class/function/value) with the app’s injector (with
@Injectable
), so that the injector can use the provider to create new instances. For a service, the provider is typically the service class itself.- This can be done either directly within the same service class, or in a specific module/component, since this process is done by annotation metadata.
- Inject that dependency by including it in the constructor argument.
- This can be done in three different ways. Each of which would be available project-wide.
Case 1: You registered the service within the same service class:
This can be done by either creating the service with Angular CLI
ng g service <service-name>
, which will automatically include the annotation:1
2
3({
providedIn: 'root', // provides the dependecy at the root level
}) Or including the above annotation manually in your service class.
When you provide the service at the root level, Angular creates a single, shared instance of
HeroService
available to the entire project and injects it into any class that asks for it. Registering the provider in the@Injectable()
metadata also allows Angular to optimize an app by removing the service from the compiled app if it isn’t used.Case 2: You registered the service within a specific
NgModule
In this case, the same instance of a service is available to all components in that
NgModule
. To register at this level, use theproviders
property of the@NgModule()
decorator.1
2
3
4
5
6
7
8({
// injecting at a module level
providers: [
BackendService,
Logger
],
...
})Case 3: You registered the service within a specific
Component
When you register a provider at the component level, you get a new instance of the service with each new instance of that component. At the component level, register a service provider in the
providers
property of the@Component()
metadata.1
2
3
4
5({
selector: 'app-hero-list',
templateUrl: './hero-list.component.html',
providers: [ HeroService ]
})
Angular Fundamentals
This chapter covers, in more details, the functionalities that Angular brings to developing a frontend application. This chapter will also touch on (in the end) how to communicate with a backend server for getting/updating/posting data.
Template Syntax
This section is a comprehensive technical summary to the Angular template language (for a more complete reference, please visit https://angular.io/guide/template-syntax). This section seeks to explain basic principles of the template language and describes most of the syntax that you’ll encounter elsewhere in the guide.
Interpolation
Interpolation refers to embedding expressions into marked up text. By default, interpolation uses as its delimiter the double curly braces, .
The text between the braces is a template expression that Angular first evaluates and then converts to a string. The following interpolation illustrates the point:
1 | <!-- "The sum of 1 + 1 is 2" --> |
With interpolation, you can basically access everything defined in the bound component.ts
including:
- methods
- variables
- constants
You can also do some basic operations within `{{ }}`. See Template Expressions for more details.
Note:
- Interpolation cannot evaluate data structure other than a primitive. If you are trying to evaluate an Object, then it will render it as
[object Object]
(obviously not what you want). If you want to use complex data structures, you need to use Property Binding or a Pipe operator|
.
Template Expressions
A template expression produces a value and appears within the double curly braces, {{ }} . Angular executes the expression and assigns it to a property of a binding target; the target could be an DOM
property, a component, or a directive.
For example: the interpolation braces in {{1 + 1}} surround the template expression 1 + 1
. In the property binding, a template expression appears in quotes to the right of the =
symbol as in [property]="expression"
.
The Angular template expression language employs a subset of JavaScript syntax supplemented with a few special operators for specific scenarios. The next sections cover three of these operators:
- pipe
|
- safe navigation operator
- non-null assertion operator
Pipe Operator |
The result of an expression might require some transformation before you’re ready to use it in a binding. For example, you might display a number as a currency, change text to uppercase, or filter a list and sort it (visit Pipes for information on available pipe functions).
Pipes are simple functions that accept an input value and return a transformed value. They’re easy to apply within template expressions, using the pipe operator (|
):
1 | <p>Title through uppercase pipe: {{title | uppercase}}</p> |
Some example usages include:
1 | <!-- convert title to uppercase, then to lowercase --> |
Note:
- The pipe operator has a higher precedence than the ternary operator (
?:
), which meansa ? b : c | x
is parsed asa ? b : (c | x)
. Nevertheless, for a number of reasons, the pipe operator cannot be used without parentheses in the first and second operands of?:
. A good practice is to use parentheses in the third operand too.
Safe Navigation Operator ?
The Angular safe navigation operator, ?
, guards against null
and undefined
values in property paths. Here, it protects against a view render failure if item
is null
(if null
error occurs, the entire module associated with it might fail to render and cause wrong html codes being displayed).
1 | <p>The item name is: {{item?.name}}</p> |
With the safe navigation operator, ?
, Angular stops evaluating the expression when it hits the first null
value and renders the view without errors.
Note:
- Sometimes however,
null
values in the property path may be OK under certain circumstances, especially when the value starts out null but the data arrives eventually. In this case, you need to be careful for using?
as it will stop evaluating immediately once it is null.
The ?
operator works perfectly with long property paths such as a?.b?.c?.d
as well.
Non-Null Assertion Operator !
By default, the type checker also throws an error if it can’t determine whether a variable will be null
or undefined
at runtime. In this case, you can tell the type checker not to throw an error by applying the postfix non-null assertion operator, !.
The Angular non-null assertion operator, !
, serves the same purpose in an Angular template. For example, you can assert that item
properties are also defined.
1 | <!-- Assert color is defined, even if according to the `Item` type it could be undefined. --> |
When the Angular compiler turns your template into TypeScript code, it prevents TypeScript from reporting that item.color
might be null
or undefined
.
Note:
- Unlike the safe navigation operator, the non-null assertion operator does not guard against
null
orundefined
. Rather, it tells the TypeScript type checker to suspend strictnull
checks for a specific property expression.
Built-in Template Functions
Sometimes a binding expression triggers a type error during AOT compilation and it is not possible or difficult to fully specify the type. To silence the error, you can use the $any()
cast function to cast the expression to the any
type as in the following example:
1 | <p>The item's undeclared best by date is: {{$any(item).bestByDate}}</p> |
When the Angular compiler turns this template into TypeScript code, it prevents TypeScript from reporting that bestByDate
is not a member of the item
object when it runs type checking on the template.
Data Binding and DOM Property
Angular provides many kinds of data-binding. Binding types can be grouped into three categories distinguished by the direction of data flow:
- From the source-to-view
- From view-to-source
- Two-way sequence: view-to-source-to-view
Type | Syntax | Category |
---|---|---|
Interpolation Property Attribute Class Style | [target]="expression" bind-target="expression" |
One-way from data source to view target |
Event | (target)="statement" on-target="statement" |
One-way from view target to data source |
Two-way | [(target)]="expression" bindon-target="expression" |
Two-way |
Binding types other than interpolation have a target name to the left of the equal sign, either surrounded by punctuation, []
or ()
, or preceded by a prefix: bind-
, on-
, bindon-
, and an expression to the right of the equal sign.
The target of a binding is the DOM property or event inside the binding punctuation: []
, ()
or [()]
, it is not the attribute of the html tag (see below section).
HTML Attribute vs. DOM Property
For more details, please see this StackOverflow post.
In summary, when you write some html code such as:
1 | <input type="text" value="Name:"> |
You have defined 2 attributes (type
and value
). However, to actually display the page, the browser will first parse this code and create an Object.
Once the browser parses this code, a HTMLInputElement object will be created, and this object will contain dozens of properties like:
accept
,accessKey
,align
,alt
,attributes
,autofocus
,baseURI
,checked
,childElementCount
,childNodes
,children
,classList
,className
,clientHeight
, etc.Source: this StackOverflow post.
In the end, if some event happened and something in your html has changed, it will be the property of the DOM object being changed, not its html attribute.
Possible Binding Targets
The target of a data-binding is something in the DOM. Depending on the binding type, the target can be a property (element, component, or directive), an event (element, component, or directive), or sometimes an attribute name. The following table summarizes the targets for the different binding types.
Type | Target | Examples |
---|---|---|
Property | Element property Component property Directive property | src , hero , and ngClass in the following:<img [src]="heroImageUrl"> <app-hero-detail [hero]="currentHero"></app-hero-detail> <div [ngClass]="{'special': isSpecial}"></div> |
Event | Element event Component event Directive event | click , deleteRequest , and myClick in the following:(click)="onSave()">Save</button> <app-hero-detail (deleteRequest)="deleteHero()"></app-hero-detail> <div (myClick)="clicked=$event" clickable>click me</div> |
Two-way | Event and property | <input [(ngModel)]="name"> |
Attribute | Attribute (the exception) | <button [attr.aria-label]="help">help</button> |
Class | class property |
<div [class.special]="isSpecial">Special</div> |
Style | style property |
<button [style.color]="isSpecial ? 'red' : 'green'"> |
Property Binding
Property binding is a one-way mechanism that lets you set the property of a view element. Property binding flows a value in one direction, from a component’s property into a target element property. Property binding uses the [property]="expression"
syntax for data binding, where the evaluation occurs in the right hand side"expression"
.
Note:
- While property binding allows you to deal with Objects, you cannot use it to read or pull values out of target elements. Similarly, you cannot use property binding to call a method on the target element. If the element raises events, you can listen to them with an event binding.
A simple example is setting the disabled
property of a button. However, you can not only set the value of an DOM
property, but also pass in variables into a component:
1 | <!-- Evaluates the buttonDisabled property of the bound component.ts --> |
Then to use that variable passed in, in your hero-detail.component.ts
:
1 | ({ |
Property Binding Targets
Targets for property binding includes:
- DOM properties (e.g.
<img [src]="someURL">
) - Built-in directives (e.g.
<p [ngClass]="someCSSClass"></p>
)- More on this covered in Built-in Directives
- Arguments directives (e.g.
<app-my-hero [myHero]="someHero"></app-my-hero>
)- To retrieve that variable
myHero
, you need to use@Input() myHero: HeroType
- To retrieve that variable
Note:
- If you use the arguments directives, you need to make sure that the returned type in that
"someHero"
matches the type you specified in@Input() myHero: HeroType
Once additional usage of arguments directive is to not use the []
, which means the expression will not be evaluated (if you not use interpolation):
1 | <app-string-init prefix="This is a one-time initialized string."></app-string-init> |
In this case, you can still access the variable prefix
, which will have the value of whatever inside " "
.
However, if you use:
1 | <app-string-init prefix="{{some-expression}}"></app-string-init> |
Then you can also access the result of that expression (has to be a primitive), which goes back to the concept of Interpolation.
Content Security
Both interpolation and property binding could face security issues such as injecting <script>
into the html.
For example:
1 | // inside app.component.ts |
Then, if you used Interpolation:
1 | <p><span>"{{evilTitle}}" is the <i>interpolated</i> evil title.</span></p> |
You might be wondering if that script will actually be injected and executed.
Fortunately, Angular data binding is on alert for dangerous HTML. In the above case, the HTML displays as is, and the JavaScript does not execute. Angular does not allow HTML with script tags to leak into the browser, neither with interpolation nor property binding.
This means the above snippet would be rendered as:
1 | "Template <script>alert("evil never sleeps")</script> Syntax" is the interpolated evil title. |
Similarly, if you used Property Binding:
1 | <!-- |
The result would also be sanitized:
1 | "Template Syntax" is the property bound evil title. |
Class and Style Binding
- You can also add and remove CSS class names from an element’s
class
attribute with a class binding.
For more information on class binding, you can visit this section Class Binding
- If you just want to change a specific style, you can also set styles dynamically with a style binding.
To create a single style binding, start with the prefix style
followed by a dot (.
) and the name of the CSS style property (for example, [style.width]="width"
). The property will be set to the value of the bound expression, which is normally a string. Optionally, you can add a unit extension like em
or %
, which requires a number type.
For example:
1 | <!-- standard style attribute setting --> |
If there are multiple styles you’d like to toggle, you can bind to the [style]
property directly without the dot (for example, [style]="styleExpr"
). The expression attached to the [style]
binding is most often a string list of styles like "width: 100px; height: 100px;"
.
You can also format the expression as an object with style names as the keys and style values as the values, like {width: '100px', height: '100px'}
.
Binding Type | Syntax | Input Type | Example Input Values |
---|---|---|---|
Single style binding | [style.width]="width" |
`string | undefined |
Single style binding with units | [style.width.px]="width" |
`number | undefined |
Multi-style binding | [style]="styleExpr" |
string |
"width: 100px; height: 100px" |
`{[key: string]: string | undefined | ||
Array <string > |
[‘width’, ‘100px’] |
Styling Precedence
A single HTML element can have its CSS class list and style values bound to multiple sources (for example, host bindings from multiple directives).
When there are multiple bindings to the same class name or style property, Angular uses a set of precedence rules to resolve conflicts and determine which classes or styles are ultimately applied to the element.
Styling precedence (highest to lowest)
- Template bindings
- Property binding (for example,
<div [class.foo]="hasFoo">
or<div [style.color]="color">
)- Map binding (for example,
<div [class]="classExpr">
or<div [style]="styleExpr">
)- Static value (for example,
<div class="foo">
or<div style="color: blue">
)- Directive host bindings
- Property binding (for example,
host: {'[class.foo]': 'hasFoo'}
orhost: {'[style.color]': 'color'}
)- Map binding (for example,
host: {'[class]': 'classExpr'}
orhost: {'[style]': 'styleExpr'}
)- Static value (for example,
host: {'class': 'foo'}
orhost: {'style': 'color: blue'}
)- Component host bindings
- Property binding (for example,
host: {'[class.foo]': 'hasFoo'}
orhost: {'[style.color]': 'color'}
)- Map binding (for example,
host: {'[class]': 'classExpr'}
orhost: {'[style]': 'styleExpr'}
)- Static value (for example,
host: {'class': 'foo'}
orhost: {'style': 'color: blue'}
)Source: https://angular.io/guide/template-syntax#styling-precedence-highest-to-lowest
Event Binding
Event binding allows you to listen for certain events such as keystrokes, mouse movements, clicks, and touches.
Angular event binding syntax consists of a target event name within parentheses on the left of an equal sign, and a quoted template statement on the right. The following event binding listens for the button’s click events, calling the component’s onSave()
method whenever a click occurs:
1 | <button (click)="onSave()">Save</button> |
Alternatively, use the on-
prefix, known as the canonical form:
1 | <button on-click="onSave()">Save</button> |
Both has the same effect.
Event Binding Targets
Element events may be the more common targets, but Angular looks first to see if the name matches an event property of a known directive, as it does in the following example:
1 | <h4>myClick is an event on the custom ClickDirective:</h4> |
If the name fails to match an element event or an output property of a known directive, Angular reports an “unknown directive” error.
$event
and event handling statements
In an event binding, Angular sets up an event listener and handler for the target event.
When the event is raised, it is caught by the listener, and then the handler executes the template statement. The template statement typically involves a receiver, which performs an action in response to the event, such as storing a value from the HTML control into a model.
However, how do we access information stored in that event
? This is actually retrieved by using $event
.
Note:
- The target event determines the shape of the
$event
object. If the target event is a native DOM element event, then$event
is a DOM event object, with properties such astarget
andtarget.value
.
For example, we can emulate the two-way-binding without NgModel
with:
1 | <input [value]="currentItem.name" |
The above snippet does two things:
- First the property binding binds the
value
property in theinput
tocurrentItem.name
- Then, the event binding is used, and the argument passed in
$event.target
will contain information about this event.
Then your updateName()
method could look like:
1 | updateName(event: EventTarget): void{ |
If the event belongs to a directive—recall that components are directives—$event
has whatever shape the directive produces.
Creating a Custom Event
If, for a certain event, you cannot handle directly with a simple method call, you can use a EventEmitter
to pass the event to other event listeners. In summary, this can be done with:
- Create a new
EventEmitter<T>()
, whereT
would be the object that you need to later on handle (optional) - Name that
EventEmitter<T>()
with a name that you will later use to listen to - Pass the object you want to deal with later with
emit(<YourObject>)
. - Catch the event with the name that you defined at Step 2
For example, you want to delete a specific item in from your DOM, but you do not want to handle it directly there, you can:
1 | <img src="{{itemImageUrl}}" [style.display]="displayNone"> |
Then your method would look like:
1 | // This component makes a request but it can't actually delete a hero. |
The component defines a deleteRequest
property that returns an EventEmitter
. At this point, when the user clicks delete, the component invokes the delete()
method, telling the EventEmitter
to emit an Item
object with an event called deleteRequest
.
This request can later be caught with another Event Binding:
1 | <app-item-detail (deleteRequest)="deleteItem($event)" [item]="currentItem"></app-item-detail> |
Two-way Binding
The two way binding binds a specific property by allowing a two way flow. This is achieved with [()]
symbol.
In fact, as demonstrated many times before, what happens under the hood is just a Property Binding []
first to initialize and bind the property from component to template, then an Event Binding ()
to bind changes (by listening to the event <propertyChange>
) back to the property.
To demonstrate this, consider the example of resizing a text using two-way binding:
1 | <!-- Inside app.component.ts, Where fontSizePx is defined to 16 --> |
Note:
- The two way binding is bound to a property which does not seem to be emitting any event. in this case, Angular will listen to the event with name
sizeChange
, which you have to define by yourself.
Inside that <app-sizer>
, there are buttons to change the size of text by changing the firing event sizeChange
:
1 | import { Component, Input, Output, EventEmitter } from '@angular/core'; |
And this would be the corresponding html
:
1 | <div> |
Under the hood, this is what is happening:
1 | <app-sizer [size]="fontSizePx" (sizeChange)="fontSizePx=$event"></app-sizer> |
This means that, by default, if you two-way bounded: [(property)]="someValue"
, it is translated to [property]="someValue" (propertyChange)="someValue=$event"
.
Note:
- This means that the object you emit must correspond to the type of the variable you defined in the property binding (
fontSizePx
).
However, if you want to use two-way binding in forms, it will be difficult as it is hard to define an entry point to trigger an event to be fired, since there are no built-in html event for detecting changes in an input
, for example.
In this case, Angular provides solutions with its built-in directive ngForm
, which you will see later in the section below.
Built-in Directives
As mentioned before, Angular offers two kinds of built-in directives: attribute directives (modifies DOM properties) and structural directives (modifies DOM structure). The below sections will cover some of the most common built-in directives of both kinds.
Attribute Directives
The most common attribute directives include:
NgClass
- adds and removes a set of CSS classes.
NgStyle
- adds and removes a set of HTML styles.
NgModel
- adds two-way data binding to an HTML form element.
NgClass
This is essentially the Class Binding that we have talked about before. The aim of having a NgClass
directive is to simplify the code for setting and changing the CSS classes of a DOM object.
Without NgClass
directive, to change a specific class you would need to write:
1 | <!-- toggle the "special" class on/off with a property --> |
And this could be painful if you are adding multiple CSS properties.
To simplify the code, you could use NgClass
and place all your logic in your typescript
:
1 | <!-- Notice it is ngClass not NgClass when used in html --> |
And the corresponding ts
file:
1 | currentClasses: {}; |
Note:
- Though the directive is called
NgClass
, the actual directive used within the DOM property starts with a lower casen
(ngClass
).
NgStyle
Similar to NgClass
, this NgStyle
directive is used for a similar purpose of simplifying the process of editing multiple CSS styles.
Without NgStyle
directive, you could have:
1 | <div [style.font-size]="isSpecial ? 'x-large' : 'smaller'"> |
With NgStyle
, you would have:
1 | <div [ngStyle]="currentStyles"> |
and this would be the corresponding ts
file:
1 | currentStyles: {}; |
NgModel
This is a directive mainly used to solve the two-way binding problem for form elements in html, for example <input>
.
Before you use NgModel
, you need to import the FormsModule
into the respective ts
class for it to be visible:
1 | import { FormsModule } from '@angular/forms'; // <--- JavaScript import from Angular |
Then, you can use the two-way binding for <input>
like this:
1 | <input [(ngModel)]="currentItem.name" id="example-ngModel"> |
What happens under the hood is:
1 | <label>(ngModelChange)="...name=$event":</label> |
For <input>
, is can actually be achieved manually with:
1 | <label>without NgModel:</label> |
If you use
(ngModelChange)
, you could not only set a property, but also call methods. For example:1
<input [ngModel]="currentItem.name" (ngModelChange)="setUppercaseName($event)" id="example-uppercase">
and this would be the corresponding method:
1
2
3
4// notice that $event would basically be the same as $event.target.value, which is converted already by NgModel
setUppercaseName(name: string) {
this.currentItem.name = name.toUpperCase();
}
NgModel
and Value Accessors
The details are specific to each kind of element and therefore the NgModel
directive only works for an element supported by a ControlValueAccessor that adapts an element to this protocol. Angular provides value accessors for all of the basic HTML form elements and the Forms guide shows how to bind to them.
Structural Directives
Structural directives are responsible for HTML layout. They shape or reshape the DOM’s structure, typically by adding, removing, and manipulating the host elements to which they are attached.
This section is an introduction to the common built-in structural directives:
NgIf
- conditionally creates or destroys sub-views from the template.
NgFor
- repeat a node for each item in a list.
NgSwitch
- a set of directives that switch among alternative views.
The deep details of structural directives are covered in the Structural Directives guide, which explains the following:
- Why you prefix the directive name with an asterisk (*).
- Using `` to group elements when there is no suitable host element for the directive.
- How to write your own structural directive.
- That you can only apply one structural directive to an element.
NgIf
You can add or remove an element from the DOM by applying an NgIf
directive to a host element.
For example, to add/remove an element based on the field isActive
would look like:
1 | <!-- Careful that for structural directives, you need to add the * in front --> |
When the isActive
expression returns a true value, NgIf
adds the ItemDetailComponent
falseNgIf
removes the ItemDetailComponent
from the DOM, destroying/removing that component and all of its sub-components.
Note:
- Showing/hiding an element is different from adding/removing an element. It is perhaps already apparent, that removing an element would incur faster performance hence user experience than leaving the element inside the DOM but just hiding it. However, removing/adding does cause structural change, so that effect has to be controlled as well.
NgIf
and Null/Undefined
Another advantage of ngIf
is that you can use it to guard against null. Show/hide is best suited for very simple use cases, however if an expression evaluated to null, Angular will throw an error if a nested expression tries to access a property of null
.
The problem can be solved natively by using NgIf
, which basically treats Null
/Undefined
to false.
For example, the following shows NgIf
guarding a Null
value (from currentCustomer
) in a div
. The currentCustomer
name (or actually the entire div
) appears only when there is a currentCustomer
(when not null):
1 | <div *ngIf="currentCustomer">Hello, {{currentCustomer.name}}</div> |
NgFor
NgFor
is a repeater directive—a way to present a list of items. You define a block of HTML that defines how a single item should be displayed and then you tell Angular to use that block as a template for rendering each item in the list.
A simple example would be:
1 | <div *ngFor="let item of items">{{item.name}}</div> |
And the above would create a number of div
s with the name of the item
stored in items
.
NgFor
and index
If you need the index of the for loop, you could also get it from the variable index
, which stores the index of the current item (0-based).
For example:
1 | <div *ngFor="let item of items; let i=index">{{i + 1}} - {{item.name}}</div> |
NgFor
and trackby
One potential problem with NgFor
is mutation: if an entry in the large list (items
, for example) changed, then you might need to make a re-query the database. This would be costly if the update is minimal yet the list is large.
The solution is to use trackBy
, which uses a custom method that returns a certain id
to tell Angular which item to re-render and which ones not to.
For example, your custom trackBy()
method could look like this:
1 | trackByItems(index: number, item: Item): number { |
Then your html would look like:
1 | <div *ngFor="let item of items; trackBy: trackByItems"> |
For more details with using trackBy
, please visit https://angular.io/guide/template-syntax#ngfor-with-trackby
NgSwitch
When used, this technically refers to the combination of NgSwitch
(attribute directive), NgSwitchCase
, and NgSwitchDefault
(structural directives).
Therefore, you use:
ngSwitch
as bindings to a switch value, which can be data of any type (e.g.string
)*ngSwitchCase
and *ngSwitchDefault
are structural directives as they add/remove elements in the DOM.
For example:
1 | <div [ngSwitch]="currentItem.feature"> |
Template Reference Variables
A template reference variable is often a reference to a DOM element within a template. It can also refer to a directive (which contains a component), an element, TemplateRef, or a web component.
Use the hash symbol (#) to declare a reference variable. The following reference variable, #phone
, declares a phone
variable on an <input>
element.
1 | <input #phone placeholder="phone number" /> |
You can refer to a template reference variable anywhere in the component’s template. The scope of those variables is the entire template:
1 | <input #phone placeholder="phone number" /> |
Alternatively, you can use the ref-
prefix alternative to #
:
1 | <!-- This has the same effect as above --> |
How a reference variable gets its value
In most cases, Angular sets the reference variable’s value to the element on which it is declared. In the previous example, phone
refers to the phone number <input>
. The button’s click handler passes the <input>
value to the component’s callPhone()
method.
The NgForm
directive can change that behavior and set the value to the NgForm
directive:
1 | <!-- Reference to the ngForm directive --> |
with NgForm
, itemForm
is a reference to the NgForm directive with the ability to track the value and validity of every control in the form.
The native <form>
element doesn’t have a form
property, but the NgForm
directive does, which allows disabling the submit button if the itemForm.form.valid
is invalid and passing the entire form control tree to the parent component’s onSubmit()
method.
@Input()
and @Output
@Input()
and @Output()
allow Angular to share data between the parent context and child directives or components. An @Input()
property is writable while an @Output()
property is observable.
For example, consider the following structure:
1 | <parent-component> |
In order for the child to communicate with the parent, you would need to use @Input()
or @Output()
. You can think of @Input()
and @Output()
like ports or doorways—@Input()
is the doorway into the component allowing data to flow in while @Output()
is the doorway out of the component, allowing the child component to send data out.
Using @Input
Use the @Input()
decorator in a child component or directive to let Angular know that a property in that component can receive its value from its parent component. It helps to remember that the data flow is from the perspective of the child component. So an @Input()
allows data to be input into the child component from the parent component.
To illustrate the use of @Input()
, these parts of your app will be edited:
- The child component class and template
- The parent component class and template
Inside the Child
In summary, you will need to add:
- Import the
Input
from,@angular/core
- Use
@Input() <variableName>:<type>
to that it can be accessed inside your component
For example, it might look like this:
1 | import { Component, Input } from '@angular/core'; // First, import Input |
Note:
- The
<variableName>
has to be the same as the variable name in the html template, which you will see below.
Then in order to pass in the variable, you can use the format of Property Binding in your html template:
1 | <app-item-detail [item]="currentItem"></app-item-detail> |
Inside the Parent
In summary, here you do not need to do much:
- All you need to do is to make a certain variable/method available in the template
Changes will only go to your html template where you will need to pass in the variable to the child component. This has been already shown above:
1 | <!-- Inside the template of app-component-ts --> |
where the app-item-detail
is the child component.
A diagram from the official site shows the overall structure:
Note:
- To watch for changes on an
@Input()
property, useOnChanges
, one of Angular’s lifecycle hooks.OnChanges
is specifically designed to work with properties that have the@Input()
decorator. See theOnChanges
section of the Lifecycle Hooks guide for more details and examples.
Using @Output
Use the @Output()
decorator in the child component or directive to allow data to flow from the child out to the parent.
Just like with @Input()
, you can use @Output()
on a property of the child component but its type should be EventEmitter
. (The child component then has to raise an event so the parent knows something has changed. To raise an event, @Output()
works hand in hand with EventEmitter
, which is a class in @angular/core
that you use to emit custom events.)
When you use @Output()
, edit these parts of your app:
- The child component class and template
- The parent component class and template
The following example features an <input>
where a user can enter a value and click a <button>
that raises an event. The EventEmitter
then relays the data to the parent component.
Inside the Child
In summary, you need to:
- Import
Output
andEventEmitter
- Decorate a property with
@Output()
, with typeEventEmitter<YourType>
- Create a method (or anything) that can emit an event
For example, your ts
would look like this:
1 | import { Output, EventEmitter } from '@angular/core'; |
Then, its html page could look like:
1 | <label>Add an item: <input #newItem></label> |
Inside the Parent
In this case, since what actually comes through is the event
object, you don’t need to use @Input
.
In summary, all you need to do is:
- Create a method (or anything) that uses
$event
which comes from the child - Edit your html page to get that event
For example, to get that newItemEvent
, your html would look like:
1 | <!-- Inside your app.component.html --> |
Your ts
for the parent component could look like:
1 | export class AppComponent { |
Using @Input
and @Output
Together
Combining the above two, you can basically have a child process properties from its parent and emit the changes back.
In summary, you need to:
- Input to a child component with Argument Binding
- Use that inputted variable with the
@Input
(and do some operations with it) - Create a
EventEmitter
and a method that emits changed result - Output that
EventEmitter
with Event Binding
An image should also summarize the information above:
To combine property and event bindings using the banana-in-a-box syntax, [()]
, see Two-way Binding.
Using Inputs and Outputs Metadata
If you have multiple properties and EventEmitters
that you need to input/output, you can use the inputs:
and outputs:
metadata. For example:
1 | // tslint:disable: no-inputs-metadata-property no-outputs-metadata-property |
Aliasing Inputs and Outputs
If, due to name collisions, you want to have a different name in your ts
than the names you inputted/outputted from/to your template, you could use aliasing.
Aliasing with @Input()
and @Output()
An example illustrates this the fastest:
1 | 'wishListItem') input2: string; // @Input(alias) ( |
Aliasing with inputs
and outputs
Metadata
With inputs
and outputs
metadata, you need to use :
1 | // tslint:disable: no-inputs-metadata-property no-outputs-metadata-property |
User Input/Event
User actions such as clicking a link, pushing a button, and entering text raise DOM events. This section explains how to bind those events to component event handlers using the Angular event binding syntax.
Binding DOM Events
You can use Angular event bindings to respond to any DOM event. Many DOM events are triggered by user input. Binding to these events provides a way to get input from the user.
To bind to a DOM event, surround the DOM event name in parentheses and assign a quoted template statement to it. All of these has been covered in the section Event Bindings
Getting Information Stored in $event
One of the most important step of interacting with user events is to get information of what the user has done.
In summary, all the information of a event (emitted either by a standard html event or a customized EventEmitter
) is encapsulated in the $event
DOM object.
For instance:
1 | template: ` |
In this case, the keyup
event is handled and passed to onKey()
method.
Now, for a standard html event, the event
object will have a heavy payload:
- All standard DOM event objects have a
target
property, a reference to the element that raised the event. - And information (such as the text inputted in the example above) can be retrieved form that
target
property
For instance, to get the string
input, you need to access the event.target.value
property:
1 | export class KeyUpComponent_v1 { |
Note:
- The properties of an
$event
object vary depending on the type of DOM event. For example, a mouse event includes different information than an input box editing event. Sometimes, in order to access a specific property, you need to typecast the event toHTMLInputElement
to access thatvalue
property.
A better coding style of the above would be:
1 | export class KeyUpComponent_v1 { |
Using Template Variables
You can also achieve the same thing with a template variable. However, you need to be careful as Angular will only reload when an asynchronous event happened.
A naïve approach that does not work because Angular does not update the template is:
1 | ({ |
To make it work, you can either emit an empty event so that Angular updates:
1 | ({ |
Or, the better approach would be:
1 | ({ |
Key Event Filtering
The (keyup)
event handler hears every keystroke. Sometimes only the Enter key matters, because it signals that the user has finished typing. One way to reduce the noise would be to examine every $event.keyCode
and take action only when the key is Enter.
There’s an easier way: bind to Angular’s keyup.enter
pseudo-event. Then Angular calls the event handler only when the user presses Enter.
1 | ({ |
However, what if you also want to save the data when the user clicked away before finishing typing and pressing enter? In this case, you need to also listen to the blur
event, which is introduced below.
Blur
Another event which is quite useful for dealing with inputs is blur
, which is the opposite of focus
, meaning that the user has clicked away from the element.
Therefore, to solve the previous problem, you can add another event binding with(blur)
to handle this:
1 | ({ |
Pipes
Pipes can be used to transform and format strings, currency amounts, dates, and other display data. Pipes are simple functions you can use in template expressions to accept an input value and return a transformed value.
Angular provides built-in pipes for typical data transformations, including transformations for internationalization (i18n), which use locale information to format data. The following are commonly used built-in pipes for data formatting:
DatePipe
: Formats a date value according to locale rules.UpperCasePipe
: Transforms text to all upper case.LowerCasePipe
: Transforms text to all lower case.CurrencyPipe
: Transforms a number to a currency string, formatted according to locale rules.DecimalPipe
: Transforms a number into a string with a decimal point, formatted according to locale rules.PercentPipe
: Transforms a number to a percentage string, formatted according to locale rules.
- For a complete list of built-in pipes, see the pipes API documentation.
- To learn more about using pipes for internationalization (i18n) efforts, see formatting data based on locale.
Using a Pipe in a Template
To apply a pipe in a template, use the pipe operator (|
) within a template expression. The basic usage has been shown in the section pipe operator (|
), but it is shown here again just to demonstrate its simplicity:
1 | <!-- where birthday = new Date(1988, 3, 15); is defined in the app.component.ts --> |
Formatting Pipe Output
Some pipes have optional parameters that you can use to fine-tune the pipe’s output. To specify the optional parameter, follow the pipe name (currency
) with a colon (:
) and the parameter value ('EUR'
).
- For example, you can use the
CurrencyPipe
with a country code such as EUR as a parameter. The template expression {{ amount | currency:'EUR' }} transforms theamount
to currency in euros.
If the pipe accepts multiple parameters, separate the values with colons.
- For example, {{ amount | currency:'EUR':'Euros '}} adds the second parameter, the string literal
'Euros '
, to the output string. You can use any valid template expression as a parameter, such as a string literal or a component property.
For a full list of built-in Angular pipes with their optional parameters, please visit: https://angular.io/api/common#pipes
A complete example would be:
1 | <p>The hero's birthday is {{ birthday | date:"MM/dd/yy" }} </p> |
Or, if you want to specify options:
1 | template: ` |
For a full list of date pipe optional parameters, please visit: https://angular.io/api/common/DatePipe
Chained Pipes
You can also chain pipes so that the output of one pipe becomes the input to the next (going from left to right).
For example:
1 | <p> |
In the above example, chained pipes first apply a format to a date value, then convert the formatted date to uppercase characters.
Making a Custom Pipe
First, we need to briefly go through how Angular finds and uses a pipe.
Pipes are actually just classes annotated with @Pipe
, implementing the PipeTransform
interface. Whenever a pipe is detected in a template, Angular will go to that annotation and execute the transform
method defined within the class.
This also provides hints for us on how to create a custom pipe function.
Creating a Class as a Pipe
In summary, you need to have a class that:
- Implements the
PipeTransform
interface - Is Annotated with
@Pipe()
For example:
1 | import { Pipe, PipeTransform } from '@angular/core'; |
Detecting changes with data binding in pipes
When you use pipes, you use data binding with a pipe to display values and respond to user actions. If the data is a primitive input value, such as String
or Number
, or an object reference as input, such as Date
or Array
, Angular executes the pipe whenever it detects a change for the input value or reference.
For example, if you edited the above example to make a two-way binding:
1 | import { Component } from '@angular/core'; |
The exponentialStrength
pipe executes every time the user changes the “normal power” value or the “boost factor”, as shown below.
However, if you change something inside a composite object (such as the month of a date, an element of an array, or an object property), you need to understand how change detection works, and how to use an impure
pipe.
Change Detection Mechanism
For elements except pipes, change detection in Angular is based on every DOM event. This means that Angular updates the page based on things such as keystrokes, mouse clicks, server response, and etc.
An example demonstrating the above has been shown in the section Using Template Variables, and here we show another example.
Consider the html page that looks like this:
1 | New hero: |
We see that the event that will get handled is keyup.enter
, which will add the value to the heroes
list. Now, since Angular updates its elements whenever there is an event, the *ngFor
will update itself automatically.
The following would be the corresponding ts
code:
1 | export class FlyingHeroesComponent { |
Change Detection for Pipes
In the section Detecting changes with data binding in pipes, you have seen that Angular seems to update/re-run pipes as well when variables/anything you had in the data binding have changed. However, this is not true for all the cases.
For detecting changes with pipes, Angular uses the approach by detecting whether if the variable’s reference has changed. This means for primitives types, this would work nicely, but for complex objects, changing one of its many property fields will not change the object’s reference, hence Angular will not update/re-run the pipe.
For example, if you changed the previous example to:
1 | New hero: |
With its corresponding pipe
class:
1 | import { Pipe, PipeTransform } from '@angular/core'; |
You will see that things don’t work since (heroes | flyingHeroes)
will not get re-run and the same heroes gets ngFor
ed. This is because the addHero()
method we have seen pushes new hero
to the heroes
list, which does not change the heroes
list’s reference.
To solve the issue, you can either:
- Change your
addHero()
method to create a new array and replaceheroes
to that new array- Since this changed the reference,
(heroes | flyingHeroes)
will get re-run and new data will be seen
- Since this changed the reference,
- Use an
impure
pipe
impure
Pipe
pure
changes means changes that causes changes to the variable’s references, which is basically what’s mentioned above.
impure
changes would be changes that are within a composite object.
Therefore, in order to solve the problem mentioned above, we need to create an impure
pipe that deals with impure
changes.
In summary, you need to:
- Create a pipe class as usual, with its
@Pipe()
annotation and implementing thePipeTransform
interface - Add the property
pure: false
in the@Pipe()
annotation
For example:
1 | ({ |
And a complete example would look like:
1 | /* tslint:disable use-pipe-transform-interface */ |
Lastly, don’t forget to change your html to use the impure
pipe:
1 | <div *ngFor="let hero of (heroes | flyingHeroesImpure)"> |
async
Pipe
The async
pipe is an impure
pipe, which is used commonly for handling event handling, asynchronous programming, and handling multiple values.
The built-in AsyncPipe
to accept an observable
as input and subscribe
to the input automatically. Without this pipe, your component code would have to subscribe to the observable to consume its values, extract the resolved values, expose them for binding, and unsubscribe when the observable is destroyed in order to prevent memory leaks. AsyncPipe
is an impure pipe that saves boilerplate code in your component to maintain the subscription and keep delivering values from that observable as they arrive.
Observables provide support for passing messages asynchronously between parts of your application (also between another server and your application, which we will discuss in Accessing Servers Over HTTP)
For details and examples of observables, see the Observables Overview.
The following code example binds an observable of message strings (message$
) to a view with the async
pipe:
1 | import { Component } from '@angular/core'; |
Caching HTTP Request
Another example of using observables
is the HTTPClient
service, which uses observables and offers the HTTPClient.get()
method to fetch data from a server. The asynchronous method get()
sends an HTTP request, and returns an observable that emits the requested data for the response.
The following code from Angular mimics an async data sending and processes it with async
pipe:
1 | import { HttpClient } from '@angular/common/http'; |
The corresponding parent component is:
1 | import { Component } from '@angular/core'; |
The built-in JsonPipe provides a way to diagnose a mysteriously failing data binding or to inspect an object for future binding.
Lifecycle Hooks
A component instance has a lifecycle that starts when Angular instantiates the component class and renders the component view along with its child views. The lifecycle continues with change detection, as Angular checks to see when data-bound properties change, and updates both the view and the component instance as needed. The lifecycle ends when Angular destroys the component instance and removes its rendered template from the DOM.
Directives have a similar lifecycle, as Angular creates, updates, and destroys instances in the course of execution.
Your application can use lifecycle hook methods to tap into key events in the lifecycle of a component or directive in order to initialize new instances, initiate change detection when needed, respond to updates during change detection, and clean up before deletion of instances. This can be achieved by implementing one or more of the lifecycle hook interfaces in the Angular core
library.
For example, to tap into the hook method named ngOnInit()
(so that Angular calls it shortly after checking the input properties for that component or directive for the first time):
1 | () |
Note:
- Each interface defines the prototype for a single hook method, whose name is the interface name prefixed with
ng
. For example, theOnInit
interface has a hook method namedngOnInit()
, as shown above.
Lifecycle Event Sequence
After your application instantiates a component or directive by calling its constructor, Angular calls the hook methods you have implemented at the appropriate point in the lifecycle of that instance.
Angular executes hook methods in the following sequence. You can use them to perform the following kinds of operations:
Hook method | Purpose | Timing |
---|---|---|
ngOnChanges() |
Respond when Angular sets or resets data-bound input properties. The method receives a SimpleChanges object of current and previous property values. Note that this happens very frequently, so any operation you perform here impacts performance significantly. See details in Using change detection hooks in this document. |
Called before ngOnInit() and whenever one or more data-bound input properties change. |
ngOnInit() |
Initialize the directive or component after Angular first displays the data-bound properties and sets the directive or component’s input properties. See details in Initializing a component or directive in this document. | Called once, after the first ngOnChanges() . |
ngDoCheck() |
Detect and act upon changes that Angular can’t or won’t detect on its own. See details and example in Defining custom change detection in this document. | Called immediately after ngOnChanges() on every change detection run, and immediately after ngOnInit() on the first run. |
ngAfterContentInit() |
Respond after Angular projects external content into the component’s view, or into the view that a directive is in.See details and example in Responding to changes in content in this document. | Called once after the first ngDoCheck() . |
ngAfterContentChecked() |
Respond after Angular checks the content projected into the directive or component.See details and example in Responding to projected content changes in this document. | Called after ngAfterContentInit() and every subsequent ngDoCheck() . |
ngAfterViewInit() |
Respond after Angular initializes the component’s views and child views, or the view that contains the directive.See details and example in Responding to view changes in this document. | Called once after the first ngAfterContentChecked() . |
ngAfterViewChecked() |
Respond after Angular checks the component’s views and child views, or the view that contains the directive. | Called after the ngAfterViewInit() and every subsequent ngAfterContentChecked() . |
ngOnDestroy() |
Cleanup just before Angular destroys the directive or component. Unsubscribe Observables and detach event handlers to avoid memory leaks. See details in Cleaning up on instance destruction in this document. | Called immediately before Angular destroys the directive or component. |
(Table source: https://angular.io/guide/lifecycle-hooks#lifecycle-event-sequence)
Using ngOnInit
Use the ngOnInit()
method to perform the following initialization tasks.
Perform complex initializations outside of the constructor. Components should be cheap and safe to construct. You should not, for example, fetch data in a component constructor. You shouldn’t worry that a new component will try to contact a remote server when created under test or before you decide to display it.
An
ngOnInit()
is a good place for a component to fetch its initial data. For an example, see the Tour of Heroes tutorial.In Flaw: Constructor does Real Work, Misko Hevery, Angular team lead, explains why you should avoid complex constructor logic.
Set up the component after Angular sets the input properties. Constructors should do no more than set the initial local variables to simple values.
Keep in mind that a directive’s data-bound input properties are not set until after construction. If you need to initialize the directive based on those properties, set them when
ngOnInit()
runs.The
ngOnChanges()
method is your first opportunity to access those properties. Angular callsngOnChanges()
beforengOnInit()
, but also many times after that. It only callsngOnInit()
once.
Using ngOnDestroy
Put cleanup logic in ngOnDestroy()
, the logic that must run before Angular destroys the directive.
This is the place to free resources that won’t be garbage-collected automatically. You risk memory leaks if you neglect to do so.
- Unsubscribe from Observables and DOM events.
- Stop interval timers.
- Unregister all callbacks that the directive registered with global or application services.
The ngOnDestroy()
method is also the time to notify another part of the application that the component is going away.
Using ngOnChanges
Angular calls the ngOnChanges()
method of a component or directive whenever it detects changes to the input properties. The following example demonstrates this by monitoring the OnChanges()
hook.
1 | ngOnChanges(changes: SimpleChanges) { |
The ngOnChanges()
method takes an object that maps each changed property name to a SimpleChange object holding the current and previous property values. This hook iterates over the changed properties and logs them.
For all the other Lifecycle Hooks not mentioned above, please visit https://angular.io/guide/lifecycle-hooks#general-examples
Forms for User Input
Handling user input with forms is the cornerstone of many common applications. Applications use forms to enable users to log in, to update a profile, to enter sensitive information, and to perform many other data-entry tasks.
Angular provides two different approaches to handling user input through forms: reactive and template-driven. Both capture user input events from the view, validate the user input, create a form model and data model to update, and provide a way to track changes.
Reactive forms and template-driven forms process and manage form data differently. Each approach offers different advantages.
- Reactive forms provide direct, explicit access to the underlying forms object model. Compared to template-driven forms, they are more robust: they’re more scalable, reusable, and testable. If forms are a key part of your application, or you’re already using reactive patterns for building your application, use reactive forms.
- Template-driven forms rely on directives in the template to create and manipulate the underlying object model. They are useful for adding a simple form to an app, such as an email list signup form. They’re easy to add to an app, but they don’t scale as well as reactive forms. If you have very basic form requirements and logic that can be managed solely in the template, template-driven forms could be a good fit.
The following sections only covers using reactive forms. If you need information on using template-driven forms, please visit https://angular.io/guide/forms-overview for more information.
Common Form Foundation Classes
Both reactive and template-driven forms are built on the following base classes.
FormControl
tracks the value and validation status of an individual form control.FormGroup
tracks the same values and status for a collection of form controls.FormArray
tracks the same values and status for an array of form controls.ControlValueAccessor
creates a bridge between AngularFormControl
instances and native DOM elements.
Setting Up Reactive Forms
In general, all the information will be sent to the FormControl
object that you will create. So, to use reactive forms, you need to:
Register the
ReactiveFormsModule
in your app module (e.g.app.module.ts
).- This module declares the reactive-form directives that you need to use reactive forms.
Import the necessary module and create a
FormControl
object in yourts
Bind to the directive
formControl
in your html template with the object you created in the step above
For example, to register the ReactiveFormsModule
(in order to use the FormControl
) directive, you can do:
1 | import { ReactiveFormsModule } from '@angular/forms'; |
Then, you can use the formControl
directive in your html with:
1 | <label> |
And that name
would be:
1 | import { Component } from '@angular/core'; |
Using Reactive Forms
Now, the key comes to accessing the value that is stored in that FormControl
object.
You can access the value in the following ways.
- Through the
valueChanges
observable where you can listen for changes in the form’s value in the template usingAsyncPipe
or in the component class using thesubscribe()
method. - With the
value
property, which gives you a snapshot of the current value.
The following example shows you how to display the current value using interpolation in the template.
1 | <p> |
The displayed value changes as you update the form control element, since changing the input will trigger input
events that will be handled by formControl
directive.
In general, reactive forms provide access to information about a given control through properties and methods provided with each instance. These properties and methods of the underlying AbstractControl class are used to control form state and determine when to display messages when handling input validation.
Read about other FormControl
properties and methods in the API Reference.
Grouping Form Controls
The above reactive form examples is fine when there is only a limited number of inputs. However, when you are dealing with a form with quite a few input fields, or even some nested input structures, you should try to use a FormGroup
together with FormControl
s to manage multiple inputs cleanly.
In summary, when you want to group multiple inputs, you need to:
- Create a top level
FormGroup
that contains all the information of its children - Create children elements within that top level
FormGroup
- For
FormControl
s, you create them withformControlName="someName"
- For nested
FormGroup
s, you create them withformGroupName="someOtherName"
- For
- Create a
FormGroup
object with the name defined in step 1 in yourts
, and pass in a map reflecting the same structure as step 2 into its constructor - Most of the time you want to save that data and use it later. So you also need to create a button (or anything else) that emits a
submit
event, and you can save all the data and handle them with(ngSubmit)="onSubmit()"
- all the data will be stored in the
FormGroup
object you created in step 3. The data will be package into a map, where the key will be either thefromControlName
or theformGroupName
- all the data will be stored in the
- *You can also add validators for your form, this will be touched on in the section Validate Form Input
Example: Simple Grouping Form
First, you create the form structure in your html:
1 | <!-- Notice that only the top level needs binding, since all the control/information of its children are passed to it --> |
Then, in your ts
, you create that FormGroup
object with name profileForm
, and pass in the same children hierarchy into its constructor:
1 | import { Component } from '@angular/core'; |
Then, if you want to save the value, you need to add a button (or anything else) that can emit a submit
event.
Luckily, this event can be emitted natively by html with <button type="submit">
:
1 | <!-- (ngSubmit) listens for the submit event and triggers onSubmit() method --> |
Now, for most of the cases, your data handling would be outside of this component where you define all the form actions. This means you will probably use an EventEmitter
and @Output
that to other components for handling.
Here, we just show you how to access the data of the forms:
1 | onSubmit() { |
And the profile.value
will be a map that looks like:
1 | {firstName: "user's input", lastName: "user's input"} |
Example: Complex Grouping Form
If you have nested FormGroups
within a top level form group, most of the logics will be the same as above.
For example, to also get address information besides name, you can nest another formGroup
inside the top level form:
1 | <form [formGroup]="profileForm"> |
Then the corresponding ts
would look like:
1 | import { Component } from '@angular/core'; |
Then the rest is the same (e.g. add a submit
button). However, since you have nested another formGroup
, the value you get from profileForm.value
will be a nested map as well:
1 | {firstName: "user's input", lastName: "user's input", address: {street: "user's input", ...}} |
Update Values in the Form Group
When you have a single FormControl
, you can simply change the values in that input by changing the value
property. However, now you have a FormGroup
that collects all its power from its child FormControl
and FormGroup
. To update/change the value of a specific child under the top level FormGroup
, you can use the methods setValue()
or patchValue()
of the FormGroup
object:
setValue()
- sets a new value for the entire
FormGroup
object, meaning that you need to set all of its childrenFormControl
orFormGroup
s. Sometimes this will not be necessary, but it can be useful since it will explicitly throw an error when the structure you provided in thesetValue()
method differs from theFormGroup
structure
- sets a new value for the entire
patchValue()
- patches specific values for the
FormGroup
object, meaning that you do not need to set all of its children. However, if there is a mismatch between the structure you provided inpatchValue()
and theFormGroup
in html, it will fail silently.
- patches specific values for the
For example, to update only the firstName
and street
information in the above form:
1 | updateProfile() { |
However, if you want to use setValue()
:
1 | updateProfile() { |
Using FormBuilder
Service
Angular also provides a builder
class that can simplify the syntax for creating complex forms. The FormBuilder
has three methods: control()
, group()
, array()
, which generates form controls, form groups, and form arrays (see Creating Dynamic Forms).
In summary, you need to:
- Import the
FormBuilder
from@angular/forms
- Use
group
to create form groups,control
to create form control,array
to create form arrays.
For example, the same form above would be refactored to be:
1 | import { Component } from '@angular/core'; |
Note:
- Notice that the value for each control name is an array containing the initial value as the first item in the array. You can define the control with just the initial value, but if your controls need sync or async validation, add sync and async validators as the second and third items in the array.
Validate Form Input
Form validation is used to ensure that user input is complete and correct. This section covers adding a single validator to a form control and displaying the overall form status. Form validation is covered more extensively in the Form Validation guide.
In summary, to add a validator, you need to:
- Import a validator function (e.g.
Validators.required
from@angular/forms
) - Add that validator function to the field(s) of your form
- *Add logic to handle the validation status
- There is a native
status
property of a form object, which will beinvalid
if any of its children isinvalid
- There is a native
For example, to make the field firstName
required, you can use the Validators.required
function:
1 | import { Validators } from '@angular/forms'; |
Alternatively, since there is also a native required
validator in html, the following has the same effect:
1 | <input type="text" formControlName="firstName" required> |
Then, you can see the status with:
1 | <p> |
which will be invalid
if firstName
is not filled, or simply with:
1 | <p> |
which will be false
if firstName
is not filled.
So a simple usage would be disabling the submit button if the status is invalid
:
1 | <button type="submit" [disabled]="!profileForm.valid">Submit</button> |
Creating Dynamic Forms with Form Array
FormArray
is an alternative to FormGroup
for managing any number of unnamed controls. As with form group instances, you can dynamically insert and remove controls from form array instances, and the form array instance value and validation status is calculated from its child controls. However, you don’t need to define a key for each control by name, so this is a great option if you don’t know the number of child values in advance.
To define a dynamic form, take the following steps.
- Import the
FormArray
class. - Define a
FormArray
control. - Access the
FormArray
control with a getter method, so that you could access its fields or populate it- You can populate the
FormArray
control withFormControl
instances orFormGroup
instances bypush()
to that array
- You can populate the
- Display the form array in a template.
The following uses a basic approach of creating a dynamic form using a form array. For more information on creating a dynamic form, please visit https://angular.io/guide/dynamic-form#building-dynamic-forms.
For example:
Your html would look like:
1 | <form [formGroup]="profileForm" (ngSubmit)="onSubmit()"> |
where aliases.controls
would return a list of FormControls
that is stored in the array.
Your ts
would look like:
1 | import { FormArray } from '@angular/forms'; |
Then, to get that form array, you can do:
1 | get aliases() { |
Now, once you have that FormArray
, you can manipulate it, such as populating it with more FormControls
:
1 | addAlias() { |
Lastly, if you look at the profileForm.values
, you will see that the aliases
will have data stored in an array:
1 | { "firstName": "", "lastName": "", "address": { "street": "", ...}, "aliases": [ "" ] } |
Accessing Servers Over HTTP
Most front-end applications need to communicate with a server over the HTTP protocol, in order to download or upload data and access other back-end services. Angular provides a simplified client HTTP API for Angular applications, the HttpClient
service class in @angular/common/http
.
The HTTP client service offers the following major features.
- The ability to request typed response objects.
- Streamlined error handling.
- Testability features.
- Request and response interception.
Requesting Data from Server with GET
Use the HTTPClient.get()
method to fetch data from a server. The asynchronous method sends an HTTP request, and returns an Observable that emits the requested data when the response is received.
Note:
- In order for this to work with servers on a different domain, you either need to have CORS protocol enabled (for Spring Boot, this can be done simply with
@CrossOrigin(origins = {"<other domain address>"})
, or use the JSONP request.
The get()
method takes two arguments; the endpoint URL from which to fetch, and an options object that you can use to configure the request.
The options
object contains fields like this:
1 | options: { |
Important options include the observe
and responseType
(params
is useful when you have query strings) properties.
- The
observe
option specifies how much of the response to return. - The
responseType
option specifies the format in which to return data.
Applications often request JSON data from a server. If you want to get JSON data, you need to do:
- specify a
URL
that maps to the get request in your backend - specify the
options
with{observe: 'body', responceType: 'json'}
- There are actually the default value as well. So if you specified nothing (did not provide the
options
), it works as well.
- There are actually the default value as well. So if you specified nothing (did not provide the
For example:
You can get JSON data (e.g. Objects) with:
1 | configUrl = 'assets/someQuery'; |
which is equivalent to:
1 | configUrl = 'assets/someQuery'; |
Now, because the service method returns an Observable
of configuration data, you need to subscribe to the method’s return value in order to see results.
The subscription callback performs minimal post-processing. It copies the data fields into the component’s config
object, which is data-bound in the component template for display.
1 | private config: Config; |
Requesting a Typed Response
The above takes quite some effort in the end to typecast to the desired object you need. While this is fine, you could make the data handling easier by declaring the type of the response object with get<Type>()
.
In summary, you need to:
- Create an
interface
rather thanclass
so that JSON can be converted to an instance of theinterface
- Supply that
interface
to theget<TypeHere>()
For example, if you have a Config
interface for the examples above:
1 | export interface Config { |
Then you can do:
1 | // this should be in one of your service class |
This can simplify your code in your component:
1 | config: Config; |
Reading a Full Response
The above example only reads the body
of a html response. However, a full response contain information such as headers
, status
, statusText
, url
, ok
, type
, and finally body
.
To get the full response, we need to change to observe: 'response'
instead of the default observe: 'body'
.
OBSERVE AND RESPONSE TYPES
The types of the
observe
andresponse
options are string unions, rather than plain strings.
1
2
3
4
5
6
7 options: {
...
observe?: 'body' | 'events' | 'response',
...
responseType?: 'arraybuffer'|'blob'|'json'|'text',
...
}This can cause confusion. For example:
1
2
3
4
5
6
7
8 // this works
client.get('/foo', {responseType: 'text'})
// but this does NOT work
const options = {
responseType: 'text',
};
client.get('/foo', options)In the second case, TypeScript infers the type of
options
to be{responseType: string}
. The type is too wide to pass toHttpClient.get
which is expecting the type ofresponseType
to be one of the specific strings.HttpClient
is typed explicitly this way so that the compiler can report the correct return type based on the options you provided.Use
as const
to let TypeScript know that you really do mean to use a constant string type:
1
2
3
4 const options = {
responseType: 'text' as const,
};
client.get('/foo', options);
So, if you need more information about the transaction than is contained in the response body (e.g. sometimes servers return special headers or status codes to indicate certain conditions that are important to the application workflow), you can have:
1 | // notice the HttpResponse<Config> object |
Now HttpClient.get()
returns an Observable
of type HttpResponse
rather than just the JSON data contained in the body.
Requesting Non-JSON Data
Not all APIs return JSON data. In this next example, a DownloaderService
method reads a text file from the server and logs the file contents, before returning those contents to the caller as an Observable<string>
.
For example:
1 | getTextFile(filename: string): Observable<string> { |
where:
- The
RxJS
tap
operator (as in “wiretap”) lets the code inspect both success and error values passing through the observable without disturbing them.data
would be the data returned from server if the request is successfulerror
would be the error message if the request is not successful
Now HttpClient.get()
returns a string rather than the default JSON because of the responseType
option, and you can easily use it with:
1 | private contents: string; |
Handling Request Errors
If the request fails on the server, HttpClient
returns an error object instead of a successful response.
The same service that performs your server transactions should also perform error inspection, interpretation, and resolution.
When an error occurs, you can obtain details of what failed in order to inform your user. In some cases, you might also automatically retry the request. The following section explores how you could achieve those.
Getting Error Details
In general, there are two types of errors can occur.
- The server backend might reject the request, returning an HTTP response with a status code such as 404 or 500. These are error responses.
- Something could go wrong on the client-side such as a network error that prevents the request from completing successfully or an exception thrown in an RxJS operator. These errors produce JavaScript
ErrorEvent
objects.
HttpClient
captures both kinds of errors in its HttpErrorResponse
. You can inspect that response to identify the error’s cause.
In summary, you need to:
Create a custom error handling method, which takes a
HttpErrorResponce
object. You can include your own logic there.- You should throw a customized Object that contains the error information with
throwError(<YourErrorObject>)
, so that you could also display on the page later
- You should throw a customized Object that contains the error information with
Pass this function to the
catchErorr
operator in thepipe
right after theget
method.- For more information of what is happening in the
pipe
, please visit: https://angular.io/guide/rx-library#error-handling
- For more information of what is happening in the
*Use the error object you thrown to display some messages on the page for users to see
For example, a simple error handling method could look like:
1 | private handleError(error: HttpErrorResponse): Observable<never>{ |
The above example basically stores information in a single string. In your own application, you can create your own ErrorObject
and store formatted information there.
Then, for the get()
method to use this, you need to have:
1 | getConfig() { |
Now, if an error actually occurs when you call the get
request, it will be handled by your handleError
function.
Lastly, if you want to use the error information that you have thrown with throwError(<whatever you have here>)
, you could:
1 | errorRequest(request: string): void{ |
Now, the error object that you have thrown before will be caught by subscribe
and stored in randomQueryError
, which then you could display by binding it to your html template.
Retrying a Failed Request
Sometimes the error is transient and goes away automatically if you try again. For example, network interruptions are common in mobile scenarios, and trying again can produce a successful result.
The RxJS library offers several retry operators. For example, the retry()
operator automatically re-subscribes to a failed Observable
a specified number of times. Re-subscribing to the result of an HttpClient
method call has the effect of reissuing the HTTP request.
In summary, you need to:
- Add the operator
retry(<number of retries>)
to thepipe
- This should go before
catchError
, since operators are executed sequentially
- This should go before
For example, to issue retries before deciding to catch the error, you could:
1 | getConfig() { |
Sending Data to the Server
In addition to fetching data from a server, HttpClient
supports other HTTP methods such as PUT, POST, and DELETE, which you can use to modify the remote data.
Making a POST Request
Apps often send data to a server with a POST request when submitting a form.
Note:
- Data that you want to send using a POST request has a similar format to a GET request, in which both uses the key-value pair separated with
&
to pass data. However, POST request stores those information in the request header, yet GET request goes directly in the URL.
In summary, to send a POST request containing the data you want to post, you need to:
- Have the data you want to post ready
- It can be an Object, for example
- Use the
post<ExpectedResultObject>(URL, <YourDataObject>, httpOptions)
- Same as
get()
method,post()
also returns anObservable
- Same as
- Actually fire the post event by using
subscribe
, which you have done withget()
as well
For example:
Your service class (that does all the backend communication) could look like:
1 | /** POST: add a new hero to the database */ |
From the above, you see the post()
method takes a resource URL and two additional parameters:
- body - The data to POST in the body of the request.
- options` - An object containing method options which, in this case, specify required headers.
Lastly, you initiate the POST request with subscribe
in your component:
1 | this.heroesService.addHero(newHero) |
Note:
- If you only call the
addHero(newHero)
method, nothing will happen as that does NOT fire the request. To actually fire the event, it is done by calling thesubscribe
method, which both fires the event and gets the response asynchronously.
Making a DELETE Request
This looks very similar to a GET request. The only difference for syntax is that you use the delete
method instead of the get
method.
In summary:
- Specify the DELETE request URL. This will look like a GET request URL.
- Send the DELETE request with
delete()
- Though the URL looks the same, a DELETE request still differs from a GET request internally. For example, if you used
@DeleteMapping
in Spring Boot, then using aget()
in Angular does NOT work (and vice versa). You need to usedelete()
.
- Though the URL looks the same, a DELETE request still differs from a GET request internally. For example, if you used
- Subscribe to fire the event, even if you do not have any result to get from the request.
A simple example would be:
Your Spring Boot could have implemented:
1 | // even this looks like a GET request, typing in the URL does NOT WORK |
Then, your Angular service class would look like:
1 | delHero(id: number): Observable<Hero>{ |
Now, to fire/retrieve the result from the request, you will have:
1 | deleteRequest(id: string): void{ |
Making a PUT Request
PUT request looks identical to POST request in terms of syntax, yet, again, they are different internally. If you are using a @PutMapping
in your Spring Boot Application, you need to send a PUT request instead of a POST (or any other request).
In summary, it is very alike making a POST request:
- Prepare the data that you want to update
- Use the
put()
method, which takes a URL for the request, a field for the actual data, and an optionalHttpOptions
object - To fire the request and fetch the returned result, use
subscribe
For example:
In your Spring Boot application, you could have:
1 | "/api/updateHero") ( |
Then in your Angular service class:
1 | updateHero(hero: Hero): Observable<Hero>{ |
Now, in your component, you could have:
1 | putNewHero(data: any): void { |
Adding and Updating Headers
Many servers require extra headers for the operations defined above. For example, a server might require an authorization token or “Content-Type” header.
Adding Headers
In summary, all you need to do is to specify the httpOptions
- Create a mapping and specify
headers: new HttpHeaders({...})
For example:
1 | import { HttpHeaders } from '@angular/common/http'; |
Then you can pass the above httpOptions
into all the request you make.
Updating Headers
Though you can’t directly modify the existing headers within the previous options object because instances of the HttpHeaders
class are immutable, you can use the set()
method instead, which returns a clone of the current instance with the new changes applied.
For example, when a token has expired and you want to reset it, you can do:
1 | httpOptions.headers = |
Configuring HTTP URL Parameters
Some requests, such as a GET and sometimes a DELETE request, contain query strings that carries some data for the backend to process. Although you could write them manually in a string
, a better way is to set them with HttpParams().set()
(again, set
method is used because HttpParams
is immutable).
In summary, the process is the same as above for adding headers:
- Create a mapping and specify
params: new HttpParams().set(...)
- Or you can create them directly with:
params: new HttpParams({fromString: 'key=value'})
- Or you can create them directly with:
For example:
If you want to create them from scratch:
1 | /* GET heroes whose name contains search term */ |
If you are updating (or creating from scratch):
1 | searchHeroes(term: string): Observable<Hero[]> { |
Intercepting Requests and Responses
With interception, you declare interceptors that inspect and transform HTTP requests from your application to a server. The same interceptors can also inspect and transform a server’s responses on their way back to the application. Multiple interceptors form a forward-and-backward chain of request/response handlers.
Interceptors can perform a variety of implicit tasks, from authentication to logging, in a routine, standard way, for every HTTP request/response.
Without interception, developers would have to implement these tasks explicitly for each HttpClient
method call.
In summary, to use an interceptor(s), you need to:
- Create a class(es) that implements
HttpInterceptor
- Implement the
intercept()
method, handling your interceptor logics there - Provide that Interceptor at the same place as you provide the
HttpClient
- Because interceptors are (optional) dependencies of the
HttpClient
service, you must provide them in the same injector (or a parent of the injector) that providesHttpClient
. Interceptors provided after DI creates theHttpClient
are ignored. - After you have provided them, they will be used automatically in the order you provided them as they are dependencies of the
HttpClient
service
- Because interceptors are (optional) dependencies of the
Creating an Interceptor
To implement an interceptor, declare a class that implements the intercept()
method of the HttpInterceptor
interface.
Below is an example of a no operation interceptor:
1 | import { Injectable } from '@angular/core'; |
Most interceptors inspect the request on the way in and forward the (perhaps altered) request to the handle()
method of the next
object which implements the HttpHandler
interface.
1 | export abstract class HttpHandler { |
Like intercept()
, the handle()
method transforms an HTTP request into an Observable
of HttpEvents
which ultimately include the server’s response. The intercept()
method could inspect that observable and alter it before returning it to the caller.
In general:
- If you want to inspect and manipulate the
HttpRequest<any>
, you should do it inside theintercept()
method.- If you want to inspect and manipulate the
HttpResponse<any>
, you should do it inside thehandle()
method.
Note:
- Both
HttpRequest
andHttpRepsonse
areHttpEvent
.
next: HttpInterceptor
The next
object represents the next interceptor in the chain of interceptors. The final next
in the chain is the HttpClient
backend handler that sends the request to the server and receives the server’s response.
Most interceptors call next.handle()
so that the request flows through to the next interceptor and, eventually, the backend handler. An interceptor could skip calling next.handle()
, short-circuit the chain, and return its own Observable
with an artificial server response.
This is a common middleware pattern found in frameworks such as Express.js.
Registering the Interceptor
The NoopInterceptor
is a service managed by Angular’s dependency injection (DI) system. Like other services, you must provide the interceptor class before the app can use it.
However. because interceptors are (optional) dependencies of the HttpClient
service, you must provide them in the same injector (or a parent of the injector) that provides HttpClient
.
For example:
If you have provided HttpClient
in the app’s root injector (as a side-effect of importing the HttpClientModule
in AppModule
), you should provide interceptors in AppModule
as well.
After importing the HTTP_INTERCEPTORS
injection token from @angular/common/http
, write the NoopInterceptor
provider like this:
1 | { provide: HTTP_INTERCEPTORS, useClass: NoopInterceptor, multi: true }, |
Note the multi: true
option. This required setting tells Angular that HTTP_INTERCEPTORS
is a token for a multiprovider
(because you will add multiple interceptors later) that injects an array of values, rather than a single value.
Though you could directly write all of the interceptors manually inside the AppModule
, it is usually more convenient to create another file that exports the array of interceptors as follows:
1 | /* "Barrel" of Http Interceptors */ |
Lastly, you import this const
into AppModule
:
1 | import { BrowserModule } from '@angular/platform-browser'; |
Interceptor Order
Interceptor will be called based on the order of registration. In the example above, NoOpInterceptor
will be called first, and then NoOpInterceptor2
will happen next.
In general, if you provide interceptors A, then B, then C, requests flow in A->B->C and responses flow out C->B->A.
However, you cannot change the order or remove interceptors later. If you need to enable and disable an interceptor dynamically, you’ll have to build that capability into the interceptor itself.
Modifying a Request
Most HttpClient
methods return observables of HttpResponse<any>
. The HttpResponse
class itself is actually an event, whose type is HttpEventType.Response
. A single HTTP request can, however, generate multiple events of other types, including upload and download progress events. The methods HttpInterceptor.intercept()
and HttpHandler.handle()
return observables of HttpEvent<any>
.
Many interceptors are only concerned with the outgoing request and return the event stream from next.handle()
without modifying it. Some interceptors, however, need to examine and modify the response from next.handle()
; these operations can see all of these events in the stream.
Although interceptors are capable of modifying requests and responses, the HttpRequest
and HttpResponse
instance properties are readonly
, rendering them largely immutable. They are immutable for a good reason: an app might retry a request several times before it succeeds, which means that the interceptor chain can re-process the same request multiple times. If an interceptor could modify the original request object, the re-tried operation would start from the modified request rather than the original. Immutability ensures that interceptors see the same request for each try.
This means that if you want to modify a request, including its header/body/URL and etc., you need to create a clone and modify it from there.
Modifying a Request Body
The readonly
assignment guard can’t prevent deep updates and, in particular, it can’t prevent you from modifying a property of a request body object. However, it is in general a better practice if you still modify its copy:
- Copy the body and make your change in the copy.
- Clone the request object, using its
clone()
method. - Replace the clone’s body with the modified copy.
1 | // copy the body and trim whitespace from the name property |
If you want to clear the body, you can use the same approach.
However, you need to be careful of setting the body to null
instead of undefined
:
1 | newReq = req.clone({ ... }); // body not mentioned => preserve original body |
Note:
- This means that if you set the cloned request body to
undefined
, Angular assumes you intend to leave the body as is.
Modifying a Request Header
Again, similar to setting the request body, you need to clone the request object again in order to set/add the request header.
In summary, you need to:
- Use the
set()
method to update/add a header- This will return a cloned header object with your changes, which saves you some time
- Clone the request object
- Replace the clone’s headers
A common usage is to add an authorization token in the header once the user is authorized, so that all the outgoing request will have that token and the user could potentially have more access to your backend APIs.
The above can be implemented with:
1 | import { AuthService } from '../auth.service'; |
In fact, the practice of cloning a request to set new headers is so common that there’s a setHeaders
shortcut for it:
1 | // Clone the request and set the new header in one step. |
So you could technically do the above with one step:
1 | import { AuthService } from '../auth.service'; |
Using Interceptors for Logging
Because interceptors can process the request and response together, they can perform tasks such as timing and logging an entire HTTP operation by using the pipe()
method.
For example, the below snippet will record the time before the event reaches the backend and the time when response is provided:
1 | import { finalize, tap } from 'rxjs/operators'; |
where:
- The RxJS
tap
operator captures whether the request succeeded or failed. - The RxJS
finalize
operator is called when the response observable either errors or completes (which it must), and reports the outcome to theMessageService
.
Note:
- Though the intercept method gets hold of the response, neither
tap
norfinalize
touch the values of the observable stream returned to the caller. You should not modify the response unless it is absolutely necessary.
Using Interceptors for Caching
Interceptors can handle requests by themselves, without forwarding to next.handle()
.
In summary, if you want to do this, you need to:
- Create and inject a
RequestCache
cache, where your cache will be stored - Write a method to determine whether if a request is cacheable, before determining to cache the request or not
- Write a method to re-send the request.
- You can simply continue to send the request with
next.handle(req)
- If you need to change something in the request, such as request header, you can do so as well.
- You can simply continue to send the request with
For example:
Your main CachingInterceptor
that intercepts and determines whether to return the result from cache or proceed the request would look like:
1 | () |
where:
- The
isCacheable()
function determines if the request is cacheable. - If the request is not cacheable, the interceptor simply forwards the request to the next handler in the chain.
- If a cacheable request is found in the cache, the interceptor returns an
of()
observable with the cached response, by-passing thenext
handler (and all other interceptors downstream). - If a cacheable request is not in cache, the code calls
sendRequest()
.- You can/should handle the cache adding before sending with
next.handle(req)
in thatsendRequest()
method. - Or you can modify the request (following the contents mentioned above), cache it, and then send it with
next.handle(req)
.
- You can/should handle the cache adding before sending with
For instance, if you want to delete the request header information in the sendRequest()
processing, you can:
1 | /** |
Note:
- Even in the case above, the original response continues untouched back up through the chain of interceptors to the application caller.
Tracking and Showing Request Progress
Sometimes applications transfer large amounts of data and those transfers can take a long time. File uploads are a typical example. You can give the users a better experience by providing feedback on the progress of such transfers.
In summary, you need to:
- Create a request that has the option
reportProgress: true
specified- If you are constructing a raw request, that is all you need to do
- If you are constructing from an existing request (e.g. POST), you also need to add
observe: event
- This is because every progress event triggers change detection, and you can see those
events
if you specifyobserve: event
- This is because every progress event triggers change detection, and you can see those
- Use
map()
to process the event that you get from the request, andtap
to provide messages for users to see
For example:
If you are constructing a raw request:
1 | const req = new HttpRequest('POST', '/upload/file', file, { |
Then, to listen and map the events reported back to string information, use map
:
1 | // The `HttpClient.request` API produces a raw event stream |
tap
gets the messages that gets returned from map
, which are strings indicating the request status:
1 | /** Return distinct message for sent, upload progress, & response events */ |