Angular 2: Callback when ngFor has finished
Asked Answered
J

8

40

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?

Jiujitsu answered 5/3, 2016 at 20:9 Comment(6)
I'm having difficulty imagining when this would be useful. Why do you want to do this? Maybe there's another way to solve your real problem.Sybaris
This might help, angular.io/docs/ts/latest/api/common/NgFor-directive.html -- "NgFor provides several exported values that can be aliased to local variables:" one of them is "last". But I agree that it sounds like the wrong solution.Abubekr
The reason is a custom directive for a dropdown using Sematic-UI. I have to invoke a method from the Semantic API do make the input a dropdown, but this must be done after ngFor has looped through all elements.Jiujitsu
Hello, Tobias, have you solved this problem somehow? We have same problems, when i need to initialize scrollbars after new items will appear in ngFor list.Oireachtas
Same problem here...Kuwait
Possible duplicate of execute a function when *ngFor finished in angular 2Szymanski
A
22

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();
  }
Acnode answered 6/3, 2016 at 12:4 Comment(8)
At least, this won't work with a li element, but with a custom component that gets nested inside.Jiujitsu
i think code should be like this ` *ngFor="#item of Items;` is't it ? btw code throws error of ready is not know native property blabla...Yuille
need to add a new component to do this work. see this demo: github.com/xmeng1/ngfor-finish-callbackMayfield
Works with this: <li *ngFor="item in Items; last = last" [attr.ready]="last ? false : true">Transferor
this is not worked as all other say that , template parsing erro for using in <li>Sabba
There's a more complete answer here: https://mcmap.net/q/67807/-execute-a-function-when-ngfor-finished-in-angular-2Marti
template parse error, m using it on select - optionKingwood
fine solution:)Damara
S
77

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://mcmap.net/q/67807/-execute-a-function-when-ngfor-finished-in-angular-2

Swot answered 28/2, 2018 at 8:28 Comment(5)
Works great. Thank you!Impersonal
This should be the accepted answer because it handles the question of the view's state properly using what Angular has provided. Everything else is just a hack.Consecutive
Amazing solution!Enamor
best solution for this problem.Tacnaarica
This causes ExpressionChangedAfterItHasBeenCheckedErrorDromond
A
22

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();
  }
Acnode answered 6/3, 2016 at 12:4 Comment(8)
At least, this won't work with a li element, but with a custom component that gets nested inside.Jiujitsu
i think code should be like this ` *ngFor="#item of Items;` is't it ? btw code throws error of ready is not know native property blabla...Yuille
need to add a new component to do this work. see this demo: github.com/xmeng1/ngfor-finish-callbackMayfield
Works with this: <li *ngFor="item in Items; last = last" [attr.ready]="last ? false : true">Transferor
this is not worked as all other say that , template parsing erro for using in <li>Sabba
There's a more complete answer here: https://mcmap.net/q/67807/-execute-a-function-when-ngfor-finished-in-angular-2Marti
template parse error, m using it on select - optionKingwood
fine solution:)Damara
G
8

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() {
  ...
}
Gormandize answered 7/1, 2017 at 14:31 Comment(8)
For me, this gets executed like a million timesTera
Hey Alex, you are right, this function will be executed by Items.lenght times. This is not a very good approach, works for small and quick things.Gormandize
No it wont be executed items.length times. It will go on for ever lolCharlton
Dont use this solution please, unless your ChangeDetectionStrategy is set to OnPush.Roomer
@MarcoNoronha I test it and it doesn't go for foreverGormandize
@Roomer then what is the best solution for this?Gormandize
You could use a pipe, as that doesnt trigger change detection, that or set the strategy to OnPush. The pipe would be something like: <ele *ngFor="let item of items; let i = last">{{ i | lastElementTrigger:optionalParameters }}</ele>Roomer
Using a filter on span would not it be a better alternative?Smaragd
C
8

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.
Cockcrow answered 12/7, 2018 at 13:49 Comment(1)
'Trivial', but needs 4 steps to be explainedPusher
J
4

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

 <li *ngFor="#item in Items; #last = last" [attr.ready]="last ? false : true">
Jankell answered 10/6, 2016 at 13:6 Comment(0)
G
3

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.

Gilbertegilbertian answered 27/6, 2016 at 4:54 Comment(0)
M
1

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.

Mayfield answered 12/5, 2017 at 10:33 Comment(0)
L
0

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");
}
Linguistics answered 24/6, 2017 at 3:3 Comment(1)
to me it looks like the best and most clean of all answersParricide

© 2022 - 2024 — McMap. All rights reserved.