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
htmltagged 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
createAngularCellWithFactory HelperThe design system provides a createAngularCellWithFactory helper that allows you to render Angular components in datatable cells without importing Lit.
The Angular helper is included in the main design system package:
npm i dap-design-system
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
})
}
]
The createAngularCellWithFactory helper:
- Uses Angular's
createComponentfunction to dynamically create components - Mounts your Angular component into the cell container using
hostElement - Sets inputs using Angular's
setInput()method (Angular 14+) - Subscribes to outputs (EventEmitters) for event handling
- Properly handles component lifecycle (creation and destruction)
- Manages cleanup when cells are removed (pagination, sorting, filtering)
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
This example demonstrates:
- Server-side pagination
- Loading states
- Angular components as cell renderers
- Event handling with outputs
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());
}
}
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();
}
}
createAngularCellWithFactory(createComponentFn, options)Creates an Angular component renderer for datatable cells.
Parameters:
createComponentFn(CreateComponentFn, required): Angular'screateComponentfunction from@angular/coreoptions.component(Type<any>, required): The Angular component class to renderoptions.inputs(Object, optional): Input bindings to pass to the componentoptions.outputs(Object, optional): Output event handlers to subscribe tooptions.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.
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.
This helper requires Angular 14+ for:
createComponentfunction for dynamic component creationsetInput()method for setting component inputs
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
})
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>`
})
The createAngularCellWithFactory helper automatically manages component lifecycle:
- Mount: Components are created using Angular's
createComponentwhen 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
Each cell with an Angular component creates its own component instance. For tables with many rows:
- Consider using virtual scrolling for large datasets
- Use
manualPaginationfor server-side pagination (recommended) - Keep cell components lightweight
- Use
OnPushchange detection strategy in cell components
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)
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
})
- Make sure you've added
CUSTOM_ELEMENTS_SCHEMAto your component - Verify that you're passing the
environmentInjectorif your component has dependencies
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>();
Due to TypeScript strict mode, you may need to cast createComponent:
createAngularCellWithFactory(createComponent as any, {
component: MyComponent,
// ...
})
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.
- React Integration - Using React components in datatable cells
- Vue Integration - Using Vue components in datatable cells
- Datatable API Reference - Full component documentation