diff --git a/packages/angular-table/src/flex-render.ts b/packages/angular-table/src/flex-render.ts index 1414d032b1..ff1e8cd245 100644 --- a/packages/angular-table/src/flex-render.ts +++ b/packages/angular-table/src/flex-render.ts @@ -15,6 +15,7 @@ import { Type, ViewContainerRef, } from '@angular/core' +import { memo } from '@tanstack/table-core' import { FlexRenderComponentProps } from './flex-render/context' import { FlexRenderFlags } from './flex-render/flags' import { @@ -27,9 +28,9 @@ import { FlexRenderTemplateView, type FlexRenderTypedContent, FlexRenderView, + FlexRenderViewAllowedType, mapToFlexRenderTypedContent, } from './flex-render/view' -import { memo } from '@tanstack/table-core' export { injectFlexRenderContext, @@ -72,7 +73,10 @@ export class FlexRenderDirective> injector: Injector = inject(Injector) renderFlags = FlexRenderFlags.ViewFirstRender - renderView: FlexRenderView | null = null + renderView: FlexRenderView< + FlexRenderViewAllowedType, + FlexRenderTypedContent + > | null = null readonly #latestContent = () => { const { content, props } = this @@ -121,11 +125,14 @@ export class FlexRenderDirective> if (latestContent.kind === 'null' || !this.renderView) { this.renderFlags |= FlexRenderFlags.ContentChanged } else { - this.renderView.content = latestContent - const { kind: previousKind } = this.renderView.previousContent - if (latestContent.kind !== previousKind) { + const { kind: currentKind } = this.renderView.content + if ( + latestContent.kind !== currentKind || + !this.renderView.eq(latestContent) + ) { this.renderFlags |= FlexRenderFlags.ContentChanged } + this.renderView.content = latestContent } this.update() } @@ -205,7 +212,7 @@ export class FlexRenderDirective> #renderViewByContent( content: FlexRenderTypedContent, - ): FlexRenderView | null { + ): FlexRenderView | null { if (content.kind === 'primitive') { return this.#renderStringContent(content) } else if (content.kind === 'templateRef') { diff --git a/packages/angular-table/src/flex-render/view.ts b/packages/angular-table/src/flex-render/view.ts index c54e57b1bb..5bcb04ebb0 100644 --- a/packages/angular-table/src/flex-render/view.ts +++ b/packages/angular-table/src/flex-render/view.ts @@ -33,8 +33,14 @@ export function mapToFlexRenderTypedContent( } } +export type FlexRenderViewAllowedType = + | FlexRenderComponentRef + | EmbeddedViewRef + | null + export abstract class FlexRenderView< - TView extends FlexRenderComponentRef | EmbeddedViewRef | null, + TView extends FlexRenderViewAllowedType, + TContent extends FlexRenderTypedContent, > { readonly view: TView #previousContent: FlexRenderTypedContent | undefined @@ -66,10 +72,13 @@ export abstract class FlexRenderView< abstract dirtyCheck(): void abstract onDestroy(callback: Function): void + + abstract eq(view: TContent): boolean } export class FlexRenderTemplateView extends FlexRenderView< - EmbeddedViewRef + EmbeddedViewRef, + Extract > { constructor( initialContent: Extract< @@ -97,10 +106,27 @@ export class FlexRenderTemplateView extends FlexRenderView< override onDestroy(callback: Function) { this.view.onDestroy(callback) } + + override eq( + compare: Extract< + FlexRenderTypedContent, + { kind: 'primitive' | 'templateRef' } + >, + ): boolean { + return ( + (this.content.kind === 'primitive' && + compare.kind === 'primitive' && + this.content.content === compare.content) || + (this.content.kind === 'templateRef' && + compare.kind === 'templateRef' && + this.content.content === compare.content) + ) + } } export class FlexRenderComponentView extends FlexRenderView< - FlexRenderComponentRef + FlexRenderComponentRef, + Extract > { constructor( initialContent: Extract< @@ -150,4 +176,20 @@ export class FlexRenderComponentView extends FlexRenderView< override onDestroy(callback: Function) { this.view.componentRef.onDestroy(callback) } + + override eq( + compare: Extract< + FlexRenderTypedContent, + { kind: 'component' | 'flexRenderComponent' } + >, + ): boolean { + return ( + (this.content.kind === 'component' && + compare.kind === 'component' && + this.content.content === compare.content) || + (this.content.kind === 'flexRenderComponent' && + compare.kind === 'flexRenderComponent' && + this.content.content.component === compare.content.component) + ) + } } diff --git a/packages/angular-table/tests/flex-render-table.test.ts b/packages/angular-table/tests/flex-render-table.test.ts index 013cb6ffa7..6c3144ad6d 100644 --- a/packages/angular-table/tests/flex-render-table.test.ts +++ b/packages/angular-table/tests/flex-render-table.test.ts @@ -119,6 +119,42 @@ describe('FlexRenderDirective', () => { expect(firstCell!.textContent).toEqual('Updated status') }) + test('Render content reactively when flexRenderComponent class changes', async () => { + const showBadgeA = signal(true) + + const { dom, fixture } = createTestTable(defaultData, [ + { + id: 'first_cell', + header: 'Status', + cell: () => { + return showBadgeA() + ? flexRenderComponent(TestABadgeComponent, { + inputs: { status: 'From A' }, + }) + : flexRenderComponent(TestBBadgeComponent, { + inputs: { status: 'From B' }, + }) + }, + }, + ]) + + const row = dom.getBodyRow(0)! + const firstCell = row.querySelector('td')! + + let firstElement = firstCell.firstElementChild as HTMLElement | null + expect(firstElement).not.toBeNull() + expect(firstElement!.tagName).toEqual('APP-TEST-A-BADGE') + expect(firstCell.textContent).toContain('From A') + + showBadgeA.set(false) + fixture.detectChanges() + + firstElement = firstCell.firstElementChild as HTMLElement | null + expect(firstElement).not.toBeNull() + expect(firstElement!.tagName).toEqual('APP-TEST-B-BADGE') + expect(firstCell.textContent).toContain('From B') + }) + test('Render content reactively based on signal value', async () => { const statusComponent = signal>('Initial status') @@ -447,3 +483,23 @@ class TestBadgeComponent { readonly status = input.required() } + +@Component({ + selector: 'app-test-a-badge', + template: `A {{ status() }}`, + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +class TestABadgeComponent { + readonly status = input.required() +} + +@Component({ + selector: 'app-test-b-badge', + template: `B {{ status() }}`, + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +class TestBBadgeComponent { + readonly status = input.required() +} diff --git a/packages/angular-table/tests/flex-render.test.ts b/packages/angular-table/tests/flex-render.test.ts index 9e0d4c6d7f..06dbd23292 100644 --- a/packages/angular-table/tests/flex-render.test.ts +++ b/packages/angular-table/tests/flex-render.test.ts @@ -1,9 +1,9 @@ import { Component, - ViewChild, input, + signal, + ViewChild, type TemplateRef, - effect, } from '@angular/core' import { TestBed, type ComponentFixture } from '@angular/core/testing' import { createColumnHelper } from '@tanstack/table-core' @@ -12,11 +12,8 @@ import { FlexRenderDirective, injectFlexRenderContext, } from '../src/flex-render' +import { flexRenderComponent } from '../src/flex-render/flex-render-component' import { setFixtureSignalInput, setFixtureSignalInputs } from './test-utils' -import { - flexRenderComponent, - FlexRenderComponent, -} from '../src/flex-render/flex-render-component' interface Data { id: string @@ -131,6 +128,44 @@ describe('FlexRenderDirective', () => { expect(fixture.nativeElement.textContent).toEqual('Updated value') }) + test('should rerender when content has conditional return with different component types', () => { + @Component({ + selector: 'app-fake-a', + template: `A component`, + standalone: true, + }) + class FakeComponentA { + context = injectFlexRenderContext<{ property: string }>() + } + + @Component({ + selector: 'app-fake-b', + template: `B component`, + standalone: true, + }) + class FakeComponentB {} + + const fixture = TestBed.createComponent(TestRenderComponent) + const showB = signal(false) + + setFixtureSignalInputs(fixture, { + content: () => { + return showB() + ? flexRenderComponent(FakeComponentB) + : flexRenderComponent(FakeComponentA) + }, + context: {}, + }) + + expect(fixture.nativeElement.textContent).toEqual('A component') + + showB.set(true) + + fixture.detectChanges() + + expect(fixture.nativeElement.textContent).toEqual('B component') + }) + // Skip for now, test framework (using ComponentRef.setInput) cannot recognize signal inputs // as component inputs test('should render custom components', () => {