-
Notifications
You must be signed in to change notification settings - Fork 2
Templates, Interpolation, and Directives
To build the user interface for our application in Angular, we create a template with HTML. To really power up that user interface, we need Angular's data binding
and directives
. Welcome back to Angular: Getting Started and in these next two modules, we create the user interface using templates, directives, and data binding.
Web applications are all about the user interface, and Angular makes it easier to build rich and powerful user interfaces. Angular gives us data binding
so we can easily display information and respond to user actions. With Angular directives, we add logic to our HTML, such as if statements and for loops. And with Angular components, we build nested user interface fragments, such as an image rotator or rating stars.
We've seen that an Angular component is a view defined with a template, it's associated code defined with a class, and additional information defined with metadata and set using a Component decorator.
Component = Template + Class(Properties, Methods) + Metadata
In these next two modules, we'll focus on techniques for building the template. In this module, we evaluate the different ways we can build a template for our component and demonstrate how to create a linked template for our view.
Then we'll build a component associated with that template and use it as a directive.
We'll detail how to set up data binding using interpolation and display the value of our component class properties in the view.
We need some basic logic in the template, so we'll leverage Angular's built-in directives.
Application Architecture
-> Welcome Component
|
index.html -> App Component->|-> Product List Component ---
| | | --> Star Component
| V |
-> Product Detail Component---
Product Data
Service
In the next module we'll look at additional data binding techniques. Looking at our application architecture, we currently have the index.html
file and our root app component in place, so we have a working sample application, but it doesn't look like much. What we really want is a list of products. In this module, we'll begin work on the Product list component
to display that list of products. Let's get started.
In the prior module, we built an inline template for our app component. We used the template
property to define the template directly in the component's metadata. But this is not the only way we can build a template for our components.
We can use the template
property and define an inline template using a simple quoted string with single or double quotes,
template:
"<h1>{{pageTitle}}</h1>"
or we can define an inline template with a multiline string by enclosing the HTML and ES2015 backticks.
template: `
<div>
<h1>{{pageTitle}}</h1>
<div>
My First Component
</div>
<div>
`
The backticks allow composing a string over several lines, making the HTML more readable. We used this technique to build our template in the last module.
There are some advantages to defining an inline template using one of these two techniques. The template is directly defined within the component, keeping the view and the code for that view in one file.
It is then easy to match up our data bindings with the class properties, such as the page title in this example. However, there are disadvantages as well. When defining the HTML in a string, most development tools don't provide IntelliSense, automatic formatting, and syntax checking, especially as we define more HTML in the template.
templateUrl:
'./product-list.component.html'
These issues become challenges. In many cases, the better option is to define a linked template
with the HTML in its own file. We can then use the templateUrl
property in the component metadata to define the URL of our HTML file.
Let's use this technique and build a linked template
for our Product List view. Here is our ultimate goal for the Product List view. The view has a nice heading. A Filter by box at the top allows the user to enter a string, the user-entered string is displayed here, and the list of products is filtered to only those with the product name containing that string. The products are listed in a neat table with a nicely formatted header. The Show Image button shows an image for each product. The product name is a link that displays the Product Detail view, which we'll build later in this tutorial. To make this page look nice with very little effort, we use the Twitter Bootstrap
styling framework. If you want to find out more about Bootstrap, check out this link, and for the stars we use the Font Awesome icon set and toolkit. To find about more about Font Awesome, check out this link. We'll install both of these in the upcoming demo. Now let's jump into a demo and start building the template for our Product List view.
First, let's install Bootstrap and Font Awesome so we can use them in our templates. Open the integrated terminal or command window. I still have the application running in this window, so I'll click plus to open another command window. Then, type npm install bootstrap font-awesome
Adarsh:APM-Start adarshmaurya$ npm install bootstrap font-awesome
npm WARN [email protected] requires a peer of [email protected] - 3 but none is installed. You must install peer dependencies yourself.
npm WARN [email protected] requires a peer of popper.js@^1.14.3 but none is installed. You must install peer dependencies yourself.
+ [email protected]
+ [email protected]
added 2 packages from 7 contributors and audited 39131 packages in 9.281s
found 0 vulnerabilities
This installs both packages. You may see some warnings here. These warnings tell us that Bootstrap requires jQuery and Popper, but we only plan to use the styling parts of Bootstrap, not the interactive features, so we don't need these dependencies. Installing the packages does not provide access to their style sheets. For that, we import the styles for these packages into our global application style sheet, which is the style.css file here.
@import "../node_modules/bootstrap/dist/css/bootstrap.min.css";
@import "../node_modules/font-awesome/css/font-awesome.min.css";
/* You can add global styles to this file, and also import other style files */
div.card-header {
font-size: large;
}
div.card {
margin-top: 10px
}
.table {
margin-top: 10px
}
We'll import the minimized version of the styles from the bootstrap/dist/css folder, and the minimized version of the styles from the font-awesome.css folder. The style sheets are then available to any template in our application.
Now we are ready to add an external template file for the product list component. By convention, each feature of the application has its own folder under the app folder. So let's add a new folder here and name it products. In that folder we'll create the template for our product list component. By convention, the name of the template is the same name as the component with an HTML extension. We'll call our product list component product-listcomponent.html
.
Let's widen that up a little bit. Now, we are ready to create the HTML for our template. Let's start with the heading. We're using Twitter Bootstrap style classes here. In the heading we display Product List.
<div cllass='card'>
<div class='card-header'>
Product List
</div>
</div>
If you don't want to type in all of this code, you can copy it from the APM-Final folder provided in my GitHub repository, as detailed in the First Things First module earlier in this tutorial. Next is the Filter by. We define an input box for entry of the filter string and we add text that displays the user-entered filter. We again use Twitter Bootstrap style classes to lay out the input box and text into rows.
<div cllass='card'>
<div class='card-header'>
Product List
</div>
<div class='card-body'>
<div class='now'>
<div class='col-md-2'>Filter by:</div>
<div class='col-md-4'>
<input type='text' />
</div>
</div>
<div class='row'>
<div class='col-md-6'>
<h4>Filtered by:</h4>
</div>
</div>
</div>
</div>
Now let's build the table. We'll use Twitter Bootstrap's table style classes. We have a table header, the first column header is a button to show the product image, and here is the table body. Hmm, we definitely don't want to hardcode in the products here, so let's leave the table body empty for now.
<div cllass='card'>
<div class='card-header'>
Product List
</div>
<div class='card-body'>
<div class='now'>
<div class='col-md-2'>Filter by:</div>
<div class='col-md-4'>
<input type='text' />
</div>
</div>
<div class='row'>
<div class='col-md-6'>
<h4>Filtered by:</h4>
</div>
</div>
</div>
<div class='table-responsive'>
<table class='table'>
<thead>
<tr>
<th>
<button>
Show Image
</button>
</th>
<th>Product</th>
<th>Code</th>
<th>Available</th>
<th>Price</th>
<th>5 Star Rating</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
So we have the start of a template defined for our component, now what? If you said we need to build the component, you are exactly right.
Remember the steps for building a component that we covered in the last module?
import {Component} from '@angular/core'
@Component({
selector: 'pm-product',
templateUrl: './product-lisy.component.html'
})
export class ProductListComponent{
pageTitle: string = 'Product List';
}
We define a class, we add a Component decorator to define the metadata and specify the template, and we import what we need. The only thing that's really different from the component we created in the last module is the template
property. Here we are using templateUrl
to define the location of our linked template instead of defining an HTML string. Notice the syntax of the path here. If we follow the convention of defining the template.html
file in the same folder as the associated component, we can use a relative path by specifying ./
. Let's jump right back to the demo and give this a try.
We are back with the sample application, exactly where we left it, and we are ready to build a new component. We start by creating a new file in the products folder. We'll name it using the component naming convention, .component, because it is an Angular component, and .ts for the extension. Then we create the class, export class ProductListComponent. We're exporting this class so it is available to other parts of the application. Next, we decorate the class with a Component decorator. It is the Component decorator that makes this class a component, and we know what that underline means, we need the import statement. Let's pass an object into the Component decorator with the appropriate properties. For the selector, we'll set pm-products. We use the same prefix as in the app component to distinguish the selector as part of the product management application.
import { Component } from "@angular/core";
@Component({
selector: 'pm-products',
templateUrl: './product-list.component.html'
})
export class ProductListComponent{
}
Then we define the templateUrl. Here we provide the path to our HTML file. Since we defined the HTML file in the same folder as the component, we can use the ./ relative past syntax here. So now we have our template defining our view, our class, which defines our associated code, and the Component decorator that defines the metadata. Our component is complete and we're ready to use it, but how?
Here is our newly created product list component, and here is the app component we created earlier.
app.component.ts
@Component({
selector: 'pm-root',
template:
<div><h1>{{pageTitle}}</h1>
<pm-products></pm-products> <----------
<div> |
}) |
export class AppComponent{ } |
product-list.component.ts
@Component({ |
selector: 'pm-products', |
templateURL: './product-list.component.html'
})
export class ProductListComponent{ }
Note that I've excluded some of the code here on this slide, such as the import statements and class details for a better fit. We'll see the complete code when we get back to the demo. When a component has a selector
defined, as we have here, we can use the component as a directive. This means that we can insert this component's template into any other component's template by using the selector as an HTML tag, like this. The product list component's template is then inserted into this location in the app component's template. So, this is the first step when using a component as a directive. Use the name defined in the selector as an HTML tag in another component's template.
When this template is displayed, Angular looks for a component that has a selector with this name. We could have hundreds of components in our application, how does our application know where to look for the selector?
The application looks to the Angular module that owns this component to find all the directives that are visible to this component. Every Angular application must have at least one Angular module, the root application module, commonly called AppModule
.
Currently, our AppModule
declares our route application component, AppComponent
. A component must belong to one, and only one, Angular module. Because the AppModule declares the AppComponent
, the AppComponent
belongs to the AppModule
. The AppModule
bootstraps the application with this component, so it is this first component that is loaded for our application. Our AppModule also imports the system BrowserModule to pull in the features it needs to run this application in a browser. So, this is what our AppModule currently looks like.
/ Organization Boundaries
BrowserModule ******> AppModule--|
| # \ Template resolution environment
| #
V V
AppComponent
#
#
V
ProductList-Component
****> Imports
----> Bootstrap
####> Declarations
An Angular module defines the boundary or context within which the component resolves its directives and dependencies. So when a component contains a directive, Angular looks to the component's module to determine which directives are visible to that component.
What does that mean for us? Well, for Angular to find the PM products directive used in the AppComponent, the ProductList-Component must also be declared in this Angular module.
This is the second step when using a component as a directive. We need to ensure that the directive is visible to any component that uses it.
There are two ways to expose a directive in an Angular module. We can declare the component in the Angular module, as we show here, or if the component is already declared in another Angular module, we can import that module, similar to how we import BrowserModule here.
Now let's jump back to the demo and give this a try. We are back in our sample app. We defined a selector for our product list component here, so we can use it as a directive in any other component.
import { Component } from "@angular/core";
@Component({
selector: 'pm-products',
templateUrl: './product-list.component.html'
})
export class ProductListComponent{
}
Let's use it in the app component. Open the app.component
file.
import { Component } from "@angular/core";
@Component({
selector: 'pm-root',
template: `
<div><h1>{{pageTitle}}</h1>
<div>My First Component</div>
</div>
`
})
export class AppComponent {
pageTitle: string = 'Softhinkers Product Management';
}
So instead of displaying <div>My First Component</div>
, we'll display our new product list template here. Replace the div tags with pm-products.
import { Component } from "@angular/core";
@Component({
selector: 'pm-root',
template: `
<div><h1>{{pageTitle}}</h1>
<pm-products></pm-products>
</div>
`
})
export class AppComponent {
pageTitle: string = 'Softhinkers Product Management';
}
Are we ready to see our result in the browser?
Adarsh:APM-Start adarshmaurya$ npm start
> [email protected] start /Users/adarshmaurya/Playground/getting-started-angul
ar/APM-Start
> ng serve -o
t install peer
** Angular Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200/ **
10% building modules 0/1 modules 1 active ...g-started-angular/APM-St 10% building modules 1/2 modules 1 active ...gular-cli-files/models/j 10% building modules 2/3 modules 1 active ...tarted-angular/APM-Start
...
4 unchanged chunks
chunk {main} main.js, main.js.map (main) 8.76 kB [initial] [rendered]
ℹ 「wdm」: Compiled successfully.
ℹ 「wdm」: Compiling...
10% building modules 0/1 modules 1 active ...g-started-angular/APM-St 30% building modules 1/2 modules 1 active ...angular/APM-Start/src/ap 50% building modules 2/3 modules 1 active ... lazy groupOptions: {} n 54% building modules 3/4 modules 1 active .../app/products/product-li 57% building modules 4/5 modules 1 active ...pp/products/product-list 92% after chunk asset optimization SourceMapDevToolPlugin main.js gen 92% after chunk asset optimization SourceMapDevToolPlugin polyfills.j 92% after chunk asset optimization SourceMapDevToolPlugin runtime.js 92% after chunk asset optimization SourceMapDevToolPlugin styles.js g 92% after chunk asset optimization SourceMapDevToolPlugin vendor.js g 92% after chunk asset optimization SourceMapDevToolPlugin resolve sou 92% after chunk asset optimization SourceMapDevToolPlugin main.js att
Date: 2018-12-16T09:44:13.224Z - Hash: 6c89e37b19a1cdb049ec - Time: 110ms
4 unchanged chunks
chunk {main} main.js, main.js.map (main) 12 kB [initial] [rendered]
ℹ 「wdm」: Compiled successfully.
And, our page does not display. Let's use our F12 tools to see why. The key part of this error is that we have a Template parse error, pm-products in not a known element, and with this error, Angular gives us a solution.
Uncaught Error: Template parse errors:
'pm-products' is not a known element:
1. If 'pm-products' is an Angular component, then verify that it is part of this module.
2. If 'pm-products' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message. ("
<div><h1>{{pageTitle}}</h1>
[ERROR ->]<pm-products></pm-products>
</div>
"): ng:///AppModule/AppComponent.html@2:4
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)
If pm-products is an Angular component, and in our case it is, then verify that it is part of this module. Ah, yes. We didn't do step two and declare it in our application's Angular module. Let's go back to the code. We'll open the app.module
, and add ProductListComponent
to the declarations array.
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { ProductListComponent } from './products/product-list.component';
@NgModule({
declarations: [
AppComponent,
ProductListComponent
],
imports: [
BrowserModule
],
bootstrap: [AppComponent]
})
export class AppModule { }
Well, we're getting a squiggly line here, that means we have an error. Any guess as to what's wrong? If you said we're missing the import, you are correct. Everything we declare must be imported; with VS Code this is easy. Notice the light bulb icon on the left, it is indicating that it has a quick fix for this underlined issue. Click the light bulb and select the desired fix, and VS Code adds the appropriate import line for us. Now that our syntax error is gone, let's try it again. There's our page. It's not complete, and it's not interactive yet, but we have the basics in place. So we successfully used our product list component as a directive. We added the selector as the directive in the containing component's template. We declared the component to the application's Angular module, and we added the appropriate import statement.
Now, we are ready to power up our user interface with data binding and some built-in Angular directives.
In Angular, binding coordinates communication between the component's class and its template, and often involves passing data. We can provide values from the class to the template for display, and the template raises events to pass user actions or user-entered values back to the class.
Binding
Coordinates communication between the component's class and it's template and often involves passing data.
Template <----values---- Class(Properties, Methods)
Template ----events----> Class(Properties, Methods)
- The binding syntax is always defined in the template. Angular provides several types of binding, and we'll look at each of them.
- In this module, we cover
interpolation
. The remaining data binding techniques are covered in the next module.
Interpolation The double curly braces that signify interpolation are readily recognizable.
Template
<h1>{{pageTitle}}</h1>
class
export class AppComponent{
pageTitle: string = 'Softhinkers Product Management'
}
The page title in this example is bound to a property in the component's class.
- Interpolation is a one-way binding, from the class property to the template, so the value here shows up here.
- Interpolation supports much more than simple properties. We can perform operations such as concatenation or simple calculations. We can even call a class method, such as getTitle(), shown here.
<h1>{{pageTitle}}</h1>
{{'Title: '+ pageTitle}}
{{2*20+1}}
{{'Title: '+ getTitle()}}
<h1 innerText={{pageTitle}}> </h1>
export class AppComponent{
pageTitle: string = 'Softhinkers Product Management';
getTitle(): string{...};
}
We use interpolation to insert the interpolated strings into the text between HTML elements, as shown here. Or, we can use interpolation with element property assignments, as in this example. Here we assign the innerText property of the H1 element to a bound value. Both of these examples display the same result. Template expression The syntax between the interpolation curly braces, is called a template expression. Angular evaluates that expression using the component as the context. So Angular looks to the component to obtain property values or to call methods. Angular then converts the result of the template expression to a string and assigns that string to an element or directive property. So, anytime we want to display read-only data, we define a property for that data in our class and use interpolation to display that data in the template. And if we need to perform simple calculations or get a result from a method, we can do that with interpolation as well.
Let's give this a try. Looking at the product list template, from our sample application, we hardcoded in the page title here in the heading. Binding the heading to a property in the class instead of hardcoding it in the HTML makes it easier to see and change when working on the code, and we could later retrieve this text from a file or database.
Let's start by adding a property in the class for the page title. We'll open the component to the right and close down the Explorer. Here in the class we specify the property name, we'll call it pageTitle, and because we are using TypeScript, we define the type for this property. Lastly, we assign a default value, Product List. With the pageTitle property in place, we can now bind to the pageTitle property in the template. We replace the hardcoded Product List here with interpolation and specify the name of the property. Now, when this template is displayed, Angular assigns the string value of the pageTitle property to the inner text property of this div element, and Product List will be displayed. Let's see the result in the browser. With our binding, the page title appears as before. So, we can confirm that it works. I've rearranged the windows so that we see both the code and the browser. Now let's make a change to our page title here, and we immediately see it in the browser. So our interpolation works! So anytime we want to display the value of a component property, we simply use interpolation. Now we're ready to add some logic to our template.
We can think of a directive as a custom HTML element or attribute we use to power up and extend our HTML. We can build our own custom directives or use Angular's built-in directives.
Directive
Custom HTML element or attribute used to power up and extend our HTML.
- Custom
- Built-In
Previously in this module, we've seen how to build a component and use it as a custom directive, we used to pm-products
directive to display our product list template.
In addition to building our own custom directives, we can use Angular's built-in directives. The built-in Angular directives will look at our structural directives. A structural directive modifies the structure, or layout, of a view by adding, removing or manipulating elements and their children. They help us to power up our HTML with if logic and for loops.
Angular Built-in Directives
Structural Directives:
- *ngIf: If logic
- *ngFor: For loops
Notice the asterisk in front of the directive name. That marks the directive as a structural directive.
*ngIf Built-in Directive
Let's look at ngIf first.
- NgIf is a structural directive that removes or recreates a portion of the DOM tree based on an expression.
- If the expression assigned to the ngIf evaluates to a false value, the element and its children are removed from the DOM.
- If the expression evaluates to a true value, a copy of the element and its children are reinserted into the DOM.
For example, say we only wanted to show the HTML table if there are some products in a list of products. We use ngIf on a table element and set it to products and products.length.
<div class='table-responsive'>
<table class='table' *ngIf='products && products.length'>
<thead> ... </thead>
<tbody> ... </tbody>
</table>
</div>
If the product's variable has a value and the product's list has a length, the table appears in the DOM. If not, the table element and all of its children are removed from the DOM.
But didn't we just say that an Angular module defines the boundary or context within which the component resolves its directives and dependencies?
How will our application find this ngIf directive? Looking back at the illustration of our AppModule, we see that it imports BrowserModule.
/ Organization Boundaries
BrowserModule ******> AppModule--|
| # \ Template resolution environment
| #
V V
AppComponent
#
#
V
ProductList-Component
****> Imports
----> Bootstrap
####> Declarations
Luckily for us, BrowserModule
exposes the ngIf
and ngFor
directives. So, any component declared by the AppModule can use the ngIf or ngFor directives. With that settled, let's try out the ngIf directive.
We are back in the sample application looking at the product-list
component and its template. We only want to display this table of products if there are some products to display. So the first thing we need is a property to hold the list of products. Where do we define that products property?
In the component's class, of course. We'll add a products property here. Hmm, but what is the type of this property? Well, we want an array of product instances, but we don't currently have anything that defines what a product is. We'll have a better solution in a later module, but for now, we'll just define products as an array of any
.
import { Component } from "@angular/core";
@Component({
selector: 'pm-products',
templateUrl: './product-list.component.html'
})
export class ProductListComponent {
pageTitle: string = 'Product List';
products: any[] = [
{
"productId": 1,
"productName": "Leaf Rake",
"productCode": "GDN-0011",
"releaseDate": "March 19, 2016",
"description": "Leaf rake with 48-inch wooden handle.",
"price": 19.95,
"starRating": 3.2,
"imageUrl": "https://openclipart.org/image/300px/svg_to_png/26215/Anonymous_Leaf_Rake.png"
},
{
"productId": 2,
"productName": "Garden Cart",
"productCode": "GDN-0023",
"releaseDate": "March 18, 2016",
"description": "15 gallon capacity rolling garden cart",
"price": 32.99,
"starRating": 4.2,
"imageUrl": "https://openclipart.org/image/300px/svg_to_png/58471/garden_cart.png"
},
{
"productId": 5,
"productName": "Hammer",
"productCode": "TBX-0048",
"releaseDate": "May 21, 2016",
"description": "Curved claw steel hammer",
"price": 8.9,
"starRating": 4.8,
"imageUrl": "https://openclipart.org/image/300px/svg_to_png/73/rejon_Hammer.png"
},
{
"productId": 8,
"productName": "Saw",
"productCode": "TBX-0022",
"releaseDate": "May 15, 2016",
"description": "15-inch steel blade hand saw",
"price": 11.55,
"starRating": 3.7,
"imageUrl": "https://openclipart.org/image/300px/svg_to_png/27070/egore911_saw.png"
},
{
"productId": 10,
"productName": "Video Game Controller",
"productCode": "GMG-0042",
"releaseDate": "October 15, 2015",
"description": "Standard two-button video game controller",
"price": 35.95,
"starRating": 4.6,
"imageUrl": "https://openclipart.org/image/300px/svg_to_png/120337/xbox-controller_01.png"
}
];
}
In TypeScript, we use any as the data type anytime we don't know or don't care what the specific data type is. We need to populate our array, but where do we get the data? In many cases, we would communicate with a back-end server to get this data. We'll look at how to do that later in this tutorial. For now, we'll just hardcode in a set of products.
If you are coding along, consider copying a few of the products from the product.json file provided with the starter files under the api, products folder. With a products property in place, we're ready the use it in the HTML.
We want to put it on the table element because that is the element we want to add or remove from the DOM. Type *ngIf=, and then our expression enclosed in quotes. We only want to show the table if there is a list of products, and that list of products contains some elements.
<div class='card'>
<div class='card-header'>
{{pageTitle}}
</div>
<div class='card-body'>
<div class='row'>
<div class='col-md-2'>Filter by:</div>
<div class='col-md-4'>
<input type='text' />
</div>
</div>
<div class='row'>
<div class='col-md-6'>
<h4>Filtered by: </h4>
</div>
</div>
<div class='table-responsive'>
<table class='table' *ngIf='products && products.length'>
<thead>
<tr>
<th>
<button class='btn btn-primary'>
Show Image
</button>
</th>
<th>Product</th>
<th>Code</th>
<th>Available</th>
<th>Price</th>
<th>5 Star Rating</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
</div>
Let's see what this looks like in the browser. We see the table header, so we know our table is displayed. Let's try this. Let's comment out the product property assignment, bring up the browser again, and we see that the table disappeared. Now if we uncomment out our table and look again at the browser, our table reappears. With ngIf, the associated element and its children are literally added or removed from the DOM, but notice that we still aren't populating the table with our products. Let's do that next.
Another structural directive is ngFor. ngFor repeats a portion of the DOM tree, once for each item in an iterable list. So we define a block of HTML that defines how we want to display a single item and tell Angular to use that block for displaying each item in the list. For example, say we want to display each product in a row of a table. We define one table row and its child table data elements. That table row element and its children are then repeated for each product in the list of products. The let
keyword here creates a template input variable called 'product'. We can reference this variable anywhere on this element, on any sibling element, or on any child element. And notice the of
instead of in here, we'll talk more about that in a moment.
<tr *ngFor='let product of products'>
<td></td>
<td>{{ product.productName }}</td>
<td>{{ product.productCode}}</td>
<td>{{ product.releaseDate}}</td>
<td>{{ product.price}}</td>
<td>{{ product.starRating}}</td>
</tr>
For now, let's jump back to our demo. We are, once again, looking at the product list component and its template. Here in the table body, we want to repeat a table row for each product in the list of products. In the table body, we'll add a tr element for the table row, and in the tr element we'll specify the ngFor, *ngFor='let product of products'. Next, we'll add the child elements. We'll insert a td, or a table data element, for each property of the product that we want to display in the table. We'll need to match them up with the table header elements. The first column displays the product image, let's skip the image for now, we'll add that in the next module, but we'll still add the td element as a placeholder. The next table header says Product, so in this column we want the product name. We'll use interpolation to bind to the product's name by using the local variable, product, and a dot to drill down to the product properties. We want productName here. How did we know that property name? Looking here at the product list component, we see the product property names here. So these are the names we use in the interpolation template expressions. Next, I'll add td elements for some of the other product properties. So for each product in our list of products, we will get a tr element for a table row and td elements for table data.
<div class='card'>
<div class='card-header'>
{{pageTitle}}
</div>
<div class='card-body'>
<div class='row'>
<div class='col-md-2'>Filter by:</div>
<div class='col-md-4'>
<input type='text' />
</div>
</div>
<div class='row'>
<div class='col-md-6'>
<h4>Filtered by: </h4>
</div>
</div>
<div class='table-responsive'>
<table class='table'
*ngIf='products && products.length'>
<thead>
<tr>
<th>
<button class='btn btn-primary'>
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></td>
<td>{{ product.productName }}</td>
<td>{{ product.productCode}}</td>
<td>{{ product.releaseDate}}</td>
<td>{{ product.price}}</td>
<td>{{ product.starRating}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
Want to see how this looks in the browser? Wow! We have our products. Doesn't that look nice? Well, our price is not very well formatted and it doesn't have a currency symbol. We'll fix that with pipes in the next module.
Looking back at the component, we defined an array for our list of products. In the template, we laid out the HTML to display one product, the product is displayed in a table row with product properties in the appropriate columns. Using an ngFor
structural directive, we repeat this table row and its columns for each product in the list of products.
for..of vs for..in
So why is this ngFor
syntax product of products
and not product in products
? The reasoning for this has to do with ES2015 for loops. ES2015 has both a for of
loop and a for in
loop.
-
The
for of
loop is similar to a for each style loop. It iterates over an iterable object, such as an array. For example, say we have an array of persons nicknames, if we use for of to iterate over this list, we'll see each nickname logged to the console.-
let nicknames=['di', 'boo', 'punkeye']; for(let nickname of nicknames){ console.log(nickname); }
di, boo, punkeye
-
-
The
for in
loop iterates over the properties of an object. When working with an array such as this example, the array indexes are innumerable properties with integer names and are otherwise identical to general object properties, so we see each array index logged to the console.-
let nicknames = ['di', 'boo', 'punkeye']; for(let nickname in nicknames){ console.log(nickname); }
0, 1, 2
-
To help remember the difference, think of
-
in
as iterating the index.
Since the ngFor
directive iterates over iterable objects, not their properties, Angular selected to use the of keyword in the ngFor
expression. Now let's finish up this module with some checklists we can use as we work with templates, interpolation, and directives.
Checklists are a great way to recheck our understanding and our work.
Template
Let's start with a template.
- Use an inline template when building shorter templates,
- Then specify the
template
property and the component decorator. Use double or single quotes to define the template string, or use the ES2015 backticks to lay out the HTML on multiple lines. When using inline templates, there is often no design time syntax checking, so pay close attention to the syntax. - Use linked templates for longer templates. Specify the
templateUrl
property and the component decorator and - Define the path to the external template file.
Component as a Directive
@Component({
selector: 'pm-products',
templateURL: './product-list.component.html'
})
export class ProductListComponent{ }
product-list.component.ts
@Component({
selector: 'pm-root',
template:
<div><h1>{{pageTitle}}</h1>
<pm-products></pm-products>
</div>
})
export class AppComponent { }
app.component.ts
@NgModule({
imports:[ BrowserModule ],
declarations:[
AppComponent,
ProductListComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
app.module.ts
This one is a more visual checklist. After building the template, we build its component and learn how to use that component as a directive. Remember our steps?
- First, we use the
directive
as an element in the template for any other component. We use thedirective component selector
as the directive name. - We then declare the component so it is available to any template associated with this Angular module. We add the component to the declarations array passed in to the NgModule decorator of the Angular module.
Interpolation
Angular's data binding was introduced in this module with a look at interpolation.
- Interpolation is one-way binding from a component class property to an element property.
- Interpolation is defined with curly braces and a template expression. That expression can be a simple property, a concatenation, a calculation or a method call. Note that no quotes are needed when using interpolation.
Structural Directive: *ngIf, *ngFor And, we saw how to use two of Angular's structural directives, *ngIf and *ngFor.
- When using these structural directives, be sure to prefix them with an asterisk and assign them to a quoted string expression.
- Use *ngIf to add or remove an element and its children from the DOM based on an expression.
- If the assigned expression is evaluated to be a true value, the element is added to DOM.
- If false, the element is removed from the DOM.
- Use *ngFor to repeat an element and its children in the DOM for each element in an iterable list.
- Define the local variable with
let
, and useof
, notin
, when defining thengFor
expression.
- Define the local variable with
In this module,
- we evaluated the differences between an
inline template
and alinked template
, and we created a linked template. - We then built a component for that template and learned how to use that component as a directive.
- We took a first look at Angular data binding through interpolation and powered up our template by using built-in Angular directives.
Here, once again, is our application architecture. In this module, we started the product list component.
Application Architecture
-> Welcome Component
|
index.html -> App Component->|-> Product List Component ---
| | | --> Star Component
| V |
-> Product Detail Component---
Product Data
Service
Next up, let's discover more of Angular's data binding features and add interactivity to the product list template.