3

I have the following parent component:

<h1>Vehicle Inventory</h1>

<p *ngIf="!vehicles"><em>No vehicle entries found</em></p>

<div *ngIf="vehicles">

    <ul class="nav nav-pills nav-fill">
        <li class="nav-item">
          <a class="nav-link" routerLink="/home/price" routerLinkActive="active">Search By price</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" routerLink="/home/make-model" routerLinkActive="active">Search By Make Or Model</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" routerLink="/home/engine-capacity" routerLinkActive="active">Search By Engine Capacity</a>
        </li>
        <li class="nav-item">
          <a class="nav-link" routerLink="/home/cylinder-variant" routerLinkActive="active">Search By Cylinder Variant</a>
        </li>
        <li class="nav-item">
            <a class="nav-link" routerLink="/home/cylinder-capacity" routerLinkActive="active">Search By Cylinder Capacity</a>
        </li>
    </ul>

  <router-outlet></router-outlet>
</div>

<table class="table" *ngIf="vehicles">
    <thead class="thead-dark">
      <tr>
        <th scope="col">Make</th>
        <th scope="col">Model</th>
        <th scope="col">Engine Capacity</th>
        <th scope="col">Cylinder Variant</th>
        <th scope="col">Top Speed</th>
        <th scope="col">Price (R)</th>
        <th scope="col">Cylinder Capacity</th>
        <th scope="col">Air Pressure/second</th>
      </tr>
    </thead>
    <tbody>
      <tr *ngFor="let vehicle of vehicles">
        <td>{{ vehicle.Make }}</td>
        <td>{{ vehicle.Model }}</td>
        <td>{{ vehicle.EngineCapacity }}</td>
        <td>{{ vehicle.CylinderVariant }}</td>
        <td>{{ vehicle.TopSpeed }}</td>
        <td>{{ vehicle.Price }}</td>
        <td>{{ vehicle.IndividualCylinderCapacity }}</td>
        <td>{{ vehicle.AirPressurePerSecond }}</td>
      </tr>
    </tbody>
  </table>

What can be viewed from the above is, I have some navigation going on here that will determine the child component being loaded into the <router-outlet>.

My child component emits an event via EventEmitter, as can be seen below:

import { Component, OnInit, Input, Output } from '@angular/core';
import { VehicleService } from '../../Services/VehicleService';
import { EventEmitter } from '@angular/core';
import { Vehicle } from '../../Models/Vehicle';

@Component({
  selector: 'app-search-cylinder-capacity',
  templateUrl: './search-cylinder-capacity.component.html',
  styleUrls: ['./search-cylinder-capacity.component.css']
})
export class SearchCylinderCapacityComponent implements OnInit {

  @Input() cylinderCapacity: any;
  @Output() dataNotifier: EventEmitter<Vehicle[]> = new EventEmitter();

  constructor(private service: VehicleService) { }

  ngOnInit() {
  }

  searchVehicle() {
    this.service
          .SearchVehiclesByCylinderCapacity(this.cylinderCapacity)
            .subscribe(response => this.dataNotifier.emit(response));

  }

}

How do I capture this event's response, so that my parent component's vehicle: Vehicle[] can be populated with the response of the event?

LHM
  • 721
  • 12
  • 31
monstertjie_za
  • 7,277
  • 8
  • 42
  • 73
  • I'd not introduce an additional service as described by vincecampanale for this behaviour. You already have a `VehicleService` which should be usable. How are you using the data emitted by `SearchVehiclesByCylinderCapacity`? – David Walschots May 19 '18 at 21:26
  • @DavidWalschots I think vincecampanale meant I should use my existing service. He just used the DateNotifierService as an example. I implemented it into my Service – monstertjie_za May 19 '18 at 21:29
  • Still I'm interested in your usage. You might not need these events. – David Walschots May 19 '18 at 21:31
  • @DavidWalschots Added an aswer, so you can see how I implemented this – monstertjie_za May 20 '18 at 06:44

3 Answers3

1

It isn't possible to listen to events from a router-outlet since it is just a placeholder. You can use a "shared service", as described in this answer: https://stackoverflow.com/a/41989983/5932590.

To mimic the dateNotifier event, a shared service might look something like this:

@Injectable()
export class DateNotifierService {
    private source = new Subject<Vehicle[]>();
    public event$ = this.source.asObservable();
    emit(eventData: Vehicle[]): void {
        this.source.next(eventData);
    }
}

You can then inject it in your child component and emit events:

export class SearchCylinderCapacityComponent {
  vehicles: Vehicle[];

  constructor(private dateNotifier: DateNotifierService) {}

  onClick(){
    this.dateNotifier.emit(this.vehicles);
  }
}

As well as inject it in your parent component and capture events:

export class ParentComponent {
  constructor(private dateNotifier: DateNotifierService) {
    dateNotifier.event$.subscribe(vehicles => console.log(vehicles));
  }
}
vince
  • 7,808
  • 3
  • 34
  • 41
  • I read with this example, that declaring an EventEmitter in a Singleton Service, is bad practice? – monstertjie_za May 19 '18 at 21:10
  • I still believe I will use this approach as it will work for my scenario, just trying to stick to good habbits – monstertjie_za May 19 '18 at 21:11
  • Ah yes, it looks like you'll need to provide it in your `app.module` or some higher level module to ensure it is a singleton. I would say it's not necessarily bad practice, but it's not ideal either. Unfortunately, it's your best bet given that `router-outlet` does not have this functionality. – vince May 19 '18 at 21:12
  • Thanks. Given the scenario, I think this will have to do ;-), was hoping for something different. – monstertjie_za May 19 '18 at 21:15
  • I know the feeling :) -- best of luck to you. – vince May 19 '18 at 21:44
1

This answer is based on the monstertjie_za's own answer to the question.

Given that it seems the VehicleService provides different ways of getting an array of vehicles. I'd expose the vehicles itself publicly.

@Injectable({
    providedIn: "root"
})
export class VehicleService {
    vehicles: Vehicle[] | undefined;

    constructor(private http: HttpClient, private configService: ConfigService) { }

    searchVehiclesByCylinderCapacity(cylinderCapacity: any): void {
        var finalEndPoint = this.configService.SearchVehiclesByCylinderCapacityEndpoint 
            + cylinderCapacity;
        this.makeRequest(finalEndPoint);
    }

    searchTop10ByAirPressure(airPressure: any): void {
        var finalEndPoint = this.configService.SearchVehiclesByAirPressureEndpoint 
            + airPressure;
        this.makeRequest(finalEndPoint);
    }

    private makeRequest(endpoint: string): void {
        this.http.get<Vehicle[]>(endpoint)
            .subscribe(vehicles => this.vehicles = vehicles);
    }
}

Then the child component only starts the invocation but doesn't actually do anything else:

@Component({
  selector: 'app-search-engine-capacity'
})
export class SearchEngineCapacityComponent {  
  constructor(private vehicleService: VehicleService) { }

  searchVehicle(): void {
      this.vehicleService.searchVehiclesByEngineCapacity(this.engineCapacity);
  }
}

And your HomeComponent simply exposes the service, which exposes the vehicles to use in your view:

@Component({
  selector: 'app-home',
})
export class HomeComponent implements OnInit {
  constructor(vehicleService: VehicleService) {}

  ngOnInit() {
    this.vehicleService.getAllVehicles();
  }
}
<table class="table" *ngIf="vehicleService.vehicles">
    <thead class="thead-dark">
      <tr>
        <th scope="col">Make</th>
        <th scope="col">Model</th>
        <th scope="col">Engine Capacity</th>
        <th scope="col">Cylinder Variant</th>
        <th scope="col">Top Speed</th>
        <th scope="col">Price (R)</th>
        <th scope="col">Cylinder Capacity</th>
        <th scope="col">Air Pressure/second</th>
      </tr>
    </thead>
    <tbody>
      <tr *ngFor="let vehicle of vehicleService.vehicles">
        <td>{{ vehicle.Make }}</td>
        <td>{{ vehicle.Model }}</td>
        <td>{{ vehicle.EngineCapacity }}</td>
        <td>{{ vehicle.CylinderVariant }}</td>
        <td>{{ vehicle.TopSpeed }}</td>
        <td>{{ vehicle.Price }}</td>
        <td>{{ vehicle.IndividualCylinderCapacity }}</td>
        <td>{{ vehicle.AirPressurePerSecond }}</td>
      </tr>
    </tbody>
</table>
David Walschots
  • 12,279
  • 5
  • 36
  • 59
  • Will the table in the parent component always update once the VehicleService.vehicles changes? There is no event telling it that it needs to update? If this will work, it might even be better to use, than using EventEmitter – monstertjie_za May 20 '18 at 07:48
  • Yes, it will. Note that even if you'd want an event-based solution. I'd not use the code you provided as an answer; because it makes each child component responsible for subscribing and then setting data in the service. It should be the service's own responsibility to manage its data. – David Walschots May 20 '18 at 07:51
  • Second note is that maybe it's best to modify your question, or add the information you gave in your answer to the question. It might become difficult for other readers to follow what is going on here :-). – David Walschots May 20 '18 at 07:52
  • 1
    Thanks. I will review, and accept your question :-) – monstertjie_za May 20 '18 at 07:54
0

Following what @vincecampanale mentioned, this is what I came up with:

In my VehicleService class, I have the following code for retrieving vehicles based on different searches. The VehicleService class also includes a dataChangedEvent: EventEmitter<Vehicle[]> which when emiited to, will hold the latest results for Vehicle.

Here is my VehicleService

import { Injectable, ErrorHandler, EventEmitter } from "@angular/core";
import { Observable, throwError } from 'rxjs';
import { catchError, retry } from 'rxjs/operators';

import { ConfigService } from "./ConfigService";
import { Vehicle } from "../Models/Vehicle";
import { HttpClient } from "@angular/common/http";
import { HttpResponse } from "selenium-webdriver/http";


@Injectable({
    providedIn: "root"
})
export class VehicleService {

    dataChangedEvent: EventEmitter<Vehicle[]> = new EventEmitter();

    constructor(private http: HttpClient, private configService: ConfigService){

    }

    SearchVehiclesByCylinderCapacity(cylinderCapacity: any): Observable<Vehicle[]> {
        var finalEndPoint = this.configService.SearchVehiclesByCylinderCapacityEndpoint + cylinderCapacity;
        return this.makeRequest(finalEndPoint);
    }
    SearchTop10ByAirPressure(airPressure: any): Observable<Vehicle[]> {
        var finalEndPoint = this.configService.SearchVehiclesByAirPressureEndpoint + airPressure;
        return this.makeRequest(finalEndPoint);
    }

    private makeRequest(endpoint: string): Observable<Vehicle[]> {
        return this.http.get<Vehicle[]>(endpoint);
    }
}

Each child component, will emit the data to the dataChangedEvent in the K, once they received a response in the subscribe() handler.

Here is a child component:

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

@Component({
  selector: 'app-search-engine-capacity',
  templateUrl: './search-engine-capacity.component.html',
  styleUrls: ['./search-engine-capacity.component.css']
})
export class SearchEngineCapacityComponent implements OnInit {

  engineCapacity: any;

  constructor(private vehicleService: VehicleService) { 

  }

  ngOnInit() {
  }

  searchVehicle(): void {
    this.vehicleService
          .SearchVehiclesByEngineCapacity(this.engineCapacity)
            .subscribe(response => this.vehicleService.dataChangedEvent.emit(response));
  }
}

Finally, in the parent component, I subscribe() to the dataChangedEvent in VehicleService, and the parent component will be notified when the data for searched vehicles changed. Below is my parent component that is sunscribing to the dataChangedEvent in VehicleService, from within the constructor

import { Component, OnInit, Input } from '@angular/core';
import { VehicleService } from '../../Services/VehicleService';
import { Vehicle } from '../../Models/Vehicle';

@Component({
  selector: 'app-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.css']
})
export class HomeComponent implements OnInit {

  vehicles: Vehicle[];

  constructor(private vehicleService: VehicleService) {
    this.vehicleService.dataChangedEvent.subscribe(response => this.vehicles = response)
   }

  ngOnInit() {
    this.vehicleService
      .GetAllVehicles()
        .subscribe(response => this.vehicles = response, 
                    error => console.log(error));
  }

  populateGrid(vehicleData: Vehicle[]){
    this.vehicles = vehicleData;
  }
}
monstertjie_za
  • 7,277
  • 8
  • 42
  • 73