Introduction to Angular

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:

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.

Angular CLI output displaying the angular version

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

  1. Open a new browser tab and go to https://github.com/angular/introduction-to-angular.
  2. From a command-line window fork and clone the repository and cd introduction-to-angular/ into the repository.
  3. From the starter code branch, enter git checkout get-started.
  4. Open the code in your preferred code editor, and open the introduction-to-angular project folder.
  5. From the command-line window, run npm install to install the dependencies required to run the server.
  6. To run the Angular web server in the background, open a separate command-line window and run ng serve to start the server.
  7. 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.

  1. Open app.component.html in your code editor; this is the template file for the AppComponent.
  2. 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>
  1. 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.

  1. 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.
  1. Next, add the new component to the AppComponent template. In app.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>
  1. Save all files and return to the browser to confirm that the message housing-list works is displayed.
  2. 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>
  1. 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;
}

  1. Save the files, then return to the browser. App now has a search box and button

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.

  1. Update the button code to match this code:
<button (click)="searchHousingLocations()">Search</button>
  1. Save this code and check the browser. There is now a compilation error:

46a528b5ddbc7ef8.png

The app throws this error because the searchHousingLocations method does not exist, so you'll need to change that.

  1. In housing-list.component.ts, add a new method at the end of the body of the HousingListComponent class:
 searchHousingLocations() {}

You'll fill in the details for this method shortly.

  1. 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.

  1. In housing-list.component.html, add an attribute called search, 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.

  1. 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.

  1. In housing-list.component.ts, add this code:
 searchHousingLocations(searchText: string) {
   console.log(searchText);
 }
  1. 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.

chrome devtools console output matching search text from UI

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.

  1. To do this, enter:
ng generate interface housing-location
  1. 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,
}
  1. Save the file and open app.component.ts.
  2. 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';
  1. Update the AppComponent class to include a property called housingLocationList of type HousingLocation[]. 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.

  1. In housing-list.component.ts, import input from @angular/core as well as HousingLocation from ./housingLocation.
import { Component, OnInit, Input } from '@angular/core';
import {HousingLocation } from '../housing-location';
  1. Create a property called locationList in the body of the component class. You're going to use Input as a decorator for locationList.
export class HousingListComponent implements OnInit {

  @Input() locationList: HousingLocation[] = [];
  ...
}

The type of this property is set to HousingLocation[].

  1. In app.component.html, update the app-housing-list element to include an attribute called locationList and set the value to housingLocationList.
<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.

  1. 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.

  1. 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.

  1. Save and return to the browser. Now, the app will display one label for each array entry in the locationList array.

listing of 3 housing locations displayed

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.

  1. In housing-list.component.ts update the HousingListComponent to have a new property called results that is of type HousingLocation[]:
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.

  1. Remove the console.log and update the code to assign the results property to the output of filtering the locationList, filtered by searchText:
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 using this.results and this.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.

  1. 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.

  1. In housing-location.component.html, replace locationList with results in the ngFor:
<article *ngFor="let location of results">...</article>
  1. 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:

search results matching the text entered into the search field

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.

  1. In housing-list.component.ts, update the import to include Output and EventEmitter from @angular/core and HousingLocation from its location:
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { HousingLocation } from '../housing-location';
  1. In the body of HousingListComponent, update the code to add a new property called locationSelectedEvent of type EventEmitter<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.

  1. Update HousingListComponent to add a new method called selectLocation that accepts a value of type housingLocation as a parameter:
selectHousingLocation(location: HousingLocation) { }
  1. 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.

  1. In housing-list-component.html update the article element to have a new button child with a click event. This event calls the selectHousingLocation method in the TypeScript class and passes a reference to the clicked location 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.

  1. In app.component.html, update the app-housing-list element to listen to the locationSelectedEvent and to handle the event with the updateSelectedLocation 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.

  1. In app.component.ts, update the code to include a new property called selectedLocation of type HousingLocation | 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.

  1. Add a new method called updateSelection with a parameter called location and with a type of HousingLocation.
updateSelectedLocation(location: HousingLocation) { } searchHousingLocations() {}
  1. In the body of the method, set the value of selectedLocation to be the location parameter:
updateSelectedLocation(location: HousingLocation) {
  this.selectedLocation = location;
}

With this part complete, the last step is to update the template to display the selected location.

  1. 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.

  1. Save the code and check the browser. Search for a location and click on one to reveal the details:

two column layout; left side displaying search results, right side displaying selected location 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.

default UI with artifacts displaying on the screen incorrectly

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.

  1. In app.component.html, update the article 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>
  1. Next, add the *ngIf attribute to the ng-container element. The value should be selectedLocation.
<article>
  <ng-container *ngIf="selectedLocation">
  ...
  </ng-container>
</article>

Now the app only displays the content of the ng-container element if selectedLocation is Truthy.

  1. 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.

  1. 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>
  1. Save the code and return to the browser to reveal the completed app.

two column application: left side displays search  results, right side displays details for selected search result

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.