Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fancy-times-jog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/angular-table': patch
---

improve flexRender instance reuse and reduce adapter allocations
1 change: 1 addition & 0 deletions packages/angular-table/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"scripts": {
"build": "ng-packagr -p ng-package.json -c tsconfig.build.json && rimraf ./dist/package.json && find dist -name '*.map' -delete",
"build:types": "tsc --emitDeclarationOnly",
"bench:flex-render": "vitest bench --run tests/flex-render/flex-render.bench.ts",
"clean": "rimraf ./build && rimraf ./dist",
"test:build": "publint --strict",
"test:eslint": "eslint ./src",
Expand Down
29 changes: 14 additions & 15 deletions packages/angular-table/src/flex-render/flags.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,33 @@
/**
* Flags used to manage and optimize the rendering lifecycle of the content of the cell
* while using {@link FlexViewRenderer}.
* Flags used to manage and optimize the rendering lifecycle of content inside
* {@link FlexViewRenderer}.
*/
export 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.
* The renderer has not completed its initial update. The first update creates
* the view from scratch, then clears this flag.
*/
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.
* 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,
/**
* 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
* 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,
/**
* 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
* A render function produced compatible content that must be synchronized
* with the mounted view without recreating it.
*/
Dirty: 1 << 3,
/**
* Indicates that the first render effect has been checked at least one time.
* The render-function effect completed its initial dependency read. That
* first execution records dependencies; subsequent executions update the
* view.
*/
RenderEffectChecked: 1 << 4,
} as const
110 changes: 87 additions & 23 deletions packages/angular-table/src/flex-render/flexRenderComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,35 @@ interface FlexRenderOptions<
TInputs extends Record<string, any>,
TOutputs extends Record<string, any>,
> {
/**
* Optional identity used to control component instance reuse.
*
* A rendered component is reused while both its component type and key are
* unchanged. Change the key to explicitly destroy and recreate the component,
* for example when new creation-time bindings, directives, or an injector
* need to be applied.
*
* Inputs and outputs do not affect component identity and are synchronized
* onto a reused component instance.
*
* @example
* ```ts
* flexRenderComponent(EditorComponent, {
* key: row.original.editorVersion,
* inputs: { value: row.original.value },
* })
* ```
*/
readonly key?: string | number
/**
* Native Angular bindings applied at component creation time via `createComponent`.
* Use this option to set inputs, outputs, or two-way bindings at creation time.
* Shouldn't be used together with {@link FlexRenderOptions#inputs} or {@link FlexRenderOptions#outputs} option.
*
* Bindings are creation-time configuration. Changing this array after the
* component has mounted does not update the existing component. Change
* {@link FlexRenderOptions#key} to recreate the component with new bindings.
*
* Binding input/outputs at creation time: {@link https://angular.dev/guide/components/programmatic-rendering#binding-inputs-outputs-and-setting-host-directives-at-creation}
*
* Two-way binding: {@link https://angular.dev/api/core/twoWayBinding}
Expand Down Expand Up @@ -54,6 +78,10 @@ interface FlexRenderOptions<
/**
* Directives to apply to the component at creation time.
*
* Directives are creation-time configuration. Changing this array after the
* component has mounted does not update the existing component. Change
* {@link FlexRenderOptions#key} to recreate the component with new directives.
*
* Binding directives at creation time: {@link https://angular.dev/guide/components/programmatic-rendering#binding-inputs-outputs-and-setting-host-directives-at-creation}
*
* Two-way binding: {@link https://angular.dev/api/core/twoWayBinding}
Expand Down Expand Up @@ -88,6 +116,8 @@ interface FlexRenderOptions<
*
* These values are assigned after the component has been created using
* [componentRef.setInput API](https://angular.dev/api/core/ComponentRef#setInput).
* On a reused component, omitted keys keep their current value. Pass
* `undefined` explicitly when an input needs to be cleared.
*
* Shouldn't be used together with {@link FlexRenderOptions#bindings} option
*/
Expand All @@ -101,7 +131,11 @@ interface FlexRenderOptions<
*/
readonly outputs?: TOutputs
/**
* Optional {@link Injector} that will be used when rendering the component
* Optional {@link Injector} that will be used when rendering the component.
*
* The injector is applied when the component is created. Change
* {@link FlexRenderOptions#key} to recreate a mounted component with a
* different injector.
*/
readonly injector?: Injector
}
Expand Down Expand Up @@ -151,14 +185,15 @@ export function flexRenderComponent<TComponent = any>(
component: Type<TComponent>,
options?: FlexRenderOptions<Inputs<TComponent>, Outputs<TComponent>>,
): FlexRenderComponent<TComponent> {
const { inputs, injector, outputs, directives, bindings } = options ?? {}
const { key, inputs, injector, outputs, directives, bindings } = options ?? {}
return new FlexRenderComponentInstance(
component,
inputs,
injector,
outputs,
directives,
bindings,
key,
)
}

Expand Down Expand Up @@ -208,17 +243,20 @@ export interface FlexRenderComponent<TComponent = any> {
*/
readonly component: Type<TComponent>
/**
* Reflected metadata about the component.
* Optional identity used together with the component type to decide whether
* an existing component instance can be reused.
*
* @see {@link FlexRenderOptions#key}
*/
readonly mirror: ComponentMirror<TComponent>
readonly key?: string | number
/**
* List of allowed input names.
* Reflected metadata about the component.
*/
readonly allowedInputNames: Array<string>
readonly mirror: ComponentMirror<TComponent>
/**
* List of allowed output names.
* Cached component metadata used by the flex renderer.
*/
readonly allowedOutputNames: Array<string>
readonly metadata: ResolvedComponentMetadata<TComponent>
/**
* Component instance outputs. Subscribed via {@link OutputEmitterRef#subscribe}
*
Expand Down Expand Up @@ -254,14 +292,13 @@ export interface FlexRenderComponent<TComponent = any> {
/**
* Wrapper class for a component that will be used as content for {@link FlexRenderDirective}
*
* Prefer {@link flexRenderComponent} helper for better type-safety
* Prefer {@link flexRenderComponent} for better type-safety.
*/
export class FlexRenderComponentInstance<
TComponent = any,
> implements FlexRenderComponent<TComponent> {
readonly mirror: ComponentMirror<TComponent>
readonly allowedInputNames: Array<string> = []
readonly allowedOutputNames: Array<string> = []
readonly metadata: ResolvedComponentMetadata<TComponent>

constructor(
readonly component: Type<TComponent>,
Expand All @@ -270,19 +307,46 @@ export class FlexRenderComponentInstance<
readonly outputs?: Outputs<TComponent>,
readonly directives?: CreateComponentDirectives,
readonly bindings?: CreateComponentBindings,
readonly key?: string | number,
) {
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)
this.metadata = resolveComponentTypeMetadata(component)
this.mirror = this.metadata.mirror
}
}

interface ResolvedComponentMetadata<TComponent = unknown> {
readonly mirror: ComponentMirror<TComponent>
readonly inputNames: ReadonlyMap<string, string>
readonly outputNames: ReadonlySet<string>
}

const typeCache = new WeakMap<Type<unknown>, ResolvedComponentMetadata>()

function resolveComponentTypeMetadata<T>(
type: Type<T>,
): ResolvedComponentMetadata<T> {
let metadata = typeCache.get(type) as ResolvedComponentMetadata<T> | undefined
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<string, string>()
const outputNames = new Set<string>()
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
}
Loading
Loading