Use Restaurant API data page

Learn how to render API data into HTML with a view.

Overview

In this part, we will:

  • Import new service into Restaurant Component
  • Call getRestaurants() method in component
  • Change restaurant markup to use new restaurant object

Problem 1: Make RestaurantComponent Use the getRestaurants Function

In this section, we will change RestaurantComponent to actually get data from the service API. Instead of two hard coded restaurants, we will see a longer list:

P1: Technical Requirements

  1. Change RestaurantComponent's restaurant property definition:
public restaurants: any[] = [];
  1. Use RestaurantService's getRestaurants to get an array of restaurants and set it on RestaurantComponent's restaurant property.

P1: How to Verify Your Solution is Correct

You should be able see a list of all restaurants when you navigate to localhost:4200/restaurants, instead of the two that were previously hard-coded.

✏️ 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 { of } from 'rxjs'; 
import { ImageUrlPipe } from '../image-url.pipe';

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

class MockRestaurantService {
  getRestaurants() {
    return of({
      data: [{
        "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"
      }]}
            )
  }
}
describe('RestaurantComponent', () => {
  let component: RestaurantComponent;
  let fixture: ComponentFixture<RestaurantComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        RouterTestingModule
      ],
      providers: [{
        provide: RestaurantService,
        useClass: MockRestaurantService
      }],
      declarations: [ RestaurantComponent, ImageUrlPipe ]
    })
    .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);
    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');
  }));

  it('should set restaurants member to response of getRestaurants method', <any>fakeAsync((): void => {
    const fixture = TestBed.createComponent(RestaurantComponent);
    fixture.detectChanges();
    tick();
    fixture.detectChanges();
    let expectedRestaurants = [{
        "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"
      }];
    expect(fixture.componentInstance.restaurants).toEqual(expectedRestaurants);
  }));

});

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

import { TestBed, async, fakeAsync, tick, flush } from '@angular/core/testing';
import { Router } from '@angular/router';
import { Location } from '@angular/common';
import { By } from '@angular/platform-browser';
import { of } from 'rxjs'; 

import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { HomeComponent } from './home/home.component';
import { RestaurantComponent } from './restaurant/restaurant.component';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ImageUrlPipe } from './image-url.pipe';
import { HttpClientModule } from '@angular/common/http';
import { RestaurantService } from './restaurant/restaurant.service';

class MockRestaurantService {
  getRestaurants() {
    return of({
      data: [{
        "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"
      }]}
            )
  }

  getStates() {
    return of({
      data: [
        {"short":"MO","name":"Missouri"},
        {"short":"CA  ","name":"California"},
        {"short":"MI","name":"Michigan"}]
    });
  }

  getCities(state:string) {
    return of({
      data: [{"name":"Sacramento","state":"CA"},{"name":"Oakland","state":"CA"}]
    });
  }

  getRestaurant(slug:string) {
    return of({
      "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"
    })
  }
}

describe('AppComponent', () => {
  let router: Router;
  let location: Location;
  let fixture;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        AppRoutingModule, HttpClientModule
      ],
      declarations: [
        AppComponent, HomeComponent, RestaurantComponent, ImageUrlPipe
      ],
      schemas: [
        NO_ERRORS_SCHEMA
      ]
    }).overrideComponent(RestaurantComponent, {
      set: {
        providers: [
          { provide: RestaurantService, useClass: MockRestaurantService }
        ]
      }
    })
    .compileComponents();

    router = TestBed.get(Router);
    location = TestBed.get(Location);

    fixture = TestBed.createComponent(AppComponent);
  }));

  it('should create the app', () => {
    const app = fixture.debugElement.componentInstance;
    expect(app).toBeTruthy();
  });

  it(`should have as title 'place-my-order'`, () => {
    const app = fixture.debugElement.componentInstance;
    expect(app.title).toEqual('place-my-order');
  });

  it('should render title in a h1 tag', () => {
    const fixture = TestBed.createComponent(AppComponent);
    fixture.detectChanges();
    const compiled = fixture.debugElement.nativeElement;
    expect(compiled.querySelector('h1').textContent).toContain('place-my-order.com');
  });

  it('should render the HomeComponent with router navigates to "/" path', () => {
    const compiled = fixture.debugElement.nativeElement;
    router.navigate(['']).then(() => {
      expect(location.path()).toBe('');
      expect(compiled.querySelector('pmo-home')).not.toBe(null);
    });
  });

  it('should render the RestaurantsComponent with router navigates to "/restaurants" path', () => {
    const compiled = fixture.debugElement.nativeElement;
    router.navigate(['restaurants']).then(() => {
      expect(location.path()).toBe('/restaurants');
      expect(compiled.querySelector('pmo-restaurant')).not.toBe(null);
    });
  });

  it('should have the home navigation link href set to ""', () => {
    fixture.detectChanges();
    let homeLink = fixture.debugElement.query(By.css('li a'));
    let href = homeLink.nativeElement.getAttribute('href');
    expect(href).toEqual('/');
  });

  it('should have the restaurants navigation link href set to ""', () => {
    fixture.detectChanges();
    let restaurantsLink = fixture.debugElement.query(By.css('li:nth-child(2) a'));
    let href = restaurantsLink.nativeElement.getAttribute('href');
    expect(href).toEqual('/restaurants');
  });

  it('should make the home navigation link class active when the router navigates to "/" path', fakeAsync(() => {
    const compiled = fixture.debugElement.nativeElement;
    router.navigate(['']).then(() => {
      fixture.detectChanges();
      tick();
      fixture.detectChanges();
      let homeLinkLi = fixture.debugElement.query(By.css('li'));
      expect(homeLinkLi.nativeElement.classList).toContain('active');
      expect(compiled.querySelectorAll('.active').length).toBe(1);
      fixture.destroy();
      flush();
    });
  }));

  it('should make the restaurants navigation link class active when the router navigates to "/restaurants" path', fakeAsync(() => {
    const compiled = fixture.debugElement.nativeElement;
    router.navigate(['restaurants']).then(() => {
      expect(location.path()).toBe('/restaurants');
      fixture.detectChanges();
      tick();
      fixture.detectChanges();
      let restaurantsLinkLi = fixture.debugElement.query(By.css('li:nth-child(2)'));
      expect(restaurantsLinkLi.nativeElement.classList).toContain('active');
      expect(compiled.querySelectorAll('.active').length).toBe(1);
      fixture.destroy();
      flush();
    });
  }));

});

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

P1: What You Need to Know

  • How to inject a service into a component
  • How to subscribe to an observable

Hint: Call the getRestaurants method in the ngOnInit method.

Inject a Service into a Component

To use a service in a component, we use dependency injection to pass the service in the component constructor function. We're then able to access methods on it for use in our component.

<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/router@7.2.0/bundles/router.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>
<base href="/">
<my-app></my-app>
<script type="typescript">
const { Component, NgModule, VERSION, OnInit, Injectable } = ng.core;
const { BrowserModule } = ng.platformBrowser;
const { CommonModule } = ng.common;
const { Routes, RouterModule } = ng.router;

@Injectable({
  providedIn: 'root'
})
class UsersService {
  private users = [
  {
    name: 'Jennifer',
    id: 1,
    role: 'admin'
  },
  {
    name: 'Steve',
    id: 2,
    role: 'user'
  },
  {
    name: 'Alice',
    id: 3,
    role: 'developer'
  }]

  constructor() { }

  getUsers() {
    return this.users;
  }

  getUser(id: number) {
    return this.users.find(x => x.id === id)
  }
}

@Component({
  selector: 'my-app',
  template: `
    <ul class="nav">
      <li routerLinkActive="active">
        <a routerLink="/about" >About</a>
      </li>
    </ul>
    <router-outlet></router-outlet>
  `
})
class AppComponent {
  constructor() {}
}

@Component({
  selector: 'about-component',
  template: `
    <p>An about component!</p>
  `
})
class AboutComponent {
  constructor() {
  }
}

@Component({
  selector: 'home-component',
  template: `
    <p>A home component!</p>
    <ul>
      <li *ngFor="let user of users">
        {{user.name}}
      </li>
    </ul>
  `
})
class HomeComponent implements OnInit{
  private users: any[] = [];

  constructor(private usersService: UsersService) {
  
  }

  ngOnInit() {
    this.users = this.usersService.getUsers();
  }
}
//THIS IS A HACK JUST FOR CODEPEN TO WORK
HomeComponent.parameters = [UsersService];

const routes: Routes = [
  { path: 'about', component: AboutComponent },
  { path: '**', component: HomeComponent }
]
@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
class AppRoutingModule { }

                 
@NgModule({
  declarations: [AppComponent, AboutComponent, HomeComponent],
  imports: [
    BrowserModule,
    CommonModule,
    AppRoutingModule
  ],
  bootstrap: [AppComponent],
  providers: []
})
class AppModule {}

const { platformBrowserDynamic } = ng.platformBrowserDynamic;

platformBrowserDynamic()
  .bootstrapModule(AppModule)
  .catch(err => console.error(err));
</script>


Subscribe to an Observable

The result of getRestaurants() is an observable. Use subscribe to listen to when an RxJS observable changes:

<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.2.1/rxjs.umd.js"></script>
<script type="typescript">
const {Observable} = rxjs;

const observable = Observable.create(function (observer) {
  observer.next(1);
  observer.next(2);
  observer.next(3);
});

observable.subscribe( function subscriber( value ){
    console.log('got value ' + value);
    // Logs 1, 2, 3
} );
</script>

P1: Solution

✏️ Update src/app/restaurant/restaurant.component.ts as follows:

import { Component, OnInit } from '@angular/core';
import { RestaurantService, ResponseData } from './restaurant.service';
import { Restaurant } from './restaurant';

@Component({
  selector: 'pmo-restaurant',
  templateUrl: './restaurant.component.html',
  styleUrls: ['./restaurant.component.less']
})
export class RestaurantComponent implements OnInit {
  public restaurants: Restaurant[] = [];

  constructor(private restaurantService: RestaurantService) { }

  ngOnInit() {
    this.restaurantService.getRestaurants().subscribe((res: ResponseData) => {
      this.restaurants = res.data;
    });
  }
}

Problem 2: Show a Loading State while Restaurants are Being Requested

Sometimes the server can take a long time to respond. It's a better experience for the user to show a loading icon to the user while data is loading:

TODO:

This icon will be shown with the following HTML:

<div class="restaurant loading"></div>

P2: Technical Requirements

  1. Create a new interface Data that represents the following object:

    let data = {
      value: [], //aray of restaurants
      isPending: false //boolean
    }
    
  2. Change the restaurants member to use the new Data type in the RestaurantsComponent.

  3. Right before you call the getRestaurants method, set the restaurants isPending value to true.

  4. Once the getRestaurants response is received, set the restaurants value to the response data and isPending value to false.

  5. Update the html to match the new restaurant object values and to show the following HTML while isPending is true:

    <div class="restaurant loading"></div>
    

P2: To Verify Solution

You should be able see a list of restaurants when you navigate to localhost:4200/restaurants!

✏️ 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 { of } from 'rxjs'; 
import { ImageUrlPipe } from '../image-url.pipe';

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

class MockRestaurantService {
  getRestaurants() {
    return of({
      data: [{
        "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"
      }]}
            )
  }
}
describe('RestaurantComponent', () => {
  let component: RestaurantComponent;
  let fixture: ComponentFixture<RestaurantComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        RouterTestingModule
      ],
      providers: [{
        provide: RestaurantService,
        useClass: MockRestaurantService
      }],
      declarations: [ RestaurantComponent, ImageUrlPipe ]
    })
    .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);
    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');
  }));

  it('should set restaurants value to restaurants response data and set isPending to false', <any>fakeAsync((): void => {
    const fixture = TestBed.createComponent(RestaurantComponent);
    fixture.detectChanges();
    tick();
    fixture.detectChanges();
    let expectedRestaurants = {
      value: [{
        "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"
      }],
      isPending: false
    }
    expect(fixture.componentInstance.restaurants).toEqual(expectedRestaurants);
  }));

  it('should show a loading div while isPending is true', () => {
    const fixture = TestBed.createComponent(RestaurantComponent);
    fixture.detectChanges();
    fixture.componentInstance.restaurants.isPending = true;
    fixture.detectChanges();
    const compiled = fixture.debugElement.nativeElement;
    let loadingDiv = compiled.querySelector('.loading');
    expect(loadingDiv).toBeTruthy();
  });

  it('should not show a loading div if isPending is false', () => {
    const fixture = TestBed.createComponent(RestaurantComponent);
    fixture.componentInstance.restaurants.isPending = false;
    fixture.detectChanges();
    const compiled = fixture.debugElement.nativeElement;
    let loadingDiv = compiled.querySelector('.loading');
    expect(loadingDiv).toBe(null);
  });

});

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

P2: What You Need to Know

  • How to write an interface (you learned this in the previous section! ✔️)
  • How to conditionally show html blocks (you learned this in a previous section! ✔️)

P2: Solution

✏️ Update src/app/restaurant/restaurant.component.ts to:

import { Component, OnInit } from '@angular/core';
import { RestaurantService, ResponseData } from './restaurant.service';
import { Restaurant } from './restaurant';

export interface Data {
  value: Array<Restaurant>;
  isPending: boolean;
}

@Component({
  selector: 'pmo-restaurant',
  templateUrl: './restaurant.component.html',
  styleUrls: ['./restaurant.component.less']
})
export class RestaurantComponent implements OnInit {
  public restaurants: Data = {
    value: [],
    isPending: false
  };
  constructor(private restaurantService: RestaurantService) { }

  ngOnInit() {
    this.restaurants.isPending = true;
    this.restaurantService.getRestaurants().subscribe((res: ResponseData) => {
      this.restaurants.value = res.data;
      this.restaurants.isPending = false;
    });
  }
}

✏️ Update src/app/restaurant/restaurant.component.html to:

<div class="restaurants">
  <h2 class="page-header">Restaurants</h2>
  <div class="restaurant loading" *ngIf="restaurants.isPending"></div>
  <ng-container *ngIf="restaurants.value.length">
    <div class="restaurant" *ngFor="let restaurant of restaurants.value">

      <img src="{{restaurant.images.thumbnail | imageUrl}}" 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>

Did You Know?

You may have noticed in our markup there's another use of routerLink:

<a class="btn" [routerLink]="['/restaurants', restaurant.slug]">
  Details
</a>

One of the ways to create a link is to pass in the individual parts to the routerLink directive. This will generate the path /restaurants/crab-cafe for the "crab cafe" restaurant from it's slug.