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/lazy-tables-initialize.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/angular-table': patch
---

Simplify lazy initialization for injected table instances
77 changes: 77 additions & 0 deletions packages/angular-table/src/injectLazyInit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {
DestroyRef,
assertInInjectionContext,
inject,
untracked,
} from '@angular/core'

const notInitializedObject = Symbol('notInitializedObject')

export function injectLazyInit<T extends object>(
initializer: () => T,
cleanup: (object: T) => void,
): T {
assertInInjectionContext(injectLazyInit)
const destroyRef = injectCompatibleDestroyRef()
let object: T | typeof notInitializedObject = notInitializedObject

destroyRef.onDestroy(() => {
if (object !== notInitializedObject) {
cleanup(object)
}
})

const getObject = () => {
if (destroyRef.destroyed && object === notInitializedObject) {
throw new Error(
'[@tanstack/angular-table] Cannot initialize object after view is destroyed',
)
}
if (object === notInitializedObject) {
object = untracked(initializer)
Comment thread
benjavicente marked this conversation as resolved.
}
return object
}

return new Proxy<T>({} as T, {
Comment thread
riccardoperra marked this conversation as resolved.
get(_, prop, receiver) {
return Reflect.get(getObject(), prop, receiver)
},
has(_, prop) {
return Reflect.has(getObject(), prop)
},
ownKeys() {
return Reflect.ownKeys(getObject())
},
getOwnPropertyDescriptor() {
return {
enumerable: true,
configurable: true,
}
},
Comment thread
riccardoperra marked this conversation as resolved.
})
}

type CompatibleDestroyRef = DestroyRef & { readonly destroyed: boolean }

function injectCompatibleDestroyRef(): CompatibleDestroyRef {
const destroyRef = inject(DestroyRef)
if ('destroyed' in destroyRef) return destroyRef
return injectLegacyDestroyRef(destroyRef)
}
function injectLegacyDestroyRef(destroyRef: DestroyRef) {
// Compatibility for Angular version < 20
let destroyed = false
destroyRef.onDestroy(() => (destroyed = true))
return {
get destroyed() {
return destroyed
},
onDestroy(callback: () => void) {
if (destroyed) {
return () => {}
}
return destroyRef.onDestroy(callback)
},
}
}
44 changes: 17 additions & 27 deletions packages/angular-table/src/injectTable.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {
DestroyRef,
Injector,
NgZone,
assertInInjectionContext,
Expand All @@ -9,7 +8,7 @@ import {
untracked,
} from '@angular/core'
import { constructTable } from '@tanstack/table-core'
import { lazyInit } from './lazySignalInitializer'
import { injectLazyInit } from './injectLazyInit'
import { angularReactivity } from './reactivity'
import type {
RowData,
Expand Down Expand Up @@ -97,47 +96,38 @@ export function injectTable<
assertInInjectionContext(injectTable)
const injector = inject(Injector)
const ngZone = inject(NgZone)
const destroyRef = inject(DestroyRef)
const options = computed(() => optionsFactory())
const coreReactivityFeature = angularReactivity(injector)

const lazyTable = ngZone.runOutsideAngular(() =>
lazyInit(() => {
const currentOptions = options()
const features = {
coreReactivityFeature,
...currentOptions.features,
} satisfies TableFeatures
return constructTable<TFeatures, TData>({
...currentOptions,
features,
})
}),
injectLazyInit(
() => {
const currentOptions = options()
const features = {
coreReactivityFeature,
...currentOptions.features,
} satisfies TableFeatures
return constructTable<TFeatures, TData>({
...currentOptions,
features,
})
},
(table) => table._reactivity.unmount?.(),
),
)

destroyRef.onDestroy(() => {
if (lazyTable.initialized) {
lazyTable.value._reactivity.unmount?.()
}
})

let previousOptions: TableOptions<TFeatures, TData> | undefined = 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) => ({
lazyTable.setOptions((previous) => ({
...previous,
...currentOptions,
})),
)
previousOptions = currentOptions
},
{ injector, debugName: 'tableOptionsUpdate' },
)

return lazyTable.value
return lazyTable
}
62 changes: 0 additions & 62 deletions packages/angular-table/src/lazySignalInitializer.ts

This file was deleted.

28 changes: 27 additions & 1 deletion packages/angular-table/tests/injectTable.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isProxy } from 'node:util/types'
import { describe, expect, test, vi } from 'vitest'
import {
ChangeDetectionStrategy,
Expand Down Expand Up @@ -37,7 +38,7 @@ describe('injectTable', () => {

@Component({
selector: 'app-table',
template: ``,
template: `{{ table.getRowModel().rows.length }}`,
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
})
Expand Down Expand Up @@ -70,6 +71,7 @@ describe('injectTable', () => {
By.directive(TableComponent),
).componentInstance as TableComponent

expect(fixture.nativeElement.textContent.trim()).toBe('1')
expect(
tableComponent.table.getRowModel().rows.map((row) => row.original),
).toEqual([{ id: '1', title: 'First' }])
Expand All @@ -83,6 +85,7 @@ describe('injectTable', () => {
TestBed.tick()
await fixture.whenRenderingDone()

expect(fixture.nativeElement.textContent.trim()).toBe('2')
expect(
tableComponent.table.getRowModel().rows.map((row) => row.original),
).toEqual([
Expand All @@ -91,6 +94,22 @@ describe('injectTable', () => {
])
})

test('should not initialize when destroyed before the first effect', () => {
@Component({ standalone: true, template: `` })
class TableComponent {
readonly data = input.required<Array<{ id: string }>>()
readonly table = injectTable(() => ({
data: this.data(),
features: stockFeatures,
columns: [],
}))
}

const fixture = TestBed.createComponent(TableComponent)

expect(() => fixture.destroy()).not.toThrow()
})

describe('Proxy table', () => {
type Data = { id: string; title: string }
const data = signal<Array<Data>>([{ id: '1', title: 'Title' }])
Expand All @@ -107,6 +126,13 @@ describe('injectTable', () => {
})),
)

test('exposes a table instance through the proxy', () => {
expect(isProxy(table)).toBe(true)
expect(table).toBeDefined()
expect(typeof table).toBe('object')
expect(typeof table.getRowModel).toBe('function')
})

test('supports "in" operator', () => {
expect('atoms' in table).toBe(true)
expect('options' in table).toBe(true)
Expand Down
Loading
Loading