0

I am trying to display images from a list of browser File objects. The template has a table with the the following tr element in it:

      <tr *ngFor="let file of files">

        <td>{{ file.name }}</td>
        <td>{{ file.type }}</td>
        <td>{{ file.size | number }}</td>
        <td>
          <ng-container *ngIf="getFile(file) | async as dataURL">
            <img src="{{ dataURL }}">
          </ng-container>
        </td>
        <td>
          <button type="button" mat-button (click)="clickRemove(file)">Remove</button>
        </td>

      </tr>

In the component, the getFile() function returns a simple Observable like this:

  getFile(file: File): Observable<string> {
    console.log('getFile called for a new Observable', file.name);
    const obs = new Observable<string>(
      subscriber => {
        const reader = new FileReader();
        console.log('getFile got subscribed to!');
        reader.addEventListener("load", ev => {
          console.log('getFile got the load event!', reader.result?.slice(0,200));
          subscriber.next(reader.result as string);
        });
        reader.readAsDataURL(file);
      }
    )
    return obs;
  }

The intent is that when the Observable is subscribed to, it will go read the data blob represented by the File, then return a string as a data URL for the img element to display. The console.log statements show that 1) it is getting subscribed to, 2) it gets the event from the FileReader then the result is returned in the subscriber.next() call.

What happens is that this loops indefinitely. Even with one row in the table I get infinitely repeated calls to getFile() for the same File, then a subscription to the returned Observable, then the file gets read again and a new DataURL returned.

Can anyone help me out by pointing out where the loop is? I suspect the problem is that the Angular's change detection logic is doing this, but I don't have any idea of how to diagnose that or fix it if that is the case.

Any help appreciated.

AlanObject
  • 9,613
  • 19
  • 86
  • 142
  • Don't call functions in template. Create a pipe instead. – developer033 Jul 21 '21 at 02:22
  • 2
    You are seeing change detection being triggered. Angular doesn’t know the value of the result of getFile() until it runs. Instead I would recommend doing the work of getFile when the list of files is created and map it to some parallel array or mapping it to an extended file object that has this dataUrl property in it. Here is a similar issue https://stackoverflow.com/questions/43081810/angular2-infinite-loop-when-i-call-method-from-a-angular-2-class-inside-templa – Ben L Jul 21 '21 at 02:26

1 Answers1

0

What does *ngIf do?

In layman's terms, the expression is evaluated in order to determine if the element should be inserted into the DOM. Every time angular "suspects" a possible change of the underlying values, the expression is evaluated again.

Short: The ngIf-expression will be evaluated more than once.

This creates your torrent of DataURL.

Maybe simply use...

<img [src]="getFile(file) | async">

... although it is not exactly the same as your example. The difference being that the img element is always inserted into your DOM, no matter the result of the (pending) file read.

An image without a valid src will show the alt-attribute text, if any. So the solution proposed by me has to be tweaked if you have special preferences for that.