javascript - Hur man på rätt sätt räknar om en boolean varje gång det observerbara inputvärdet ändras?


Jag har en komponent till vilken jag skickar ett objekt som dess ingång.

Det objektet kommer från ett API, så det är inte tillgängligt direkt. Och objektet som returneras från API:et kan också vara tomt.

I mallen vill jag dölja värdet om det inte är ett tomt objekt.

Så jag har detta i min komponent:

import { Component, Input, OnChanges, SimpleChanges } from "@angular/core";
import { BehaviorSubject, Observable, of } from "rxjs";
import { tap } from "rxjs/operators";
import { not, isEmpty } from "ramda";

@Component({
  selector: "hello",
  template: `
    {{ hasName | async }} {{ name | async }}
    <h1 *ngIf="(hasName | async)">Hello {{ name | async }}!</h1>
  `,
  styles: [
    `
      h1 {
        font-family: Lato;
      }
    `
  ]
})
export class HelloComponent {
  @Input() name: Observable<{} | { first: string }> = of({});
  hasName: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);

  ngOnInit() {
    this.name.pipe(
      tap(name => console.log(name)),
      // the actual logic to calculate the boolean is quite involved, but I  
      // have simplified it here for brevity.
      tap(name => this.hasName.next(not(isEmpty(name)))) 
    );
  }
}

Problemet är att console.log i tapen aldrig skrivs ut och den andra tapen aldrig körs heller, så värdet för hasName är alltid false som satt i slutet av klassen.

Jag tror att det som händer är att i OnInit är this.name fortfarande of({}), och inte det faktiska observerbara värdet som skickas in genom mallen från föräldern.

Jag använder of({}) eftersom att skriva this.name som Observable<{} | { first: string}> | undefined leder till mer boilerplate i klassen för att kontrollera att det verkligen är definierat innan det används.

Hur kan jag få detta att fungera på ett idiomatiskt sätt?

Komplett Stackblitz: https://stackblitz.com/edit/angular-ivy-gxqh2d?devtoolsheight=33&file=src/app/hello.component.ts

Svar #1

Du kan förenkla den här koden mycket.

För det första, TS-definitionen.

{} | { first: string }

Kan göras som

{ first?: string }

Även om det kan se ut som att jag gör någon snygg plockning här, gör jag det egentligen inte. Med det första alternativet, om du försöker komma åt variable.first, får du ett fel som säger att det inte finns eftersom unionen inte kan garantera detta. Med det senare är det okej.

Därefter, mocken. När du arbetar med observables, försök att inte tilldela observable på nytt. Här är hur du kan göra det du gjorde utan att tilldela på nytt:

  public name$: Observable<{ first?: string }> = concat(
    of({}),
    interval(2000).pipe(
      map(() => ({
        first: `${Math.random()}`
      }))
    )
  );

Slutligen, barnkomponenten. Att skicka observables som input är en kodlukt i 99% av fallen. Här är allt du vill ha en presentational komponent (även kallad dumb komponent).

Den här dumma komponenten borde inte ha någon logik (så mycket som möjligt åtminstone), och här är vad du kan göra:

type Nil = null | undefined

@Component({
  selector: "hello",
  template: `
    <h1 *ngIf="name?.first">Hello {{ name.first }}!</h1>
  `
})
export class HelloComponent {
  @Input() name: { first?: string } | Nil;
}

Vilket betyder att när du kallar detta från föräldern, gör du bara:

<hello [name]="name$ | async"></hello>

Här är en uppdaterad stackblitz: https://stackblitz.com/edit/angular-ivy-hnv23a?file=src%2Fapp%2Fapp.component.ts

Svar #2

Om du vill reagera när inmatningsvärdet ändras kan du skriva din egen setter:

export class HelloComponent {
  @Input() set name(name: Observable<{} | { first: string }>) {
    this.name$ = name.pipe(
      tap(name => console.log(name)),
      tap(name => this.hasName.next(not(isEmpty(name))))
    );
  }

  name$: Observable<{} | { first: string }>;

  ...
}

Bind sedan i mallar async till name$ istället.

Uppdaterad demo: https://stackblitz.com/edit/angular-ivy-7u7xkd?file=src%2Fapp%2Fhello.component.ts