#Transmitting Data Between Two Child Components

29 messages · Page 1 of 1 (latest)

brave swallow
#

Hey Everyone. I'm new to these web dev frameworks in general and I was looking for some advice with this simple school project I am working on. I have a solution which I've already submitted but I'm not sure if the architecture is very good.

I just need to send an API request based on which part of an SVG someone clicks (the country in a world map) and update the UI based on the data received from that request.

I have a parent world component that has two child components: map and info-box. map is responsible for reporting the selected country. info-box displays the information based on the selected country.

map uses an output event to emit the country and process the data from a service. world binds the emitted country to the info-box and then info-box simply displays the content fine. I feel like this solution could use a lot of work though.

I don't like that map processes the data from the service. I don't know if all this input/output binding is efficient.

#

Here is the relevant code but do let me know if more would be helpful:

#

world.ts

import { Component } from '@angular/core';
import { Map } from './map/map';
import { InfoBox } from './info-box/info-box';
import { Colors } from './colors/colors';
import { CommonModule } from '@angular/common';
import { Country } from './wbapi/country';


@Component({
  selector: 'app-world',
  imports: [Map, InfoBox, CommonModule],
  template: `
<div>
  <!-- https://stackoverflow.com/questions/53066823/how-do-i-import-svg-from-file-to-a-component-in-angular-5 -->
  <div [ngStyle]="{ 'background-color': colors.background }" class="col" id="world-map">
    <app-map (countryEvent)="updateCountry($event)"></app-map>
  </div>
  <div class="col" id="info-box">
    <app-info-box [country]="country$"></app-info-box>
  </div>
</div>`,
  styleUrl: './world.css',
})
export class World {
  public colors = {
    ...Colors
  };
  public country$: Country | null = null;

  updateCountry(country: Country) {
    this.country$ = country;
  }
}
#

map.ts:

import { Component, OnInit, output, inject } from '@angular/core';
import { Colors } from '../colors/colors';
import { Wbapi } from '../wbapi/wbapi';
import { lastValueFrom } from 'rxjs';
import { Country } from '../wbapi/country';

@Component({
  selector: 'app-map',
  imports: [],
  templateUrl: './map.svg',
})
export class Map implements OnInit {
  public colors = {
    ...Colors
  };
  private wbapi = inject(Wbapi);
  countryEvent = output<Country>();

  constructor() { }

  ngOnInit(): void {
    document.querySelectorAll<SVGPathElement>('app-map path').forEach((path) => {
      path.style.fill = this.colors.country;
      path.addEventListener('click', async () => {
        path.style.fill = this.colors.click;
        let data = await lastValueFrom(this.wbapi.lookup(path.id));
        data = data[1][0];
        this.countryEvent.emit({
          name: data.name,
          latitude: data.latitude,
          longitude: data.longitude,
          incomeLevel: data.incomeLevel.value,
          region: data.region.value,
          capital: data.capitalCity
        });
      });
      path.addEventListener('mouseover', () => {
        path.style.strokeWidth = '1px';
        path.style.stroke = this.colors.outline;
      });
      path.addEventListener('mouseleave', () => {
        path.style.strokeWidth = '0px';
        path.style.fill = this.colors.country;
      })
    });
  }
}
#

info-box.ts:

import { Component, input } from '@angular/core';
import { Country } from '../wbapi/country';

@Component({
  selector: 'app-info-box',
  imports: [],
  template: `
@if (country()) {
  <div class="col" id="info-box">
    <div class="info-box-title">Info Box</div>
    <div class="term-definition">
      <p class="term">Name:</p>
      <p class="definition">{{ country()!.name }}</p>
    </div>
    <div class="term-definition">
      <p class="term">Capital:</p>
      <p class="definition">{{ country()!.capital }}</p>
    </div>
    <div class="term-definition">
      <p class="term">Region:</p>
      <p class="definition">{{ country()!.region }}</p>
    </div>
    <div class="term-definition">
      <p class="term">Income Level:</p>
      <p class="definition">{{ country()!.incomeLevel }}</p>
    </div>
    <div class="term-definition">
      <p class="term">Latitude:</p>
      <p class="definition">{{ country()!.latitude }}</p>
    </div>
    <div class="term-definition">
      <p class="term">Longitude:</p>
      <p class="definition">{{ country()!.longitude }}</p>
    </div>
  </div>
}`,
  styleUrl: './info-box.css',
})
export class InfoBox {
  country = input.required<Country | null>();
}
rapid ether
# brave swallow Hey Everyone. I'm new to these web dev frameworks in general and I was looking f...

It's not a performance problem, but you basically have 2 alternative approaches here (that are both defined by "Where do I put the request"):

  1. Have your request in world. That way only world does the service interactions and shares the resulting data with map and info-box. In this scenario world basically "manages" everything while map is just displaying stuff and reporting user events (That's what's called a Dumb Component)

  2. Have your request in service. Basically in your service, have the observable that results from making the request publicly exposed. That way anyone that injects the service can access the same data.
    Given how RXJS and cold observables work though, you'll need to be careful and use the rxjs operator shareReplay(1) or something like it. Otherwise, everytime somebody accesses the observable will fire another HTTP request.

That last one could look somewhat like this:

@Injectable({...})
export class MyService {
  private readonly httpClient = inject(HttpClient);

  private readonly loadTriggered$ = new Subject<ParamsForRequest>();
  public readonly myData$ = this.loadTriggered$.pipe(
    switchMap(params => this.loadMyData(params)),
    shareReplay(1),
  );

  loadMyData(params: ParamsForRequest){
    return this.httpClient... the rest of the request ...;
  }
}
#

Note that the second example looks a lot nicer if you use httpresource instead of doing it in rxjs, but httpresource is experimental still

brave swallow
# rapid ether Note that the second example looks a **lot** nicer if you use httpresource inste...

Yeah. That was the main reason why I was unsure about utilizing it at this point.

As for your first suggestion, I did have world manage the requests earlier but then I had an error where the displayed data was always from 1 request ago. adding a manual changedetectoref update like so fixed it tho:

import { ChangeDetectorRef, Component, inject, OnInit } from '@angular/core';
import { Map } from './map/map';
import { Wbapi } from './wbapi/wbapi';
import { Colors } from './colors/colors';
import { AsyncPipe, CommonModule } from '@angular/common';
import { Country } from './wbapi/country';
import { lastValueFrom } from 'rxjs';


@Component({
  selector: 'app-world',
  imports: [Map, CommonModule, AsyncPipe],
  template: `
<div>
  <div [ngStyle]="{ 'background-color': colors.background }" class="col" id="world-map">
    <app-map (countryCodeEvent)="updateCountry($event)"></app-map>
  </div>
  @if (country$; as country) {
<!-- Excluded for brevity -->
  }
</div>`,
  styleUrl: './world.css',
})
export class World {
  public colors = {
    ...Colors
  };
  private wbapi = inject(Wbapi);
  public country$!: Country | null;

  constructor(private cd: ChangeDetectorRef) { }

  async updateCountry(countryCode: string) {
    let data = await lastValueFrom(this.wbapi.lookup(countryCode));
    data = data[1][0];
    this.country$ = {
      name: data.name,
      latitude: data.latitude,
      longitude: data.longitude,
      incomeLevel: data.incomeLevel.value,
      region: data.region.value,
      capital: data.capitalCity
    };
    this.cd.detectChanges();
  }
}

this is obv not ideal.

rapid ether
#

Do not use lastValueFrom or await, like, ever

#

(Okay there obviously are exceptions but you are not in such an exceptional scenario)

brave swallow
rapid ether
#

Also yuo want to work declarative. Basically, your component should define the data you want inside its signals and observables, then you access them in the template and just display stuff.

rapid ether
brave swallow
rapid ether
#

What world should look like would be this:

export class World {
  private readonly wbapi = inject(Wbapi);
  private readonly countryCodeEvent$ = new Subject<string>();
  private readonly country$ = this.countryCodeEvent$.pipe(
    switchMap(countryCode => this.wbapi.lookup(countrycode))
  );
  public readonly country = toSignal(this.country$);

  updateCountry(countryCode: string){
    this.countryCodeEvent$.next(countryCode);
  }
}

Now you can access country wherever

brave swallow
#

oh. Thank you! I'm going to marvel at this for a little while.

#

So, in these declarative js frameworks, async/await is generally discouraged because it messes with the event loop/lifecycle stuff?

That might be an oversimplification, I suppose.

rapid ether
#

The general idea is that if you have an event, you want to somehow get it in an observable shape as that observable will fire every time that event happens and execute all the code that depends on that observable value.
If you want to trigger something every time an event happens, you want to subscribe to that observable.
If you want to "derive" a value from an observable, you use pipe.

In your case you have the event "countryCodeEvent" (Better name: "countryCodeChanged"). So we push that into a subject so that anything that depends on it can update itself.

Okay, based on whatever the current countryCode is we want to fetch the country, right? So that means our country depends on countryCode, so we derive it from that.
To do the fetching we need to do an http request though, which means we product an observable (from the http request) every time a countryCode event happens.
To deal with that, we need rxjs operators such as exhaustMap, switchMap, concatMap etc., which all lead to different behavior for any open http request that exists when a second event arrives.
switchMap: Any HTTP Request still running from the previous event will be canceled
exhaustMap: If there is an HTTP Request still running from the previous event, the new event gets ignored
concatMap: If there is an http request still running, it'll first wait for that to complete before firing an http request for the next event

rapid ether
#

Angular is designed to represent that scenario with observables (or signals, but this specific scenario has an easier time with observables imo)

brave swallow
#

Ah. Makes sense. Thank you for your explanation! I will need to look a bit more into how exactly observables work/some examples but this is a great starting point.

rapid ether
#

You basically want to very often ask yourself the question of "Who depends on what and how do I derive my data from that"

dense pivot
#

is it still better than going pure signals?

rapid ether
# dense pivot may I know why private readonly countryCodeEvent$ = new Subject<string>();?

You mean by Subject instead of signal?
2 reasons:

  1. Signals do not have an equivalent to switchMap/exhaustMap/concatMap.
    Or if you're a bit broader in your interpretation, they sorta do in the form of http-resource but that has limitations and is a tad more cumbersome to deal with.

Trying to build something with similar behavior is... pretty meh in signal world.
So when you need to do something where you regularly fire an http-request based on events, you'll tend to shift towards observables.

  1. Signals do not represent events very well. They represent state. They do not have the concept of an event. Here we explicitly care about an event happening and then reacting to that.
    Signals when trying to model the same can have funky edge-cases because their mental model does not well represent events happening.
dense pivot
#

got it. thank you for that potatoadmire

normal citrus
#

Shared service with 2 functions. One is set an observable, the other returns it. So you can subscribe to it anywhere.

rapid ether
#

Please don't set an observable, update it by pushing a new value into a subject somewhere from which you derive your observable.
If you replace the entire observable that means you'll only get an update through it by fetching the current observable from the service

rapid ether
#

Observables basically should only ever be set once and be readonly