简体   繁体   中英

Angular 2: Callback when ngFor has finished

In Angular 1 I have written a custom directive ("repeater-ready") to use with ng-repeat to invoke a callback method when the iteration has been completed:

if ($scope.$last === true)
{
    $timeout(() =>
    {
        $scope.$parent.$parent.$eval(someCallbackMethod);
    });
}

Usage in markup:

<li ng-repeat="item in vm.Items track by item.Identifier"
    repeater-ready="vm.CallThisWhenNgRepeatHasFinished()">

How can I achieve a similar functionality with ngFor in Angular 2?

You can use @ViewChildren for that purpose

@Component({
  selector: 'my-app',
  template: `
    <ul *ngIf="!isHidden">
      <li #allTheseThings *ngFor="let i of items; let last = last">{{i}}</li>
    </ul>

    <br>

    <button (click)="items.push('another')">Add Another</button>

    <button (click)="isHidden = !isHidden">{{isHidden ? 'Show' :  'Hide'}}</button>
  `,
})
export class App {
  items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];

  @ViewChildren('allTheseThings') things: QueryList<any>;

  ngAfterViewInit() {
    this.things.changes.subscribe(t => {
      this.ngForRendred();
    })
  }

  ngForRendred() {
    console.log('NgFor is Rendered');
  }
}

origional Answer is here https://stackoverflow.com/a/37088348/5700401

You can use something like this ( ngFor local variables ):

<li *ngFor="#item in Items; #last = last" [ready]="last ? false : true">

Then you can Intercept input property changes with a setter

  @Input()
  set ready(isReady: boolean) {
    if (isReady) someCallbackMethod();
  }

For me works in Angular2 using Typescript.

<li *ngFor="let item in Items; let last = last">
  ...
  <span *ngIf="last">{{ngForCallback()}}</span>
</li>

Then you can handle using this function

public ngForCallback() {
  ...
}

The solution is quite trivial. If you need to know when ngFor completes printing all the DOM elements to the browser window, do the following:

1. Add a placeholder

Add a placeholder for the content being printed:

<div *ngIf="!contentPrinted">Rendering content...</div>

2. Add a container

Create a container with display: none for the content. When all items are printed, do display: block . contentPrinted is a component flag property, which defaults to false :

<ul [class.visible]="contentPrinted"> ...items </ul>

3. Create a callback method

Add onContentPrinted() to the component, which disables itself after ngFor completes:

onContentPrinted() { this.contentPrinted = true; this.changeDetector.detectChanges(); }

And don't forget to use ChangeDetectorRef to avoid ExpressionChangedAfterItHasBeenCheckedError .

4. Use ngFor last value

Declare last variable on ngFor . Use it inside li to run a method when this item is the last one :

<li *ngFor="let item of items; let last = last"> ... <ng-container *ngIf="last && !contentPrinted"> {{ onContentPrinted() }} </ng-container> <li>

  • Use contentPrinted component flag property to run onContentPrinted() only once .
  • Use ng-container to make no impact on the layout.

Instead of [ready], use [attr.ready] like below

<li *ngFor="#item in Items; #last = last" [attr.ready]="last ? false : true">

I found in RC3 the accepted answer doesn't work. However, I have found a way to deal with this. For me, I need to know when ngFor has finished to run the MDL componentHandler to upgrade the components.

First you will need a directive.

upgradeComponents.directive.ts

import { Directive, ElementRef, Input } from '@angular/core';

declare var componentHandler : any;

@Directive({ selector: '[upgrade-components]' })
export class UpgradeComponentsDirective{

    @Input('upgrade-components')
    set upgradeComponents(upgrade : boolean){
        if(upgrade) componentHandler.upgradeAllRegistered();
    }
}

Next import this into your component and add it to the directives

import {UpgradeComponentsDirective} from './upgradeComponents.directive';

@Component({
    templateUrl: 'templates/mytemplate.html',
    directives: [UpgradeComponentsDirective]
})

Now in the HTML set the "upgrade-components" attribute to true.

 <div *ngFor='let item of items;let last=last' [upgrade-components]="last ? true : false">

When this attribute is set to true, it will run the method under the @Input() declaration. In my case it runs componentHandler.upgradeAllRegistered(). However, it could be used for anything of your choosing. By binding to the 'last' property of the ngFor statement, this will run when it is finished.

You will not need to use [attr.upgrade-components] even though this is not a native attribute due to it now being a bonafide directive.

I write a demo for this issue. The theory is based on the accepted answer but this answer is not complete because the li should be a custom component which can accept a ready input.

I write a complete demo for this issue.

Define a new component:

import {Component, Input, OnInit} from '@angular/core';

@Component({
  selector: 'app-li-ready',
  templateUrl: './li-ready.component.html',
  styleUrls: ['./li-ready.component.css']
})
export class LiReadyComponent implements OnInit {

  items: string[] = [];

  @Input() item;
  constructor() { }

  ngOnInit(): void {
    console.log('LiReadyComponent');
  }

  @Input()
  set ready(isReady: boolean) {
    if (isReady) {
      console.log('===isReady!');
    }
  }
}

template

{{item}}

usage in the app component

<app-li-ready *ngFor="let item of items;  let last1 = last;" [ready]="last1" [item]="item"></app-li-ready>

You will see the log in the console will print all the item string and then print the isReady.

I haven't yet looked in depth of how ngFor renders elements under the hood. But from observation, I've noticed it often tends to evaluate expressions more than once per each item it's iterating.

This causes any typescript method call made when checking ngFor 'last' variable to get, sometimes, triggered more than once.

To guarantee a one call to your typescript method by ngFor when it properly finishes iterating through items, you need to add a small protection against the multiple expression re-evaluation that ngFor does under the hood.

Here is one way to do it (via a directive), hope it helps:

The directive code

import { Directive, OnDestroy, Input, AfterViewInit } from '@angular/core';

@Directive({
  selector: '[callback]'
})
export class CallbackDirective implements AfterViewInit, OnDestroy {
  is_init:boolean = false;
  called:boolean = false;
  @Input('callback') callback:()=>any;

  constructor() { }

  ngAfterViewInit():void{
    this.is_init = true;
  }

  ngOnDestroy():void {
    this.is_init = false;
    this.called = false;
  }

  @Input('callback-condition') 
  set condition(value: any) {
      if (value==false || this.called) return;

      // in case callback-condition is set prior ngAfterViewInit is called
      if (!this.is_init) {
        setTimeout(()=>this.condition = value, 50);
        return;
      }

      if (this.callback) {
        this.callback();
        this.called = true;
      }
      else console.error("callback is null");

  }

}

After declaring the above directive in your module (assuming you know how to do so, if not, ask and I'll hopefully update this with a code snippet), here is how to use the directive with ngFor:

<li *ngFor="let item of some_list;let last = last;" [callback]="doSomething" [callback-condition]="last">{{item}}</li>

'doSomething' is the method name in your TypeScript file that you want to call when ngFor finishes iterating through items.

Note: 'doSomething' doesn't have brackets '()' here as we're just passing a reference to the typescript method and not actually calling it here.

And finally here is how 'doSomething' method looks like in your typescript file:

public doSomething=()=> {
    console.log("triggered from the directive's parent component when ngFor finishes iterating");
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM