@tanstack/angular-table 9.0.0-alpha.10 → 9.0.0-alpha.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,241 @@
1
+ import {
2
+ ChangeDetectorRef,
3
+ ComponentRef,
4
+ Injectable,
5
+ Injector,
6
+ KeyValueDiffer,
7
+ KeyValueDiffers,
8
+ OutputEmitterRef,
9
+ OutputRefSubscription,
10
+ ViewContainerRef,
11
+ } from '@angular/core'
12
+ import { FlexRenderComponent } from './flexRenderComponent'
13
+
14
+ @Injectable()
15
+ export class FlexRenderComponentFactory {
16
+ readonly #viewContainerRef: ViewContainerRef
17
+
18
+ constructor(viewContainerRef: ViewContainerRef) {
19
+ this.#viewContainerRef = viewContainerRef
20
+ }
21
+
22
+ createComponent<T>(
23
+ flexRenderComponent: FlexRenderComponent<T>,
24
+ componentInjector: Injector,
25
+ ): FlexRenderComponentRef<T> {
26
+ const componentRef = this.#viewContainerRef.createComponent(
27
+ flexRenderComponent.component,
28
+ {
29
+ injector: componentInjector,
30
+ directives: flexRenderComponent.directives,
31
+ bindings: flexRenderComponent.bindings ?? [],
32
+ },
33
+ )
34
+ const view = new FlexRenderComponentRef(
35
+ componentRef,
36
+ flexRenderComponent,
37
+ componentInjector,
38
+ )
39
+
40
+ const { inputs, outputs } = flexRenderComponent
41
+
42
+ if (inputs) view.setInputs(inputs)
43
+ if (outputs) view.setOutputs(outputs)
44
+
45
+ return view
46
+ }
47
+ }
48
+
49
+ export class FlexRenderComponentRef<T> {
50
+ readonly #keyValueDiffersFactory: KeyValueDiffers
51
+ #componentData: FlexRenderComponent<T>
52
+ #inputValueDiffer: KeyValueDiffer<string, unknown>
53
+
54
+ readonly #outputRegistry: FlexRenderComponentOutputManager
55
+
56
+ constructor(
57
+ readonly componentRef: ComponentRef<T>,
58
+ componentData: FlexRenderComponent<T>,
59
+ readonly componentInjector: Injector,
60
+ ) {
61
+ this.#componentData = componentData
62
+ this.#keyValueDiffersFactory = componentInjector.get(KeyValueDiffers)
63
+
64
+ this.#outputRegistry = new FlexRenderComponentOutputManager(
65
+ this.#keyValueDiffersFactory,
66
+ this.outputs,
67
+ )
68
+
69
+ this.#inputValueDiffer = this.#keyValueDiffersFactory
70
+ .find(this.inputs)
71
+ .create()
72
+ this.#inputValueDiffer.diff(this.inputs)
73
+
74
+ this.componentRef.onDestroy(() => this.#outputRegistry.unsubscribeAll())
75
+ }
76
+
77
+ get component() {
78
+ return this.#componentData.component
79
+ }
80
+
81
+ get inputs() {
82
+ return this.#componentData.inputs ?? {}
83
+ }
84
+
85
+ get outputs() {
86
+ return this.#componentData.outputs ?? {}
87
+ }
88
+
89
+ /**
90
+ * Get component input and output diff by the given item
91
+ */
92
+ diff(item: FlexRenderComponent<T>) {
93
+ return {
94
+ inputDiff: this.#inputValueDiffer.diff(item.inputs ?? {}),
95
+ outputDiff: this.#outputRegistry.diff(item.outputs ?? {}),
96
+ }
97
+ }
98
+ /**
99
+ *
100
+ * @param compare Whether the current ref component instance is the same as the given one
101
+ */
102
+ eqType(compare: FlexRenderComponent<T>): boolean {
103
+ return compare.component === this.component
104
+ }
105
+
106
+ /**
107
+ * Tries to update current component refs input by the new given content component.
108
+ */
109
+ update(content: FlexRenderComponent<T>) {
110
+ const eq = this.eqType(content)
111
+ if (!eq) return
112
+ const { inputDiff, outputDiff } = this.diff(content)
113
+ if (inputDiff) {
114
+ inputDiff.forEachAddedItem((item) =>
115
+ this.setInput(item.key, item.currentValue),
116
+ )
117
+ inputDiff.forEachChangedItem((item) =>
118
+ this.setInput(item.key, item.currentValue),
119
+ )
120
+ inputDiff.forEachRemovedItem((item) => this.setInput(item.key, undefined))
121
+ }
122
+ if (outputDiff) {
123
+ outputDiff.forEachAddedItem((item) => {
124
+ this.setOutput(item.key, item.currentValue)
125
+ })
126
+ outputDiff.forEachChangedItem((item) => {
127
+ if (item.currentValue) {
128
+ this.#outputRegistry.setListener(item.key, item.currentValue)
129
+ } else {
130
+ this.#outputRegistry.unsubscribe(item.key)
131
+ }
132
+ })
133
+ outputDiff.forEachRemovedItem((item) => {
134
+ this.#outputRegistry.unsubscribe(item.key)
135
+ })
136
+ }
137
+
138
+ this.#componentData = content
139
+ }
140
+
141
+ markAsDirty(): void {
142
+ this.componentRef.injector.get(ChangeDetectorRef).markForCheck()
143
+ }
144
+
145
+ setInputs(inputs: Record<string, unknown>) {
146
+ for (const prop in inputs) {
147
+ this.setInput(prop, inputs[prop])
148
+ }
149
+ }
150
+
151
+ setInput(key: string, value: unknown) {
152
+ if (this.#componentData.allowedInputNames.includes(key)) {
153
+ this.componentRef.setInput(key, value)
154
+ }
155
+ }
156
+
157
+ setOutputs(
158
+ outputs: Record<
159
+ string,
160
+ OutputEmitterRef<unknown>['emit'] | null | undefined
161
+ >,
162
+ ) {
163
+ this.#outputRegistry.unsubscribeAll()
164
+ for (const prop in outputs) {
165
+ this.setOutput(prop, outputs[prop])
166
+ }
167
+ }
168
+
169
+ setOutput(
170
+ outputName: string,
171
+ emit: OutputEmitterRef<unknown>['emit'] | undefined | null,
172
+ ): void {
173
+ if (!this.#componentData.allowedOutputNames.includes(outputName)) return
174
+ if (!emit) {
175
+ this.#outputRegistry.unsubscribe(outputName)
176
+ return
177
+ }
178
+
179
+ const hasListener = this.#outputRegistry.hasListener(outputName)
180
+ this.#outputRegistry.setListener(outputName, emit)
181
+
182
+ if (hasListener) {
183
+ return
184
+ }
185
+
186
+ const instance = this.componentRef.instance
187
+ const output = instance[outputName as keyof typeof instance]
188
+ if (output && output instanceof OutputEmitterRef) {
189
+ output.subscribe((value) => {
190
+ this.#outputRegistry.getListener(outputName)?.(value)
191
+ })
192
+ }
193
+ }
194
+ }
195
+
196
+ class FlexRenderComponentOutputManager {
197
+ readonly #outputSubscribers: Record<string, OutputRefSubscription> = {}
198
+ readonly #outputListeners: Record<string, (...args: Array<any>) => void> = {}
199
+
200
+ readonly #valueDiffer: KeyValueDiffer<
201
+ string,
202
+ undefined | null | OutputEmitterRef<unknown>['emit']
203
+ >
204
+
205
+ constructor(keyValueDiffers: KeyValueDiffers, initialOutputs: any) {
206
+ this.#valueDiffer = keyValueDiffers.find(initialOutputs).create()
207
+ if (initialOutputs) {
208
+ this.#valueDiffer.diff(initialOutputs)
209
+ }
210
+ }
211
+
212
+ hasListener(outputName: string) {
213
+ return outputName in this.#outputListeners
214
+ }
215
+
216
+ setListener(outputName: string, callback: (...args: Array<any>) => void) {
217
+ this.#outputListeners[outputName] = callback
218
+ }
219
+
220
+ getListener(outputName: string) {
221
+ return this.#outputListeners[outputName]
222
+ }
223
+
224
+ unsubscribeAll(): void {
225
+ for (const prop in this.#outputSubscribers) {
226
+ this.unsubscribe(prop)
227
+ }
228
+ }
229
+
230
+ unsubscribe(outputName: string) {
231
+ if (outputName in this.#outputSubscribers) {
232
+ this.#outputSubscribers[outputName]?.unsubscribe()
233
+ delete this.#outputSubscribers[outputName]
234
+ delete this.#outputListeners[outputName]
235
+ }
236
+ }
237
+
238
+ diff(outputs: Record<string, OutputEmitterRef<unknown>['emit'] | undefined>) {
239
+ return this.#valueDiffer.diff(outputs ?? {})
240
+ }
241
+ }
@@ -0,0 +1,383 @@
1
+ import {
2
+ Injector,
3
+ computed,
4
+ effect,
5
+ runInInjectionContext,
6
+ untracked,
7
+ } from '@angular/core'
8
+ import { TanStackTableToken } from '../helpers/table'
9
+ import { TanStackTableCellToken } from '../helpers/cell'
10
+ import { TanStackTableHeaderToken } from '../helpers/header'
11
+ import { FlexRenderComponentProps } from './context'
12
+ import { FlexRenderFlags } from './flags'
13
+ import {
14
+ FlexRenderComponentView,
15
+ FlexRenderTemplateView,
16
+ mapToFlexRenderTypedContent,
17
+ } from './view'
18
+ import { flexRenderComponent } from './flexRenderComponent'
19
+ import { FlexRenderComponentFactory } from './flexRenderComponentFactory'
20
+ import type { FlexRenderComponent } from './flexRenderComponent'
21
+ import type {
22
+ EffectRef,
23
+ TemplateRef,
24
+ Type,
25
+ ViewContainerRef,
26
+ } from '@angular/core'
27
+ import type { FlexRenderTypedContent, FlexRenderView } from './view'
28
+ import type {
29
+ CellContext,
30
+ CellData,
31
+ HeaderContext,
32
+ RowData,
33
+ TableFeatures,
34
+ } from '@tanstack/table-core'
35
+
36
+ /**
37
+ * Content supported by the `flexRender` directive when declaring
38
+ * a table column header/cell.
39
+ */
40
+ export type FlexRenderContent<TProps extends NonNullable<unknown>> =
41
+ | string
42
+ | number
43
+ | Type<TProps>
44
+ | FlexRenderComponent<TProps>
45
+ | TemplateRef<{ $implicit: TProps }>
46
+ | null
47
+ | Record<any, any>
48
+ | undefined
49
+
50
+ /**
51
+ * Input content supported by the `flexRender` directives.
52
+ */
53
+ export type FlexRenderInputContent<TProps extends NonNullable<unknown>> =
54
+ | number
55
+ | string
56
+ | ((props: TProps) => FlexRenderContent<TProps>)
57
+ | null
58
+ | undefined
59
+
60
+ /**
61
+ * Options used to create a {@link FlexViewRenderer}.
62
+ *
63
+ * This renderer is designed to be embedded inside a directive/component that owns the
64
+ * `ViewContainerRef` and possibly a fallback `TemplateRef`.
65
+ */
66
+ interface RendererViewOptions<TProps extends NonNullable<unknown>> {
67
+ /**
68
+ * Signal-like getter that returns the latest renderable content.
69
+ */
70
+ content: () => FlexRenderInputContent<TProps>
71
+ /**
72
+ * Signal-like getter returning the current props/context object.
73
+ */
74
+ props: () => NoInfer<TProps>
75
+ /**
76
+ * Getter returning the base injector to evaluate render functions in.
77
+ *
78
+ * If `content` is a function, it will be executed inside this injection context
79
+ * via `runInInjectionContext` so Angular DI works as expected.
80
+ */
81
+ injector: () => Injector
82
+ /**
83
+ * Container that will host the dynamically created view/component.
84
+ */
85
+ viewContainerRef: ViewContainerRef
86
+ /**
87
+ * Fallback template used for primitive rendering.
88
+ *
89
+ * The template is instantiated with `$implicit` set to the primitive string/number.
90
+ */
91
+ templateRef: TemplateRef<unknown>
92
+ }
93
+
94
+ /**
95
+ * Internal view renderer used by Angular TanStack Table to implement `flexRender` directives.
96
+ *
97
+ * @internal Use FlexRender directives instead.
98
+ */
99
+ export class FlexViewRenderer<
100
+ TFeatures extends TableFeatures,
101
+ TRowData extends RowData,
102
+ TValue extends CellData,
103
+ TProps extends
104
+ | NonNullable<unknown>
105
+ | CellContext<TFeatures, TRowData, TValue>
106
+ | HeaderContext<TFeatures, TRowData, TValue>,
107
+ > {
108
+ #renderFlags = FlexRenderFlags.ViewFirstRender
109
+ #renderView: FlexRenderView<any> | null = null
110
+ #currentRenderEffectRef: EffectRef | null = null
111
+ #content: () => FlexRenderInputContent<TProps>
112
+ #props: () => TProps
113
+ #injector: () => Injector
114
+ #viewContainerRef: ViewContainerRef
115
+ #templateRef: TemplateRef<unknown>
116
+ #flexRenderComponentFactory: FlexRenderComponentFactory
117
+
118
+ readonly #getLatestContentValue = () => {
119
+ const content = this.#content()
120
+ const props = this.#props()
121
+ return typeof content !== 'function'
122
+ ? content
123
+ : runInInjectionContext(this.#injector(), () => content(props))
124
+ }
125
+
126
+ readonly #latestContent = computed(() => this.#getLatestContentValue())
127
+
128
+ #getContentValue = computed(() => {
129
+ const latestContent = this.#latestContent()
130
+ return mapToFlexRenderTypedContent(latestContent)
131
+ })
132
+
133
+ constructor(options: RendererViewOptions<TProps>) {
134
+ this.#content = options.content
135
+ this.#props = options.props
136
+ this.#injector = options.injector
137
+ this.#templateRef = options.templateRef
138
+ this.#viewContainerRef = options.viewContainerRef
139
+ this.#flexRenderComponentFactory = new FlexRenderComponentFactory(
140
+ this.#viewContainerRef,
141
+ )
142
+ }
143
+
144
+ mount(): EffectRef {
145
+ let previousContent: FlexRenderInputContent<TProps>
146
+ let previousProps: TProps
147
+
148
+ return effect(() => {
149
+ const props = this.#props()
150
+ const content = this.#content()
151
+
152
+ if (!(this.#renderFlags & FlexRenderFlags.ViewFirstRender)) {
153
+ if (previousContent !== content) {
154
+ this.#renderFlags |= FlexRenderFlags.ContentChanged
155
+ }
156
+ if (previousProps !== props) {
157
+ this.#renderFlags |= FlexRenderFlags.PropsReferenceChanged
158
+ }
159
+ }
160
+
161
+ untracked(() => this.#update())
162
+
163
+ if (FlexRenderFlags.ViewFirstRender & this.#renderFlags) {
164
+ this.#renderFlags &= ~FlexRenderFlags.ViewFirstRender
165
+ }
166
+
167
+ previousContent = content
168
+ previousProps = props
169
+ })
170
+ }
171
+
172
+ destroy(): void {
173
+ if (this.#currentRenderEffectRef) {
174
+ this.#currentRenderEffectRef.destroy()
175
+ this.#currentRenderEffectRef = null
176
+ }
177
+ if (this.#renderView) {
178
+ this.#renderView.unmount()
179
+ this.#renderView = null
180
+ }
181
+ }
182
+
183
+ #update() {
184
+ if (
185
+ this.#renderFlags &
186
+ (FlexRenderFlags.ContentChanged | FlexRenderFlags.ViewFirstRender)
187
+ ) {
188
+ this.#render()
189
+ return
190
+ }
191
+
192
+ if (this.#renderFlags & FlexRenderFlags.PropsReferenceChanged) {
193
+ if (this.#renderView) this.#renderView.updateProps(this.#props())
194
+ this.#renderFlags &= ~FlexRenderFlags.PropsReferenceChanged
195
+ }
196
+
197
+ if (this.#renderFlags & FlexRenderFlags.Dirty) {
198
+ if (this.#renderView) this.#renderView.dirtyCheck()
199
+ this.#renderFlags &= ~FlexRenderFlags.Dirty
200
+ }
201
+ }
202
+
203
+ #render() {
204
+ // When the view is recreated from scratch (content change or first render),
205
+ // we have to destroy the current effect listener since it will be recreated
206
+ // skipping the first call (FlexRenderFlags.RenderEffectChecked)
207
+ if (this.#shouldRecreateEntireView() && this.#currentRenderEffectRef) {
208
+ this.#currentRenderEffectRef.destroy()
209
+ this.#currentRenderEffectRef = null
210
+ this.#renderFlags &= ~FlexRenderFlags.RenderEffectChecked
211
+ }
212
+
213
+ this.#viewContainerRef.clear()
214
+ if (this.#renderView) {
215
+ this.#renderView.unmount()
216
+ this.#renderView = null
217
+ }
218
+
219
+ this.#renderFlags =
220
+ (this.#renderFlags & FlexRenderFlags.ViewFirstRender) |
221
+ (this.#renderFlags & FlexRenderFlags.RenderEffectChecked)
222
+
223
+ const resolvedContent = this.#getContentValue()
224
+ this.#renderView = this.#renderViewByContent(resolvedContent)
225
+ // If the content is a function `content(props)`, we initialize an effect
226
+ // to react to changes. If the current fn uses signals, we will set the DirtySignal flag
227
+ // to re-schedule the component updates
228
+ if (
229
+ !this.#currentRenderEffectRef &&
230
+ typeof untracked(this.#content) === 'function'
231
+ ) {
232
+ this.#currentRenderEffectRef = effect(
233
+ () => {
234
+ this.#latestContent()
235
+ if (!(this.#renderFlags & FlexRenderFlags.RenderEffectChecked)) {
236
+ this.#renderFlags |= FlexRenderFlags.RenderEffectChecked
237
+ return
238
+ }
239
+ this.#renderFlags |= FlexRenderFlags.Dirty
240
+ this.#doCheck()
241
+ },
242
+ { injector: this.#viewContainerRef.injector },
243
+ )
244
+ }
245
+ }
246
+
247
+ #shouldRecreateEntireView() {
248
+ return (
249
+ this.#renderFlags &
250
+ FlexRenderFlags.ContentChanged &
251
+ FlexRenderFlags.ViewFirstRender
252
+ )
253
+ }
254
+
255
+ #doCheck() {
256
+ const latestContent = this.#getContentValue()
257
+ if (latestContent.kind === 'null' || !this.#renderView) {
258
+ this.#renderFlags |= FlexRenderFlags.ContentChanged
259
+ } else {
260
+ this.#renderView.content = latestContent
261
+ const { kind: previousKind } = this.#renderView.previousContent
262
+ if (latestContent.kind !== previousKind) {
263
+ this.#renderFlags |= FlexRenderFlags.ContentChanged
264
+ }
265
+ }
266
+ this.#update()
267
+ }
268
+
269
+ #renderViewByContent(
270
+ content: FlexRenderTypedContent,
271
+ ): FlexRenderView<any> | null {
272
+ if (content.kind === 'primitive') {
273
+ return this.#renderStringContent(content)
274
+ } else if (content.kind === 'templateRef') {
275
+ return this.#renderTemplateRefContent(content)
276
+ } else if (content.kind === 'flexRenderComponent') {
277
+ return this.#renderComponent(content)
278
+ } else if (content.kind === 'component') {
279
+ return this.#renderCustomComponent(content)
280
+ } else {
281
+ return null
282
+ }
283
+ }
284
+
285
+ #renderStringContent(
286
+ template: Extract<FlexRenderTypedContent, { kind: 'primitive' }>,
287
+ ): FlexRenderTemplateView {
288
+ const context = () => {
289
+ const content = this.#content()
290
+ return typeof content === 'string' || typeof content === 'number'
291
+ ? content
292
+ : runInInjectionContext(this.#injector(), () =>
293
+ content?.(this.#props()),
294
+ )
295
+ }
296
+ const ref = this.#viewContainerRef.createEmbeddedView(this.#templateRef, {
297
+ get $implicit() {
298
+ return context()
299
+ },
300
+ })
301
+ return new FlexRenderTemplateView(template, ref)
302
+ }
303
+
304
+ #renderTemplateRefContent(
305
+ template: Extract<FlexRenderTypedContent, { kind: 'templateRef' }>,
306
+ ): FlexRenderTemplateView {
307
+ const latestContext = () => this.#props()
308
+ const view = this.#viewContainerRef.createEmbeddedView(
309
+ template.content,
310
+ {
311
+ get $implicit() {
312
+ return latestContext()
313
+ },
314
+ },
315
+ { injector: this.#getInjector() },
316
+ )
317
+ return new FlexRenderTemplateView(template, view)
318
+ }
319
+
320
+ #renderComponent(
321
+ flexRenderComponent: Extract<
322
+ FlexRenderTypedContent,
323
+ { kind: 'flexRenderComponent' }
324
+ >,
325
+ ): FlexRenderComponentView {
326
+ const { injector } = flexRenderComponent.content
327
+ const componentInjector = this.#getInjector(injector)
328
+ const view = this.#flexRenderComponentFactory.createComponent(
329
+ flexRenderComponent.content,
330
+ componentInjector,
331
+ )
332
+ return new FlexRenderComponentView(flexRenderComponent, view)
333
+ }
334
+
335
+ #renderCustomComponent(
336
+ component: Extract<FlexRenderTypedContent, { kind: 'component' }>,
337
+ ): FlexRenderComponentView {
338
+ const instance = flexRenderComponent(component.content, {
339
+ inputs: this.#props(),
340
+ })
341
+ const injector = this.#getInjector(instance.injector)
342
+ const view = this.#flexRenderComponentFactory.createComponent(
343
+ instance,
344
+ injector,
345
+ )
346
+ return new FlexRenderComponentView(component, view)
347
+ }
348
+
349
+ #getInjector(parentInjector?: Injector) {
350
+ const getContext = () => this.#props()
351
+ const proxy = new Proxy(this.#props(), {
352
+ get: (_, key) => getContext()[key as keyof typeof _],
353
+ })
354
+
355
+ const staticProviders = []
356
+ if ('table' in proxy) {
357
+ staticProviders.push({
358
+ provide: TanStackTableToken,
359
+ useValue: () => proxy.table,
360
+ })
361
+ }
362
+ if ('cell' in proxy) {
363
+ staticProviders.push({
364
+ provide: TanStackTableCellToken,
365
+ useValue: () => proxy.cell,
366
+ })
367
+ }
368
+ if ('header' in proxy) {
369
+ staticProviders.push({
370
+ provide: TanStackTableHeaderToken,
371
+ useValue: () => proxy.header,
372
+ })
373
+ }
374
+
375
+ return Injector.create({
376
+ parent: parentInjector ?? this.#injector(),
377
+ providers: [
378
+ ...staticProviders,
379
+ { provide: FlexRenderComponentProps, useValue: proxy },
380
+ ],
381
+ })
382
+ }
383
+ }