Angular 2:ngFor完成后的回调函数

40

在Angular 1中,我编写了一个自定义指令(“repeater-ready”),与ng-repeat一起使用,以在迭代完成时调用回调方法:

在Angular 1中,我编写了一个自定义指令(“repeater-ready”),与ng-repeat一起使用,以在迭代完成时调用回调方法:

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

在标记中的使用方法:

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

如何在Angular 2中使用 ngFor 实现类似的功能?


1
我很难想象这会有什么用处。你为什么要这样做?也许有另一种方法来解决你的实际问题。 - Douglas
1
这可能会有所帮助,https://angular.io/docs/ts/latest/api/common/NgFor-directive.html -- "NgFor提供了几个可以别名为本地变量的导出值:"其中之一是"last"。但我同意这听起来像是错误的解决方案。 - Dave Bush
1
原因是使用Sematic-UI的自定义指令来创建下拉菜单。我必须从Semantic API中调用一个方法来将输入转换为下拉菜单,但这必须在ngFor循环遍历所有元素之后完成。 - Tobias Punke
你好,Tobias,你解决了这个问题吗?我们遇到了相同的问题,当我需要在ngFor列表中出现新项目后初始化滚动条。 - prespic
同样的问题在这里... - Marek
1
可能是 在 Angular 2 中 *ngFor 完成后执行函数 的重复问题。 - Al-Mothafar
8个回答

77
你可以使用 @ViewChildren 来实现这个目的。
@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');
  }
}

原始答案在这里: https://dev59.com/IVoU5IYBdhLWcg3wzZLw#37088348

非常好用。谢谢! - Devon Sams
8
这应该成为被接受的答案,因为它正确处理了视图状态的问题,使用了Angular提供的方法。其他所有方法都只是一个hack。 - Reactgular
惊人的解决方案! - Max
这个问题的最佳解决方案。 - Pavel B.
这会导致 ExpressionChangedAfterItHasBeenCheckedError 错误。 - user1034912

22
你可以使用类似这样的语法 (ngFor本地变量):
<li *ngFor="#item in Items; #last = last" [ready]="last ? false : true">

你可以使用 setter 拦截输入属性的更改。 查看详情

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

2
至少,这不适用于 li 元素,但适用于嵌套在内部的自定义组件。 - Tobias Punke
8
我认为代码应该像这样 *ngFor="#item of Items;",对吧?顺便说一下,代码会抛出一个“ready不是已知本地属性blabla...”的错误。 - Pardeep Jain
需要添加一个新组件来完成这项工作。请查看此演示:https://github.com/xmeng1/ngfor-finish-callback - Xin Meng
3
这段代码的意思是:遍历Items数组中的每一个元素,对于最后一个元素,设置一个名为"ready"的布尔属性为false,其他元素的"ready"属性为true。 - claudchan
这个程序出了问题,所有人都说在使用<li>标签时出现了模板解析错误。 - Mohammad Mirzaeyan
显示剩余3条评论

8

对我来说,使用TypeScript在Angular2中工作得很好。

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

然后您可以使用此函数进行处理。
public ngForCallback() {
  ...
}

17
对我来说,这将被执行数百万次。 - Alex
1
嘿,Alex,你说得对,这个函数将被执行Items.lenght次。这不是一个很好的方法,适用于小而快的事情。 - FACode
不,它不会执行items.length次。它将永远继续下去,哈哈。 - Marco Noronha
1
请不要使用此解决方案,除非您的ChangeDetectionStrategy设置为OnPush。 - DarkNeuron
@MarcoNoronha 我测试了一下,它不会无限运行。 - FACode
显示剩余3条评论

7
解决方案非常简单。如果您需要知道何时ngFor完成将所有DOM元素打印到浏览器窗口,请按以下操作进行:

1. 添加占位符

添加正在打印的内容的占位符:

<div *ngIf="!contentPrinted">正在渲染内容...</div>

2. 添加容器

创建一个具有display:none的容器,用于内容。当所有项目都打印完毕后,执行display:blockcontentPrinted是组件标志属性,默认为false

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

3. 创建回调方法

向组件添加onContentPrinted(),在ngFor完成后禁用它自己:

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

不要忘记使用ChangeDetectorRef避免ExpressionChangedAfterItHasBeenCheckedError

4. 使用ngFor的last

ngFor上声明last变量。在li中使用它,当此项是最后一个时运行方法:

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

  • 使用contentPrinted组件标志属性仅运行onContentPrinted() 一次
  • 使用ng-container不会对布局产生任何影响。

7
“Trivial” 意思是“琐碎的”,但需要4个步骤来解释。 - Matthieu Charbonnier

4

请使用 [attr.ready] 替代 [ready],如下:

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

3

我发现在RC3中,已经接受的答案不起作用。然而,我找到了一种方法来处理这个问题。对我来说,我需要知道何时ngFor完成运行MDL componentHandler以升级组件。

首先,您需要一个指令。

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

下一步将其导入到您的组件中,并将其添加到指令中。
import {UpgradeComponentsDirective} from './upgradeComponents.directive';

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

现在在HTML中将"upgrade-components"属性设置为true。
 <div *ngFor='let item of items;let last=last' [upgrade-components]="last ? true : false">

当将该属性设置为 true 时,它将运行 @Input() 声明下的方法。在我的情况下,它运行了 componentHandler.upgradeAllRegistered() 方法。但是它可以用于您选择的任何内容。通过绑定 ngFor 语句的 'last' 属性,当其完成时会运行该方法。

即使这不是本地属性,你也不需要使用 [attr.upgrade-components],因为它现在是一个真正的指令。


1

我为这个问题编写了一个演示。理论基于被接受的答案,但是该答案不完整,因为li应该是一个可以接受ready输入的自定义组件。

我为这个问题编写了一个完整的演示

定义一个新组件:

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!');
    }
  }
}

模板

{{item}}

在应用组件中的使用。
<app-li-ready *ngFor="let item of items;  let last1 = last;" [ready]="last1" [item]="item"></app-li-ready>

你将会在控制台看到所有的项目字符串被打印出来,然后打印出isReady。

0

我还没有深入研究ngFor如何在幕后呈现元素。但是从观察中,我注意到它通常倾向于每个正在迭代的项计算多次表达式。

这会导致在检查ngFor“last”变量时进行任何typescript方法调用时,有时会触发多次。

为了保证ngFor在正确完成对项的迭代时仅调用一次您的typescript方法,您需要针对ngFor在幕后执行的多个表达式重新评估添加一些小的保护。

以下是一种通过指令实现的方法,希望能有所帮助:

指令代码

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");

  }

}

在您的模块中声明上述指令(假设您知道如何执行此操作,如果不知道,请问我,我会尽快更新代码片段),以下是如何使用ngFor指令:

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

'doSomething' 是你的 TypeScript 文件中的方法名称,当 ngFor 完成对项目的迭代时,你想要调用它。

注意:这里 'doSomething' 没有括号 '()',因为我们只是传递了一个对 TypeScript 方法的引用,而不是在此处实际调用它。

最后,这是你的 TypeScript 文件中 'doSomething' 方法的样子:

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

对我来说,这看起来是所有答案中最好和最干净的。 - Jesús Franco

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接