Angular 9 - 渲染有限数量的组件的孩子

Posted

技术标签:

【中文标题】Angular 9 - 渲染有限数量的组件的孩子【英文标题】:Angular 9 - Rendering a limited number of component's children 【发布时间】:2020-08-15 05:22:48 【问题描述】:

我有一个 ButtonGroup 组件,它将呈现一定数量的 ButtonAction 组件。 我尝试为每个 ButtonAction 分配一个模板属性 (TemplateRef),以便我可以循环并将它们传递给 ng-template(通过 *ngTemplateOutlet)。 我直接将 TemplateRef 注入到 ButtonAction 的构造函数中,但我得到错误 “No provider for TemplateRef”。 由于我的目标是只渲染一个组件的有限数量,我发现的另一个解决方案是通过指令访问模板。但我不想强迫我们的用户对每个孩子使用指令。 那么,我该怎么办?

@Component(
  selector: 'button-group',
  template: `
    <div>
       <ng-content *ngIf="canDisplayAllChildren; else limitedChildren"></ng-content>

       <ng-template #limitedChildren>
         <ng-container *ngFor="let button of buttons">
           <ng-template [ngTemplateOutlet]="button.template"></ng-template>
         </ng-container>
       </ng-template>

       <button-action (click)="toggle()" *ngIf="shouldLimitChildren">
         <icon [name]="'action-more-fill-vert'"></icon>
       </button-action>
    </div>
  `,
)
export class ButtonGroupComponent 
    @Input()
    public maxVisible: number;

    @ContentChildren(ButtonActionComponent) 
    public buttons: QueryList<ButtonActionComponent>;

    public isClosed: boolean = true;

    public toggle() 
        this.isClosed = !this.isClosed;
    

    public get shouldLimitChildren() 
        return this.hasLimit && this.buttons.length > this.maxVisible;
    

    public get canDisplayAllChildren() 
        return !this.shouldLimitChildren || this.isOpen;
       

ButtonActionComponent 在哪里:

@Component(
  selector: "button-action",
  template: `
    ...
  `
)
export class ButtonActionComponent 
    ...
  constructor(public element: ElementRef, public template: TemplateRef<any>) 

【问题讨论】:

如果我没有混淆,您可以只在指令中注入 TemplateRef。 【参考方案1】:

我花了一些时间想出一个假想的解决方案,但我认为我可能有一些有用的东西,它不依赖于添加到您的子组件的显式指令。

由于无法在不涉及结构指令的情况下使用TemplateRef,我想到了一种类似于React.cloneElement API 的机制。


所以,让我们定义一个基本的ButtonComponent,它将用作ButtonGroupComponent 的子代。

// button.component.ts

import  Component, Input  from "@angular/core";

@Component(
  selector: "app-button",
  template: `
    <button> text </button>
  `
)
export class ButtonComponent 
  @Input()
  public text: string;



GroupComponent 应该克隆并仅将通过maxVisible 输入属性指定的子节点数量附加到其视图中,我还为根本没有提供它的情况提供了POSITIVE_INFINITY 默认值,允许显示所有孩子:

// group.component.ts

...

@Input()
public maxVisible: number = Number.POSITIVE_INFINITY;

...

让我们要求 Angular 在我们的内容中提供孩子(我想说这是对差异的最好解释:https://***.com/a/34327754/3359473):

// group.component.ts

...

@ContentChildren(ButtonComponent)
private children: QueryList<ButtonComponent>;

...

我们现在需要让 Angular 注入一些东西:

    我们当前手动将子实例化到的容器; 一个工厂解析器,可帮助我们动态创建组件;
// group.component.ts

...

constructor(
  private container: ViewContainerRef,
  private factoryResolver: ComponentFactoryResolver
) 

private factory = this.factoryResolver.resolveComponentFactory(ButtonComponent);

...

现在我们已经从 Angular 获得了我们需要的任何东西,我们可以拦截实现 AfterContentInit 接口并添加 ngAfterContentInit 生命周期的内容初始化。

我们需要循环遍历我们的子组件,动态创建新组件并将新组件的所有公共属性设置为给定的子组件:

// group.component.ts

...

ngAfterContentInit() 
  Promise.resolve().then(this.initChildren);


private initChildren = () => 
  // here we are converting the QueryList to an array
  this.children.toArray()

    // here we are taking only the elements we need to show
    .slice(0, this.maxVisible)

    // and for each child
    .forEach(child => 

      // we create the new component in the container injected
      // in the constructor the using the factory we created from
      // the resolver, also given by Angular in our constructor
      const component = this.container.createComponent(this.factory);

      // we clone all the properties from the user-given child
      // to the brand new component instance
      this.clonePropertiesFrom(child, component.instance);
    );
;

// nothing too fancy here, just cycling all the properties from
// one object and setting with the same values on another object
private clonePropertiesFrom(from: ButtonComponent, to: ButtonComponent) 
  Object.getOwnPropertyNames(from).forEach(property => 
    to[property] = from[property];
  );


...

完整的GroupComponent 应如下所示:

// group.component.ts

import 
  Component,
  ContentChildren,
  QueryList,
  AfterContentInit,
  ViewContainerRef,
  ComponentFactoryResolver,
  Input
 from "@angular/core";
import  ButtonComponent  from "./button.component";

@Component(
  selector: "app-group",
  template: ``
)
export class GroupComponent implements AfterContentInit 
  @Input()
  public maxVisible: number = Number.POSITIVE_INFINITY;

  @ContentChildren(ButtonComponent)
  public children: QueryList<ButtonComponent>;

  constructor(
    private container: ViewContainerRef,
    private factoryResolver: ComponentFactoryResolver
  ) 

  private factory = this.factoryResolver.resolveComponentFactory(
    ButtonComponent
  );

  ngAfterContentInit() 
    Promise.resolve().then(this.initChildren);
  

  private initChildren = () => 
    this.children
      .toArray()
      .slice(0, this.maxVisible)
      .forEach(child => 
        const component = this.container.createComponent(this.factory);
        this.clonePropertiesFrom(child, component.instance);
      );
  ;

  private clonePropertiesFrom(from: ButtonComponent, to: ButtonComponent) 
    Object.getOwnPropertyNames(from).forEach(property => 
      to[property] = from[property];
    );
  


注意我们是在运行时创建ButtonComponent,所以我们需要将它添加到AppModuleentryComponents数组中(这里是参考:https://angular.io/guide/entry-components)。

// app.module.ts

import  BrowserModule  from "@angular/platform-browser";
import  NgModule  from "@angular/core";

import  AppComponent  from "./app.component";
import  ButtonComponent  from "./button.component";
import  GroupComponent  from "./group.component";

@NgModule(
  declarations: [AppComponent, ButtonComponent, GroupComponent],
  imports: [BrowserModule],
  providers: [],
  bootstrap: [AppComponent],
  entryComponents: [ButtonComponent]
)
export class AppModule 

使用这两个简单的组件,您应该能够仅渲染给定子组件的子集,并保持非常清晰的用法:

<!-- app.component.html -->

<app-group [maxVisible]="3">
  <app-button [text]="'Button 1'"></app-button>
  <app-button [text]="'Button 2'"></app-button>
  <app-button [text]="'Button 3'"></app-button>
  <app-button [text]="'Button 4'"></app-button>
  <app-button [text]="'Button 5'"></app-button>
</app-group>

在这种情况下,应该只渲染第一个、第二个和第三个孩子。


我测试过的所有代码框都是这个: https://codesandbox.io/s/nervous-darkness-6zorf?file=/src/app/app.component.html

希望这会有所帮助。

【讨论】:

以上是关于Angular 9 - 渲染有限数量的组件的孩子的主要内容,如果未能解决你的问题,请参考以下文章

Angular2 - 删除所有/最近的子组件后无法添加子组件

使用 Angular Routing 渲染两个组件

Angular 9即将发布:改进Ivy编译和渲染管道

angular引用组件版本一定要相同吗

反应我必须重新渲染父母才能重新渲染孩子吗?

对象作为反应组件无效。 (找到:带有键 的对象)。如果您打算渲染一组孩子,请改用数组