1. Introduction
What you'll build
In this codelab, you'll build a housing app with Angular. The completed app will feature the ability to view home listings based on user search, and view details of a housing location.
You'll build everything with Angular using Angular's powerful tooling and great browser integration.
This is the app you'll be building today
What you'll learn
- How to use the Angular CLI to scaffold a new project.
- How to use Angular components to build a user interface.
- How to share data in components and other parts of an app.
- How to use event handlers in Angular.
- How to deploy the app to Firebase Hosting using the Angular CLI.
What you'll need
- Basic knowledge of HTML, CSS, TypeScript (or JavaScript), Git, and the command line.
2. Environment setup
Set up your local environment
To complete this codelab, you need the following software installed on your local machine:
- Node version ^12.20.2, ^14.15.5 , or ^16.10.0.
- Code Editor - VS Code or another code editor of your choice.
- Angular Language Service Plugin for VS Code.
Install the Angular CLI
Once all of your dependencies are configured, you can install the Angular CLI from a command-line window on your computer:
npm install -g @angular/cli
To confirm that your configuration is correct, run this command from your computer's command line:
ng –version
If the command works successfully, you'll find a message similar to the screenshot below.
Get the code
The code for this codelab contains the intermediate steps and the final solution in different branches. To get started, download the code from GitHub
- Open a new browser tab and go to
https://github.com/angular/introduction-to-angular
. - From a command-line window fork and clone the repository and
cd introduction-to-angular/
into the repository. - From the starter code branch, enter
git checkout get-started
. - Open the code in your preferred code editor, and open the
introduction-to-angular
project folder. - From the command-line window, run
npm install
to install the dependencies required to run the server. - To run the Angular web server in the background, open a separate command-line window and run
ng serve
to start the server. - Open a browser tab to
http://localhost:4200
.
With the app up and running, you can start building the Fairhouse app.
3. Create your first component
Components are the core building blocks for Angular apps. Think of components as bricks used for construction. When starting out, a brick doesn't have much power, but when combined with other bricks you can build amazing structures.
The same goes for apps built with Angular.
Components have 3 main aspects:
- An HTML file for the template.
- A CSSfile for the styles.
- A TypeScript file for the behavior of the app.
The first component you're going to update is AppComponent
.
- Open
app.component.html
in your code editor; this is the template file for theAppComponent
. - Delete all the code in this file and replace it with this:
<main>
<header><img src="../assets/logo.svg" alt="fairhouse">Fairhouse</header>
<section>
</section>
</main>
- Save the code and check the browser. With the development server running, the changes are reflected in the browser when we save.
Congratulations, you successfully updated your first Angular app. Look for more great things ahead. Let's continue.
Next, you'll add a text field for search, and a button to the UI.
Components have many benefits, with one being the ability to organize the UI. You're going to create a component that contains the text field, the button for search, and eventually the list of locations.
To create this new component, you'll use the Angular CLI. The Angular CLI is the set of command-line tools that help with scaffolding, deployment, and more.
- From the command line, enter:
ng generate component housing-list
Here are the parts of this command:
ng
is the Angular CLI.- The command generates the type of action to perform. In this case, generate scaffolding for something.
- The component represents the "what" we want to create.
- housing-list is the name of the component.
- Next, add the new component to the
AppComponent
template. Inapp.component.html
, update the template code:
<main>
<header><img src="../assets/logo.svg" alt="fairhouse">Fairhouse</header>
<section>
<app-housing-list></app-housing-list>
</section>
</main>
- Save all files and return to the browser to confirm that the message
housing-list works
is displayed. - In your code editor, navigate to
housing-list.component.html
, remove the existing code, and replace it with:
<label for="location-search">Search for a new place</label>
<input id="location-search" placeholder="Ex: Chicago"><button>Search</button>
- In
housing-list.component.css
, add the following styles:
input, button {
border: solid 1px #555B6E;
border-radius: 2px;
display: inline-block;
padding: 0;
}
input {
width: 400px;
height: 40px;
border-radius: 2px 0 0 2px;
color: #888c9c;
border: solid 1px #888c9c;
padding: 0 5px 0 10px;
}
button {
width: 70px;
height: 42px;
background-color: #4468e8;
color: white;
border: solid 1px #4468e8;
border-radius: 0 2px 2px 0;
}
article {
margin: 40px 0 10px 0;
color: #202845;
}
article, article > p {
color: #202845;
}
article> p:first-of-type {
font-weight: bold;
font-size: 22pt;
}
img {
width: 490px;
border-radius: 5pt;
}
label {
display: block;
color: #888c9c;
}
- Save the files, then return to the browser.
Our Angular app is starting to take shape.
4. Event Handling
The app has an input field and button but it is missing the interaction. On the web, you typically interact with controls and invoke the use of events and event handlers. You'll use this strategy to build your app.
You'll make these changes in housing-list.component.html
.
To add a click handler, you'll need to add the event listener to the button. In Angular, the syntax is to surround the name of the event in parentheses and assign it a value. Here, you name the method that is called when the button is clicked. Let's call it searchHousingLocations
. Don't forget to add the parentheses to the end of this function name to call it.
- Update the button code to match this code:
<button (click)="searchHousingLocations()">Search</button>
- Save this code and check the browser. There is now a compilation error:
The app throws this error because the searchHousingLocations
method does not exist, so you'll need to change that.
- In
housing-list.component.ts
, add a new method at the end of the body of theHousingListComponent
class:
searchHousingLocations() {}
You'll fill in the details for this method shortly.
- Save this code to update the browser and resolve the error.
Our next step is to get the value of the input field and pass it as an argument to the searchHousingLocations
method. You'll use an Angular feature called a template variable, which provides a way to get a reference to an element in a template and interact with it.
- In
housing-list.component.html
, add an attribute calledsearch
, with a hashtag as a prefix to the input.
<label for="location-search">Search for a new place</label>
<input id="location-search" #search><button (click)="searchHousingLocations()">Search</button>
Now, we have a reference to the input. We have access to the .value
property of the input as well.
- Pass the value of the input to the
searchHousingLocations
method,
<input id="location-search" #search><button (click)="searchHousingLocations(search.value)">Search</button>
Until now, you've been passing the value as a parameter, but let's update the method to use the parameter. Right now, the parameter is used in a console.log
command; later, it's used as a search parameter.
- In
housing-list.component.ts
, add this code:
searchHousingLocations(searchText: string) {
console.log(searchText);
}
- Save the code and then, in the browser, open Chrome DevTools and navigate to the Console tab. Enter any value into the input. Choose Search and verify that the value displays in the Console tab of Chrome DevTools.
You've successfully added an event handler and your app can take input from users.
5. Search results
The next step is to display results based on the user input. Each location has string properties for name, city, state, photo, a number property for availableUnits, and two boolean properties for laundry and wifi:
name: "Location One",
city: "Chicago",
state: "IL",
photo: "/path/to/photo.jpg",
availableUnits: 4,
wifi: true,
laundry: true
You can represent this data as a plain JavaScript object, but it's better to use the TypeScript support in Angular. Use types to help avoid errors during build time.
We can use types to define the characteristics of the data, also known as "shaping the data." In TypeScript, interfaces are commonly used for this purpose. Let's create an interface representing our housing location data. In the editor's terminal, use the Angular CLI to create a HousingLocation type.
- To do this, enter:
ng generate interface housing-location
- In
housing-location.ts
, add the type details for our interface. Give each property the appropriate type based on our design:
export interface HousingLocation {
name: string,
city: string,
state: string,
photo: string,
availableUnits: number,
wifi: boolean,
laundry: boolean,
}
- Save the file and open
app.component.ts
. - To create an array containing data that represents the housing locations by importing the housing location interface from
./housing-location
.
import { HousingLocation } from './housing-location';
- Update the
AppComponent
class to include a property calledhousingLocationList
of typeHousingLocation[]
. Populate the array with the following values:
housingLocationList: HousingLocation[] = [
{
name: "Acme Fresh Start Housing",
city: "Chicago",
state: "IL",
photo: "../assets/housing-1.jpg",
availableUnits: 4,
wifi: true,
laundry: true,
},
{
name: "A113 Transitional Housing",
city: "Santa Monica",
state: "CA",
photo: "../assets/housing-2.jpg",
availableUnits: 0,
wifi: false,
laundry: true,
},
{
name: "Warm Beds Housing Support",
city: "Juneau",
state: "AK",
photo: "../assets/housing-3.jpg",
availableUnits: 1,
wifi: false,
laundry: false,
}
];
You don't have to instantiate new instances of a class to get objects; we can take advantage of the type information provided by the interface. The data in our objects has to be the same "shape"; that is, it has to match the properties defined on the interface.
The data is stored in app.component.ts
but we need to share it with other components. One solution is to use services in Angular but to reduce the complexity of the app, we'll use the Input decorator provided by Angular. The input decorator allows a component to receive a value from a template. You'll use it to share the housingLocationList
array with the HousingListComponent
.
- In
housing-list.component.ts
, importinput
from@angular/core
as well asHousingLocation
from./housingLocation
.
import { Component, OnInit, Input } from '@angular/core';
import {HousingLocation } from '../housing-location';
- Create a property called
locationList
in the body of the component class. You're going to useInput
as a decorator forlocationList
.
export class HousingListComponent implements OnInit {
@Input() locationList: HousingLocation[] = [];
...
}
The type of this property is set to HousingLocation[]
.
- In
app.component.html
, update theapp-housing-list
element to include an attribute calledlocationList
and set the value tohousingLocationList
.
<main>
...
<app-housing-list [locationList]="housingLocationList"></app-housing-list>
</main>
The locationList
attribute must be enclosed in square brackets ( [ ]
) so that Angular can dynamically bind the value of the locationList property to a variable or expression. Otherwise, Angular treats the value on the right-hand side of the equals sign as a string.
If you are getting any errors at this point, check that:
- The input attribute name spelling matches the spelling in the property in the TypeScript class. Case matters here, as well.
- The property name on the right-hand side of the equal sign is spelled correctly.
- The input property is enclosed in square brackets.
The data-sharing configuration is complete! The next step is to display the results in the browser. Since the data is in array format, we need to use an Angular feature that lets you loop over data and repeat of elements in templates, *ngFor
.
- In
housing-list.component.html
, update the article element in the template to use*ngFor
so that you can display the array entries in the browser:
<article *ngFor="let location of locationList"></article>
The value assigned to the ngFor
attribute is Angular template syntax. It creates a local variable in the template. Angular uses the local variable in the scope of the article
element between the open and closing tags.
To learn more about ngFor and template syntax, refer to the Angular Documentation.
The ngFor
repeats an article element for each entry of the locationList
array. Next, you'll display values from the location variable.
- Update the template to add a paragraph element(
<p>
) element. The child of the paragraph element is an interpolated value from the location property:
<input #search><button (click)="searchHousingLocations(search.value)">Search</button>
<article *ngFor="let location of locationList">
<p>{{location.name}}</p>
</article>
In Angular templates, you can use text interpolation to display values with the double curly bracket ( {{ }}
) syntax.
- Save and return to the browser. Now, the app will display one label for each array entry in the
locationList
array.
The data is shared from the app component to the housing list component, and we're iterating over each of those values to display them in the browser.
We've just covered some ways to share data between components, used some new template syntax and the ngFor directive.
6. Filter search results
Currently, the app displays all results instead of results based on a user's search. To change that, you need to update the HousingListComponent
so that the app can function as intended.
- In
housing-list.component.ts
update theHousingListComponent
to have a new property calledresults
that is of typeHousingLocation[]
:
export class HousingListComponent implements OnInit {
@Input() locationList: HousingLocation[] = [];
results: HousingLocation[] = [];
...
The results array represents the housing locations that match the user search. The next step is to update the searchHousingLocations
method to filter the values.
- Remove the
console.log
and update the code to assign the results property to the output of filtering thelocationList
, filtered bysearchText
:
searchHousingLocations(searchText: string) {
this.results = this.locationList.filter(
(location: HousingLocation) => location.city
.toLowerCase()
.includes(
searchText.toLowerCase()
));
}
In this code, we're using the array filter method and only accepting values that contain the searchText
. All of the values are compared using the lowercase versions of the strings.
Two things to note:
- The
this
prefix must be used when referencing properties of a class inside methods. That's why we're usingthis.results
andthis.locationList
. - The search function here only matches against the city property of a location, but you can update the code to include more properties.
Although this code works as is, you can improve it.
- Update the code to prevent searching through the array if
searchText
is blank:
searchHousingLocations(searchText: string) {
if (!searchText) return;
...
}
The method has been updated and there's a template change that you need to make before the results are displayed in the browser.
- In
housing-location.component.html
, replacelocationList
withresults
in thengFor
:
<article *ngFor="let location of results">...</article>
- Save the code and return to the browser. Using the input, search for a location from the sample data (for example, Chicago).
The app displays only the matching results:
You've just completed the additional functionality required to fully link the user input to the search results. The app is nearly complete.
Next, you'll display more details about the app to finish it.
7. Display the details
The app needs to support clicking on a search result and displaying the information in a details panel. The HousingListComponent
knows which result has been clicked since the data is displayed in that component. We need a way to share the data from the HousingListComponent
to the parent component AppComponent
.
In Angular, @Input()
sends data from parent to child, while @Output()
lets components send an event with data from the child to their parent component. The Output decorator uses an EventEmitter
to notify any listeners of any events. In this case, you want to emit an event representing the click of a search result. Along with the selection event, you want to send the selected item as a part of the payload.
- In
housing-list.component.ts
, update theimport
to includeOutput
andEventEmitter
from@angular/core
andHousingLocation
from its location:
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { HousingLocation } from '../housing-location';
- In the body of
HousingListComponent
, update the code to add a new property calledlocationSelectedEvent
of typeEventEmitter<HousingLocation>();
:
@Output() locationSelectedEvent = new EventEmitter<HousingLocation>();
The locationSelectedEvent
property is decorated with @Output()
, which makes this a part of the API of this component. With EventEmitter
, you take advantage of the generics API for the class by providing it the type HousingLocation
. When an event is emitted by the locationSelectedEvent
, listeners to the event can expect any accompanying data to be of type HousingLocation
. This is the type safety supporting our development and reducing the potential for some errors.
We need to trigger the locationSelectedEvent
whenever a user clicks on a location from the list.
- Update
HousingListComponent
to add a new method calledselectLocation
that accepts a value of typehousingLocation
as a parameter:
selectHousingLocation(location: HousingLocation) { }
- In the body of the method, emit a new event from the
locationSelectedEvent
emitter. The value that is emitted is the location selected by the user.
selectHousingLocation(location: HousingLocation) {
this.locationSelectedEvent.emit(location);
}
Let's link this to the template.
- In
housing-list-component.html
update the article element to have a new button child with aclick event
. This event calls theselectHousingLocation
method in the TypeScript class and passes a reference to the clickedlocation
as an argument.
<article *ngFor="let location of results" >
<p>{{location.name}}</p>
<button (click)="selectHousingLocation(location)">View</button>
</article>
The housing locations now have a clickable button and you're passing the values back into the component.
The final step in this process is to update the AppComponent
to listen to the event and update the display accordingly.
- In
app.component.html
, update theapp-housing-list
element to listen to thelocationSelectedEvent
and to handle the event with theupdateSelectedLocation
method:
<app-housing-list [locationList]="housingLocationList" (locationSelectedEvent)="updateSelectedLocation($event)"></app-housing-list>
The $event
is provided by Angular when dealing with event handlers in templates. The $event argument is an object of type HousingLocation
because that is what we set the EventEmitter
's type parameter to be. Angular handles all this for you. You just need to confirm that your templates are correct.
- In
app.component.ts
, update the code to include a new property calledselectedLocation
of typeHousingLocation | undefined
.
selectedLocation: HousingLocation | undefined;
This uses a TypeScript feature called a Union Type. Unions let variables accept one of multiple types. In this case, you want the value of selectedLocation
to be HousingLocation
or undefined
because you're not specifying a default value for selectedLocation
.
You need to implement updateSelectedLocation
.
- Add a new method called
updateSelection
with a parameter calledlocation
and with a type ofHousingLocation
.
updateSelectedLocation(location: HousingLocation) { } searchHousingLocations() {}
- In the body of the method, set the value of
selectedLocation
to be thelocation
parameter:
updateSelectedLocation(location: HousingLocation) {
this.selectedLocation = location;
}
With this part complete, the last step is to update the template to display the selected location.
- In
app.component.html
, add a new<article>
element that we'll use to display the properties of the selected location. Update the template with the following code:
<article>
<img [src]="selectedLocation?.photo">
<p>{{selectedLocation?.name}}</p>
<p>{{selectedLocation?.availableUnits}}</p>
<p>{{selectedLocation?.city}}, {{selectedLocation?.state}}</p>
<p>{{selectedLocation?.laundry ? "Has laundry" : "Does Not have laundry"}}</p>
<p>{{selectedLocation?.wifi ? "Has wifi" : "Does Not have wifi"}}</p>
</article>
Since the selectedLocation
can be undefined
, you use the optional chaining operator to retrieve the values from the property. You're also using ternary syntax for the wifi
and laundry
boolean values. This provides the opportunity to display a custom message, depending on the value.
- Save the code and check the browser. Search for a location and click on one to reveal the details:
This looks great, but there is still an issue to resolve. When the page loads initially, there are some text artifacts from the details panel that should not be displayed. Angular has some ways to conditionally display content that you'll use in the next step.
For now, be excited with how far the app has come. Here's what you've implemented so far:
- You can share data from the child components to the parent components using the Output decorator and the EventEmitter.
- You've also successfully allowed your users to enter a value and search using that value.
- The app can display the search results and users can click to see more details.
This is excellent work so far. Let's update the templates and complete the app.
8. Polish the templates
The UI currently contains text artifacts from the details panel that should be conditionally displayed. We're going to use two Angular features, ng-container
and *ngIf
.
If you apply the ngIf
directive to the article
element directly, it causes a layout shift when the user makes the first selection. To improve this experience, you can wrap the location details in another element that is a child of the article
. This element has no styling or function and just adds weight to the DOM. To avoid that, you can use ng-container
. You can apply directives to it, but it won't show up in the final DOM.
- In
app.component.html
, update thearticle
element to match this code:
<article>
<ng-container>
<img [src]="selectedLocation?.photo">
<p>{{selectedLocation?.name}}</p>
<p>{{selectedLocation?.city}}, {{selectedLocation?.state}}</p>
<p>Available Units: {{selectedLocation?.availableUnits}}</p>
<p>{{selectedLocation?.laundry ? "Has laundry" : "Does Not have laundry"}}</p>
<p>{{selectedLocation?.wifi ? "Has wifi" : "Does Not have wifi"}}</p>
</ng-container>
</article>
- Next, add the
*ngIf
attribute to theng-container
element. The value should beselectedLocation
.
<article>
<ng-container *ngIf="selectedLocation">
...
</ng-container>
</article>
Now the app only displays the content of the ng-container
element if selectedLocation
is Truthy.
- Save this code and confirm that the browser no longer displays the text artifacts when the page loads.
There's one final update we can make to our app. The search results in housing-location.component.html
should display more details.
- In
housing-location.component.html
, update the code to:
<label for="location-search">Search for a new place</label>
<input id="location-search" #search placeholder="Ex: Chicago"><button
(click)="searchHousingLocations(search.value)">Search</button>
<article *ngFor="let location of results" (click)="selectHousingLocation(location)">
<img [src]="location.photo" [alt]="location.name">
<p>{{location.name}}</p>
<p>{{location.city}}, {{location.state}}</p>
<button (click)="selectHousingLocation(location)">View</button>
</article>
- Save the code and return to the browser to reveal the completed app.
The app looks great now, and is fully functional. Well done.
9. Congratulations
Thanks for taking this journey and using Angular to build Fairhouse.
You created a user interface using Angular. Using the Angular CLI, you created components and interfaces. Then, you used the powerful template features in Angular to build a functional app that displays images, handles events and more.
What's next?
If you want to continue to build functionality, here are some ideas:
- The data is hard-coded in the app. A great refactor is to add a service to contain the data.
- The details page is currently displayed on the same page, but it would be cool to move the details to their own page and take advantage of Angular routing.
- One other update would be to host the data at a rest endpoint and use the HTTP package in Angular to load the data at runtime.
Plenty of opportunities for fun.