Chapter 6. Implementing component communications

This chapter covers

  • Creating loosely coupled components
  • How a parent component should pass data to its child, and vice versa
  • Implementing the Mediator design pattern to create reusable components
  • A component lifecycle
  • Understanding change detection

We’ve established that any Angular application is a tree of components. While designing components, you need to ensure that they’re reusable and self-contained and at the same time have some means for communicating with each other. In this chapter, we’ll focus on how components can pass data to each other in a loosely coupled manner.

First, we’ll show you how a parent component can pass data to its child by binding to the input properties of the child. Then you’ll see how a child component can send data to its parent by emitting events via its output properties.

We’ll continue with an example that applies the Mediator design pattern to arrange data exchange between components that don’t have parent-child relationships. The Mediator pattern is probably the most important pattern in any component-based framework. Finally, we’ll discuss the lifecycle of an Angular component and the hooks you can use to provide application-specific code that intercepts important events during a component’s creation, lifespan, and destruction.

6.1. Inter-component communication

Figure 6.1 shows a view that consists of a number of components that are numbered and have different shapes for easier reference. Some of the components contain other components (let’s call the outer ones containers), and others are peers. To abstract this from any particular UI framework, we avoided using HTML elements like input fields, drop-downs, and buttons, but you can extrapolate this into a view of your real-world application.

Figure 6.1. A view consists of components.

When you design a view that consists of multiple components, the less they know about each other, the better. Say a user clicks the button in component 4, which has to initiate some actions in component 5. Is it possible to implement this scenario without component 4 knowing that component 5 exists? Yes, it is.

You’ve seen already examples of loosely coupling components by using dependency injection. Now we’ll show you a different technique for achieving the same goal, using bindings and events.

6.1.1. Input and output properties

Think of an Angular component as a black box with outlets. Some of them are marked as @Input(), and others are marked as @Output(). You can create a component with as many inputs and outputs as you want.

If an Angular component needs to receive values from the outside world, you can bind the producers of these values to the corresponding inputs of the component. Whom are they received from? The component doesn’t have to know. The component just needs to know what to do with these values when they’re provided.

If a component needs to communicate values to the outside world, it can emit events through its outputs. Whom are they emitted to? The component doesn’t have to know. Whoever is interested can listen or subscribe to the events that a component emits.

Let’s implement these principles. First you’ll create an OrderComponent that can receive order requests from the outside world.

Input properties

The input properties of a component are decorated with @Input and are used to get data from the parent component. Imagine that you want to create a UI component for placing orders to buy stocks. It will know how to connect to the stock exchange, but that’s irrelevant in the context of this discussion of input properties. You want to ensure that OrderComponent receives data from other components via its properties marked with @Input annotations.

Listing 6.1 includes two components: AppComponent (the parent) and Order-Component (the child). The latter has two properties, stockSymbol and quantity, marked with @Input annotations. The AppComponent allows users to enter a stock symbol, which is passed to the OrderComponent via bindings.

You’ll also pass quantity to the OrderComponent; but you won’t use binding with quantity, so you can see the case when a parent needs to pass to the child a value that won’t be changing. You’ll leave off the binding mechanism by not surrounding the quantity attribute in the <order-processor> tag with square brackets.

Listing 6.1. input_property_binding.ts

Tip

Because you don’t use binding for the quantity attribute, the value 100 arrives in OrderComponent as a string (all values in HTML attributes are strings). If you want to preserve the types, use bindings, like this: [quantity]="100".

Note

If you change the value of stockSymbol or quantity in the Order-Component, the change won’t affect the property values of the parent component. Property binding is unidirectional: from parent to child.

Figure 6.2 shows the browser’s window after the user types IBM in the input field. The OrderComponent received the input values.

Figure 6.2. OrderComponent got the values.

The next question is, how can a component intercept the moment when one of its input properties changes? A simple way is to change the input property to a setter. You use stockSymbol in the template of the component, so you need the getter as well. Because you have a public setter, rename the variable to _stockSymbol and make it private.

Listing 6.2. Adding the setter and getter
private _stockSymbol: string;

@Input()
set stockSymbol(value: string) {
    this._stockSymbol = value;
    if (this._stockSymbol != undefined) {
      console.log(`Sending a Buy order to NASDAQ:
      ${this.stockSymbol}{this.quantity}`);
    }
}

get stockSymbol(): string {
    return this._stockSymbol;
}

When this application starts, all input variables are initialized with default values, and the change-detection mechanism qualifies the initialization as a change of the bound variable stockSymbol. The setter is invoked, and, to avoid sending an order for the undefinedstockSymbol, you check its value in the setter.

Note

In section 6.2.1, we’ll show you how to intercept the changes in input properties without using setters.

In chapter 3, we showed you how to pass parameters to a component using Activated-Route. In this scenario, parameters are passed via constructor. Binding to @Input() parameters is a solution for passing data from parent to child, and it works only for components located within the same route.

Output properties and custom events

Angular components can dispatch custom events using the EventEmitter object. These events can be handled either in the component or by its parents. EventEmitter is a subclass of Subject (implemented in RxJS) that can serve as both observable and observer. In other words, EventEmitter can dispatch custom events using its emit() method as well as consume observables using its subscribe() method. Because this section is about sending data from a component to the outside world, we’ll focus on dispatching custom events here.

Let’s say you need to write a UI component that’s connected to a stock exchange and displays changing stock prices. This component may be used in a financial dashboard application in a brokerage firm. In addition to displaying prices, the component should also send events with the latest prices to the outside world so other components can apply business logic to the changing prices.

Let’s create a PriceQuoterComponent that implements such functionality. For this example, you won’t connect to any servers but will rather emulate the changing prices using a random number generator. Displaying changing prices in PriceQuoterComponent is pretty straightforward—you’ll bind the stockSymbol and lastPrice properties to the component’s template.

You’ll notify the outside world by emitting custom events via the @Output property of the component. Not only will you fire the event as soon as the price changes, but this event will also carry a payload: an object with the stock symbol and the latest price. The following script implements this functionality.

Listing 6.3. output-property-binding.ts

The event handler receives the object of type IPriceQuote, and you extract the values of stockSymbol and lastPrice from it. If you run this example, you’ll see the prices update every second in both PriceQuoterComponent (shaded background) as well as AppComponent (white background), as shown in figure 6.3.

Figure 6.3. Running the output properties example

Tip

By default, the name of a custom event is the same as the name of the output property, which is lastPrice in this case. If you want to emit an event with a different name, specify the name of the event as an argument to the @Output annotation. For example, to emit an event called last-price, declare the output property as @Output('last-price') lastPrice;.

In listing 5.3, you create PriceQuoterComponent as an Angular component because it includes the UI. But the business may require the functionality for price-quote retrieval without the UI, in order to reuse it both in trader applications and on large dashboards. You could implement the same functionality as an injectable service, as you did with ProductService in the online auction project.

Event bubbling

As we write this, Angular doesn’t offer a syntax to support event bubbling. For PriceQuoterComponent, this means if you try to listen to the last-price event not on this component but on its parent, the event won’t bubble up there. In the following code snippet, the last-price event won’t reach the <div> because it’s the parent of <price-quoter>:

<div (last-price)="priceQuoteHandler($event)">
  <price-quoter ></price-quoter>
</div>

If event bubbling is important to your application, don’t use EventEmitter; use native DOM events, instead. The following example is another version of PriceQuoterComponent that handles event bubbling without using Angular’s EventEmitter:

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { NgModule, Component, ElementRef }        from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

interface IPriceQuote {
  stockSymbol: string,
  lastPrice: number
}

@Component({
  selector: 'price-quoter',
  template: `PriceQuoter: {{stockSymbol}} ${{price}}`,
  styles:[`:host {background: pink;}`]
})
class PriceQuoterComponent {
  stockSymbol: string = "IBM";
  price:number;

  constructor(element: ElementRef) {
    setInterval(() => {
      let priceQuote: IPriceQuote = {
        stockSymbol: this.stockSymbol,
        lastPrice: 100*Math.random()
      };

      this.price = priceQuote.lastPrice;

      element.nativeElement
         .dispatchEvent(new CustomEvent('last-price', {
           detail: priceQuote,
           bubbles: true
         }));
    }, 1000);
  }
}

@Component({
  selector: 'app',
  template: `
    <div (last-price)="priceQuoteHandler($event)">
      <price-quoter></price-quoter>
    </div>
    <br>
    AppComponent received: {{stockSymbol}} ${{price}}
  `
})
class AppComponent {

  stockSymbol: string;
  price:number;

  priceQuoteHandler(event: CustomEvent) {
    this.stockSymbol = event.detail.stockSymbol;
    this.price = event.detail.lastPrice;
  }
}
@NgModule({
  imports:      [ BrowserModule],
  declarations: [ AppComponent, PriceQuoterComponent],
  bootstrap:    [ AppComponent ]
})
class AppModule { }

platformBrowserDynamic().bootstrapModule(AppModule);

In the preceding application, Angular injects a reference to the DOM element that represents <price-quoter> using ElementRef, and then the custom event is dispatched by invoking element.nativeElement.dispatchEvent(). Event bubbling will work here, but keep in mind that this code becomes browser-specific and won’t work with non-HTML renderers.

6.1.2. The Mediator pattern

When you design a component-based UI, each component should be self-contained, and components shouldn’t rely on the existence of other UI components. Such loosely coupled components can be implemented using the Mediator design pattern, which according to Wikipedia “defines how a set of objects interact” (https://en.wikipedia.org/wiki/Mediator_pattern). We’ll explain what this means by analogy with toy interconnecting bricks.

Imagine a child playing with building bricks (think components) that “don’t know” about each other. Today this child (the mediator) can use some blocks to build a house, and tomorrow they’ll construct a boat from the same components.

Note

The role of the mediator is to ensure that components properly fit together according to the task at hand while remaining loosely coupled.

Let’s revisit figure 6.1. Each component except 1 has a parent (a container) that can play the role of mediator. The top-level mediator is container 1, which is responsible for making sure components 2, 3, and 6 can communicate if need be. On the other hand, component 2 is a mediator for 4 and 5. Component 3 is the mediator for 7 and 8.

The mediator needs to receive data from one component and pass it to another. Let’s go back to examples of monitoring stock prices.

Imagine a trader monitoring the prices of several stocks. At some point, the trader clicks the Buy button next to a stock symbol to place a purchase order with the stock exchange. You can easily add a Buy button to PriceQuoterComponent from the previous section, but this component doesn’t know how to place orders to buy stocks. PriceQuoterComponent will notify the mediator (AppComponent) that the trader wants to purchase a particular stock at that very moment.

The mediator should know which component can place purchase orders and how to pass the stock symbol and quantity to it. Figure 6.4 shows how an AppComponent can mediate the communication between PriceQuoterComponent and OrderComponent.

Note

Emitting events works like broadcasting. PriceQuoterComponent emits events via the @Output property without knowing who will receive the events. OrderComponent waits for the value of the @Input property to change as a signal for placing an order.

Figure 6.4. Mediating communications

To demonstrate the Mediator pattern in action, let’s write a small application that consists of the two components shown in figure 6.4. You can find this application in the mediator directory, which has the following files:

  • stock.ts— The interface defining a value object that represents a stock
  • price-quoter.ts— PriceQuoterComponent
  • order.ts— OrderComponent
  • mediator.ts— PriceQuoterComponent and OrderComponent

You’ll use the Stock interface in two scenarios:

  • To represent the payload of the event emitted by PriceQuoteComponent
  • To represent the data given to the OrderComponent via binding

The content of the stock.ts file is as follows.

Listing 6.4. stock.ts
export interface Stock {
  stockSymbol: string;
  bidPrice: number;
}

Suppose you use SystemJS to transpile the TypeScript on the fly. By default, SystemJS will turn the content of the stock.ts file into an empty stock.js module, and you’ll get an error when the SystemJS loader tries to import it. You need to let SystemJS know that it has to treat Stock as a module. This can be done while configuring SystemJS by using the meta annotation, as shown in the following code extract from systemjs.config.js:

packages: {...},
meta: {
  'app/mediator/stock.ts': {
    format: 'es6'
  }
}

The PriceQuoteComponent, shown next, has a Buy button and the buy output property. It emits the buy event only when the user clicks the Buy button.

Listing 6.5. price-quoter.ts
import {Component, Output, Directive, EventEmitter} from '@angular/core';
import {Stock} from './stock';

@Component({
    selector: 'price-quoter',
    template: `<strong><input type="button" value="Buy"
     (click)="buyStocks($event)">
        {{stockSymbol}} ${{lastPrice | currency:'USD':true:'1.2-2'}}
         </strong>
              `,
    styles:[`:host {background: pink; padding: 5px 15px 15px 15px;}`]
})
export class PriceQuoterComponent {
    @Output() buy: EventEmitter <Stock> = new EventEmitter();

    stockSymbol: string = "IBM";
    lastPrice:number;

    constructor() {
        setInterval(() => {
            this.lastPrice = 100*Math.random();
        }, 2000);
    }

    buyStocks(): void{

        let stockToBuy: Stock = {
            stockSymbol: this.stockSymbol,
            bidPrice: this.lastPrice
        };

        this.buy.emit(stockToBuy);
    }
}

When the mediator (AppComponent) receives the buy event from <price-quoter>, it extracts the payload from this event and assigns it to the stock variable, which is bound to the input parameter of <order-processor>. The code is shown next.

Listing 6.6. mediator.ts
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { NgModule, Component} from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import {OrderComponent} from './order';
import {PriceQuoterComponent} from './price-quoter';
import {Stock} from './stock';

@Component({
    selector: 'app',
    template: `
    <price-quoter (buy)="priceQuoteHandler($event)"></price-quoter><br>
    <br/>
    <order-processor [stock]="stock"></order-processor>
  `
})
class AppComponent {
    stock: Stock;

    priceQuoteHandler(event:Stock) {
        this.stock = event;
    }
}
@NgModule({
    imports:      [ BrowserModule],
    declarations: [ AppComponent, OrderComponent,
                    PriceQuoterComponent],
    bootstrap:    [ AppComponent ]
})
class AppModule { }

platformBrowserDynamic().bootstrapModule(AppModule);

When the value of the buy input property on OrderComponent changes, its setter displays the message “Placed order ...”, showing the stockSymbol and the bidPrice.

Listing 6.7. order.ts
import {Component, Input} from '@angular/core';
import {Stock} from './stock';

@Component({
    selector: 'order-processor',
    template: `{{message}}`,
    styles:[`:host {background: cyan;}`]
})
export class OrderComponent {

    message:string = "Waiting for the orders...";

    private _stock: Stock;

    @Input() set stock(value: Stock ){
        if (value && value.bidPrice != undefined) {
            this.message = `Placed order to buy 100 shares of
            {value.stockSymbol} at $${value.bidPrice.toFixed(2)}`;
        }
    }

    get stock(): Stock{
        return this._stock;
    }
}

The screenshot in figure 6.5 was taken after the user clicked the Buy button when the price of the IBM stock was $12.17. PriceQuote-Component is rendered on top, and Order-Component is at the bottom. They’re self-contained and loosely coupled.

Figure 6.5. Running the mediator example

Tip

Don’t start implementing the UI components of your application until you’ve identified your mediators, the custom reusable components, and the means of communication between them.

The Mediator design pattern is a good fit for the online auction as well. Imagine the last minutes of a bidding war for a hot item. Users monitor frequently updated bids and click the button to increase their bids.

An alternative implementation of Mediator

In this section, you saw how sibling components use their parent as a mediator. If components don’t have the same parent or aren’t displayed at the same time (the router may not display the required component at the moment), you can use an injectable service as a mediator. Whenever the component is created, the mediator service is injected, and the component can subscribe to events emitted by the service (as opposed to using @Input() parameters like OrderComponent did).

If you’d like to see this in action, read the “Providing search results to HomeComponent” section in the hands-on exercise in chapter 8. Check the code of the ProductService that plays the role of the mediator. This service defines the searchEvent: Event-Emitter variable, which is used by SearchComponent to emit the data entered by the user. HomeComponent subscribes to the searchEvent variable to receive the text entered by the user in the search form.

6.1.3. Changing templates at runtime with ngContent

In some cases, you’ll want to be able to dynamically change the content of a component’s template at runtime. In AngularJS, this was known as transclusion, but the new term for it is projection. In Angular, you can project a fragment of the parent component’s template onto its child by using the ngContent directive. The syntax is pretty simple and requires two steps:

  1. In the child component’s template, include the tags <ng-content></ng-content> (the insertion point).
  2. In the parent component, include the HTML fragment that you want to project into the child’s insertion point between tags representing the child component (such as <my-child>):
    template: `
      ...
      <my-child>
        <div>Passing this div to the child</div>
      </my-child>
      ...
    `

In this example, the parent component won’t render the content placed between <my-child> and </my-child>. The following listing illustrates this technique.

Listing 6.8. basic-ng-content.ts

We’ll also use this example to illustrate how the Shadow DOM and Angular’s View-Encapsulation work. Have you noticed that both parent and child components use the .wrapper style in their outermost <div> elements? In a regular HTML page, this would mean both parent and child would be rendered with the same style. We’ll show that it’s possible to encapsulate styles in child components so they don’t conflict with parent styles if their names are the same.

Figure 6.6 shows the running application in ViewEncapsulation.Native mode with the Developer Tools panel open. The ChildComponent got the HTML content from AppComponent and created Shadow DOM nodes for parent and child (see #shadow-root on the right). Note that the .wrapper style from the parent’s <div> (cyan background, if you’re reading this book in color) wasn’t applied to the child’s <div> also using the .wrapper style, which is rendered in a light green background. The child’s #shadow-root acts as a wall protecting the child’s styles from inheriting the parent’s styles.

Figure 6.6. Running basic-ng-content.ts with ViewEncapsulation.Native

The screenshot in figure 6.7 was taken after changing encapsulation to ViewEncapsulation.Emulated. The DOM structure is different, and there are no longer any #shadow-root nodes. Angular generated additional attributes for the parent’s and child’s elements to implement encapsulation, but the UI is rendered the same way.

Figure 6.7. Running basic-ng-content.ts with ViewEncapsulation.Emulated

Figure 6.8 shows the same example running with encapsulation set to ViewEncapsulation.None. In this case, all of the parent’s and child’s elements were merged into the main DOM tree, and the styles weren’t encapsulated—the entire window is shown with the parent’s light green background.

Figure 6.8. Running basic-ng-content.ts with ViewEncapsulation.None

Projecting into multiple areas

A component can have more than one <ng-content> tag in its template. Let’s consider an example where a child component’s template is split into three areas: header, content, and footer. The HTML markup for the header and footer could be projected by the parent component, and the content area could be defined in the child component. To implement this, the child component needs to include two separate pairs of <ng-content></ng-content> populated by the parent (header and footer).

To ensure that the header and footer content will be rendered in the proper <ng-content> areas, you’ll use the select attribute, which can be any valid selector (a CSS class, a tag name, and so on). The child’s template could look like this:

<ng-content select=".header"></ng-content>
<div>This content is defined in child</div>
<ng-content select=".footer"></ng-content>

The content that arrives from the parent will be matched by the selector and rendered in the corresponding area. Here’s the complete code to implement this.

Listing 6.9. ng-content-selector.ts
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { NgModule, Component}      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

@Component({
  selector: 'child',
  styles: ['.child {background: lightgreen;}'],
  template: `
    <div class="child">
     <h2>Child</h2>
      <ng-content select=".header" ></ng-content>
      <div>This content is defined in child</div>
      <ng-content select=".footer"></ng-content>
    </div>
  `
})
class ChildComponent {}

@Component({
  selector: 'app',
  styles: ['.app {background: cyan;}'],
  template: `
    <div class="app">
     <h2>Parent</h2>
      <div>This div is defined in the Parent's template</div>
      <child>
        <div class="header" >Child got this header from parent {{todaysDate}}
        </div>
        <div class="footer">Child got this footer from parent</div>
      </child>
    </div>
  `
})
class AppComponent {
  todaysDate: string = new Date().toLocaleDateString();
}

@NgModule({
  imports:      [ BrowserModule],
  declarations: [ AppComponent, ChildComponent],
  bootstrap:    [ AppComponent ]
})
class AppModule { }

platformBrowserDynamic().bootstrapModule(AppModule);

Note that you use property binding in App-Component to include today’s date in the header. The projected HTML can only bind the properties visible in the parent’s scope, so you can’t use the child’s properties in the parent’s binding expression.

Running this example will render the page shown in figure 6.9. The ngContent directive with the select attribute allows you to create a universal component with a view divided into several areas that get their markup from the outside.

Figure 6.9. Running ng-content-select.ts

Direct binding to innerHTML

You can bind a component property with HTML content directly to template, as in this example:

<p [innerHTML]="myComponentProperty"></p>

But using ngContent is preferable to binding to innerHTML for the following reasons:

  • innerHTML is a browser-specific API, whereas ngContent is platform independent.
  • With ng-content, you can define multiple slots where the HTML fragments will be inserted.
  • ngContent allows you to bind the parent component’s properties into projected HTML.

6.2. Component lifecycle

Various events happen during the lifecycle of an Angular component. When a component is created, the change-detection mechanism (explained in the next section) begins monitoring the component. The component is initialized, added to the DOM, and rendered so the user can see it. After that, the state of the component (the values of its properties) may change, causing re-rendering of the UI; and finally, the component is destroyed.

Figure 6.10 shows the lifecycle hooks (callbacks) where you can add custom code if need be. The callbacks shown on the light gray background will be invoked only once, and those on the darker background multiple times.

Figure 6.10. A component’s lifecycle

The user sees the component after the initialization phase is complete. Then the change-detection mechanism ensures that the component’s properties stay in sync with its UI. If the component is removed from the DOM tree as a result of the router’s navigation or a structural directive (such as ngIf), Angular initiates the destroy phase.

The constructor is invoked first when the instance of the component is being created, but the component’s properties aren’t initialized yet in the constructor. After the constructor’s code is complete, Angular will invoke the following callbacks if you implemented them:

  • ngOnChanges()—Called when a parent component modifies (or initializes) the values bound to the input properties of a child. If the component has no input properties, ngOnChanges() isn’t invoked. If you wish to implement a custom change-detection algorithm, it must be placed in DoCheck(). But implementing manual change detection there may be costly, because DoCheck() is invoked after every change-detection cycle.
  • ngOnInit()—Invoked after the first invocation of ngOnChanges(), if any. Although you might initialize some component variables in the constructor, the properties of the component aren’t ready yet. By the time ngOnInit() is invoked, the component’s properties will have been initialized.
  • ngAfterContentInit()—Invoked when the child component’s state is initialized, if you used the ngContent directive to pass some HTML code to it.
  • ngAfterContentChecked()—Invoked on the child component that used ngContent after it gets the content from the parent (or during the change-detection phase), if the bindings used in ngContent change.
  • ngAfterViewInit()—Invoked when the binding on the component’s template is complete. The parent component is initialized first, and, if it has children, this callback is invoked after all children are ready.
  • ngAfterViewChecked()—Invoked when the change-detection mechanism checks whether there are any changes in the component template’s bindings. This callback may be called more than once as the result of modifications in this or other components.

Whenever you see the word Content in the name of the lifecycle callback method, that method is applied if content is projected using <ng-content>. When you see the word View in the name of the callback method, it applies to the template of the component. The word Checked means the component’s changes are applied and the component is synchronized with the DOM.

Some applications may need to invoke specific business logic whenever the value of a property changes. For example, financial applications need to log each of a trader’s steps, so if a trader places a buy order at $101 and then immediately changes the price to $100, this must be tracked in a log file. This may be a good use case for adding logging in the DoCheck() callback.

During the destruction phase, your application may clean up system resources. Say your component is subscribed to an application-level service that keeps track of the application state (such as an application store offered by the Redux library). When Angular destroys this component, it should unsubscribe from the state service in the ngOnDestroy() callback.

When not to write code in constructors

In the online auction application, you inject ProductService in the constructor of HomeComponent and invoke the getProducts() method right there. If the getProducts() method needed to use values of the component’s properties, you’d move the invocation of this method to ngOnInit()to ensure that all properties were initialized by the time you called getProducts(). The other reason to move code from the constructor to ngOnInit() is to keep the constructor’s code light without starting any long-running synchronous functions from there.

Note

Each lifecycle callback is declared in the interface with a name that matches the name of the callback without the prefix ng. For example, if you’re planning to implement functionality in the ngOnChanges() callback, add implements OnChanges to your class declaration.

For more information about the component lifecycle, read the Angular documentation on lifecycle hooks at http://mng.bz/6huZ. In the next section, you’ll see an example that uses one of the lifecycle hooks.

6.2.1. Using ngOnChanges

Let’s illustrate component lifecycle hooks using ngOnChanges(). This example will include parent and child components, and the latter will have two input properties: greeting and user. The first property is a string, and the second is an Object with one property, name. To understand why the ngOnChanges() callback may or may not be invoked, you need to be familiar with the concept of mutable versus immutable objects.

Mutable vs. immutable

JavaScript strings are immutable, which means when a string value is created in memory, it will never change. Consider the following code snippet:

var greeting = "Hello";
greeting = "Hello Mary";

The first line creates the value Hello at a certain memory location, such as @287651. The second line doesn’t change the value at that address but creates the new string Hello Mary at a different location, such as @286777. Now you have two strings in memory, and each of them is immutable.

What happens to the variable greeting? Its value changed, because it pointed initially at one memory location and then to another.

JavaScript objects are mutable, which means after the object instance is created at a certain memory location, it stays there even if the values of its properties change. Consider the following code:

var user = {name: "John"};
user.name = "Mary";

After the first line, the object is created, and the user variable points at a certain memory location, such as @277500. The string John has been created at another memory location, such as @287600, and the user.name variable stores the reference to this address.

After the second line is executed, the new string Mary is created at another location, such as @287700, and the user.name variable stores the reference to this new address. But the user variable still stores the memory address @277500. In other words, you mutated the content of the object @277500.

Let’s add the hook ngOnChanges() to the child component to demonstrate how it intercepts modifications of the input properties. This application has parent and child components. The child has two input properties (greetings and user) and one regular property (message). The user can modify the values of the input properties of the child. Let’s demonstrate what property values will be given to the ngOnChanges() method if it’s invoked.

Listing 6.10. ng-onchanges-with-param.ts

When Angular invokes ngOnChanges(), it provides the values of each modified input property. Each modified value is represented by an instance of the SimpleChange object that contains the current and previous values of the modified input property. The Simple.change.isFirstChange() method allows you to determine whether the value was set for the first time or if it’s being updated. You use JSON.stringify() to pretty-print the received values.

Note

TypeScript has a structural type system, so the type of the argument changes of ngOnChanges() is specified by including a description of the expected data. As an alternative, you could declare an interface (such as interface IChanges {[key: string]: SimpleChange};), and the function signature would look like ngOnChanges(changes: IChanges). The preceding declaration of the user property in AppComponent is yet another example of using the structural type.

Let’s see if changing greeting and user.name in the UI results in the invocation of ngOnChanges() on the child component. Figure 6.11 shows a screenshot after we ran listing 6.10 with the Chrome Developer Tools open.

Figure 6.11. Initial invocation of ngOnChanges()

Initially, when the application applied the binding to the child component’s input properties, they had no values. The ngOnChanges() callback was invoked, and the previous values of both greeting and user were changed from {} to Hello and {name: "John"}, respectively.

Enabling production mode

In figure 6.11, you can see a message stating that Angular 2 is running in development mode, which performs assertions and other checks within the framework. One such assertion verifies that a change-detection pass doesn’t result in additional changes to any bindings (for example, your code doesn’t modify the UI in lifecycle callbacks).

To enable production mode, invoke enableProdMode() in your application before invoking the bootstrap() method. Enabling production mode will result in better performance in the browser.

Let’s have the user change the values in all the input fields. After adding the word dear to the Greeting field and moving the focus away, Angular’s change-detection mechanism refreshes the binding to the child’s immutable input property, greeting; invokes the ngOnChanges() callback; and prints the previous value as Hello and the current one as Hello dear, as shown in figure 6.12.

Figure 6.12. Invocation of ngOnChanges() after the greeting is changed

Now suppose the user adds the word Smith in the User Name field and moves the focus from this field: no new messages are printed on the console, as shown in figure 6.13. That’s because the user changed only the name property of the mutable user object; the reference to the user object itself didn’t change. This explains why ngOnChanges() wasn’t invoked. Changing the value in the message property of the ChildComponent didn’t invoke ngOnChanges() either, because this property wasn’t annotated with @Input.

Figure 6.13. ngOnChanges() wasn’t invoked this time.

Note

Although Angular doesn’t update bindings to input properties if the object reference hasn’t changed, the change-detection mechanism still catches property updates on each object property. This is why John Smith, the new value of the User Name in the child component, has been rendered.

Earlier, in section 6.1.1, you used a setter to intercept the moment when the value of the input parameter changed. You could have used ngOnChanges() instead of the setters there. There are use cases when using ngOnChanges() instead of a setter isn’t an option, and you’ll see why in the hands-on section of this chapter.

6.3. A high-level overview of change detection

Angular’s change-detection (CD) mechanism is implemented in zone.js (a.k.a. the Zone). Its main purpose is to keep the changes in the component properties (the model) and the UI in sync. CD is initiated by any asynchronous event that happens in the browser (the user clicked a button, data is received from a server, a script invoked the setTimeout() function, and so on).

When CD runs its cycle, it checks all the bindings in the component’s template. Why might binding expressions need to be updated? Because one of the component’s properties changed.

Note

The CD mechanism applies changes from a component’s property to the UI. CD never changes the value of the component’s property.

You can think of an application as a tree of components with the root component on the top of this tree. When Angular compiles component templates, each component gets its own change detector. When CD is initiated by the Zone, it makes one pass starting from the root down to the leaf components, trying to see whether the UI of each component needs to be updated.

Angular implements two CD strategies: Default and OnPush. If all components use the Default strategy, the Zone checks the entire component tree regardless of where the change happened. If a particular component declares the OnPush strategy, the Zone checks this component and its children only if the bindings to the component’s input properties have changed. To declare the OnPush strategy, you just need to add the following line to the component’s template:

changeDetection: ChangeDetectionStrategy.OnPush

Let’s get familiar with these strategies using three components: the parent, child, and grandchild shown in figure 6.14.

Figure 6.14. Change-detection strategies

Let’s say a property of the parent was modified. CD will begin checking the component and all of its descendants. The left side of figure 6.14 illustrates the default CD strategy: all three components are checked for changes.

The right side of figure 6.14 illustrates what happens when the child component has the OnPush CD strategy. CD starts from the top, but it sees that the child component has declared the OnPush strategy. If no bindings to the input properties of the child component have changed, CD doesn’t check either the child or the grandchild.

Figure 6.14 shows a small application with only three components, but real-world apps can have hundreds of components. With the OnPush strategy, you can opt out of CD for specific branches of the tree.

Figure 6.15 shows a CD cycle caused by an event in the GrandChild1 component. Even though this event happened in the leaf component, the CD cycle starts from the top; it’s performed on each branch except the branches that originate from a component with the OnPush CD strategy and that has no changes in the bindings to its input properties. Components excluded from this CD cycle are shown on the white background.

Figure 6.15. Excluding a branch from a CD cycle

This was a brief overview of the CD mechanism, which is probably the most sophisticated module of Angular. You should learn about CD in depth only if you need to work on performance-tuning a UI-intensive application, such as a data grid containing hundreds of cells with constantly changing values. For more details about change detection in Angular, read the article “Change Detection in Angular 2” by Victor Savkin at http://mng.bz/bD6v.

6.4. Exposing a child component’s API

You’ve learned how a parent component can pass data to its child using bindings to input properties. But there are other cases when the parent just needs to use the API exposed by the child. We’ll show you an example that illustrates how a parent component can use the child’s API from both the template and the TypeScript code.

Let’s create a simple application in which a child component has a greet() method that will be invoked by the parent. To illustrate different techniques, the parent will use two instances of the same child component. These instances have different template variable names:

<child #child1></child>
<child #child2></child>

Now you can declare a variable in your TypeScript code, annotated with @ViewChild. This annotation is provided by Angular to get a reference to a child component, and you’ll use it with the first child:

@ViewChild('child1')
firstChild: ChildComponent;
...
this.firstChild.greet('Child 1');

This code instructs Angular to find the child component identified by the template variable child1 and place the reference to this component into the firstChild variable.

To illustrate another technique, you can access the second child component not from the TypeScript code but from the parent’s template. It’s as simple as this:

<button (click)="child2.greet('Child 2')">Invoke greet() on child 2</button>

The full code illustrating both techniques follows.

Listing 6.11. exposing-child-api.ts
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { NgModule, Component, ViewChild, AfterViewInit } from
 '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

@Component({
    selector: 'child',
    template: `<h3>Child</h3>`

})

class ChildComponent {
    greet(name) {
        console.log(`Hello from{name}.`);
    }
}

@Component({
    selector: 'app',
    template: `
    <h1>Parent</h1>
    <child #child1></child>
    <child #child2></child>

    <button (click)="child2.greet('Child 2')">Invoke greet() on child 2
     </button>
  `
})
class AppComponent implements AfterViewInit {
    @ViewChild('child1')
    firstChild: ChildComponent;

    ngAfterViewInit() {
        this.firstChild.greet('Child 1');
    }
}

@NgModule({
    imports:      [ BrowserModule],
    declarations: [ AppComponent, ChildComponent],
    bootstrap:    [ AppComponent ]
})
class AppModule { }

platformBrowserDynamic().bootstrapModule(AppModule);

When you run this app, it prints “Hello from Child 1.” on the console. Click the button, and it will print “Hello from Child 2.” as shown in figure 6.16.

Figure 6.16. Accessing the child’s API

Updating the UI from lifecycle hooks

Listing 6.11 uses the ngAfterViewInit() component lifecycle hook to invoke the API on the child. If the child’s greet() method doesn’t change the UI, this code works fine; but if you try to change the UI from greet(), Angular will throw an exception because the UI is changed after ngAfterViewInit() was fired. That’s because this hook is called in the same event loop for both parent and child components.

There are two ways to deal with this issue. You can run the application in production mode so Angular won’t do the additional bindings check, or you can use setTimeout() for the code updating the UI so it runs in the next event loop.

6.5. Hands-on: adding a rating feature to the online auction

In this section, you’ll add a rating feature to the auction. Previous versions of this application just displayed the rating, but now you want to let users rate a product. In chapter 4, you created the Produce Details view; here you’ll add the Leave a Review button, which allows users to navigate to a view where they can assign one to five stars to a product and enter a review. A fragment of the new Produce Details view is shown in figure 6.17.

Figure 6.17. The Produce Details view

StarsComponent will have an input property, which will be modified. The newly added rating’s value needs to be communicated to its parent ProductItemComponent.

Note

We’ll use the auction application developed in chapter 5 as a starting point for this exercise. If you prefer to see the final version of this project, browse the source code in the auction folder for chapter 6. Otherwise, copy the auction folder from chapter 5 to a separate location. Then copy the package.json file from the auction folder for chapter 6, run npm install, and follow the instructions in this section.

Installing a type-definition file

In this version of the app, you want to use the Array.fill() method in the Stars-Component; but this API is available only in ES6, and the TypeScript compiler will complain. You already have the ES6 shim installed locally as a part of the core.js package. But because you want to keep ES5 as a target for transpiling, you need an ES6 shim type-declaration file so this API can be recognized by the TypeScript compiler.

You’ll install an additional type definition file (see appendix B) from the npm repository. In general, you’ll need to install type-definition files whenever you add a third-party JavaScript library.

Follow these steps:

  1. Install the type-definition file for ES6 shim. To do so, open a command window and run the following command:
    npm install @types/es6-shim --save-dev
    This command installs the es6-shim.d.ts file in the node_modules/@types directory and saves this configuration in the devDependencies section of package.json file. You should be using TypeScript 2.0, which knows to look for type-definition files in the @types directory.
  2. Modify the code of StarsComponent. The new version should work in two modes: read-only for displaying stars based on the data provided by ProductService, and writable for allowing users to click stars to set a new rating value. Figure 6.17 shows the rendering of ProductDetailComponent (the parent) where StarsComponent (the child) is in read-only mode. If the user clicks the Leave a Review button, read-only mode should be turned off. You’ll add a readonly input variable to toggle this mode. The second input variable, rating, is for assigning ratings. You’ll also add one output variable, ratingChange, that will emit an event with the newly set rating; this will be used by the parent component to recalculate the average rating. When the user clicks one of the stars, it will invoke the fillStarsWithColor() method, which will assign the value to rating and emit the rating’s value by dispatching an event. Modify the code of the stars.ts file so it looks like the following listing.
    Listing 6.12. stars.ts
    import {Component, EventEmitter, Input, Output} from '@angular/core';
    
    @Component({
      selector: 'auction-stars',
      styles: [`.starrating { color: #d17581; }`],
      templateUrl: 'app/components/stars/stars.html'
    })
    export default class StarsComponent {
      private _rating: number;
      private stars: boolean[];
    
      private maxStars: number =5;
    
      @Input() readonly: boolean = true;
    
      @Input() get rating(): number {
        return this._rating;
      }
    
      set rating(value: number) {
        this._rating = value || 0;
        this.stars = Array(this.maxStars).fill(true, 0, this.rating);
      }
    
      @Output() ratingChange: EventEmitter<number> = new EventEmitter();
    
      fillStarsWithColor(index) {
    
        if (!this.readonly) {
          this.rating = index + 1;
          this.ratingChange.emit(this.rating);
        }
      }
    }
    You use the setter for the input rating. This setter can be invoked either from within StarsComponent (to render an existing rating) or from its parent (when the user clicks the stars). In this application, using ngOnChanges() wouldn’t work, because it would be invoked only once by the parent when StarsComponent was created. Note the use of the ES6 method fill() in the rating() setter. You populate the stars array with the value true from element zero to whatever the rating value is. For each star that has to be filled with color, you store the value true; and for empty stars, you store false.
  3. Modify the template of StarsComponent in the stars.html file as shown in listing 6.13. Using the ngFor directive, you loop through the stars array, which stores Boolean values. You’ll use the stock images that come with the Bootstrap library for filled and empty stars (see http://getbootstrap.com/components). Based on the value of the array’s element, you render either a star filled with color or an empty one. When the user clicks the star, you pass its index to the function fillStarsWithColor().
    Listing 6.13. Revised stars.html
    <p>
      <span *ngFor="let star of stars; let i = index"
            class="starrating glyphicon glyphicon-star"
            [class.glyphicon-star-empty]="!star"
            (click)="fillStarsWithColor(i)">
      </span>
      <span *ngIf="rating">{{rating | number:'.0-2'}} stars</span>
    </p>
    The number pipe formats the rating’s value to show two digits after the decimal point.
  4. Modify the template of ProductDetailComponent. ProductDetailComponent has a Leave a Review button that should provide a means for rating a product and leaving a review. Clicking this button will toggle the visibility of a <div> that allows users to click stars and enter a review, as shown in figure 6.18.
    Figure 6.18. The Leave a Review view

    Here StarsComponent works in editable mode, and the user can give the selected product up to five stars. This is how the template implementing the view in figure 6.18 could look:
    <div [hidden]="isReviewHidden">
         <div><auction-stars [(rating)]="newRating"
           [readonly]="false" class="large"></auction-stars></div>
         <div><textarea [(ngModel)]="newComment"></textarea></div>
         <div><button (click)="addReview()" class="btn">Add review
          </button></div>
     </div>
    readonly mode is turned off. Note that you use two-way binding in two places: [(rating)] and [(ngModel)]. Earlier in this chapter, we discussed the use of the ngModel directive for two-way binding; but if you have an input property (such as rating) and an output property that has the same name plus the suffix Change (such as ratingChange), you’re allowed to use the [()] syntax with such properties. The Leave a Review button toggles the visibility of the preceding <div>. This is how it can be implemented:
    <button (click)="isReviewHidden = !isReviewHidden"
                    class="btn btn-success btn-green">Leave a Review</button>
    Replace the content of the product-detail.html file with the following.
    Listing 6.14. product-detail.html
    <div class="thumbnail">
        <img src="http://placehold.it/820x320">
        <div>
            <h4 class="pull-right">{{ product.price }}</h4>
            <h4>{{ product.title }}</h4>
            <p>{{ product.description }}</p>
        </div>
        <div class="ratings">
            <p class="pull-right">{{ reviews.length }} reviews</p>
            <p><auction-stars [rating]="product.rating" ></auction-stars></p>
        </div>
    </div>
    <div class="well" id="reviews-anchor">
        <div class="row">
            <div class="col-md-12"></div>
        </div>
        <div class="text-right">
            <button (click)="isReviewHidden = !isReviewHidden"
                    class="btn btn-success btn-green">Leave a Review</button>
        </div>
    
        <div [hidden]="isReviewHidden">
            <div><auction-stars [(rating)]="newRating"
              [readonly]="false" class="large"></auction-stars></div>
            <div><textarea [(ngModel)]="newComment"></textarea></div>
            <div><button (click)="addReview()" class="btn">Add review</button>
            </div>
    </div>
    
        <div class="row" *ngFor="#review of reviews">
            <hr>
            <div class="col-md-12">
                <auction-stars [rating]="review.rating"></auction-stars>
                <span>{{ review.user }}</span>
                <span class="pull-right">{{ review.timestamp | date: 'shortDate' }}</span>
                <p>{{ review.comment }}</p>
            </div>
        </div>
    </div>
    After typing a review and giving stars to a product, the user clicks the Add Review button, which invokes addReview() on the component. Let’s implement it in TypeScript.
  5. Modify the product-detail.ts file. Adding a review should do two things: send the newly entered review to the server and recalculate the average product rating on the UI. You’ll recalculate the average on the UI, but you won’t implement the communication with the server; you’ll log the review on the browser’s console. Then you’ll add the new review to the array of existing reviews. The following code fragment from ProductDetailComponent implements this functionality:
    addReview() {
      let review = new Review(0, this.product.id, new Date(), 'Anonymous',
          this.newRating, this.newComment);
      console.log("Adding review " + JSON.stringify(review));
      this.reviews = [...this.reviews, review];
    
      this.product.rating = this.averageRating(this.reviews);
    
      this.resetForm();
    }
    
    averageRating(reviews: Review[]) {
      let sum = reviews.reduce((average, review) => average + review.rating, 0);
      return sum / reviews.length;
    }
    After creating a new instance of the Review object, you need to add it to the reviews array. The spread operator lets you write it in an elegant way:
    this.reviews = [...this.reviews, review];
    The reviews array gets the values of all existing elements (...this.reviews) plus the new one (review). The recalculated average is assigned to the rating property, which is propagated to the UI via binding. What’s left? Replace the content of the product-detail.ts file with the following code, and this hands-on exercise is over!
    Listing 6.15. product-detail.ts
    import {Component} from '@angular/core';
    import {ActivatedRoute} from '@angular/router';
    import {Product, Review, ProductService} from
    
     '../../services/product-service';
    import StarsComponent from '../stars/stars';
    
    @Component({
      selector: 'auction-product-page',
      styles: ['auction-stars.large {font-size: 24px;}'],
      templateUrl: 'app/components/product-detail/product-detail.html'
    })
    export default class ProductDetailComponent {
      product: Product;
      reviews: Review[];
    
      newComment: string;
      newRating: number;
    
      isReviewHidden: boolean = true;
    
      constructor(route: ActivatedRoute, productService: ProductService) {
    
        let prodId: number = parseInt(route.snapshot.params['productId']);
        this.product = productService.getProductById(prodId);
    
        this.reviews = productService.getReviewsForProduct(this.product.id);
      }
    
      addReview() {
        let review = new Review(0, this.product.id, new Date(), 'Anonymous',
            this.newRating, this.newComment);
        console.log("Adding review " + JSON.stringify(review));
        this.reviews = [...this.reviews, review];
        this.product.rating = this.averageRating(this.reviews);
    
        this.resetForm();
      }
    
      averageRating(reviews: Review[]) {
        let sum = reviews.reduce((average, review) => average + review.rating,0);
        return sum / reviews.length;
      }
    
      resetForm() {
        this.newRating = 0;
        this.newComment = null;
        this.isReviewHidden = true;
      }
    }

6.6. Summary

Any Angular application is a hierarchy of components that need to communicate with each other. This chapter was dedicated to covering different ways of arranging such communication. Binding to the component’s input properties and dispatching events via the output properties allow you to create loosely coupled components. By means of its change-detection mechanism, Angular intercepts changes in components’ properties to ensure that their bindings are updated.

Each component goes through a certain set of events during its lifecycle. Angular provides several lifecycle hooks where you can write code to intercept these events and apply custom logic there.

These are the main takeaways for this chapter:

  • Parent and child components should avoid direct access to each other’s internals but should communicate via input and output properties.
  • A component can emit custom events via its output properties, and these events can carry an application-specific payload.
  • Communications between unrelated components can be arranged by using the Mediator design pattern.
  • A parent component can pass one or more template fragments to a child at runtime.
  • Each Angular component lets you intercept major lifecycle events of a component and insert application-specific code there.
  • The Angular change-detection mechanism automatically monitors changes to components’ properties and updates the UI accordingly.
  • You can mark selected branches of your app component tree to be excluded from the change-detection process.
..................Content has been hidden....................

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