0% found this document useful (0 votes)
55 views

CS 465 Module Six Full Stack Guide

Uploaded by

b8jacks
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
55 views

CS 465 Module Six Full Stack Guide

Uploaded by

b8jacks
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 74

CS 465 Module Six Full Stack Guide

Module Six: Single-Page Application

In this module, you will begin creating a single-page application (SPA) that uses the API calls to fetch and
manipulate data efficiently. You will use Angular. Angular is an industry standard for front-end web-
application development. Angular is the basis for the coding framework for developing the SPA. To learn
more about Angular, review the Angular website.

Create Git Branch for Module Six


Before you begin, you must create your new branch in Git for Module Six. To create your new branch, in
a PowerShell window in the travlr project directory, enter the following command:

git checkout -b module6

Creating the Angular Admin Site


This next development phase will change how the application is displayed to the user. Up to this point,
the server has handled all the application logic, with webpages rendered server-side and then presented
to the client’s browser for display to the user. In this development phase, you will create an SPA. An SPA
is a common web-application development form where all application-rendering work happens on the
client side and effectively minimizes client-server traffic.

To begin this development process, you must first install Angular and create the admin site.

1. Install the latest version of the Angular command line interface (CLI). This guide is based on
Angular v17. You should also be able to use this guide with newer versions of Angular. Carefully
read the release notes to see if changes to the Angular API have impacted any of the concepts or
methodologies used in this guide. A developer would be responsible for reviewing release notes
when building and maintaining an application in a professional environment.

Note: There is an at sign (@) in the command to install the Angular CLI. The at sign differs from what you
have used when installing a package in Node.js. The at sign designates the Angular namespace, which is
a packaging or protection mechanism in Node.js. When installing a package beneath @angular, you
should have confidence that the package was developed and published by the Angular development
team.

npm install -g @angular/cli


2. Now that you have installed Angular in the Node.js environment, create a new Angular
application. To do this step, run a command to create a new tree within the existing
development tree.

ng new travlr-admin --defaults=true --skipGit=true --skipTests=true --directory app_admin


You will be asked if you want to share usage data for what you are building with Google for analytics.
Answering this question with an ‘n’ for no is safe.
3. Now that you have created the Angular app in the travlr project tree, you must make two
structural changes to conform to industry best practices for application organization. First, open
the project in VS Code and find the new app_admin folder. Descend the app_admin tree and
create a src/assets/css folder to store the stylesheets for the application.
4. Once you have created the css folder, drag the style.css file from app_admin/src into the css
folder. Moving the style.css file will help provide organizational consistency. Moving the style.css
file will also require some additional editing of the default application.
5. Now that you have moved the stylesheet, tell the Angular platform where to find it. To do this
step, edit the file app_admin/angular.json and replace the two references to src/styles.css with
src/assets/css/styles.css.
6. Replace the title and add a loading message. This step will require you to edit the
app_admin/src/index.html file.

7. After completing the preliminary setup, open another PowerShell window and start the
administrative server. Move into the app_admin folder and start the server with the following
command:

ng serve
8. Now the application is running, and you can attach to it. Open a new tab in your browser and
connect to the app at http://localhost:4200.

9. The title “Hello, travlr-admin” is rather clunky. You can adjust the title in the
/src/app/app.component.ts to change this string and save it. The browser will immediately
refresh, and the text should render as specified.
When you save this window, the client window re-renders.

You have now created the basic Angular application that will support the administrative functionality for
the Travlr Getaways application.

Create Trip Listing Component


Now that you have the initial application, you need to create an Angular component so that the
application will display useful data. This component will be used to display the list of trips. Review the
Angular Components Overview to learn more about Angular components.

1. In your PowerShell window, change to the app_admin folder in your application and generate
an Angular component called trip-listing to list the trips.

ng generate component trip-listing


2. Create a data folder beneath src/app and create a trips.ts file. This file will contain the
TypeScript code that is based on the JSON data (collection of trip records) you created earlier.
Copy the contents of the trips.json file in the upper level /data folder and assign the data to a
variable called trips.
3. Edit the trip-listing.component.ts file to add an import of the new trips.ts file. Define an
appropriate class variable within the TripListingComponent class to contain the data. You will
also need to add OnInit to the imports for the class and set up the basic structure for the
TripListingComponent.

The next step will require you to edit the trip-listing.component.html file and replace the initial
contents: <p>trip-listing works!</p> with <pre>{ { trips | json } }</pre>.

Adding the trips array to the class file makes the variable accessible within the HTML. Notice that
Angular uses the double curly brace notation like Handlebars.
4. Make one more edit to the app.component.ts file and pull in the TripListingComponent. This
edit will allow you to access the data you pulled into that component from the data file.

With one more small edit, you can dynamically pull the title variable from the AppComponent class and
the trips array via the app-trip-listing selector from the TripListingComponent class and render them in
the application. This edit will be made in the app.component.html file.
Once this file is saved, the application should automatically refresh and show the JSON in application.

Now that you have access to the data, you must determine how to format it to make it usable for a
front-end application. To handle this step, you will involve the Bootstrap CSS framework. Adding
Bootstrap requires an additional install in the Node.js environment. Learn more about the Bootstrap CSS
framework at Get Started With Bootstrap.

5. Install jQuery to enable several data manipulations in the Node.js environment. Once you install
jQuery, use the node ls -g command to show what you have installed in the global context in the
Node.js environment.

Then use the npm ls command.

6. Keep in mind that the Bootstrap CSS framework is versioned. Pay attention to the versions that
you use and include in the application. This Full Stack Guide uses the following versions:

• Bootstrap version 5.3.2


• Popper version 2.11.8
• jQuery version 3.7.1

The tags needed to import these packages into the codebase can be found in the following locations:

• Bootstrap and Popper: Get Started With Bootstrap


• jQuery: jQuery CDN – Latest Stable Versions

When working with these tools, you may need to change versions to use new features or address
material defects in existing code. You will add code to the app_admin/src/index.html file.

You will add the Bootstrap CSS package as a link in the head section. You will also add the Javascript
packages for Popper, Bootstrap, and jQuery packages as script entries in the body. For this Full Stack
Guide, use the following configurations:
• Bootstrap (head section):

<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
rel="stylesheet" integrity="sha384-
T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
crossorigin="anonymous">

• Bootstrap (body section):

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
integrity="sha384-
C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"
crossorigin="anonymous"></script>

• Popper (body section):

<script src="https://cdn.jsdelivr.net/npm/@popperjs/[email protected]/dist/umd/popper.min.js"
integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r"
crossorigin="anonymous"></script>

• jQuery (body section):

<script src="https://code.jquery.com/jquery-3.7.1.slim.min.js" integrity="sha256-


kmHvs0B+OpCW5GVHUNjv9rOmY0IvSIRcf7zGUDTDQM8=" crossorigin="anonymous"></script>

Note: Adding the Bootstrap CSS package is required to make the Bootstrap CSS framework available to
the entire application and to allow cross-origin resource sharing (CORS) requests to be made when
retrieving this code. You can learn more about CORS on Mozilla’s Cross-Origin Resource Sharing (CORS)
webpage.

7. Copy the images folder from the Express public folder src/app/assets so the images are
available to the Angular application.

8. In the trip-listing.component.html file, replace the initial code that displayed the JSON data
with a segment of HTML code that can be rendered for each data record provided by the trip-
listing.component.

The contents of the trip-listing.component.html file will be replaced with the following code:

<!-- <pre>{{trips | json}}</pre> -->


<div class="row">
<div *ngFor= “let trip of trips”>
<div class="card ml-4" style="width: 18rem;">
<div class="card-header">{{ trip.name }}</div>
<img src="assets/images/{{ trip.image }}"
alt="trip thumbnail" class="card-img-top">
<div class="card-body">
<h6 class="card-subtitle mb-2 text-muted">
{{ trip.resort }}
</h6>
<p class="card-subtitle mt-3 mb-3 text-muted">
{{trip.length}} only {{trip.perPerson|currency:'USD':true}} per person
</p>
<p class="card-text" [innerHTML]="trip.description”>
</p>
</div>
</div>
</div>
</div>
This code relies on the Bootstrap CSS framework to generate a pleasing view of the data.
Note: By default, the cards will appear vertically in the window. To make them appear horizontally, you
must make certain that you have enough horizontal space. Each row in the Bootstrap framework
consumes 12 columns, proportional to the width of your browser window. When the file is saved, it will
appear like the screenshot below.
If you edit the *ngFor’ tag-set in the trip-listing.component.html file, you can add a class attribute that
will set the number of columns that each card should consume. After 12 columns have been consumed,
the next cards will appear beneath the first set of cards. For example, when editing the tag-set to read
<div *ngFor="let trip of trips" class="col-3">, - you should see the following result:

9. The app.component.html page can be cleaned up to render the listing with a navigation bar and
some white space. To clean up the page, replace the contents with the following code:

<!-- <h1>{{ title }}</h1> -->


<nav class=” navbar navbar-default”>
<div class="container-fluid">
<a class="navbar-brand" href="#">{{ title }}</a>
</div>
</nav>

<div class="container">
<app-trip-listing></app-trip-listing>
</div>
Replacing the code will turn the title into a hotlink to the admin page.
Refactor Trip Rendering Logic Into an Angular Component
Now that you can see the trip listing in something other than an array of JSON objects, things are looking
up for the application. However, there is still a fundamental problem: The application can only show the
trip listing one way, as cards. You may have a problem if the user needs to toggle between a card view
and a list two. Using two types of rendering for the same data can be a coding nightmare and difficult
for the coding to handle.

However, if you pull the card rendering logic into a separate component, the trip listing only needs to
toggle the correct component to switch the layout. This action can be accomplished with a selector tag.
The technique of separating the logic so that one class only does one thing is referred to as separation of
concerns (SoC). SoC is a powerful software engineering principle, and it distinguishes itself by reducing
single large mega classes to smaller classes that are easier to manage and must only account for a single
responsibility. You can learn more about SoC at Effective Software Design’s Separation of Concerns
webpage.

1. Begin the process of pulling the rendering into a separate component by generating a new
component called trip-card. You will accomplish this step from PowerShell within the
app_admin directory with the following command:

ng generate component trip-card

2. With the new component, you must change a pair of files. First, pull the rendering code from
the trip-listing.component.html file and place it in the trip-card.component.html file. For this
purpose, the trip-card.component.html file has the following content:

<div class="card" style="width: 16rem;">


<div class="card-header">{{ trip.name }}</div>
<img src="assets/images/{{ trip.image }}"
alt="trip thumbnail" class="card-img-top">
<div class="card-body">
<h6 class="card-subtitle mb-2 text-muted">
{{ trip.resort }}
</h6>
<p class="card-subtitle mt-3 mb-3 text-muted">
{{trip.length}} only {{trip.perPerson|currency:'USD':true}} per person
</p>
<p class="card-text" [innerHTML]="trip.description”>
</p>
</div>
</div>

This section is replaced in the trip-listing.component.html file with the following code:

<app-trip-card [trip]="trip" class="card-deck mt-2"></app-trip-card>


3. To pull this content into the application, add code to the trip-card.component.ts file to add the
Input directive so that the trip-listing component can pass the trip data and render an instance
of a trip.

Your trip-card.component.ts file should contain the following code:

import { Component, OnInit, Input } from '@angular/core';


import { CommonModule } from '@angular/common';

@Component({
selector: ‘app-trip-card’,
standalone: true,
imports: [CommonModule],
templateUrl: './trip-card.component.html',
styleUrl: './trip-card.component.css'
})
export class TripCardComponent implements OnInit {

@Input(‘trip’) trip: any;

constructor() {}

ngOnInit(): void {

}
}
4. If you have been paying attention to your console window, you will notice an error at this point.

You see this error because you are calling a new component from the trip-listing component, but trip-
listing does not know what it is or can do. To fix the error, you must make one more change in the trip-
listing.component.ts file.

Your updated trip-listing.component.ts file should look like this:

import { Component, OnInit } from '@angular/core';


import { CommonModule } from '@angular/common';
import { trips } from '../data/trips';
import { TripCardComponent } from '../trip-card/trip-card.component';
@Component({
selector: ‘app-trip-listing’,
standalone: true,
imports: [CommonModule, TripCardComponent],
templateUrl: './trip-listing.component.html',
styleUrl: './trip-listing.component.css'
})

export class TripListingComponent implements OnInit {


trips: Array<any> = trips;

constructor() {}

ngOnInit(): void {

}
}

Once you have made and saved these changes, in your ng serve PowerShell window, you will see
something like the following output:
Your output window for the application should look like the screenshot below.

With these quick steps, you have created a component that handles the rendering of a single trip. You
have also refactored the existing code to simplify the application. Additionally, the component can be
used anywhere in the application.

Create Trip Data Service


Now that you have seen what it takes to create the necessary components to render the trips, start
thinking about where the data will come from. In the SPA, obtaining information from another part of
the application is increasingly common, particularly with distributed or multitier architectures. In the
SPA, this means calling a REST endpoint to obtain data. Angular supports SoCs by defining a service to
contain functions or objects that components use to perform actions.

In Module Five, you created an /api REST endpoint on the back-end application using Express. The
endpoint was designed to handle data requests, and you could successfully test that with the
application. Now, you will create an Angular service to handle access to the back-end endpoint for trip
information.
1. The first step in allowing the Angular admin site to make calls against the Express back-end API
is to adjust the Express application to allow for external calls. This change is made in the app.js
file at the root of the application tree. The request enables what is generally referred to as
CORS.

Refer to the “Allowing CORS Requests in Express” section in Chapter 9 of the textbook for more
information.

2. Create a new Angular service called trip-data to allow you to configure the appropriate data
paths. To accomplish this step, use the following command:

ng generate service trip-data


3. You must do some housekeeping to prepare the application structure for more services later.
Create a new folder under app_admin/src/app called services and move the trip-data.service.*
files from the app_admin/src/app folder into the new app_admin/src/app/services folder.
When complete, your file structure should resemble the screenshot below.
4. For the new basic service to function correctly, you need a way to communicate what type of
data it will handle. Create a models folder and put it parallel to the data and service directories
under app_admin/src/app. In this folder, you will create an interface to define the data for a
single trip received from the API endpoint as a JSON object. You will define the model in a file
named trip.ts.

You will use instances of this interface to transfer HTML form data to the component for rendering and
between components and the REST endpoint. Angular will automatically marshal the data back and
forth between JSON and JavaScript objects.
5. Now that the interface is defined, introduce it to the service by editing the trip-data.service.ts
file and adding some code. This step will be very straightforward because you won’t introduce
error handling on the connection at this time. You will introduce another module for import
because the HTTP connection in the service is asynchronous.

Recall that you had to treat the internal API calls differently in the Express application because they
were issued asynchronously in Node.js. The same item applies here with Angular, but the
implementation is slightly different. The module that you will introduce is observable from the rxjs
package.

Because you are returning an observable object, the component can attach to that object and get a
notification when the async call has been completed and the associated promise is fulfilled.
6. Before you move forward with making the change to the trip-listing.component.ts file, you
must make one small change to the application so that the new data service will be functional.
Enable the HTTP services at the application level by adding two lines to the app.config.ts file in
the app_admin/src/app folder.

The two lines you added to the file allow the Angular environment to communicate via HTTP. Without
these lines, the new service couldn’t function.

7. Now, you must make some changes to the trip-listing.components.ts file so that you can use
the new service and pull the data from the database instead of the data file. You must make the
following changes:

a. Import the Trip model.

import { Trip } from '../models/trip';

b. Import the TripDataService.

import { TripDataService } from '../services/trip-data.service';

c. Register TripDataService as a provider.

providers: [TripDataService]

d. Create a constructor to initialize the TripDataService.

constructor(private tripDataService: TripDataService) {


console.log('trip-listing constructor');
}

e. Create a method that will call the getTrips() method in TripDataService.


private getStuff(): void {
this.tripDataService.getTrips()
.subscribe({
next: (value: any) => {
this.trips = value;
if(value.length > 0)
{
this.message = 'There are ' + value.length + ' trips available.';
}
else{
this.message = 'There were no trips retrieved from the database';
}
console.log(this.message);
},
error: (error: any) => {
console.log('Error: ' + error);
}
})
}

f. Create an ngOnInit method that will call the private method when the component is
initialized.

ngOnInit(): void {
console.log('ngOnInit');
this.getStuff();
}
You can see how these changes are completed in the following screenshot:

Make certain the Express app is running in a PowerShell window. From this console, you can see each
time an API call is made to the /api/trips endpoint. This function is very helpful with debugging.
Additionally, open the developer tools panel for your web browser. In Microsoft Edge, you can achieve
this with CTRL+Shift+I. This method is helpful in debugging Angular applications because you can watch
the local console and output console.log messages to provide debugging information for your
development process. In the screenshot below, you can see the results of the message that you put in
the getStuff method.
You can also see warnings in this window. Notice there is an issue with the currency pipe that you are
using in the trip-card.component.html file. Newer versions of Angular making items from previous
versions obsolete is a common occurrence. When you find these items, you need to fix them.

8. Fixing the error upon inspecting the results of the service is very straightforward. The offending
issue is with the pipe in the trip-card.component.html file, which formats the data as currency.
In the previous instance, the third argument to the pipe was a Boolean value that indicated that
the conversions should be done. In this case, the file becomes one of three flags: code, symbol,
or symbol-narrow. For this case, you will make the flag ‘symbol’, but feel free to experiment and
see what happens when you change it through each option.

Notice that while the display is the same, there are no errors in the developer’s console.
Add Trips
You have created an application that will display the trips. But if you want to change a trip definition or
add a new trip, you must manually edit the Mongo database. Manual editing is not practical in a
production application, so you will add code that allows you to add and edit trip information. This
process will require adding new components, forms, and routes. You must also update the logic on the
backend to add more API capability for the /api endpoint to support these new capabilities.

1. Create a new Angular component called add-trip that will support adding a new trip to the
application.

ng generate component add-trip

2. Change the trip-listing.component.html file to add a button that uses the add-trip functionality
for the application. Also, change the column definition for the application to make the display
more pleasing and the cards correctly flow when changing screen sizes. This functionality should
work on both mobile devices and desktops.

The button is set to execute an addTrip() method when pressed. You will wire this up in the coming
steps. The change to the class definition for the trip selector specifies to Bootstrap that each card will
consume 4 of 12 columns on a medium or larger screen that is 768 pixels or more and will wrap, or stack
the cards vertically, on anything smaller.
3. However, you cannot see the update’s impact at this point. As you save this file, you will be
informed that the addTrip() property does not exist for the TripListingComponent.

Next, you will modify the trip-listing.component.ts file to add the new capability.

4. You will make three changes in this file:

• Add an import statement to bring in the routing capability.


• Update the constructor to initialize the routing capability.
• Add an addTrip() method that will support the new button.

The import statement is shown in the screenshot below.

The import statement pulls in the Angular routing capability.

The final two changes are shown in the screenshot below.


You must make the addTrip() method public because it will need to be accessed externally from the
perspective of the trip-listing component. At this point, the window with the new updates looks like the
screenshot below.
The display is visually pleasing. But if you shrink the screen size, the vertically stacked cards on a smaller
screen have the same width, and the scale does not look correct. The following screenshot shows what
you see with the viewport shrunk to fewer than 768 pixels in width.

You can make one more small change to improve things and make the cards scale dynamically. Edit the
trip-card.component.html file and remove the width parameter from the card.
With this change, the card will now automatically scale with the viewport’s size.

When you expand the viewport back to more than 768 pixels in width, it looks like the screenshot
below.
5. If you were to test the application now by pressing the add-trip button, you would generate an
error because you do not yet have the necessary logic for it to do anything.

The next step is to wire up the routing capability for the application. The built-in routing capability in
Angular is powerful and will allow you to update the application to become more dynamic with its
display capabilities.

6. It is common for SPAs and web applications to have many URLs for their various pages and
features. Angular’s routing service allows you to simplify how URLs are addressed in the SPA.
Follow a best practice by isolating the routing code. Inject the code into the app.routes.ts file to
enable routing across the entire application.

As you can see, you created two paths in the definition to activate each of these components separately
with the application.

Note: You must import each component for which you provide a route.
7. Make a small change to the app-component.html file to enable the routing logic to handle the
page displays rather than hard-coding components into the application.

The original code has been commented out of the original code for the trip-listing component and
replaced with Angular’s router-outlet tag. The router-outlet tag allows the routing configuration to
control when to display the components in the application. Examining the resulting rendered page
shows that the routing is working.
8. Now that you have routing correctly configured, work on the add-trip component. Edit the add-
trip.component.ts file. The contents for this file should look like the following code:

import { Component, OnInit } from '@angular/core';


import { CommonModule } from '@angular/common';
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { Router } from "@angular/router";
import { TripDataService } from '../services/trip-data.service';

@Component({
selector: ‘app-add-trip’,
standalone: true,
imports: [CommonModule],
templateUrl: './add-trip.component.html',
styleUrl: './add-trip.component.css'
})

export class AddTripComponent implements OnInit {


addForm!: FormGroup;
submitted = false;

constructor(
private formBuilder: FormBuilder,
private router: Router,
private tripService: TripDataService
){}

ngOnInit() {
this.addForm = this.formBuilder.group({
_id: [],
code: [“, Validators.required],
name: [“, Validators.required],
length: [“, Validators.required],
start: [“, Validators.required],
resort: [“, Validators.required],
perPerson: [“, Validators.required],
image: [“, Validators.required],
description: [“, Validators.required],
})
}

public onSubmit() {
this.submitted = true;
if(this.addForm.valid){
this.tripService.addTrip(this.addForm.value)
.subscribe( {
next: (data: any) => {
console.log(data);
this.router.navigate(['']);
},
error: (error: any) => {
console.log('Error: ' + error);
}});
}
}
// get the form short name to access the form fields
get f() { return this.addForm.controls; }
}

This code initializes routing and sets up both the form and the appropriate data structure (the
formBuilder.group) that shows how you will access data that the user enters into the form. The
@Angular/forms module handles the data binding between the form and the variables.

The onSubmit() method is declared public so it can be called on the button press. This method uses the
TripDataService to pass the data back to the Express application. You can see the similarity in how the
subscription to the TripDataService method is handled. You use the same methodology in the trip-listing
component when you get the data from the service. You should note that at this time, you have an error
because the addTrip method does not exist in TripDataService.
9. Edit the services/trip-data.service.ts file to add a method for addTrip. Take this opportunity to
refactor the URL variable out of the getTrips method because both the getTrips and the addTrip
methods use this variable.

This edit will satisfy the binding for the add-trip component. However, the edit will not get you
anywhere because it is necessary to add the HTML code to generate the form and build the back-end
API method to accept the posted form data.

10. Now that you have the add-trip component logic built, you must create the form that will allow
the user to enter the data that needs to be collected. This form belongs in the add-
trip.component.html file and is straightforward.

Note: The control names for each form field must match exactly the individual field names from the
formGroup defined in your add-trip.component.ts file. This file should contain the following code:

<div class="col-md-4">
<h2 class="text-center">Add Trip</h2>
<form *ngIf="addForm" [formGroup]="addForm" (ngSubmit)="onSubmit()">

<div class="form-group">
<label>Code:</label>
<input type="text" formControlName="code"
placeholder="Code" class="form-control"
[ngClass]="{ 'is-invalid': submitted && f['code'].errors }">
<div *ngIf="submitted && f['code'].errors">
<div *ngIf="f['code'].errors?.['required']">
Trip Code is required
</div>
</div>
</div>

<div class="form-group">
<label>Name:</label>
<input type="text" formControlName="name"
placeholder="Name" class="form-control"
[ngClass]="{ 'is-invalid': submitted && f['name'].errors }">
<div *ngIf="submitted && f['name'].errors">
<div *ngIf="f['name'].errors?.['required']">
Name is required
</div>
</div>
</div>

<div class="form-group">
<label>Length:</label>
<input type="text" formControlName="length"
placeholder="Name" class="form-control"
[ngClass]="{ 'is-invalid': submitted && f['length'].errors }">
<div *ngIf="submitted && f['length'].errors”>
<div *ngIf="f['length'].errors?.['required']">
Length is required
</div>
</div>
</div>

<div class="form-group">
<label>Start:</label>
<input type="date" formControlName="start"
placeholder="Start" class="form-control"
[ngClass]="{ 'is-invalid': submitted && f['start'].errors }">
<div *ngIf="submitted && f['start'].errors”>
<div *ngIf="f['start'].errors?.['required']">
Date is required
</div>
</div>
</div>

<div class="form-group">
<label>Resort:</label>
<input type="text" formControlName="resort"
placeholder="Resort" class="form-control"
[ngClass]="{ 'is-invalid': submitted && f['resort'].errors }">
<div *ngIf="submitted && f['resort'].errors”>
<div *ngIf="f['resort'].errors?.['required']">
Resort is required
</div>
</div>
</div>

<div class="form-group">
<label>Per Person:</label>
<input type="text" formControlName="perPerson"
placeholder="Perperson" class="form-control"
[ngClass]="{ 'is-invalid': submitted && f['perPerson'].errors }">
<div *ngIf="submitted && f['perPerson'].errors”>
<div *ngIf="f['perPerson'].errors?.['required']">
Per Person is required
</div>
</div>
</div>

<div class="form-group">
<label>Image Name:</label>
<input type="text" formControlName="image"
placeholder="Image" class="form-control"
[ngClass]="{ 'is-invalid': submitted && f['image'].errors }">
<div *ngIf="submitted && f['image'].errors”>
<div *ngIf="f['image'].errors?.['required']">
Image Name is required
</div>
</div>
</div>

<div class="form-group">
<label>Description:</label>
<input type="text" formControlName="description"
placeholder="Description" class="form-control"
[ngClass]="{ 'is-invalid': submitted && f['description'].errors }">
<div *ngIf="submitted && f['description'].errors">
<div *ngIf="f['description'].errors?.['required']">
Description is required
</div>
</div>
</div>

<button type="submit" class="btn btn-info">Save</button>


</form>
</div>
When you save this file, notice one more item that you must address. There is an error in the Angular
build.
If you double-check the add-trip.component.ts file, you will find that you have the appropriate
definitions for addFrom. You have imported FormGroup, but it still is not working. You must make two
more changes. Make addForm a public variable and add the ReactiveFormsModule to the imports for
this component.

Saving the file will result in a clean compile and no errors.

However, selecting the Add Trip button and completing the form will generate a 404 error because the
backend does not know how to treat the post command.
11. Go back to the backend. Edit the app_api/routes/index.js file to add a post option to the
endpoint and designate a new method you must create in the tripsController.
12. Modify the tripsController file to handle the data that is passed back within the request body.
Add your new method to the modules.exports list.
13. Start by testing the endpoint with Postman. Set the query type to Post and the API endpoint to
http://localhost:3000/api/trips. Select the Body tab, and then select x-www-form-urlencoded
as the data type. Add the appropriate key-value pairs for each field of the record you will add.

Once you have verified that you have added each field and that they are spelled correctly, you can press
the Send button to bounce your query against the back-end API endpoint. If everything is successful, the
call will return a JSON object with the record that you just entered into the MongoDB. You will see the
result of this in the PowerShell window where you are running your Express server.
You should also be able to look at your front end and see that there is now a fourth record available.

14. Now that the test works directly with the backend, test to make certain that the backend also
works when the request is submitted from the AngularJS front-end application. You have two
choices. You can remove the record you just added from the Mongo database or add a duplicate
record. Go through the process of removing the record from Mongo so that you can definitively
determine the test’s success with the Angular application.

In MongoDB Compass or DBeaver, find the record that you just added via Postman and remove it from
the database.
Click Delete to confirm the removal of the record.

15. Now that you only have three records in the database, go back to the Angular application and
test the data entry form. From the Angular application, press the Add Trip button.
Fill in the form and press the Save button. This action will submit your record to the Mongo database
through the back-end Express API. You should see results that match what you saw when you used
Postman to test the API.

You can see in the inspector that four trips are now available instead of three. If you scroll down, you
can see the trip displayed in your application.

Edit Trips
Now that you have added the code and controls to add a trip to the application, adding the code to edit
or update an existing trip should be straightforward. Adding the code should help pull together all the
concepts that you have learned so far with an exercise that rounds out the initial capabilities for the
application.

1. The action of updating a record in the database uses a new HTTP verb: PUT. You used GET to
perform the read operation from the database. You used POST to add a record to the database.
Now, use PUT to update a record in the database. Start this process by creating a new method in
the app_api/controllers/trips.js file. The method will be like the one you created for adding a
new trip.

To the file, you will add the following code:

// PUT: /trips/:tripCode - Adds a new Trip


// Regardless of outcome, response must include HTML status code
// and JSON message to the requesting client
const tripsUpdateTrip = async(req, res) => {

// Uncomment for debugging


console.log(req.params);
console.log(req.body);

const q = await Model


.findOneAndUpdate(
{‘code’: req.params.tripCode },
{
code: req.body.code,
name: req.body.name,
length: req.body.length,
start: req.body.start,
resort: req.body.resort,
perPerson: req.body.perPerson,
image: req.body.image,
description: req.body.description
}
)
.exec();

if(!q)
{ // Database returned no data
return res
.status(400)
.json(err);
} else { // Return resulting updated trip
return res
.status(201)
.json(q);
}

// Uncomment the following line to show results of operation


// on the console
// console.log(q);
};

The biggest difference between the update and add methods is that you use the findOneAndUpdate
method from Mongoose. This method directs the database to locate the specified record, which you
select with a parameter for the API endpoint, and update the document based on the fields that are
passed in. In this case, you are passing in a complete document. But you could also pass in only the field
that needs to be updated.
Add the new method to the module.exports structure.

2. Now that you have constructed the new method, add a route so that the Express application can
find the method to call when it receives a PUT request. You must update the
app_api/routes/index.js file to complete this step.
3. That update will add a route for the Express backend for the HTTP PUT verb. However, the
application will not work yet. When you enabled CORS in the application to support the APIs,
you did not specify the HTTP configuration you would use. As such, CORS only enables GET and
POST by default. You must edit the app.js file and add one more configuration line.

To enable access to the additional HTTP verbs, add the following line to the definition of the /api
endpoint:

res.header(‘Access-Control-Allow-Methods’, ‘GET, POST, PUT, DELETE’);

Once you have completed this step, restart your Express backend server process.
4. Before you enable this for the Angular front end, test with Postman. Make a small change to the
query that you previously sent. Add the tripcode parameter to the end of the URL and set the
value to the code you added in the previous section. Then change the value of one of the
variables in the body of the request.

Once you have prepared the query, press the Send button and test your API call. You should get a 201
Created message at the bottom of the window. If you expand that section, you will see the original
record that you just updated.
Go back to Mongo Compass or DBeaver and view the record in the Mongo database to verify the
update.

You can see that the update was successful, and the API endpoint is functional.
5. Now that you have built the backend for the update functionality, adjust the Angular application
to provide a front-end interface to support updates. You will start by adding two methods to the
TripDataService. The first method is to grab a single trip record, and the second is to update a
single trip record.

Note: For both of these methods, the URL was extended to support the addition of the parameter for
the tripCode that will specify an individual record.
6. For the front end, you need another component to manage the editing process. Create a new
component called edit-trip by entering the following command:

ng generate component edit-trip

7. The HTML for the form to edit a record is nearly identical to the form for adding a record. Copy
the contents of add-trip.component.html into edit-trip.component.html and replace the page
title and the name of the form.
8. Now, add an Edit button to the bottom of the trip-card so that it renders the edit option with
each card presented.

When the Edit Trip button is pressed, the editTrip method will be called with a parameter of the trip
displayed in the current trip-card.
9. Now that you have edited the display side of the project, add an editTrip method to the
TripCard component.

You will notice that you added two items to the import section: Router and Trip. Adding Router was
necessary so that the method could get Angular to route the component. You added Trip so that you can
easily access the data fields in the editTrip method.
You also modified the constructor to bind the Router object that you imported. In the editTrip method,
use local storage in the browser to set the tripCode variable so that you can use it later. Once you save
this file, the browser should update, and you should see the new Edit Trip buttons.

10. Even though the buttons are now showing, they will not activate the edit-trip component yet.
You must write the logic into the edit-trip component. The component must grab the existing
data record and present it to update. To do this step, you must edit several sections in the edit-
trip component:

a. Add sections to the imports. These sections are the same as in the add-trip component
and can be copied from there.

import { Component, OnInit } from '@angular/core';


import { CommonModule } from '@angular/common';
import { Router } from '@angular/router';
import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from "@angular/forms";
import { TripDataService } from '../services/trip-data.service';
import { Trip } from '../models/trip';

b. Adjust the imports statement so that you pull in the ReactiveFormsModule to wire up
the form.

@Component({
selector: ‘app-edit-trip’,
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
templateUrl: './edit-trip.component.html',
styleUrl: './edit-trip.component.css'
})

c. Add local variables. You need these variables to be able to manipulate the data.

public editForm!: FormGroup;


trip!: Trip;
submitted = false;
message : string = '';

d. Build the constructor. The constructor enables you to build the form and route the
component. The constructor also allows you to pull data from the TripDataService.

constructor(
private formBuilder: FormBuilder,
private router: Router,
private tripDataService: TripDataService
) {}

e. Implement OnInit because the component does some heavy lifting when it is
instantiated.

export class EditTripComponent implements OnInit {

f. Populate the ngOnInit method. This step is where the component does the heavy lifting.
Grab the previously stashed tripCode and do a lookup on the existing record. Use the
lookup to populate the form and set up the editing process.

ngOnInit() : void{

// Retrieve stashed trip ID


let tripCode = localStorage.getItem("tripCode");
if (!tripCode) {
alert("Something wrong, couldn’t find where I stashed tripCode!");
this.router.navigate(['']);
return;
}

console.log('EditTripComponent::ngOnInit');
console.log('tripcode:' + tripCode);

this.editForm = this.formBuilder.group({
_id: [],
code: [tripCode, Validators.required],
name: [“, Validators.required],
length: [“, Validators.required],
start: [“, Validators.required],
resort: [“, Validators.required],
perPerson: [“, Validators.required],
image: [“, Validators.required],
description: ['', Validators.required]
})

this.tripDataService.getTrip(tripCode)
.subscribe({
next: (value: any) => {
this.trip = value;
// Populate our record into the form
this.editForm.patchValue(value[0]);
if(!value)
{
this.message = 'No Trip Retrieved!';
}
else{
this.message = 'Trip: ' + tripCode + ' retrieved';
}
console.log(this.message);
},
error: (error: any) => {
console.log('Error: ' + error);
}
})
}

g. Create the onSubmit method. This method will be executed when you commit the edit.
This method drives the communication to the backend and routes the component to the
main screen.

public onSubmit()
{
this.submitted = true;

if(this.editForm.valid)
{
this.tripDataService.updateTrip(this.editForm.value)
.subscribe({
next: (value: any) => {
console.log(value);
this.router.navigate(['']);
},
error: (error: any) => {
console.log('Error: ' + error);
}
})
}
}

h. Add a quick-access method to get at the form fields. This method will be identical to the
one you built in the add-trip component.

// get the form short name to access the form fields


get f() { return this.editForm.controls; }

11. To activate this component, add it to the router by editing the app.routes.ts file and adding the
component route.
12. Go back to the application and see what you have. Select one of the trips to edit and see if
everything populates correctly.

You will notice an error in the console. The value that you receive from the database is in a different
format than the simple date widget you selected for the panel. Thinking about how you would address
the difference between the data and the form is useful. As a challenging activity, modify the code for the
edit-trip component to update the date picker correctly from the record retrieved from the database.

13. In the meantime, select a date as part of your editing process and make a small change to any
other field except for the trip code. Make your selections and press Save.

Having selected the Save option, can you see your change reflected in the card?
You can see the edit that you made reflected on the page. If you close your browser and reconnect, you
can see that the change is persistent and has been saved in the Mongo database. You can also see the
changes in Mongo Compass or DBeaver.
Finalizing Module Six

1. Now that you have completed Module Six, go back to Git and add everything to tracking. Start
by checking the status of what has changed by using the git status command.

2. Add all changes into tracking using the git add . command.
3. Commit the changes using git commit -m ‘Module 6 completed baseline’.
4. Push the changes back to GitHub for safekeeping using git push --set-upstream origin module6.

You might also like