Filter Restaurants by City page

Learn how to make a service take multiple parameters.

Overview

In this part, we will:

  • Update getRestaurants service method to take params
  • Update component to pass state and city params to service method

Problem

Now that we are able to capture a user's state and city preferences, we want to only return restaurants in the selected city. Modify the getRestaurants method in the src/app/restaurant/restaurant.service.ts file to take two string parameters, one for city, and one for state. The requested url with params should look like this: '/api/restaurants?filter[address.state]=IL&filter[address.city]=Chicago'

Technical Requirements

In the src/app/restaurant/restaurant.component.ts file, update the call to the getRestaurants service method to use the city and state values capture from the user's form input.

How to Verify Your Solution is Correct

If you've implemented the solution correctly, when you use the select boxes to choose state and city, you should see a list of just restaurants from the selected city returned.

✏️ 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 { RestaurantComponent } from './restaurant.component';
import { RestaurantService } from './restaurant.service';
import { ImageUrlPipe } from '../image-url.pipe';
import { ReactiveFormsModule } from '@angular/forms';

const restaurantAPIResponse = {
  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"
  }]};
class MockRestaurantService {
  getRestaurants(state, city) {
    return of(restaurantAPIResponse)
  }

  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"}]
    });
  }
}
describe('RestaurantComponent', () => {
  let component: RestaurantComponent;
  let fixture: ComponentFixture<RestaurantComponent>;
  let injectedService;
  
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        RouterTestingModule,
        ReactiveFormsModule
      ],
      providers: [{
        provide: RestaurantService,
        useClass: MockRestaurantService
      }],
      declarations: [ RestaurantComponent, ImageUrlPipe ]
    })
    .compileComponents();
    injectedService = TestBed.get(RestaurantService);
  }));

  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.componentInstance.form.get('state').patchValue('CA');
    fixture.componentInstance.form.get('city').patchValue('Sacramento');
    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.componentInstance.form.get('state').patchValue('CA');
    fixture.componentInstance.form.get('city').patchValue('Sacramento');
    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 when state and city form values are selected', <any>fakeAsync((): void => {
    const fixture = TestBed.createComponent(RestaurantComponent);
    fixture.detectChanges();
    tick();
    fixture.componentInstance.form.get('state').patchValue('CA');
    fixture.componentInstance.form.get('city').patchValue('Sacramento');
    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);
  });

  it('should have a form property with city and state keys', () => {
    const fixture = TestBed.createComponent(RestaurantComponent);
    fixture.detectChanges();
    expect(fixture.componentInstance.form.controls.state).toBeTruthy();
    expect(fixture.componentInstance.form.controls.city).toBeTruthy();
  });

  it('should show a state dropdown', () => {
    const fixture = TestBed.createComponent(RestaurantComponent);
    fixture.detectChanges();
    const compiled = fixture.debugElement.nativeElement;
    let stateSelect = compiled.querySelector('select[formcontrolname="state"]');
    expect(stateSelect).toBeTruthy();
  });

  it('should show a city dropdown', () => {
    const fixture = TestBed.createComponent(RestaurantComponent);
    fixture.detectChanges();
    const compiled = fixture.debugElement.nativeElement;
    let citySelect = compiled.querySelector('select[formcontrolname="city"]');
    expect(citySelect).toBeTruthy();
  });

  it('should set states value to states response data and set isPending to false', <any>fakeAsync((): void => {
    const fixture = TestBed.createComponent(RestaurantComponent);
    fixture.detectChanges();
    tick();
    fixture.detectChanges();
    let expectedStates = {
      value: [
        {"short":"MO","name":"Missouri"},
        {"short":"CA  ","name":"California"},
        {"short":"MI","name":"Michigan"}
      ],
      isPending: false
    }
    expect(fixture.componentInstance.states).toEqual(expectedStates);
  }));

  it('should set state dropdown options to be values of states member', <any>fakeAsync((): void => {
    const fixture = TestBed.createComponent(RestaurantComponent);
    fixture.detectChanges();
    tick();
    fixture.detectChanges();
    const compiled = fixture.debugElement.nativeElement;
    let stateOption = compiled.querySelector('select[formcontrolname="state"] option:nth-child(2)');
    expect(stateOption.textContent).toEqual(' Missouri');
    expect(stateOption.value).toEqual('MO');
  }));

  it('should set cities value to cities response data and set isPending to false', <any>fakeAsync((): void => {
    const fixture = TestBed.createComponent(RestaurantComponent);
    fixture.detectChanges();
    tick();
    fixture.componentInstance.form.get('state').patchValue('CA');
    fixture.detectChanges();
    let expectedCities = {
      value: [
        {"name":"Sacramento","state":"CA"},
        {"name":"Oakland","state":"CA"}
      ],
      isPending: false
    }
    expect(fixture.componentInstance.cities).toEqual(expectedCities);
  }));

  it('should set city dropdown options to be values of cities member when state value is selected', <any>fakeAsync((): void => {
    const fixture = TestBed.createComponent(RestaurantComponent);
    fixture.detectChanges();
    tick();
    fixture.componentInstance.form.get('state').patchValue('CA');
    fixture.detectChanges();
    const compiled = fixture.debugElement.nativeElement;
    let cityOption = compiled.querySelector('select[formcontrolname="city"] option:nth-child(2)');
    expect(cityOption.textContent).toEqual(' Sacramento');
    expect(cityOption.value).toEqual('Sacramento');
  }));

  it('state dropdown should be disabled until states are populated', <any>fakeAsync((): void => {
    const fixture = TestBed.createComponent(RestaurantComponent);
    let storeGetStatesFunc = fixture.componentInstance.getStates;
    fixture.componentInstance.getStates = () => {}; //preventing getStates func from being called 
    fixture.detectChanges(); //detecting changes for createForm func to be called
    let stateFormControl1 = fixture.componentInstance.form.get('state');
    expect(stateFormControl1.enabled).toBe(false);
    fixture.componentInstance.getStates = storeGetStatesFunc;
    fixture.componentInstance.getStates();  //calling getStates func when we want it
    fixture.detectChanges();
    let stateFormControl2 = fixture.componentInstance.form.get('state');
    expect(stateFormControl2.enabled).toBe(true);
  }));

  it('city dropdown should be disabled until cities are populated', <any>fakeAsync((): void => {
    const fixture = TestBed.createComponent(RestaurantComponent);
    fixture.detectChanges(); //detecting changes for createForm func to be called
    let cityFormControl1 = fixture.componentInstance.form.get('city');
    expect(cityFormControl1.enabled).toBe(false);
    fixture.componentInstance.form.get('state').patchValue('CA');
    fixture.detectChanges();
    let cityFormControl2 = fixture.componentInstance.form.get('city');
    expect(cityFormControl2.enabled).toBe(true);
  }));

  it('should reset list of cities when new state is selected', <any>fakeAsync((): void => {
    const fixture = TestBed.createComponent(RestaurantComponent);
    fixture.detectChanges(); //detecting changes for createForm func to be called
    fixture.componentInstance.form.get('state').patchValue('CA');
    fixture.componentInstance.form.get('city').patchValue('Sacramento');
    fixture.detectChanges();
    expect(fixture.componentInstance.restaurants.value.length).toEqual(2);
    fixture.componentInstance.form.get('state').patchValue('MO');
    fixture.detectChanges();
    expect(fixture.componentInstance.restaurants.value.length).toEqual(0);
  }));

  it('should call getRestaurants method with two string params', <any>fakeAsync((): void => {
    const fixture = TestBed.createComponent(RestaurantComponent);
    let getRestaurantsSpy = spyOn(injectedService, 'getRestaurants').and.returnValue(of(restaurantAPIResponse));
    fixture.detectChanges();
    fixture.componentInstance.form.get('state').patchValue('CA');
    fixture.componentInstance.form.get('city').patchValue('Sacramento');
    fixture.detectChanges();
    tick();
    expect(getRestaurantsSpy).toHaveBeenCalledWith('CA','Sacramento');
  }));
});

✏️ Update the spec file src/app/restaurant/restaurant.service.spec.ts

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

import { ResponseData, RestaurantService, State, City } from './restaurant.service';
import { Restaurant } from './restaurant';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';

describe('RestaurantService', () => {
  let httpMock : HttpTestingController;
  let restaurantService: RestaurantService;

  beforeEach(() => TestBed.configureTestingModule({
    imports: [HttpClientTestingModule],
    providers: [
      RestaurantService
    ]
  }));

  beforeEach(() => {
    httpMock = TestBed.get(HttpTestingController);
    restaurantService = TestBed.get(RestaurantService);
  })

  it('should be created', () => {
    expect(restaurantService).toBeTruthy();
  });
 
  it('should make a get request to restaurants', () => {
    const mockRestaurants = {
      data: [{
      "name":"Brunch Place",
      "slug":"brunch-place",
      "images":{
        "thumbnail":"node_modules/place-my-order-assets/images/4-thumbnail.jpg",
        "owner":"node_modules/place-my-order-assets/images/2-owner.jpg",
        "banner":"node_modules/place-my-order-assets/images/2-banner.jpg"},
        "menu":{
          "lunch":[
            {"name":"Ricotta Gnocchi","price":15.99},
            {"name":"Garlic Fries","price":15.99},
            {"name":"Charred Octopus","price":25.99}
          ],
          "dinner":[
            {"name":"Steamed Mussels","price":21.99},
            {"name":"Roasted Salmon","price":23.99},
            {"name":"Crab Pancakes with Sorrel Syrup","price":35.99}
          ]
        },
        "address":{
          "street":"2451 W Washburne Ave",
          "city":"Ann Arbor","state":"MI","zip":"53295"},
          "_id":"xugqxQIX5rPJTLBv"
        },
        {
          "name":"Taco Joint",
          "slug":"taco-joint",
          "images":{
            "thumbnail":"node_modules/place-my-order-assets/images/4-thumbnail.jpg",
            "owner":"node_modules/place-my-order-assets/images/2-owner.jpg",
            "banner":"node_modules/place-my-order-assets/images/2-banner.jpg"},
            "menu":{
              "lunch":[
                {"name":"Beef Tacos","price":15.99},
                {"name":"Chicken Tacos","price":15.99},
                {"name":"Guacamole","price":25.99}
              ],
              "dinner":[
                {"name":"Shrimp Tacos","price":21.99},
                {"name":"Chicken Enchilada","price":23.99},
                {"name":"Elotes","price":35.99}
              ]
            },
            "address":{
              "street":"13 N 21st St",
              "city":"Chicago","state":"IL","zip":"53295"},
              "_id":"xugqxQIX5dfgdgTLBv"
            }]
          };

    restaurantService.getRestaurants("IL","Chicago").subscribe((restaurants:ResponseData<Restaurant>) => {
      expect(restaurants).toEqual(mockRestaurants);
    });

    let url = 'http://localhost:7070/restaurants?filter%5Baddress.state%5D=IL&filter%5Baddress.city%5D=Chicago';
    //url parses to 'http://localhost:7070/restaurants?filter[address.state]=IL&filter[address.city]=Chicago'
    const req = httpMock.expectOne(url);


    expect(req.request.method).toEqual('GET');
    req.flush(mockRestaurants);

    httpMock.verify();
  });

  it('can set proper properties on restaurant type', () => {
    let restaurant: Restaurant = {
      name:"Taco Joint",
      slug:"taco-joint",
      images:{
        thumbnail:"node_modules/place-my-order-assets/images/4-thumbnail.jpg",
        owner:"node_modules/place-my-order-assets/images/2-owner.jpg",
        banner:"node_modules/place-my-order-assets/images/2-banner.jpg"
      },
      menu:{
        lunch:[
          {name:"Beef Tacos","price":15.99},
          {name:"Chicken Tacos","price":15.99},
          {name:"Guacamole","price":25.99}
        ],
        dinner:[
          {name:"Shrimp Tacos","price":21.99},
          {name:"Chicken Enchilada","price":23.99},
          {name:"Elotes","price":35.99}
        ]
      },
      address:{
        street:"13 N 21st St",
        city:"Chicago","state":"IL","zip":"53295"
      },
      _id:"xugqxQIX5dfgdgTLBv"
    }
    //will error if interface isn't implemented correctly
    expect(true).toBe(true);
  });

  it('should make a get request to states', () => {
    const mockStates = {
      data: [
        {name: 'Missouri', short: 'MO'}
      ]
    };

    restaurantService.getStates().subscribe((states: ResponseData<State>) => {
      expect(states).toEqual(mockStates);
    });

    let url = 'http://localhost:7070/states';
    const req = httpMock.expectOne(url);

    expect(req.request.method).toEqual('GET');
    req.flush(mockStates);

    httpMock.verify();
  });

  it('should make a get request to cities', () => {
    const mockCities = {
      data: [
        {name: 'Kansas City', state: 'MO'}
      ]
    };

    restaurantService.getCities('MO').subscribe((cities: ResponseData<City>) => {
      expect(cities).toEqual(mockCities);
    });

    let url = 'http://localhost:7070/cities?state=MO';
    const req = httpMock.expectOne(url);
    expect(req.request.method).toEqual('GET');
    req.flush(mockCities);

    httpMock.verify();
  });

});

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

What You Need to Know

  • How to use HttpParams (you learned this in the previous section! ✔️)

Solution

✏️ Update src/app/restaurant/restaurant.service.ts

import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Restaurant } from './restaurant';
import { environment } from '../../environments/environment';

export interface ResponseData<dataType> {
  data: Array<dataType>;
}

export interface State {
  name: string;
  short: string;
}

export interface City {
  name: string;
  state: string;
}

@Injectable({
  providedIn: 'root'
})
export class RestaurantService {

  constructor(private httpClient: HttpClient) { }

  getRestaurants(state:string, city: string) {
    const params = new HttpParams().set('filter[address.state]', state).set('filter[address.city]', city);
    return this.httpClient.get<ResponseData<Restaurant>>(environment.apiUrl + '/restaurants', {params});
  }

  getStates() {
    return this.httpClient.get<ResponseData<State>>(environment.apiUrl + '/states');
  }

  getCities(state:string) {
    const params = new HttpParams().set('state', state);
    return this.httpClient.get<ResponseData<City>>(environment.apiUrl + '/cities', {params});
  }
}

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

import { Component, OnInit, OnDestroy } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { Subscription } from 'rxjs';

import { RestaurantService, ResponseData, State, City } from './restaurant.service';
import { Restaurant } from './restaurant';

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

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

  public restaurants: Data<Restaurant> = {
    value: [],
    isPending: false
  }

  public states: Data<State> = {
    isPending: false,
    value: []
  };

  public cities: Data<City> = {
    isPending: false,
    value: []
  }

  private subscription: Subscription;

  constructor(
    private restaurantService: RestaurantService,
    private fb: FormBuilder
    ) {
  }

  ngOnInit() {
    this.createForm();

    this.getStates();
  }

  ngOnDestroy() {
    if(this.subscription) {
      this.subscription.unsubscribe();
    }
  }

  createForm() {
    this.form = this.fb.group({
      state: {value: '', disabled: true},
      city: {value: '', disabled: true},
    });

    this.onChanges();
  }

  onChanges(): void {
    let state:string;
    const stateChanges = this.form.get('state').valueChanges.subscribe(val => {
      this.restaurants.value = [];
      if (val) {
        //only enable city if state has value
        this.form.get('city').enable({
          onlySelf: true,
          emitEvent: false
        });
        //if state has a value and has changed, clear previous city value
        if (state != val) {
          this.form.get('city').patchValue('');
        }
        //fetch cities based on state val
        this.getCities(val);
        state = val;
      }
      else {
        //disable city if no value
        this.form.get('city').disable({
          onlySelf: true,
          emitEvent: false
        });
        state = '';
      }
    });
    this.subscription = stateChanges;


    const cityChanges = this.form.get('city').valueChanges.subscribe(val => {
      if(val) {
        this.getRestaurants(state, val);
      }
    });
    this.subscription.add(cityChanges);
  }

  getStates() {
    this.restaurantService.getStates().subscribe((res: ResponseData<State>) => {
      this.states.value = res.data;
      this.states.isPending = false;
      this.form.get('state').enable();
    });
  }

  getCities(state:string) {
    this.cities.isPending = true;
    this.restaurantService.getCities(state).subscribe((res: ResponseData<City>) => {
      this.cities.value = res.data;
      this.cities.isPending = false;
      this.form.get('city').enable({
        onlySelf: true,
        emitEvent: false
      });
    });
  }

  getRestaurants(state: string, city:string) {
    this.restaurantService.getRestaurants(state, city).subscribe((res: ResponseData<Restaurant>) => {
      this.restaurants.value = res.data;
      this.restaurants.isPending = false;
    });
  }
}