Skip to content

Data Binding & Pipes

Adarsh Kumar Maurya edited this page Dec 18, 2018 · 2 revisions

Introduction

There's more to data binding than just displaying component properties. Welcome back to Angular: Getting Started and in this chapter, we explore more data binding features and transform bound data with pipes. To provide a great interactive user experience, we want to bind DOM elements to component properties so the component can change the look and feel as needed. We can use bindings to change element colors or styles based on data values, update font size based on user preferences, or set an image source from a database field, and we want notification of user actions and other events from the DOM so the component can respond accordingly.

For example, we respond to a click on a button to hide or show images. And sometimes, we want the best of both worlds using two-way binding to set an element property and receive event notifications of user changes to that property.

In this chapter,

  • We'll use Angular's property binding to set HTML element properties in the DOM, - We walk through how to handle user events such as a button click with event binding, and
  • How to handle user input with two-way binding.
  • Lastly, we'll discover how to transform bound data with pipes.

And here once again is our application architecture.

                              Application Architecture


                               -> Welcome Component
                              |
 index.html -> App Component->|-> Product List Component ---
                              |           |                  | --> Star Component
                              |           V                  |  
                               -> Product Detail Component---
               Product Data
                 Service

We have the first cut of our Product List Component, but it doesn't have any interactivity. In this chapter, we'll use data binding features to add interactivity to the Product List Component. Let's get started.

Property Binding

Property binding allows us to set a property of an element to the value of a template expression.

<img [src] = 'product.imageUrl'
       |            |
Element Property  Template Expression

Here we bind the source property of the image element to the imageUrl property of the product, effectively defining the source of the image from information in our component class. Our binding target is always enclosed in square brackets e.g[] to the left of the equals and identifies a property of the element. The binding source is always enclosed in quotes('') to the right of the equals and specifies the template expression. For comparison, here is a similar binding using interpolation.

<img src={{product.imageUrl}}>

Note that in this case, the element property is not enclosed in square brackets and the template expression is enclosed in curly braces with no quotes. If you need to include the template expression as part of a larger expression, such as this example, you may need to use interpolation.

<img src ='http://openclipart.org/{{product.imageUrl}}'

Like interpolation, property binding is one way from the source class property to the target element property. If effectively allows us to control our template's DOM from our component class.

Now let's add some property binding to our sample application. Here we are back in the editor looking at the product list component and its associated template. Let's use property binding to bind the source of our product image.

We use an image element to display our product image, and we use property binding to bind the image's source, or src property. So we enclose the src in square brackets. On the right side of the equals, we define the template expression in quotes. We want to bind to the product's imageUrl property from the ProductListComponent class.

Let's also use property binding to bind the title property of the image to the product's name. Let's check out the result in the browser.

<tbody>
                    <tr *ngFor='let product of products'>
                        <td>
                            <img [src]='product.imageUrl'
                                 [title]='product.productName'>
                        </td>
                        <td>{{ product.productName }}</td>
                        <td>{{ product.productCode}}</td>
                        <td>{{ product.releaseDate}}</td>
                        <td>{{ product.price}}</td>
                        <td>{{ product.starRating}}</td>
                    </tr>
                </tbody>

Whoa, they're big, but we do see images. If we hover over an image, we see the image title, but this image is rather large for display in our table. Let's use property binding to set some style properties. Let's add properties for the image width and image margin to our component class. The image width is a number, so we specify its type with a colon and then number. Let's set it to 50. The image margin is also a number, and we'll set it to 2. Back in the template, we use property binding to bind the image styles. We want to bind the style property width in pixels. We'll bind that to the image width property from the component class. Notice that we don't prefix this property with product because imageWidth is a property of the component class, not the product object. And we do the same with the style margin and pixels, and set that to the imageMargin class property.

<tbody>
                    <tr *ngFor='let product of products'>
                        <td>
                            <img [src]='product.imageUrl'
                                 [title]='product.productName'
                                 [style.width.px]='imageWidth'
                                 [style.margin.px]='imageMargin'>
                        </td>
                        <td>{{ product.productName }}</td>
                        <td>{{ product.productCode}}</td>
                        <td>{{ product.releaseDate}}</td>
                        <td>{{ product.price}}</td>
                        <td>{{ product.starRating}}</td>
                    </tr>
                </tbody>

So now we've seen how to use property binding to bind several properties of the image element to properties of the component's class. Looking again at the browser, that looks much better, but our images are always displayed. The Show Image button doesn't work yet. To hook up the button, we need to respond to user events. Let's do that next.

Handling Events with Event Binding

So far, all of our data binding has been one way from the component class property to the element property.

Class                                               Template

export class ListComponent {
pageTitle: string = 'Product List';-----------><h1> {{pageTitle}} </h1> 
products: any[] =[...];-----------------------><img [src] = 'product.imageUrl'>                                                
}

But there are times we need to send information the other way to respond to user events. For example, to perform an operation when the user clicks a button, a component listens for user actions using event binding as shown here.

Template                                           Class
<h1>{{pageTitle}}</h1>
<img [src]='product.imageUrl'>
<button (click) ='toggleImage()'> -------------> toggleImage():void {...}
          |             |
       Target Event()  Template Statement''

Notice how similar the syntax is to property binding. In this example, the component listens for the click event on a button. The name of the bound event is enclosed in parentheses, identifying it as the target event. To the right of the equals is the template statement. This is often the name of a component class method followed by open and closing parentheses and enclosed in quotes.

If the defined event occurs, the template statement is executed, calling the specified method in the component. Wondering where you might find a list of valid DOM events? Check out this link. The Mozilla Developer Network provides a thorough list of standard events along with some documentation. Now let's give event binding a try.

In this demo, we want to implement our Show Image button. First, we define a class property that keeps track of whether the images are currently displayed. We'll call that property showImage. Since this property is true or false, we define its type as boolean, and let's set its default value to false so the images are not displayed when the page is first loaded.

export class ProductListComponent {
    pageTitle: string = 'Product List';
    imageWidth: number = 50;
    imageMargin: number =2;
    showImage: boolean =false;
...
    toggleImage(): void {
          this.showImage = !this.showImage;
     }

Next we build the method that the event binding will call. By convention, methods are normally created after all of the properties are defined, so we'll put it down here. Let's name the method toggleImage. Notice that TypeScript does not require any keywords such as function. Following the method name with open and closing parentheses identifies it as a method. Our method won't have a return type, so we specify the return type as void. The body of the method simply toggles the state of the showImage property. Back in the template, we are ready to set up the event binding.

 <thead>
                    <tr>
                        <th>
                            <button class='btn btn-primary'
                            (click)='toggleImage()'>
                                Show Image
                            </button>
                        </th>
                        <th>Product</th>
                        <th>Code</th>
                        <th>Available</th>
                        <th>Price</th>
                        <th>5 Star Rating</th>
                    </tr>
                </thead>

...
  <tbody>
                    <tr *ngFor='let product of products'>
                        <td>
                            <img *ngIf='showImage'
                                [src]='product.imageUrl'
                                 [title]='product.productName'
                                 [style.width.px]='imageWidth'
                                 [style.margin.px]='imageMargin'>
                        </td>
...

On the button element, we define the click as the target event by enclosing it in parentheses. We assign it to our method enclosed in quotes. When the user clicks the button, the binding calls our method, so the only thing left is to actually hide or show the image. Recall how we add logic to add or remove HTML elements from the DOM? If you've said the *ngIf directive, you are right. We'll add ngIf to the image element. We only want this image element if the showImage flag is true, so we type *ngIf='showImage'. The image element will then only be added to the DOM if showImage is true.

```html
 <thead>
                    <tr>
                        <th>
                            <button class='btn btn-primary'
                            (click)='toggleImage()'>
                                {{showImage ?'Hide' : 'Show'}} Image
                            </button>

Let's see if this works. Are we ready to give it a try? Click the button, ahh, and the image appears. Click the button again, and the image disappears. Oh, very cool. The only odd thing is the button text. When the image is displayed, the button text should change to Hide Image. So up here where we have the button text, let's use interpolation. When showImage is true, we want the button text to say Hide Image, and when showImage is false, we want the button text to say Show Image. We accomplish this using a JavaScript conditional operator. We specify the condition, which is showImage, and a question mark, then we specify the true expression. So when showImage is true, we want to display Hide Image. Then we add a colon and the false expression, so when showImage is false, we want it say Show Image. Basically, we can read this syntax as if showImage is true, display Hide, otherwise display Show. Let's check this out in the browser. So our button text now says Show Image. If we click it, it says Hide Image. Ahh, that seems a little more user-friendly.

Now that we have our images working, let's tackle the Filter by box, and for that we would need two-way binding.

Handling Input with Two-way Binding

When working with user-entry HTML elements such as an input element, we often want to display a component class property in the template and update that property when the user makes a change. This process requires two-way binding.

              <------- 
   Template                Class
              -------->

To specify two-way binding in Angular, we use the *ngModel directive. We enclose ngModel in square brackets[] to indicate property binding from the class property to the input element, and parentheses() to indicate event binding to send a notification of the user-entered data back to the class property.

Template                                  Class
<input [(ngModel)] ='listFilter'>       export class ListComponent {
           |                              listFilter: string = 'cart';
      BananaBox[()]                        }

We assign this directive to a template expression. To help us remember which order to put the two sets of brackets, visualize a banana in a box, square brackets for the box on the outside, and parentheses for the banana on the inside.

Now we have another Angular directive that we can use. Every time we want to use an Angular directive in a template, we need to consider how to make that directive visible to the component associated with that template.

Recall that an Angular module defines the boundary or context within which the component resolves its directives and dependencies. The illustration of our AppModule currently looks like this.

                                          /  Organization Boundaries
BrowserModule**>FormsModule**>AppModule--|   
                               |   #      \   Template resolution environment
                               |   #
                               V   V
                            AppComponent
                                #
                                #
                                V
                    ProductList-Component
****> Imports
----> Bootstrap
####> Declarations

We want to use the ngModel directive in our ProductList Component, which is owned by AppModule. So in the AppModule, we need to import the appropriate system module that exposes the ngModel directive. Since the ngModel directive is most often used when building data entry forms, ngModel is part of FormsModule, so we import that here. Now the ngModel directive and any of the other Angular forms directives are visible to any component declared by AppModule, including our Product-List Component.

Now let's give this a try. We are back in the editor and looking at the product list component and its associated template. Recall that we defined a Filter by input box here and display the entered filter here. We'll later use the filter to filter our list of products. Let's start by adding a component class property for the list filter. This property is a string and will set a default initial value for filtering the list of products. We hardcode the filter string here, but you can imagine that we store the user's last entered filter and use that as the default instead. With that, we can set up the two-way binding.

export class ProductListComponent {
  pageTitle: string = 'Product List';
  imageWidth: number = 50;
  imageMargin: number = 2;
  showImage: boolean = false;
  listFilter: string = 'cart';
...

On the input element, we draw a banana in a box, then specify the ngModel directive. We bind to the component class listFilter property. product-list.component.html

    <div class='card-body'>
        <div class='row'>
            <div class='col-md-2'>Filter by:</div>
            <div class='col-md-4'>
                <input type='text'
                       [(ngModel)]='listFilter' />
            </div>
         <div class='row'>
            <div class='col-md-6'>
                <h4>Filtered by: {{listFilter}}</h4>
            </div>
...

We want to display the list filter here, so we use interpolation. Recall that interpolation has no quotes. Are we done? Not quite. Let's see what happens if we try to run. The page doesn't appear. If we press F12, we see the error is, Can't bind 'ngModel' since it isn't a known property of input.

Uncaught Error: Template parse errors:
Can't bind to 'ngModel' since it isn't a known property of 'input'. ("
            <div class='col-md-4'>
                <input type='text'
                       [ERROR ->][(ngModel)]='listFilter' />
            </div>
        </div>
"): ng:///AppModule/ProductListComponent.html@9:23
    at syntaxError (compiler.js:2547)
    at TemplateParser.push../node_modules/@angular/compiler/fesm5/compiler.js.TemplateParser.parse (compiler.js:19495)
    at JitCompiler.push../node_modules/@angular/compiler/fesm5/compiler.js.JitCompiler._parseTemplate (compiler.js:25041)
    at JitCompiler.push../node_modules/@angular/compiler/fesm5/compiler.js.JitCompiler._compileTemplate (compiler.js:25028)
    at compiler.js:24971
    at Set.forEach (<anonymous>)
    at JitCompiler.push../node_modules/@angular/compiler/fesm5/compiler.js.JitCompiler._compileComponents (compiler.js:24971)
    at compiler.js:24881
    at Object.then (compiler.js:2538)
    at JitCompiler.push../node_modules/@angular/compiler/fesm5/compiler.js.JitCompiler._compileModuleAndComponents (compiler.js:24880)

This is telling us that Angular can't find the ngModel directive. Recall from the paragraph above that the ngModel directive is part of the Angular module for forms called FormsModule. To expose this directive to our product list component, we need to import the FormsModule in the module that owns the product list component, which is our AppModule. We start by importing FormsModule from angular/forms. We then add FormsModule to the imports array for the ngModule decorator.

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import {FormsModule} from '@angular/forms';

import { AppComponent } from './app.component';
import { ProductListComponent } from './products/product-list.component';

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

Why the imports array and not the declarations? Our directives, components, and pipes are declared here in the declarations array.

...
  declarations: [
    AppComponent,
    ProductListComponent
  ],
...

Directives, components, and pipes we use from other sources, such as Angular itself or third parties, are defined in external Angular modules we add to the imports array here.

...
 imports: [
    BrowserModule,
    FormsModule
  ],
  boots
...

Now that we've told Angular where to find the ngModel, let's see the result. When the page displays, we see cart as the default value. It is displayed here and here. If we modify the entry, notice that the displayed filter text is also updated. That's because we are using two-way binding. Notice that the list of products is not yet filtered. We'll do that in the next module.

There is one more thing we do want to address now, and that is the data formatting. That price should really look like a price and show the appropriate currency symbol.

Transforming Data with Pipes

With Angular's data binding, displaying data is easy. Just bind an element property to a class property, and we're done! Well, not always.

Pipes

Sometimes the data is not in a format appropriate for display. That's where pipes come in handy.

  • Pipes transform bound properties before they are displayed, so we can alter the property values to make them more user-friendly or more locale-appropriate.
  • Angular provides some built-in pipes for formatting values, such as date, number, decimal, percent, currency, uppercase, lowercase, and so on.
  • Angular also provides a few pipes for working with objects, such as the json pipe to display the content of an object as a json string, which is helpful when debugging, and a slice pipe, which selects a specific subset of elements from a list.
  • We can also build our own custom pipes as we'll see in the next chapter.

Pipe Examples

Let's start with a simple example. Say we want to display the product code in lowercase. We can add the pipe character after the property in the template expression, and then specify the lowercase pipe.

{{ product.productCode | lowercase }}

The product code is then transformed into lowercase before it is displayed.

We can also use pipes in property bindings. Add the pipe after the property in the template expression and specify the desired pipe.

<img [src] ='product.imgUrl'
     [title]='product.productName | uppercase'>

In this example, we specified the uppercase pipe, so the image title will appear in all caps.

If needed, we can chain pipes. In this example, the price is transformed into a currency.

{{ product.price | currency | lowercase }}

By default, the currency pipe adds the all caps, three-letter abbreviation of the local currency to the amount. If we want to display that abbreviation in lowercase, we can transform it again by simply adding another pipe.

Some pipes support parameters. Parameters are defined by specifying a colon and the parameter value.

{{ product.price | currency:'INR':'symbol':'1.2-2' }}

For example, the currency pipe has three parameters: the desired currency code, a string defining how to show the currency symbol, and digit info. The digit info consists of the minimum number of integer digits, the minimum number of fractional digits, and the maximum number of fractional digits. The value here of 1.2-2 means that at least 1 digit to the left of the decimal, and at least 2 digits to the right of the decimal, and no more than 2 digits to the right of the decimal, effectively defining 2 decimal places.

With that, let's jump back to the demo.

We specify the pipes in the template, so we are looking at the product-list.component template. Let's add a lowercase pipe for the product code and a currency pipe for the price.

For the product code, we simply insert the pipe character after the property in the template expression then type lowercase. That's it. For the price, we insert a pipe character and currency. That's all that is required. But let's try out a few of the parameters. We'll specify USD symbol to display the dollar sign instead of the currency abbreviation, and 1.2-2 to specify that we want at least 1 number to the left of the decimal place and 2 and only 2 numbers to the right of the decimal place.

 <tr *ngFor='let product of products'>
                        <td>
                            <img *ngIf='showImage'
                                 [src]='product.imageUrl'
                                 [title]='product.productName'
                                 [style.width.px]='imageWidth'
                                 [style.margin.px]='imageMargin'>
                        </td>
                        <td>{{ product.productName }}</td>
                        <td>{{ product.productCode | lowercase }}</td>
                        <td>{{ product.releaseDate}}</td>
                        <td>{{ product.price | currency: 'INR':'symbol':'1.2-2' }}</td>
                        <td>{{ product.starRating}}</td>
                    </tr>

Let's see how we did. Ah. Looking at the result, we now see the product code in lowercase and the price displayed nicely as a currency. So we can easily perform simple data transformations using the built-in pipes in the template expressions for our bindings. Feel free to try out some of the other pipes from the slides. Let's finish up this module with some diagrams and a checklist we can use as we work with bindings and pipes.

Checklists and Summary

Data binding makes it easy to display class properties from our component and set DOM element properties to our class property values to better control the view, and the component can listen for events from the DOM to respond as needed for an interactive user experience.

There are four basic types of binding in Angular. Here is a diagram as a memory aid.

       Interpolation:{{pageTitle}}
      <------------------------------------------------
DOM                                                      Component
      Property Binding: <img [src]='product.imageUrl'>                                    
      <------------------------------------------------
      
      Event Binding: <button (click)='toggleImage()'
      ------------------------------------------------->

      Two-Way Binding: <input [(ngModel)]='listFilter'/>
      <------------------------------------------------>

  • Interpolation inserts interpolated strings into the text between HTML elements or assigns element properties. Be sure to wrap the template expression in curly braces and no quotes.
  • Property binding sets an elements property to the value of a template expression. The binding target specifies a property of the element and must be enclosed in square brackets. The binding source specifies the template expression and must be enclosed in quotes.
  • Event binding listens for events and executes a template statement when the event occurs. The target event specifies an event name and must be enclosed in parentheses. The template statement often defines the name of the method to call when the event occurs and must be enclosed in quotes.
  • Two-way binding displays a component class property and updates the property when the user makes a change. Use the banana in a box syntax with the ngModel directive. The binding source specifies the template expression and must be enclosed in quotes.

Here are some things to remember when using ngModel.

  • Define ngModel within the banana in a box i.e [(ngModel)]for two-way binding.
  • Be sure to add FormsModule from the angular/forms package to the imports array of an appropriate Angular module, in this case, AppModule.

This ensures that the ngModel directive is available to any template defined in a component associated with that module. We'll talk more about Angular modules later in this tutorial.

The data we have in our component may not be in the format we want for display.

  • We can use a pipe and a template to transform that data to a more user-friendly format. To use a pipe, specify the pipe character, the name of the pipe, and any pipe parameters separated with colons. Here is an example of the currency pipe with three parameters.
{{ product.price | currency:'USD':'symbol':'1.2-2' }}

This module covered data binding and pipes. We looked at property binding, event binding, and two-way binding.

Lastly, we discovered how to transform bound data to a more user-friendly format using built-in pipes. And here, once again, is our application architecture.

                              Application Architecture


                               -> Welcome Component
                              |
 index.html -> App Component->|-> Product List Component ---
                              |           |                  | --> Star Component
                              |           V                  |  
                               -> Product Detail Component---
               Product Data
                  Service

In this chapter, we finished more of the product list component, but it could be better. Next up, we'll see several techniques for improving our component.

Clone this wiki locally