Components
Components are perhaps the most important feature in Angular. With components, you can create reusable blocks of isolated code that behave as if they are part of the HTML specification.
Take the following example using our MPage table component.
<h1>Table Output</h1> <div class="custom-table-css"> <mpage-table [tableData]="myData" [params]="{paginator: true, columnFilter: false}"></mpage-table> </div>
Other than the property binding of the tableData and params values, the <mpage-table> component doesn't look any different from standard HTML elements such as <h1> and <div>.
Lets create our first custom component.
-
Open your app.component.html file and either remove or comment out the counter
paragraph. We won't need it anymore so the choice is up to you.
<!-- <p>Counter = {{ counter }}</p> -->
-
Using either the terminal built into Visual Studio Code or a new terminal window, ensure you are in your
main folder of your Angular application (e.g. c:\MPage\intro-to-angular), and type in the Angular CLI command
to add a new component. We are going to add a component called name-card.
ng generate component name-card
info CLI commands can be short formed
Many of the CLI commands can be short formed to single letters instead of full words such as "generate" and "component". The generate command above could have been typed in as ng g c name-card. For our tutorials however we will be using the long form to avoid any confusion.
- Take a look at your file explorer in Visual Studio Code. You should now see a new sub-folder inside the app folder called "name-card". If you expand this folder, you will see four new files that start with the name "name-card.component."
-
These files follow the same naming convention as our main "app.component" files, offering an HTML, localized SCSS, TypeScript and spec.ts files. Go ahead and open the name-card.component.ts file. You should see the following block of code.
import { Component, OnInit } from '@angular/core'; @Component({ selector: 'app-name-card', templateUrl: './name-card.component.html', styleUrls: ['./name-card.component.scss'] }) export class NameCardComponent implements OnInit { constructor() { } ngOnInit(): void { } }
This class is very similar to app.component.ts, however there are a few differences which have been highlighted.
The first difference you will see is the import of the "OnInit" lifecycle hook. This import, along with the "implements OnInit" and "ngOnInit()" methods in our class give us a place to store any component initialization code we may have. If you component does not use the "ngOnInit()" method, you can safely remove these references.
There are a number of Angular lifecycle hooks that exist however the most common one you will use is the OnInit hook. Full documentation on lifecycle hooks exist on the Angular documentation site at https://angular.io/guide/lifecycle-hooks.
The "ngOnInit()" method is called once when your component is first initialized and is never called again.
In addition to "ngOnInit()", we also have a new "constructor()" method in our class. The code in your constructor method executes first in your class before any of the Angular lifecycle hooks and before any Angular specific functionality takes place. Think of the constructor as a low level class initializer than runs first before anything else.
Any services that you use will need to be passed through the constructor. We will cover services in the next section of this course.
The 'app-name-card' selector shown in the @Component decorator of the last bullet represents the HTML element name given to our new component. The Angular CLI defaults a prefix of "app" followed by your component name. You can change this value however the TypeScript linter in some editors may give you warnings that you are not following standard naming conventions.
-
Right now our new component doesn't do anything as it doesn't have anything calling it. Open your
app.component.html file and add the following line of code at the bottom.
<app-name-card></app-name-card>
- View your running application in your web browser where you should see the text "name-card works!" at the bottom of the page.
-
At this point, our name card component doesn't do anything special other than displaying a label. Before we
dig into making our name card do something, we will establish an array of names data that will be passed to the
name card component. In you app.component.ts file, add the following array definition
in your variable declaration section.
public people = [ {nameFirst: 'John', nameLast: 'Simpson', age: 51, gender: 'Male'}, {nameFirst: 'Bob', nameLast: 'Smith', age: 36, gender: 'Male'}, {nameFirst: 'Mary', nameLast: 'Smith', age: 32, gender: 'Female'}, {nameFirst: 'Theresa', nameLast: 'Banner', age: 25, gender: 'Female'} ];
-
Return to your app.component.html file and add the following code to your page
directly above the <app-name-card></app-name-card> element.
{{ people | json }}
This line will use template binding to display the people array using the JSON pipe found in Angular. The JSON pipe converts a JavaScript object into JSON which is viewable on our page and a fantastic tool for debugging.info Angular Pipes
Angular offers pipes to quickly format your data. Pipes exist for converting text to Upper/Lower case, displaying currency, decimals, dates and more. The official Angular pipe documentation offers more on the subject. We will be using pipes in our training courses however we will not be covering how to create your own pipes.
*ngFor Directive
One of the most common tasks we will perform in our HTML templates is to loop through an array to repeat components. Our current page is no exception as we plan on looping through each row of our people array to display the content.
While TypeScript has its own methods for looping, HTML doesn't offer any type of looping functionality. This is where the Angular *ngFor directive comes in.
Any HTML element can be used as an anchor for an *ngFor loop including your own custom components. The syntax of the *ngFor directive is:
<Element Name *ngFor="let item of array; index as indexName">
Where:
- Element Name represents the HTML element name such as div or p.
- item represents a single row from the array being referenced.
- array represents the array used in the loop.
- index is an optional zero-based index that can be used when referencing your array.
- indexName" is the name given to the index variable.
Any HTML elements contained inside an *ngFor top level element will have access to the item, array and indexName values.
-
In your code, add the following line below {{ people | json }} to see the content of your array displayed
as their own lines.
<div *ngFor="let person of people; index as i"> {{ i }}. {{ person | json }} </div>
Viewing your page should show the following results: Above you can see each "person" row being displayed next to the "i" index variable which starts at a value of 0. -
Next, we will loop through the people array again but this time we will use our new component. Modify the
<app-name-card></app-name-card> element to appear as follows.
<app-name-card *ngFor="let person of people"></app-name-card>
When you view your page again, you will see the text "name-card works!" repeated 4 times. -
Right now our component is displaying however it has no way to display any information from the person row. To
do this, we need to introduce an input in our component to allow the person value through. First, we will
modify our template to bind the person value. Please note this next step will show as an error until we add
the input to our component.
<app-name-card *ngFor="let person of people" [person]="person"></app-name-card>
-
Open your name-card.component.ts file and add the following line of code between the
"export class" line and the "constructor" method.
@Input() person: any;
This will create a variable called person that is of an "any" data type. Using "any" is acceptable in your code as it simply means the variable can be anything from a string to an array or anything in-between. It is however better to define custom data types where possible to allow TypeScript syntax enforcement as well as better editor integration. Later when we start creating Angular services we will discuss this in greater detail. -
Let's test our imported variable. Open name-card.component.html and replace the existing
code with:
<p>{{ person.nameLast }}, {{ person.nameFirst }}</p>
When you view your running page, you should see all for names in the order of last name, first name. -
One of the wonderful things about Angular components is that components can also contain other components. Our name-card component is responsible for displaying the name, age and gender of a person. We could easily just display these values in our component but instead, we are going to create a new component that will display a label above each field.
Using the same command prompt you used to create name-card, create a new component called labeled-field.
ng generate component labeled-field
-
Open the newly created labeled-field.component.ts file and add the following input
variables for label and value.
Visual Studio Code should automatically add the Input import.
import { Component, Input, OnInit } from '@angular/core'; @Component({ selector: 'app-labeled-field', templateUrl: './labeled-field.component.html', styleUrls: ['./labeled-field.component.scss'] }) export class LabeledFieldComponent implements OnInit { @Input() label = ''; @Input() value = ''; constructor() { } ngOnInit(): void { } }
-
Open labeled-field.component.html and replace the content with the following code:
<div class="field"> <div class="field-label">{{ label }}</div> <div>{{ value }}</div> </div>
-
You may have noticed CSS class identifiers for the "field" and "field-label" styles in the last bullet. These styles will be responsible for formatting the output of our div elements. These two style definitions could be put in our global styles.scss file however this is also a great use-case for using our component style file. The added benefit is if we ever want to copy this component into another project, the component defined styles will move with it.
Open labeled-field-component.scss and add the following code:
div.field { display: inline-block; padding: 0.3rem 0.2rem; margin-right: 10px; } div.field-label { font-size: 0.7rem; }
The display: inline-block; line is the important piece in this file that indicates the main div should display inline with other elements rather than display on its own line. The other style declarations are for paddings, margins and label font size. -
Our new component is ready to use. Return to the name-card.component.html and replace
the content with the following code:
<div> <app-labeled-field label="Name" [value]="person.nameLast + ', ' + person.nameFirst"></app-labeled-field> <app-labeled-field label="Age" [value]="person.age"></app-labeled-field> <app-labeled-field label="Gender" [value]="person.gender"></app-labeled-field> </div>
You may have noticed that our "label" input is not wrapped in square brackets but the "value" input is. The reason for this is that the label input uses a text value where the value input is using a variable expression. You could have represented the label input as [label]="'Name'" however that would be inefficient. - View your Angular application in the web browser. Your new content for your labeled fields should appear as follows:
Our next section will introduce us to Angular services where we will move our person object to a service that can be accessed from anywhere in our application.