Skip to content

Commit 2e4af82

Browse files
fix(angular): [v9] fix conditional flexRenderComponent rendering (#6219)
* fix(angular): fix conditional flexRenderComponent rendering This resolve a rendering issue with flexRenderDirective that doesn't re-render component while using conditional flexRenderComponent return in column configuration * chore: fix sherif * lint: fix solid devtools lint
1 parent b52e3aa commit 2e4af82

File tree

7 files changed

+176
-25
lines changed

7 files changed

+176
-25
lines changed

examples/react/row-selection/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
},
2020
"devDependencies": {
2121
"@rollup/plugin-replace": "^6.0.3",
22-
"@tanstack/react-devtools": "0.10.1",
22+
"@tanstack/react-devtools": "^0.10.1",
2323
"@tanstack/react-table-devtools": "9.0.0-alpha.11",
2424
"@types/react": "^19.2.14",
2525
"@types/react-dom": "^19.2.3",

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

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,33 +5,37 @@ import {
55
runInInjectionContext,
66
untracked,
77
} from '@angular/core'
8-
import { TanStackTableToken } from '../helpers/table'
98
import { TanStackTableCellToken } from '../helpers/cell'
109
import { TanStackTableHeaderToken } from '../helpers/header'
10+
import { TanStackTableToken } from '../helpers/table'
1111
import { FlexRenderComponentProps } from './context'
1212
import { FlexRenderFlags } from './flags'
13+
import { flexRenderComponent } from './flexRenderComponent'
14+
import { FlexRenderComponentFactory } from './flexRenderComponentFactory'
1315
import {
1416
FlexRenderComponentView,
1517
FlexRenderTemplateView,
1618
mapToFlexRenderTypedContent,
1719
} from './view'
18-
import { flexRenderComponent } from './flexRenderComponent'
19-
import { FlexRenderComponentFactory } from './flexRenderComponentFactory'
20-
import type { FlexRenderComponent } from './flexRenderComponent'
2120
import type {
22-
EffectRef,
23-
TemplateRef,
24-
Type,
25-
ViewContainerRef,
26-
} from '@angular/core'
27-
import type { FlexRenderTypedContent, FlexRenderView } from './view'
21+
FlexRenderTypedContent,
22+
FlexRenderView,
23+
FlexRenderViewAllowedType,
24+
} from './view'
25+
import type { FlexRenderComponent } from './flexRenderComponent'
2826
import type {
2927
CellContext,
3028
CellData,
3129
HeaderContext,
3230
RowData,
3331
TableFeatures,
3432
} from '@tanstack/table-core'
33+
import type {
34+
EffectRef,
35+
TemplateRef,
36+
Type,
37+
ViewContainerRef,
38+
} from '@angular/core'
3539

3640
/**
3741
* Content supported by the `flexRender` directive when declaring
@@ -106,7 +110,10 @@ export class FlexViewRenderer<
106110
| HeaderContext<TFeatures, TRowData, TValue>,
107111
> {
108112
#renderFlags = FlexRenderFlags.ViewFirstRender
109-
#renderView: FlexRenderView<any> | null = null
113+
#renderView: FlexRenderView<
114+
FlexRenderViewAllowedType,
115+
FlexRenderTypedContent
116+
> | null = null
110117
#currentRenderEffectRef: EffectRef | null = null
111118
#content: () => FlexRenderInputContent<TProps>
112119
#props: () => TProps
@@ -257,18 +264,21 @@ export class FlexViewRenderer<
257264
if (latestContent.kind === 'null' || !this.#renderView) {
258265
this.#renderFlags |= FlexRenderFlags.ContentChanged
259266
} else {
260-
this.#renderView.content = latestContent
261-
const { kind: previousKind } = this.#renderView.previousContent
262-
if (latestContent.kind !== previousKind) {
267+
const { kind: currentKind } = this.#renderView.content
268+
if (
269+
latestContent.kind !== currentKind ||
270+
!this.#renderView.eq(latestContent)
271+
) {
263272
this.#renderFlags |= FlexRenderFlags.ContentChanged
264273
}
274+
this.#renderView.content = latestContent
265275
}
266276
this.#update()
267277
}
268278

269279
#renderViewByContent(
270280
content: FlexRenderTypedContent,
271-
): FlexRenderView<any> | null {
281+
): FlexRenderView<FlexRenderViewAllowedType, FlexRenderTypedContent> | null {
272282
if (content.kind === 'primitive') {
273283
return this.#renderStringContent(content)
274284
} 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
@@ -35,8 +35,14 @@ export function mapToFlexRenderTypedContent(
3535
}
3636
}
3737

38+
export type FlexRenderViewAllowedType =
39+
| FlexRenderComponentRef<any>
40+
| EmbeddedViewRef<unknown>
41+
| null
42+
3843
export abstract class FlexRenderView<
39-
TView extends FlexRenderComponentRef<any> | EmbeddedViewRef<unknown> | null,
44+
TView extends FlexRenderViewAllowedType,
45+
TContent extends FlexRenderTypedContent,
4046
> {
4147
readonly view: TView
4248
#previousContent: FlexRenderTypedContent | undefined
@@ -69,11 +75,14 @@ export abstract class FlexRenderView<
6975

7076
abstract onDestroy(callback: Function): void
7177

78+
abstract eq(view: TContent): boolean
79+
7280
abstract unmount(): void
7381
}
7482

7583
export class FlexRenderTemplateView extends FlexRenderView<
76-
EmbeddedViewRef<unknown>
84+
EmbeddedViewRef<unknown>,
85+
Extract<FlexRenderTypedContent, { kind: 'primitive' | 'templateRef' }>
7786
> {
7887
constructor(
7988
initialContent: Extract<
@@ -105,10 +114,27 @@ export class FlexRenderTemplateView extends FlexRenderView<
105114
override onDestroy(callback: Function) {
106115
this.view.onDestroy(callback)
107116
}
117+
118+
override eq(
119+
compare: Extract<
120+
FlexRenderTypedContent,
121+
{ kind: 'primitive' | 'templateRef' }
122+
>,
123+
): boolean {
124+
return (
125+
(this.content.kind === 'primitive' &&
126+
compare.kind === 'primitive' &&
127+
this.content.content === compare.content) ||
128+
(this.content.kind === 'templateRef' &&
129+
compare.kind === 'templateRef' &&
130+
this.content.content === compare.content)
131+
)
132+
}
108133
}
109134

110135
export class FlexRenderComponentView extends FlexRenderView<
111-
FlexRenderComponentRef<unknown>
136+
FlexRenderComponentRef<unknown>,
137+
Extract<FlexRenderTypedContent, { kind: 'component' | 'flexRenderComponent' }>
112138
> {
113139
constructor(
114140
initialContent: Extract<
@@ -162,4 +188,20 @@ export class FlexRenderComponentView extends FlexRenderView<
162188
override onDestroy(callback: Function) {
163189
this.view.componentRef.onDestroy(callback)
164190
}
191+
192+
override eq(
193+
compare: Extract<
194+
FlexRenderTypedContent,
195+
{ kind: 'component' | 'flexRenderComponent' }
196+
>,
197+
): boolean {
198+
return (
199+
(this.content.kind === 'component' &&
200+
compare.kind === 'component' &&
201+
this.content.content === compare.content) ||
202+
(this.content.kind === 'flexRenderComponent' &&
203+
compare.kind === 'flexRenderComponent' &&
204+
this.content.content.component === compare.content.component)
205+
)
206+
}
165207
}

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

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

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

@@ -546,3 +582,23 @@ class TestBadgeComponent {
546582

547583
readonly status = input.required<string>()
548584
}
585+
586+
@Component({
587+
selector: 'app-test-a-badge',
588+
template: `<span>A {{ status() }}</span>`,
589+
standalone: true,
590+
changeDetection: ChangeDetectionStrategy.OnPush,
591+
})
592+
class TestABadgeComponent {
593+
readonly status = input.required<string>()
594+
}
595+
596+
@Component({
597+
selector: 'app-test-b-badge',
598+
template: `<span>B {{ status() }}</span>`,
599+
standalone: true,
600+
changeDetection: ChangeDetectionStrategy.OnPush,
601+
})
602+
class TestBBadgeComponent {
603+
readonly status = input.required<string>()
604+
}

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

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
1-
import { Component, ViewChild, input } from '@angular/core'
2-
import { TestBed } from '@angular/core/testing'
1+
import {
2+
Component,
3+
input,
4+
signal,
5+
ViewChild,
6+
type TemplateRef,
7+
} from '@angular/core'
8+
import { TestBed, type ComponentFixture } from '@angular/core/testing'
39
import { createColumnHelper } from '@tanstack/table-core'
410
import { describe, expect, test } from 'vitest'
511
import {
612
FlexRender,
7-
FlexRenderDirective,
813
flexRenderComponent,
14+
FlexRenderDirective,
915
injectFlexRenderContext,
1016
} from '../../src'
1117
import { setFixtureSignalInput, setFixtureSignalInputs } from '../test-utils'
12-
import type { TemplateRef } from '@angular/core'
13-
import type { ComponentFixture } from '@angular/core/testing'
1418

1519
interface Data {
1620
id: string
@@ -129,6 +133,44 @@ describe('FlexRenderDirective', () => {
129133
expect(fixture.nativeElement.textContent).toEqual('Updated value')
130134
})
131135

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

packages/solid-table-devtools/src/production/plugin.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createSolidPlugin } from '@tanstack/devtools-utils/solid'
22
import { TableDevtoolsPanel } from './TableDevtools'
3+
import type { TanStackDevtoolsPlugin } from '@tanstack/devtools'
34

45
type SolidTableDevtoolsPlugin = ReturnType<
56
ReturnType<typeof createSolidPlugin>[0]

pnpm-lock.yaml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)