Home > front end >  Pipe never gets executed when server response with new data
Pipe never gets executed when server response with new data

Time:04-09

I created a subscription in my template to watch for changes to an object. The initial loading of the object displays the correct data for the property tags, when I add an item to the data it goes to a web server and returns a list of all the tags that are attached to the item (to keep the item in sync with the server). However, the newly added item isn't reflected on the page. I am not 100% sure why. I think it is because my of() statement but I am not sure. What I am seeing is that zip().pipe() never gets executed.

Do I need to use something other than of?

Note: I am trying to follow the declarative pattern to eliminate the usage of .subscribe()

Sub-Note: Once I get it working I plan on trying to remove the subscribe on this line this.server.file().subscribe

Stackblitz

export interface FileInfo {
  tags: string[];
}

@Component({
  selector: 'my-app',
  template: `
  <input #tag /><button (click)="addTag(tag.value)">Add Tag</button>

  <div *ngIf="data$ | async as data">
    <div *ngFor="let tag of data.tags">{{ tag }}</div>
  </div>
  `,
})
export class AppComponent {
  data$ = new Observable<FileInfo>();

  constructor(
    // Used to mimic server responses
    private readonly server: WebServer
  ) {}

  ngOnInit() {
    // I plan on removing this subscribe once I get a grasp on this
    this.server.file().subscribe((img) => {
      this.data$ = of(img);
    });
  }

  addTag(newTag: string) {
    const data$ = this.server.save(newTag);
    this.data$.pipe(concatMap((i) => this.zip(data$)));
  }

  private zip(tags$: Observable<string[]>) {
    return zip(this.data$, tags$).pipe(
      tap((i) => console.log('zipping', i)),
      map(([img, tags]) => ({ ...img, tags } as FileInfo))
    );
  }
}

CodePudding user response:

You're missusing the observable. After you subscribe with it, in the template with the async pipe, you should not update it's reference.

If you need to update the data, you must use a Subject.

export class AppComponent {
  private readonly data = new BehaviorSubject<FileInfo>(null);
  data$ = this.data.asObservable();

  constructor(
    // Used to mimic server responses
    private readonly server: WebServer
  ) {}

  ngOnInit() {
    this.server.file().subscribe((result) => this.data.next(result));
  }

  addTag(newTag: string) {
    this.server
      .save(newTag)
      .subscribe((tags) => this.data.next({ ...this.data.value, tags }));
  }
}

Also, your service could be a lot simpler:

@Injectable({ providedIn: 'root' })
export class WebServer {
  private readonly tags = ['dog', 'cat'];

  file(): Observable<FileInfo> {
    return of({ tags: this.tags });
  }

  save(tag: string) {
    this.tags.push(tag);
    return of(this.tags);
  }
}

Here's the working code:

https://stackblitz.com/edit/angular-ivy-my3wlu?file=src/app/app.component.ts

CodePudding user response:

Try completely converting webserver.service.ts to provide an observables of tags and FileInfo like this:

import { Injectable } from '@angular/core';
import { concat, Observable, of, Subject } from 'rxjs';
import { delay, map, shareReplay, tap } from 'rxjs/operators';
import { FileInfo } from './app.component'; // best practice is to move this to its own file, btw

@Injectable({ providedIn: 'root' })
export class WebServer {
  private fakeServerTagArray = ['dog', 'cat'];
  private readonly initialTags$ = of(this.fakeServerTagArray);
  private readonly tagToSave$: Subject<string> = new Subject();

  public readonly tags$: Observable<string[]> = concat(
    this.initialTags$,
    this.tagToSave$.pipe(
      tap(this.fakeServerTagArray.push),
      delay(100),
      map(() => this.fakeServerTagArray),
      shareReplay(1) // performant if more than one thing might listen, useless if only one thing listens
    )
  );

  public readonly file$: Observable<FileInfo> = this.tags$.pipe(
    map(tags => ({tags})),
    shareReplay(1) // performant if more than one thing might listen, useless if only one thing listens
  );

  save(tag: string): void {
    this.tagToSave$.next(tag);
  }
}

and now your AppComponent can just be

@Component({
  selector: 'my-app',
  template: `
  <input #tag /><button (click)="addTag(tag.value)">Add Tag</button>

  <div *ngIf="server.file$ | async as data">
    <div *ngFor="let tag of data.tags">{{ tag }}</div>
  </div>
  `,
})
export class AppComponent {
  constructor(
    private readonly server: WebServer;
  ) {}

  addTag(newTag: string) {
    this.server.save(newTag);
  }
}

Caveat: If you ever call WebServer.save while WebServer.tags$ or downstream isn't subscribed to, nothing will happen. In your case, no big deal, because the | async in your template subscribes. But if you ever split it up so that saving a tag is in a different component, the service will need to be slightly modified to ensure that the "save new tag" server API call is still made.

  • Related