Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ffd84ec
fix array types and imports
riccardoperra Feb 14, 2025
56ba513
toComputed return a named function, add more granular flags
riccardoperra Feb 14, 2025
7168077
remove table proxy implementation, improve angular reactivity feature
riccardoperra Feb 14, 2025
90a8622
wip: add flex render custom notifier when table state change
riccardoperra Feb 14, 2025
961a7df
ci: apply automated fixes
autofix-ci[bot] Feb 14, 2025
f09feee
wip: angularReactivityFeature replace _rootNotifier with get
riccardoperra Feb 14, 2025
634c18a
ci: apply automated fixes
autofix-ci[bot] Feb 14, 2025
0a6c2a1
wip: some renames, types improvements and test cases
riccardoperra Feb 15, 2025
57466bb
ci: apply automated fixes
autofix-ci[bot] Feb 15, 2025
350d55a
wip: fix types, add reactive symbol to detect whether a property is r…
riccardoperra Feb 15, 2025
d53121f
wip: setup benchmark test
riccardoperra Feb 15, 2025
6dc2349
wip: skipBaseProperties skip properties that ends with Handler
riccardoperra Feb 15, 2025
810c37a
wip: skipBaseProperties skip properties that ends with Handler, pass …
riccardoperra Feb 15, 2025
4037ed9
Merge remote-tracking branch 'origin/alpha' into feat/alpha_angular_g…
riccardoperra Apr 5, 2025
d226238
align base branch, fix lazy init
riccardoperra Apr 5, 2025
a42dca8
Merge remote-tracking branch 'origin/store' into feat/alpha_angular_g…
riccardoperra Jan 6, 2026
fb6364a
fix table helper types
riccardoperra Jan 6, 2026
337bfeb
cleanup lazy signal initializer
riccardoperra Jan 6, 2026
f6cafde
table reactivity feature cleanup
riccardoperra Jan 6, 2026
63e9b51
flex render cleanup and export FlexRender as directive
riccardoperra Jan 6, 2026
698dfea
refactor injectTable to work with tanstack store reactivity
riccardoperra Jan 6, 2026
fe29a78
fix injectTable test
riccardoperra Jan 6, 2026
b3badb7
fix test utils test
riccardoperra Jan 6, 2026
78ba6f9
refactor reactivity handling in angular features and improve prototyp…
riccardoperra Jan 6, 2026
1676d2a
fix row-dnd tsconfig
riccardoperra Jan 6, 2026
e4f6fdf
fix test
riccardoperra Jan 6, 2026
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
2 changes: 1 addition & 1 deletion examples/angular/row-dnd/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"sourceMap": true,
"declaration": false,
"experimentalDecorators": true,
"moduleResolution": "node",
"moduleResolution": "bundler",
"importHelpers": true,
"target": "ES2022",
"module": "ES2022",
Expand Down
1 change: 1 addition & 0 deletions packages/angular-table/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"test:build": "publint --strict",
"test:eslint": "eslint ./src",
"test:lib": "vitest",
"test:benchmark": "vitest bench",
"test:lib:dev": "vitest --watch",
"test:types": "tsc && vitest --typecheck"
},
Expand Down
220 changes: 99 additions & 121 deletions packages/angular-table/src/angularReactivityFeature.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { computed, signal } from '@angular/core'
import { toComputed } from './proxy'
import { computed, isSignal, signal } from '@angular/core'
import { defineLazyComputedProperty, markReactive } from './reactivityUtils'
import type { Signal } from '@angular/core'
import type {
RowData,
Expand All @@ -20,16 +20,32 @@ declare module '@tanstack/table-core' {
> extends Table_AngularReactivity<TFeatures, TData> {}
}

type SkipPropertyFn = (property: string) => boolean

export interface AngularReactivityFlags {
header: boolean | SkipPropertyFn
column: boolean | SkipPropertyFn
row: boolean | SkipPropertyFn
cell: boolean | SkipPropertyFn
}

interface TableOptions_AngularReactivity {
enableExperimentalReactivity?: boolean
reactivity?: Partial<AngularReactivityFlags>
}

interface Table_AngularReactivity<
TFeatures extends TableFeatures,
TData extends RowData,
> {
_rootNotifier?: Signal<Table<TFeatures, TData>>
_setRootNotifier?: (signal: Signal<Table<TFeatures, TData>>) => void
/**
* Returns a table signal that updates whenever the table state or options changes.
*/
get: Signal<Table<TFeatures, TData>>
/**
* @internal
*/
setTableNotifier: (signal: Signal<Table<TFeatures, TData>>) => void
}

interface AngularReactivityFeatureConstructors<
Expand All @@ -40,75 +56,96 @@ interface AngularReactivityFeatureConstructors<
Table: Table_AngularReactivity<TFeatures, TData>
}

const getUserSkipPropertyFn = (
value: undefined | null | boolean | SkipPropertyFn,
defaultPropertyFn: SkipPropertyFn,
) => {
if (typeof value === 'boolean') {
return defaultPropertyFn
}

return value ?? defaultPropertyFn
}

export function constructAngularReactivityFeature<
TFeatures extends TableFeatures,
TData extends RowData,
>(): TableFeature<AngularReactivityFeatureConstructors<TFeatures, TData>> {
return {
getDefaultTableOptions(table) {
return { enableExperimentalReactivity: false }
return {
reactivity: {
header: true,
column: true,
row: true,
cell: true,
},
}
},
constructTableAPIs: (table) => {
if (!table.options.enableExperimentalReactivity) {
return
}
const rootNotifier = signal<Signal<any> | null>(null)

table._rootNotifier = computed(() => rootNotifier()?.(), {
equal: () => false,
}) as any

table._setRootNotifier = (notifier) => {
rootNotifier.set(notifier)
}

// Set reactive props on table instance
setReactiveProps(table._rootNotifier!, table, {
table.setTableNotifier = (notifier) => rootNotifier.set(notifier)
table.get = computed(() => rootNotifier()!(), { equal: () => false })
markReactive(table)
setReactiveProps(table.get, table, {
overridePrototype: false,
skipProperty: skipBaseProperties,
})
},

assignCellPrototype: (prototype, table) => {
if (!table.options.enableExperimentalReactivity) {
if (table.options.reactivity?.cell === false) {
return
}
// Store reference to table for runtime access
;(prototype as any).__angularTable = table
setReactivePropsOnPrototype(prototype, {
skipProperty: skipBaseProperties,
markReactive(prototype)
setReactiveProps(table.get, prototype, {
skipProperty: getUserSkipPropertyFn(
table.options.reactivity?.cell,
skipBaseProperties,
),
overridePrototype: true,
})
},

assignColumnPrototype: (prototype, table) => {
if (!table.options.enableExperimentalReactivity) {
if (table.options.reactivity?.column === false) {
return
}
// Store reference to table for runtime access
;(prototype as any).__angularTable = table
setReactivePropsOnPrototype(prototype, {
skipProperty: skipBaseProperties,
markReactive(prototype)
setReactiveProps(table.get, prototype, {
skipProperty: getUserSkipPropertyFn(
table.options.reactivity?.cell,
skipBaseProperties,
),
overridePrototype: true,
})
},

assignHeaderPrototype: (prototype, table) => {
if (!table.options.enableExperimentalReactivity) {
if (table.options.reactivity?.header === false) {
return
}
// Store reference to table for runtime access
;(prototype as any).__angularTable = table
setReactivePropsOnPrototype(prototype, {
skipProperty: skipBaseProperties,
markReactive(prototype)
setReactiveProps(table.get, prototype, {
skipProperty: getUserSkipPropertyFn(
table.options.reactivity?.cell,
skipBaseProperties,
),
overridePrototype: true,
})
},

assignRowPrototype: (prototype, table) => {
if (!table.options.enableExperimentalReactivity) {
if (table.options.reactivity?.row === false) {
return
}
// Store reference to table for runtime access
;(prototype as any).__angularTable = table
setReactivePropsOnPrototype(prototype, {
skipProperty: skipBaseProperties,
markReactive(prototype)
setReactiveProps(table.get, prototype, {
skipProperty: getUserSkipPropertyFn(
table.options.reactivity?.cell,
skipBaseProperties,
),
overridePrototype: true,
})
},
}
Expand All @@ -117,101 +154,42 @@ export function constructAngularReactivityFeature<
export const angularReactivityFeature = constructAngularReactivityFeature()

function skipBaseProperties(property: string): boolean {
return property.endsWith('Handler') || !property.startsWith('get')
return (
// equals `getContext`
property === 'getContext' ||
// start with `_`
property[0] === '_' ||
// doesn't start with `get`, but faster
!(property[0] === 'g' && property[1] === 'e' && property[2] === 't') ||
// ends with `Handler`
property.endsWith('Handler')
)
}

export function setReactiveProps(
function setReactiveProps(
notifier: Signal<Table<any, any>>,
obj: { [key: string]: any },
options: {
overridePrototype?: boolean
skipProperty: (property: string) => boolean
},
) {
const { skipProperty } = options

for (const property in obj) {
const value = obj[property]
if (typeof value !== 'function') {
continue
}
if (skipProperty(property)) {
if (
isSignal(value) ||
typeof value !== 'function' ||
skipProperty(property)
) {
continue
}
Object.defineProperty(obj, property, {
enumerable: true,
configurable: false,
value: toComputed(notifier, value, property),
defineLazyComputedProperty(notifier, {
valueFn: value,
property,
originalObject: obj,
overridePrototype: options.overridePrototype,
})
}
}

function setReactivePropsOnPrototype(
prototype: Record<string, any>,
options: {
skipProperty: (property: string) => boolean
},
) {
const { skipProperty } = options

// Wrap methods on the prototype that will be lazily wrapped at instance access time
// We intercept property access on the prototype to wrap methods when they're first accessed
const propertyNames = Object.getOwnPropertyNames(prototype)
for (const property of propertyNames) {
if (property === 'table' || property.startsWith('__angular')) {
continue
}
const descriptor = Object.getOwnPropertyDescriptor(prototype, property)
if (descriptor && typeof descriptor.value === 'function') {
if (skipProperty(property)) {
continue
}
// Store original method
const originalMethod = descriptor.value
// Replace with a function that will be wrapped at instance creation time
Object.defineProperty(prototype, property, {
enumerable: descriptor.enumerable,
configurable: descriptor.configurable,
value: function (this: any, ...args: Array<any>) {
// Get the table from the instance
const table = this.table
if (table && table._rootNotifier) {
// Check if already wrapped on this instance
const instanceDescriptor = Object.getOwnPropertyDescriptor(
this,
property,
)
if (
instanceDescriptor &&
instanceDescriptor.value?.__angularWrapped
) {
return instanceDescriptor.value.apply(this, args)
}
// Wrap the method with toComputed using the table's rootNotifier
// Create a wrapper function that calls the original method
const boundMethod = originalMethod.bind(this)
const wrapped = toComputed(
table._rootNotifier,
boundMethod,
property,
) as any
wrapped.__angularWrapped = true
// Cache the wrapped version on the instance
Object.defineProperty(this, property, {
enumerable: true,
configurable: true,
value: wrapped,
})
// Call the wrapped function with args
if (args.length === 0) {
return wrapped()
} else if (args.length === 1) {
return wrapped(args[0])
} else {
return wrapped(args[0], ...args.slice(1))
}
}
return originalMethod.apply(this, args)
},
})
}
}
}
7 changes: 2 additions & 5 deletions packages/angular-table/src/createTableHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import type {
export type TableHelper<
TFeatures extends TableFeatures,
TData extends RowData = any,
> = Omit<TableHelper_Core<TFeatures, TData>, 'tableCreator'> & {
> = Omit<TableHelper_Core<TFeatures>, 'tableCreator'> & {
injectTable: <TInferData extends TData, TSelected = {}>(
tableOptions: () => Omit<
TableOptions<TFeatures, TInferData>,
Expand All @@ -30,10 +30,7 @@ export function createTableHelper<
tableHelperOptions: TableHelperOptions<TFeatures, TData>,
): TableHelper<TFeatures, TData> {
const tableHelper = constructTableHelper(
injectTable as unknown as (
tableOptions: () => TableOptions<TFeatures, TData>,
selector?: (state: TableState<TFeatures>) => any,
) => AngularTable<TFeatures, TData, any>,
injectTable as unknown as any,
tableHelperOptions,
)
return {
Expand Down
Loading
Loading