@tanstack/angular-table 9.0.0-alpha.9 → 9.0.0-beta.1
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.
- package/README.md +127 -0
- package/dist/README.md +127 -0
- package/dist/fesm2022/tanstack-angular-table-static-functions.mjs +6 -0
- package/dist/fesm2022/tanstack-angular-table-static-functions.mjs.map +1 -0
- package/dist/fesm2022/tanstack-angular-table.mjs +1336 -239
- package/dist/fesm2022/tanstack-angular-table.mjs.map +1 -1
- package/dist/types/tanstack-angular-table-static-functions.d.ts +1 -0
- package/dist/types/tanstack-angular-table.d.ts +793 -0
- package/package.json +39 -19
- package/skills/angular/angular-rendering-directives/SKILL.md +415 -0
- package/skills/angular/angular-rendering-directives/references/content-shapes.md +142 -0
- package/skills/angular/angular-rendering-directives/references/create-table-hook-registries.md +89 -0
- package/skills/angular/angular-rendering-directives/references/di-tokens.md +171 -0
- package/skills/angular/angular-rendering-directives/references/flex-render-component-options.md +64 -0
- package/skills/angular/client-to-server/SKILL.md +467 -0
- package/skills/angular/compose-with-tanstack-query/SKILL.md +482 -0
- package/skills/angular/compose-with-tanstack-store/SKILL.md +397 -0
- package/skills/angular/compose-with-tanstack-virtual/SKILL.md +400 -0
- package/skills/angular/getting-started/SKILL.md +496 -0
- package/skills/angular/getting-started/references/feature-row-model-mapping.md +48 -0
- package/skills/angular/migrate-v8-to-v9/SKILL.md +419 -0
- package/skills/angular/migrate-v8-to-v9/references/v8-to-v9-mapping.md +261 -0
- package/skills/angular/production-readiness/SKILL.md +469 -0
- package/skills/angular/table-state/SKILL.md +429 -0
- package/skills/angular/table-state/references/external-atoms-and-app-hook.md +152 -0
- package/src/flex-render/context.ts +14 -0
- package/src/flex-render/flags.ts +34 -0
- package/src/flex-render/flexRenderComponent.ts +288 -0
- package/src/flex-render/flexRenderComponentFactory.ts +251 -0
- package/src/flex-render/renderer.ts +393 -0
- package/src/flex-render/view.ts +226 -0
- package/src/flexRender.ts +124 -0
- package/src/helpers/cell.ts +104 -0
- package/src/helpers/createTableHook.ts +499 -0
- package/src/helpers/flexRenderCell.ts +136 -0
- package/src/helpers/header.ts +99 -0
- package/src/helpers/table.ts +85 -0
- package/src/index.ts +23 -70
- package/src/injectTable.ts +212 -0
- package/src/{lazy-signal-initializer.ts → lazySignalInitializer.ts} +2 -2
- package/src/reactivity.ts +105 -0
- package/static-functions/index.ts +1 -0
- package/static-functions/ng-package.json +6 -0
- package/dist/esm2022/flex-render.mjs +0 -148
- package/dist/esm2022/index.mjs +0 -48
- package/dist/esm2022/lazy-signal-initializer.mjs +0 -43
- package/dist/esm2022/proxy.mjs +0 -83
- package/dist/esm2022/tanstack-angular-table.mjs +0 -5
- package/dist/flex-render.d.ts +0 -30
- package/dist/index.d.ts +0 -5
- package/dist/lazy-signal-initializer.d.ts +0 -5
- package/dist/proxy.d.ts +0 -3
- package/src/flex-render.ts +0 -184
- package/src/proxy.ts +0 -97
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Injector,
|
|
3
|
+
computed,
|
|
4
|
+
effect,
|
|
5
|
+
runInInjectionContext,
|
|
6
|
+
untracked,
|
|
7
|
+
} from '@angular/core'
|
|
8
|
+
import { TanStackTableCellToken } from '../helpers/cell'
|
|
9
|
+
import { TanStackTableHeaderToken } from '../helpers/header'
|
|
10
|
+
import { TanStackTableToken } from '../helpers/table'
|
|
11
|
+
import { FlexRenderComponentProps } from './context'
|
|
12
|
+
import { FlexRenderFlags } from './flags'
|
|
13
|
+
import { flexRenderComponent } from './flexRenderComponent'
|
|
14
|
+
import { FlexRenderComponentFactory } from './flexRenderComponentFactory'
|
|
15
|
+
import {
|
|
16
|
+
FlexRenderComponentView,
|
|
17
|
+
FlexRenderTemplateView,
|
|
18
|
+
mapToFlexRenderTypedContent,
|
|
19
|
+
} from './view'
|
|
20
|
+
import type {
|
|
21
|
+
FlexRenderTypedContent,
|
|
22
|
+
FlexRenderView,
|
|
23
|
+
FlexRenderViewAllowedType,
|
|
24
|
+
} from './view'
|
|
25
|
+
import type { FlexRenderComponent } from './flexRenderComponent'
|
|
26
|
+
import type {
|
|
27
|
+
CellContext,
|
|
28
|
+
CellData,
|
|
29
|
+
HeaderContext,
|
|
30
|
+
RowData,
|
|
31
|
+
TableFeatures,
|
|
32
|
+
} from '@tanstack/table-core'
|
|
33
|
+
import type {
|
|
34
|
+
EffectRef,
|
|
35
|
+
TemplateRef,
|
|
36
|
+
Type,
|
|
37
|
+
ViewContainerRef,
|
|
38
|
+
} from '@angular/core'
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Content supported by the `flexRender` directive when declaring
|
|
42
|
+
* a table column header/cell.
|
|
43
|
+
*/
|
|
44
|
+
export type FlexRenderContent<TProps extends NonNullable<unknown>> =
|
|
45
|
+
| string
|
|
46
|
+
| number
|
|
47
|
+
| Type<TProps>
|
|
48
|
+
| FlexRenderComponent<TProps>
|
|
49
|
+
| TemplateRef<{ $implicit: TProps }>
|
|
50
|
+
| null
|
|
51
|
+
| Record<any, any>
|
|
52
|
+
| undefined
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Input content supported by the `flexRender` directives.
|
|
56
|
+
*/
|
|
57
|
+
export type FlexRenderInputContent<TProps extends NonNullable<unknown>> =
|
|
58
|
+
| number
|
|
59
|
+
| string
|
|
60
|
+
| ((props: TProps) => FlexRenderContent<TProps>)
|
|
61
|
+
| null
|
|
62
|
+
| undefined
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Options used to create a {@link FlexViewRenderer}.
|
|
66
|
+
*
|
|
67
|
+
* This renderer is designed to be embedded inside a directive/component that owns the
|
|
68
|
+
* `ViewContainerRef` and possibly a fallback `TemplateRef`.
|
|
69
|
+
*/
|
|
70
|
+
interface RendererViewOptions<TProps extends NonNullable<unknown>> {
|
|
71
|
+
/**
|
|
72
|
+
* Signal-like getter that returns the latest renderable content.
|
|
73
|
+
*/
|
|
74
|
+
content: () => FlexRenderInputContent<TProps>
|
|
75
|
+
/**
|
|
76
|
+
* Signal-like getter returning the current props/context object.
|
|
77
|
+
*/
|
|
78
|
+
props: () => NoInfer<TProps>
|
|
79
|
+
/**
|
|
80
|
+
* Getter returning the base injector to evaluate render functions in.
|
|
81
|
+
*
|
|
82
|
+
* If `content` is a function, it will be executed inside this injection context
|
|
83
|
+
* via `runInInjectionContext` so Angular DI works as expected.
|
|
84
|
+
*/
|
|
85
|
+
injector: () => Injector
|
|
86
|
+
/**
|
|
87
|
+
* Container that will host the dynamically created view/component.
|
|
88
|
+
*/
|
|
89
|
+
viewContainerRef: ViewContainerRef
|
|
90
|
+
/**
|
|
91
|
+
* Fallback template used for primitive rendering.
|
|
92
|
+
*
|
|
93
|
+
* The template is instantiated with `$implicit` set to the primitive string/number.
|
|
94
|
+
*/
|
|
95
|
+
templateRef: TemplateRef<unknown>
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Internal view renderer used by Angular TanStack Table to implement `flexRender` directives.
|
|
100
|
+
*
|
|
101
|
+
* @internal Use FlexRender directives instead.
|
|
102
|
+
*/
|
|
103
|
+
export class FlexViewRenderer<
|
|
104
|
+
TFeatures extends TableFeatures,
|
|
105
|
+
TRowData extends RowData,
|
|
106
|
+
TValue extends CellData,
|
|
107
|
+
TProps extends
|
|
108
|
+
| NonNullable<unknown>
|
|
109
|
+
| CellContext<TFeatures, TRowData, TValue>
|
|
110
|
+
| HeaderContext<TFeatures, TRowData, TValue>,
|
|
111
|
+
> {
|
|
112
|
+
#renderFlags = FlexRenderFlags.ViewFirstRender
|
|
113
|
+
#renderView: FlexRenderView<
|
|
114
|
+
FlexRenderViewAllowedType,
|
|
115
|
+
FlexRenderTypedContent
|
|
116
|
+
> | null = null
|
|
117
|
+
#currentRenderEffectRef: EffectRef | null = null
|
|
118
|
+
#content: () => FlexRenderInputContent<TProps>
|
|
119
|
+
#props: () => TProps
|
|
120
|
+
#injector: () => Injector
|
|
121
|
+
#viewContainerRef: ViewContainerRef
|
|
122
|
+
#templateRef: TemplateRef<unknown>
|
|
123
|
+
#flexRenderComponentFactory: FlexRenderComponentFactory
|
|
124
|
+
|
|
125
|
+
readonly #getLatestContentValue = () => {
|
|
126
|
+
const content = this.#content()
|
|
127
|
+
const props = this.#props()
|
|
128
|
+
return typeof content !== 'function'
|
|
129
|
+
? content
|
|
130
|
+
: runInInjectionContext(this.#injector(), () => content(props))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
readonly #latestContent = computed(() => this.#getLatestContentValue())
|
|
134
|
+
|
|
135
|
+
#getContentValue = computed(() => {
|
|
136
|
+
const latestContent = this.#latestContent()
|
|
137
|
+
return mapToFlexRenderTypedContent(latestContent)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
constructor(options: RendererViewOptions<TProps>) {
|
|
141
|
+
this.#content = options.content
|
|
142
|
+
this.#props = options.props
|
|
143
|
+
this.#injector = options.injector
|
|
144
|
+
this.#templateRef = options.templateRef
|
|
145
|
+
this.#viewContainerRef = options.viewContainerRef
|
|
146
|
+
this.#flexRenderComponentFactory = new FlexRenderComponentFactory(
|
|
147
|
+
this.#viewContainerRef,
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
mount(): EffectRef {
|
|
152
|
+
let previousContent: FlexRenderInputContent<TProps>
|
|
153
|
+
let previousProps: TProps
|
|
154
|
+
|
|
155
|
+
return effect(() => {
|
|
156
|
+
const props = this.#props()
|
|
157
|
+
const content = this.#content()
|
|
158
|
+
|
|
159
|
+
if (!(this.#renderFlags & FlexRenderFlags.ViewFirstRender)) {
|
|
160
|
+
if (previousContent !== content) {
|
|
161
|
+
this.#renderFlags |= FlexRenderFlags.ContentChanged
|
|
162
|
+
}
|
|
163
|
+
if (previousProps !== props) {
|
|
164
|
+
this.#renderFlags |= FlexRenderFlags.PropsReferenceChanged
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
untracked(() => this.#update())
|
|
169
|
+
|
|
170
|
+
if (FlexRenderFlags.ViewFirstRender & this.#renderFlags) {
|
|
171
|
+
this.#renderFlags &= ~FlexRenderFlags.ViewFirstRender
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
previousContent = content
|
|
175
|
+
previousProps = props
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
destroy(): void {
|
|
180
|
+
if (this.#currentRenderEffectRef) {
|
|
181
|
+
this.#currentRenderEffectRef.destroy()
|
|
182
|
+
this.#currentRenderEffectRef = null
|
|
183
|
+
}
|
|
184
|
+
if (this.#renderView) {
|
|
185
|
+
this.#renderView.unmount()
|
|
186
|
+
this.#renderView = null
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
#update() {
|
|
191
|
+
if (
|
|
192
|
+
this.#renderFlags &
|
|
193
|
+
(FlexRenderFlags.ContentChanged | FlexRenderFlags.ViewFirstRender)
|
|
194
|
+
) {
|
|
195
|
+
this.#render()
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (this.#renderFlags & FlexRenderFlags.PropsReferenceChanged) {
|
|
200
|
+
if (this.#renderView) this.#renderView.updateProps(this.#props())
|
|
201
|
+
this.#renderFlags &= ~FlexRenderFlags.PropsReferenceChanged
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (this.#renderFlags & FlexRenderFlags.Dirty) {
|
|
205
|
+
if (this.#renderView) this.#renderView.dirtyCheck()
|
|
206
|
+
this.#renderFlags &= ~FlexRenderFlags.Dirty
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#render() {
|
|
211
|
+
// When the view is recreated from scratch (content change or first render),
|
|
212
|
+
// we have to destroy the current effect listener since it will be recreated
|
|
213
|
+
// skipping the first call (FlexRenderFlags.RenderEffectChecked)
|
|
214
|
+
if (this.#shouldRecreateEntireView() && this.#currentRenderEffectRef) {
|
|
215
|
+
this.#currentRenderEffectRef.destroy()
|
|
216
|
+
this.#currentRenderEffectRef = null
|
|
217
|
+
this.#renderFlags &= ~FlexRenderFlags.RenderEffectChecked
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
this.#viewContainerRef.clear()
|
|
221
|
+
if (this.#renderView) {
|
|
222
|
+
this.#renderView.unmount()
|
|
223
|
+
this.#renderView = null
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
this.#renderFlags =
|
|
227
|
+
(this.#renderFlags & FlexRenderFlags.ViewFirstRender) |
|
|
228
|
+
(this.#renderFlags & FlexRenderFlags.RenderEffectChecked)
|
|
229
|
+
|
|
230
|
+
const resolvedContent = this.#getContentValue()
|
|
231
|
+
this.#renderView = this.#renderViewByContent(resolvedContent)
|
|
232
|
+
// If the content is a function `content(props)`, we initialize an effect
|
|
233
|
+
// to react to changes. If the current fn uses signals, we will set the DirtySignal flag
|
|
234
|
+
// to re-schedule the component updates
|
|
235
|
+
if (
|
|
236
|
+
!this.#currentRenderEffectRef &&
|
|
237
|
+
typeof untracked(this.#content) === 'function'
|
|
238
|
+
) {
|
|
239
|
+
this.#currentRenderEffectRef = effect(
|
|
240
|
+
() => {
|
|
241
|
+
this.#latestContent()
|
|
242
|
+
if (!(this.#renderFlags & FlexRenderFlags.RenderEffectChecked)) {
|
|
243
|
+
this.#renderFlags |= FlexRenderFlags.RenderEffectChecked
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
this.#renderFlags |= FlexRenderFlags.Dirty
|
|
247
|
+
this.#doCheck()
|
|
248
|
+
},
|
|
249
|
+
{ injector: this.#viewContainerRef.injector },
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
#shouldRecreateEntireView() {
|
|
255
|
+
return (
|
|
256
|
+
this.#renderFlags &
|
|
257
|
+
FlexRenderFlags.ContentChanged &
|
|
258
|
+
FlexRenderFlags.ViewFirstRender
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
#doCheck() {
|
|
263
|
+
const latestContent = this.#getContentValue()
|
|
264
|
+
if (latestContent.kind === 'null' || !this.#renderView) {
|
|
265
|
+
this.#renderFlags |= FlexRenderFlags.ContentChanged
|
|
266
|
+
} else {
|
|
267
|
+
const { kind: currentKind } = this.#renderView.content
|
|
268
|
+
if (
|
|
269
|
+
latestContent.kind !== currentKind ||
|
|
270
|
+
!this.#renderView.eq(latestContent)
|
|
271
|
+
) {
|
|
272
|
+
this.#renderFlags |= FlexRenderFlags.ContentChanged
|
|
273
|
+
}
|
|
274
|
+
this.#renderView.content = latestContent
|
|
275
|
+
}
|
|
276
|
+
this.#update()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
#renderViewByContent(
|
|
280
|
+
content: FlexRenderTypedContent,
|
|
281
|
+
): FlexRenderView<FlexRenderViewAllowedType, FlexRenderTypedContent> | null {
|
|
282
|
+
if (content.kind === 'primitive') {
|
|
283
|
+
return this.#renderStringContent(content)
|
|
284
|
+
} else if (content.kind === 'templateRef') {
|
|
285
|
+
return this.#renderTemplateRefContent(content)
|
|
286
|
+
} else if (content.kind === 'flexRenderComponent') {
|
|
287
|
+
return this.#renderComponent(content)
|
|
288
|
+
} else if (content.kind === 'component') {
|
|
289
|
+
return this.#renderCustomComponent(content)
|
|
290
|
+
} else {
|
|
291
|
+
return null
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
#renderStringContent(
|
|
296
|
+
template: Extract<FlexRenderTypedContent, { kind: 'primitive' }>,
|
|
297
|
+
): FlexRenderTemplateView {
|
|
298
|
+
const context = () => {
|
|
299
|
+
const content = this.#content()
|
|
300
|
+
return typeof content === 'string' || typeof content === 'number'
|
|
301
|
+
? content
|
|
302
|
+
: runInInjectionContext(this.#injector(), () =>
|
|
303
|
+
content?.(this.#props()),
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
const ref = this.#viewContainerRef.createEmbeddedView(this.#templateRef, {
|
|
307
|
+
get $implicit() {
|
|
308
|
+
return context()
|
|
309
|
+
},
|
|
310
|
+
})
|
|
311
|
+
return new FlexRenderTemplateView(template, ref)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
#renderTemplateRefContent(
|
|
315
|
+
template: Extract<FlexRenderTypedContent, { kind: 'templateRef' }>,
|
|
316
|
+
): FlexRenderTemplateView {
|
|
317
|
+
const latestContext = () => this.#props()
|
|
318
|
+
const view = this.#viewContainerRef.createEmbeddedView(
|
|
319
|
+
template.content,
|
|
320
|
+
{
|
|
321
|
+
get $implicit() {
|
|
322
|
+
return latestContext()
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
{ injector: this.#getInjector() },
|
|
326
|
+
)
|
|
327
|
+
return new FlexRenderTemplateView(template, view)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
#renderComponent(
|
|
331
|
+
flexRenderComponent: Extract<
|
|
332
|
+
FlexRenderTypedContent,
|
|
333
|
+
{ kind: 'flexRenderComponent' }
|
|
334
|
+
>,
|
|
335
|
+
): FlexRenderComponentView {
|
|
336
|
+
const { injector } = flexRenderComponent.content
|
|
337
|
+
const componentInjector = this.#getInjector(injector)
|
|
338
|
+
const view = this.#flexRenderComponentFactory.createComponent(
|
|
339
|
+
flexRenderComponent.content,
|
|
340
|
+
componentInjector,
|
|
341
|
+
)
|
|
342
|
+
return new FlexRenderComponentView(flexRenderComponent, view)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
#renderCustomComponent(
|
|
346
|
+
component: Extract<FlexRenderTypedContent, { kind: 'component' }>,
|
|
347
|
+
): FlexRenderComponentView {
|
|
348
|
+
const instance = flexRenderComponent(component.content, {
|
|
349
|
+
inputs: this.#props(),
|
|
350
|
+
})
|
|
351
|
+
const injector = this.#getInjector(instance.injector)
|
|
352
|
+
const view = this.#flexRenderComponentFactory.createComponent(
|
|
353
|
+
instance,
|
|
354
|
+
injector,
|
|
355
|
+
)
|
|
356
|
+
return new FlexRenderComponentView(component, view)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
#getInjector(parentInjector?: Injector) {
|
|
360
|
+
const getContext = () => this.#props()
|
|
361
|
+
const proxy = new Proxy(this.#props(), {
|
|
362
|
+
get: (_, key) => getContext()[key as keyof typeof _],
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
const staticProviders = []
|
|
366
|
+
if ('table' in proxy) {
|
|
367
|
+
staticProviders.push({
|
|
368
|
+
provide: TanStackTableToken,
|
|
369
|
+
useValue: () => proxy.table,
|
|
370
|
+
})
|
|
371
|
+
}
|
|
372
|
+
if ('cell' in proxy) {
|
|
373
|
+
staticProviders.push({
|
|
374
|
+
provide: TanStackTableCellToken,
|
|
375
|
+
useValue: () => proxy.cell,
|
|
376
|
+
})
|
|
377
|
+
}
|
|
378
|
+
if ('header' in proxy) {
|
|
379
|
+
staticProviders.push({
|
|
380
|
+
provide: TanStackTableHeaderToken,
|
|
381
|
+
useValue: () => proxy.header,
|
|
382
|
+
})
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return Injector.create({
|
|
386
|
+
parent: parentInjector ?? this.#injector(),
|
|
387
|
+
providers: [
|
|
388
|
+
...staticProviders,
|
|
389
|
+
{ provide: FlexRenderComponentProps, useValue: proxy },
|
|
390
|
+
],
|
|
391
|
+
})
|
|
392
|
+
}
|
|
393
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { TemplateRef, Type } from '@angular/core'
|
|
2
|
+
import { FlexRenderComponentInstance } from './flexRenderComponent'
|
|
3
|
+
import type { FlexRenderComponent } from './flexRenderComponent'
|
|
4
|
+
import type { FlexRenderContent } from './renderer'
|
|
5
|
+
import type { EmbeddedViewRef } from '@angular/core'
|
|
6
|
+
import type { FlexRenderComponentRef } from './flexRenderComponentFactory'
|
|
7
|
+
|
|
8
|
+
export type FlexRenderTypedContent =
|
|
9
|
+
| { kind: 'null' }
|
|
10
|
+
| {
|
|
11
|
+
kind: 'primitive'
|
|
12
|
+
content: string | number | Record<string, any>
|
|
13
|
+
}
|
|
14
|
+
| { kind: 'flexRenderComponent'; content: FlexRenderComponent<unknown> }
|
|
15
|
+
| { kind: 'templateRef'; content: TemplateRef<unknown> }
|
|
16
|
+
| { kind: 'component'; content: Type<unknown> }
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Normalizes arbitrary Angular flex-render content into the renderer's internal
|
|
20
|
+
* tagged representation.
|
|
21
|
+
*
|
|
22
|
+
* This lets the directive decide whether to reuse, update, or recreate an
|
|
23
|
+
* embedded view or component view.
|
|
24
|
+
*/
|
|
25
|
+
export function mapToFlexRenderTypedContent(
|
|
26
|
+
content: FlexRenderContent<any>,
|
|
27
|
+
): FlexRenderTypedContent {
|
|
28
|
+
if (content === null || content === undefined) {
|
|
29
|
+
return { kind: 'null' }
|
|
30
|
+
}
|
|
31
|
+
if (typeof content === 'string' || typeof content === 'number') {
|
|
32
|
+
return { kind: 'primitive', content }
|
|
33
|
+
}
|
|
34
|
+
if (content instanceof FlexRenderComponentInstance) {
|
|
35
|
+
return { kind: 'flexRenderComponent', content }
|
|
36
|
+
} else if (content instanceof TemplateRef) {
|
|
37
|
+
return { kind: 'templateRef', content }
|
|
38
|
+
} else if (content instanceof Type) {
|
|
39
|
+
return { kind: 'component', content }
|
|
40
|
+
} else {
|
|
41
|
+
return { kind: 'primitive', content }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type FlexRenderViewAllowedType =
|
|
46
|
+
| FlexRenderComponentRef<any>
|
|
47
|
+
| EmbeddedViewRef<unknown>
|
|
48
|
+
| null
|
|
49
|
+
|
|
50
|
+
export abstract class FlexRenderView<
|
|
51
|
+
TView extends FlexRenderViewAllowedType,
|
|
52
|
+
TContent extends FlexRenderTypedContent,
|
|
53
|
+
> {
|
|
54
|
+
readonly view: TView
|
|
55
|
+
#previousContent: FlexRenderTypedContent | undefined
|
|
56
|
+
#content: FlexRenderTypedContent
|
|
57
|
+
|
|
58
|
+
protected constructor(
|
|
59
|
+
initialContent: Exclude<FlexRenderTypedContent, { kind: 'null' }>,
|
|
60
|
+
view: TView,
|
|
61
|
+
) {
|
|
62
|
+
this.#content = initialContent
|
|
63
|
+
this.view = view
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
get previousContent(): FlexRenderTypedContent {
|
|
67
|
+
return this.#previousContent ?? { kind: 'null' }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get content() {
|
|
71
|
+
return this.#content
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
set content(content: FlexRenderTypedContent) {
|
|
75
|
+
this.#previousContent = this.#content
|
|
76
|
+
this.#content = content
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
abstract updateProps(props: Record<string, any>): void
|
|
80
|
+
|
|
81
|
+
abstract dirtyCheck(): void
|
|
82
|
+
|
|
83
|
+
abstract onDestroy(callback: Function): void
|
|
84
|
+
|
|
85
|
+
abstract eq(view: TContent): boolean
|
|
86
|
+
|
|
87
|
+
abstract unmount(): void
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Tracks an Angular embedded template view rendered by `FlexRenderDirective`.
|
|
92
|
+
*
|
|
93
|
+
* Template views receive updated props through their proxied context and can be
|
|
94
|
+
* reused while the rendered content kind stays compatible.
|
|
95
|
+
*/
|
|
96
|
+
export class FlexRenderTemplateView extends FlexRenderView<
|
|
97
|
+
EmbeddedViewRef<unknown>,
|
|
98
|
+
Extract<FlexRenderTypedContent, { kind: 'primitive' | 'templateRef' }>
|
|
99
|
+
> {
|
|
100
|
+
constructor(
|
|
101
|
+
initialContent: Extract<
|
|
102
|
+
FlexRenderTypedContent,
|
|
103
|
+
{ kind: 'primitive' | 'templateRef' }
|
|
104
|
+
>,
|
|
105
|
+
view: EmbeddedViewRef<unknown>,
|
|
106
|
+
) {
|
|
107
|
+
super(initialContent, view)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
override updateProps(props: Record<string, any>) {
|
|
111
|
+
this.view.markForCheck()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
override dirtyCheck() {
|
|
115
|
+
// Basically a no-op. When the view is created via EmbeddedViewRef, we don't need to do any manual update
|
|
116
|
+
// since this type of content has a proxy as a context, then every time the root component is checked for changes,
|
|
117
|
+
// the property getter will be re-evaluated.
|
|
118
|
+
//
|
|
119
|
+
// If in a future we need to manually mark the view as dirty, just uncomment next line
|
|
120
|
+
// this.view.markForCheck()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
override unmount() {
|
|
124
|
+
this.view.destroy()
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
override onDestroy(callback: Function) {
|
|
128
|
+
this.view.onDestroy(callback)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
override eq(
|
|
132
|
+
compare: Extract<
|
|
133
|
+
FlexRenderTypedContent,
|
|
134
|
+
{ kind: 'primitive' | 'templateRef' }
|
|
135
|
+
>,
|
|
136
|
+
): boolean {
|
|
137
|
+
return (
|
|
138
|
+
(this.content.kind === 'primitive' &&
|
|
139
|
+
compare.kind === 'primitive' &&
|
|
140
|
+
this.content.content === compare.content) ||
|
|
141
|
+
(this.content.kind === 'templateRef' &&
|
|
142
|
+
compare.kind === 'templateRef' &&
|
|
143
|
+
this.content.content === compare.content)
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Tracks an Angular component view rendered by `FlexRenderDirective`.
|
|
150
|
+
*
|
|
151
|
+
* Component views own input/output updates for `flexRenderComponent(...)`
|
|
152
|
+
* results and component classes rendered directly from column definitions.
|
|
153
|
+
*/
|
|
154
|
+
export class FlexRenderComponentView extends FlexRenderView<
|
|
155
|
+
FlexRenderComponentRef<unknown>,
|
|
156
|
+
Extract<FlexRenderTypedContent, { kind: 'component' | 'flexRenderComponent' }>
|
|
157
|
+
> {
|
|
158
|
+
constructor(
|
|
159
|
+
initialContent: Extract<
|
|
160
|
+
FlexRenderTypedContent,
|
|
161
|
+
{ kind: 'component' | 'flexRenderComponent' }
|
|
162
|
+
>,
|
|
163
|
+
view: FlexRenderComponentRef<unknown>,
|
|
164
|
+
) {
|
|
165
|
+
super(initialContent, view)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
override updateProps(props: Record<string, any>) {
|
|
169
|
+
switch (this.content.kind) {
|
|
170
|
+
case 'component': {
|
|
171
|
+
this.view.setInputs(props)
|
|
172
|
+
break
|
|
173
|
+
}
|
|
174
|
+
case 'flexRenderComponent': {
|
|
175
|
+
// No-op. When FlexRenderFlags.PropsReferenceChanged is set,
|
|
176
|
+
// FlexRenderComponent will be updated into `dirtyCheck`.
|
|
177
|
+
break
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
override dirtyCheck() {
|
|
183
|
+
switch (this.content.kind) {
|
|
184
|
+
case 'component': {
|
|
185
|
+
// Component context is currently valuated with the cell context. Since it's reference
|
|
186
|
+
// shouldn't change, we force mark the component as dirty in order to re-evaluate function invocation in view.
|
|
187
|
+
// NOTE: this should behave like having a component with ChangeDetectionStrategy.Default
|
|
188
|
+
this.view.markAsDirty()
|
|
189
|
+
break
|
|
190
|
+
}
|
|
191
|
+
case 'flexRenderComponent': {
|
|
192
|
+
// Given context instance will always have a different reference than the previous one,
|
|
193
|
+
// so instead of recreating the entire view, we will only update the current view
|
|
194
|
+
if (this.view.eqType(this.content.content)) {
|
|
195
|
+
this.view.update(this.content.content)
|
|
196
|
+
}
|
|
197
|
+
this.view.markAsDirty()
|
|
198
|
+
break
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
override unmount() {
|
|
204
|
+
this.view.componentRef.destroy()
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
override onDestroy(callback: Function) {
|
|
208
|
+
this.view.componentRef.onDestroy(callback)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
override eq(
|
|
212
|
+
compare: Extract<
|
|
213
|
+
FlexRenderTypedContent,
|
|
214
|
+
{ kind: 'component' | 'flexRenderComponent' }
|
|
215
|
+
>,
|
|
216
|
+
): boolean {
|
|
217
|
+
return (
|
|
218
|
+
(this.content.kind === 'component' &&
|
|
219
|
+
compare.kind === 'component' &&
|
|
220
|
+
this.content.content === compare.content) ||
|
|
221
|
+
(this.content.kind === 'flexRenderComponent' &&
|
|
222
|
+
compare.kind === 'flexRenderComponent' &&
|
|
223
|
+
this.content.content.component === compare.content.component)
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
}
|