Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 13 additions & 6 deletions packages/angular-table/src/flex-render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -27,9 +28,9 @@ import {
FlexRenderTemplateView,
type FlexRenderTypedContent,
FlexRenderView,
FlexRenderViewAllowedType,
mapToFlexRenderTypedContent,
} from './flex-render/view'
import { memo } from '@tanstack/table-core'

export {
injectFlexRenderContext,
Expand Down Expand Up @@ -72,7 +73,10 @@ export class FlexRenderDirective<TProps extends NonNullable<unknown>>
injector: Injector = inject(Injector)

renderFlags = FlexRenderFlags.ViewFirstRender
renderView: FlexRenderView<any> | null = null
renderView: FlexRenderView<
FlexRenderViewAllowedType,
FlexRenderTypedContent
> | null = null

readonly #latestContent = () => {
const { content, props } = this
Expand Down Expand Up @@ -121,11 +125,14 @@ export class FlexRenderDirective<TProps extends NonNullable<unknown>>
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()
}
Expand Down Expand Up @@ -205,7 +212,7 @@ export class FlexRenderDirective<TProps extends NonNullable<unknown>>

#renderViewByContent(
content: FlexRenderTypedContent,
): FlexRenderView<any> | null {
): FlexRenderView<FlexRenderViewAllowedType, FlexRenderTypedContent> | null {
if (content.kind === 'primitive') {
return this.#renderStringContent(content)
} else if (content.kind === 'templateRef') {
Expand Down
48 changes: 45 additions & 3 deletions packages/angular-table/src/flex-render/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,14 @@ export function mapToFlexRenderTypedContent(
}
}

export type FlexRenderViewAllowedType =
| FlexRenderComponentRef<any>
| EmbeddedViewRef<unknown>
| null

export abstract class FlexRenderView<
TView extends FlexRenderComponentRef<any> | EmbeddedViewRef<unknown> | null,
TView extends FlexRenderViewAllowedType,
TContent extends FlexRenderTypedContent,
> {
readonly view: TView
#previousContent: FlexRenderTypedContent | undefined
Expand Down Expand Up @@ -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<unknown>
EmbeddedViewRef<unknown>,
Extract<FlexRenderTypedContent, { kind: 'primitive' | 'templateRef' }>
> {
constructor(
initialContent: Extract<
Expand Down Expand Up @@ -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<unknown>
FlexRenderComponentRef<unknown>,
Extract<FlexRenderTypedContent, { kind: 'component' | 'flexRenderComponent' }>
> {
constructor(
initialContent: Extract<
Expand Down Expand Up @@ -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)
)
}
}
56 changes: 56 additions & 0 deletions packages/angular-table/tests/flex-render-table.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<FlexRenderContent<any>>('Initial status')

Expand Down Expand Up @@ -447,3 +483,23 @@ class TestBadgeComponent {

readonly status = input.required<string>()
}

@Component({
selector: 'app-test-a-badge',
template: `<span>A {{ status() }}</span>`,
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
})
class TestABadgeComponent {
readonly status = input.required<string>()
}

@Component({
selector: 'app-test-b-badge',
template: `<span>B {{ status() }}</span>`,
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
})
class TestBBadgeComponent {
readonly status = input.required<string>()
}
47 changes: 41 additions & 6 deletions packages/angular-table/tests/flex-render.test.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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
Expand Down Expand Up @@ -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', () => {
Expand Down
Loading