Angular Integration for Datatable The Problem

When using the datatable component with custom cell renderers, the underlying TanStack Table library expects cell content to be returned as Lit templates. This creates a problem for Angular users:

// ❌ This forces Angular users to import Lit
import { html } from 'lit'

const columns = [
  {
    id: 'status',
    cell: (row) => html`<dap-ds-badge variant="success">${row.status}</dap-ds-badge>`
  }
]

Why this is a problem:

  • Angular developers must learn and import Lit's html tagged template function
  • Adds unnecessary Lit as a dependency in Angular projects
  • Defeats the purpose of framework-agnostic web components
  • Can't use Angular components in table cells without Lit
The Solution: createAngularCellWithFactory Helper

The design system provides a createAngularCellWithFactory helper that allows you to render Angular components in datatable cells without importing Lit.

Installation

The Angular helper is included in the main design system package:

npm i dap-design-system
Basic Usage
import { createComponent, EnvironmentInjector, inject } from '@angular/core'
import { createAngularCellWithFactory } from 'dap-design-system/helpers/angular'
import { StatusBadgeComponent } from './status-badge.component'

// Inside a component class
private readonly injector = inject(EnvironmentInjector)

columns = [
  {
    id: 'status',
    header: 'Status',
    cell: (info) => createAngularCellWithFactory(createComponent, {
      component: StatusBadgeComponent,
      inputs: {
        status: info.row.original.status,
        variant: 'success'
      },
      environmentInjector: this.injector
    })
  }
]
How It Works

The createAngularCellWithFactory helper:

  1. Uses Angular's createComponent function to dynamically create components
  2. Mounts your Angular component into the cell container using hostElement
  3. Sets inputs using Angular's setInput() method (Angular 14+)
  4. Subscribes to outputs (EventEmitters) for event handling
  5. Properly handles component lifecycle (creation and destruction)
  6. Manages cleanup when cells are removed (pagination, sorting, filtering)
Type Signature
interface AngularCellOptions {
  component: Type<any>
  inputs?: Record<string, any>
  outputs?: Record<string, (...args: any[]) => void>
  content?: string
  environmentInjector?: EnvironmentInjector
}

function createAngularCellWithFactory(
  createComponentFn: CreateComponentFn,
  options: AngularCellOptions
): RenderCallbackContent
Complete Example with Remote Data

This example demonstrates:

  • Server-side pagination
  • Loading states
  • Angular components as cell renderers
  • Event handling with outputs
1. Create Your Angular Components

status-badge.component.ts:

import {
  ChangeDetectionStrategy,
  Component,
  CUSTOM_ELEMENTS_SCHEMA,
  input,
  output,
} from '@angular/core';

@Component({
  selector: 'app-status-badge',
  changeDetection: ChangeDetectionStrategy.OnPush,
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <dap-ds-badge [attr.variant]="variant()" (click)="handleClick()">
      {{ status() }}
    </dap-ds-badge>
  `,
})
export class StatusBadgeComponent {
  status = input.required<string>();
  variant = input<string>('default');

  click = output<string>();

  handleClick() {
    this.click.emit(this.status());
  }
}

actions-cell.component.ts:

import {
  ChangeDetectionStrategy,
  Component,
  CUSTOM_ELEMENTS_SCHEMA,
  input,
  output,
} from '@angular/core';

export interface RowData {
  id: number;
  name: string;
  email: string;
  status: string;
  role: string;
}

@Component({
  selector: 'app-actions-cell',
  changeDetection: ChangeDetectionStrategy.OnPush,
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <div style="display: flex; gap: 0.5rem">
      <dap-ds-button size="xs" variant="primary" (click)="handleEdit()">
        Edit
      </dap-ds-button>
      <dap-ds-button size="xs" variant="danger" (click)="handleDelete()">
        Delete
      </dap-ds-button>
    </div>
  `,
})
export class ActionsCellComponent {
  rowData = input.required<RowData>();

  edit = output<RowData>();
  delete = output<RowData>();

  handleEdit() {
    this.edit.emit(this.rowData());
  }

  handleDelete() {
    this.delete.emit(this.rowData());
  }
}
2. Set Up Your Datatable Component

datatable-demo.component.ts:

import {
  ChangeDetectionStrategy,
  Component,
  createComponent,
  CUSTOM_ELEMENTS_SCHEMA,
  EnvironmentInjector,
  inject,
  signal,
} from '@angular/core';
import { createAngularCellWithFactory } from 'dap-design-system/helpers/angular';

import { ActionsCellComponent, RowData } from './actions-cell.component';
import { StatusBadgeComponent } from './status-badge.component';

@Component({
  selector: 'app-datatable-demo',
  changeDetection: ChangeDetectionStrategy.OnPush,
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <div class="datatable-demo">
      <h2>Angular Datatable Demo with Remote Data & Pagination</h2>

      @if (loading() && users().length === 0) {
        <div class="loading-message">Loading data...</div>
      }

      <dap-ds-datatable
        [data]="users()"
        [columns]="columns()"
        [rowCount]="rowCount()"
        [pager]="true"
        [pagination]="pagination()"
        [manualPagination]="true"
        [loading]="loading()"
        loadingType="spinner"
        (dds-pagination-change)="handlePaginationChange($event)"
      ></dap-ds-datatable>
    </div>
  `,
  styles: `
    .datatable-demo {
      padding: 2rem;
      max-width: 1200px;
      margin: 0 auto;
    }

    .loading-message {
      padding: 2rem;
      text-align: center;
      color: #666;
    }
  `,
})
export class DatatableDemoComponent {
  private readonly injector = inject(EnvironmentInjector);

  users = signal<RowData[]>([]);
  loading = signal(true);
  rowCount = signal(0);
  pagination = signal({
    pageIndex: 0,
    pageSize: 10,
  });

  // Column definitions using createAngularCellWithFactory
  columns = () => [
    {
      id: 'id',
      header: 'ID',
      accessorKey: 'id',
      size: 60,
    },
    {
      id: 'name',
      header: 'Name',
      accessorKey: 'name',
    },
    {
      id: 'email',
      header: 'Email',
      accessorKey: 'email',
    },
    {
      id: 'status',
      header: 'Status',
      cell: (info: { row: { original: RowData } }) => {
        const row = info.row.original;
        const variant =
          row.status === 'active'
            ? 'success'
            : row.status === 'pending'
              ? 'warning'
              : 'danger';

        return createAngularCellWithFactory(createComponent as any, {
          component: StatusBadgeComponent,
          inputs: {
            status: row.status,
            variant: variant,
          },
          outputs: {
            click: (status: string) => {
              console.log('Status badge clicked:', status);
            },
          },
          environmentInjector: this.injector,
        });
      },
    },
    {
      id: 'actions',
      header: 'Actions',
      cell: (info: { row: { original: RowData } }) => {
        return createAngularCellWithFactory(createComponent as any, {
          component: ActionsCellComponent,
          inputs: {
            rowData: info.row.original,
          },
          outputs: {
            edit: (rowData: RowData) => this.handleEdit(rowData),
            delete: (rowData: RowData) => this.handleDelete(rowData),
          },
          environmentInjector: this.injector,
        });
      },
    },
  ];

  constructor() {
    this.fetchData();
  }

  async fetchData() {
    this.loading.set(true);

    // Simulate network delay
    await new Promise((resolve) => setTimeout(resolve, 500));

    // In a real app, this would be an API call
    const result = await fetch(
      `/api/users?page=${this.pagination().pageIndex}&size=${this.pagination().pageSize}`
    );
    const data = await result.json();

    this.users.set(data.users);
    this.rowCount.set(data.total);
    this.loading.set(false);
  }

  handleEdit(rowData: RowData) {
    alert(`Editing user: ${rowData.name}`);
  }

  handleDelete(rowData: RowData) {
    if (confirm(`Delete user ${rowData.name}?`)) {
      this.fetchData();
    }
  }

  handlePaginationChange(event: Event) {
    const customEvent = event as CustomEvent<{
      pagination: { pageIndex: number; pageSize: number };
    }>;
    this.pagination.set(customEvent.detail.pagination);
    this.fetchData();
  }
}
API Reference createAngularCellWithFactory(createComponentFn, options)

Creates an Angular component renderer for datatable cells.

Parameters:

  • createComponentFn (CreateComponentFn, required): Angular's createComponent function from @angular/core
  • options.component (Type<any>, required): The Angular component class to render
  • options.inputs (Object, optional): Input bindings to pass to the component
  • options.outputs (Object, optional): Output event handlers to subscribe to
  • options.content (string, optional): Content to project into the component (ng-content)
  • options.environmentInjector (EnvironmentInjector, optional): Injector for dependency injection context

Returns: A render callback that the datatable will use to mount the Angular component.

Output Event Handling

Outputs are automatically subscribed to using Angular's EventEmitter pattern:

createAngularCellWithFactory(createComponent, {
  component: MyComponent,
  outputs: {
    click: (data) => console.log('clicked', data),
    change: (value) => console.log('changed', value),
    customEvent: (data) => console.log('custom', data)
  }
})

The helper subscribes to each output's EventEmitter and calls your handler when events are emitted.

Important Notes Angular Version Requirements

This helper requires Angular 14+ for:

  • createComponent function for dynamic component creation
  • setInput() method for setting component inputs
Using EnvironmentInjector

For proper dependency injection in your cell components, you must provide the EnvironmentInjector:

import { EnvironmentInjector, inject } from '@angular/core'

// Inside your component class
private readonly injector = inject(EnvironmentInjector)

// When creating cells
createAngularCellWithFactory(createComponent, {
  component: MyComponent,
  environmentInjector: this.injector
})
Custom Elements Schema

Angular needs to be configured to recognize custom elements. Add CUSTOM_ELEMENTS_SCHEMA to your component:

import { CUSTOM_ELEMENTS_SCHEMA, Component } from '@angular/core'

@Component({
  selector: 'my-component',
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `<dap-ds-datatable ...></dap-ds-datatable>`
})
Component Lifecycle

The createAngularCellWithFactory helper automatically manages component lifecycle:

  • Mount: Components are created using Angular's createComponent when cells are rendered
  • Change Detection: detectChanges() is called after inputs are set
  • Unmount: Components are properly destroyed when cells are removed (e.g., pagination, sorting)
  • Cleanup: Output subscriptions are unsubscribed to prevent memory leaks
Performance Considerations

Each cell with an Angular component creates its own component instance. For tables with many rows:

  • Consider using virtual scrolling for large datasets
  • Use manualPagination for server-side pagination (recommended)
  • Keep cell components lightweight
  • Use OnPush change detection strategy in cell components
TypeScript Support Importing Types
import { createAngularCellWithFactory } from 'dap-design-system/helpers/angular'
import type { AngularCellOptions, AngularCellRenderer } from 'dap-design-system/helpers/angular'

// Type-safe options
const options: AngularCellOptions = {
  component: MyComponent,
  inputs: {
    value: 'test'
  }
}

const cellRenderer = createAngularCellWithFactory(createComponent, options)
Type-Safe Cell Renderers
import type { AngularCellRenderer } from 'dap-design-system/helpers/angular'

interface User {
  id: number
  status: string
}

const statusCell: AngularCellRenderer<User> = (row) =>
  createAngularCellWithFactory(createComponent, {
    component: StatusBadgeComponent,
    inputs: { status: row.status },
    environmentInjector: injector
  })
Troubleshooting Components Not Rendering
  1. Make sure you've added CUSTOM_ELEMENTS_SCHEMA to your component
  2. Verify that you're passing the environmentInjector if your component has dependencies
Outputs Not Firing

Ensure your component outputs are defined using Angular's output() function:

// ✅ Correct - using output() function
click = output<string>();

// ❌ Incorrect - using @Output decorator with EventEmitter directly without emit
@Output() click = new EventEmitter<string>();
Type Casting for createComponent

Due to TypeScript strict mode, you may need to cast createComponent:

createAngularCellWithFactory(createComponent as any, {
  component: MyComponent,
  // ...
})
Change Detection Not Running

If your component isn't updating, ensure you're using OnPush change detection and that inputs are properly set:

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  // ...
})

The helper automatically calls detectChanges() after setting inputs, but if you're updating data asynchronously, you may need to handle change detection manually.

See Also