Skip to content

Commit 05ecd5e

Browse files
riccardoperralachlancollinsautofix-ci[bot]
authored
feat(angular-table): Refactor Flex render implementation - Zoneless, Better type safety, allows reactive values into cell content, re-render when cell context changes, allow to pass signal inputs into custom components (#5856)
* feat: flex render granular updates * updates * cleanup * cleanup * cleanup * fix test * angular add explicit version of typescript * Fix typescript versions * add some testing for flex render in table * fix test infra * refactor flex render * update lock * fix tests, cleanup code * fix tests, cleanup code * flex render signal content support * flex render signal content support * improve view flags, handle state update in zoneless * improve view flags, handle state update in zoneless * fix * ci: apply automated fixes * clean docs test fix doc add flexRenderComponent util * test cases * fix: enable computed rowModels * fix test for rowModel * assures that `updateProps` update inputs only for Component reference type * Merge pull request #1 from riccardoperra/feat/angular-flex-render-support-output-binding add support for angular outputs in flex-render-component --------- Co-authored-by: Lachlan Collins <[email protected]> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 5de0729 commit 05ecd5e

20 files changed

+1618
-468
lines changed

docs/framework/angular/angular-table.md

Lines changed: 155 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -40,41 +40,181 @@ FlexRender supports any type of content supported by Angular:
4040
- A [TemplateRef](https://angular.dev/api/core/TemplateRef)
4141
- A [Component](https://angular.dev/api/core/Component) wrapped into `FlexRenderComponent`
4242

43-
Example:
43+
You can just use the `cell.renderValue` or `cell.getValue` APIs to render the cells of your table. However,
44+
these APIs will only spit out the raw cell values (from accessor functions).
45+
If you are using the `cell: () => any` column definition options, you will want to use the `FlexRenderDirective` from the adapter.
46+
47+
Cell column definition is **reactive** and runs into an **injection context**, then you can inject services or make use of signals to automatically modify the rendered content.
48+
49+
#### Example
4450

4551
```ts
4652
@Component({
4753
imports: [FlexRenderDirective],
4854
//...
4955
})
56+
class YourComponent {}
5057
```
5158

5259
```angular-html
5360
5461
<tbody>
5562
@for (row of table.getRowModel().rows; track row.id) {
56-
<tr>
57-
@for (cell of row.getVisibleCells(); track cell.id) {
58-
<td>
59-
<ng-container
60-
*flexRender="
63+
<tr>
64+
@for (cell of row.getVisibleCells(); track cell.id) {
65+
<td>
66+
<ng-container
67+
*flexRender="
6168
cell.column.columnDef.cell;
6269
props: cell.getContext();
6370
let cell
6471
"
65-
>
66-
<!-- if you want to render a simple string -->
67-
{{ cell }}
68-
<!-- if you want to render an html string -->
69-
<div [innerHTML]="cell"></div>
70-
</ng-container>
71-
</td>
72-
}
73-
</tr>
72+
>
73+
<!-- if you want to render a simple string -->
74+
{{ cell }}
75+
<!-- if you want to render an html string -->
76+
<div [innerHTML]="cell"></div>
77+
</ng-container>
78+
</td>
79+
}
80+
</tr>
7481
}
7582
</tbody>
7683
```
7784

85+
#### Rendering a Component
86+
87+
To render a Component into a specific column header/cell/footer, you can pass a `FlexRenderComponent` instantiated with
88+
your `ComponentType, with the ability to include parameters such as inputs, outputs and a custom injector.
89+
90+
```ts
91+
import {flexRenderComponent} from "./flex-render-component";
92+
import {ChangeDetectionStrategy, input, output} from "@angular/core";
93+
94+
@Component({
95+
template: `
96+
...
97+
`,
98+
standalone: true,
99+
changeDetectionStrategy: ChangeDetectionStrategy.OnPush,
100+
host: {
101+
'(click)': 'clickEvent.emit($event)'
102+
}
103+
})
104+
class CustomCell {
105+
readonly content = input.required<string>();
106+
readonly cellType = input<MyType>();
107+
108+
// An output that will emit for every cell click
109+
readonly clickEvent = output<Event>();
110+
}
111+
112+
class AppComponent {
113+
columns: ColumnDef<unknown>[] = [
114+
{
115+
id: 'custom-cell',
116+
header: () => {
117+
const translateService = inject(TranslateService);
118+
return translateService.translate('...');
119+
},
120+
cell: (context) => {
121+
return flexRenderComponent(
122+
MyCustomComponent,
123+
{
124+
injector, // Optional injector
125+
inputs: {
126+
// Mandatory input since we are using `input.required()
127+
content: context.row.original.rowProperty,
128+
// cellType? - Optional input
129+
},
130+
outputs: {
131+
clickEvent: () => {
132+
// Do something
133+
}
134+
}
135+
}
136+
)
137+
},
138+
},
139+
]
140+
}
141+
```
142+
143+
Underneath, this utilizes
144+
the [ViewContainerRef#createComponent](https://angular.dev/api/core/ViewContainerRef#createComponent) api.
145+
Therefore, you should declare your custom inputs using the @Input decorator or input/model signals.
146+
147+
You can still access the table cell context through the `injectFlexRenderContext` function, which returns the context
148+
value based on the props you pass to the `FlexRenderDirective`.
149+
150+
```ts
151+
152+
@Component({
153+
// ...
154+
})
155+
class CustomCellComponent {
156+
// context of a cell component
157+
readonly context = injectFlexRenderContext<CellContext<TData, TValue>>();
158+
// context of a header/footer component
159+
readonly context = injectFlexRenderContext<HeaderContext<TData, TValue>>();
160+
}
161+
```
162+
163+
Alternatively, you can render a component into a specific column header, cell, or footer by passing the component type
164+
to the corresponding column definitions. These column definitions will be provided to the `flexRender` directive along
165+
with the `context`.
166+
167+
```ts
168+
class AppComponent {
169+
columns: ColumnDef<Person>[] = [
170+
{
171+
id: 'select',
172+
header: () => TableHeadSelectionComponent<Person>,
173+
cell: () => TableRowSelectionComponent<Person>,
174+
},
175+
]
176+
}
177+
```
178+
179+
```angular-html
180+
<ng-container
181+
*flexRender="
182+
header.column.columnDef.header;
183+
props: header.getContext();
184+
let headerCell
185+
"
186+
>
187+
{{ headerCell }}
188+
</ng-container>
189+
```
190+
191+
Properties of `context` provided in the `flexRender` directive will be accessible to your component.
192+
You can explicitly define the context properties required by your component.
193+
In this example, the context provided to flexRender is of type HeaderContext.
194+
Input signal `table`, which is a property of HeaderContext together with `column` and `header` properties,
195+
is then defined to be used in the component. If any of the context properties are
196+
needed in your component, feel free to use them. Please take note that only input signal is supported,
197+
when defining access to context properties, using this approach.
198+
199+
```angular-ts
200+
@Component({
201+
template: `
202+
<input
203+
type="checkbox"
204+
[checked]="table().getIsAllRowsSelected()"
205+
[indeterminate]="table().getIsSomeRowsSelected()"
206+
(change)="table().toggleAllRowsSelected()"
207+
/>
208+
`,
209+
// ...
210+
})
211+
export class TableHeadSelectionComponent<T> {
212+
//column = input.required<Column<T, unknown>>()
213+
//header = input.required<Header<T, unknown>>()
214+
table = input.required<Table<T>>()
215+
}
216+
```
217+
78218
#### Rendering a TemplateRef
79219

80220
In order to render a TemplateRef into a specific column header/cell/footer, you can pass the TemplateRef into the column
@@ -171,101 +311,3 @@ class AppComponent {
171311
]
172312
}
173313
```
174-
175-
#### Rendering a Component
176-
177-
To render a Component into a specific column header/cell/footer, you can pass a `FlexRenderComponent instantiated with
178-
your `ComponentType, with the ability to include optional parameters such as inputs and an injector.
179-
180-
```ts
181-
import {FlexRenderComponent} from "@tanstack/angular-table";
182-
183-
class AppComponent {
184-
columns: ColumnDef<unknown>[] = [
185-
{
186-
id: 'customCell',
187-
header: () => new FlexRenderComponent(
188-
CustomCellComponent,
189-
{}, // optional inputs
190-
injector // optional injector
191-
),
192-
cell: () => this.customCell(),
193-
},
194-
]
195-
}
196-
```
197-
198-
Underneath, this utilizes
199-
the [ViewContainerRef#createComponent](https://angular.dev/api/core/ViewContainerRef#createComponent) api.
200-
Therefore, you should declare your custom inputs using the @Input decorator or input/model signals.
201-
202-
You can still access the table cell context through the `injectFlexRenderContext` function, which returns the context
203-
value based on the props you pass to the `FlexRenderDirective`.
204-
205-
```ts
206-
@Component({
207-
// ...
208-
})
209-
class CustomCellComponent {
210-
// context of a cell component
211-
readonly context = injectFlexRenderContext<CellContext<TData, TValue>>();
212-
// context of a header/footer component
213-
readonly context = injectFlexRenderContext<HeaderContext<TData, TValue>>();
214-
}
215-
```
216-
217-
Alternatively, you can render a component into a specific column header, cell, or footer by passing the component type
218-
to the corresponding column definitions. These column definitions will be provided to the `flexRender` directive along with the `context`.
219-
220-
```ts
221-
import {FlexRenderComponent} from "@tanstack/angular-table";
222-
223-
class AppComponent {
224-
columns: ColumnDef<Person>[] = [
225-
{
226-
id: 'select',
227-
header: () => TableHeadSelectionComponent<Person>,
228-
cell: () => TableRowSelectionComponent<Person>,
229-
},
230-
]
231-
}
232-
```
233-
234-
```angular2html
235-
<ng-container
236-
*flexRender="
237-
header.column.columnDef.header;
238-
props: header.getContext();
239-
let headerCell
240-
"
241-
>
242-
{{ headerCell }}
243-
</ng-container>
244-
```
245-
246-
Properties of `context` provided in the `flexRender` directive will be accessible to your component.
247-
You can explicitly define the context properties required by your component.
248-
In this example, the context provided to flexRender is of type HeaderContext.
249-
Input signal `table`, which is a property of HeaderContext together with `column` and `header` properties,
250-
is then defined to be used in the component. If any of the context properties are
251-
needed in your component, feel free to use them. Please take note that only input signal is supported,
252-
when defining access to context properties, using this approach.
253-
254-
```angular-ts
255-
@Component({
256-
template: `
257-
<input
258-
type="checkbox"
259-
[checked]="table().getIsAllRowsSelected()"
260-
[indeterminate]="table().getIsSomeRowsSelected()"
261-
(change)="table().toggleAllRowsSelected()"
262-
/>
263-
`,
264-
// ...
265-
})
266-
export class TableHeadSelectionComponent<T> {
267-
//column = input.required<Column<T, unknown>>()
268-
//header = input.required<Header<T, unknown>>()
269-
table = input.required<Table<T>>()
270-
}
271-
```

examples/angular/row-selection/src/app/app.component.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
import {
1010
ColumnDef,
1111
createAngularTable,
12-
FlexRenderComponent,
12+
flexRenderComponent,
1313
FlexRenderDirective,
1414
getCoreRowModel,
1515
getFilteredRowModel,
@@ -43,10 +43,10 @@ export class AppComponent {
4343
{
4444
id: 'select',
4545
header: () => {
46-
return new FlexRenderComponent(TableHeadSelectionComponent)
46+
return flexRenderComponent(TableHeadSelectionComponent)
4747
},
4848
cell: () => {
49-
return new FlexRenderComponent(TableRowSelectionComponent)
49+
return flexRenderComponent(TableRowSelectionComponent)
5050
},
5151
},
5252
{

packages/angular-table/package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
],
4444
"scripts": {
4545
"clean": "rimraf ./build",
46-
"test:types": "tsc --noEmit",
46+
"test:types": "tsc --noEmit && vitest --typecheck",
4747
"test:lib": "vitest",
4848
"test:lib:dev": "vitest --watch",
4949
"build": "ng-packagr -p ng-package.json -c tsconfig.build.json && rimraf ./build/lib/package.json"
@@ -53,11 +53,14 @@
5353
"tslib": "^2.6.2"
5454
},
5555
"devDependencies": {
56-
"@analogjs/vite-plugin-angular": "^1.3.1",
56+
"@analogjs/vite-plugin-angular": "^1.11.0",
57+
"@analogjs/vitest-angular": "^1.11.0",
5758
"@angular/core": "^17.3.9",
5859
"@angular/platform-browser": "^17.3.9",
5960
"@angular/platform-browser-dynamic": "^17.3.9",
60-
"ng-packagr": "^17.3.0"
61+
"ng-packagr": "^17.3.0",
62+
"typescript": "5.4.5",
63+
"vitest": "^1.6.0"
6164
},
6265
"peerDependencies": {
6366
"@angular/core": ">=17"

0 commit comments

Comments
 (0)