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 it's template

Generate 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.less
|   |       |── 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.less 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.

Bind 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 {
  public myTitle: string = '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>


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>{{myData}}</p>
    <p>{{myObject | json}}</p>
  `
})
class ChildComponent {
  @Input() myData: string;
  @Input() myObject: any;
  constructor() {}
}

@Component({
  selector: 'my-app',
  template: `
   <child-component myData="Hello string."></child-component>
   <child-component [myData]="myTitle"></child-component>
   <child-component [myObject]="sillyObject"></child-component>
  `
})
class AppComponent {
  public myTitle: string = 'Hello world.';
  public sillyObject = {
    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's title 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 --save

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

section copied - angular.json ✏️ Update angular.json:

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "place-my-order": {
      "root": "",
      "sourceRoot": "src",
      "projectType": "application",
      "prefix": "pmo",
      "schematics": {
        "@schematics/angular:component": {
          "style": "less"
        }
      },
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "dist/place-my-order",
            "index": "src/index.html",
            "main": "src/main.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "src/tsconfig.app.json",
            "assets": [
              "src/favicon.ico",
              "src/assets",
              {
                "glob": "**/*",
                "input": "./node_modules/place-my-order-assets/images/",
                "output": "./assets/images"
              }
            ],
            "styles": [
              "src/styles.less",
              "./node_modules/place-my-order-assets/less/styles.less"
            ],
            "scripts": [],
            "es5BrowserSupport": true
          },
          "configurations": {
            "production": {
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ],
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "extractCss": true,
              "namedChunks": false,
              "aot": true,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": true,
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "2mb",
                  "maximumError": "5mb"
                }
              ]
            }
          }
        },
        "serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "options": {
            "browserTarget": "place-my-order:build"
          },
          "configurations": {
            "production": {
              "browserTarget": "place-my-order:build:production"
            }
          }
        },
        "extract-i18n": {
          "builder": "@angular-devkit/build-angular:extract-i18n",
          "options": {
            "browserTarget": "place-my-order:build"
          }
        },
        "test": {
          "builder": "@angular-devkit/build-angular:karma",
          "options": {
            "main": "src/test.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "src/tsconfig.spec.json",
            "karmaConfig": "src/karma.conf.js",
            "styles": [
              "src/styles.less"
            ],
            "scripts": [],
            "assets": [
              "src/favicon.ico",
              "src/assets"
            ]
          }
        },
        "lint": {
          "builder": "@angular-devkit/build-angular:tslint",
          "options": {
            "tsConfig": [
              "src/tsconfig.app.json",
              "src/tsconfig.spec.json"
            ],
            "exclude": [
              "**/node_modules/**"
            ]
          }
        }
      }
    },
    "place-my-order-e2e": {
      "root": "e2e/",
      "projectType": "application",
      "prefix": "",
      "architect": {
        "e2e": {
          "builder": "@angular-devkit/build-angular:protractor",
          "options": {
            "protractorConfig": "e2e/protractor.conf.js",
            "devServerTarget": "place-my-order:serve"
          },
          "configurations": {
            "production": {
              "devServerTarget": "place-my-order:serve:production"
            }
          }
        },
        "lint": {
          "builder": "@angular-devkit/build-angular:tslint",
          "options": {
            "tsConfig": "e2e/tsconfig.e2e.json",
            "exclude": [
              "**/node_modules/**"
            ]
          }
        }
      }
    }
  },
  "defaultProject": "place-my-order"
}
        

Any time changes are made to the angular.json file, we need to restart our server to catch the new changes.

✏️ Restart your server.

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></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 it's own page with a unique route.

P1: How to Verify Your Solution is Correct

✏️ Update the spec file src/app/home/home.component.spec.ts to be:

import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { HomeComponent } from './home.component';

describe('HomeComponent', () => {
  let component: HomeComponent;
  let fixture: ComponentFixture<HomeComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ HomeComponent ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    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.title).toContain('Ordering food has never been easier');
  });

});

If you've implemented the solution correctly, when you run npm run test all tests will pass!

P1: 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>

src/app/home/home.component.ts

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

@Component({
  selector: 'pmo-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.less']
})
export class HomeComponent implements OnInit {
  public title: string = 'Ordering food has never been easier';

  constructor() { }

  ngOnInit() {
  }

}

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 {
  public showMyContent: boolean = 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 {
  public 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 dev tools. 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 {
  public certainThing: boolean = 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 {
  public classIsActive: boolean = 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 {
  public 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: Technical Requirements

Here is the markup to show for each restaurant:

<img 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 />

To solve this problem, use Angular directives to iterate through data and display properties of restaurants.

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';


let 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',
  styleUrls: ['./restaurant.component.less']
})
export class RestaurantComponent implements OnInit {
  public restaurants: any[] = [];

  constructor() { }

  ngOnInit() {
    setTimeout(() => {
      this.restaurants = fakeRestaurants;
    }, 500)
  }

}

✏️ 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 -->

    <!-- inside this container, show the following markup for each restaurant -->
    <div class="restaurant">

    </div>
    <!-- end of restaurant markup -->
</div>

Reminder: The markup that should be written out for each restaurant can be found in the exercise description.

✏️ 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></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 { async, ComponentFixture, TestBed, tick, fakeAsync } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';

import { RestaurantComponent } from './restaurant.component';

describe('RestaurantComponent', () => {
  let component: RestaurantComponent;
  let fixture: ComponentFixture<RestaurantComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        RouterTestingModule
      ],
      declarations: [ RestaurantComponent ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(RestaurantComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should render title in a h2 tag', () => {
    const fixture = TestBed.createComponent(RestaurantComponent);
    fixture.detectChanges();
    const compiled = fixture.debugElement.nativeElement;
    expect(compiled.querySelector('h2').textContent).toContain('Restaurants');
  });

  it('should not show any restaurants markup if no restaurants', () => {
    const fixture = TestBed.createComponent(RestaurantComponent);
    fixture.detectChanges();
    const compiled = fixture.debugElement.nativeElement;
    expect(compiled.querySelector('.restaurant')).toBe(null);
  });

  it('should have two .restaurant divs',  <any>fakeAsync((): void => {
    const fixture = TestBed.createComponent(RestaurantComponent);
    fixture.detectChanges();
    tick(501);
    fixture.detectChanges();
    const compiled = fixture.debugElement.nativeElement;
    let restaurantDivs = compiled.getElementsByClassName('restaurant');
    let hoursDivs = compiled.getElementsByClassName('hours-price');
    expect(restaurantDivs.length).toEqual(2);
    expect(hoursDivs.length).toEqual(2);
  }));

  it('should display restaurant information',  <any>fakeAsync((): void => {
    const fixture = TestBed.createComponent(RestaurantComponent);
    fixture.detectChanges();
    tick(501);
    fixture.detectChanges();
    const compiled = fixture.debugElement.nativeElement;
    expect(compiled.querySelector('.restaurant h3').textContent).toContain('Poutine Palace');
  }));

});

If you've implemented the solution correctly, when you run npm run test all tests will pass!

P2: 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 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>