@tanstack/angular-table
Version:
Headless UI for building powerful tables & datagrids for Angular.
1,401 lines • 54.1 kB
JavaScript
import * as i0 from '@angular/core';
import { InjectionToken, input, inject, Directive, reflectComponentType, Injectable, ChangeDetectorRef, OutputEmitterRef, TemplateRef, Type, runInInjectionContext, computed, effect, untracked, Injector, ViewContainerRef, DestroyRef, assertInInjectionContext, NgZone, signal } 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.1", ngImport: i0, type: TanStackTableCell, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.1", 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.1", 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.1", ngImport: i0, type: TanStackTableHeader, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.1", 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.1", 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.1", ngImport: i0, type: TanStackTable, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.1", 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.1", 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 content inside
* {@link FlexViewRenderer}.
*/
const FlexRenderFlags = {
/**
* The renderer has not completed its initial update. The first update creates
* the view from scratch, then clears this flag.
*/
ViewFirstRender: 1 << 0,
/**
* The `content` input changed by reference, or its resolved value is not
* compatible with the mounted view. The next update recreates the view.
*/
ContentChanged: 1 << 1,
/**
* The `props` input changed by reference. Components receive the latest
* inputs and embedded templates are marked so their getter-backed context is
* evaluated again.
*/
PropsReferenceChanged: 1 << 2,
/**
* A render function produced compatible content that must be synchronized
* with the mounted view without recreating it.
*/
Dirty: 1 << 3,
/**
* The render-function effect completed its initial dependency read. That
* first execution records dependencies; subsequent executions update the
* view.
*/
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 { key, inputs, injector, outputs, directives, bindings } = options ?? {};
return new FlexRenderComponentInstance(component, inputs, injector, outputs, directives, bindings, key);
}
/**
* Wrapper class for a component that will be used as content for {@link FlexRenderDirective}
*
* Prefer {@link flexRenderComponent} for better type-safety.
*/
class FlexRenderComponentInstance {
component;
inputs;
injector;
outputs;
directives;
bindings;
key;
mirror;
metadata;
constructor(component, inputs, injector, outputs, directives, bindings, key) {
this.component = component;
this.inputs = inputs;
this.injector = injector;
this.outputs = outputs;
this.directives = directives;
this.bindings = bindings;
this.key = key;
this.metadata = resolveComponentTypeMetadata(component);
this.mirror = this.metadata.mirror;
}
}
const typeCache = new WeakMap();
function resolveComponentTypeMetadata(type) {
let metadata = typeCache.get(type);
if (metadata)
return metadata;
const mirror = reflectComponentType(type);
if (!mirror) {
throw new Error(`[@tanstack-table/angular] The provided symbol is not a component`);
}
const inputNames = new Map();
const outputNames = new Set();
for (const input of mirror.inputs) {
inputNames.set(input.propName, input.templateName);
if (input.templateName !== input.propName) {
inputNames.set(input.templateName, input.templateName);
}
}
for (const output of mirror.outputs) {
// Outputs are read from the component instance, so only their class
// property names are valid here. Template aliases are not instance keys.
outputNames.add(output.propName);
}
metadata = { mirror, inputNames, outputNames };
typeCache.set(type, metadata);
return metadata;
}
/**
* 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.1", ngImport: i0, type: FlexRenderComponentFactory, deps: [{ token: i0.ViewContainerRef }], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.1", ngImport: i0, type: FlexRenderComponentFactory });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.1", 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;
#componentData;
#creationKey;
#outputRegistry;
constructor(componentRef, componentData, componentInjector) {
this.componentRef = componentRef;
this.componentInjector = componentInjector;
this.#componentData = componentData;
this.#creationKey = componentData.key;
this.#outputRegistry = new FlexRenderComponentOutputManager();
this.componentRef.onDestroy(() => this.#outputRegistry.unsubscribeAll());
}
get component() {
return this.#componentData.component;
}
get inputs() {
return this.#componentData.inputs ?? {};
}
get outputs() {
return this.#componentData.outputs ?? {};
}
/**
*
* @param compare Whether the current ref component instance is the same as the given one
*/
eqType(compare) {
return compare.component === this.component;
}
canReuse(compare) {
return this.eqType(compare) && Object.is(compare.key, this.#creationKey);
}
/**
* Tries to update current component refs input by the new given content component.
*/
update(content) {
if (!this.canReuse(content))
return;
this.#syncInputs(content.inputs ?? {});
this.#syncOutputs(content.outputs ?? {});
this.#componentData = content;
}
markAsDirty() {
this.componentRef.injector.get(ChangeDetectorRef).markForCheck();
}
setInputs(inputs) {
for (const prop of Object.keys(inputs)) {
this.setInput(prop, inputs[prop]);
}
}
setInput(key, value) {
const inputName = this.#componentData.metadata.inputNames.get(key);
if (inputName === undefined)
return;
this.componentRef.setInput(inputName, value);
}
setOutputs(outputs) {
this.#outputRegistry.unsubscribeAll();
for (const prop of Object.keys(outputs)) {
this.setOutput(prop, outputs[prop]);
}
}
setOutput(key, emit) {
if (!this.#componentData.metadata.outputNames.has(key))
return;
const outputName = key;
if (!emit) {
this.#outputRegistry.unsubscribe(outputName);
return;
}
// If the output was already subscribed, just swap the listener callback.
const hasSubscription = this.#outputRegistry.hasSubscription(outputName);
this.#outputRegistry.setListener(outputName, emit);
if (hasSubscription) {
return;
}
const instance = this.componentRef.instance;
const output = instance[outputName];
if (output && output instanceof OutputEmitterRef) {
this.#outputRegistry.setSubscription(outputName, output.subscribe((value) => {
this.#outputRegistry.getListener(outputName)?.(value);
}));
}
}
#syncInputs(newInputs) {
// Inputs use patch semantics: omitted keys keep their current value, while
// an explicitly provided `undefined` is forwarded to Angular.
for (const prop of Object.keys(newInputs)) {
this.setInput(prop, newInputs[prop]);
}
}
#syncOutputs(outputs) {
const outputKeys = Object.keys(outputs);
const currentSubscribedKeys = this.#outputRegistry.getSubscribedKeys();
// When outputs updates, unsubscribe missing keys
for (const key of currentSubscribedKeys) {
if (!outputKeys.includes(key)) {
this.#outputRegistry.unsubscribe(key);
}
}
for (const prop of outputKeys) {
this.setOutput(prop, outputs[prop]);
}
}
}
class FlexRenderComponentOutputManager {
#outputSubscribers = new Map();
#outputListeners = new Map();
getSubscribedKeys() {
return Array.from(this.#outputListeners.keys());
}
hasSubscription(outputName) {
return this.#outputSubscribers.has(outputName);
}
setListener(outputName, callback) {
this.#outputListeners.set(outputName, callback);
}
getListener(outputName) {
return this.#outputListeners.get(outputName);
}
setSubscription(outputName, subscription) {
this.#outputSubscribers.set(outputName, subscription);
}
unsubscribeAll() {
for (const outputName of this.#outputListeners.keys()) {
this.unsubscribe(outputName);
}
}
unsubscribe(outputName) {
this.#outputSubscribers.get(outputName)?.unsubscribe();
this.#outputSubscribers.delete(outputName);
this.#outputListeners.delete(outputName);
}
}
/**
* 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;
#content;
constructor(initialContent, view) {
this.#content = initialContent;
this.view = view;
}
get content() {
return this.#content;
}
set content(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) {
if (this.content.kind === 'templateRef') {
// Template contexts are getter-backed. Mark the embedded view so Angular
// reads the latest props; the context object itself does not need to be
// replaced.
this.view.markForCheck();
}
}
dirtyCheck() {
if (this.content.kind === 'primitive') {
// Primitive contexts are getter-backed too. The renderer has already
// memoized the new value, so checking the view is enough to refresh
// `$implicit` without mutating the context.
this.view.markForCheck();
}
}
unmount() {
this.view.destroy();
}
canReuse(compare) {
return ((this.content.kind === 'primitive' && compare.kind === 'primitive') ||
(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. A props change can produce a new wrapper descriptor; its
// inputs and outputs are synchronized by `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': {
// Render functions commonly create a new descriptor on every run. If
// its type and key still identify the mounted instance, update that
// instance instead of recreating the component view.
if (this.view.eqType(this.content.content)) {
this.view.update(this.content.content);
}
this.view.markAsDirty();
break;
}
}
}
unmount() {
this.view.componentRef.destroy();
}
canReuse(compare) {
return ((this.content.kind === 'component' &&
compare.kind === 'component' &&
this.content.content === compare.content) ||
(this.content.kind === 'flexRenderComponent' &&
compare.kind === 'flexRenderComponent' &&
this.view.canReuse(compare.content)));
}
}
/**
* Internal view renderer used by Angular TanStack Table to implement `flexRender` directives.
*
* @internal Use FlexRender directives instead.
*/
class FlexViewRenderer {
#renderFlags = FlexRenderFlags.ViewFirstRender;
#renderView = null;
#outerRenderEffectRef = 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(() => {
return mapToFlexRenderTypedContent(this.#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() {
if (this.#outerRenderEffectRef) {
return this.#outerRenderEffectRef;
}
let previousContent;
let previousProps;
this.#outerRenderEffectRef = effect(() => {
const props = this.#props();
const content = this.#content();
if (!(this.#renderFlags & FlexRenderFlags.ViewFirstRender)) {
if (previousContent !== content) {
// A new content input may install a different render function (or
// stop rendering a function), so its dependency effect must be
// replaced. Incompatible values returned by the same function only
// recreate the view and keep the existing effect.
this.#destroyContentEffect();
this.#renderFlags |= FlexRenderFlags.ContentChanged;
}
if (previousProps !== props) {
this.#renderFlags |= FlexRenderFlags.PropsReferenceChanged;
}
}
untracked(() => this.#update());
if (this.#renderFlags & FlexRenderFlags.ViewFirstRender) {
this.#renderFlags &= ~FlexRenderFlags.ViewFirstRender;
}
previousContent = content;
previousProps = props;
}, { ...(ngDevMode ? { debugName: "#outerRenderEffectRef" } : /* istanbul ignore next */ {}), injector: this.#viewContainerRef.injector });
return this.#outerRenderEffectRef;
}
destroy() {
if (this.#outerRenderEffectRef) {
this.#outerRenderEffectRef.destroy();
this.#outerRenderEffectRef = null;
}
this.#destroyContentEffect();
this.#destroyView();
this.#renderFlags = FlexRenderFlags.ViewFirstRender;
}
#destroyContentEffect() {
if (this.#currentRenderEffectRef) {
this.#currentRenderEffectRef.destroy();
this.#currentRenderEffectRef = null;
}
this.#renderFlags &= ~FlexRenderFlags.RenderEffectChecked;
}
#update() {
if (this.#renderFlags &
(FlexRenderFlags.ContentChanged | FlexRenderFlags.ViewFirstRender)) {
this.#render();
return;
}
if (this.#renderFlags & FlexRenderFlags.PropsReferenceChanged) {
this.#renderView?.updateProps(this.#props());
this.#renderFlags &= ~FlexRenderFlags.PropsReferenceChanged;
}
if (this.#renderFlags & FlexRenderFlags.Dirty) {
this.#renderView?.dirtyCheck();
this.#renderFlags &= ~FlexRenderFlags.Dirty;
}
}
#render() {
// Resolved content can require a new view without changing the render
// function. Preserve its effect and checked state across that replacement.
this.#destroyView();
this.#renderFlags &=
FlexRenderFlags.ViewFirstRender | FlexRenderFlags.RenderEffectChecked;
const content = this.#getContentValue();
this.#renderView = this.#renderViewByContent(content);
// Render functions can read signals. Keep their dependency tracking in a
// dedicated effect so the outer effect remains responsible only for
// content and props input-reference changes.
if (!this.#currentRenderEffectRef &&
typeof untracked(this.#content) === 'function') {
this.#currentRenderEffectRef = effect(() => {
const latestContent = this.#getContentValue();
if (!(this.#renderFlags & FlexRenderFlags.RenderEffectChecked)) {
this.#renderFlags |= FlexRenderFlags.RenderEffectChecked;
return;
}
untracked(() => {
this.#renderFlags |= FlexRenderFlags.Dirty;
this.#doCheck(latestContent);
});
}, { ...(ngDevMode ? { debugName: "#currentRenderEffectRef" } : /* istanbul ignore next */ {}), injector: this.#viewContainerRef.injector });
}
}
#doCheck(latestContent) {
if (latestContent.kind === 'null' ||
!this.#renderView ||
!this.#renderView.canReuse(latestContent)) {
this.#renderFlags |= FlexRenderFlags.ContentChanged;
}
else {
this.#renderView.content = latestContent;
}
this.#update();
}
#destroyView() {
if (this.#renderView) {
this.#renderView.unmount();
this.#renderView = null;
}
}
#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);
}
return null;
}
#renderStringContent(template) {
const latestContent = () => untracked(this.#getContentValue);
const ref = this.#viewContainerRef.createEmbeddedView(this.#templateRef, {
get $implicit() {
// The view can be checked while an incompatible replacement is being
// scheduled. Only expose content that still belongs to this context.
const content = latestContent();
return content.kind === 'primitive' ? content.content : undefined;
},
});
return new FlexRenderTemplateView(template, ref);
}
#renderTemplateRefContent(template) {
const latestProps = () => untracked(this.#props);
const view = this.#viewContainerRef.createEmbeddedView(template.content, {
get $implicit() {
return latestProps();
},
}, { injector: this.#getInjector() });
return new FlexRenderTemplateView(template, view);
}
#renderComponent(flexRenderComponent) {
const componentInjector = this.#getInjector(flexRenderComponent.content.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.1", ngImport: i0, type: FlexRenderCell, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.1", 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.1", 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.1", ngImport: i0, type: FlexRenderDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.1", 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.1", 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 }] }] } });
function lazyInit(initializer) {
assertInInjectionContext(lazyInit);
let object = null;
const initializeObject = () => {
if (!object) {
object = untracked(() => initializer());
}
};
effect(() => initializeObject(), {
debugName: 'tableLazyInitEffect',
});
const table = () => { };
const proxy = 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,
};
},
});
return {
value: proxy,
get rawValue() {
return object;
},
get initialized() {
return !!object;
},
};
}
function signalToReadonlyAtom(signal, injector, debugName) {
const _signal = Object.assign(signal, {
get: () => signal(),
subscribe: (observer) => {
return untracked(() => toObservable(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(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(optionsFactory) {
assertInInjectionContext(injectTable);
const injector = inject(Injector);
const ngZone = inject(NgZone);
const destroyRef = inject(DestroyRef);
const options = computed(() => optionsFactory(), /* @ts-ignore */
...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
const coreReactivityFeature = angularReactivity(injector);
const lazyTable = ngZone.runOutsideAngular(() => lazyInit(() => {
const currentOptions = options();
const features = {
coreReactivityFeature,
...currentOptions.features,
};
return constructTable({
...currentOptions,
features,
});
}));
destroyRef.onDestroy(() => {
if (lazyTable.initialized) {
lazyTable.value._reactivity.unmount?.();
}
});
let previousOptions = undefined;
effect(() => {
const currentOptions = options();
// rawValue will be always valued here due to internal lazyInit effect
const tableInstance = lazyTable.rawValue;
if (previousOptions === currentOptions)
return;
untracked(() => tableInstance.setOptions((previous) => ({
...previous,
...currentOptions,
})));
previousOptions = currentOptions;
}, { injector, debugName: 'tableOptionsUpdate' });
return lazyTable.value;
}
/**
* 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 { injectAppTable, createAppColumnHelper } = createTableHook({
* features,
* tableComponents: {},
* cellComponents: {},
* headerComponents: {},
* })
* ```
*/
function createTableHook({ tableComponents, cellComponents, headerComponents, ...defaultTableOptions }) {
function injectTableContext$1() {
// `injectAppTable` Object.assign-es `tableComponents` onto the same table
// instance it returns (via `constructTableAPIs`), and that instance is what
// gets provided to DI, so this asserts the runtime shape.
return injectTableContext();
}
function injectTableHeaderContext$1() {
// `injectAppTable` Object.assign-es `headerComponents` onto the header
// prototype (via `assignHeaderPrototype`), so every header instance carries
// them. This asserts the runtime shape.
return injectTableHeaderContext();
}
function injectTableCellContext$1() {
// `injectAppTable` Object.assign-es `cellComponents` onto the cell prototype
// (via `assignCellPrototype`), so every cell instance carries them. This
// asserts the runtime shape.
return injectTableCellContext();
}
function injectFlexRenderHeaderContext() {
return injectFlexRenderContext();
}
function injectFlexRenderCellContext() {
return injectFlexRenderContext();
}
function injectAppTable(tableOptions) {
function appCell(cell) {
return cell;
}
function appHeader(header) {
return header;
}
function appFooter(footer) {
return footer;
}
const appTableFeatures = {
constructTableAPIs: (table) => {
Object.assign(table, tableComponents, { appCell, appHeader, appFooter });
},
assignCellPrototype(prototype) {
Object.assign(prototype, cellComponents);
},
assignHeaderPrototype(prototype) {
Object.assign(prototype, headerComponents);
},
};
return injectTable(() => {
return {
...defaultTableOptions,
...tableOptions(),
features: {
...defaultTableOptions.features,
appTableFeatures,
},
};
});
}
function createAppColumnHelper() {
// The runtime implementation is the same - components are attached at render time
// This cast provides the enhanced types for column definitions
return createColumnHelper();
}
return {
createAppColumnHelper,
injectTableContext: injectTableContext$1,
injectTableHeaderContext: injectTableHeaderContext$1,
injectTableCellContext: injectTableCellContext$1,
injectFlexRenderHeaderContext,
injectFlexRenderCellContext,
injectAppTable,
};
}
/**
* Constant helper to import FlexRender directives.
*
* You should prefer to use this constant over importing the directives separately,
* as it ensures you always have the correct set of directives over library updates.
*
* @see {@link FlexRenderDirective} and {@link FlexRenderCell} for more details on the directives included in this export.
*/
const FlexRender = [FlexRenderDirective, FlexRenderCell];
/**
* Generated bundle index. Do not edit.
*/
export { FlexRender, FlexRenderCell, FlexRenderComponentInstance, FlexRenderDirective, TanStackTable, TanStackTableCell, TanStackTableCellToken, TanStackTableHeader, TanStackTableHeaderToken, TanStackTableToken, createTableHook, flexRenderComponent, injectFlexRenderContext, injectTable, injectTableCellContext, injectTableContext, injectTableHeaderContext };
//# sourceMappingURL=tanstack-angular-table.mjs.map