Awad Unit V
Awad Unit V
Edit the “src/app/app-routing.module.ts” file to define a route for the home component. For
example, you can add the following to the routes array:
Javascript
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
Edit the “src/app/home/home.component.html” file to add some content. For example, you
can add the following:
HTML
import { AppRoutingModule }
from './app-routing.module';
import { AppComponent }
from './app.component';
import { HomeComponent }
from './home/home.component';
@NgModule({
declarations: [
AppComponent,
HomeComponent
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Routing in Angular allows the users to create a single-page application with multiple views
and allows navigation between them. Users can switch between these views without losing
the application state and properties.
Approach:
Create an Angular app that to be used.
Create the navigation links inside the app component and then provide
the “routerLink” directive to each route and pass the route value to “routerLink” directive.
Then add the routes to the routing.module.ts file and then import the routing.module.ts into
the app.module.ts file.
Syntax:
HTML:
<li><a routerLink="/about" >About Us</a></li>
<router-outlet></router-outlet>
TS:
{ path: 'about', component: AboutComponent }
Example: We are going to create a simple angular application that uses angular routing. So
first, we create an Angular app by running the below command in CLI.
ng new learn-routing
Then we are creating simple navigation that allows us to navigate between the different
components, and we have created some components as well, so users can switch between
these components using routing.
app.component.html
<span>
<ul>
<li><a routerLink="/" >Home</a></li>
<li><a routerLink="/products" >Products</a></li>
<li><a routerLink="/about" >About Us</a></li>
<li><a routerLink="/contact" >Contact Us</a></li>
</ul>
</span>
<router-outlet></router-outlet>
Here the router-outlet is routing functionality that is used by the router to mark wherein a
template, a matched component should be inserted.
Then inside the app-routing.module.ts file, we have provided these routes and let the
angular know about these routes.
app-routing.module.ts
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
providers: []
})
export class AppRoutingModule { }
And then simply import “AppRouting” module inside the app/module.ts file inside
the @NgModule imports.
app.module.ts
@NgModule({
declarations: [
AppComponent,
HomeComponent,
ProductComponent,
AboutComponent,
ContactComponent
],
imports: [
AppRoutingModule,
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
So now run this using “ng serve” in CLI and open localhost://4200 in the browser here you
see your navigation bar, and you can navigate from one component to another without page
reload.
Output:
The Model
The model contains the business end of your application. For simple CRUD (Create Read
Update Delete) applications, the model is usually a simple data model. For more complex
applications, the model will naturally reflect that increase in complexity. In the application
you see here, the model will hold a simple array of text notes. Each note has an ID, a title,
and a text. In Angular, the model is coded up in so-called services. The ng command lets
you create a new service.
@Injectable({
providedIn: 'root'
})
constructor() {
this.notes = JSON.parse(localStorage.getItem('notes')) || [];
for (const note of this.notes) {
if (note.id >= this.nextId) this.nextId = note.id+1;
}
this.update();
}
subscribe(observer: Observer<NoteInfo[]>) {
this.notesSubject.subscribe(observer);
}
deleteNote(id: number) {
const index = this.findIndex(id);
this.notes.splice(index, 1);
this.update();
}
private update() {
localStorage.setItem('notes', JSON.stringify(this.notes));
this.notesSubject.next(this.notes.map(
note => ({id: note.id, title: note.title})
));
}
The app.component contains the main page layout, but not any meaningful content. You
will have to add some components that will render any content. Use the ng
generate command again like this.
This generates two components. Each component is made up of a .html, .css, and a .ts file.
For now, don’t worry about the .ts file. I’ll get to that in the next section. (Remember, there
is also a .spec.ts file that I am ignoring completely in this tutorial.)
Open src/app/home/home.component.html and change the content to the following.
<h1>Angular Notes</h1>
<h2>A simple app showcasing the MVVM pattern.</h2>
Next, open src/app/notes/notes.component.html and replace the content with the code
below.
<div fxLayout="row" fxLayout.xs="column" fxLayoutAlign="center" class="notes">
<mat-list fxFlex="100%" fxFlex.gt-sm="20%">
<mat-list-item *ngFor='let note of notes'>
<a>
{{note.title}}
</a>
</mat-list-item>
</mat-list>
<mat-divider fxShow="false" fxShow.gt-sm [vertical]="true"></mat-divider>
<mat-divider fxShow="true" fxShow.gt-sm="false" [vertical]="false"></mat-divider>
<div fxFlex="100%" fxFlex.gt-sm="70%" *ngIf="!editNote" class="note-container">
<h3>{{currentNote.title}}</h3>
<p>
{{currentNote.text}}
</p>
<div fxLayout="row" fxLayoutAlign="space-between center" >
<button mat-raised-button color="primary">Edit</button>
<button mat-raised-button color="warn">Delete</button>
<button mat-raised-button color="primary">New Note</button>
</div>
</div>
<div fxFlex="100%" fxFlex.gt-sm="70%" *ngIf="editNote" class="form-container">
<form [formGroup]="editNoteForm">
<mat-form-field class="full-width">
<input matInput placeholder="Title" formControlName="title">
</mat-form-field>
<mat-form-field class="full-width">
<textarea matInput placeholder="Note text" formControlName="text"></textarea>
</mat-form-field>
<button mat-raised-button color="primary">Update</button>
</form>
</div>
</div>
.notes a {
cursor: pointer;
}
.form-container, .note-container {
padding-left: 2rem;
padding-right: 2rem;
}
.full-width {
width: 80%;
display: block;
}
So far, so good!
Have a look at src/app/notes/notes.component.html which represents the main View of the
application. You will notice placeholders such as {{note.title}} which look like they can be
filled with values. In the version shown above, the View does not seem to refer to any piece
of code in the application.
If you were to follow the MVC pattern, the View would define slots into which the data
could be inserted. It would also provide methods for registering a callback whenever a
button is clicked. In this respect, the View would remain completely ignorant of the
Controller.
The Controller would actively fill the values and register callback methods with the View.
Only the Controller would know about both the View and the Model and link the two
together.
As you will see below, Angular takes a different approach, called the MVVM pattern. Here
the Controller is replaced by a ViewModel. This will be the topic of the next section.
The ViewModel
The ViewModel lives in the .ts files of the components.
Open src/app/notes/notes.component.ts and fill it with the code below.
import { Component, OnInit } from '@angular/core';
import { Note, NoteInfo, NotesService } from '../notes.service';
import { BehaviorSubject } from 'rxjs';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
@Component({
selector: 'app-notes',
templateUrl: './notes.component.html',
styleUrls: ['./notes.component.css']
})
export class NotesComponent implements OnInit {
ngOnInit() {
this.notesModel.subscribe(this.notes);
this.editNoteForm = this.formBuilder.group({
title: ['', Validators.required],
text: ['', Validators.required]
});
}
onSelectNote(id: number) {
this.currentNote = this.notesModel.getNote(id);
}
noteSelected(): boolean {
return this.currentNote.id >= 0;
}
onNewNote() {
this.editNoteForm.reset();
this.createNote = true;
this.editNote = true;
}
onEditNote() {
if (this.currentNote.id < 0) return;
this.editNoteForm.get('title').setValue(this.currentNote.title);
this.editNoteForm.get('text').setValue(this.currentNote.text);
this.createNote = false;
this.editNote = true;
}
onDeleteNote() {
if (this.currentNote.id < 0) return;
this.notesModel.deleteNote(this.currentNote.id);
this.currentNote = {id:-1, title: '', text:''};
this.editNote = false;
}
updateNote() {
if (!this.editNoteForm.valid) return;
const title = this.editNoteForm.get('title').value;
const text = this.editNoteForm.get('text').value;
if (this.createNote) {
this.currentNote = this.notesModel.addNote(title, text);
} else {
const id = this.currentNote.id;
this.notesModel.updateNote(id, title, text);
this.currentNote = {id, title, text};
}
this.editNote = false;
}
}
In the @Component decorator of the class, you can see the reference to the
View .html and .css files. In the rest of the class, on the other hand, there is no reference to
the View whatsoever. Instead, the ViewModel, contained in the NotesComponent class,
exposes properties and methods that can be accessed by the View. This means that,
compared to the MVC architecture, the dependency is reversed. The ViewModel has no
knowledge of the View but provides a Model-like API that can be used by the View. If you
take another look at src/app/notes/notes.component.html you can see that the template
interpolation, such as {{currentNote.text}} directly accesses the properties of
the NotesComponent.
The last step to make your application work is to tell the router which components are
responsible for the different routes. Open src/app/app-routing.module.ts and edit the
content to match the code below.
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { NotesComponent } from './notes/notes.component';
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
This will link the HomeComponent to the default route and the NotesComponent to
the notes route.
For the main application component, I will define a few methods which will be
implemented later on. Open src/app/app.component.ts and update the content to look like
the following.
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
public title = 'Angular Notes';
public isAuthenticated: boolean;
ngOnInit() {
this.isAuthenticated = false;
}
login() {
}
logout() {
}
}
The component contains two properties title and isAuthenticated. The second one of these
is a flag that indicates whether the user has logged into the application. Right now, it is
simply set to false. Two empty methods act as callbacks to trigger logging in or logging
out. For now, I have left them empty, but you will be filling them in later on.
Complete the View
With this knowledge about the direction of dependency, you can update the View so that
the buttons and forms perform actions on the ViewModel.
Open src/app/notes/notes.component.html again and change the code to look like this.
<div fxLayout="row" fxLayout.xs="column" fxLayoutAlign="center" class="notes">
<mat-list fxFlex="100%" fxFlex.gt-sm="20%">
<mat-list-item *ngFor='let note of notes | async'>
<a (click)="onSelectNote(note.id)">
{{note.title}}
</a>
</mat-list-item>
</mat-list>
<mat-divider fxShow="false" fxShow.gt-sm [vertical]="true"></mat-divider>
<mat-divider fxShow="true" fxShow.gt-sm="false" [vertical]="false"></mat-divider>
<div fxFlex="100%" fxFlex.gt-sm="70%" *ngIf="!editNote" class="note-container">
<h3>{{currentNote.title}}</h3>
<p>
{{currentNote.text}}
</p>
<div fxLayout="row" fxLayoutAlign="space-between center" >
<button mat-raised-button color="primary" (click)="onEditNote()"
*ngIf="noteSelected()">Edit</button>
<button mat-raised-button color="warn" (click)="onDeleteNote()"
*ngIf="noteSelected()">Delete</button>
<button mat-raised-button color="primary" (click)="onNewNote()">New
Note</button>
</div>
</div>
<div fxFlex="100%" fxFlex.gt-sm="70%" *ngIf="editNote" class="form-container">
<form [formGroup]="editNoteForm" (ngSubmit)="updateNote()">
<mat-form-field class="full-width">
<input matInput placeholder="Title" formControlName="title">
</mat-form-field>
<mat-form-field class="full-width">
<textarea matInput placeholder="Note text" formControlName="text"></textarea>
</mat-form-field>
<button mat-raised-button color="primary">Update</button>
</form>
</div>
</div>
constructor(
private backend: BackendService,
private logger: Logger) { }
getHeroes() {
Dependency injection (DI) is the part of the Angular framework that provides components
with access to services and other resources. Angular provides the ability for you to inject a
service into a component to give that component access to the service.
Add the @Injectable() decorator to a service class so that Angular can inject it into a
component as a dependency; the optional argument tells Angular where to register this class
by default.
content_copy@Injectable({providedIn: 'root'})
export class HeroService {
• Something injectable must be registered with an injector before it can be created
and used.
• Register an injectable with a provider, an object that tells an injector how to obtain
or create a dependency. For a service class, the provider is typically the class itself.
• You don't have to create injectors. Under the hood Angular creates an application-
wide root injector for you during the bootstrap process. It creates additional child
injectors as needed.
An injectable dependency doesn't have to be a class — it could be a function, for
example, or a value.
When Angular creates a new instance of a component class, it determines which services or
other dependencies that component needs by looking at the constructor parameter types.
For example, the constructor of HeroListComponent needs HeroService.
src/app/hero-list.component.ts (constructor)
content_copyconstructor(private service: HeroService) { }
When Angular discovers that a component depends on a service, it first checks if the
injector has any existing instances of that service. If a requested service instance doesn't yet
exist, the injector makes one using the registered provider and adds it to the injector before
returning the service to Angular.
When all requested services have been resolved and returned, Angular can call the
component's constructor with those services as arguments.
The process of HeroService injection looks something like this.
Providing services
You must register at least one provider of any service you are going to use. The provider
can be part of the service's own metadata, making that service available everywhere, or you
can register providers with specific components. You register providers in the metadata of
the service (in the @Injectable() decorator) or @Component() metadata
• By default, the Angular CLI command ng generate service registers a provider with
the root injector for your service by including provider metadata in
the @Injectable() decorator. The tutorial uses this method to register the provider
of HeroService class definition.
hero.service.ts (provide in root)
content_copy@Injectable({providedIn: 'root'})
export class HeroService {
When you provide the service at the root level, Angular creates a single, shared
instance of HeroService and injects it into any class that asks for it. Registering the
provider in the @Injectable() metadata also allows Angular to optimize an app by
removing the service from the compiled application if it isn't used, a process known
as tree-shaking.
• When you register a provider at the component level, you get a new instance of the
service with each new instance of that component. At the component level, register
a service provider in the providers property of the @Component() metadata.
src/app/hero-list.component.ts (component providers)
content_copy@Component({
standalone: true,
selector: 'app-hero-list',
templateUrl: './hero-list.component.html',
imports: [ NgFor, NgIf, HeroDetailComponent ],
providers: [ HeroService ]
})
USING UGLIFY JS TO MINIFY AND CONCATENATE SCRIPTS
UglifyJS is a JavaScript parser, minifier, compressor and beautifier toolkit.
Note:
• uglify-js supports JavaScript and most language features in ECMAScript.
• For more exotic parts of ECMAScript, process your source file with transpilers
like Babel before passing onto uglify-js.
• uglify-js@3 has a simplified API and CLI that is not backwards compatible
with uglify-js@2.
Install
First make sure you have installed the latest version of node.js (You may need to restart
your computer after this step).
From NPM for use as a command line app:
npm install uglify-js -g
From NPM for programmatic use:
npm install uglify-js
Minify options
• annotations — pass false to ignore all comment annotations and elide them from
output. Useful when, for instance, external tools incorrectly
applied /*@__PURE__*/ or /*#__PURE__*/. Pass true to both compress and retain
comment annotations in output to allow for further processing downstream.
• compress (default: {}) — pass false to skip compressing entirely. Pass an object to
specify custom compress options.
• expression (default: false) — parse as a single expression, e.g. JSON.
• ie (default: false) — enable workarounds for Internet Explorer bugs.
• keep_fargs (default: false) — pass true to prevent discarding or mangling of
function arguments.
• keep_fnames (default: false) — pass true to prevent discarding or mangling of
function names. Useful for code relying on Function.prototype.name.
• mangle (default: true) — pass false to skip mangling names, or pass an object to
specify mangle options (see below).
o mangle.properties (default: false) — a subcategory of the mangle option. Pass an
object to specify custom mangle property options.
• module (default: true) — process input as ES module, i.e. implicit "use strict"; and
support for top-level await. When explicitly specified, also enables toplevel.
• nameCache (default: null) — pass an empty object {} or a previously
used nameCache object if you wish to cache mangled variable and property names
across multiple invocations of minify(). Note: this is a read/write
property. minify() will read the name cache state of this object and update it during
minification so that it may be reused or externally persisted by the user.
• output (default: null) — pass an object if you wish to specify additional output
options. The defaults are optimized for best compression.
• parse (default: {}) — pass an object if you wish to specify some additional parse
options.
• sourceMap (default: false) — pass an object if you wish to specify source map
options.
• toplevel (default: false) — set to true if you wish to enable top level variable and
function name mangling and to drop unused variables and functions.
• v8 (default: false) — enable workarounds for Chrome & Node.js bugs.
• warnings (default: false) — pass true to return compressor warnings
in result.warnings. Use the value "verbose" for more detailed warnings.
• webkit (default: false) — enable workarounds for Safari/WebKit bugs. PhantomJS
users should set this option to true.
Minify options structure
{
parse: {
// parse options
},
compress: {
// compress options
},
mangle: {
// mangle options
properties: {
// mangle property options
}
},
output: {
// output options
},
sourceMap: {
// source map options
},
nameCache: null, // or specify a name cache object
toplevel: false,
warnings: false,
}
1. Create a Script to Concatenate and Minify: You can create a simple Node.js script to
concatenate and minify your JavaScript files. Here’s an example:
JavaScript
const fs = require('fs');
const UglifyJS = require('uglify-js');
Hmmmm, this isn’t quite what you wanted, but at least the pipe seems to be working.
There’s a good reason for this output: security. Angular protects you and your application
from malicious attacks by preventing HTML from being injected into a data binding.
Think about when you let visitors write reviews for locations, for example. If they could add
any HTML they wanted to, someone could easily insert a <script> tag and run some
JavaScript, hijacking the page.
But there’s a way to let a subset of HTML tags through into a binding, which you’ll look at
next.
SAFELY BINDING HTML BY USING A PROPERTY BINDING
Angular lets you pass through some HTML tags if you use a property binding instead of the
default bindings you normally use for content. This technique works only for a subset of
HTML tags to prevent XSS hacks, attacks, and weaknesses. Think of property binding as
being “one-way” binding. The component can’t read the data back out and use it, but it can
update it and change the data in the binding.
You used property bindings when you passed data into nested components.
Remember building the About page? There, you were binding data to a property you defined
in the nested component, which you called content. Here, you’re binding to a native property
of a tag—in this case, innerHTML.
Property bindings are denoted by wrapping square brackets around them and then passing the
value. You can remove the content binding in about.component .html and use a property
binding:
<div class="col-12 col-lg-8" [innerHTML]="pageContent.content |
htmlLineBreaks"></div>
Note that you can apply pipes to this type of binding too, so you’re still using your
htmlLineBreaks pipe. Finally, when you view the About page in the browser, you’ll see the
line breaks in place, looking like figure 9.15.
Success! You’ve made a great start toward building Loc8r as an Angular SPA.
You’ve got a couple of pages, some routing and navigation, geolocation, and a great modular
application architecture. Keep on moving!
With the data service method ready, you can import the service into the Details page
component, ready to use.
IMPORTING THE DATA SERVICE INTO THE COMPONENT
You’ve imported a service into a component before—the data service into the homelist
component—so we won’t dwell on the process too much here. You’ll need to import the data
service into the Details page component, add it to the providers, and then make it available by
declaring it in the class constructor.
While you’re here, you’ll also import the Location class from the home-list component and
empty the default page content. All these updates to details-page.component are shown in the
following listing.
The only real thing to be careful with here is the case of loc8rDataService in the constructor:
the class type definition has an uppercase L, and the local instance is defined with a
lowercase l.Now you’re ready to get the URL parameter into the component.
USING URL PARAMETERS IN A COMPONENT
Given that using URL parameters in an app is a common requirement, the process is
surprisingly complicated. You need three new pieces of functionality:
ActivatedRoute from the Angular router to get you the value of the current route from
inside the component
ParamMap from the Angular router to get you the URL parameters of the active route as
an Observable
Dr. R.PRIYA Page 23
PCA20D01J –ADVANCED WEB APPLICATION DEVELOPMENT UNIT V
switchMap from RxJS to get the values from the ParamMap Observable and use them to
call your API, creating a second Observable
The following snippet shows in bold the additions needed in details-page.component.ts to
import these pieces of functionality:
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { Loc8rDataService } from '../loc8r-data.service';
import { Location } from '../home-list/home-list.component';
import { switchMap } from 'rxjs/operators';
You also need to make the activated route available to the component by defining a private
member route of type ActivatedRoute in the constructor:
constructor(
private loc8rDataService: Loc8rDataService,
private route: ActivatedRoute
){}
Now comes the complicated bit. Complete these steps to get a location ID from the URL
parameter and turn it into location data from the API:
1 When the component initializes, use switchMap to subscribe to the paramMap Observable
of the activated route.
2 When the paramMap Observable returns a ParamMap object, get the value of the locationId
URL parameter.
3 Call the getLocationsById method of your data service, passing it the ID.
4 Return the API call so that it returns an Observable.
5 Subscribe to listen for when the Observable returns the data from your API.
The result should be a single object of type Location.
6 Set the content for the page header and sidebar, using the location name returned from the
API.
Phew! That’s a lot of steps for a seemingly simple process. All this takes place in the
ngOnInit lifecycle hook in details-page.component.ts. The next listing shows what the code
looks like.
That’s some fairly dense code; a lot is happening in a few lines and commands. We
recommend reading the plan and the annotated code a few times to piece everything together.
It’s powerful, a little different from what you’ve seen so far, and about as complex as you’ll
see in this book. In particular, note the two chained Observables: first, the route paramMap
being subscribed to by the switchMap, which returns the second. The good news is that when
you’re done, your Details page shows the location name in the page header and the sidebar,
as shown in figure 10.5.
You’re now using the location ID in the URL to query the database and passing a bit of the
returned data to two of the components on the page. Before you build out the main part of the
Details page, make sure that the final component is getting the data it needs.
BUILDING THE DETAILS PAGE VIEW
For the location details, you’ve already got a Pug template with Pug data bindings, and you
need to transform this template into HTML with Angular bindings. You have quite a few
bindings to put in place, as well as some loops, utilizing Angular’s *ngFor construct. You’ll
use the rating-stars component that you created for the challenge at the end of chapter 9 to
show the overall rating and the rating for each review. If you haven’t created this component,
refer to the book’s code repository on GitHub. You’ll also need to allow line breaks in the
review text by using the htmlLineBreaks pipe.
That code listing is long, but that’s to be expected, as quite a lot is going on in the Details
page. If you look at the page in the browser now, it looks about right. You have a few things
to fix, but you know about them.
Although the page looks good, if you open the JavaScript console, you’ll see that the page
has thrown a lot of errors along the lines of Cannot read property 'rating' of undefined. These
errors are binding errors, happening because the nested location details component is trying
to bind to data as soon as the page loads, but you don’t have any data until after the API call
has completed.
WORKING WITH FORMS AND HANDLING SUBMITTED DATA
In this section, you’ll create the Add Review page in Angular, and have it submit data to the
API. Rather than navigate to a separate form page when the Add Review button is clicked,
it’ll display a form inline in the page. When the form is submitted, you’ll have Angular
handle the data, submit it to the API, and display the new review at the top of the list. You’ll
start by seeing what’s involved with creating the form in Angular.
Creating the review form in Angular
To create the review form, you’ll get the HTML in place, add data bindings to the input
fields, make sure that they all work as expected, and, finally, ensure that the form is initially
hidden and is displayed only if the button is clicked.
PUTTING THE FORM HTML IN PLACE
Add the inline form to the page just after the Customer reviews<h2> tag, as shown in the
following listing. Much of the layout is taken from the form you used in Express, including
the form input names and IDs.
Right now, you’re not doing anything clever or asking Angular to do anything. You’ve put
raw HTML with some Bootstrap classes in the template. In the browser, this looks like figure
10.7.
This looks good and, on the face of it, seems to work. But you want the rating to be a number,
and in a select option, value="5" is a string containing the character 5.
WORKING WITH SELECT VALUES THAT ARE NOT STRINGS
A select option value is by default a string, but your database requires a number for the
rating. Angular has a way to help you get different types of data from a select field.
Instead of using value="STRING VALUE" inside each <option>, use [ngValue]=
"ANGULAR EXPRESSION". When written out, the value of [ngValue] looks like a string,
but it’s an Angular expression. This could be an object or a true Boolean, but you want a
number.
In location-details.component.html, update each of the <option> tags to use [ngValue]
instead of value:
<option [ngValue]="5">5</option>
<option [ngValue]="4">4</option>
<option [ngValue]="3">3</option>
<option [ngValue]="2">2</option>
<option [ngValue]="1">1</option>
Now Angular gets the value of the <select> as a number, not a string. This technique will be
useful when you submit the data to the API. Next, you hide the form by default, as you don’t
want it showing all the time.
SETTING THE VISIBILITY OF THE FORM
You don’t want the Add Review section of the page to be visible when the page loads; you
want the Add Review button to show it, and when the form is displayed, you want the Cancel
button to hide it again.
To show and hide the form, you can use your old friend *ngIf. *ngIf needs a Boolean value to
decide whether to show the element it’s applied to, so you’ll need to define one in the
component.
In location-details.component.ts, define a new public member formVisible of type boolean
with a default value of false:
public formVisible: boolean = false;
You’ve set the default value to false, as you want the form to be hidden by default. To use
this Boolean to set the visibility of the form, locate the <div> surrounding the
<form> in location-details.component.html, and add the *ngIf directive to it like this:
<h2 class="card-title">Customer reviews</h2>
<div *ngIf="formVisible">
<form action="">
Now the form is hidden by default when the page loads.
TOGGLING THE VISIBILITY OF THE FORM
To change the visibility of the form, you need a way to change the value of form-Visible
when the Add Review and Cancel buttons are clicked. Not surprisingly, Angular has an on-
click handler you can use to track clicks of elements and then do something.
Angular’s click handler is accessed by adding (click) to the element and giving it an Angular
expression. This expression could be one that calls a public member in the component class
or any other kind of valid expression. You want to set formVisible to true when the Add
Review button is clicked and false when the Cancel button is clicked.
In location-details.component.html, change the Add Review button from an <a>
tag to a <button>, removing the href attribute and replacing it with a (click) setting
formVisible to be true:
<button (click)="formVisible=true" class="btn btn-primary float-right">Add
review</button>
In a similar way, add a (click) to the Cancel button, setting formVisible to be false:
<button type="button" (click)="formVisible=false" class="btn btn-default
float-right">Cancel</button>
With those click handlers in place, you can use the two buttons to show and hide the review
form, using the formVisible property of the component to keep track of the status. The final
thing you need to do is hook up the form so that when it’s submitted, a review is added.