4

I have an extreme need not to rebuild the DOM for an array of values, each of which holds an object representing a YouTube livestream. I am using ngFor to iterate out the livestreams.

However, when the livestreams change (this could be because the user wants to alter the viewing order, so the livestream they want to watch the most is on top - see reorder() below), ngFor rebuilds the elements, which causes YouTube embeds to be removed from the DOM, and reinserted; which results in the running livestreams to stop playing, and also reloads them - which takes a second or so. This is not optimal.

Here is a Plunkr of my issue

I have tried to use trackBy to follow the values by the name of the livestream (a property in my object) rather than the object itself, but this does not appear to have resolved the issue.

Template:

<livestream-player *ngFor="let livestream of userPrefs.visibleLivestreams(); trackBy: livestreamTrackByFn"        [video]="livestream.url | sanitize:'resource'">
</livestream-player>

Array of elements: userPrefs.visibleLivestreams()

public visibleLivestreams() : Livestream[] {

   let livestreams = [
       { name: 'a', url: 'youtubeUrl', ... }, 
       { name: 'b', url: 'youtubeUrl', ... },
       { name: 'c', url: 'youtubeUrl', ... }
   ];

   // _visibleLivestreams, used below to order and filter, 
   // is a simple, ordered, array of strings representing 
   // the livestream names, stored in localstorage.

   return livestreams
        .filter(l => this._visibleLivestreams.indexOf(l.name) != -1);
        .sort((a, b) => this._visibleLivestreams.indexOf(a.name) < this._visibleLivestreams.indexOf(b.name) ? -1 : 1);
}

Not-working trackBy functionality used in *ngFor template

public livestreamTrackByFn(index: number, item: Livestream) : string {
    return item.name;
}

Reorder functionality

/**
 * Reorders elements of the livestreams array by shifting off the
 * 0th element, and pushing it the last position in the array.
 * 
 * Calling this function results in the DOM being rebuilt!
 */
public rotate() : void {
    // shift element off _visibleLivestreams
    let elem = this.userPrefs.visibleLivestreams.shift();
    // push element onto _visibleStreams
    this.userPrefs.visibleLivestreams.push(elem);
}

As the above implementation rebuilds the DOM unnecessarily, I'm really looking for a workaround for this problem. My thoughts so far are creating a custom *ngFor-like structural directive, using flexbox to manage the order, or simply identifying a small change in my existing implementation which resolves this issue.

marked-down
  • 9,958
  • 22
  • 87
  • 150
  • http://stackoverflow.com/questions/7434230/how-to-prevent-an-iframe-from-reloading-when-moving-it-in-the-dom http://stackoverflow.com/questions/16333749/sort-list-without-reloading-iframe – yurzui Dec 05 '16 at 05:25

2 Answers2

0

That's because public visibleLivestreams() : Livestream[] { returns a new array every time it's called. trackBy can't help here.

Rather assign the result to a property and bind to this property instead.

constructor() { // or some other event that causes the result to change
    this.liveStreams = this.userPrefs.visibleLivestreams();
<livestream-player *ngFor="let livestream of userPrefs.visibleLivestreams"        [video]="livestream.url | sanitize:'resource'">

This is way more efficient because with your code change detection calls userPrefs.visibleLivestreams(); every time change detection runs, and that can be quite often.

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • 1
    Okay, but when the livestreams *do* change, and I need to redisplay them in a different order, I still have to reassign `this.liveStreams` which will cause the elements to be removed and re-added anyway, right? – marked-down Dec 04 '16 at 20:02
  • You can do that of course. Just reuse the previous array and remove and add elements depending to your needs instead of creating a new array. – Günter Zöchbauer Dec 04 '16 at 20:02
  • I am now `.sort`ing the array (a mutatable function that alters the original array in place) on change and *ngFor is still removing and adding the elements. Ideas? – marked-down Dec 04 '16 at 20:31
  • Can you reproduce in a Plunker. – Günter Zöchbauer Dec 04 '16 at 20:32
  • Sure! Click "rotate" to see the livestreams reload (except curiously the bottom one) https://embed.plnkr.co/sMSwU2wLFvKpBNgmNTJK/ It's that reload effect that I need to prevent while maintaining the ability to reorder. – marked-down Dec 04 '16 at 20:47
  • 1
    Thanks for the Plunker. Very helpful :) To me it doesn't look like an Angular2 issue. I don't know what actually causes it, but if you move the ` – Günter Zöchbauer Dec 05 '16 at 06:35
  • Looks like it may just be an inherent property of iframes in dynamic documents. The best thing I can think of as a solution is to use flexbox to achieve my result. Feel free to edit that into your answer and I'll accept :) Thanks for your help! – marked-down Dec 06 '16 at 01:57
  • I believe the reloading effect is caused when the URL is sanitized, which happens whenever the data changes (even if the ngFor is tracked correctly the sanitizer is still run against the url). To test this, you could take the sanitizer pipe out, put the iframe markup into a my-video component, and provide a sanitized url as part of the my-video initialization. This should ensure the URL is only sanitized once for each video component, and will hopefully stop the video from reloading. If successful you can then handle sanitizing the URL again only if it changes (or leave it only on init). – Griffin May 04 '17 at 12:58
  • I don't see how this would be caused by the sanitizer. If the data (`userPrefs.visibleLivestreams()`) doesn't change, the `sanitize` pipe won't be called. – Günter Zöchbauer May 04 '17 at 13:28
-1

When the page loads for the first time, ngFor loads the whole list. After that, if there are changes in the list in which you are looping at that time, only the updated item will be reflected.

skymook
  • 3,401
  • 35
  • 39