5
Advanced components

This chapter covers

  • How to handle and optimize change detection
  • Communication between different components
  • Ways to style and different ways styles are encapsulated
  • Rendering components dynamically on the fly

Chapter 4 covered many of the basics of components, but there is so much more! Here we’ll dive into additional capabilities that will come in handy as you build more interesting applications.

We’ll look at how change detection works in more detail, and look at how to use the OnPush capability to reduce the amount of work Angular has to do to improve rendering efficiency.

Although components can use inputs and outputs, there are additional ways to have components talk to one another. We’ll look at why you might choose to use a different approach and how to implement it.

There are three primary ways to render styles for a component, and choosing different modes potentially has a significant impact on the way your components are displayed. You may want to internalize the CSS styling as much as possible, or you may not want any styling to be internalized, but to use the global CSS styles instead.

Finally, we’ll look at how to render a component dynamically, and why you might want to do that. It’s not very common, but there are moments where it’s useful.

As discussed in chapter 4, I highly recommend that you keep your components focused. As we learn more about component capabilities, you’ll see that it’s important to avoid overloading your components.

This chapter will continue with the example from chapter 4, so refer to it for how to set up the example. Everything will build upon what you learned, so the examples will be expanded to demonstrate more advanced capabilities of components.

I can say with almost 100% certainty that no component will use every single capability, because it would most likely not function. However, mastery of these additional concepts will help you write more complex and dynamic applications. Let’s start by taking a look at change detection and how to optimize performance.

5.1 Change detection and optimizations

Angular ships with a change detection framework that determines when components need to be rendered if inputs have changed. Components need to react to changes made somewhere in the component tree, and the way they change is through inputs.

Changes are always triggered by some asynchronous activity, such as when a user interacts with the page. When these changes occur, there is a chance (though no guarantee) that the application state or data has changed. Here are some examples:

  • A user clicks a button to trigger a form submission (user activity).
  • An interval fires every x seconds to refresh data (intervals or timers).
  • Callbacks, observables, or promises are resolved (XHR requests, event streams).

These are all events or asynchronous handlers, but they may come from different sources. We’ll dig deeper into the way that observables and XHR requests behave in other chapters, but here we’re curious about how user actions and an interval trigger changes in Angular.

Angular has to know that an asynchronous activity occurred, but the setInterval and setTimeout APIs in JavaScript occur outside Angular’s awareness. Angular has monkey-patched the default implementation of setInterval and setTimeout to have them properly trigger Angular’s change detection when an interval or timeout is resolved. Likewise, when an event binding is handled in Angular, it knows to trigger change detection. There are some special things to do if you write code outside of Angular that needs to trigger change detection, but I won’t cover that here.

One the change detection mechanism is triggered, it will start from the top of the component tree and check each node to see whether the component model has changed and requires rendering. That’s why input properties have to be made known to Angular, or it would fail to know how to detect changes.

Angular has two change detection modes: Default and OnPush. The Default mode will always check the component for changes on each change detection cycle. Angular has highly optimized this process so that it’s efficient to run these checks—within a couple milliseconds for most cases. That’s important when data is easily mutated between components, and it can be difficult to ensure that values haven’t changed around the application.

You can also use the OnPush mode, which explicitly tells Angular that this component only needs to check for changes if one of the component inputs has changed. That means if a parent component hasn’t changed, it’s known that the child component’s inputs won’t change, so it can skip change detection on that component (and any grandchild components). Just because an input has changed doesn’t mean the component itself has to change; perhaps the input is an object with a changed property that the component doesn’t use. Keeping tracking of your data structures in your application can help to optimize when values are passed around and how change detection fires.

Figure 5.1 illustrates the two types of change detection. Imagine there’s a component tree with two properties, and the property 'b' is changed by some user input. The default mode will update the value in the component and then check all components underneath it for changes. The OnPush mode only checks child components that have an input binding specifically for the changed property and skips checking the other components.

c05-1.png

Figure 5.1 Change detection starts at the top and goes down the tree by default, or with OnPush only goes down the tree with changed inputs.

I recommend spending time reading about change detection in more detail from one of the people who helped create it, Victor Savkin: https://vsavkin.com/change-detection-in-angular-2-4f216b855d4c.

Our Nodes Row component is a candidate for using OnPush because everything comes into the component via an input and the component’s state is always linked to the values passed in (such as if the utilization is above 70% and the danger classes need to be applied). Open src/app/nodes-row/nodes-row.component.ts and we’ll make a minor adjustment (see the following listing) to enable OnPush for it.

Listing 5.1 Nodes Row component using OnPush

import { Component, Input, ChangeDetectionStrategy } from '@angular/core';     

@Component({
  selector: '[app-nodes-row]',
  templateUrl: './nodes-row.component.html',
  styleUrls: ['./nodes-row.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush     
})

That’s it! Import and declare the component changeDetection property to OnPush, and your component is now only going through change detection when the inputs have changed. The strategy is a property of the component metadata, which is set to the Default mode by default (believe it or not!).

Now we can apply the same changes to our Metric component because it also only reflects the input values provided to it when rendered. Try to make the same change to that file yourself.

There’s another way to intercept and detect changes using the OnChanges lifecycle hook, and because we’re already intercepting the inputs with getter/setter methods in the Metric component, let’s modify it again to use OnChanges and OnPush mode.

Open src/app/metric/metric.component.ts and update it to the code you see in listing 5.2. It replaces the getter and setter methods with an OnChanges lifecycle hook and also uses OnPush mode.

Listing 5.2 Metric component using OnPush mode and OnChanges lifecycle hook

import { Component, Input, ChangeDetectionStrategy, OnChanges } from '@angular/core';     

@Component({
  selector: 'app-metric',
  templateUrl: './metric.component.html',
  styleUrls: ['./metric.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush     
})
export class MetricComponent implements OnChanges {     
  @Input('used') value: number = 0;     
  @Input('available') max: number = 100;     

  ngOnChanges(changes) {     
    if (changes.value && isNaN(changes.value.currentValue)) this.value = 0;
    if (changes.max && isNaN(changes.max.currentValue)) this.max = 0;     
  }     

  isDanger() {
    return this.value / this.max > 0.7;
  }
}

If you implemented the OnPush mode yourself, it should start by importing the strategy helper and then adding the changeDetection property. Here you also import the OnChanges interface and declare the class to implement it.

We still need to declare our input properties, so we go back to the previous way to declare them without the getter and setter methods. The ngOnChanges method implements the lifecycle hook for OnChanges, and it provides a single parameter as an object populated with any changed inputs, which then have their current and previous values available. For example, if only the value input was changed in the parent, then only the change.value property will be set on the lifecycle hook.

Inside the OnChange lifecycle hook, we run the same basic checks for whether the values are numeric or not, and if validation fails we reset the value. It should be noted that any changes to the inputs would propagate to any child components (should there be any). The value of using this approach is that we aren’t creating private properties while still intercepting and validating the inputs, and the logic doesn’t run when the component requests a property. It will only run the lifecycle hook for a component when the inputs have changed for that specific component.

If you ever need to run code every time that change detection is run on a component (regardless of whether you use OnPush or not), you can use the DoCheck lifecycle hook. This allows you to run some logic that can check for changes that exist in your component that Angular can’t detect automatically. If you need to trigger your own type of change detection, this is the lifecycle hook that will help you do that. It’s less commonly used, but do be aware of its existence for situations where OnChanges doesn’t get you what you need.

In this section we’ve optimized the Nodes Row and Metric components' change detection by only checking them when an input has changed. The Metric component also now uses the OnChanges lifecycle hook to validate inputs, which can be more efficient and hooks into the change detection lifecycle.

5.2 Communicating between components

There are a number of ways to communicate between components, and we’ve already gone into depth about inputs as a way to communicate data from a parent component to its child. That doesn’t give us a way to communicate back up to a parent, though, or to another component in the tree that isn’t a direct descendent.

As you think about building your components, particularly ones that are highly modular, they will likely need to emit events when things happen so other components can easily take advantage of knowing when things are happening elsewhere. For example, you could create a tabs component and emit an event that describes what tab is currently visible. Another component might be interested in knowing when the tab selection changes so that you can update the state of that component, such as a nearby panel that has contextual help information based on the current tab.

You’ve seen inputs are the way to push data down the component tree to children, and events are the way to pass data and notifications up through the component tree to parents. When used this way, Angular considers these events outputs. We’re going to use outputs to inform parent components of when changes happen so we can react to those events.

Additionally, we’ll look at a couple ways to use other components in this section: using a View Child (which gives you access to a child component controller in a component controller) and using a local variable (which gives you access to a child component controller in a component’s template). Each has a different design and might work in more than one use case, but we’ll see each of them in action with our app.

Let’s take a look at our application component tree again, but this time we’ll annotate the inputs and communication flows (figure 5.2). We want to make it possible to click a button in the Navbar component and have it generate the data in the dashboard (like a refresh button). The Navbar component and the Dashboard component are child components of the App component, so how can we get them to talk? It’s fairly simple using events and local template variables.

c05-2.png

Figure 5.2 Components sharing data, emitting events, and accessing other components via local variables

In figure 5.2 you can see that the Dashboard component binds data into all child components, whereas the Nodes component also binds some data into its children. But all data flows from the Dashboard component down to children.

However, with the Navbar component we need a way to communicate with the Dashboard component to tell it to generate data again. We’ll use an output event (onRefresh), which will alert the App component when the user clicks the refresh button. Then once the App component detects that the button was clicked, it can handle that event by telling the Dashboard to regenerate data, and it does that by referencing the Dashboard controller in the template using a local variable.

5.2.1 Output events and template variables

To illustrate how this works, we need to make a few changes to our Navbar and App components. Let’s start by opening the src/app/navbar/navbar.component.ts file. We’ll need to declare an output event, like we declare an input, so that Angular understands how to register and handle that event, as shown in the following listing.

Listing 5.3 Navbar component using an output

import { Component, Output, EventEmitter } from '@angular/core';     

@Component({
  selector: 'app-navbar',
  templateUrl: './navbar.component.html',
  styleUrls: ['./navbar.component.css']
})
export class NavbarComponent  {
  @Output() onRefresh: EventEmitter<null> = new EventEmitter<null>();     

  refresh() {     
    this.onRefresh.emit();     
  }     
}

We begin by importing the Output decorator and EventEmitter factory object. We need both of these to set up an output event. The EventEmitter is a special object that helps us emit custom events that work with Angular’s change detection.

Next, we declare a new property called onRefresh and add the @Output() decorator to it. This will alert Angular that there is now an event output based on the name of the property, which will allow us to then use event binding to listen for this event in our template, like (onRefresh)="..expression..". As with the @Input() decorator, you could optionally pass an alias to change the event name that’s used for event binding.

The output is typed to an EventEmitter<null> type, which means this variable will hold an EventEmitter that doesn’t emit any data. Optionally, it could declare to emit data, such as a date object that contains the moment the event was fired.

At this point we’ve wired up the output properly, but we still need to devise a way to emit refresh events. We’ve added the refresh() method to the Navbar component to call the onRefresh.emit() method that’s provided by the EventEmitter object. This is the line that will fire the event and alert the parent component that is listening.

Now we need to add the click event binding to the Navbar button so it will trigger the custom output event. Open the src/app/navbar/navbar.component.html file and update the button line to have the click handler, as you see in the following code. It should call the refresh() method:

<button class="btn btn-success" type="button" (click)="refresh()">Reload</button>

Clicking the Reload button in the navbar will now trigger the refresh() method, which will then emit the custom output event onRefresh. You can see what happens in figure 5.3.

c05-3.png

Figure 5.3 Overview of component tree, how user clicks button and it triggers refresh of data via an output event

Our App component (the parent of both Navbar and Dashboard) is now being alerted when the user clicks the button, but we haven’t implemented an event binding to capture and respond to it. To do that, open the src/app/app.component.html file and modify it to the following:

<app-navbar (onRefresh)="dashboard.generateData()"></app-navbar>
<app-dashboard #dashboard></app-dashboard>

On the first line, we’ve added the event binding to react to the onRefresh output event when it fires, but it needs to call the method from the Dashboard component. Because the Dashboard component has the method to generate data, we need a way to call it from the App component. The second line adds #dashboard to the template on the Dashboard component. This denotes a local template variable, accessible as the variable dashboard, that references the Dashboard component controller so we can call methods from the Dashboard component—it allows us to call a method from the Dashboard component anywhere in the App component template, even though we aren’t inside the Dashboard component. But only public methods are available—private methods like randomInteger(), for example, aren’t available.

If no components are listening to the event, then it will not propagate up the component tree. Output events only go to the parent component, unlike regular DOM events (such as click or keypress) that do propagate up the DOM tree.

This event emitting trick lets us use one component in another part of the template, and in this case we use it to handle the onRefresh event by calling the dashboard.generateData() method from the Dashboard component. This is handy for accessing components that exist in the same template. The major negative is that it only allows you to access the component controller from the template and not from the controller, meaning the App component controller can’t call the Dashboard component methods directly. Luckily, there’s another way to reference another component using a View Child.

5.2.2 View Child to reference components

In order to access a child component’s controller inside a parent controller, we can leverage ViewChild to inject that controller into our component. This gives us a direct reference to the child controller so we can implement a call to the Dashboard component from the App component controller.

ViewChild is a decorator for a controller property, like Inject or Output, which tells Angular to fill in that property with a reference to a specific child component controller. It’s limited to injecting only children, so if you try to inject a component that isn’t a direct descendent, it will provide you with an undefined value.

Let’s see this in action by updating our App component to get a reference to the Dashboard component and have it directly handle the onRefresh event from the Navbar. Open the src/app/app.component.ts file and replace it with what you see in listing 5.4.

Listing 5.4 App component controller

import { Component, ViewChild } from '@angular/core';     
import { DashboardComponent } from './dashboard/dashboard.component';     

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  @ViewChild(DashboardComponent) dashboard: DashboardComponent;     

  refresh() {     
    this.dashboard.generateData();     
  }     
}

After we import the ViewChild decorator, we also need to import the Dashboard­Component itself. We need to make sure it’s available during compiling and that we can directly reference it.

The App component then gets a single property, dashboard. The @ViewChild() decorator sits in front, and we pass a reference to the DashboardComponent so Angular knows exactly what component to use. We also give the property a type of DashboardComponent because it’s an instance of that controller.

Finally, we add a refresh() method, which calls the Dashboard component controller to generate the data. Compared to the template variable approach, this gives us an opportunity for additional work that might be difficult or not possible in the template.

We need to update the App component template to call the new controller method instead of using a template variable. Back in src/app/app.component.html, you should change the contents to the following:

<app-navbar (onRefresh)="refresh()"></app-navbar>
<app-dashboard></app-dashboard>

Now the template doesn’t have a reference to the Dashboard component, but instead calls the App component’s refresh method when the output event is fired. The result is the same—the Dashboard will generate a new set of data—but the approach is different.

You may be wondering which approach you should use, and it largely boils down to where you want to store the logic. If you ever need to do anything more than access properties or methods of a child controller, you probably need to use the ViewChild approach. This does cause a coupling between the two components, which should be avoided when possible. But if you can reference a child controller and call a method directly in the template, it can save code and reduce coupling.

5.3 Styling components and encapsulation modes

Angular can use different ways of rendering that change the way the component can be styled. Components are usually designed to manage their own state, and that includes the visual styling required for the component to display. There is almost always some kind of global CSS styling that you will apply to provide a functional base for your application default styling, but components can hold their own styles that are rendered in isolation from the rest of the application.

If you add CSS styles for a component, those styles aren’t globally exposed, and you will avoid having to deal with CSS rules from one component overriding another component. There’s a way to render component styles to the global styling that’s only recommended in a few situations.

There are multiple ways to add styles to a component. It’s best to add your styles in the same way across all of your components because mixing and matching can have some interesting (and sometimes unexpected) side effects. It can also be potentially challenging if you use an external library that does something differently, so keep an eye on how your dependencies work to ensure they don’t conflict.

5.3.1 Adding styles to a component

Styles can be added several ways. You can always add CSS using a global approach, where you include a linked CSS file into your application index.html file or reference the file in the .angular-cli.json file styles property (which we already did for Ng-bootstrap). These styles are generic and will be applied to any matching elements in the page. That’s good for common and shared styling that needs to go everywhere, but not good when you want to have isolated components that define their own styling.

To better isolate our styling, we can use one of the following approaches—these are ways to add styles that are specific to a single component:

  • Inline CSS —Component templates can have inline CSS or style attributes to set the styles of the elements. These are the default ways to add style rules to HTML elements regardless of whether you’re using Angular.
  • Component-linked CSS —Using the component’s styleUrls property with links to external CSS files. Angular will load the CSS file and inject the rules inside a style element for your app.
  • Component inline CSS —Using the component’s styles property with an array of CSS rules, Angular will inject the rules inside a style element for your app.

Let’s take a look at how these different approaches can be used in an example. Here we have a simple component that has styling applied from five different approaches:

  • Global CSS rules
  • Inline CSSstyle element
  • Inline style declaration
  • Component styles property
  • Component styleUrls property linked to a CSS file

We’ll modify the Reload button in the navbar to have a different background color so we can see how these different approaches get applied. We’ll start by adding some CSS to our CSS file that the CLI generates with each component, which is linked using the styleUrls property. Open the src/app/navbar/navbar.component.css file and add the following CSS rule to it:

.btn { background-color: #e32738; }

That overrides the global color set by the bootstrap CSS library and gives our button a red color (instead of the default green). Notice that the other buttons on the page (in the Nodes Row components) aren’t changed, even though they also have the .btn class applied. We’ll cover how that happens shortly, but that’s the visual result of Angular’s encapsulation features.

Next, we’ll add some styles by updating the component metadata in the src/app/navbar/navbar.component.ts file with the following styles property:

@Component({
  selector: 'app-navbar',
  templateUrl: './navbar.component.html',
  styleUrls: ['./navbar.component.css'],
  styles: [`.btn { background-color: #999999; }`]
})

The styles property lets us provide an array of strings (here we’re using the backtick character to make a string literal) that should contain valid CSS rules. Now when you look at the application, it should appear to be a gray button. That means that styles inside the styles property will override anything from the CSS files loaded through the styleUrls property. The trick here is that whichever is declared last will win, so when you have both styles and styleUrls, the one declared last will override the first regardless of which one is used, due to the way the compiling works. (At time of writing, the CLI complains if you declare both styles and styleUrls in the CLI, but it still seems to work.)

Now open up the navbar template at src/app/navbar/navbar.component.html and add the following style element into the template:

<style>.btn { background-color: #3274b8; }</style>

Once you save this, the application will reload, and all of a sudden your button will now be blue, which means that inline style declarations will override any values provide via the styles or styleUrls properties.

The last way to add styles is directly on the element itself using the style attribute. While still in the component template, modify the button to include this inline CSS declaration to make the button purple:

<button class="btn btn-success" type="button" style="background-color: #8616f6" (click)="refresh()">Reload</button>

This exercise showed us that if a CSS rule is set with a different value in all of these places, you might be surprised which rule is applied. It should be noted that using the !important value will raise that particular rule above any others, should you dare to use it. Here’s the priority order in which the rules are applied if the same rule is declared in multiple places, with each item overriding any rules below it:

  1. Inline style attribute rules
  2. Inline style block rules in the template
  3. Component styles rules or styleUrls rules (if both, then last declared has priority)
  4. Global CSS rules

Declaring a rule using inline styles is always going to be the highest priority rule. This is probably expected because, except for when the !important value is declared, inline styles are given highest priority by the browser. The style block from the template is the next highest priority. Then the next highest are rules declared in the component styles array or styleUrls external files, and lastly any global styles.

All CSS rules are added to the end of the document head inside a new style element. As the application is rendered, components will get loaded, and those styles will get added to the document head. But depending on the encapsulation mode, the way those styles are rendered can change the way those styles are processed.

One important note: all these guidelines are based on using the CLI for building. It’s possible to change the build process and obtain different priorities or results, just so you’re aware. Now let’s look at how different encapsulation modes work and why we might want to change the default settings.

5.3.2 Encapsulation modes

Angular wants to ensure that you can build modular components that can be easily shared. One key capability is the ability to ensure that CSS styling for a component doesn’t bleed over into the rest of the app, which is called styling encapsulation. Have you ever used a third-party library that included some CSS which conflicted with something else in your application and caused something to display improperly? Most likely you’ve run into this situation, and Angular offers some options to avoid it.

Until recently, there was no certain way to encapsulate CSS styling or HTML elements of a particular DOM element. The most common approach is to use specific class naming conventions, as are found in most popular CSS libraries. These conventions provide a specific nomenclature for CSS classes that limits the chances of the same class being used elsewhere. Although these might typically be effective, there’s no guarantee that class names or styles won’t collide, and it provides no encapsulation for inner HTML elements.

Enter the Shadow DOM, the official native browser standard for encapsulating styles. Shadow DOM provides us with a good set of features to ensure that our styles don’t conflict and bleed in or out of a component, except that it might not be available on older browsers. If you need to brush up on Shadow DOM, refer to chapter 1.

Angular comes with three encapsulation modes for a view. In the earlier styling example, you were using the default Emulated mode.

  • None —No encapsulation is used during rendering of the view, and the component DOM is subject to the normal rules of CSS. Templates aren’t modified when injected into the app except for the removal of any CSS style elements from the template to the document head.
  • Emulated —Emulated encapsulation is used to simulate styling encapsulation by adding unique CSS selectors to your CSS rules at runtime. CSS can cascade into the component easily from the global CSS rules.
  • Native —Uses the native Shadow DOM for styling and markup encapsulation and provides the best encapsulation. All styles are injected inside the shadow root and are therefore localized to the component. None of the templates or styles declared for the component are visible outside the component.

Each encapsulation mode applies styles in a different manner, so it’s important to understand how encapsulation modes and styling render the view. The order in which styles are applied remains constant regardless of which encapsulation mode is used, but the way in which the underlying styles are injected and modify the app does change.

Now let’s take a closer look at the different modes, how they behave, and why you might decide to select a different mode than the default mode. As we look at each mode, try setting the mode in the Metric component and inspecting the output as it’s rendered in the page to see how it adds styles into the document head.

No encapsulation mode

If you use no encapsulation by setting in the component, you’ll bypass any native or emulated Shadow DOM features. It will render the component into the HTML document as is, as if you had written the HTML markup directly into the body yourself. This is the default mode for components without any styles.

To set this mode, you would use the encapsulation property of the component metadata to set it like so—of course, you’ll need to import the ViewEncapsulation enum from @angular/core:

encapsulation: ViewEncapsulation.None 

Once the mode is set, any styles you declare for the component will be hoisted up from the component template into the document head, and this is the only real modification made to the markup. The style element is moved into the document head as is, which is a way of injecting global CSS rules. It’s possible another component rendered later would inject a competing CSS rule, so render order matters as well.

Here’s a summary of reasons why you might use, or avoid using, components with no encapsulation mode:

  • Styles bleed out —Sometimes applications are designed with CSS libraries where encapsulating the internal styling of each component is unnecessary or undesired. If you don’t put styles into your components, then encapsulation may not be necessary.
  • Global styles bleed in —The global background style for a header element is applied because of the lack of encapsulation, which may or may not be desired behavior.
  • Templates are unmodified —Because this mode injects the template as is (after relocating the styles), your DOM elements won’t have any special transformations applied.

When using no encapsulation mode, you can’t use any of the special Shadow DOM selectors like :host or ::shadow. These have no context because no Shadow DOM (native or emulated) features are enabled.

Now let’s look at the emulated mode and see how it behaves in comparison.

Emulated Shadow DOM encapsulation mode

Emulated mode applies some transformations to any styles by adding unique attributes to HTML markup and CSS rules to increase the specificity of the CSS rules by making them unique. Because this isn’t a true encapsulation, it’s referred to as emulated, but it has many of the same benefits. This is the default mode for Angular components that have any styles declared (regardless of how they’re declared in the component).

Emulated mode is primarily about preventing styles in the component from bleeding out to the global rules (which happens when no encapsulation is used). In order to accomplish this, the view will render the template and styles with unique attributes to increase the specificity of the CSS rules inside the component. Emulated mode is the default mode, but if you want to explicitly declare it, you would set the property on the component metadata:

encapsulation: ViewEncapsulation.Emulated.

During the render, the styles are first extracted from the component template or component properties. Then a unique attribute is generated, something similar to _ngcontent-ofq-3. It uses the _ngcontent prefix with a unique ending so each component can have a unique CSS selector. Multiple instances of the same component have the same unique attribute. Lastly, the view renders by adding the unique attribute to the component DOM nodes and adding it into the CSS selector rules.

Here’s a quick overview of the behaviors of emulated encapsulation mode and why you would (or wouldn’t) want to use it:

  • Styles are isolated —The rendering of the styles and markup adds the unique attributes to ensure that CSS rules don’t collide with global styles.
  • Styles bleed in —The global styling can still bleed into the component, which can be useful to allow a common styling to be shared. It also could conflict with the component if a rule is added somewhere globally that you didn’t intend to bleed into the component.
  • Unique selectors —The rendered DOM gets a unique attribute, which means if you have global styles that you want to apply into the component, you’ll need to write CSS selectors accordingly.

Now let’s finish the encapsulation modes by looking at the native mode and how it uses Shadow DOM.

Native Shadow DOM encapsulation mode

The Shadow DOM is a powerful tool for encapsulating markup and styles from the rest of the application. Angular can use Shadow DOM to inject all the content instead of putting it into the primary document. This means that the template and the styles are truly isolated from the rest of the application.

Native browser support for Shadow DOM is limited and can be checked at http://caniuse.com/#feat=shadowdom. The benefits of Shadow DOM may not extend to all the browsers you need to support, but there is also a good polyfill to backfill the support at http://webcomponents.org/polyfills/shadow-dom/. Even with a polyfill, older browsers may not be supported, so you should consider all of your needs.

While the component renders, it creates a shadow root in the component. The template is injected into the shadow root, along with the styles from the sibling and any parent component. As far as the rest of the document goes, this shadow root protects the contents from being visible to anything outside of the component.

Angular intends that nested components should be able to share styling, but with the shadow root, Angular makes those styles available to the component by injecting them into the shadow root as well.

Here’s a summary of the way that the native mode works and why you might or might not want to use it:

  • Uses Shadow DOM —For true encapsulation, the native option is the best bet. It will protect your components from the styles of the document and encapsulate the markup as well.
  • Parent and sibling styles bleed in —Due to the way Angular renders the component, it also injects the parent and sibling components’ styles, which can cause issues like styles bleeding in (which you might not want).
  • Limited support —Browser support for Shadow DOM is limited and may require the use of a polyfill to allow its use in applications.

So far, we’ve looked at how to bind data into the view from the component. This is important to allow us to inject data and modify properties of the view dynamically. We also looked at how to use event bindings to call from the view back to the component, which can be used to update values or call methods. And we’ve looked at the various ways to style a component and how to encapsulate styles.

We’ve covered a lot of ground, but now we’re going to look at Angular directives and pipes. They provide additional options to modify the display of data or elements in our view and add additional logic to templates.

5.4 Dynamically rendering components

Applications sometimes need to dynamically render a component based on the current application state. You may not always know what component needs to be on the screen, or perhaps the user interactions call for a new component to display in the page.

There are a few fairly common situations that you’ve seen where a dynamic component is often a good solution. For example

  • Modals that display over the page with dynamic content
  • Alerts that conditionally display
  • Carousel or tabs that might dynamically expand the amount of content
  • Collapsible content that needs to be removed afterward

These situations all have something in common: They don’t always need to be on the screen or are dependent on conditions outside of their own power. Angular gives us the ability to use lower-level APIs to call, that let us render a component that doesn’t already exist in a template, on demand.

I’ll show two examples of using ng-bootstrap to generate a modal and then talk about how to do it all ourselves using Angular’s APIs to create an alert component. With ng-bootstrap, most of the magic is hidden behind a helpful service, but it will give us the ability to quickly get the capability working before we build it by hand.

5.4.1 Using the Ng-bootstrap modal for dynamic components

Let’s start by generating a new component. This component is going to show details about the node when you click the View button in the Nodes Row component. We’re going to call this the Nodes Detail component:

ng generate component nodes-detail

Now replace the component’s controller, found in src/app/nodes-detail/nodes-detail.component.ts, with the code in listing 5.5. This controller has similar logic to determine whether the node exceeds its utilization or not. This is the component that will open inside the modal and won’t be loaded until called for.

Listing 5.5 Nodes Detail component controller

import {Component, Input} from '@angular/core';
import {NgbActiveModal} from '@ng-bootstrap/ng-bootstrap';     

@Component({
  selector: 'app-nodes-detail',
  templateUrl: './nodes-detail.component.html',
  styleUrls: ['./nodes-detail.component.css']
})
export class NodesDetailComponent {
  @Input() node;     

  constructor(public activeModal: NgbActiveModal) {}     

  isDanger(prop) {
    return this.node[prop].used / this.node[prop].available > 0.7;
  }

  getType(prop) {
    return (this.isDanger(prop)) ? 'danger' : 'success';
  }
}

The Ng-bootstrap library provides a useful service called NgbActiveModal, which is an instance of the modal controller that loads the Nodes Detail component. It will allow us to dismiss the modal when we like, either on demand or based on user action. This will be more apparent when we add the template for this component.

Unlike other components so far, this component won’t be called from a parent’s template. But we will need to pass it an input for the data, so we still need to declare the input property.

Now we need the template to make the component functional. Open src/app/nodes-detail/nodes-detail.component.html and replace the existing code with the following listing.

Listing 5.6Nodes Detail component template

<div class="modal-header">
  <button type="button" class="close" aria-label="Close" (click)="activeModal.dismiss()">     
    <span aria-hidden="true">&times;</span>
  </button>
  <h4 class="modal-title">{{node.name}}</h4>
</div>
<div class="modal-body container">
  <div class="col-xs-6">
    <app-metric [used]="node.cpu.used" [available]="node.cpu.available">
      <metric-title>CPU</metric-title>     
    </app-metric>     
  </div>
  <div class="col-xs-6">
    <app-metric [used]="node.mem.used" [available]="node.mem.available">
      <metric-title>Memory</metric-title>     
    </app-metric>     
  </div>
</div>

There’s a lot of markup here needed for the display of a modal with bootstrap’s CSS styling, but it has a modal header with a title and close button and a body containing two Metric components. Notice that we can use any components that have been registered with our Application module inside of a dynamic component, which is handy.

Remember in our controller we had a property named activeModal, and it was an instance of the NgbActiveModal service. We use that in the template to call the dismiss method, which will close the modal itself. This is why we included that property in our controller. You can also see how the node property, which is our only input binding, is used to display data or pass data into the other components.

The contents of this component are fairly similar to the rest of the application, so we don’t need to spend time on it. What we’re now interested in is how to trigger the modal to open from the Nodes Row component. Open the src/app/nodes-row/nodes-row.component.html file and add the following event binding to the button:

<td><button class="btn btn-secondary" (click)="open(node)">View</button></td>

Now we need to open src/app/nodes-row/nodes-row.component.ts and implement this new method. The code in the following listing contains the updated controller, and changes are annotated for you.

Listing 5.7 Nodes Row component template additions for modal

import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';     
import { NodesDetailComponent } from '../nodes-detail/nodes-detail.component';     

@Component({
  selector: '[app-nodes-row]',
  templateUrl: './nodes-row.component.html',
  styleUrls: ['./nodes-row.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class NodesRowComponent {
  @Input() node: any;

  constructor(private modalService: NgbModal) {}     

  isDanger(prop) {
    return this.node[prop].used / this.node[prop].available > 0.7;
  }

  open(node) {     
    const modal = this.modalService.open(NodesDetailComponent);     
    modal.componentInstance.node = node;     
  }     
}

Here we import the NgbModal service that will allow us to create a new modal. We saw the NgbActiveModal service in the Nodes Detail component, which, once the modal and its component has been created, will allow the Nodes Detail component to reference the active modal instance. We need to import the Nodes Detail component as well. The constructor also sets the modalService property with an instance of the NgbModal service.

In the open() method, we pass in a reference to the node data to use. Then we create a new modal instance using the modalService, which takes as a parameter the component to be rendered in the modal. It stores a reference to this newly created component in the componentInstance property, which allows us to set the node input binding that was passed in during the click.

That wires up all we need to trigger the modal from the Nodes Row component. But if you try it, the modal doesn’t work quite yet because there are a few minor details we need to implement that will allow us to open this modal.

First, open the src/app/dashboard/dashboard.component.html file and add the following code line to the bottom of the template—we need to give the Modal service a place to render the component:

<template ngbModalContainer></template>

This is a placeholder template element that has the NgbModalContainer directive on it, which tells Ng-bootstrap where in the template to render this component. Components have to be rendered somewhere in the template, and this is Ng-bootstrap’s way of defining the location to render.

Secondly, we need to add a new entry to our App module. When the CLI processes Angular, it needs to know what components might be rendered dynamically because it will process them differently. Open the src/app/app.module.ts file and add a new line to the NgModule decorator:

entryComponents: [NodesDetailComponent],

Entry components are any components that need to be rendered dynamically in the browser, which will also include components that are linked to routes (more on this in chapter 7). The CLI will try to optimize components by default and not include the component factory class. But to dynamically render, the component factory class is needed to render, so this tells the CLI compiler how to properly process it during build time.

This example is a bit specific for Ng-bootstrap’s implementation of the modal, so it will only get us so far in understanding how to build our own dynamic components.

5.4.2 Dynamically creating a component and rendering it

The Ng-bootstrap modal example is a nice way to create a modal, but it abstracts some of the capability away from us. We want to see how it works directly and will build upon the knowledge of our components that we have so far to create our own dynamically rendered component.

When we dynamically render a component, Angular needs a few things. It needs to know what component to render, where to render it, and where it can get a copy of it. This all happens during the compilation process for any template, but in this case we have no template and have to invoke the APIs ourselves. These are the Angular capabilities we’ll use to handle this process:

  • ViewContainerRef—This is a reference to an element in the application that Angular understands and that gives us a reference point to where we can render our component.
  • ViewChild—This will give us the ability to reference an element in our controller as a ViewContainerRef type, giving us access to APIs needed to render components.
  • ComponentFactoryResolver—An Angular service that gets us the component factory (which is needed to render) for any component that has been added to the entry components list.

As we build our example, you’ll see these three capabilities working together. We’re going to build an alert box that appears when the data is refreshed and then removes itself after a certain amount of time. This will give us insight into how to dynamically render a component and remove it from the page, which accomplishes much the same thing as what you get from the Ng-bootstrap modal service.

Start by generating a new component. From the CLI, run the following command to set up this new component:

ng generate component alert

The template for this component is going to be simple; it has some bootstrap-flavored markup that makes an alert box and binds the date of the last refresh. Open src/app/alert/alert.component.html and replace its contents with the following:

<div class="container mt-2">
  <div class="alert alert-warning" role="alert">
    The data was refreshed at {{date | date:'medium'}}
  </div>
</div>

Likewise, the component controller is going to be empty except for a single input property. Open src/app/alert/alert.component.ts and replace its contents with the code from the following listing.

Listing 5.8 Alert component controller

import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-alert',
  templateUrl: './alert.component.html',
  styleUrls: ['./alert.component.css']
})
export class AlertComponent {
  @Input() date: Date;
}

So far, so good. There’s nothing special about this component that we haven’t already seen in this chapter, so I’m going to move to the next step. Because this component will be dynamically rendered, we need to add it to the list of entryComponents, so open up src/app/app.modules.ts again and add it to the list. At this point, our component itself is ready to be dynamically rendered:

entryComponents: [
  NodesDetailComponent,
  AlertComponent
],

Now we can start to work on the mechanics that will trigger the rendering of the component out to the screen. As with the modal example, we’ll need to create a template element in our application that can be used to render out, so open the src/app/dashboard/dashboard.component.html file and update it to have the following template:

<app-navbar (onRefresh)="refresh()"></app-navbar>
<ng-template #alertBox></ng-template>
<app-dashboard></app-dashboard>

The #alertBox attribute is another template local variable that we can use to identify this element later on. This will be the element that we render the component alongside. Open up the src/app/app.component.ts file and replace it with the code from the following listing.

Listing 5.9 App component controller

import { Component, ViewChild, ComponentFactoryResolver, ComponentRef, ViewContainerRef } from '@angular/core';     
import { DashboardComponent } from './dashboard/dashboard.component';
import { AlertComponent } from './alert/alert.component';     

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  alertRef: ComponentRef<AlertComponent>;     
  @ViewChild(DashboardComponent) dashboard: DashboardComponent;
  @ViewChild('alertBox', {read: ViewContainerRef}) alertBox: ViewContainerRef;     

  constructor(private ComponentFactoryResolver: ComponentFactoryResolver) {}     

  alert(date) {     
    if (!this.alertRef) {     
      const alertComponent = this.ComponentFactoryResolver.resolveComponentFactory(AlertComponent);     
      this.alertRef = this.alertBox.createComponent(alertComponent);     
    }     

    this.alertRef.instance.date = date;     
    this.alertRef.changeDetectorRef.detectChanges();     

    setTimeout(() => this.destroyAlert(), 5000);     
  }     

  destroyAlert() {     
    if (this.alertRef) {     
      this.alertRef.destroy();     
      delete this.alertRef;     
    }     
  }     

  refresh() {
    this.dashboard.generateData();
  }
}

This might look complicated at first, so let’s break it down. It’s fairly plain how it’s working and constructed.

First we must import a few additional objects, and we’ll look at their roles as we go along. We also import the Alert component itself so we can properly reference it during rendering.

We then add two properties, with alertRef being a component reference to the Alert component (which is the declared typing). We will want to have this reference so we can keep track of the alert and later remove it if we want. The second property is another View Child, called alertBox. The ViewChild syntax is different because it allows us to pass in a string to reference a local template variable by that name and then “read” it as a particular type of entity—in this case, a ViewContainerRef. It will get the element based on the template variable and then parse it as a ViewContainerRef type. This will give us access to a critical API shortly. These are only properties, though. So far, nothing has been instantiated.

The constructor sets the ComponentFactoryResolver property to the Factory Resolver service, which is what we’ll need in order to look up a copy of the component factory class before rendering it.

The primary magic of this occurs inside the alert() method. We’ll walk through this line by line. First, it looks to see if there is already something stored on the alertRef (meaning the Alert component has already been created), and if so it skips past the creation of another alert and moves on to update the binding. But if no Alert component exists yet, it uses the ComponentFactoryResolver.resolveComponentFactory() method to get an instance of the component factory (it seems a bit redundant, but it’s the API name). At this point, the component will be available in its raw form (not yet rendered).

The next line uses the alertBox to create the component from the factory instance we received previously. Remember, alertBox is the instance of the element where we will inject the component, wrapped in a ViewContainerRef instance. At this point, the component will be rendered into the template where the template element was declared.

The next two lines (outside of the conditional) set the binding data for the component and then trigger change detection to run. Because we changed binding data manually, we need to alert Angular to check something (this was asynchronous from Angular’s typical rendering process!).

Lastly, a timeout is set to call the deleteAlert method after five seconds so the alert doesn’t remain on the screen forever. If we look at this method more closely, you can see it will check if there is an existing instance of an Alert component. If so, it will use the destroy() method that all components have to remove it from the page.

If you’re trying to run this example, you’ll find it doesn’t work yet. We’ve missed an important step! Nowhere do we call the App component’s alert() method, so it won’t appear. To do that, we’ll emit an event from the Dashboard component that will fire when data is generated, along with the timestamp when it was updated.

Open up src/app/dashboard/dashboard.component.ts. We’re going to add a new output event. Be sure to import the Output and EventEmitter objects at the top of the file. Then add a new property like you see here:

@Output() onRefresh: EventEmitter<Date> = new EventEmitter<Date>();

Now inside the generateData() method add this line to the end of the method:

this.onRefresh.emit(new Date());

This sets up a new output event from the Dashboard component, and this time we’re passing some data during the emit phase. We can capture this information in the App component and use it to pass into our Alert component. This is simple now—all we have to do is update the src/app/app.component.html file one more time by adding an event binding to the Dashboard component:

<app-dashboard (onRefresh)="alert($event)"></app-dashboard>

Voila! Our Alert component should now appear after every data generation event, regardless of whether you click the button in the top right or wait 15 seconds for it to automatically regenerate. It will also automatically close the alert after five seconds.

That was a whirlwind tour of dynamic components. There are several different ways to generate a component besides this one, but this is a solid approach that you can use in your own applications.

Summary

We’ve been busy in this chapter. We covered a lot of content about components, how they work, their various capabilities, and much more. Here’s what we talked about in this chapter:

  • We looked at change detection in more detail, along with how to leverage the OnPush mode to better optimize when a component is rendered.
  • We looked at several lifecycle event handlers. There are a number of other ones that have their own use cases as well:
  • The OnInit lifecycle hook fires only once after the constructor and when the input properties are available.
  • The OnChanges lifecycle hook fires whenever an input property changes.
  • The OnDestroy lifecycle hook fires whenever a component is about to be removed from the page.
  • We talked about how to communicate between components using output properties and the built-in EventEmitter, and how to reference child components as a View Child.
  • We talked about the styling of components using CSS in various ways and how different encapsulation modes can affect the way that content is rendered on the page.
  • We finished by looking at how to render out components dynamically, with two examples. The first was a prebuilt service using the Ng-bootstrap modal service to render our component on demand, whereas the second was managed entirely by us.
..................Content has been hidden....................

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