@tanstack/angular-table
Version:
Headless UI for building powerful tables & datagrids for Angular.
1,299 lines (1,284 loc) • 53.9 kB
JavaScript
import * as i0 from '@angular/core';
import { InjectionToken, input, inject, Directive, reflectComponentType, Injectable, KeyValueDiffers, ChangeDetectorRef, OutputEmitterRef, TemplateRef, Type, runInInjectionContext, computed, effect, untracked, Injector, ViewContainerRef, DestroyRef, NgZone, signal, assertInInjectionContext } from '@angular/core';
import { constructTable, createColumnHelper } from '@tanstack/table-core';
export * from '@tanstack/table-core';
import { toObservable } from '@angular/core/rxjs-interop';
export { shallow } from '@tanstack/angular-store';
/**
* Injection token that provides access to the current cell.
*
* This token is provided by the {@link TanStackTableCell} directive.
*/
const TanStackTableCellToken = new InjectionToken('[TanStack Table] CellContext');
/**
* Provides a TanStack Table `Cell` instance in Angular DI.
*
* The cell can be injected by:
* - any descendant of an element using `[tanStackTableCell]="..."`
* - any component instantiated by `*flexRender` when the render props contains `cell`
*
* @example
* Inject from the nearest `[tanStackTableCell]`:
* ```html
* <td [tanStackTableCell]="cell">
* <app-cell-actions />
* </td>
* ```
*
* ```ts
* @Component({
* selector: 'app-cell-actions',
* template: `{{ cell().id }}`,
* })
* export class CellActionsComponent {
* readonly cell = injectTableCellContext()
* }
* ```
*
* @example
* Inject inside a component rendered via `flexRender`:
* ```ts
* @Component({
* selector: 'app-price-cell',
* template: `{{ cell().getValue() }}`,
* })
* export class PriceCellComponent {
* readonly cell = injectTableCellContext()
* }
* ```
*/
class TanStackTableCell {
/**
* The current TanStack Table cell.
*
* Provided as a required signal input so DI consumers always read the latest value.
*/
cell = input.required({ ...(ngDevMode ? { debugName: "cell" } : /* istanbul ignore next */ {}), alias: 'tanStackTableCell' });
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: TanStackTableCell, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.0", type: TanStackTableCell, isStandalone: true, selector: "[tanStackTableCell]", inputs: { cell: { classPropertyName: "cell", publicName: "tanStackTableCell", isSignal: true, isRequired: true, transformFunction: null } }, providers: [
{
provide: TanStackTableCellToken,
useFactory: () => inject(TanStackTableCell).cell,
},
], exportAs: ["cell"], ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: TanStackTableCell, decorators: [{
type: Directive,
args: [{
selector: '[tanStackTableCell]',
exportAs: 'cell',
providers: [
{
provide: TanStackTableCellToken,
useFactory: () => inject(TanStackTableCell).cell,
},
],
}]
}], propDecorators: { cell: [{ type: i0.Input, args: [{ isSignal: true, alias: "tanStackTableCell", required: true }] }] } });
/**
* Injects the current TanStack Table cell signal.
*
* Available when:
* - there is a nearest `[tanStackTableCell]` directive in the DI tree, or
* - the caller is rendered via `*flexRender` with render props containing `cell`
*/
function injectTableCellContext() {
return inject(TanStackTableCellToken);
}
/**
* Injection token that provides access to the current header.
*
* This token is provided by the {@link TanStackTableHeader} directive.
*/
const TanStackTableHeaderToken = new InjectionToken('[TanStack Table] HeaderContext');
/**
* Provides a TanStack Table `Header` instance in Angular DI.
*
* The header can be injected by:
* - any descendant of an element using `[tanStackTableHeader]="..."`
* - any component instantiated by `*flexRender` when the render props contains `header`
*
* @example
* ```html
* <th [tanStackTableHeader]="header">
* <app-sort-indicator />
* </th>
* ```
*
* ```ts
* @Component({
* selector: 'app-sort-indicator',
* template: `
* <button (click)="toggle()">
* {{ header().column.id }}
* </button>
* `,
* })
* export class SortIndicatorComponent {
* readonly header = injectTableHeaderContext()
*
* toggle() {
* this.header().column.toggleSorting()
* }
* }
* ```
*/
class TanStackTableHeader {
/**
* The current TanStack Table header.
*
* Provided as a required signal input so DI consumers always read the latest value.
*/
header = input.required({ ...(ngDevMode ? { debugName: "header" } : /* istanbul ignore next */ {}), alias: 'tanStackTableHeader' });
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: TanStackTableHeader, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.0", type: TanStackTableHeader, isStandalone: true, selector: "[tanStackTableHeader]", inputs: { header: { classPropertyName: "header", publicName: "tanStackTableHeader", isSignal: true, isRequired: true, transformFunction: null } }, providers: [
{
provide: TanStackTableHeaderToken,
useFactory: () => inject(TanStackTableHeader).header,
},
], exportAs: ["header"], ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: TanStackTableHeader, decorators: [{
type: Directive,
args: [{
selector: '[tanStackTableHeader]',
exportAs: 'header',
providers: [
{
provide: TanStackTableHeaderToken,
useFactory: () => inject(TanStackTableHeader).header,
},
],
}]
}], propDecorators: { header: [{ type: i0.Input, args: [{ isSignal: true, alias: "tanStackTableHeader", required: true }] }] } });
/**
* Injects the current TanStack Table header signal.
*
* Available when:
* - there is a nearest `[tanStackTableHeader]` directive in the DI tree, or
* - the caller is rendered via `*flexRender` with render props containing `header`
*/
function injectTableHeaderContext() {
return inject(TanStackTableHeaderToken);
}
/**
* Injection token that provides access to the current {@link AngularTable} instance.
*
* This token is provided by the {@link TanStackTable} directive.
*/
const TanStackTableToken = new InjectionToken('[TanStack Table] Table Context');
/**
* Provides a TanStack Table instance (`AngularTable`) in Angular DI.
*
* The table can be injected by:
* - any descendant of an element using `[tanStackTable]="..."`
* - any component instantiated by `*flexRender` when the render props contains `table`
*
* @example
* ```html
* <div [tanStackTable]="table">
* <app-pagination />
* </div>
* ```
*
* ```ts
* @Component({
* selector: 'app-pagination',
* template: `
* <button (click)="prev()" [disabled]="!table().getCanPreviousPage()">Prev</button>
* <button (click)="next()" [disabled]="!table().getCanNextPage()">Next</button>
* `,
* })
* export class PaginationComponent {
* readonly table = injectTableContext()
*
* prev() {
* this.table().previousPage()
* }
* next() {
* this.table().nextPage()
* }
* }
* ```
*/
class TanStackTable {
/**
* The current TanStack Table instance.
*
* Provided as a required signal input so DI consumers always read the latest value.
*/
table = input.required({ ...(ngDevMode ? { debugName: "table" } : /* istanbul ignore next */ {}), alias: 'tanStackTable' });
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: TanStackTable, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.0", type: TanStackTable, isStandalone: true, selector: "[tanStackTable]", inputs: { table: { classPropertyName: "table", publicName: "tanStackTable", isSignal: true, isRequired: true, transformFunction: null } }, providers: [
{
provide: TanStackTableToken,
useFactory: () => inject(TanStackTable).table,
},
], exportAs: ["table"], ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: TanStackTable, decorators: [{
type: Directive,
args: [{
selector: '[tanStackTable]',
exportAs: 'table',
providers: [
{
provide: TanStackTableToken,
useFactory: () => inject(TanStackTable).table,
},
],
}]
}], propDecorators: { table: [{ type: i0.Input, args: [{ isSignal: true, alias: "tanStackTable", required: true }] }] } });
/**
* Injects the current TanStack Table instance signal.
*
* Available when:
* - there is a nearest `[tanStackTable]` directive in the DI tree, or
* - the caller is rendered via `*flexRender` with render props containing `table`
*/
function injectTableContext() {
return inject(TanStackTableToken);
}
const FlexRenderComponentProps = new InjectionToken('[@tanstack/angular-table] Flex render component context props');
/**
* Inject the flex render context props.
*
* Can be used in components rendered via FlexRender directives.
*/
function injectFlexRenderContext() {
return inject(FlexRenderComponentProps);
}
/**
* Flags used to manage and optimize the rendering lifecycle of the content of the cell
* while using {@link FlexViewRenderer}.
*/
const FlexRenderFlags = {
/**
* Indicates that the view is being created for the first time or will be cleared during the next update phase.
* This is the initial state and will transition after the first ngDoCheck.
*/
ViewFirstRender: 1 << 0,
/**
* Indicates the `content` property has been modified or the view requires a complete re-render.
* When this flag is enabled, the view will be cleared and recreated from scratch.
*/
ContentChanged: 1 << 1,
/**
* Indicates that the `props` property reference has changed.
* When this flag is enabled, the view context is updated based on the type of the content.
*
* For Component view, inputs will be updated and view will be marked as dirty.
* For TemplateRef and primitive values, view will be marked as dirty
*/
PropsReferenceChanged: 1 << 2,
/**
* Indicates that the current rendered view needs to be checked for changes.
* This will be set to true when `content(props)` result has changed or during
* forced update
*/
Dirty: 1 << 3,
/**
* Indicates that the first render effect has been checked at least one time.
*/
RenderEffectChecked: 1 << 4,
};
/**
* Helper function to create a {@link FlexRenderComponent} instance, with better type-safety.
*
* @example
* ```ts
* import {flexRenderComponent} from '@tanstack/angular-table'
* import {inputBinding, outputBinding} from '@angular/core';
*
* const columns = [
* {
* cell: ({ row }) => {
* return flexRenderComponent(MyComponent, {
* inputs: { value: mySignalValue() },
* outputs: { valueChange: (val) => {} }
* // or using angular native createComponent#binding api
* bindings: [
* inputBinding('value', mySignalValue),
* outputBinding('valueChange', value => {
* console.log("my value changed to", value)
* })
* ]
* })
* },
* },
* ]
* ```
*/
function flexRenderComponent(component, options) {
const { inputs, injector, outputs, directives, bindings } = options ?? {};
return new FlexRenderComponentInstance(component, inputs, injector, outputs, directives, bindings);
}
/**
* Wrapper class for a component that will be used as content for {@link FlexRenderDirective}
*
* Prefer {@link flexRenderComponent} helper for better type-safety
*/
class FlexRenderComponentInstance {
component;
inputs;
injector;
outputs;
directives;
bindings;
mirror;
allowedInputNames = [];
allowedOutputNames = [];
constructor(component, inputs, injector, outputs, directives, bindings) {
this.component = component;
this.inputs = inputs;
this.injector = injector;
this.outputs = outputs;
this.directives = directives;
this.bindings = bindings;
const mirror = reflectComponentType(component);
if (!mirror) {
throw new Error(`[@tanstack-table/angular] The provided symbol is not a component`);
}
this.mirror = mirror;
for (const input of this.mirror.inputs) {
this.allowedInputNames.push(input.propName);
}
for (const output of this.mirror.outputs) {
this.allowedOutputNames.push(output.propName);
}
}
}
/**
* Creates and manages Angular component instances used by flex-rendered table
* content.
*/
class FlexRenderComponentFactory {
#viewContainerRef;
constructor(viewContainerRef) {
this.#viewContainerRef = viewContainerRef;
}
createComponent(flexRenderComponent, componentInjector) {
const componentRef = this.#viewContainerRef.createComponent(flexRenderComponent.component, {
injector: componentInjector,
directives: flexRenderComponent.directives,
bindings: flexRenderComponent.bindings ?? [],
});
const view = new FlexRenderComponentRef(componentRef, flexRenderComponent, componentInjector);
const { inputs, outputs } = flexRenderComponent;
if (inputs)
view.setInputs(inputs);
if (outputs)
view.setOutputs(outputs);
return view;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: FlexRenderComponentFactory, deps: [{ token: i0.ViewContainerRef }], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: FlexRenderComponentFactory });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: FlexRenderComponentFactory, decorators: [{
type: Injectable
}], ctorParameters: () => [{ type: i0.ViewContainerRef }] });
/**
* Runtime wrapper around an Angular component rendered by `FlexRenderDirective`.
*
* It diffs inputs and outputs across table updates so component renderers can
* be reused instead of recreated on every cell/header render.
*/
class FlexRenderComponentRef {
componentRef;
componentInjector;
#keyValueDiffersFactory;
#componentData;
#inputValueDiffer;
#outputRegistry;
constructor(componentRef, componentData, componentInjector) {
this.componentRef = componentRef;
this.componentInjector = componentInjector;
this.#componentData = componentData;
this.#keyValueDiffersFactory = componentInjector.get(KeyValueDiffers);
this.#outputRegistry = new FlexRenderComponentOutputManager(this.#keyValueDiffersFactory, this.outputs);
this.#inputValueDiffer = this.#keyValueDiffersFactory
.find(this.inputs)
.create();
this.#inputValueDiffer.diff(this.inputs);
this.componentRef.onDestroy(() => this.#outputRegistry.unsubscribeAll());
}
get component() {
return this.#componentData.component;
}
get inputs() {
return this.#componentData.inputs ?? {};
}
get outputs() {
return this.#componentData.outputs ?? {};
}
/**
* Get component input and output diff by the given item
*/
diff(item) {
return {
inputDiff: this.#inputValueDiffer.diff(item.inputs ?? {}),
outputDiff: this.#outputRegistry.diff(item.outputs ?? {}),
};
}
/**
*
* @param compare Whether the current ref component instance is the same as the given one
*/
eqType(compare) {
return compare.component === this.component;
}
/**
* Tries to update current component refs input by the new given content component.
*/
update(content) {
const eq = this.eqType(content);
if (!eq)
return;
const { inputDiff, outputDiff } = this.diff(content);
if (inputDiff) {
inputDiff.forEachAddedItem((item) => this.setInput(item.key, item.currentValue));
inputDiff.forEachChangedItem((item) => this.setInput(item.key, item.currentValue));
inputDiff.forEachRemovedItem((item) => this.setInput(item.key, undefined));
}
if (outputDiff) {
outputDiff.forEachAddedItem((item) => {
this.setOutput(item.key, item.currentValue);
});
outputDiff.forEachChangedItem((item) => {
if (item.currentValue) {
this.#outputRegistry.setListener(item.key, item.currentValue);
}
else {
this.#outputRegistry.unsubscribe(item.key);
}
});
outputDiff.forEachRemovedItem((item) => {
this.#outputRegistry.unsubscribe(item.key);
});
}
this.#componentData = content;
}
markAsDirty() {
this.componentRef.injector.get(ChangeDetectorRef).markForCheck();
}
setInputs(inputs) {
for (const prop in inputs) {
this.setInput(prop, inputs[prop]);
}
}
setInput(key, value) {
if (this.#componentData.allowedInputNames.includes(key)) {
this.componentRef.setInput(key, value);
}
}
setOutputs(outputs) {
this.#outputRegistry.unsubscribeAll();
for (const prop in outputs) {
this.setOutput(prop, outputs[prop]);
}
}
setOutput(outputName, emit) {
if (!this.#componentData.allowedOutputNames.includes(outputName))
return;
if (!emit) {
this.#outputRegistry.unsubscribe(outputName);
return;
}
const hasListener = this.#outputRegistry.hasListener(outputName);
this.#outputRegistry.setListener(outputName, emit);
if (hasListener) {
return;
}
const instance = this.componentRef.instance;
const output = instance[outputName];
if (output && output instanceof OutputEmitterRef) {
output.subscribe((value) => {
this.#outputRegistry.getListener(outputName)?.(value);
});
}
}
}
class FlexRenderComponentOutputManager {
#outputSubscribers = {};
#outputListeners = {};
#valueDiffer;
constructor(keyValueDiffers, initialOutputs) {
this.#valueDiffer = keyValueDiffers.find(initialOutputs).create();
if (initialOutputs) {
this.#valueDiffer.diff(initialOutputs);
}
}
hasListener(outputName) {
return outputName in this.#outputListeners;
}
setListener(outputName, callback) {
this.#outputListeners[outputName] = callback;
}
getListener(outputName) {
return this.#outputListeners[outputName];
}
unsubscribeAll() {
for (const prop in this.#outputSubscribers) {
this.unsubscribe(prop);
}
}
unsubscribe(outputName) {
if (outputName in this.#outputSubscribers) {
this.#outputSubscribers[outputName]?.unsubscribe();
delete this.#outputSubscribers[outputName];
delete this.#outputListeners[outputName];
}
}
diff(outputs) {
return this.#valueDiffer.diff(outputs);
}
}
/**
* Normalizes arbitrary Angular flex-render content into the renderer's internal
* tagged representation.
*
* This lets the directive decide whether to reuse, update, or recreate an
* embedded view or component view.
*/
function mapToFlexRenderTypedContent(content) {
if (content === null || content === undefined) {
return { kind: 'null' };
}
if (typeof content === 'string' || typeof content === 'number') {
return { kind: 'primitive', content };
}
if (content instanceof FlexRenderComponentInstance) {
return { kind: 'flexRenderComponent', content };
}
else if (content instanceof TemplateRef) {
return { kind: 'templateRef', content };
}
else if (content instanceof Type) {
return { kind: 'component', content };
}
else {
return { kind: 'primitive', content };
}
}
class FlexRenderView {
view;
#previousContent;
#content;
constructor(initialContent, view) {
this.#content = initialContent;
this.view = view;
}
get previousContent() {
return this.#previousContent ?? { kind: 'null' };
}
get content() {
return this.#content;
}
set content(content) {
this.#previousContent = this.#content;
this.#content = content;
}
}
/**
* Tracks an Angular embedded template view rendered by `FlexRenderDirective`.
*
* Template views receive updated props through their proxied context and can be
* reused while the rendered content kind stays compatible.
*/
class FlexRenderTemplateView extends FlexRenderView {
constructor(initialContent, view) {
super(initialContent, view);
}
updateProps(_props) {
this.view.markForCheck();
}
dirtyCheck() {
// Basically a no-op. When the view is created via EmbeddedViewRef, we don't need to do any manual update
// since this type of content has a proxy as a context, then every time the root component is checked for changes,
// the property getter will be re-evaluated.
//
// If in a future we need to manually mark the view as dirty, just uncomment next line
// this.view.markForCheck()
}
unmount() {
this.view.destroy();
}
onDestroy(callback) {
this.view.onDestroy(callback);
}
eq(compare) {
return ((this.content.kind === 'primitive' &&
compare.kind === 'primitive' &&
this.content.content === compare.content) ||
(this.content.kind === 'templateRef' &&
compare.kind === 'templateRef' &&
this.content.content === compare.content));
}
}
/**
* Tracks an Angular component view rendered by `FlexRenderDirective`.
*
* Component views own input/output updates for `flexRenderComponent(...)`
* results and component classes rendered directly from column definitions.
*/
class FlexRenderComponentView extends FlexRenderView {
constructor(initialContent, view) {
super(initialContent, view);
}
updateProps(props) {
switch (this.content.kind) {
case 'component': {
this.view.setInputs(props);
break;
}
case 'flexRenderComponent': {
// No-op. When FlexRenderFlags.PropsReferenceChanged is set,
// FlexRenderComponent will be updated into `dirtyCheck`.
break;
}
}
}
dirtyCheck() {
switch (this.content.kind) {
case 'component': {
// Component context is currently valuated with the cell context. Since it's reference
// shouldn't change, we force mark the component as dirty in order to re-evaluate function invocation in view.
// NOTE: this should behave like having a component with ChangeDetectionStrategy.Default
this.view.markAsDirty();
break;
}
case 'flexRenderComponent': {
// Given context instance will always have a different reference than the previous one,
// so instead of recreating the entire view, we will only update the current view
if (this.view.eqType(this.content.content)) {
this.view.update(this.content.content);
}
this.view.markAsDirty();
break;
}
}
}
unmount() {
this.view.componentRef.destroy();
}
onDestroy(callback) {
this.view.componentRef.onDestroy(callback);
}
eq(compare) {
return ((this.content.kind === 'component' &&
compare.kind === 'component' &&
this.content.content === compare.content) ||
(this.content.kind === 'flexRenderComponent' &&
compare.kind === 'flexRenderComponent' &&
this.content.content.component === compare.content.component));
}
}
/**
* Internal view renderer used by Angular TanStack Table to implement `flexRender` directives.
*
* @internal Use FlexRender directives instead.
*/
class FlexViewRenderer {
#renderFlags = FlexRenderFlags.ViewFirstRender;
#renderView = null;
#currentRenderEffectRef = null;
#content;
#props;
#injector;
#viewContainerRef;
#templateRef;
#flexRenderComponentFactory;
#getLatestContentValue = () => {
const content = this.#content();
const props = this.#props();
return typeof content !== 'function'
? content
: runInInjectionContext(this.#injector(), () => content(props));
};
#latestContent = computed(() => this.#getLatestContentValue(), /* @ts-ignore */
...(ngDevMode ? [{ debugName: "#latestContent" }] : /* istanbul ignore next */ []));
#getContentValue = computed(() => {
const latestContent = this.#latestContent();
return mapToFlexRenderTypedContent(latestContent);
}, /* @ts-ignore */
...(ngDevMode ? [{ debugName: "#getContentValue" }] : /* istanbul ignore next */ []));
constructor(options) {
this.#content = options.content;
this.#props = options.props;
this.#injector = options.injector;
this.#templateRef = options.templateRef;
this.#viewContainerRef = options.viewContainerRef;
this.#flexRenderComponentFactory = new FlexRenderComponentFactory(this.#viewContainerRef);
}
mount() {
let previousContent;
let previousProps;
return effect(() => {
const props = this.#props();
const content = this.#content();
if (!(this.#renderFlags & FlexRenderFlags.ViewFirstRender)) {
if (previousContent !== content) {
this.#renderFlags |= FlexRenderFlags.ContentChanged;
}
if (previousProps !== props) {
this.#renderFlags |= FlexRenderFlags.PropsReferenceChanged;
}
}
untracked(() => this.#update());
if (FlexRenderFlags.ViewFirstRender & this.#renderFlags) {
this.#renderFlags &= ~FlexRenderFlags.ViewFirstRender;
}
previousContent = content;
previousProps = props;
});
}
destroy() {
if (this.#currentRenderEffectRef) {
this.#currentRenderEffectRef.destroy();
this.#currentRenderEffectRef = null;
}
if (this.#renderView) {
this.#renderView.unmount();
this.#renderView = null;
}
}
#update() {
if (this.#renderFlags &
(FlexRenderFlags.ContentChanged | FlexRenderFlags.ViewFirstRender)) {
this.#render();
return;
}
if (this.#renderFlags & FlexRenderFlags.PropsReferenceChanged) {
if (this.#renderView)
this.#renderView.updateProps(this.#props());
this.#renderFlags &= ~FlexRenderFlags.PropsReferenceChanged;
}
if (this.#renderFlags & FlexRenderFlags.Dirty) {
if (this.#renderView)
this.#renderView.dirtyCheck();
this.#renderFlags &= ~FlexRenderFlags.Dirty;
}
}
#render() {
// When the view is recreated from scratch (content change or first render),
// we have to destroy the current effect listener since it will be recreated
// skipping the first call (FlexRenderFlags.RenderEffectChecked)
if (this.#shouldRecreateEntireView() && this.#currentRenderEffectRef) {
this.#currentRenderEffectRef.destroy();
this.#currentRenderEffectRef = null;
this.#renderFlags &= ~FlexRenderFlags.RenderEffectChecked;
}
this.#viewContainerRef.clear();
if (this.#renderView) {
this.#renderView.unmount();
this.#renderView = null;
}
this.#renderFlags =
(this.#renderFlags & FlexRenderFlags.ViewFirstRender) |
(this.#renderFlags & FlexRenderFlags.RenderEffectChecked);
const resolvedContent = this.#getContentValue();
this.#renderView = this.#renderViewByContent(resolvedContent);
// If the content is a function `content(props)`, we initialize an effect
// to react to changes. If the current fn uses signals, we will set the DirtySignal flag
// to re-schedule the component updates
if (!this.#currentRenderEffectRef &&
typeof untracked(this.#content) === 'function') {
this.#currentRenderEffectRef = effect(() => {
this.#latestContent();
if (!(this.#renderFlags & FlexRenderFlags.RenderEffectChecked)) {
this.#renderFlags |= FlexRenderFlags.RenderEffectChecked;
return;
}
this.#renderFlags |= FlexRenderFlags.Dirty;
this.#doCheck();
}, { ...(ngDevMode ? { debugName: "#currentRenderEffectRef" } : /* istanbul ignore next */ {}), injector: this.#viewContainerRef.injector });
}
}
#shouldRecreateEntireView() {
return (this.#renderFlags &
FlexRenderFlags.ContentChanged &
FlexRenderFlags.ViewFirstRender);
}
#doCheck() {
const latestContent = this.#getContentValue();
if (latestContent.kind === 'null' || !this.#renderView) {
this.#renderFlags |= FlexRenderFlags.ContentChanged;
}
else {
const { kind: currentKind } = this.#renderView.content;
if (latestContent.kind !== currentKind ||
!this.#renderView.eq(latestContent)) {
this.#renderFlags |= FlexRenderFlags.ContentChanged;
}
this.#renderView.content = latestContent;
}
this.#update();
}
#renderViewByContent(content) {
if (content.kind === 'primitive') {
return this.#renderStringContent(content);
}
else if (content.kind === 'templateRef') {
return this.#renderTemplateRefContent(content);
}
else if (content.kind === 'flexRenderComponent') {
return this.#renderComponent(content);
}
else if (content.kind === 'component') {
return this.#renderCustomComponent(content);
}
else {
return null;
}
}
#renderStringContent(template) {
const context = () => {
const content = this.#content();
return typeof content === 'string' || typeof content === 'number'
? content
: runInInjectionContext(this.#injector(), () => content?.(this.#props()));
};
const ref = this.#viewContainerRef.createEmbeddedView(this.#templateRef, {
get $implicit() {
return context();
},
});
return new FlexRenderTemplateView(template, ref);
}
#renderTemplateRefContent(template) {
const latestContext = () => this.#props();
const view = this.#viewContainerRef.createEmbeddedView(template.content, {
get $implicit() {
return latestContext();
},
}, { injector: this.#getInjector() });
return new FlexRenderTemplateView(template, view);
}
#renderComponent(flexRenderComponent) {
const { injector } = flexRenderComponent.content;
const componentInjector = this.#getInjector(injector);
const view = this.#flexRenderComponentFactory.createComponent(flexRenderComponent.content, componentInjector);
return new FlexRenderComponentView(flexRenderComponent, view);
}
#renderCustomComponent(component) {
const instance = flexRenderComponent(component.content, {
inputs: this.#props(),
});
const injector = this.#getInjector(instance.injector);
const view = this.#flexRenderComponentFactory.createComponent(instance, injector);
return new FlexRenderComponentView(component, view);
}
#getInjector(parentInjector) {
const getContext = () => this.#props();
const proxy = new Proxy(this.#props(), {
get: (_, key) => getContext()[key],
});
const staticProviders = [];
if ('table' in proxy) {
staticProviders.push({
provide: TanStackTableToken,
useValue: () => proxy.table,
});
}
if ('cell' in proxy) {
staticProviders.push({
provide: TanStackTableCellToken,
useValue: () => proxy.cell,
});
}
if ('header' in proxy) {
staticProviders.push({
provide: TanStackTableHeaderToken,
useValue: () => proxy.header,
});
}
return Injector.create({
parent: parentInjector ?? this.#injector(),
providers: [
...staticProviders,
{ provide: FlexRenderComponentProps, useValue: proxy },
],
});
}
}
/**
* Simplified directive wrapper of `*flexRender`.
*
* Use this utility component to render headers, cells, or footers with custom markup.
*
* Only one prop (`cell`, `header`, or `footer`) may be passed based on the used selector.
*
* @example
* ```html
* <td *flexRenderCell="cell; let cell">{{cell}}</td>
* <th *flexRenderHeader="header; let header">{{header}}</th>
* <th *flexRenderFooter="footer; let footer">{{footer}}</th>
* ```
*
* This replaces calling `*flexRender` directly like this:
* ```html
* <td *flexRender="cell.column.columnDef.cell; props: cell.getContext(); let cell">{{cell}}</td>
* <td *flexRender="header.column.columnDef.header; props: header.getContext(); let header">{{header}}</td>
* <td *flexRender="footer.column.columnDef.footer; props: footer.getContext(); let footer">{{footer}}</td>
* ```
*
* Can be imported through {@link FlexRenderCell} or {@link FlexRender}, with
* the latter preferred.
*
* @example
* ```ts
* import {FlexRender} from '@tanstack/angular-table'
*
* @Component({
* // ...
* imports: [
* FlexRender
* ]
* })
* ```
*/
class FlexRenderCell {
cell = input(undefined, { ...(ngDevMode ? { debugName: "cell" } : /* istanbul ignore next */ {}), alias: 'flexRenderCell' });
header = input(undefined, { ...(ngDevMode ? { debugName: "header" } : /* istanbul ignore next */ {}), alias: 'flexRenderHeader' });
footer = input(undefined, { ...(ngDevMode ? { debugName: "footer" } : /* istanbul ignore next */ {}), alias: 'flexRenderFooter' });
#renderData = computed(() => {
const cell = this.cell();
const header = this.header();
const footer = this.footer();
if (cell) {
const def = cell.column.columnDef;
const groupingCell = cell;
const groupingDef = def;
if (groupingCell.getIsAggregated?.()) {
return [groupingDef.aggregatedCell ?? def.cell, cell.getContext()];
}
if (groupingCell.getIsPlaceholder?.()) {
return [null, null];
}
return [cell.column.columnDef.cell, cell.getContext()];
}
if (header) {
return [header.column.columnDef.header, header.getContext()];
}
if (footer) {
return [footer.column.columnDef.footer, footer.getContext()];
}
return [null, null];
}, { ...(ngDevMode ? { debugName: "#renderData" } : /* istanbul ignore next */ {}), equal: (a, b) => {
return a[0] === b[0] && a[1] === b[1];
} });
#injector = inject(Injector);
#templateRef = inject(TemplateRef);
#viewContainerRef = inject(ViewContainerRef);
constructor() {
const content = computed(() => this.#renderData()[0], /* @ts-ignore */
...(ngDevMode ? [{ debugName: "content" }] : /* istanbul ignore next */ []));
const props = computed(() => this.#renderData()[1], /* @ts-ignore */
...(ngDevMode ? [{ debugName: "props" }] : /* istanbul ignore next */ []));
const renderer = new FlexViewRenderer({
content: content,
props: props,
injector: () => this.#injector,
templateRef: this.#templateRef,
viewContainerRef: this.#viewContainerRef,
});
renderer.mount();
inject(DestroyRef).onDestroy(() => {
renderer.destroy();
});
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: FlexRenderCell, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.0", type: FlexRenderCell, isStandalone: true, selector: "ng-template[flexRenderCell], ng-template[flexRenderFooter], ng-template[flexRenderHeader]", inputs: { cell: { classPropertyName: "cell", publicName: "flexRenderCell", isSignal: true, isRequired: false, transformFunction: null }, header: { classPropertyName: "header", publicName: "flexRenderHeader", isSignal: true, isRequired: false, transformFunction: null }, footer: { classPropertyName: "footer", publicName: "flexRenderFooter", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: FlexRenderCell, decorators: [{
type: Directive,
args: [{
selector: 'ng-template[flexRenderCell], ng-template[flexRenderFooter], ng-template[flexRenderHeader]',
}]
}], ctorParameters: () => [], propDecorators: { cell: [{ type: i0.Input, args: [{ isSignal: true, alias: "flexRenderCell", required: false }] }], header: [{ type: i0.Input, args: [{ isSignal: true, alias: "flexRenderHeader", required: false }] }], footer: [{ type: i0.Input, args: [{ isSignal: true, alias: "flexRenderFooter", required: false }] }] } });
/**
* Use this utility directive to render headers, cells, or footers with custom markup.
*
* Note: If you are rendering cell, header, or footer without custom context or other props,
* you can use the {@link FlexRenderCell} directive as shorthand instead .
*
* @example
* ```ts
* import {FlexRender} from '@tanstack/angular-table';
*
* @Component({
* imports: [FlexRender],
* template: `
* <td
* *flexRender="
* cell.column.columnDef.cell;
* props: cell.getContext();
* let cell"
* >
* {{cell}}
* </td>
*
* <th
* *flexRender="
* header.column.columnDef.header;
* props: header.getContext();
* let header"
* >
* {{header}}
* </td>
*
* <td
* *flexRender="
* footer.column.columnDef.footer;
* props: footer.getContext();
* let footer"
* >
* {{footer}}
* </td>
* `,
* })
* class App {
* }
* ```
*
* Can be imported through {@link FlexRenderDirective} or {@link FlexRender},
* with the latter preferred.
*/
class FlexRenderDirective {
content = input(undefined, { ...(ngDevMode ? { debugName: "content" } : /* istanbul ignore next */ {}), alias: 'flexRender' });
props = input({}, { ...(ngDevMode ? { debugName: "props" } : /* istanbul ignore next */ {}), alias: 'flexRenderProps' });
injector = input(inject(Injector), { ...(ngDevMode ? { debugName: "injector" } : /* istanbul ignore next */ {}), alias: 'flexRenderInjector' });
#viewContainerRef = inject(ViewContainerRef);
#templateRef = inject(TemplateRef);
constructor() {
const renderer = new FlexViewRenderer({
content: this.content,
props: this.props,
injector: this.injector,
templateRef: this.#templateRef,
viewContainerRef: this.#viewContainerRef,
});
renderer.mount();
inject(DestroyRef).onDestroy(() => {
renderer.destroy();
});
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: FlexRenderDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.0", type: FlexRenderDirective, isStandalone: true, selector: "ng-template[flexRender]", inputs: { content: { classPropertyName: "content", publicName: "flexRender", isSignal: true, isRequired: false, transformFunction: null }, props: { classPropertyName: "props", publicName: "flexRenderProps", isSignal: true, isRequired: false, transformFunction: null }, injector: { classPropertyName: "injector", publicName: "flexRenderInjector", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.0", ngImport: i0, type: FlexRenderDirective, decorators: [{
type: Directive,
args: [{
selector: 'ng-template[flexRender]',
}]
}], ctorParameters: () => [], propDecorators: { content: [{ type: i0.Input, args: [{ isSignal: true, alias: "flexRender", required: false }] }], props: [{ type: i0.Input, args: [{ isSignal: true, alias: "flexRenderProps", required: false }] }], injector: [{ type: i0.Input, args: [{ isSignal: true, alias: "flexRenderInjector", required: false }] }] } });
/**
* Implementation from @tanstack/angular-query
* {https://github.com/TanStack/query/blob/main/packages/angular-query-experimental/src/util/lazy-init/lazy-init.ts}
*/
function lazyInit(initializer) {
let object = null;
const initializeObject = () => {
if (!object) {
object = untracked(() => initializer());
}
};
queueMicrotask(() => initializeObject());
const table = () => { };
return new Proxy(table, {
apply(target, thisArg, argArray) {
initializeObject();
if (typeof object === 'function') {
return Reflect.apply(object, thisArg, argArray);
}
return Reflect.apply(target, thisArg, argArray);
},
get(_, prop, receiver) {
initializeObject();
return Reflect.get(object, prop, receiver);
},
has(_, prop) {
initializeObject();
return Reflect.has(object, prop);
},
ownKeys() {
initializeObject();
return Reflect.ownKeys(object);
},
getOwnPropertyDescriptor() {
return {
enumerable: true,
configurable: true,
};
},
});
}
function signalToReadonlyAtom(signal, injector, debugName) {
const _signal = Object.assign(signal, {
get: () => signal(),
subscribe: (observer) => {
return untracked(() => toObservable(computed(signal), { injector: injector }).subscribe(observer));
},
});
if (debugName) {
_signal.toString = () => debugName;
}
return _signal;
}
function signalToWritableAtom(signal, injector, debugName) {
const _signal = Object.assign(signal.asReadonly(), {
set: (updater) => {
typeof updater === 'function'
? signal.update(updater)
: signal.set(updater);
},
get: () => signal(),
subscribe: (observer) => {
return untracked(() => toObservable(computed(signal), { injector: injector }).subscribe(observer));
},
});
if (debugName) {
_signal.toString = () => debugName;
}
return _signal;
}
/**
* Creates the table-core reactivity bindings used by the Angular adapter.
*
* Table state atoms are backed by TanStack Store atoms. The options store stays
* framework-native because row-model APIs read `table.options` directly during
* render. Readonly table atoms bridge Store dependency tracking into Angular
* computed signals.
*/
function angularReactivity(injector) {
const ngZone = injector.get(NgZone);
const subscriptions = new Set();
return {
createOptionsStore: true,
wrapExternalAtoms: true,
addSubscription: (subscription) => {
subscriptions.add(subscription);
},
unmount: () => {
subscriptions.forEach((s) => s.unsubscribe());
subscriptions.clear();
},
schedule: (fn) => ngZone.runOutsideAngular(() => queueMicrotask(fn)),
createReadonlyAtom: (fn, options) => {
const signal = computed(() => fn(), {
equal: options?.compare,
debugName: options?.debugName,
});
return signalToReadonlyAtom(signal, injector, options?.debugName);
},
createWritableAtom: (value, options) => {
const writableSignal = signal(value, {
equal: options?.compare,
debugName: options?.debugName,
});
return signalToWritableAtom(writableSignal, injector, options?.debugName);
},
untrack: untracked,
batch: (fn) => fn(),
};
}
/**
* Creates and returns an Angular-reactive table instance.
*
* The initializer is intentionally re-evaluated whenever any signal read inside it changes.
* This is how the adapter keeps the table in sync with Angular's reactivity model.
*
* Because of that behavior, keep expensive/static values (for example `columns`, feature setup, row models)
* as stable references outside the initializer, and only read reactive state (`data()`, pagination/filter/sorting signals, etc.)
* inside it.
*
* The returned table is also signal-reactive: table state and table APIs are wired for Angular signals, so you can safely consume table methods inside `computed(...)` and `effect(...)`.
*
* @example
* 1. Register the table features you need
* ```ts
* // Register only the features you need
* import {tableFeatures, rowPaginationFeature} from '@tanstack/angular-table';
* const features = tableFeatures({
* rowPaginationFeature,
* // ...all other features you need
* })
*
* // Use all table core features
* import {stockFeatures} from '@tanstack/angular-table';
* const features = tableFeatures(stockFeatures);
* ```
* 2. Prepare the table columns
* ```ts
* import {ColumnDef} from '@tanstack/angular-table';
*
* type MyData = {}
*
* const columns: ColumnDef<typeof features, MyData>[] = [
* // ...column definitions
* ]
*
* // or using createColumnHelper
* import {createColumnHelper} from '@tanstack/angular-table';
* const columnHelper = createColumnHelper<typeof features, MyData>();
* const columns = columnHelper.columns([
* columnHelper.accessor(...),
* // ...other columns
* ])
* ```
* 3. Create the table instance with `injectTable`
* ```ts
* const table = injectTable(() => {
* // ...table options,
* features,
* columns: columns,
* data: myDataSignal(),
* })
* ```
*
* @returns An Angular-reactive TanStack Table instance.
*/
function injectTable(options) {
assertInInjectionContext(injectTable);
const injector = inject(Injector);
const ngZone = inject(NgZone);
return ngZone.runOutsideAngular(() => lazyInit(() => {
// Explicit type arguments skip generic inference from the spread object
// (a type-check hot spot); the spread only adds the angular reactivity
// binding to `features`.
const table = constructTable({
...options(),
features: {
coreReactivityFeature: angularReactivity(injector),
...options().features,
},
});
injector.get(DestroyRef).onDestroy(() => {
table._reactivity.unmount?.();
});
let isMount = true;
effect(() => {
const newOptions = options();
if (isMount) {
isMount = false;
return;
}
untracked(() => table.setOptions((previous) => ({
...previous,
...newOptions,
})));
}, { injector, debugName: 'tableOptionsUpdate' });
return table;
}));
}
/**
* Creates app-scoped Angular table helpers with features, row models, and
* renderable component maps pre-bound.
*
* Use this when an app or design system wants typed `injectAppTable`,
* pre-bound column helpers, and typed table/cell/header context injection
* helpers without repeating the same feature and component generics.
*
* @example
* ```ts
* const { inje