@tanstack/angular-table 8.20.5 → 8.21.2

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,236 @@
1
+ import {
2
+ ChangeDetectorRef,
3
+ ComponentRef,
4
+ inject,
5
+ Injectable,
6
+ Injector,
7
+ KeyValueDiffer,
8
+ KeyValueDiffers,
9
+ OutputEmitterRef,
10
+ OutputRefSubscription,
11
+ ViewContainerRef,
12
+ } from '@angular/core'
13
+ import { FlexRenderComponent } from './flex-render-component'
14
+
15
+ @Injectable()
16
+ export class FlexRenderComponentFactory {
17
+ #viewContainerRef = inject(ViewContainerRef)
18
+
19
+ createComponent<T>(
20
+ flexRenderComponent: FlexRenderComponent<T>,
21
+ componentInjector: Injector
22
+ ): FlexRenderComponentRef<T> {
23
+ const componentRef = this.#viewContainerRef.createComponent(
24
+ flexRenderComponent.component,
25
+ {
26
+ injector: componentInjector,
27
+ }
28
+ )
29
+ const view = new FlexRenderComponentRef(
30
+ componentRef,
31
+ flexRenderComponent,
32
+ componentInjector
33
+ )
34
+
35
+ const { inputs, outputs } = flexRenderComponent
36
+
37
+ if (inputs) view.setInputs(inputs)
38
+ if (outputs) view.setOutputs(outputs)
39
+
40
+ return view
41
+ }
42
+ }
43
+
44
+ export class FlexRenderComponentRef<T> {
45
+ readonly #keyValueDiffersFactory: KeyValueDiffers
46
+ #componentData: FlexRenderComponent<T>
47
+ #inputValueDiffer: KeyValueDiffer<string, unknown>
48
+
49
+ readonly #outputRegistry: FlexRenderComponentOutputManager
50
+
51
+ constructor(
52
+ readonly componentRef: ComponentRef<T>,
53
+ componentData: FlexRenderComponent<T>,
54
+ readonly componentInjector: Injector
55
+ ) {
56
+ this.#componentData = componentData
57
+ this.#keyValueDiffersFactory = componentInjector.get(KeyValueDiffers)
58
+
59
+ this.#outputRegistry = new FlexRenderComponentOutputManager(
60
+ this.#keyValueDiffersFactory,
61
+ this.outputs
62
+ )
63
+
64
+ this.#inputValueDiffer = this.#keyValueDiffersFactory
65
+ .find(this.inputs)
66
+ .create()
67
+ this.#inputValueDiffer.diff(this.inputs)
68
+
69
+ this.componentRef.onDestroy(() => this.#outputRegistry.unsubscribeAll())
70
+ }
71
+
72
+ get component() {
73
+ return this.#componentData.component
74
+ }
75
+
76
+ get inputs() {
77
+ return this.#componentData.inputs ?? {}
78
+ }
79
+
80
+ get outputs() {
81
+ return this.#componentData.outputs ?? {}
82
+ }
83
+
84
+ /**
85
+ * Get component input and output diff by the given item
86
+ */
87
+ diff(item: FlexRenderComponent<T>) {
88
+ return {
89
+ inputDiff: this.#inputValueDiffer.diff(item.inputs ?? {}),
90
+ outputDiff: this.#outputRegistry.diff(item.outputs ?? {}),
91
+ }
92
+ }
93
+ /**
94
+ *
95
+ * @param compare Whether the current ref component instance is the same as the given one
96
+ */
97
+ eqType(compare: FlexRenderComponent<T>): boolean {
98
+ return compare.component === this.component
99
+ }
100
+
101
+ /**
102
+ * Tries to update current component refs input by the new given content component.
103
+ */
104
+ update(content: FlexRenderComponent<T>) {
105
+ const eq = this.eqType(content)
106
+ if (!eq) return
107
+ const { inputDiff, outputDiff } = this.diff(content)
108
+ if (inputDiff) {
109
+ inputDiff.forEachAddedItem(item =>
110
+ this.setInput(item.key, item.currentValue)
111
+ )
112
+ inputDiff.forEachChangedItem(item =>
113
+ this.setInput(item.key, item.currentValue)
114
+ )
115
+ inputDiff.forEachRemovedItem(item => this.setInput(item.key, undefined))
116
+ }
117
+ if (outputDiff) {
118
+ outputDiff.forEachAddedItem(item => {
119
+ this.setOutput(item.key, item.currentValue)
120
+ })
121
+ outputDiff.forEachChangedItem(item => {
122
+ if (item.currentValue) {
123
+ this.#outputRegistry.setListener(item.key, item.currentValue)
124
+ } else {
125
+ this.#outputRegistry.unsubscribe(item.key)
126
+ }
127
+ })
128
+ outputDiff.forEachRemovedItem(item => {
129
+ this.#outputRegistry.unsubscribe(item.key)
130
+ })
131
+ }
132
+
133
+ this.#componentData = content
134
+ }
135
+
136
+ markAsDirty(): void {
137
+ this.componentRef.injector.get(ChangeDetectorRef).markForCheck()
138
+ }
139
+
140
+ setInputs(inputs: Record<string, unknown>) {
141
+ for (const prop in inputs) {
142
+ this.setInput(prop, inputs[prop])
143
+ }
144
+ }
145
+
146
+ setInput(key: string, value: unknown) {
147
+ if (this.#componentData.allowedInputNames.includes(key)) {
148
+ this.componentRef.setInput(key, value)
149
+ }
150
+ }
151
+
152
+ setOutputs(
153
+ outputs: Record<
154
+ string,
155
+ OutputEmitterRef<unknown>['emit'] | null | undefined
156
+ >
157
+ ) {
158
+ this.#outputRegistry.unsubscribeAll()
159
+ for (const prop in outputs) {
160
+ this.setOutput(prop, outputs[prop])
161
+ }
162
+ }
163
+
164
+ setOutput(
165
+ outputName: string,
166
+ emit: OutputEmitterRef<unknown>['emit'] | undefined | null
167
+ ): void {
168
+ if (!this.#componentData.allowedOutputNames.includes(outputName)) return
169
+ if (!emit) {
170
+ this.#outputRegistry.unsubscribe(outputName)
171
+ return
172
+ }
173
+
174
+ const hasListener = this.#outputRegistry.hasListener(outputName)
175
+ this.#outputRegistry.setListener(outputName, emit)
176
+
177
+ if (hasListener) {
178
+ return
179
+ }
180
+
181
+ const instance = this.componentRef.instance
182
+ const output = instance[outputName as keyof typeof instance]
183
+ if (output && output instanceof OutputEmitterRef) {
184
+ output.subscribe(value => {
185
+ this.#outputRegistry.getListener(outputName)?.(value)
186
+ })
187
+ }
188
+ }
189
+ }
190
+
191
+ class FlexRenderComponentOutputManager {
192
+ readonly #outputSubscribers: Record<string, OutputRefSubscription> = {}
193
+ readonly #outputListeners: Record<string, (...args: any[]) => void> = {}
194
+
195
+ readonly #valueDiffer: KeyValueDiffer<
196
+ string,
197
+ undefined | null | OutputEmitterRef<unknown>['emit']
198
+ >
199
+
200
+ constructor(keyValueDiffers: KeyValueDiffers, initialOutputs: any) {
201
+ this.#valueDiffer = keyValueDiffers.find(initialOutputs).create()
202
+ if (initialOutputs) {
203
+ this.#valueDiffer.diff(initialOutputs)
204
+ }
205
+ }
206
+
207
+ hasListener(outputName: string) {
208
+ return outputName in this.#outputListeners
209
+ }
210
+
211
+ setListener(outputName: string, callback: (...args: any[]) => void) {
212
+ this.#outputListeners[outputName] = callback
213
+ }
214
+
215
+ getListener(outputName: string) {
216
+ return this.#outputListeners[outputName]
217
+ }
218
+
219
+ unsubscribeAll(): void {
220
+ for (const prop in this.#outputSubscribers) {
221
+ this.unsubscribe(prop)
222
+ }
223
+ }
224
+
225
+ unsubscribe(outputName: string) {
226
+ if (outputName in this.#outputSubscribers) {
227
+ this.#outputSubscribers[outputName]?.unsubscribe()
228
+ delete this.#outputSubscribers[outputName]
229
+ delete this.#outputListeners[outputName]
230
+ }
231
+ }
232
+
233
+ diff(outputs: Record<string, OutputEmitterRef<unknown>['emit'] | undefined>) {
234
+ return this.#valueDiffer.diff(outputs ?? {})
235
+ }
236
+ }
@@ -0,0 +1,119 @@
1
+ import {
2
+ ComponentMirror,
3
+ Injector,
4
+ InputSignal,
5
+ OutputEmitterRef,
6
+ reflectComponentType,
7
+ Type,
8
+ } from '@angular/core'
9
+
10
+ type Inputs<T> = {
11
+ [K in keyof T as T[K] extends InputSignal<infer R>
12
+ ? K
13
+ : never]?: T[K] extends InputSignal<infer R> ? R : never
14
+ }
15
+
16
+ type Outputs<T> = {
17
+ [K in keyof T as T[K] extends OutputEmitterRef<infer R>
18
+ ? K
19
+ : never]?: T[K] extends OutputEmitterRef<infer R>
20
+ ? OutputEmitterRef<R>['emit']
21
+ : never
22
+ }
23
+
24
+ type OptionalKeys<T, K = keyof T> = K extends keyof T
25
+ ? T[K] extends Required<T>[K]
26
+ ? undefined extends T[K]
27
+ ? K
28
+ : never
29
+ : K
30
+ : never
31
+
32
+ interface FlexRenderRequiredOptions<
33
+ TInputs extends Record<string, any>,
34
+ TOutputs extends Record<string, any>,
35
+ > {
36
+ /**
37
+ * Component instance inputs. They will be set via [componentRef.setInput API](https://angular.dev/api/core/ComponentRef#setInput)
38
+ */
39
+ inputs: TInputs
40
+ /**
41
+ * Component instance outputs.
42
+ */
43
+ outputs?: TOutputs
44
+ /**
45
+ * Optional {@link Injector} that will be used when rendering the component
46
+ */
47
+ injector?: Injector
48
+ }
49
+
50
+ interface FlexRenderOptions<
51
+ TInputs extends Record<string, any>,
52
+ TOutputs extends Record<string, any>,
53
+ > {
54
+ /**
55
+ * Component instance inputs. They will be set via [componentRef.setInput API](https://angular.dev/api/core/ComponentRef#setInput)
56
+ */
57
+ inputs?: TInputs
58
+ /**
59
+ * Component instance outputs.
60
+ */
61
+ outputs?: TOutputs
62
+ /**
63
+ * Optional {@link Injector} that will be used when rendering the component
64
+ */
65
+ injector?: Injector
66
+ }
67
+
68
+ /**
69
+ * Helper function to create a [@link FlexRenderComponent] instance, with better type-safety.
70
+ *
71
+ * - options object must be passed when the given component instance contains at least one required signal input.
72
+ * - options/inputs is typed with the given component inputs
73
+ * - options/outputs is typed with the given component outputs
74
+ */
75
+ export function flexRenderComponent<
76
+ TComponent = any,
77
+ TInputs extends Inputs<TComponent> = Inputs<TComponent>,
78
+ TOutputs extends Outputs<TComponent> = Outputs<TComponent>,
79
+ >(
80
+ component: Type<TComponent>,
81
+ ...options: OptionalKeys<TInputs> extends never
82
+ ? [FlexRenderOptions<TInputs, TOutputs>?]
83
+ : [FlexRenderRequiredOptions<TInputs, TOutputs>]
84
+ ) {
85
+ const { inputs, injector, outputs } = options?.[0] ?? {}
86
+ return new FlexRenderComponent(component, inputs, injector, outputs)
87
+ }
88
+
89
+ /**
90
+ * Wrapper class for a component that will be used as content for {@link FlexRenderDirective}
91
+ *
92
+ * Prefer {@link flexRenderComponent} helper for better type-safety
93
+ */
94
+ export class FlexRenderComponent<TComponent = any> {
95
+ readonly mirror: ComponentMirror<TComponent>
96
+ readonly allowedInputNames: string[] = []
97
+ readonly allowedOutputNames: string[] = []
98
+
99
+ constructor(
100
+ readonly component: Type<TComponent>,
101
+ readonly inputs?: Inputs<TComponent>,
102
+ readonly injector?: Injector,
103
+ readonly outputs?: Outputs<TComponent>
104
+ ) {
105
+ const mirror = reflectComponentType(component)
106
+ if (!mirror) {
107
+ throw new Error(
108
+ `[@tanstack-table/angular] The provided symbol is not a component`
109
+ )
110
+ }
111
+ this.mirror = mirror
112
+ for (const input of this.mirror.inputs) {
113
+ this.allowedInputNames.push(input.propName)
114
+ }
115
+ for (const output of this.mirror.outputs) {
116
+ this.allowedOutputNames.push(output.propName)
117
+ }
118
+ }
119
+ }
@@ -0,0 +1,153 @@
1
+ import { FlexRenderComponentRef } from './flex-render-component-ref'
2
+ import { EmbeddedViewRef, TemplateRef, Type } from '@angular/core'
3
+ import type { FlexRenderContent } from '../flex-render'
4
+ import { FlexRenderComponent } from './flex-render-component'
5
+
6
+ export type FlexRenderTypedContent =
7
+ | { kind: 'null' }
8
+ | {
9
+ kind: 'primitive'
10
+ content: string | number | Record<string, any>
11
+ }
12
+ | { kind: 'flexRenderComponent'; content: FlexRenderComponent<unknown> }
13
+ | { kind: 'templateRef'; content: TemplateRef<unknown> }
14
+ | { kind: 'component'; content: Type<unknown> }
15
+
16
+ export function mapToFlexRenderTypedContent(
17
+ content: FlexRenderContent<any>
18
+ ): FlexRenderTypedContent {
19
+ if (content === null || content === undefined) {
20
+ return { kind: 'null' }
21
+ }
22
+ if (typeof content === 'string' || typeof content === 'number') {
23
+ return { kind: 'primitive', content }
24
+ }
25
+ if (content instanceof FlexRenderComponent) {
26
+ return { kind: 'flexRenderComponent', content }
27
+ } else if (content instanceof TemplateRef) {
28
+ return { kind: 'templateRef', content }
29
+ } else if (content instanceof Type) {
30
+ return { kind: 'component', content }
31
+ } else {
32
+ return { kind: 'primitive', content }
33
+ }
34
+ }
35
+
36
+ export abstract class FlexRenderView<
37
+ TView extends FlexRenderComponentRef<any> | EmbeddedViewRef<unknown> | null,
38
+ > {
39
+ readonly view: TView
40
+ #previousContent: FlexRenderTypedContent | undefined
41
+ #content: FlexRenderTypedContent
42
+
43
+ protected constructor(
44
+ initialContent: Exclude<FlexRenderTypedContent, { kind: 'null' }>,
45
+ view: TView
46
+ ) {
47
+ this.#content = initialContent
48
+ this.view = view
49
+ }
50
+
51
+ get previousContent(): FlexRenderTypedContent {
52
+ return this.#previousContent ?? { kind: 'null' }
53
+ }
54
+
55
+ get content() {
56
+ return this.#content
57
+ }
58
+
59
+ set content(content: FlexRenderTypedContent) {
60
+ this.#previousContent = this.#content
61
+ this.#content = content
62
+ }
63
+
64
+ abstract updateProps(props: Record<string, any>): void
65
+
66
+ abstract dirtyCheck(): void
67
+
68
+ abstract onDestroy(callback: Function): void
69
+ }
70
+
71
+ export class FlexRenderTemplateView extends FlexRenderView<
72
+ EmbeddedViewRef<unknown>
73
+ > {
74
+ constructor(
75
+ initialContent: Extract<
76
+ FlexRenderTypedContent,
77
+ { kind: 'primitive' | 'templateRef' }
78
+ >,
79
+ view: EmbeddedViewRef<unknown>
80
+ ) {
81
+ super(initialContent, view)
82
+ }
83
+
84
+ override updateProps(props: Record<string, any>) {
85
+ this.view.markForCheck()
86
+ }
87
+
88
+ override dirtyCheck() {
89
+ // Basically a no-op. When the view is created via EmbeddedViewRef, we don't need to do any manual update
90
+ // since this type of content has a proxy as a context, then every time the root component is checked for changes,
91
+ // the property getter will be re-evaluated.
92
+ //
93
+ // If in a future we need to manually mark the view as dirty, just uncomment next line
94
+ // this.view.markForCheck()
95
+ }
96
+
97
+ override onDestroy(callback: Function) {
98
+ this.view.onDestroy(callback)
99
+ }
100
+ }
101
+
102
+ export class FlexRenderComponentView extends FlexRenderView<
103
+ FlexRenderComponentRef<unknown>
104
+ > {
105
+ constructor(
106
+ initialContent: Extract<
107
+ FlexRenderTypedContent,
108
+ { kind: 'component' | 'flexRenderComponent' }
109
+ >,
110
+ view: FlexRenderComponentRef<unknown>
111
+ ) {
112
+ super(initialContent, view)
113
+ }
114
+
115
+ override updateProps(props: Record<string, any>) {
116
+ switch (this.content.kind) {
117
+ case 'component': {
118
+ this.view.setInputs(props)
119
+ break
120
+ }
121
+ case 'flexRenderComponent': {
122
+ // No-op. When FlexRenderFlags.PropsReferenceChanged is set,
123
+ // FlexRenderComponent will be updated into `dirtyCheck`.
124
+ break
125
+ }
126
+ }
127
+ }
128
+
129
+ override dirtyCheck() {
130
+ switch (this.content.kind) {
131
+ case 'component': {
132
+ // Component context is currently valuated with the cell context. Since it's reference
133
+ // shouldn't change, we force mark the component as dirty in order to re-evaluate function invocation in view.
134
+ // NOTE: this should behave like having a component with ChangeDetectionStrategy.Default
135
+ this.view.markAsDirty()
136
+ break
137
+ }
138
+ case 'flexRenderComponent': {
139
+ // Given context instance will always have a different reference than the previous one,
140
+ // so instead of recreating the entire view, we will only update the current view
141
+ if (this.view.eqType(this.content.content)) {
142
+ this.view.update(this.content.content)
143
+ }
144
+ this.view.markAsDirty()
145
+ break
146
+ }
147
+ }
148
+ }
149
+
150
+ override onDestroy(callback: Function) {
151
+ this.view.componentRef.onDestroy(callback)
152
+ }
153
+ }