Skip to content

Commit 9d57db0

Browse files
committed
improve view flags, handle state update in zoneless
1 parent 377446e commit 9d57db0

File tree

4 files changed

+186
-88
lines changed

4 files changed

+186
-88
lines changed
Lines changed: 122 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import {
2-
computed,
2+
ChangeDetectorRef,
33
Directive,
44
DoCheck,
55
effect,
6+
type EffectRef,
67
Inject,
78
inject,
89
Injector,
@@ -12,7 +13,6 @@ import {
1213
SimpleChanges,
1314
TemplateRef,
1415
Type,
15-
untracked,
1616
ViewContainerRef,
1717
} from '@angular/core'
1818
import { FlexRenderComponentProps } from './flex-render/context'
@@ -26,6 +26,7 @@ import {
2626
FlexRenderView,
2727
mapToFlexRenderTypedContent,
2828
} from './flex-render/view'
29+
import { memo } from '@tanstack/table-core'
2930

3031
export {
3132
injectFlexRenderContext,
@@ -39,7 +40,7 @@ export type FlexRenderContent<TProps extends NonNullable<unknown>> =
3940
| FlexRenderComponent<TProps>
4041
| TemplateRef<{ $implicit: TProps }>
4142
| null
42-
| Record<string, any>
43+
| Record<any, any>
4344
| undefined
4445

4546
@Directive({
@@ -50,6 +51,9 @@ export type FlexRenderContent<TProps extends NonNullable<unknown>> =
5051
export class FlexRenderDirective<TProps extends NonNullable<unknown>>
5152
implements OnChanges, DoCheck
5253
{
54+
readonly #flexRenderComponentFactory = inject(FlexRenderComponentFactory)
55+
readonly #changeDetectorRef = inject(ChangeDetectorRef)
56+
5357
@Input({ required: true, alias: 'flexRender' })
5458
content:
5559
| number
@@ -64,10 +68,24 @@ export class FlexRenderDirective<TProps extends NonNullable<unknown>>
6468
@Input({ required: false, alias: 'flexRenderInjector' })
6569
injector: Injector = inject(Injector)
6670

67-
readonly #flexRenderComponentFactory = inject(FlexRenderComponentFactory)
68-
renderFlags = FlexRenderFlags.Creation
71+
renderFlags = FlexRenderFlags.ViewFirstRender
6972
renderView: FlexRenderView<any> | null = null
7073

74+
readonly #latestContent = () => {
75+
const { content, props } = this
76+
return typeof content !== 'function'
77+
? content
78+
: runInInjectionContext(this.injector, () => content(props))
79+
}
80+
81+
#getContentValue = memo(
82+
() => [this.#latestContent(), this.props, this.content],
83+
latestContent => {
84+
return mapToFlexRenderTypedContent(latestContent)
85+
},
86+
{ key: 'flexRenderContentValue', debug: () => false }
87+
)
88+
7189
constructor(
7290
@Inject(ViewContainerRef)
7391
private readonly viewContainerRef: ViewContainerRef,
@@ -80,120 +98,145 @@ export class FlexRenderDirective<TProps extends NonNullable<unknown>>
8098
this.renderFlags |= FlexRenderFlags.PropsReferenceChanged
8199
}
82100
if (changes['content']) {
83-
this.renderFlags |= FlexRenderFlags.ContentChanged
84-
this.checkView()
101+
this.renderFlags |=
102+
FlexRenderFlags.ContentChanged | FlexRenderFlags.ViewFirstRender
103+
this.update()
85104
}
86105
}
87106

88107
ngDoCheck(): void {
89-
if (this.renderFlags & FlexRenderFlags.Creation) {
108+
if (this.renderFlags & FlexRenderFlags.ViewFirstRender) {
90109
// On the initial render, the view is created during the `ngOnChanges` hook.
91110
// Since `ngDoCheck` is called immediately afterward, there's no need to check for changes in this phase.
92-
this.renderFlags &= ~FlexRenderFlags.Creation
111+
this.renderFlags &= ~FlexRenderFlags.ViewFirstRender
93112
return
94113
}
95114

96-
// TODO: Optimization for V9?. We could check for signal changes when
97-
// we have a way to detect here whether the table state changes
115+
console.log('go into do check')
116+
117+
// TODO: Optimization for V9 / future updates?. We could check for dirty signal changes when
118+
// we are able to detect whether the table state changes here
98119
// const isChanged =
99120
// this.renderFlags &
100121
// (FlexRenderFlags.DirtySignal | FlexRenderFlags.PropsReferenceChanged)
101122
// if (!isChanged) {
102123
// return
103124
// }
125+
// if (this.renderFlags & FlexRenderFlags.DirtySignal) {
126+
// this.renderFlags &= ~FlexRenderFlags.DirtySignal
127+
// return
128+
// }
104129

105-
const contentToRender = untracked(() => this.#getContentValue(this.props))
130+
this.renderFlags |= FlexRenderFlags.DirtyCheck
131+
this.checkViewChanges()
132+
}
106133

107-
if (contentToRender.kind === 'null' || !this.renderView) {
108-
this.renderFlags |= FlexRenderFlags.Creation
134+
checkViewChanges(): void {
135+
const latestContent = this.#getContentValue()
136+
if (latestContent.kind === 'null' || !this.renderView) {
137+
this.renderFlags |= FlexRenderFlags.ContentChanged
109138
} else {
110-
this.renderView.setContent(contentToRender.content)
111-
this.renderFlags |= FlexRenderFlags.DirtyCheck
112-
113-
const previousContentInfo = this.renderView.previousContent
114-
if (contentToRender.kind !== previousContentInfo.kind) {
139+
this.renderView.content = latestContent
140+
const { kind: previousKind } = this.renderView.previousContent
141+
if (latestContent.kind !== previousKind) {
115142
this.renderFlags |= FlexRenderFlags.ContentChanged
116143
}
117-
this.renderFlags &= ~FlexRenderFlags.Pristine
118144
}
119-
120-
this.checkView()
145+
this.update()
121146
}
122147

123-
checkView() {
148+
update() {
124149
if (
125150
this.renderFlags &
126-
(FlexRenderFlags.ContentChanged | FlexRenderFlags.Creation)
151+
(FlexRenderFlags.ContentChanged | FlexRenderFlags.ViewFirstRender)
127152
) {
128153
this.render()
129154
return
130155
}
131-
132156
if (this.renderFlags & FlexRenderFlags.PropsReferenceChanged) {
133157
if (this.renderView) this.renderView.updateProps(this.props)
134158
this.renderFlags &= ~FlexRenderFlags.PropsReferenceChanged
135159
}
136-
137-
if (this.renderFlags & FlexRenderFlags.DirtyCheck) {
160+
if (
161+
this.renderFlags &
162+
(FlexRenderFlags.DirtyCheck | FlexRenderFlags.DirtySignal)
163+
) {
138164
if (this.renderView) this.renderView.dirtyCheck()
139165
this.renderFlags &= ~(
140166
FlexRenderFlags.DirtyCheck | FlexRenderFlags.DirtySignal
141167
)
142168
}
143169
}
144170

171+
#currentEffectRef: EffectRef | null = null
172+
145173
render() {
174+
if (this.#shouldRecreateEntireView() && this.#currentEffectRef) {
175+
this.#currentEffectRef.destroy()
176+
this.#currentEffectRef = null
177+
this.renderFlags &= ~FlexRenderFlags.RenderEffectChecked
178+
}
179+
146180
this.viewContainerRef.clear()
147-
const resolvedContent = this.#getContentValue(this.props)
181+
this.renderFlags =
182+
FlexRenderFlags.Pristine |
183+
(this.renderFlags & FlexRenderFlags.ViewFirstRender) |
184+
(this.renderFlags & FlexRenderFlags.RenderEffectChecked)
148185

186+
const resolvedContent = this.#getContentValue()
149187
if (resolvedContent.kind === 'null') {
150188
this.renderView = null
151-
return
189+
} else {
190+
this.renderView = this.#renderViewByContent(resolvedContent)
152191
}
153192

154-
this.renderView = this.#renderViewByContent(resolvedContent)
155-
156-
if (typeof this.content === 'function') {
157-
let firstRender = true
158-
const effectRef = effect(
193+
// If the content is a function `content(props)`, we initialize an effect
194+
// in order to react to changes if the given definition use signals.
195+
if (!this.#currentEffectRef && typeof this.content === 'function') {
196+
this.#currentEffectRef = effect(
159197
() => {
160-
resolvedContent.computedContent()
161-
if (firstRender) {
162-
firstRender = true
198+
this.#latestContent()
199+
if (!(this.renderFlags & FlexRenderFlags.RenderEffectChecked)) {
200+
this.renderFlags |= FlexRenderFlags.RenderEffectChecked
163201
return
164202
}
165203
this.renderFlags |= FlexRenderFlags.DirtySignal
204+
// This will mark the view as changed,
205+
// so we'll try to check for updates into ngDoCheck
206+
this.#changeDetectorRef.markForCheck()
166207
},
167-
{ injector: this.injector }
208+
{ injector: this.viewContainerRef.injector }
168209
)
169-
if (this.renderView) {
170-
this.renderView.onDestroy(() => {
171-
effectRef.destroy()
172-
})
173-
}
174210
}
211+
}
175212

176-
this.renderFlags |= FlexRenderFlags.Pristine
177-
this.renderFlags &= ~FlexRenderFlags.ContentChanged
213+
#shouldRecreateEntireView() {
214+
return (
215+
this.renderFlags &
216+
FlexRenderFlags.ContentChanged &
217+
FlexRenderFlags.ViewFirstRender
218+
)
178219
}
179220

180221
#renderViewByContent(
181222
content: FlexRenderTypedContent
182223
): FlexRenderView<any> | null {
183224
if (content.kind === 'primitive') {
184-
return this.#renderStringContent()
225+
return this.#renderStringContent(content)
185226
} else if (content.kind === 'templateRef') {
186-
return this.#renderTemplateRefContent(content.content)
227+
return this.#renderTemplateRefContent(content)
187228
} else if (content.kind === 'flexRenderComponent') {
188-
return this.#renderComponent(content.content)
229+
return this.#renderComponent(content)
189230
} else if (content.kind === 'component') {
190-
return this.#renderCustomComponent(content.content)
231+
return this.#renderCustomComponent(content)
191232
} else {
192233
return null
193234
}
194235
}
195236

196-
#renderStringContent(): FlexRenderTemplateView {
237+
#renderStringContent(
238+
template: Extract<FlexRenderTypedContent, { kind: 'primitive' }>
239+
): FlexRenderTemplateView {
197240
const context = () => {
198241
return typeof this.content === 'string' ||
199242
typeof this.content === 'number'
@@ -205,23 +248,28 @@ export class FlexRenderDirective<TProps extends NonNullable<unknown>>
205248
return context()
206249
},
207250
})
208-
return new FlexRenderTemplateView(context(), ref)
251+
return new FlexRenderTemplateView(template, ref)
209252
}
210253

211-
#renderTemplateRefContent(content: TemplateRef<any>): FlexRenderTemplateView {
254+
#renderTemplateRefContent(
255+
template: Extract<FlexRenderTypedContent, { kind: 'templateRef' }>
256+
): FlexRenderTemplateView {
212257
const latestContext = () => this.props
213-
const view = this.viewContainerRef.createEmbeddedView(content, {
258+
const view = this.viewContainerRef.createEmbeddedView(template.content, {
214259
get $implicit() {
215260
return latestContext()
216261
},
217262
})
218-
return new FlexRenderTemplateView(content, view)
263+
return new FlexRenderTemplateView(template, view)
219264
}
220265

221266
#renderComponent(
222-
flexRenderComponent: FlexRenderComponent
267+
flexRenderComponent: Extract<
268+
FlexRenderTypedContent,
269+
{ kind: 'flexRenderComponent' }
270+
>
223271
): FlexRenderComponentView {
224-
const { inputs, injector } = flexRenderComponent
272+
const { inputs, injector } = flexRenderComponent.content
225273

226274
const getContext = () => this.props
227275
const proxy = new Proxy(this.props, {
@@ -232,33 +280,35 @@ export class FlexRenderDirective<TProps extends NonNullable<unknown>>
232280
providers: [{ provide: FlexRenderComponentProps, useValue: proxy }],
233281
})
234282
const view = this.#flexRenderComponentFactory.createComponent(
235-
flexRenderComponent,
283+
flexRenderComponent.content,
236284
componentInjector
237285
)
238-
if (inputs) {
239-
view.setInputs(inputs)
240-
}
286+
if (inputs) view.setInputs(inputs)
241287
return new FlexRenderComponentView(flexRenderComponent, view)
242288
}
243289

244-
#renderCustomComponent(component: Type<unknown>): FlexRenderComponentView {
290+
#renderCustomComponent(
291+
component: Extract<FlexRenderTypedContent, { kind: 'component' }>
292+
): FlexRenderComponentView {
245293
const view = this.#flexRenderComponentFactory.createComponent(
246-
new FlexRenderComponent(component, this.props),
294+
new FlexRenderComponent(component.content, this.props),
247295
this.injector
248296
)
249297
view.setInputs({ ...this.props })
250298
return new FlexRenderComponentView(component, view)
251299
}
300+
}
252301

253-
#getContentValue(context: TProps) {
254-
const content = this.content
255-
const computedContent = computed(() => {
256-
return typeof content !== 'function'
257-
? content
258-
: runInInjectionContext(this.injector, () => content(context))
259-
})
260-
return Object.assign(mapToFlexRenderTypedContent(computedContent()), {
261-
computedContent,
262-
})
302+
function logFlags(place: string, flags: FlexRenderFlags, val: any) {
303+
console.group(`${place}`, val)
304+
const result = {} as Record<string, boolean>
305+
for (const key in FlexRenderFlags) {
306+
// Skip the reverse mapping of numeric values to keys in enums
307+
if (isNaN(Number(key))) {
308+
const flagValue = FlexRenderFlags[key as keyof typeof FlexRenderFlags]
309+
console.log(key, !!(flags & flagValue))
310+
}
263311
}
312+
console.groupEnd()
313+
return result
264314
}

packages/angular-table/src/flex-render/flags.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ export enum FlexRenderFlags {
77
* Indicates that the view is being created for the first time or will be cleared during the next update phase.
88
* This is the initial state and will transition after the first ngDoCheck.
99
*/
10-
Creation = 1 << 0,
10+
ViewFirstRender = 1 << 0,
1111
/**
1212
* Represents a state where the view is not dirty, meaning no changes require rendering updates.
1313
*/
1414
Pristine = 1 << 1,
1515
/**
16-
* Represents a state where the `content` property has been modified or the view requires a complete re-render.
17-
* When this flag is enabled, the view is cleared and recreated from scratch.
16+
* Indicates the `content` property has been modified or the view requires a complete re-render.
17+
* When this flag is enabled, the view will be cleared and recreated from scratch.
1818
*/
1919
ContentChanged = 1 << 2,
2020
/**
@@ -33,4 +33,8 @@ export enum FlexRenderFlags {
3333
* Indicates that a signal within the `content(props)` result has changed
3434
*/
3535
DirtySignal = 1 << 5,
36+
/**
37+
* Indicates that the first render effect has been checked at least one time.
38+
*/
39+
RenderEffectChecked = 1 << 6,
3640
}

0 commit comments

Comments
 (0)