Creating Components page
Learn how to create basic components in Angular and iterate through data using an Angular for loop.
Overview
In this part, we will:
- Install already created assets
- Learn about components
- Learn about displaying data in component templates
- Create a home component
- Use interpolation binding in our home component view
- Learn about directives
- Create a restaurant component that shows a list of restaurants
Problem 1: Creating a Home Component With a Dynamic Title Member
Let’s begin to build out the main views of our app. We’ll create a home view now, and restaurant list view in the next exercise.
P1: What you need to know
- How to generate a new Angular component
- How to bind data in a component to its template
Generating components
In Angular, Components are the basic building blocks that help us craft the UI. They are classes that handle views, allow management of user interaction, and displaying information via data binding. Data binding is the term for connecting data or information to the UI. An example would be an input field that a user enters a value into.
Recommended reading: Angular Lifecyle Hooks
The best way to create a new component is by using the Angular CLI:
ng g component new-component-name
This will create a new component for us and import it in our root module.
├── src/
| ├── app/
| | ├── new-component-name/
| | |── new-component-name.component.ts
| | |── new-component-name.component.spec.ts
| | |── new-component-name.component.css
| | |── new-component-name.component.html
Generated components have the same structure - a name.component.ts file that will contain the boilerplate code for the Angular component class. This class will also have a component decorator pointing to the name.component.css file for styles, and the name.component.html file for it’s template. Styles and templates can also be written inline in the decorator with backticks to escape the code using the keys style
and template
.
Binding data to components
Like most modern JS frameworks, Angular provides us a way of displaying data dynamically in the DOM. Properties declared in a component can be used in the component template with double curly brace syntax: {{myTitle}}
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.2.1/rxjs.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/core-js/2.5.7/core.js"/></script>
<script src="https://unpkg.com/@angular/core@7.2.0/bundles/core.umd.js"/></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/zone.js/0.8.26/zone.min.js"></script>
<script src="https://unpkg.com/@angular/common@7.2.0/bundles/common.umd.js"></script>
<script src="https://unpkg.com/@angular/compiler@7.2.0/bundles/compiler.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser@7.2.0/bundles/platform-browser.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser-dynamic@7.2.0/bundles/platform-browser-dynamic.umd.js"></script>
<my-app></my-app>
<script type="typescript">
// app.js
const { Component, VERSION } = ng.core;
@Component({
selector: 'my-app',
template: `
<p>{{myTitle}}</p>
`
})
class AppComponent {
myTitle = 'Hello world.';
constructor() {
}
}
// main.js
const { BrowserModule } = ng.platformBrowser;
const { NgModule } = ng.core;
const { CommonModule } = ng.common;
@NgModule({
imports: [
BrowserModule,
CommonModule,
],
declarations: [AppComponent],
bootstrap: [AppComponent],
providers: []
})
class AppModule {}
const { platformBrowserDynamic } = ng.platformBrowserDynamic;
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err));
</script>
Passing data to child components
Data can also be passed to child components. Data can be passed with expression context to determine if passed data is just a string for example, or a property on the component class. This example shows passing data as a string as well as a component member using the [ ]
syntax.
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.2.1/rxjs.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/core-js/2.5.7/core.js"/></script>
<script src="https://unpkg.com/@angular/core@7.2.0/bundles/core.umd.js"/></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/zone.js/0.8.26/zone.min.js"></script>
<script src="https://unpkg.com/@angular/common@7.2.0/bundles/common.umd.js"></script>
<script src="https://unpkg.com/@angular/compiler@7.2.0/bundles/compiler.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser@7.2.0/bundles/platform-browser.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser-dynamic@7.2.0/bundles/platform-browser-dynamic.umd.js"></script>
<my-app></my-app>
<script type="typescript">
// app.js
const { Component, VERSION, Input } = ng.core;
@Component({
selector: 'child-component',
template: `
<p>{{childData}}</p>
<p>{{childObject | json}}</p>
`
})
class ChildComponent {
@Input() childData: string;
@Input() childObject: any;
constructor() {}
}
@Component({
selector: 'my-app',
template: `
<child-component childData="Hello string."></child-component>
<child-component [childData]="parentTitle"></child-component>
<child-component [childObject]="parentObject"></child-component>
`
})
class AppComponent {
parentTitle = 'Hello world.';
parentObject = {
name: 'hello there',
id: 5
}
constructor() {
}
}
// main.js
const { BrowserModule } = ng.platformBrowser;
const { NgModule } = ng.core;
const { CommonModule } = ng.common;
@NgModule({
imports: [
BrowserModule,
CommonModule,
],
declarations: [AppComponent, ChildComponent],
bootstrap: [AppComponent],
providers: []
})
class AppModule {}
const { platformBrowserDynamic } = ng.platformBrowserDynamic;
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err));
</script>
P1: Technical requirements
Create a component that displays a title read from a component’s title
member.
The component should provide the following HTML:
<div class="homepage">
<img
src="./assets/images/homepage-hero.jpg"
alt="Restaurant table with glasses."
width="250"
height="380"
/>
<h1><!-- TITLE GOES HERE --></h1>
<p>
We make it easier than ever to order gourmet food from your favorite local
restaurants.
</p>
<p>
<a class="btn" routerLink="/restaurants" role="button">
Choose a Restaurant
</a>
</p>
</div>
Notice the
TITLE GOES HERE
part of the HTML.TITLE GOES HERE
should be replaced by something that reads the component’stitle
property.
The component’s title
member should have a string value of
"Ordering food has never been easier"
.
P1: Setup
To get this application up and running quicker so we can focus on the architecture, we’ll import some pre-created styles and assets to save us time.
✏️ Run:
npm install place-my-order-assets@0
Open the angular.json
file, and make the following changes to include these files in our build process. This will copy the images into our assets directory for when we serve our application.
Pay close attention that you’re making these changes under the "build" key and not the "test" key, as the code looks very similar. The build key should be close to line 24.
section copied - angular.json
✏️ Update angular.json:{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"place-my-order": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"standalone": false
},
"@schematics/angular:directive": {
"standalone": false
},
"@schematics/angular:pipe": {
"standalone": false
}
},
"root": "",
"sourceRoot": "src",
"prefix": "pmo",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": "dist/place-my-order",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": [
"zone.js"
],
"tsConfig": "tsconfig.app.json",
"assets": [
"src/favicon.ico",
"src/assets",
{
"glob": "**/*",
"input": "./node_modules/place-my-order-assets/images/",
"output": "./assets/images"
}
],
"styles": [
"src/styles.css",
"./node_modules/place-my-order-assets/css/place-my-order-assets.css"
],
"scripts": []
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"production": {
"buildTarget": "place-my-order:build:production"
},
"development": {
"buildTarget": "place-my-order:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "place-my-order:build"
}
},
"test": {
"builder": "@angular-devkit/build-angular:karma",
"options": {
"polyfills": [
"zone.js",
"zone.js/testing"
],
"tsConfig": "tsconfig.spec.json",
"assets": [
"src/favicon.ico",
"src/assets"
],
"styles": [
"src/styles.css"
],
"scripts": []
}
}
}
}
},
"cli": {
"analytics": false
}
}
Any time changes are made to the angular.json
file, we need to restart our server to catch the new changes.
✏️ Quit process by running Ctrl + C
.
While we are building the component, it will be nice to see it in the
application. One of the ways components can be rendered is by putting them in markup. We’ll do this by putting our <pmo-home></pmo-home>
tag in our base app component markup. To see the
<pmo-home>
component, do the following.
✏️ Run:
ng g component home
✏️ Update src/app/app.component.html to be:
<h1>Place My Order App: Coming Soon!</h1>
<router-outlet />
<pmo-home></pmo-home>
✏️ Update src/app/home/home.component.html to be:
<div class="homepage">
<img
src="./assets/images/homepage-hero.jpg"
alt="Restaurant table with glasses."
width="250"
height="380"
/>
<h1><!-- TITLE GOES HERE --></h1>
<p>
We make it easier than ever to order gourmet food from your favorite local
restaurants.
</p>
<p>
<a class="btn" routerLink="/restaurants" role="button">
Choose a Restaurant
</a>
</p>
</div>
Run npm run start
, and your app should compile with no errors, and you’ll be able to see the home component. Later we’ll move the home component to its own page with a unique route.
Having issues with your local setup?
You can get through most of this tutorial by using an online code editor. You won’t be able to run our tests to verify your solution, but you will be able to make changes to your app and see them live.
You can use one of these two online editors:
P1: How to verify your solution is correct
✏️ Update the spec file src/app/home/home.component.spec.ts to be:
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HomeComponent } from './home.component';
describe('HomeComponent', () => {
let component: HomeComponent;
let fixture: ComponentFixture<HomeComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [HomeComponent]
})
.compileComponents();
fixture = TestBed.createComponent(HomeComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should render title in a h1 tag', () => {
const fixture = TestBed.createComponent(HomeComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain(
'Ordering food has never been easier'
);
});
it('should have a member "title" on the component', () => {
const fixture = TestBed.createComponent(HomeComponent);
fixture.detectChanges();
expect((fixture.componentInstance as any).title).toContain(
'Ordering food has never been easier'
);
});
});
P1: Solution
If you’ve implemented the solution correctly, the tests will pass when you run npm run test
!
Click to see the solution
✏️ Update src/app/home/home.component.html<div class="homepage">
<img
src="./assets/images/homepage-hero.jpg"
alt="Restaurant table with glasses."
width="250"
height="380"
/>
<h1>{{ title }}</h1>
<p>
We make it easier than ever to order gourmet food from your favorite local
restaurants.
</p>
<p>
<a class="btn" routerLink="/restaurants" role="button">
Choose a Restaurant
</a>
</p>
</div>
✏️ Update src/app/home/home.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'pmo-home',
templateUrl: './home.component.html',
styleUrl: './home.component.css'
})
export class HomeComponent {
title = 'Ordering food has never been easier';
}
Problem 2: Write Restaurant Component Markup that Displays a List of Restaurants
We want to display a list of restaurants in our UI once the data has been set on the restaurants member. It will look like:
Note: We will fix the missing images in the next step.
P2: What you need to know
To solve this exercise, you’ll need to learn some of Angular’s more common template directives. Template directives in Angular help us iterate through and manipulate data we’ve bound to the DOM. You’ll need some of the following:
- *ngIf — conditionally show content.
- *ngFor — iterate through a list and create content for each item in the list.
- ng-container — a container for content that is not rendered, but can be referenced by other directives.
- ng-class — set classes on elements.
*ngIf
ngIf is a structural directive that allows us to conditionally render content. It can be paired with ng-template to render an else
block.
This example shows content blocks based on the value of boolean showMyContent
.
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.2.1/rxjs.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/core-js/2.5.7/core.js"/></script>
<script src="https://unpkg.com/@angular/core@7.2.0/bundles/core.umd.js"/></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/zone.js/0.8.26/zone.min.js"></script>
<script src="https://unpkg.com/@angular/common@7.2.0/bundles/common.umd.js"></script>
<script src="https://unpkg.com/@angular/compiler@7.2.0/bundles/compiler.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser@7.2.0/bundles/platform-browser.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser-dynamic@7.2.0/bundles/platform-browser-dynamic.umd.js"></script>
<my-app></my-app>
<script type="typescript">
// app.js
const { Component, VERSION } = ng.core;
@Component({
selector: 'my-app',
template: `
<div *ngIf="showMyContent; else showMyFalseTemplate">
I will render if showMyContent is true.
</div>
<ng-template #showMyFalseTemplate>
I render if showMyContent is not true
</ng-template>
`
})
class AppComponent {
showMyContent = false;
constructor() {
}
}
// main.js
const { BrowserModule } = ng.platformBrowser;
const { NgModule } = ng.core;
const { CommonModule } = ng.common;
@NgModule({
imports: [
BrowserModule,
CommonModule,
],
declarations: [AppComponent],
bootstrap: [AppComponent],
providers: []
})
class AppModule {}
const { platformBrowserDynamic } = ng.platformBrowserDynamic;
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err));
</script>
*ngFor
ngFor is a structural directive that allows to iteratively create content in our templates.
This example displays each name in the myList
array in an li
tag.
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.2.1/rxjs.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/core-js/2.5.7/core.js"/></script>
<script src="https://unpkg.com/@angular/core@7.2.0/bundles/core.umd.js"/></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/zone.js/0.8.26/zone.min.js"></script>
<script src="https://unpkg.com/@angular/common@7.2.0/bundles/common.umd.js"></script>
<script src="https://unpkg.com/@angular/compiler@7.2.0/bundles/compiler.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser@7.2.0/bundles/platform-browser.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser-dynamic@7.2.0/bundles/platform-browser-dynamic.umd.js"></script>
<my-app></my-app>
<script type="typescript">
// app.js
const { Component, VERSION } = ng.core;
@Component({
selector: 'my-app',
template: `
<ul>
<li *ngFor="let name of myList">{{name}}</li>
</ul>
`
})
class AppComponent {
myList: string[] = ['blue', 'charlie', 'delta', 'echo'];
constructor() {
}
}
// main.js
const { BrowserModule } = ng.platformBrowser;
const { NgModule } = ng.core;
const { CommonModule } = ng.common;
@NgModule({
imports: [
BrowserModule,
CommonModule,
],
declarations: [AppComponent],
bootstrap: [AppComponent],
providers: []
})
class AppModule {}
const { platformBrowserDynamic } = ng.platformBrowserDynamic;
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err));
</script>
ng-container
ng-container is an element that allows us to create template bindings without creating a DOM element. Only one structural directive is allowed per host element (to avoid confusion around which directive would take precedence) making this directive handy for when we have several logic directives to apply to content.
This example shows using logic directives to display data without creating additional DOM elements.
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.2.1/rxjs.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/core-js/2.5.7/core.js"/></script>
<script src="https://unpkg.com/@angular/core@7.2.0/bundles/core.umd.js"/></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/zone.js/0.8.26/zone.min.js"></script>
<script src="https://unpkg.com/@angular/common@7.2.0/bundles/common.umd.js"></script>
<script src="https://unpkg.com/@angular/compiler@7.2.0/bundles/compiler.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser@7.2.0/bundles/platform-browser.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser-dynamic@7.2.0/bundles/platform-browser-dynamic.umd.js"></script>
<my-app></my-app>
<script type="typescript">
// app.js
const { Component, VERSION } = ng.core;
@Component({
selector: 'my-app',
template: `
<ng-container>
<p>Inspect me using DevTools. Look, Ma, no ng-container!</p>
</ng-container>
<ng-container *ngIf="certainThing">
<ng-container *ngFor="let thing of myThings">{{thing.name}} {{thing.id}}</ng-container>
</ng-container>
`
})
class AppComponent {
certainThing = true;
myThings : any[] = [
{name: 'Blue', id: 001},
{name: 'Charlie', id: 002},
{name: 'Delta', id: 003},
{name: 'Echo', id: 004}
]
constructor() {
}
}
// main.js
const { BrowserModule } = ng.platformBrowser;
const { NgModule } = ng.core;
const { CommonModule } = ng.common;
@NgModule({
imports: [
BrowserModule,
CommonModule,
],
declarations: [AppComponent],
bootstrap: [AppComponent],
providers: []
})
class AppModule {}
const { platformBrowserDynamic } = ng.platformBrowserDynamic;
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err));
</script>
ng-class
The ng-class directive is a way to set classes on elements based on boolean logic. ng-class can take a single class, an array of classes, key value pairs with boolean values, or regexes.
This example shows various ways classes can be added to elements.
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.2.1/rxjs.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/core-js/2.5.7/core.js"/></script>
<script src="https://unpkg.com/@angular/core@7.2.0/bundles/core.umd.js"/></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/zone.js/0.8.26/zone.min.js"></script>
<script src="https://unpkg.com/@angular/common@7.2.0/bundles/common.umd.js"></script>
<script src="https://unpkg.com/@angular/compiler@7.2.0/bundles/compiler.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser@7.2.0/bundles/platform-browser.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser-dynamic@7.2.0/bundles/platform-browser-dynamic.umd.js"></script>
<my-app></my-app>
<script type="typescript">
// app.js
const { Component, VERSION } = ng.core;
@Component({
selector: 'my-app',
template: `
<div [ngClass]="'first second'">...</div>
<div [ngClass]="['first', 'second']">...</div>
<div [ngClass]="{'first': true, 'second': true, 'third': false}">...</div>
<div [ngClass]="stringExp|arrayExp|objExp">...</div>
<div [ngClass]="{'class1 class2 class3' : true}">...</div>
<div [ngClass]="{'my-active-class' : classIsActive}">
Setting classes
</div>
<button (click)="classIsActive = classIsActive ? false: true">set class active</button>
`
})
class AppComponent {
classIsActive = false;
constructor() {
}
}
// main.js
const { BrowserModule } = ng.platformBrowser;
const { NgModule } = ng.core;
const { CommonModule } = ng.common;
@NgModule({
imports: [
BrowserModule,
CommonModule,
],
declarations: [AppComponent],
bootstrap: [AppComponent],
providers: []
})
class AppModule {}
const { platformBrowserDynamic } = ng.platformBrowserDynamic;
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err));
</script>
<style>
.my-active-class {
background: pink;
}
</style>
Notice in the above example our ng-class
is surrounded by [ ]
. This signals that we’re passing in an object, instead of just a string.
When using property binding, ngClass="value"
will evaluate the value as a string of "value" and [ngClass]="value"
as whatever the component property value is.
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.2.1/rxjs.umd.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/core-js/2.5.7/core.js"/></script>
<script src="https://unpkg.com/@angular/core@7.2.0/bundles/core.umd.js"/></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/zone.js/0.8.26/zone.min.js"></script>
<script src="https://unpkg.com/@angular/common@7.2.0/bundles/common.umd.js"></script>
<script src="https://unpkg.com/@angular/compiler@7.2.0/bundles/compiler.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser@7.2.0/bundles/platform-browser.umd.js"></script>
<script src="https://unpkg.com/@angular/platform-browser-dynamic@7.2.0/bundles/platform-browser-dynamic.umd.js"></script>
<my-app></my-app>
<script type="typescript">
// app.js
const { Component, VERSION } = ng.core;
@Component({
selector: 'my-app',
template: `
<div ngClass="my-active-class">...</div>
<div [ngClass]="classSetOnComponent">...</div>
`
})
class AppComponent {
classSetOnComponent: string = 'my-active-class';
constructor() {
}
}
// main.js
const { BrowserModule } = ng.platformBrowser;
const { NgModule } = ng.core;
const { CommonModule } = ng.common;
@NgModule({
imports: [
BrowserModule,
CommonModule,
],
declarations: [AppComponent],
bootstrap: [AppComponent],
providers: []
})
class AppModule {}
const { platformBrowserDynamic } = ng.platformBrowserDynamic;
platformBrowserDynamic()
.bootstrapModule(AppModule)
.catch(err => console.error(err));
</script>
<style>
.my-active-class {
background: pink;
}
</style>
P2: Setup
Let’s create our restaurant component as well. This will be a component that displays a list of restaurants.
✏️ Run:
ng g component restaurant
For now, we’ll use fake data for a list of restaurants in the component, and put the data in a setTimeout to simulate an api call.
✏️ Update src/app/restaurant/restaurant.component.ts to be:
import { Component, OnInit } from '@angular/core';
const fakeRestaurants = [
{
name: 'Poutine Palace',
slug: 'poutine-palace',
images: {
thumbnail: 'node_modules/place-my-order-assets/images/4-thumbnail.jpg',
owner: 'node_modules/place-my-order-assets/images/3-owner.jpg',
banner: 'node_modules/place-my-order-assets/images/2-banner.jpg',
},
menu: {
lunch: [
{
name: 'Crab Pancakes with Sorrel Syrup',
price: 35.99,
},
{
name: 'Steamed Mussels',
price: 21.99,
},
{
name: 'Spinach Fennel Watercress Ravioli',
price: 35.99,
},
],
dinner: [
{
name: 'Gunthorp Chicken',
price: 21.99,
},
{
name: 'Herring in Lavender Dill Reduction',
price: 45.99,
},
{
name: 'Chicken with Tomato Carrot Chutney Sauce',
price: 45.99,
},
],
},
address: {
street: '230 W Kinzie Street',
city: 'Green Bay',
state: 'WI',
zip: '53205',
},
_id: '3ZOZyTY1LH26LnVw',
},
{
name: 'Cheese Curd City',
slug: 'cheese-curd-city',
images: {
thumbnail: 'node_modules/place-my-order-assets/images/2-thumbnail.jpg',
owner: 'node_modules/place-my-order-assets/images/3-owner.jpg',
banner: 'node_modules/place-my-order-assets/images/2-banner.jpg',
},
menu: {
lunch: [
{
name: 'Ricotta Gnocchi',
price: 15.99,
},
{
name: 'Gunthorp Chicken',
price: 21.99,
},
{
name: 'Garlic Fries',
price: 15.99,
},
],
dinner: [
{
name: 'Herring in Lavender Dill Reduction',
price: 45.99,
},
{
name: 'Truffle Noodles',
price: 14.99,
},
{
name: 'Charred Octopus',
price: 25.99,
},
],
},
address: {
street: '2451 W Washburne Ave',
city: 'Green Bay',
state: 'WI',
zip: '53295',
},
_id: 'Ar0qBJHxM3ecOhcr',
},
];
@Component({
selector: 'pmo-restaurant',
templateUrl: './restaurant.component.html',
styleUrl: './restaurant.component.css',
})
export class RestaurantComponent implements OnInit {
restaurants: any[] = [];
ngOnInit(): void {
setTimeout(() => {
this.restaurants = fakeRestaurants;
}, 500);
}
}
To solve this problem, use Angular directives to iterate through data and display properties of restaurants.
✏️ Update src/app/restaurant/restaurant.component.html to be:
<div class="restaurants">
<h2 class="page-header">Restaurants</h2>
<!-- if restaurants has a length show the list -->
<!-- show the following markup for each restaurant -->
<div class="restaurant">
<img alt="" src="{{ restaurant.images.thumbnail }}" width="100" height="100" />
<h3>{{ restaurant.name }}</h3>
<div class="address" *ngIf="restaurant.address">
{{ restaurant.address.street }}<br />{{ restaurant.address.city }},
{{ restaurant.address.state }} {{ restaurant.address.zip }}
</div>
<div class="hours-price">
$$$<br />
Hours: M-F 10am-11pm
<span class="open-now">Open Now</span>
</div>
<a class="btn" [routerLink]="['/restaurants', restaurant.slug]"> Details </a>
<br />
</div>
<!-- end of restaurant markup -->
</div>
✏️ To see our component working, we can paste it into our src/app/app.component.html file just like with the home component:
<h1>Place My Order App: Coming Soon!</h1>
<router-outlet />
<pmo-restaurant></pmo-restaurant>
P2: How to verify your solution is correct
✏️ Update the spec file src/app/restaurant/restaurant.component.spec.ts to be:
import {
ComponentFixture,
fakeAsync,
TestBed,
tick,
} from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { RestaurantComponent } from './restaurant.component';
describe('RestaurantComponent', () => {
let fixture: ComponentFixture<RestaurantComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [RouterTestingModule],
declarations: [RestaurantComponent],
}).compileComponents();
fixture = TestBed.createComponent(RestaurantComponent);
});
it('should create', () => {
const component: RestaurantComponent = fixture.componentInstance;
expect(component).toBeTruthy();
});
it('should render title in a h2 tag', () => {
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h2')?.textContent).toContain('Restaurants');
});
it('should not show any restaurants markup if no restaurants', () => {
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('.restaurant')).toBe(null);
});
it('should have two .restaurant divs', fakeAsync((): void => {
fixture.detectChanges();
tick(501);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
const restaurantDivs = compiled.getElementsByClassName('restaurant');
const hoursDivs = compiled.getElementsByClassName('hours-price');
expect(restaurantDivs.length).toEqual(2);
expect(hoursDivs.length).toEqual(2);
}));
it('should display restaurant information', fakeAsync((): void => {
fixture.detectChanges();
tick(501);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('.restaurant h3')?.textContent).toContain(
'Poutine Palace'
);
}));
});
P2: Solution
If you’ve implemented the solution correctly, the tests will pass when you run npm run test
!
Click to see the solution
✏️ Update src/app/restaurant/restaurant.component.html to the following:<div class="restaurants">
<h2 class="page-header">Restaurants</h2>
<ng-container *ngIf="restaurants.length">
<div class="restaurant" *ngFor="let restaurant of restaurants">
<img alt="" src="{{ restaurant.images.thumbnail }}" width="100" height="100" />
<h3>{{ restaurant.name }}</h3>
<div class="address" *ngIf="restaurant.address">
{{ restaurant.address.street }}<br />{{ restaurant.address.city }},
{{ restaurant.address.state }} {{ restaurant.address.zip }}
</div>
<div class="hours-price">
$$$<br />
Hours: M-F 10am-11pm
<span class="open-now">Open Now</span>
</div>
<a class="btn" [routerLink]="['/restaurants', restaurant.slug]">
Details
</a>
<br />
</div>
</ng-container>
</div>