Skip to content

Commit 74ca4d8

Browse files
fix(angular): fix conditional flexRenderComponent rendering (#6218)
Solve a rendering issue with flexRenderDirective that doesn't re-render component while using conditional `flexRenderComponent` in the same cell column configuration (same cell reference in template, so it's a case where you are not updating table state but relies on external data outside of table scope)
1 parent e172109 commit 74ca4d8

File tree

4 files changed

+155
-15
lines changed

4 files changed

+155
-15
lines changed

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

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
Type,
1616
ViewContainerRef,
1717
} from '@angular/core'
18+
import { memo } from '@tanstack/table-core'
1819
import { FlexRenderComponentProps } from './flex-render/context'
1920
import { FlexRenderFlags } from './flex-render/flags'
2021
import {
@@ -27,9 +28,9 @@ import {
2728
FlexRenderTemplateView,
2829
type FlexRenderTypedContent,
2930
FlexRenderView,
31+
FlexRenderViewAllowedType,
3032
mapToFlexRenderTypedContent,
3133
} from './flex-render/view'
32-
import { memo } from '@tanstack/table-core'
3334

3435
export {
3536
injectFlexRenderContext,
@@ -72,7 +73,10 @@ export class FlexRenderDirective<TProps extends NonNullable<unknown>>
7273
injector: Injector = inject(Injector)
7374

7475
renderFlags = FlexRenderFlags.ViewFirstRender
75-
renderView: FlexRenderView<any> | null = null
76+
renderView: FlexRenderView<
77+
FlexRenderViewAllowedType,
78+
FlexRenderTypedContent
79+
> | null = null
7680

7781
readonly #latestContent = () => {
7882
const { content, props } = this
@@ -121,11 +125,14 @@ export class FlexRenderDirective<TProps extends NonNullable<unknown>>
121125
if (latestContent.kind === 'null' || !this.renderView) {
122126
this.renderFlags |= FlexRenderFlags.ContentChanged
123127
} else {
124-
this.renderView.content = latestContent
125-
const { kind: previousKind } = this.renderView.previousContent
126-
if (latestContent.kind !== previousKind) {
128+
const { kind: currentKind } = this.renderView.content
129+
if (
130+
latestContent.kind !== currentKind ||
131+
!this.renderView.eq(latestContent)
132+
) {
127133
this.renderFlags |= FlexRenderFlags.ContentChanged
128134
}
135+
this.renderView.content = latestContent
129136
}
130137
this.update()
131138
}
@@ -205,7 +212,7 @@ export class FlexRenderDirective<TProps extends NonNullable<unknown>>
205212

206213
#renderViewByContent(
207214
content: FlexRenderTypedContent,
208-
): FlexRenderView<any> | null {
215+
): FlexRenderView<FlexRenderViewAllowedType, FlexRenderTypedContent> | null {
209216
if (content.kind === 'primitive') {
210217
return this.#renderStringContent(content)
211218
} else if (content.kind === 'templateRef') {

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

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,14 @@ export function mapToFlexRenderTypedContent(
3333
}
3434
}
3535

36+
export type FlexRenderViewAllowedType =
37+
| FlexRenderComponentRef<any>
38+
| EmbeddedViewRef<unknown>
39+
| null
40+
3641
export abstract class FlexRenderView<
37-
TView extends FlexRenderComponentRef<any> | EmbeddedViewRef<unknown> | null,
42+
TView extends FlexRenderViewAllowedType,
43+
TContent extends FlexRenderTypedContent,
3844
> {
3945
readonly view: TView
4046
#previousContent: FlexRenderTypedContent | undefined
@@ -66,10 +72,13 @@ export abstract class FlexRenderView<
6672
abstract dirtyCheck(): void
6773

6874
abstract onDestroy(callback: Function): void
75+
76+
abstract eq(view: TContent): boolean
6977
}
7078

7179
export class FlexRenderTemplateView extends FlexRenderView<
72-
EmbeddedViewRef<unknown>
80+
EmbeddedViewRef<unknown>,
81+
Extract<FlexRenderTypedContent, { kind: 'primitive' | 'templateRef' }>
7382
> {
7483
constructor(
7584
initialContent: Extract<
@@ -97,10 +106,27 @@ export class FlexRenderTemplateView extends FlexRenderView<
97106
override onDestroy(callback: Function) {
98107
this.view.onDestroy(callback)
99108
}
109+
110+
override eq(
111+
compare: Extract<
112+
FlexRenderTypedContent,
113+
{ kind: 'primitive' | 'templateRef' }
114+
>,
115+
): boolean {
116+
return (
117+
(this.content.kind === 'primitive' &&
118+
compare.kind === 'primitive' &&
119+
this.content.content === compare.content) ||
120+
(this.content.kind === 'templateRef' &&
121+
compare.kind === 'templateRef' &&
122+
this.content.content === compare.content)
123+
)
124+
}
100125
}
101126

102127
export class FlexRenderComponentView extends FlexRenderView<
103-
FlexRenderComponentRef<unknown>
128+
FlexRenderComponentRef<unknown>,
129+
Extract<FlexRenderTypedContent, { kind: 'component' | 'flexRenderComponent' }>
104130
> {
105131
constructor(
106132
initialContent: Extract<
@@ -150,4 +176,20 @@ export class FlexRenderComponentView extends FlexRenderView<
150176
override onDestroy(callback: Function) {
151177
this.view.componentRef.onDestroy(callback)
152178
}
179+
180+
override eq(
181+
compare: Extract<
182+
FlexRenderTypedContent,
183+
{ kind: 'component' | 'flexRenderComponent' }
184+
>,
185+
): boolean {
186+
return (
187+
(this.content.kind === 'component' &&
188+
compare.kind === 'component' &&
189+
this.content.content === compare.content) ||
190+
(this.content.kind === 'flexRenderComponent' &&
191+
compare.kind === 'flexRenderComponent' &&
192+
this.content.content.component === compare.content.component)
193+
)
194+
}
153195
}

packages/angular-table/tests/flex-render-table.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,42 @@ describe('FlexRenderDirective', () => {
119119
expect(firstCell!.textContent).toEqual('Updated status')
120120
})
121121

122+
test('Render content reactively when flexRenderComponent class changes', async () => {
123+
const showBadgeA = signal(true)
124+
125+
const { dom, fixture } = createTestTable(defaultData, [
126+
{
127+
id: 'first_cell',
128+
header: 'Status',
129+
cell: () => {
130+
return showBadgeA()
131+
? flexRenderComponent(TestABadgeComponent, {
132+
inputs: { status: 'From A' },
133+
})
134+
: flexRenderComponent(TestBBadgeComponent, {
135+
inputs: { status: 'From B' },
136+
})
137+
},
138+
},
139+
])
140+
141+
const row = dom.getBodyRow(0)!
142+
const firstCell = row.querySelector('td')!
143+
144+
let firstElement = firstCell.firstElementChild as HTMLElement | null
145+
expect(firstElement).not.toBeNull()
146+
expect(firstElement!.tagName).toEqual('APP-TEST-A-BADGE')
147+
expect(firstCell.textContent).toContain('From A')
148+
149+
showBadgeA.set(false)
150+
fixture.detectChanges()
151+
152+
firstElement = firstCell.firstElementChild as HTMLElement | null
153+
expect(firstElement).not.toBeNull()
154+
expect(firstElement!.tagName).toEqual('APP-TEST-B-BADGE')
155+
expect(firstCell.textContent).toContain('From B')
156+
})
157+
122158
test('Render content reactively based on signal value', async () => {
123159
const statusComponent = signal<FlexRenderContent<any>>('Initial status')
124160

@@ -447,3 +483,23 @@ class TestBadgeComponent {
447483

448484
readonly status = input.required<string>()
449485
}
486+
487+
@Component({
488+
selector: 'app-test-a-badge',
489+
template: `<span>A {{ status() }}</span>`,
490+
standalone: true,
491+
changeDetection: ChangeDetectionStrategy.OnPush,
492+
})
493+
class TestABadgeComponent {
494+
readonly status = input.required<string>()
495+
}
496+
497+
@Component({
498+
selector: 'app-test-b-badge',
499+
template: `<span>B {{ status() }}</span>`,
500+
standalone: true,
501+
changeDetection: ChangeDetectionStrategy.OnPush,
502+
})
503+
class TestBBadgeComponent {
504+
readonly status = input.required<string>()
505+
}

packages/angular-table/tests/flex-render.test.ts

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import {
22
Component,
3-
ViewChild,
43
input,
4+
signal,
5+
ViewChild,
56
type TemplateRef,
6-
effect,
77
} from '@angular/core'
88
import { TestBed, type ComponentFixture } from '@angular/core/testing'
99
import { createColumnHelper } from '@tanstack/table-core'
@@ -12,11 +12,8 @@ import {
1212
FlexRenderDirective,
1313
injectFlexRenderContext,
1414
} from '../src/flex-render'
15+
import { flexRenderComponent } from '../src/flex-render/flex-render-component'
1516
import { setFixtureSignalInput, setFixtureSignalInputs } from './test-utils'
16-
import {
17-
flexRenderComponent,
18-
FlexRenderComponent,
19-
} from '../src/flex-render/flex-render-component'
2017

2118
interface Data {
2219
id: string
@@ -131,6 +128,44 @@ describe('FlexRenderDirective', () => {
131128
expect(fixture.nativeElement.textContent).toEqual('Updated value')
132129
})
133130

131+
test('should rerender when content has conditional return with different component types', () => {
132+
@Component({
133+
selector: 'app-fake-a',
134+
template: `A component`,
135+
standalone: true,
136+
})
137+
class FakeComponentA {
138+
context = injectFlexRenderContext<{ property: string }>()
139+
}
140+
141+
@Component({
142+
selector: 'app-fake-b',
143+
template: `B component`,
144+
standalone: true,
145+
})
146+
class FakeComponentB {}
147+
148+
const fixture = TestBed.createComponent(TestRenderComponent)
149+
const showB = signal(false)
150+
151+
setFixtureSignalInputs(fixture, {
152+
content: () => {
153+
return showB()
154+
? flexRenderComponent(FakeComponentB)
155+
: flexRenderComponent(FakeComponentA)
156+
},
157+
context: {},
158+
})
159+
160+
expect(fixture.nativeElement.textContent).toEqual('A component')
161+
162+
showB.set(true)
163+
164+
fixture.detectChanges()
165+
166+
expect(fixture.nativeElement.textContent).toEqual('B component')
167+
})
168+
134169
// Skip for now, test framework (using ComponentRef.setInput) cannot recognize signal inputs
135170
// as component inputs
136171
test('should render custom components', () => {

0 commit comments

Comments
 (0)