Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Strange oddities to data access in a Angular 8 component

I'm developing a service that processes an xml file and returns a data structure of interfaces.
At first I thought that the service had correctly returned all the data, but then I realized some unclear things, in particular when I was going to read the data structure in a component.
This is my service:

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { AppConfig } from 'src/app/app.config';
import { forkJoin, Subscription } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class BibliographyParserService {

  private editionUrls = AppConfig.evtSettings.files.editionUrls || [];
  private bibliographicCitations: Array<BibliographicCitation> = [];
  private subscriptions: Array<Subscription> = [];

  constructor(
    private http: HttpClient,
  ) {
  }

  private getHttpCallsOBSStream() {
    return this.editionUrls.map((path) =>  this.http.get(path, { responseType: 'text'}));
  }

  public getBibliographicCitations(): Array<BibliographicCitation> {
    const parser = new DOMParser();
    this.subscriptions.push(forkJoin(this.getHttpCallsOBSStream()).subscribe((responses) => {
      responses.forEach(response => {
        Array.from(parser.parseFromString(response, 'text/xml').getElementsByTagName('bibl')).forEach(citation => {
          if (citation.getElementsByTagName('author').length === 0 &&
              citation.getElementsByTagName('title').length === 0 &&
              citation.getElementsByTagName('date').length === 0) {
            const interfacedCitation: BibliographicCitation = {
              title: citation.textContent.replace(/\s+/g, ' '),
            };
            if (!this.bibliographicCitations.includes(interfacedCitation)) { this.bibliographicCitations.push(interfacedCitation); }
          } else {
            const interfacedCitation: BibliographicCitation = {
              authors: citation.getElementsByTagName('author'),
              title: String(citation.getElementsByTagName('title')[0]).replace(/\s+/g, ' '),
              date: citation.getElementsByTagName('date')[0],
            };
            if (!this.bibliographicCitations.includes(interfacedCitation)) { this.bibliographicCitations.push(interfacedCitation); }
          }
        });
      });
    }));
    return this.bibliographicCitations;
  }
}

export interface BibliographicCitation {
  authors?: HTMLCollectionOf<Element>;
  title: string;
  date?: Element;
}

And this is my component:

import { Component, AfterViewInit } from '@angular/core';
import { BibliographyParserService } from 'src/app/services/xml-parsers/bibliography-parser.service';

@Component({
  selector: 'evt-bibliography',
  templateUrl: './bibliography.component.html',
  styleUrls: ['./bibliography.component.scss']
})
export class BibliographyComponent implements AfterViewInit{

  constructor(
    public bps: BibliographyParserService,
  ) {
    console.log(this.bps.getBibliographicCitations());         // WORKS, return the correct data structure
    this.bps.getBibliographicCitations().forEach(console.log); // DOESN'T RETURN ANYTHING!
    console.log(this.bps.getBibliographicCitations().length);  // RETURN 0
  }

  ngAfterViewInit() {
    (document.querySelectorAll('.cloosingRood')[0] as HTMLElement).onclick = () => {
      (document.querySelectorAll('.biblSpace')[0] as HTMLElement).style.display = 'none';
    };
  }
}

The very strange thing is with those three logs. We can see different things between them.
With the first log I can see the whole data structure in console.
With the second, nothing happens.
With the third, the length is equal to 0, which is not true because as shown in the first log, the data structure is full...!
I don't understand why these oddities. Is there anything I missed from the angular documentation?

PS: I don't want to make the subscription in the component, otherwise I would have already solved... I want to separate logic from visualization and create the data structure in the service, as I did.

like image 477
Memmo Avatar asked Dec 06 '19 17:12

Memmo


People also ask

What does !: Means in Angular?

Means safe navigation operator. From Docs. The Angular safe navigation operator (?.) is a fluent and convenient way to guard against null and undefined values in property paths. Here it is, protecting against a view render failure if the currentHero is null.


2 Answers

There are two problems here:

  1. You are mixing imperative and reactive programming.

You can in no way know when the forkJoin will emit. Therefore, you cannot be sure that the instance variable bibliographicCitations will have updated when you return from getBibliographicCitations. In could be synchronous or asynchronous. You need to make the method observable:

getBibliographicCitations(): Observable<Array<BibliographicCitation>>;

A simple way of doing this would be to refactor the method to just setup an Observable:

private refreshSub = new Subject<void>();
private bibliographicCitations$: Observable<BibliographicCitation[]>;

refresh(): void {
  this.refreshSub.next();
}

private buildObservables(): void {
  this.bibliographicCitations$ = this.refreshSub.pipe(
    switchMap(() => forkJoin(this.getHttpCallsOBSStream()),
      map(responses => {
        // Get all elements from response.
        const elements = responses.reduce((acc, response) => [
          ...acc,
          ...parser.parseFromString(response, 'text/xml').getElementsByTagName('bibl')
        ], [] as Element[]);

        // Use all elements to query for stuff.
        return elements.reduce((acc, element) => {
          if (['author', 'title', 'date'].every(tag => element.getElementsByTagName(tag).length === 0)) {
            return [...acc, { title: element.textContent.replace(/\s+/g, ' ') }];
          } else {
            return [...acc, {
              authors: element.getElementsByTagName('author'),
              title: `${element.getElementsByTagName('title')[0]}`.replace(/\s+/g, ' '),
              date: element.getElementsByTagName('date')[0],
            }];
          }
        }, [] as BibliographicCitation[]);
      })
    shareReplay(1)
  );
}

Then you can add a getter-method for that Observable in your service.

getBibliographicCitations(): Observable<Array<BibliographicCitation>> {
  return this.bibliographicCitations$;
}

And the refresh-method can be used to retrigger a read.

With all that in place, you can use the getBibliographicCitations inside the component and subscribe to it there. The key is that you should only subscribe when you're truly ready to use the value. Storing emissions from an observable is an anti-pattern.

  1. You are creating new subscriptions at every call to getBibliographicCitations

Every time you call your method getBibliographicCitations a new Subscription is created. That means that after calling it three times, there will be 3 subscriptions operating with their own DOMParser. And each one will modify the instance variable bibliographicCitations.

If you want to avoid duplicate subscriptions, you would have to unsubscribe on previous subscriptions before creating new ones. But, none of that would be necessary if you go with the code above and set up the Observable once.

like image 139
blid Avatar answered Oct 12 '22 23:10

blid


You don't have to subscribe into the service, but return an observable.
The way you create it is indifferent, the important thing is to respect the syntax, which for an angular novice is not easy, understandably!
In light of the answer given by blid, I propose this solution that mediates reactive and imperative programming, without destroying what you've done so far.
Obviously if someone is comfortable with the imperative programming to create certain data structures, he is free to do what he likes, but if you decide to use an angular environment, you need to know all the opportunities it offers!

Anyway... this is the service:

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { AppConfig } from 'src/app/app.config';
import { forkJoin, Observable } from 'rxjs';
import { shareReplay, map } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class BibliographyParserService {
  private editionUrls = AppConfig.evtSettings.files.editionUrls || [];
  private bibliographicCitations$: Observable<BibliographicCitation[]>;

  constructor(
    private http: HttpClient,
  ) {
    const parser = new DOMParser();
    const bibliographicCitations: Array<BibliographicCitation> = [];
    this.bibliographicCitations$ = forkJoin(this.getHttpCallsOBSStream()).pipe( //use pipe...
      map(responses => {                                                        //...and map
        responses.forEach(response => {
          Array.from(parser.parseFromString(response, 'text/xml').getElementsByTagName('bibl')).forEach(citation => {
            if (citation.getElementsByTagName('author').length === 0 &&
                citation.getElementsByTagName('title').length === 0 &&
                citation.getElementsByTagName('date').length === 0) {
              const interfacedCitation: BibliographicCitation = {
                title: citation.textContent.replace(/\s+/g, ' '),
              };
              if (!bibliographicCitations.includes(interfacedCitation)) { bibliographicCitations.push(interfacedCitation); }
            } else {
              const interfacedCitation: BibliographicCitation = {
                authors: citation.getElementsByTagName('author'),
                title: String(citation.getElementsByTagName('title')[0]).replace(/\s+/g, ' '),
                date: citation.getElementsByTagName('date')[0],
              };
              if (!bibliographicCitations.includes(interfacedCitation)) { bibliographicCitations.push(interfacedCitation); }
            }
          });
        });
        return bibliographicCitations; //This is the core!!!
        }),
      shareReplay(1)
    );
  }

  private getHttpCallsOBSStream() {
    return this.editionUrls.map((path) =>  this.http.get(path, { responseType: 'text'}));
  }

  public getBibliographicCitations(): Observable<Array<BibliographicCitation>> {
    return this.bibliographicCitations$;
  }
}

export interface BibliographicCitation {
  authors?: HTMLCollectionOf<Element>;
  title: string;
  date?: Element;
}

And this is an example of what you will do in a component:

constructor(
    public bps: BibliographyParserService,
  ) {
    this.bps.getBibliographicCitations().subscribe(response => {
        response.forEach(cit => {
            console.log(cit);
        });
    });
}
like image 21
Memmo Avatar answered Oct 13 '22 00:10

Memmo