@tanstack/angular-table 8.20.1 → 8.21.0

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.
@@ -1,20 +1,40 @@
1
1
  import {
2
2
  ChangeDetectorRef,
3
- ComponentRef,
4
3
  Directive,
5
- EmbeddedViewRef,
4
+ DoCheck,
5
+ effect,
6
+ type EffectRef,
6
7
  Inject,
7
8
  inject,
8
- InjectionToken,
9
9
  Injector,
10
10
  Input,
11
- isSignal,
12
- type OnChanges,
13
- type SimpleChanges,
11
+ OnChanges,
12
+ runInInjectionContext,
13
+ SimpleChanges,
14
14
  TemplateRef,
15
15
  Type,
16
16
  ViewContainerRef,
17
17
  } from '@angular/core'
18
+ import { FlexRenderComponentProps } from './flex-render/context'
19
+ import { FlexRenderFlags } from './flex-render/flags'
20
+ import {
21
+ flexRenderComponent,
22
+ FlexRenderComponent,
23
+ } from './flex-render/flex-render-component'
24
+ import { FlexRenderComponentFactory } from './flex-render/flex-render-component-ref'
25
+ import {
26
+ FlexRenderComponentView,
27
+ FlexRenderTemplateView,
28
+ type FlexRenderTypedContent,
29
+ FlexRenderView,
30
+ mapToFlexRenderTypedContent,
31
+ } from './flex-render/view'
32
+ import { memo } from '@tanstack/table-core'
33
+
34
+ export {
35
+ injectFlexRenderContext,
36
+ type FlexRenderComponentProps,
37
+ } from './flex-render/context'
18
38
 
19
39
  export type FlexRenderContent<TProps extends NonNullable<unknown>> =
20
40
  | string
@@ -23,15 +43,20 @@ export type FlexRenderContent<TProps extends NonNullable<unknown>> =
23
43
  | FlexRenderComponent<TProps>
24
44
  | TemplateRef<{ $implicit: TProps }>
25
45
  | null
46
+ | Record<any, any>
26
47
  | undefined
27
48
 
28
49
  @Directive({
29
50
  selector: '[flexRender]',
30
51
  standalone: true,
52
+ providers: [FlexRenderComponentFactory],
31
53
  })
32
54
  export class FlexRenderDirective<TProps extends NonNullable<unknown>>
33
- implements OnChanges
55
+ implements OnChanges, DoCheck
34
56
  {
57
+ readonly #flexRenderComponentFactory = inject(FlexRenderComponentFactory)
58
+ readonly #changeDetectorRef = inject(ChangeDetectorRef)
59
+
35
60
  @Input({ required: true, alias: 'flexRender' })
36
61
  content:
37
62
  | number
@@ -46,6 +71,24 @@ export class FlexRenderDirective<TProps extends NonNullable<unknown>>
46
71
  @Input({ required: false, alias: 'flexRenderInjector' })
47
72
  injector: Injector = inject(Injector)
48
73
 
74
+ renderFlags = FlexRenderFlags.ViewFirstRender
75
+ renderView: FlexRenderView<any> | null = null
76
+
77
+ readonly #latestContent = () => {
78
+ const { content, props } = this
79
+ return typeof content !== 'function'
80
+ ? content
81
+ : runInInjectionContext(this.injector, () => content(props))
82
+ }
83
+
84
+ #getContentValue = memo(
85
+ () => [this.#latestContent(), this.props, this.content],
86
+ latestContent => {
87
+ return mapToFlexRenderTypedContent(latestContent)
88
+ },
89
+ { key: 'flexRenderContentValue', debug: () => false }
90
+ )
91
+
49
92
  constructor(
50
93
  @Inject(ViewContainerRef)
51
94
  private readonly viewContainerRef: ViewContainerRef,
@@ -53,132 +96,191 @@ export class FlexRenderDirective<TProps extends NonNullable<unknown>>
53
96
  private readonly templateRef: TemplateRef<any>
54
97
  ) {}
55
98
 
56
- ref?: ComponentRef<unknown> | EmbeddedViewRef<unknown> | null = null
57
-
58
99
  ngOnChanges(changes: SimpleChanges) {
59
- if (this.ref instanceof ComponentRef) {
60
- this.ref.injector.get(ChangeDetectorRef).markForCheck()
100
+ if (changes['props']) {
101
+ this.renderFlags |= FlexRenderFlags.PropsReferenceChanged
61
102
  }
62
- if (!changes['content']) {
63
- return
103
+ if (changes['content']) {
104
+ this.renderFlags |=
105
+ FlexRenderFlags.ContentChanged | FlexRenderFlags.ViewFirstRender
106
+ this.update()
64
107
  }
65
- this.render()
66
108
  }
67
109
 
68
- render() {
69
- this.viewContainerRef.clear()
70
- const { content, props } = this
71
- if (content === null || content === undefined) {
72
- this.ref = null
110
+ ngDoCheck(): void {
111
+ if (this.renderFlags & FlexRenderFlags.ViewFirstRender) {
112
+ // On the initial render, the view is created during the `ngOnChanges` hook.
113
+ // Since `ngDoCheck` is called immediately afterward, there's no need to check for changes in this phase.
114
+ this.renderFlags &= ~FlexRenderFlags.ViewFirstRender
73
115
  return
74
116
  }
75
- if (typeof content === 'function') {
76
- return this.renderContent(content(props))
117
+
118
+ this.renderFlags |= FlexRenderFlags.DirtyCheck
119
+
120
+ const latestContent = this.#getContentValue()
121
+ if (latestContent.kind === 'null' || !this.renderView) {
122
+ this.renderFlags |= FlexRenderFlags.ContentChanged
77
123
  } else {
78
- return this.renderContent(content)
124
+ this.renderView.content = latestContent
125
+ const { kind: previousKind } = this.renderView.previousContent
126
+ if (latestContent.kind !== previousKind) {
127
+ this.renderFlags |= FlexRenderFlags.ContentChanged
128
+ }
129
+ }
130
+ this.update()
131
+ }
132
+
133
+ update() {
134
+ if (
135
+ this.renderFlags &
136
+ (FlexRenderFlags.ContentChanged | FlexRenderFlags.ViewFirstRender)
137
+ ) {
138
+ this.render()
139
+ return
140
+ }
141
+ if (this.renderFlags & FlexRenderFlags.PropsReferenceChanged) {
142
+ if (this.renderView) this.renderView.updateProps(this.props)
143
+ this.renderFlags &= ~FlexRenderFlags.PropsReferenceChanged
144
+ }
145
+ if (
146
+ this.renderFlags &
147
+ (FlexRenderFlags.DirtyCheck | FlexRenderFlags.DirtySignal)
148
+ ) {
149
+ if (this.renderView) this.renderView.dirtyCheck()
150
+ this.renderFlags &= ~(
151
+ FlexRenderFlags.DirtyCheck | FlexRenderFlags.DirtySignal
152
+ )
79
153
  }
80
154
  }
81
155
 
82
- private renderContent(content: FlexRenderContent<TProps>) {
83
- if (typeof content === 'string' || typeof content === 'number') {
84
- return this.renderStringContent()
156
+ #currentEffectRef: EffectRef | null = null
157
+
158
+ render() {
159
+ if (this.#shouldRecreateEntireView() && this.#currentEffectRef) {
160
+ this.#currentEffectRef.destroy()
161
+ this.#currentEffectRef = null
162
+ this.renderFlags &= ~FlexRenderFlags.RenderEffectChecked
163
+ }
164
+
165
+ this.viewContainerRef.clear()
166
+ this.renderFlags =
167
+ FlexRenderFlags.Pristine |
168
+ (this.renderFlags & FlexRenderFlags.ViewFirstRender) |
169
+ (this.renderFlags & FlexRenderFlags.RenderEffectChecked)
170
+
171
+ const resolvedContent = this.#getContentValue()
172
+ if (resolvedContent.kind === 'null') {
173
+ this.renderView = null
174
+ } else {
175
+ this.renderView = this.#renderViewByContent(resolvedContent)
85
176
  }
86
- if (content instanceof TemplateRef) {
87
- return this.viewContainerRef.createEmbeddedView(
88
- content,
89
- this.getTemplateRefContext()
177
+
178
+ // If the content is a function `content(props)`, we initialize an effect
179
+ // in order to react to changes if the given definition use signals.
180
+ if (!this.#currentEffectRef && typeof this.content === 'function') {
181
+ this.#currentEffectRef = effect(
182
+ () => {
183
+ this.#latestContent()
184
+ if (!(this.renderFlags & FlexRenderFlags.RenderEffectChecked)) {
185
+ this.renderFlags |= FlexRenderFlags.RenderEffectChecked
186
+ return
187
+ }
188
+ this.renderFlags |= FlexRenderFlags.DirtySignal
189
+ // This will mark the view as changed,
190
+ // so we'll try to check for updates into ngDoCheck
191
+ this.#changeDetectorRef.markForCheck()
192
+ },
193
+ { injector: this.viewContainerRef.injector }
90
194
  )
91
- } else if (content instanceof FlexRenderComponent) {
92
- return this.renderComponent(content)
93
- } else if (content instanceof Type) {
94
- return this.renderCustomComponent(content)
195
+ }
196
+ }
197
+
198
+ #shouldRecreateEntireView() {
199
+ return (
200
+ this.renderFlags &
201
+ FlexRenderFlags.ContentChanged &
202
+ FlexRenderFlags.ViewFirstRender
203
+ )
204
+ }
205
+
206
+ #renderViewByContent(
207
+ content: FlexRenderTypedContent
208
+ ): FlexRenderView<any> | null {
209
+ if (content.kind === 'primitive') {
210
+ return this.#renderStringContent(content)
211
+ } else if (content.kind === 'templateRef') {
212
+ return this.#renderTemplateRefContent(content)
213
+ } else if (content.kind === 'flexRenderComponent') {
214
+ return this.#renderComponent(content)
215
+ } else if (content.kind === 'component') {
216
+ return this.#renderCustomComponent(content)
95
217
  } else {
96
218
  return null
97
219
  }
98
220
  }
99
221
 
100
- private renderStringContent(): EmbeddedViewRef<unknown> {
222
+ #renderStringContent(
223
+ template: Extract<FlexRenderTypedContent, { kind: 'primitive' }>
224
+ ): FlexRenderTemplateView {
101
225
  const context = () => {
102
226
  return typeof this.content === 'string' ||
103
227
  typeof this.content === 'number'
104
228
  ? this.content
105
229
  : this.content?.(this.props)
106
230
  }
107
- return this.viewContainerRef.createEmbeddedView(this.templateRef, {
231
+ const ref = this.viewContainerRef.createEmbeddedView(this.templateRef, {
108
232
  get $implicit() {
109
233
  return context()
110
234
  },
111
235
  })
236
+ return new FlexRenderTemplateView(template, ref)
237
+ }
238
+
239
+ #renderTemplateRefContent(
240
+ template: Extract<FlexRenderTypedContent, { kind: 'templateRef' }>
241
+ ): FlexRenderTemplateView {
242
+ const latestContext = () => this.props
243
+ const view = this.viewContainerRef.createEmbeddedView(template.content, {
244
+ get $implicit() {
245
+ return latestContext()
246
+ },
247
+ })
248
+ return new FlexRenderTemplateView(template, view)
112
249
  }
113
250
 
114
- private renderComponent(
115
- flexRenderComponent: FlexRenderComponent<TProps>
116
- ): ComponentRef<unknown> {
117
- const { component, inputs, injector } = flexRenderComponent
251
+ #renderComponent(
252
+ flexRenderComponent: Extract<
253
+ FlexRenderTypedContent,
254
+ { kind: 'flexRenderComponent' }
255
+ >
256
+ ): FlexRenderComponentView {
257
+ const { inputs, outputs, injector } = flexRenderComponent.content
118
258
 
119
259
  const getContext = () => this.props
120
-
121
260
  const proxy = new Proxy(this.props, {
122
- get: (_, key) => getContext()?.[key as keyof typeof _],
261
+ get: (_, key) => getContext()[key as keyof typeof _],
123
262
  })
124
-
125
263
  const componentInjector = Injector.create({
126
264
  parent: injector ?? this.injector,
127
265
  providers: [{ provide: FlexRenderComponentProps, useValue: proxy }],
128
266
  })
129
-
130
- const componentRef = this.viewContainerRef.createComponent(component, {
131
- injector: componentInjector,
132
- })
133
- for (const prop in inputs) {
134
- if (componentRef.instance?.hasOwnProperty(prop)) {
135
- componentRef.setInput(prop, inputs[prop])
136
- }
137
- }
138
- return componentRef
139
- }
140
-
141
- private renderCustomComponent(
142
- component: Type<unknown>
143
- ): ComponentRef<unknown> {
144
- const componentRef = this.viewContainerRef.createComponent(component, {
145
- injector: this.injector,
146
- })
147
- for (const prop in this.props) {
148
- // Only signal based input can be added here
149
- if (
150
- componentRef.instance?.hasOwnProperty(prop) &&
151
- // @ts-ignore
152
- isSignal(componentRef.instance[prop])
153
- ) {
154
- componentRef.setInput(prop, this.props[prop])
155
- }
156
- }
157
- return componentRef
267
+ const view = this.#flexRenderComponentFactory.createComponent(
268
+ flexRenderComponent.content,
269
+ componentInjector
270
+ )
271
+ return new FlexRenderComponentView(flexRenderComponent, view)
158
272
  }
159
273
 
160
- private getTemplateRefContext() {
161
- const getContext = () => this.props
162
- return {
163
- get $implicit() {
164
- return getContext()
165
- },
166
- }
274
+ #renderCustomComponent(
275
+ component: Extract<FlexRenderTypedContent, { kind: 'component' }>
276
+ ): FlexRenderComponentView {
277
+ const view = this.#flexRenderComponentFactory.createComponent(
278
+ flexRenderComponent(component.content, {
279
+ inputs: this.props,
280
+ injector: this.injector,
281
+ }),
282
+ this.injector
283
+ )
284
+ return new FlexRenderComponentView(component, view)
167
285
  }
168
286
  }
169
-
170
- export class FlexRenderComponent<T extends NonNullable<unknown>> {
171
- constructor(
172
- readonly component: Type<unknown>,
173
- readonly inputs: T = {} as T,
174
- readonly injector?: Injector
175
- ) {}
176
- }
177
-
178
- const FlexRenderComponentProps = new InjectionToken<NonNullable<unknown>>(
179
- '[@tanstack/angular-table] Flex render component context props'
180
- )
181
-
182
- export function injectFlexRenderContext<T extends NonNullable<unknown>>(): T {
183
- return inject<T>(FlexRenderComponentProps)
184
- }
package/src/index.ts CHANGED
@@ -14,11 +14,17 @@ export * from '@tanstack/table-core'
14
14
 
15
15
  export {
16
16
  type FlexRenderContent,
17
- FlexRenderComponent,
18
17
  FlexRenderDirective,
18
+ FlexRenderDirective as FlexRender,
19
19
  injectFlexRenderContext,
20
+ type FlexRenderComponentProps,
20
21
  } from './flex-render'
21
22
 
23
+ export {
24
+ FlexRenderComponent,
25
+ flexRenderComponent,
26
+ } from './flex-render/flex-render-component'
27
+
22
28
  export function createAngularTable<TData extends RowData>(
23
29
  options: () => TableOptions<TData>
24
30
  ): Table<TData> & Signal<Table<TData>> {
package/src/proxy.ts CHANGED
@@ -23,8 +23,11 @@ export function proxifyTable<T>(
23
23
  */
24
24
  if (
25
25
  property.startsWith('get') &&
26
- !property.endsWith('Handler') &&
27
- !property.endsWith('Model')
26
+ !property.endsWith('Handler')
27
+ // e.g. getCoreRowModel, getSelectedRowModel etc.
28
+ // We need that after a signal change even `rowModel` may mark the view as dirty.
29
+ // This allows to always get the latest `getContext` value while using flexRender
30
+ // && !property.endsWith('Model')
28
31
  ) {
29
32
  const maybeFn = table[property] as Function | never
30
33
  if (typeof maybeFn === 'function') {