47

I trying to create a application with angular 2,i want when last element rendered in *ngFor,execute a function, somthing like this :

<ul>
  <li *ngFor="#i of items">{{i.text}}</li> <==== i want when this completed, execute a functuon in my class
</ul>

Thanks.

  • what you actually want to last element of *ngFor ? do you want to perfrom some action like *ngIf on last element ? – Pardeep Jain May 07 '16 at 11:27
  • 1
    i want call slider function...i tried ngAfterViewinit or something like this but dont worked...when i use setTimeout() it is worked but is not a good idea... what do you think i should do? –  May 07 '16 at 11:38
  • ohh no setTimeOut is not an good idea, really interesting question just wait ill try and give you answer within few minutes. – Pardeep Jain May 07 '16 at 11:44
  • Do you want to call your method only aftwr the first time then list is rendered? Or should it be called again if, for example, you add/remove an item in the list? – mcgraphix May 07 '16 at 12:04

7 Answers7

78

Update

You can use @ViewChildren for that purpose

There are three cases

1. on initialization ngFor element is not rendred due to ngIf on it or it's parent

  • in which case, whenver ngIf becomes truthy, you will be notified by the ViewChildren subscription

2. on initialization ngFor element is rendred regardless of ngIf on it or it's parent

  • in which case ViewChildren subscription will not notify you for the first time, but you can be sure it's rendered in the ngAfterViewInit hook

3. items are added/removed to/from the ngFor Array

  • in which case also ViewChildren subscription will notify you

[Plunker Demo] (see console log there)

@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.ngForRendered();
    })
  }

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

Original

You can do it like this ( but see the side Effects for yourself )

<ul>
  <li *ngFor="let i of items; let last = last">{{i}} {{last ? callFunction(i) : ''}}</li>
</ul>

Which is Useless, unless used with changeDetectionStrategy.OnPush

Then you can have control over how many times change detection occurs, hence how many times your function is called.

i.e: You can trigger next changeDetection when the data of items changes, and your function will give proper indication that ngFor is rendered for real change.

Dane Brouwer
  • 2,827
  • 1
  • 22
  • 30
Ankit Singh
  • 24,525
  • 11
  • 66
  • 89
  • 1
    this answer is better as it does not create a `span` – smnbbrv May 07 '16 at 12:10
  • @smnbbrv haha no need to create extra `span` in my answer too, i have created just for trying something new :p but did't got successful. – Pardeep Jain May 07 '16 at 12:12
  • but you'll have to take care of the function calls, it's going to be called every time change detection ticks – Ankit Singh May 07 '16 at 12:12
  • @PardeepJain yep, I know :) Still the answer is the answer. And still the function is going to be called so many times... I don't really think the author's intention is a good way – smnbbrv May 07 '16 at 12:13
  • yups @smnbbrv totally agreed with your point. even i am shocked why function called too many times trying to figure out whats the reason, when found ill update my answer. – Pardeep Jain May 07 '16 at 12:15
  • @A_Singh did you know why change detection runs 8 times ,i mean alert poping up 8 times why ? – Pardeep Jain May 07 '16 at 12:17
  • 1
    because recursion maybe, ngFor renders and at last calls the function, that ticks change detection, which triggers ngFor to render again and call the function again, and so on. I'm not sure why it would stop after 8 – Ankit Singh May 07 '16 at 12:20
  • me niether, let's just wait for Gunter or Thierry's answer – Ankit Singh May 07 '16 at 12:25
  • haha exactly well said @A_Singh waiting for angular2 king's answer. – Pardeep Jain May 07 '16 at 12:40
  • why this function always repeat????? i test it with an alert and i get in a loop...and my function repeat!!!! –  May 07 '16 at 12:43
  • read the comments [on this anwser](http://stackoverflow.com/a/37088348/5612697) you will understand – Ankit Singh May 07 '16 at 12:47
  • One simple but shabby work-around is to use a boolean to determine whether to run your function. It will still check the bool a million times but the cost of that is minute by comparison. In my case, I just set it to false and left it since I only needed to run the function once upon completion of the *ngFor – Methodician Jul 05 '16 at 22:44
  • Requesting an up to date and more complete example for future viewers. – DarkNeuron May 10 '17 at 10:01
  • Thanks for the updated version Singh. It works perfectly, and doesn't feel hacky. – DarkNeuron Jun 15 '17 at 12:11
  • Glad, that I could help :) – Ankit Singh Jun 15 '17 at 12:26
9

I used a slight hack of the approach others have complained about and it worked like a charm:

<ul>
  <li *ngFor="let i of items; let last = last">{{i}} {{last ? callFunction(i) : ''}}</li>
</ul>

Then in your component:

shouldDoIt = true; // initialize it to true for the first run

callFunction(stuff) {
    if (this.shouldDoIt) {
        // Do all the things with the stuff
        this.shouldDoIt = false; // set it to false until you need to trigger again
    }
}

This doesn't address the root of the problem with this approach but it's an extremely low-cost hack that made my app run smoothly. I do hope the Angular team will implement a more friendly way to trigger some code after the *ngFor loads its content.

Methodician
  • 2,396
  • 5
  • 30
  • 49
3

you can do the same by getting last index using #last of *ngFor and call function by getting last index value and do your stuff whatever you want. here is code for the same -

<ul>
   <li *ngFor="#item of items; #last = last">
    <span *ngIf='last'>{{hello(last)}}</span>
    {{item}}
   </li>
  </ul>


items: Array<number> = [1,2,3,4,5]
  constructor() { console.clear();}
  hello(a){
    if(a==true)
      this.callbackFunction();
  }
  callbackFunction(){
    console.log("last element");
  }

working example for the same Working Plunker

Pardeep Jain
  • 84,110
  • 37
  • 165
  • 215
2

I ran into this problem as well. In my case I was calling a web service to retrieve data and needed to execute javascript to initialize each template produced by *ngFor. Here's what I came up with:

updateData() {
  this.dataService.getData().subscribe(
    function(data) {
      this.data = data;
      setTimeout(javascriptInitializationFunction, 0);
    },
    function(err) { /* handle it */ }
  );
}

setTimout will wait for the DOM To be updated before executing. This isn't exactly what you asked, but hopefully it helps.

bdunn
  • 472
  • 4
  • 19
  • It's working but in many cases it would produce a kind of hacky experience. For example if the data is showing products in the DOM, and you need to make design change with a JavaScript function (instead of your `javascriptInitializationFunction`). This solution would create a glimps of the products (data) before the javascript function would be runned. I had this experience with implementing [Gridify](https://github.com/hongkhanh/gridify) in an Angular application. – Peter Højlund Palluth May 13 '18 at 15:19
  • You can implement the accepted answer in this solution as well. – Peter Højlund Palluth May 13 '18 at 16:54
1

I am facing the same question and find one way to walk around this. Not sure whether it is suitable for you or not.
Pre-conditions is you are using ngFor to render inbound properties.

context: I need call MDL's upgradeAllRegistered to make checkbox pretty after fetch grid data from back-end.

I add one setTimeout inside 'ngOnChanges';

grid.componet.ts:

export class GridComponent {
  @Input() public rows: Array<any>;
    ngOnChanges(changes: {[propertyName: string]: SimpleChange}) {
    for (let propName in changes) {
      if (propName == 'rows') {
        setTimeout(() => componentHandler.upgradeAllRegistered());
      }
    }
  }
}

grid.component.html:

<tr #rowDOM *ngFor="let row of rows;"></tr>
1

I'd managed to perform some hacks to prevent event get triggred (I found that scrolling always trigger those events)

By adding : [Add in your component]

private globalFlag = -1;

and this is my event that will be triggered when looping is finished

ev(flag){
    if(flag != this.globalFlag){
      console.log("TRIGGER!")
      this.globalFlag = flag;

    }
  }

and this the looping code

 <div *ngFor="let var of list;let i=index;let last=last">
    {{last ? ev(i) : ''}}
</div>

The main idea is to prevent event be trigger if the flag was same. So it only be trigger when the globalFlag value differed with current flag that passed from ngFor. Sorry for my bad english, hope you can understand my idea.

0

Possible solution: the answer is displayed on https://stackblitz.com/edit/angular-phpauq?file=src%2Fapp%2Fapp.component.ts

app.component.html

<div [innerHTML]="html"></div>

app.component.ts

 html = '';
ngAfterViewInit(){
       let  i;
       this.html += '<ul>';
       for(i = 0 ; i < this.abc.length ; i++){
           this.html += '<li>';
           this.html +=  this.abc[i];
           if(parseInt(i) === this.abc.length - 1){
           this.callMethod(i);
           }
           this.html += '</li>';
           }
           this.html += '</ul>';
    }


    callMethod(text){
    alert(text);
    }
Mahi
  • 3,748
  • 4
  • 35
  • 70