Chapter 4. Understanding and Using Angular Components
In the previous chapter, we did a deep dive into the built-in directives that Angular offers that allow us to perform common functionality like hiding and showing elements, repeating templates, and so on. We worked with directives like ngIf
and ngForOf
and got a feel for how and when to use them.
In this chapter, we will go a bit deeper into components, those elements we have been creating to render the UI and let users interact with the applications we build. We will cover some of the more useful attributes you can specify when creating components, how to think about the lifecycle of the component and the various hooks that Angular gives you, and finally, cover how to pass data into and out of your custom components. By the end of the chapter, you should be able to perform most common tasks related to components while understanding what you are doing and why.
Components—A Recap
In the previous chapter, we saw that Angular only has directives, and that directives are reused for multiple purposes. We dealt with attribute and structural directives, which allow us to change the behavior of an existing element or to change the structure of the template being rendered.
The third kind of directives are components, which we have been using pretty much from the first chapter. To some extent, you can consider an Angular application to be nothing but a tree of components. Each component in turn has some behavior and a template that gets rendered. This template can then continue to use other components, thus forming a tree of components, which is the Angular application that gets rendered in the browser.
At its very simplest, a component is nothing but a class that encapsulates behavior (what data to load, what data to render, and how to respond to user interactions) and a template (how the data is rendered). But there are multiple ways to define that as well, along with other options, which we will cover in the following sections.
Defining a Component
We define a component using the TypeScript decorator Component
. This allows us to annotate any class with some metadata that teaches Angular how the component works, what to render, and so on. Let’s take a look again at the stock-item
component we created to see what a simple component would look like, and we will build up from there:
@
Component
({
selector
:
'app-stock-item'
,
templateUrl
:
'./stock-item.component.html'
,
styleUrls
:
[
'./stock-item.component.css'
]
})
export
class
StockItemComponent
implements
OnInit
{
// Code omitted here for clarity
}
The very basic component only needs a selector (to tell Angular how to find instances of the component being used) and a template (that Angular has to render when it finds the element). All other attributes in the Component
decorator are optional. In the preceding example, we have defined that the StockItemComponent
is to be rendered whenever Angular encounters the app-stock-item
selector, and to render the stock-item.component.html file when it encounters the element. Let’s talk about the attributes of the decorator in a bit more detail.
Selector
The selector attribute, as we touched upon briefly in Chapter 2, allows us to define how Angular identifies when the component is used in HTML. The selector takes a string value, which is the CSS selector Angular will use to identify the element. The recommended practice when we create new components is to use element selectors (like we did with app-stock-item
), but technically you could use any other selector as well. For example, here are a few ways you could specify the selector attribute and how you would use it in the HTML:
-
selector: 'app-stock-item'
would result in the component being used as<app-stock-item></app-stock-item>
in the HTML. -
selector: '.app-stock-item'
would result in the component being used as a CSS class like<div class="app-stock-item"></div>
in the HTML. -
selector: '[app-stock-item]'
would result in the component being used as an attribute on an existing element like<div app-stock-item></div>
in the HTML.
You can make the selector as simple or complex as you want, but as a rule of thumb, try to stick to simple element selectors unless you have a very strong reason not to.
Template
We have been using templateUrl
so far to define what the template to be used along with a component is. The path you pass to the templateUrl
attribute is relative to the path of the component. In the previous case, we can either specify the templateUrl
as:
templateUrl: './stock.item.component.html'
or:
templateUrl: 'stock.item.component.html'
and it would work. But if you try to specify an absolute URL or anything else, your compilation would break. One interesting thing to note is that unlike AngularJS (1.x), the application Angular builds does not load the template by URL at runtime. Instead, Angular precompiles a build and ensures that the template is inlined as part of the build process.
Instead of templateUrl
, we could also specify the template inline in the component, using the template
option. This allows us to have the component contain all the information instead of splitting it across HTML and TypeScript code.
Tip
Only one of template
and templateUrl
can be specified in a component. You cannot use both, but at least one is essential.
There is no impact on the final generated application as Angular compiles the code into a single bundle. The only reason you might want to split your template code into a separate file is to get nicer IDE features such as syntax completion and the like, which are specific to file extensions. Generally, you might want to keep your templates separate if they are over three or four lines or have any complexity.
Let’s see how our stock-item
component might look with an inline template:
import
{
Component
,
OnInit
}
from
'@angular/core'
;
import
{
Stock
}
from
'../../model/stock'
;
@
Component
({
selector
:
'app-stock-item'
,
template
:
`
<
div
class
=
"stock-container"
>
<
div
class
=
"name"
>
{{
stock
.
name
+
' ('
+
stock
.
code
+
')'
}}
<
/div>
<
div
class
=
"price"
[
class
]
=
"stock.isPositiveChange() ? 'positive' : 'negative'"
>
$
{{
stock
.
price
}}
<
/div>
<
button
(
click
)
=
"toggleFavorite($event)"
*
ngIf
=
"!stock.favorite"
>
Add
to
Favorite
<
/button>
<
/div>
`
,
styleUrls
:
[
'./stock-item.component.css'
]
})
export
class
StockItemComponent
implements
OnInit
{
// Code omitted here for clarity
}
Tip
ECMAScript 2015 (and TypeScript) allows us to define multiline templates using the ` (backtick) symbol, instead of doing string concatenation across multiple lines using the + (plus) operator. We leverage this usually when we define inline templates.
You can find the completed code in the chapter4/component-template folder in the GitHub repository.
All we have done is taken the template and moved it into the template
attribute of the Component
decorator. In this particular case though, because there are more than a few lines with some amount of work being done, I would recommend not moving it inline. Note that as a result of moving it to template
, we have removed the previous templateUrl
attribute.
Styles
A given component can have multiple styles attached to it. This allows you to pull in component-specific CSS, as well as potentially any other common CSS that needs to be applied to it. Similar to templates, you can either inline your CSS using the styles
attribute, or if there is a significant amount of CSS or you want to leverage your IDE, you can pull it out into a separate file and pull it into your component using the styleUrls
attribute. Both of these take an array as an input.
One thing that Angular promotes out of the box is complete encapsulation and isolation of styles. That means by default, the styles you define and use in one component will not affect/impact any other parent or child component. This ensures that you can be confident that the CSS classes you define in any component will not unknowingly affect anything else, unless you explicitly pull in the necessary styles.
Again, just like templates, Angular will not pull in these styles at runtime, but rather precompile and create a bundle with the necessary styles. Thus, the choice of using styles
or styleUrls
is a personal one, without any major impact at runtime.
Warning
Do not use both styles
and styleUrls
together. Angular will end up picking one or the other and will lead to unexpected behavior.
Let’s quickly see how the component might look if we inlined the styles:
import
{
Component
,
OnInit
}
from
'@angular/core'
;
import
{
Stock
}
from
'../../model/stock'
;
@
Component
({
selector
:
'app-stock-item'
,
templateUrl
:
'stock-item.component.html'
,
styles
:
[
`
.
stock
-
container
{
border
:1px
solid
black
;
border
-
radius
:5px
;
display
:inline
-
block
;
padding
:10px
;
}
.
positive
{
color
:green
;
}
.
negative
{
color
:red
;
}
`
]
})
export
class
StockItemComponent
implements
OnInit
{
// Code omitted here for clarity
}
You can find the completed code in the chapter4/component-style folder in the GitHub repository.
You can of course choose to pass in multiple style strings to the attribute. The decision between using styles
and styleUrls
is one of personal preference and has no impact on the final performance of the application.
Style Encapsulation
In the preceding section, we talked about how Angular encapsulates the styles to ensure that it doesn’t contaminate any of your other components. In fact, you can actually tell Angular whether it needs to do this or not, or if the styles can be accessible globally. You can set this by using the encapsulation
attribute on the Component
decorator. The encapsulation
attribute takes one of three values:
ViewEncapsulation.Emulated
-
This the default, where Angular creates shimmed CSS to emulate the behavior that shadow DOMs and shadow roots provide.
ViewEncapsulation.Native
-
This is the ideal, where Angular will use shadow roots. This will only work on browsers and platforms that natively support it.
ViewEncapsulation.None
-
Uses global CSS, without any encapsulation.
What Is the Shadow DOM?
HTML, CSS, and JavaScript have a default tendency to be global in the context of the current page. What this means is that an ID given to an element can easily clash with another element somewhere else on the page. Similarly, a CSS rule given to a button in one corner of the page might end up impacting another totally unrelated button.
We end up having to come up with specific naming conventions, use CSS hacks like !important
, and use many more techniques to work around this generally in our day-to-day development.
Shadow DOM fixes this by scoping HTML DOM and CSS. It provides the ability to have scoped styling to a component (thus preventing the styles from leaking out and affecting the rest of the application) and also the ability to isolate and make the DOM self-contained.
You can read up on it more in the documentation for self-contained web components.
The best way to see how this impacts our application is to make a slight change and see how our application behaves under different circumstances.
First, let’s add the following snippet of code to the app.component.css file. We are using the same base as the previous chapter, and the completed code is available in the chapter4/component-style-encapsulation folder:
.name
{
font-size
:
50px
;
}
If we run the application right now, there is no impact on our application. Now, let’s try changing the encapsulation
property on the main AppComponent
. We will change the component as follows:
import
{
Component
,
ViewEncapsulation
}
from
'@angular/core'
;
@
Component
({
selector
:
'app-root'
,
templateUrl
:
'./app.component.html'
,
styleUrls
:
[
'./app.component.css'
],
encapsulation
:ViewEncapsulation.None
})
export
class
AppComponent
{
title
=
'app works!'
;
}
We added the encapsulation: ViewEncapsulation.None
line to our Component
decorator (of course, after importing the ViewEncapsulation
enum from Angular). Now if we refresh our application, you will see that the name of the stock has been blown up to 50px. This is because the styles applied on the AppComponent
are not restricted to just the component but are now taking the global namespace. Thus, any element that adds the name
class to itself will get this font-size applied to it.
ViewEncapsulation.None
is a good way of applying common styles to all child components, but definitely adds the risk of polluting the global CSS namespace and having unintentional effects.
Others
There are a lot more attributes than what we covered on the Component
decorator. We will briefly review a few of those here, and will reserve discussion of others for later chapters when they become more relevant. Here is a quick highlight of some of the other major attributes and their uses:
- Stripping white spaces
-
Angular allows you to strip any unnecessary white spaces from your template (as defined by Angular, including more than one space, space between elements, etc.). This can help reduce the build size by compressing your HTML. You can set this feature (which is set to
false
by default) by using thepreserveWhitespaces
attribute on the component. You can read more about this feature in the official documentation. - Animations
-
Angular gives you multiple triggers to control and animate each part of the component and its lifecycle. To accomplish this, it provides its own DSL, which allows Angular to animate on state changes within the element.
- Interpolation
-
There are times when the default Angular interpolation markers (the double-curlies
{{
and}}
) interfere with integrating with other frameworks or technologies. For those scenarios, Angular allows you to override the interpolation identifiers at a component level by specifying the start and end delimiters. You can do so by using theinterpolation
attribute, which takes an array of two strings, the opening and closing markers for the interpolation. By default, they are['{{', '}}']
, but you override it by, say, providinginterpolation: ['<<', '>>']
to replace the interpolation symbols for just that component to<<
and>>
. - View providers
-
View providers allow you to define providers that inject classes/services into a component or any of its children. Usually, you won’t need it, but if there are certain components where you want to override, or restrict the availability of a class or a service, you can specify an array of providers to a component using the
viewProviders
attribute. We will cover this in more detail in Chapter 8. - Exporting the component
-
We have been working so far by using the component class’s functions within the context of the template. But there are use cases (especially when we start dealing with directives and more complex components) for which we might want to allow the user of the component to call functions on the component from outside. A use case might be that we provide a carousel component, but want to provide functionality to allow the user of the component to control the next/previous functionality. In these cases, we can use the
exportAs
attribute of theComponent
decorator. changeDetection
-
By default, Angular checks every binding in the UI to see if it needs to update any UI element whenever any value changes in our component. This is acceptable for most applications, but as our applications get larger in size and complexity, we might want control over how and when Angular updates the UI. Instead of Angular deciding when it needs to update the UI, we might want to be explicit and tell Angular when it needs to update the UI manually. To do this, we use the
changeDetection
attribute, where we can override the default value ofChangeDetectionStrategy.Default
toChangeDetectionStrategy.OnPush
. This means that after the initial render, it will be up to us to let Angular know when the value changes. Angular will not check the component’s bindings automatically. We will cover this in more detail later in the chapter.
There are a lot more attributes and features with regards to components that we don’t cover in this chapter. You should take a look at the official documentation for components to get familiar with what else is possible, or dive deeper into the details.
Components and Modules
Before we go into the details of the lifecycle of a component, let’s quickly sidetrack into how components are linked to modules and what their relation is. In Chapter 2, we saw how any time we created a new component, we had to include it in a module. If you create a new component, and do not add it to a module, Angular will complain that you have components that are not part of any modules.
For any component to be used within the context of a module, it has to be imported into your module declaration file and declared in the declarations
array. This ensures that the component is visible to all other components within the module.
There are three specific attributes on the NgModule
that directly impact components and their usage, which are important to know. While only declarations
is important initially, once you start working with multiple modules, or if you are either creating or importing other modules, the other two attributes become essential:
declarations
-
The
declarations
attribute ensures that components and directives are available to use within the scope of the module. The Angular CLI will automatically add your component or directive to the module when you create a component through it. When you first start out building Angular applications, you might easily forget to add your newly created components to thedeclarations
attribute, so keep track of that (if you are not using the Angular CLI, that is!) in order to avoid this common mistake. imports
-
The
imports
attribute allows you to specify modules that you want imported and accessible within your module. This is mostly as a way to pull in third-party modules to make the components and services available within your application. If you want to use a component from other modules, make sure you import the relevant modules into the module you have declared and where the component exists. exports
-
The
exports
attribute is relevant if you either have multiple modules or you need to create a library that will be used by other developers. Unless you export a component, it cannot be accessed or used outside of the direct module where the component is declared. As a general rule of thumb, if you will need to use the component in another module, make sure you export it.
Tip
If you are facing issues using a component, where Angular fails to recognize a component or says it does not recognize an element, it most likely is due to misconfigured modules. Check, in order, the following:
-
Whether the component is added as a declaration in the module.
-
In case it is not a component that you wrote, make sure that you have imported the module that provides/exports the component.
-
If you created a new component that needs to be used in other components, make sure that you export the component in its module so that any application including the module will get access to your newly created component.
Input and Output
One common use case when we start creating components is that we want to separate the content that a component uses from the component itself. A component is truly useful when it is reusable. One of the ways we can make a component reusable (rather than having default, hardcoded values inside it) is by passing in different inputs depending on the use case. Similarly, there might be cases where we want hooks from a component when a certain activity happens within its context.
Angular provides hooks to specify each of these through decorators, aptly named Input
and Output
. These, unlike the Component
and NgModule
decorators, apply at a class member variable level.
Input
When we add an Input
decorator on a member variable, it automatically allows you to pass in values to the component for that particular input via Angular’s data binding syntax.
Let’s see how we can extend our stock-item
component from the previous chapter to allow us to pass in the stock object, rather than hardcoding it within the component itself. The finished example is available in the GitHub repository in the chapter4/component-input folder. If you want to code along and don’t have the previous code, you can use the chapter3/ng-if codebase as the starter to code along from.
We will first modify the stock-item
component to mark the stock as an input to the component, but instead of initializing the stock object, we will mark it as an Input
to the component. We do this by importing the decorator and using it for the stock
variable. The code for the stock-item.component.ts file should look like the following:
import
{
Component
,
OnInit
,
Input
}
from
'@angular/core'
;
import
{
Stock
}
from
'../../model/stock'
;
@
Component
({
selector
:
'app-stock-item'
,
templateUrl
:
'./stock-item.component.html'
,
styleUrls
:
[
'./stock-item.component.css'
]
})
export
class
StockItemComponent
{
@
Input
()
public
stock
:Stock
;
constructor
()
{
}
toggleFavorite
(
event
)
{
this
.
stock
.
favorite
=
!
this
.
stock
.
favorite
;
}
}
We have removed all instantiation logic from the app-stock-item
component, and marked the stock
variable as an input. This means that the initialization logic has been moved out, and the component is only responsible for receiving the value of the stock from the parent component and just rendering the data.
Next, let’s take a look at the AppComponent
and how we can change that to now pass in the data to the StockItemComponent
:
import
{
Component
,
OnInit
}
from
'@angular/core'
;
import
{
Stock
}
from
'app/model/stock'
;
@
Component
({
selector
:
'app-root'
,
templateUrl
:
'./app.component.html'
,
styleUrls
:
[
'./app.component.css'
]
})
export
class
AppComponent
implements
OnInit
{
title
=
'Stock Market App'
;
public
stockObj
:Stock
;
ngOnInit
()
:
void
{
this
.
stockObj
=
new
Stock
(
'Test Stock Company'
,
'TSC'
,
85
,
80
);
}
}
We just moved the initialization of the stock object from the StockItemComponent
to the AppComponent
. Finally, let’s take a look at the template of the AppComponent
to see how we can pass in the stock to the StockItemComponent
:
<h1>
{{title}}</h1>
<app-stock-item
[
stock
]="
stockObj
"
></app-stock-item>
We use Angular’s data binding to pass in the stock from the AppComponent
to the StockItemComponent
. The name of the attribute (stock
) has to match the name of the variable in the component that has been marked as input. The attribute name is case sensitive, so make sure it matches exactly with the input variable name. The value that we pass to it is the reference of the object in the AppComponent
class, which is stockObj
.
HTML and Case-Sensitive Attributes?
You might wonder how this is even possible. Angular has its own HTML parser under the covers that parses the templates for Angular-specific syntax, and does not rely on the DOM API for some of these. This is why Angular attributes are and can be case-sensitive.
These inputs are data bound, so if you end up changing the value of the object in AppComponent
, it will automatically be reflected in the child StockItemComponent
.
Output
Just like we can pass data into a component, we can also register and listen for events from a component. We use data binding to pass data in, and we use event binding syntax to register for events. We use the Output
decorator to accomplish this.
We register an EventEmitter
as an output from any component. We can then trigger the event using the EventEmitter
object, which will allow any component bound to the event to get the notification and act accordingly.
We can use the code from the previous example where we registered an Input
decorator and continue on from there. Let’s now extend the StockComponent
to trigger an event when it is favorited, and move the data manipulation out from the component to its parent. This makes sense as well because the parent component is responsible for the data and should be the single source of truth. Thus, we will let the parent AppComponent
register for the toggleFavorite
event and change the state of the stock when the event is triggered.
The finished code for this is in the chapter4/component-output folder.
Take a look at the StockItemComponent
code in src/app/stock/stock-item/stock-item.component.ts:
import
{
Component
,
OnInit
,
Input
,
Output
,
EventEmitter
}
from
'@angular/core'
;
import
{
Stock
}
from
'../../model/stock'
;
@
Component
({
selector
:
'app-stock-item'
,
templateUrl
:
'./stock-item.component.html'
,
styleUrls
:
[
'./stock-item.component.css'
]
})
export
class
StockItemComponent
{
@
Input
()
public
stock
:Stock
;
@
Output
()
private
toggleFavorite
:EventEmitter
<
Stock
>
;
constructor
()
{
this
.
toggleFavorite
=
new
EventEmitter
<
Stock
>
();
}
onToggleFavorite
(
event
)
{
this
.
toggleFavorite
.
emit
(
this
.
stock
);
}
}
A few important things to note:
-
We imported the
Output
decorator as well as theEventEmitter
from the Angular library. -
We created a new class member called
toggleFavorite
of typeEventEmitter
, and renamed our method toonToggleFavorite
. TheEventEmitter
can be typed for additional type safety. -
We need to ensure that the
EventEmitter
instance is initialized, as it is not auto-initialized for us. Either do it inline or do it in the constructor as we did earlier. -
The
onToggleFavorite
now just calls a method on theEventEmitter
to emit the entire stock object. This means that all listeners of thetoggleFavorite
event will get the current stock object as a parameter.
We will also change stock-item.component.html to call the onToggleFavorite
method instead of toggleFavorite
. The HTML markup remains pretty much the same otherwise:
<div
class=
"stock-container"
>
<div
class=
"name"
>
{{stock.name + ' (' + stock.code + ')'}}</div>
<div
class=
"price"
[
class
]="
stock
.
isPositiveChange
()
?
'
positive
'
:
'
negative
'"
>
$ {{stock.price}}</div>
<button
(
click
)="
onToggleFavorite
($
event
)"
*
ngIf=
"!stock.favorite"
>
Add to Favorite</button>
</div>
Next, we add a method to the AppComponent
that should be triggered whenever the onToggleFavorite
method is triggered, which we will add event binding on:
import
{
Component
,
OnInit
}
from
'@angular/core'
;
import
{
Stock
}
from
'app/model/stock'
;
@
Component
({
selector
:
'app-root'
,
templateUrl
:
'./app.component.html'
,
styleUrls
:
[
'./app.component.css'
]
})
export
class
AppComponent
implements
OnInit
{
title
=
'app works!'
;
public
stock
:Stock
;
ngOnInit
()
:
void
{
this
.
stock
=
new
Stock
(
'Test Stock Company'
,
'TSC'
,
85
,
80
);
}
onToggleFavorite
(
stock
:Stock
)
{
console
.
log
(
'Favorite for stock '
,
stock
,
' was triggered'
);
this
.
stock
.
favorite
=
!
this
.
stock
.
favorite
;
}
}
The only thing new is the onToggleFavorite
method we have added, which takes a stock as an argument. In this particular case, we don’t use the stock passed to it other than for logging, but you could base any decision/work on that. Note also that the name of the function is not relevant, and you could name it whatever you want.
Finally, let’s tie it all together by subscribing to the new output from our StockComponent
in the app-component.html file:
<h1>
{{title}}</h1>
<app-stock-item
[
stock
]="
stock
"
(
toggleFavorite
)="
onToggleFavorite
($
event
)"
>
</app-stock-item>
We just added an event binding using Angular’s event-binding syntax to the output declared in the stock-item
component. Notice again that it is case sensitive and it has to exactly match what member variable we decorated with the Output
decorator. Also, to get access to the value emitted by the component, we use the keyword $event
as a parameter to the function. Without it, the function would still get triggered, but you would not get any arguments with it.
With this, if you run the application (remember, ng serve
), you should see the fully functional app, and when you click the Add to Favorite button, it should trigger the method in the AppComponent
.
Change Detection
We mentioned changeDetection
as an attribute on the Component
decorator. Now that we have seen how Input
and Output
decorators work, let’s deep dive a little bit into how Angular performs its change detection at a component level.
By default, Angular applies the ChangeDetectionStrategy.Default
mechanism to the changeDetection
attribute. This means that every time Angular notices an event (say, a server response or a user interaction), it will go through each component in the component tree, and check each of the bindings individually to see if any of the values have changed and need to be updated in the view.
For a very large application, you will have lots of bindings on a given page. When a user takes any action, you as a developer might know for sure that most of the page will not change. In such cases, you can actually give a hint to the Angular change detector to check or not check certain components as you see fit. For any given component, we can accomplish this by changing the ChangeDetectionStrategy
from the default to ChangeDetectionStrategy.OnPush
. What this tells Angular is that the bindings for this particular component will need to be checked only based on the Input
to this component.
Let’s consider a few examples to see how this might play out. Say we have a component tree A → B → C. That is, we have a root component A, which uses a component B in its template, which in turn uses a component C. And let’s say component B passes in a composite object compositeObj
to component C as input. Maybe something like:
<c [inputToC]="compositeObj"></c>
That is, inputToC
is the input variable marked with the Input
decorator in component C, and is passed the object compositeObj
from component B. Now say we marked component C’s changeDetection
attribute as ChangeDetectionStrategy.OnPush
. Here are the implications of that change:
-
If component C has bindings to any attributes of
compositeObj
, they will work as usual (no change from default behavior). -
If component C makes any changes to any of the attributes of
compositeObj
, they will also be updated immediately (no change from default behavior). -
If the parent component B creates a new
compositeObj
or changes the reference ofcompositeObj
(think new operator, or assign from a server response), then component C would recognize the change and update its bindings for the new value (no change from default behavior, but internal behavior changes on how Angular recognizes the change). -
If the parent component B changes any attribute on the
compositeObj
directly (as a response to a user action outside component B), then these changes would not be updated in component C (major change from the default behavior). -
If the parent component B changes any attribute on response to an event emitter from component C, and then changes any attribute on the
compositeObj
(without changing the reference), this would still work and the bindings would get updated. This is because the change originates from component C (no change from default behavior).
Angular provides ways for us to signal when to check the bindings from within the component as well, to have absolute control on Angular’s data binding. We will cover these in “Change Detection”. For now, it is good to understand the difference between the two change detection strategies that Angular provides.
Let’s now modify the example code to see this in action. First, modify the stock-item.component.ts file to change the ChangeDetectionStrategy
in the child component:
import
{
Component
,
OnInit
,
Input
,
Output
}
from
'@angular/core'
;
import
{
EventEmitter
,
ChangeDetectionStrategy
}
from
'@angular/core'
;
import
{
Stock
}
from
'../../model/stock'
;
@
Component
({
selector
:
'app-stock-item'
,
templateUrl
:
'./stock-item.component.html'
,
styleUrls
:
[
'./stock-item.component.css'
],
changeDetection
:ChangeDetectionStrategy.OnPush
})
export
class
StockItemComponent
{
@
Input
()
public
stock
:Stock
;
@
Output
()
private
toggleFavorite
:EventEmitter
<
Stock
>
;
constructor
()
{
this
.
toggleFavorite
=
new
EventEmitter
<
Stock
>
();
}
onToggleFavorite
(
event
)
{
this
.
toggleFavorite
.
emit
(
this
.
stock
);
}
changeStockPrice() {
this
.
stock
.
price
+=
5
;
}
}
In addition to changing the ChangeDetectionStrategy
, we also added another function to changeStockPrice()
. We will use these functions to demonstrate the behavior of the change detection in the context of our application.
Next, let’s quickly modify stock-item.component.html to allow us to trigger the new function. We will simply add a new button to trigger and change the stock price when the button is clicked:
<div
class=
"stock-container"
>
<div
class=
"name"
>
{{stock.name + ' (' + stock.code + ')'}}</div>
<div
class=
"price"
[
class
]="
stock
.
isPositiveChange
()
?
'
positive
'
:
'
negative
'"
>
$ {{stock.price}}</div>
<button
(
click
)="
onToggleFavorite
($
event
)"
*
ngIf=
"!stock.favorite"
>
Add to Favorite</button>
<button
(
click
)="
changeStockPrice
()"
>
Change Price</button>
</div>
There is no change to the HTML of the template other than adding a new button to change the stock price. Similarly, let’s quickly change the main app.component.html file to add another button to trigger the change of the price from the parent component (similar to component B in the earlier hypothetical example):
<h1>
{{title}}</h1>
<app-stock-item
[
stock
]="
stock
"
(
toggleFavorite
)="
onToggleFavorite
($
event
)"
>
</app-stock-item>
<button
(
click
)="
changeStockObject
()"
>
Change Stock</button>
<button
(
click
)="
changeStockPrice
()"
>
Change Price</button>
We have added two new buttons to this template: one that will change the reference of the stock object directly, and another that will modify the existing reference of the stock object to change the price from the parent AppComponent
. Now finally, we can see how all of this is hooked up in the app.component.ts file:
import
{
Component
,
OnInit
}
from
'@angular/core'
;
import
{
Stock
}
from
'app/model/stock'
;
@
Component
({
selector
:
'app-root'
,
templateUrl
:
'./app.component.html'
,
styleUrls
:
[
'./app.component.css'
]
})
export
class
AppComponent
implements
OnInit
{
title
=
'app works!'
;
public
stock
:Stock
;
private
counter
:number
=
1
;
ngOnInit
()
:
void
{
this
.
stock
=
new
Stock
(
'Test Stock Company - '
+
this
.
counter
++
,
'TSC'
,
85
,
80
);
}
onToggleFavorite
(
stock
:Stock
)
{
// This will update the value in the stock item component
// Because it is triggered as a result of an event
// binding from the stock item component
this
.
stock
.
favorite
=
!
this
.
stock
.
favorite
;
}
changeStockObject() {
// This will update the value in the stock item component
// Because we are creating a new reference for the stock input
this
.
stock
=
new
Stock
(
'Test Stock Company - '
+
this
.
counter
++
,
'TSC'
,
85
,
80
);
}
changeStockPrice() {
// This will not update the value in the stock item component
// because it is changing the same reference and angular will
// not check for it in the OnPush change detection strategy.
this
.
stock
.
price
+=
10
;
}
}
The app.component.ts file has seen the most changes. The preceding code is also well annotated with comments to explain the expected behavior when each of these functions are triggered. We have added two new methods: changeStockObject()
, which creates a new instance of the stock
object in the AppComponent
, and changeStockPrice()
, which modifies the prices of the stock
object in the AppComponent
. We have also added a counter just to keep track of how many times we create a new stock object, but that is not strictly necessary.
Now when you run this application, you should expect to see the following behavior:
-
Clicking Add to Favorite within the
StockItemComponent
still works as expected. -
Clicking Change Price within the
StockItemComponent
will increase the price of the stock by $5 each time. -
Clicking Change Stock outside the
StockItemComponent
will change the name of the stock with each click. (This is why we added the counter!) -
Clicking Change Price outside the
StockItemComponent
will have no impact (even though the actual value of the stock will jump if you click Change Price inside after this). This shows that the model is getting updated, but Angular is not updating the view.
You should also change back the ChangeDetectionStrategy
to default to see the difference in action.
Component Lifecycle
Components (and directives) in Angular have their own lifecycle, from creation, rendering, changing, to destruction. This lifecycle executes in preorder tree traversal order, from top to bottom. After Angular renders a component, it starts the lifecycle for each of its children, and so on until the entire application is rendered.
There are times when these lifecycle events are useful to us in developing our application, so Angular provides hooks into this lifecycle so that we can observe and react as necessary. Figure 4-1 shows the lifecycle hooks of a component, in the order in which they are invoked.
Angular will first call the constructor for any component, and then the various steps mentioned earlier in order. Some of them, like the OnInit
and AfterContentInit
(basically, any lifecycle hook ending with Init
) is called only once, when a component is initialized, while the others are called whenever any content changes. The OnDestroy
hook is also called only once for a component.
Each of these lifecycle steps comes with an interface that should be implemented when a component cares about that particular lifecycle, and each interface provides a function starting with ng
that needs to be implemented. For example, the OnInit
lifecycle step needs a function called ngOnInit
to be implemented in the component and so on.
We will walk through each of the lifecycle steps here, and then use one example to see this all in action and the ordering of lifecycle steps within a component and across components.
There is also one more concept to learn, which we will briefly touch upon in this chapter, and come back to later in more detail—the concept of ViewChildren
and ContentChildren
.
ViewChildren
is any child component whose tags/selectors (mostly elements, as that is the recommendation for components) appear within the template of the component. So in our case, app-stock-item
would be a ViewChild
of the AppComponent
.
ContentChildren
is any child component that gets projected into the view of the component, but is not directly included in the template within the component. Imagine something like a carousel, where the functionality is encapsulated in the component, but the view, which could be images or pages of a book, comes from the user of the component. Those are generally achieved through ContentChildren
. We will cover this in more depth later in this chapter.
Interfaces and Functions
Table 4-1 shows the interfaces and functions in the order in which they are called, along with specific details about the step if there is anything to note. Note that we are only covering component-specific lifecycle steps, and they are slightly different from a directive’s lifecycle.
Let’s try to add all these hooks to our existing application to see the order of execution in a real-world scenario. We will add all of these hooks to both our AppComponent
and the StockItemComponent
, with a simple console.log
to just see when and how these functions are executed. We will use the base from the output example to build from, so in case you are not coding along, you can take the example from chapter4/component-output to build from there.
The final finished example is also available in chapter4/component-lifecycle.
First, we can modify the src/app/app.component.ts file and add the hooks as follows:
import
{
Component
,
SimpleChanges
,
OnInit
,
OnChanges
,
OnDestroy
,
DoCheck
,
AfterViewChecked
,
AfterViewInit
,
AfterContentChecked
,
AfterContentInit
}
from
'@angular/core'
;
import
{
Stock
}
from
'app/model/stock'
;
@
Component
({
selector
:
'app-root'
,
templateUrl
:
'./app.component.html'
,
styleUrls
:
[
'./app.component.css'
]
})
export
class
AppComponent
implements
OnInit
,
OnChanges
,
OnDestroy
,
DoCheck
,
AfterContentChecked
,
AfterContentInit
,
AfterViewChecked
,
AfterViewInit
{
title
=
'app works!'
;
public
stock
:Stock
;
onToggleFavorite
(
stock
:Stock
)
{
console
.
log
(
'Favorite for stock '
,
stock
,
' was triggered'
);
this
.
stock
.
favorite
=
!
this
.
stock
.
favorite
;
}
ngOnInit
()
:
void
{
this
.
stock
=
new
Stock
(
'Test Stock Company'
,
'TSC'
,
85
,
80
);
console
.
log
(
'App Component - On Init'
);
}
ngAfterViewInit
()
:
void
{
console
.
log
(
'App Component - After View Init'
);
}
ngAfterViewChecked
()
:
void
{
console
.
log
(
'App Component - After View Checked'
);
}
ngAfterContentInit
()
:
void
{
console
.
log
(
'App Component - After Content Init'
);
}
ngAfterContentChecked
()
:
void
{
console
.
log
(
'App Component - After Content Checked'
);
}
ngDoCheck
()
:
void
{
console
.
log
(
'App Component - Do Check'
);
}
ngOnDestroy
()
:
void
{
console
.
log
(
'App Component - On Destroy'
);
}
ngOnChanges
(
changes
:SimpleChanges
)
:
void
{
console
.
log
(
'App Component - On Changes - '
,
changes
);
}
}
You can see that we have implemented the interfaces for OnInit, OnChanges, OnDestroy, DoCheck, AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit
on the AppComponent
class, and then went ahead and implemented the respective functions. Each of the methods simply prints out a log statement mentioning the component name and the trigger method name.
Similarly, we can do the same for the StockItemComponent
:
import
{
Component
,
SimpleChanges
,
OnInit
,
OnChanges
,
OnDestroy
,
DoCheck
,
AfterViewChecked
,
AfterViewInit
,
AfterContentChecked
,
AfterContentInit
,
Input
,
Output
,
EventEmitter
}
from
'@angular/core'
;
import
{
Stock
}
from
'../../model/stock'
;
@
Component
({
selector
:
'app-stock-item'
,
templateUrl
:
'./stock-item.component.html'
,
styleUrls
:
[
'./stock-item.component.css'
]
})
export
class
StockItemComponent
implements
OnInit
,
OnChanges
,
OnDestroy
,
DoCheck
,
AfterContentChecked
,
AfterContentInit
,
AfterViewChecked
,
AfterViewInit
{
@
Input
()
public
stock
:Stock
;
@
Output
()
private
toggleFavorite
:EventEmitter
<
Stock
>
;
constructor
()
{
this
.
toggleFavorite
=
new
EventEmitter
<
Stock
>
();
}
onToggleFavorite
(
event
)
{
this
.
toggleFavorite
.
emit
(
this
.
stock
);
}
ngOnInit
()
:
void
{
console
.
log
(
'Stock Item Component - On Init'
);
}
ngAfterViewInit
()
:
void
{
console
.
log
(
'Stock Item Component - After View Init'
);
}
ngAfterViewChecked
()
:
void
{
console
.
log
(
'Stock Item Component - After View Checked'
);
}
ngAfterContentInit
()
:
void
{
console
.
log
(
'Stock Item Component - After Content Init'
);
}
ngAfterContentChecked
()
:
void
{
console
.
log
(
'Stock Item Component - After Content Checked'
);
}
ngDoCheck
()
:
void
{
console
.
log
(
'Stock Item Component - Do Check'
);
}
ngOnDestroy
()
:
void
{
console
.
log
(
'Stock Item Component - On Destroy'
);
}
ngOnChanges
(
changes
:SimpleChanges
)
:
void
{
console
.
log
(
'Stock Item Component - On Changes - '
,
changes
);
}
}
We have done exactly the same thing we did on the AppComponent
with the StockItemComponent
. Now, we can run this application to see it in action.
When you run it, open the JavaScript console in the browser. You should see, in order of execution:
-
First, the
AppComponent
gets created. Then the following hooks are triggered on theAppComponent
:-
On Init
-
Do Check
-
After Content Init
-
After Content Checked
The preceding two immediately execute because we don’t have any content projection in our application so far.
-
-
Next, the
StockItemComponent OnChanges
executes, with the input to theStockItemComponent
being recognized as the change, followed by the hooks listed here within theStockItemComponent
:-
On Init
-
Do Check
-
After Content Init
-
After Content Checked
-
After View Init
-
After View Checked
-
-
Finally, there are no more subcomponents to traverse down on, so Angular steps back out to the parent
AppComponent
, and executes the following:-
After View Init
-
After View Checked
-
This gives us a nice view of how and in which order Angular goes around initializing and the tree traversal it does under the covers. These hooks become very useful for certain trickier initialization logic, and are definitely essential for cleanup once your component is done and dusted, to avoid memory leaks.
View Projection
The last thing we will cover in this chapter is the concept of view projection. Projection is an important idea in Angular as it gives us more flexibility when we develop our components and again gives us another tool to make them truly reusable under different contexts.
Projection is useful when we want to build components but set some parts of the UI of the component to not be an innate part of it. For example, say we were building a component for a carousel. A carousel has a few simple capabilities: it is able to display an item, and allow us to navigate to the next/previous element. Your carousel component might also have other features like lazy loading, etc. But one thing that is not the purview of the carousel component is the content it displays. A user of the component might want to display an image, a page of a book, or any other random thing.
Thus, in these cases, the view would be controlled by the user of the component, and the functionality would be provided by the component itself. This is but one use case where we might want to use projection in our components.
Let’s see how we might use content projection in our Angular application. We will use the base from the input example to build from, so in case you are not coding along, you can take the example from chapter4/component-input to build from there.
The final finished example is available in chapter4/component-projection.
First, we will modify our StockItemComponent
to allow for content projection. There is no code change in our component class; we only need to modify the src/app/stock/stock-item/stock-item.component.html file as follows:
<div
class=
"stock-container"
>
<div
class=
"name"
>
{{stock.name + ' (' + stock.code + ')'}}
</div>
<div
class=
"price"
[
class
]
=
"
stock
.
isPositiveChange
(
)
?
'
positive
'
:
'
negative
'
"
>
$ {{stock.price}}
</div>
<ng-content
>
</ng-content>
</div>
We have simply removed the buttons we previously had, and are going to let the user of the component decide what buttons are to be shown. To allow for this, we have replaced the buttons with an ng-content
element. There is no other change required in the component.
Next, we will make a change to the AppComponent
, simply to add a method for testing purposes. Modify the src/app/app.component.ts file as follows:
/** Imports and decorators skipped for brevity **/
export
class
AppComponent
implements
OnInit
{
/** Constructor and OnInit skipped for brevity **/
testMethod() {
console
.
log
(
'Test method in AppComponent triggered'
);
}
}
We have simply added a method that will log to the console when it is triggered. With this in place, now let’s see how we can use our updated StockItemComponent
and use the power of projection. Modify the app.component.html file as follows:
<h1>
{{title}}</h1>
<app-stock-item
[
stock
]="
stockObj
"
>
<button
(
click
)="
testMethod
()"
>
With Button 1</button>
</app-stock-item>
<app-stock-item
[
stock
]="
stockObj
"
>
No buttons for you!!</app-stock-item>
We have added two instances of the app-stock-item
component in our HTML. And both of these now have some content inside them, as opposed to previously where these elements had no content. In one, we have a button that triggers the testMethod
we added in the AppComponent
, and the other simply has text content.
When we run our Angular application and open it in the browser, we should see something like Figure 4-2.
Notice that the two stock item components on our browser, each with slightly different content, are based on what we provided. If you click the button in the first stock widget, you will see that the method in the AppComponent
gets called and the console.log
is triggered.
Thus, users of the component now have the capability to change part of the UI of the component as they see fit. We can even access functionality from the parent component as well, which makes it truly flexible. It is also possible to project multiple different sections and content into our child component. While the official Angular documentation is spare on this topic, there is a great article that can give you more insight on content projection.
Conclusion
In this chapter, we went into a lot more depth on components, and saw some of the more commonly used attributes when creating components. We took a detailed look at the Component
decorator, talking about attributes like template
versus templateUrl
, styles, and also covered at a high level how Angular’s change detection works and how we can override it.
We then covered the lifecycle of a component, as well as the hooks that Angular provides for us to hook on to and react to some of these lifecycle events. Finally, we covered projection in components and how we can make some truly powerful components that allow the user of the component to decide parts of the UI.
In the next chapter, we will do a quick detour to understand unit testing of components, and see how we can test both the logic that drives the component as well as the view that gets rendered.
Exercise
For our third exercise, we can build on top of the previous exercise (chapter3/exercise) by including concepts from this chapter:
-
Create a
ProductListComponent
. Initialize an array of products there, instead of initializing a single product in theProductComponent
. Change its template to useNgFor
to create aProductItemComponent
for each product. -
Use inline templates and styles on the
ProductListComponent
. Generate it using the Angular CLI with that setting rather than generating it and changing it manually. -
Change the
ProductItemComponent
to take the product as an input. -
Move the increment/decrement logic from the
ProductItem
to theProductListComponent
. Use an index or product ID to find the product and change its quantity. -
Move the
ProductItemComponent
to be optimal and move from the defaultChangeDetectionStrategy
to anOnPush
ChangeDetectionStrategy
.
All of this can be accomplished using concepts covered in this chapter. You can check out the finished solution in chapter4/exercise/ecommerce.
Get Angular: Up and Running now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.