Angular 表格

@tanstack/angular-table 适配器是核心表格逻辑的包装器。它的大部分工作与以“Angular 信号”方式管理状态、提供类型以及单元格/表头/表脚模板的渲染实现有关。

导出

@tanstack/angular-table 重新导出 @tanstack/table-core 的所有 API 以及以下内容:

createAngularTable

接受一个选项函数或一个返回表格选项的计算值,并返回一个表格。

ts
import {createAngularTable} from '@tanstack/angular-table'

export class AppComponent {
  data = signal<Person[]>([])

  table = createAngularTable(() => ({
    data: this.data(),
    columns: defaultColumns,
    getCoreRowModel: getCoreRowModel(),
  }))
}

// ...在模板中渲染你的表格
import {createAngularTable} from '@tanstack/angular-table'

export class AppComponent {
  data = signal<Person[]>([])

  table = createAngularTable(() => ({
    data: this.data(),
    columns: defaultColumns,
    getCoreRowModel: getCoreRowModel(),
  }))
}

// ...在模板中渲染你的表格

FlexRender

一个 Angular 结构指令,用于渲染具有动态值的单元格/表头/表脚模板。

FlexRender 支持 Angular 支持的任何类型的内容:

  • 字符串,或通过 innerHTML 实现的 html 字符串
  • TemplateRef
  • 包装到 FlexRenderComponent 中的 Component

您可以直接使用 cell.renderValuecell.getValue API 来渲染表格的单元格。但是, 这些 API 只会输出原始单元格值(来自访问器函数)。 如果您正在使用 cell: () => any 列定义选项,则需要使用适配器中的 FlexRenderDirective

单元格列定义是响应式的,并在注入上下文中运行,然后您可以注入服务或利用信号来自动修改渲染的内容。

示例

ts
@Component({
  imports: [FlexRenderDirective],
  //...
})
class YourComponent {}
@Component({
  imports: [FlexRenderDirective],
  //...
})
class YourComponent {}
angular-html

<tbody>
@for (row of table.getRowModel().rows; track row.id) {
  <tr>
    @for (cell of row.getVisibleCells(); track cell.id) {
      <td>
        <ng-container
          *flexRender="
              cell.column.columnDef.cell;
              props: cell.getContext();
              let cell
            "
        >
          <!-- 如果要渲染简单字符串 -->
          {{ cell }}
          <!-- 如果要渲染 html 字符串 -->
          <div [innerHTML]="cell"></div>
        </ng-container>
      </td>
    }
  </tr>
}
</tbody>

<tbody>
@for (row of table.getRowModel().rows; track row.id) {
  <tr>
    @for (cell of row.getVisibleCells(); track cell.id) {
      <td>
        <ng-container
          *flexRender="
              cell.column.columnDef.cell;
              props: cell.getContext();
              let cell
            "
        >
          <!-- 如果要渲染简单字符串 -->
          {{ cell }}
          <!-- 如果要渲染 html 字符串 -->
          <div [innerHTML]="cell"></div>
        </ng-container>
      </td>
    }
  </tr>
}
</tbody>

渲染组件

要将组件渲染到特定的列表头/单元格/表脚,您可以传递一个使用您的 ComponentType 实例化的 FlexRenderComponent,并能够包含诸如输入、输出和自定义注入器之类的参数。

ts
import {flexRenderComponent} from "./flex-render-component";
import {ChangeDetectionStrategy, input, output} from "@angular/core";

@Component({
  template: `
    ...
  `,
  standalone: true,
  changeDetectionStrategy: ChangeDetectionStrategy.OnPush,
  host: {
    '(click)': 'clickEvent.emit($event)'
  }
})
class CustomCell {
  readonly content = input.required<string>();
  readonly cellType = input<MyType>();

  // 每次单元格点击都会触发的输出
  readonly clickEvent = output<Event>();
}

class AppComponent {
  columns: ColumnDef<unknown>[] = [
    {
      id: 'custom-cell',
      header: () => {
        const translateService = inject(TranslateService);
        return translateService.translate('...');
      },
      cell: (context) => {
        return flexRenderComponent(
          MyCustomComponent,
          {
            injector, // 可选注入器
            inputs: {
              // 强制输入,因为我们使用了 `input.required()
              content: context.row.original.rowProperty,
              // cellType? - 可选输入
            },
            outputs: {
              clickEvent: () => {
                // 执行某些操作
              }
            }
          }
        )
      },
    },
  ]
}
import {flexRenderComponent} from "./flex-render-component";
import {ChangeDetectionStrategy, input, output} from "@angular/core";

@Component({
  template: `
    ...
  `,
  standalone: true,
  changeDetectionStrategy: ChangeDetectionStrategy.OnPush,
  host: {
    '(click)': 'clickEvent.emit($event)'
  }
})
class CustomCell {
  readonly content = input.required<string>();
  readonly cellType = input<MyType>();

  // 每次单元格点击都会触发的输出
  readonly clickEvent = output<Event>();
}

class AppComponent {
  columns: ColumnDef<unknown>[] = [
    {
      id: 'custom-cell',
      header: () => {
        const translateService = inject(TranslateService);
        return translateService.translate('...');
      },
      cell: (context) => {
        return flexRenderComponent(
          MyCustomComponent,
          {
            injector, // 可选注入器
            inputs: {
              // 强制输入,因为我们使用了 `input.required()
              content: context.row.original.rowProperty,
              // cellType? - 可选输入
            },
            outputs: {
              clickEvent: () => {
                // 执行某些操作
              }
            }
          }
        )
      },
    },
  ]
}

在此之下,这利用了 ViewContainerRef#createComponent API。因此,您应该使用 @Input 装饰器或输入/模型信号来声明您的自定义输入。

您仍然可以通过 injectFlexRenderContext 函数访问表格单元格上下文,该函数根据您传递给 FlexRenderDirective 的 props 返回上下文值。

ts

@Component({
  // ...
})
class CustomCellComponent {
  // 单元格组件的上下文
  readonly context = injectFlexRenderContext<CellContext<TData, TValue>>();
  // 页眉/页脚组件的上下文
  readonly context = injectFlexRenderContext<HeaderContext<TData, TValue>>();
}

@Component({
  // ...
})
class CustomCellComponent {
  // 单元格组件的上下文
  readonly context = injectFlexRenderContext<CellContext<TData, TValue>>();
  // 页眉/页脚组件的上下文
  readonly context = injectFlexRenderContext<HeaderContext<TData, TValue>>();
}

或者,您可以通过将组件类型传递给相应的列定义,将组件渲染到特定的列表头、单元格或表脚。这些列定义将与 context 一起提供给 flexRender 指令。

ts
class AppComponent {
  columns: ColumnDef<Person>[] = [
    {
      id: 'select',
      header: () => TableHeadSelectionComponent<Person>,
      cell: () => TableRowSelectionComponent<Person>,
    },
  ]
}
class AppComponent {
  columns: ColumnDef<Person>[] = [
    {
      id: 'select',
      header: () => TableHeadSelectionComponent<Person>,
      cell: () => TableRowSelectionComponent<Person>,
    },
  ]
}
angular-html
<ng-container
  *flexRender="
    header.column.columnDef.header;
    props: header.getContext();
    let headerCell
  "
>
  {{ headerCell }}
</ng-container>
<ng-container
  *flexRender="
    header.column.columnDef.header;
    props: header.getContext();
    let headerCell
  "
>
  {{ headerCell }}
</ng-container>

flexRender 指令中提供的 context 的属性将可供您的组件访问。您可以显式定义组件所需的上下文属性。在此示例中,提供给 flexRender 的上下文类型为 HeaderContext。然后定义输入信号 table(它是 HeaderContext 的属性以及 columnheader 属性)以在组件中使用。如果您的组件中需要任何上下文属性,请随意使用它们。请注意,在使用此方法定义对上下文属性的访问时,仅支持输入信号。

angular-ts
@Component({
  template: `
    <input
      type="checkbox"
      [checked]="table().getIsAllRowsSelected()"
      [indeterminate]="table().getIsSomeRowsSelected()"
      (change)="table().toggleAllRowsSelected()"
    />
  `,
  // ...
})
export class TableHeadSelectionComponent<T> {
  //column = input.required<Column<T, unknown>>()
  //header = input.required<Header<T, unknown>>()
  table = input.required<Table<T>>()
}
@Component({
  template: `
    <input
      type="checkbox"
      [checked]="table().getIsAllRowsSelected()"
      [indeterminate]="table().getIsSomeRowsSelected()"
      (change)="table().toggleAllRowsSelected()"
    />
  `,
  // ...
})
export class TableHeadSelectionComponent<T> {
  //column = input.required<Column<T, unknown>>()
  //header = input.required<Header<T, unknown>>()
  table = input.required<Table<T>>()
}

渲染 TemplateRef

为了将 TemplateRef 渲染到特定的列表头/单元格/表脚,您可以将 TemplateRef 传递到列定义中。

您可以通过 $implicit 属性访问 TemplateRef 数据,该属性的值基于传递给 flexRender 的 props 字段的内容。

在大多数情况下,每个 TemplateRef 都将使用 $implicit 上下文进行渲染,其值基于单元格类型,如下所示:

  • 表头:HeaderContext<T, ?>
  • 单元格:CellContext<T, ?>,
  • 表脚:HeaderContext<T, ?>
angular-html

<ng-container
  *flexRender="
              cell.column.columnDef.cell;
              props: cell.getContext();
              let cell
            "
>
  <!-- 如果要渲染简单字符串 -->
  {{ cell }}
  <!-- 如果要渲染 html 字符串 -->
  <div [innerHTML]="cell"></div>
</ng-container>

<ng-template #myCell let-context>
  <!-- 使用上下文渲染某些内容 -->
</ng-template>

<ng-container
  *flexRender="
              cell.column.columnDef.cell;
              props: cell.getContext();
              let cell
            "
>
  <!-- 如果要渲染简单字符串 -->
  {{ cell }}
  <!-- 如果要渲染 html 字符串 -->
  <div [innerHTML]="cell"></div>
</ng-container>

<ng-template #myCell let-context>
  <!-- 使用上下文渲染某些内容 -->
</ng-template>

完整示例:

angular-ts
import type {
  CellContext,
  ColumnDef,
  HeaderContext,
} from '@tanstack/angular-table'
import {Component, TemplateRef, viewChild} from '@angular/core'

@Component({
  template: `
    <tbody>
      @for (row of table.getRowModel().rows; track row.id) {
        <tr>
          @for (cell of row.getVisibleCells(); track cell.id) {
            <td>
              <ng-container
                *flexRender="
                  cell.column.columnDef.cell;
                  props: cell.getContext(); // 提供给 TemplateRef 的数据
                  let cell
                "
              >
                <!-- 如果要渲染简单字符串 -->
                {{ cell }}
                <!-- 如果要渲染 html 字符串 -->
                <div [innerHTML]="cell"></div>
              </ng-container>
            </td>
          }
        </tr>
      }
    </tbody>

    <ng-template #customHeader let-context>
      {{ context.getValue() }}
    </ng-template>
    <ng-template #customCell let-context>
      {{ context.getValue() }}
    </ng-template>
  `,
})
class AppComponent {
  customHeader =
    viewChild.required<TemplateRef<{ $implicit: HeaderContext<any, any> }>>(
      'customHeader'
    )
  customCell =
    viewChild.required<TemplateRef<{ $implicit: CellContext<any, any> }>>(
      'customCell'
    )

  columns: ColumnDef<unknown>[] = [
    {
      id: 'customCell',
      header: () => this.customHeader(),
      cell: () => this.customCell(),
    },
  ]
}
import type {
  CellContext,
  ColumnDef,
  HeaderContext,
} from '@tanstack/angular-table'
import {Component, TemplateRef, viewChild} from '@angular/core'

@Component({
  template: `
    <tbody>
      @for (row of table.getRowModel().rows; track row.id) {
        <tr>
          @for (cell of row.getVisibleCells(); track cell.id) {
            <td>
              <ng-container
                *flexRender="
                  cell.column.columnDef.cell;
                  props: cell.getContext(); // 提供给 TemplateRef 的数据
                  let cell
                "
              >
                <!-- 如果要渲染简单字符串 -->
                {{ cell }}
                <!-- 如果要渲染 html 字符串 -->
                <div [innerHTML]="cell"></div>
              </ng-container>
            </td>
          }
        </tr>
      }
    </tbody>

    <ng-template #customHeader let-context>
      {{ context.getValue() }}
    </ng-template>
    <ng-template #customCell let-context>
      {{ context.getValue() }}
    </ng-template>
  `,
})
class AppComponent {
  customHeader =
    viewChild.required<TemplateRef<{ $implicit: HeaderContext<any, any> }>>(
      'customHeader'
    )
  customCell =
    viewChild.required<TemplateRef<{ $implicit: CellContext<any, any> }>>(
      'customCell'
    )

  columns: ColumnDef<unknown>[] = [
    {
      id: 'customCell',
      header: () => this.customHeader(),
      cell: () => this.customCell(),
    },
  ]
}