UNPKG

@tanstack/angular-table

Version:

Headless UI for building powerful tables & datagrids for Angular.

1,299 lines (1,284 loc) 53.9 kB
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