Search code examples
angulartypescriptrxjsangular-pipeswitchmap

RxJS: Modification a previous value further down the pipe chain


In my project, I came across an intriguing case of executing several requests in a pipe chain. It's all about displaying images in the quill text editor.

backend returns content in the form:

content: "<img alt="alt.png" id="1234567890"><p>content</p>"

As you can see, the src attribute is missing in the image. We need 2 extra shots to the api to get it. It is difficult because we operate on the id of the image that is sewn into string.

Incorrect solution for the purposes of understanding the essence only!

ngOnInit(): void {
    this.selectedTemplate
      .pipe(
        map(e => {
            const document = new DOMParser().parseFromString(e.content, 'text/html');
            document.querySelectorAll('img').forEach(img => {
              this._imageService.getImageToken(img.getAttribute('id')).pipe(
                switchMap(result => {
                    return this._imageService.getImageByToken(result.token);
                })).subscribe(result => {
                    img.setAttribute('src', result)
                });
            });
            return {
                ...e,
                content: document.body.innerHTML
            }
        }),
        tap(e => this.form.setValue({ id: e.id, content: e.content, title: e.name })),
      )
      .subscribe();
  }

I want all the data to be fetched before going to the tap operators. As it stands, the solution works, but it looks terrible. Any ideas how to improve this without touching the backend?


Solution

  • Here is a quick refactoring that I would perform.

    This extracts some of the bloated logic into its own function and also returns an observable rather than nesting subscriptions.

    Something to consider is that the imperative API for working with DOM Docs is a poor fit for Observables. You end up with a flow that's ordering effects on the document rather than transforming values.

    If this is a small project, that's fine. Otherwise I would refactor this further to separate all the mutations on the document into a separate logical unit. Something to consider.

    _setImgSources(document: Document): Observable<Document> {
      return merge(...
        [...document.querySelectorAll('img')].map(img => 
          this._imageService.getImageToken(img.getAttribute('id')).pipe(
            switchMap(imgToken => this._imageService.getImageByToken(imgToken.token)),
            tap(imgSrc => img.setAttribute('src', imgSrc)),
          )
        )
      ).pipe(
        ignoreElements(),
        endWith(document)
      );
    }
    
    ngOnInit(): void {
      this.selectedTemplate.pipe(
    
        switchMap(templ => 
          this._setImgSources(
            new DOMParser().parseFromString(templ.content, 'text/html')
          ).pipe(
            map(doc => ({
              ...templ, 
              content: doc.body.innerHTML
            }))
          )
        ),
    
      ).subscribe(({id, content, name}) => this.form.setValue({ 
        id, 
        content, 
        title: name 
      }));
    
    }