Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
f6a2112
feat: flex render granular updates
riccardoperra Jan 2, 2025
d1f9947
updates
riccardoperra Jan 2, 2025
2580676
cleanup
riccardoperra Jan 2, 2025
9f47ce3
cleanup
riccardoperra Jan 2, 2025
23def9a
cleanup
riccardoperra Jan 2, 2025
93b47d8
fix test
riccardoperra Jan 2, 2025
94bed11
angular add explicit version of typescript
riccardoperra Jan 3, 2025
a1b4e7f
Fix typescript versions
lachlancollins Jan 3, 2025
88905b6
add some testing for flex render in table
riccardoperra Jan 3, 2025
0a413b8
fix test infra
riccardoperra Jan 4, 2025
3d99cdd
refactor flex render
riccardoperra Jan 4, 2025
ee8b79f
update lock
riccardoperra Jan 4, 2025
9deb152
fix tests, cleanup code
riccardoperra Jan 4, 2025
700fc1f
fix tests, cleanup code
riccardoperra Jan 4, 2025
0249458
flex render signal content support
riccardoperra Jan 4, 2025
377446e
flex render signal content support
riccardoperra Jan 4, 2025
9d57db0
improve view flags, handle state update in zoneless
riccardoperra Jan 5, 2025
db39511
improve view flags, handle state update in zoneless
riccardoperra Jan 5, 2025
f385f0a
fix
riccardoperra Jan 5, 2025
b99169a
ci: apply automated fixes
autofix-ci[bot] Jan 5, 2025
2c1725b
clean docs
riccardoperra Jan 5, 2025
79569bd
test cases
riccardoperra Jan 6, 2025
4f0149a
fix: enable computed rowModels
riccardoperra Jan 6, 2025
3b2f6fa
fix test for rowModel
riccardoperra Jan 6, 2025
f822645
assures that `updateProps` update inputs only for Component reference…
riccardoperra Jan 6, 2025
1d9b642
Merge pull request #1 from riccardoperra/feat/angular-flex-render-sup…
riccardoperra Jan 9, 2025
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
268 changes: 155 additions & 113 deletions docs/framework/angular/angular-table.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,41 +40,181 @@ FlexRender supports any type of content supported by Angular:
- A [TemplateRef](https://angular.dev/api/core/TemplateRef)
- A [Component](https://angular.dev/api/core/Component) wrapped into `FlexRenderComponent`

Example:
You can just use the `cell.renderValue` or `cell.getValue` APIs to render the cells of your table. However,
these APIs will only spit out the raw cell values (from accessor functions).
If you are using the `cell: () => any` column definition options, you will want to use the `FlexRenderDirective` from the adapter.

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.

#### Example

```ts
@Component({
imports: [FlexRenderDirective],
//...
})
class YourComponent {}
```

```angular-html

<tbody>
@for (row of table.getRowModel().rows; track row.id) {
<tr>
@for (cell of row.getVisibleCells(); track cell.id) {
<td>
<ng-container
*flexRender="
<tr>
@for (cell of row.getVisibleCells(); track cell.id) {
<td>
<ng-container
*flexRender="
cell.column.columnDef.cell;
props: cell.getContext();
let cell
"
>
<!-- if you want to render a simple string -->
{{ cell }}
<!-- if you want to render an html string -->
<div [innerHTML]="cell"></div>
</ng-container>
</td>
}
</tr>
>
<!-- if you want to render a simple string -->
{{ cell }}
<!-- if you want to render an html string -->
<div [innerHTML]="cell"></div>
</ng-container>
</td>
}
</tr>
}
</tbody>
```

#### Rendering a Component

To render a Component into a specific column header/cell/footer, you can pass a `FlexRenderComponent` instantiated with
your `ComponentType, with the ability to include parameters such as inputs, outputs and a custom injector.

```ts
import {flexRenderComponent} from "./flex-render-component";
import {ChangeDetectionStrategy, input, output} from "@angular/core";

@Component({
template: `
...
`,
standalone: true,
changeDetectionStrategy: ChangeDetectionStrategy.OnPush,
host: {
'(click)': 'clickEvent.emit($event)'
}
})
class CustomCell {
readonly content = input.required<string>();
readonly cellType = input<MyType>();

// An output that will emit for every cell click
readonly clickEvent = output<Event>();
}

class AppComponent {
columns: ColumnDef<unknown>[] = [
{
id: 'custom-cell',
header: () => {
const translateService = inject(TranslateService);
return translateService.translate('...');
},
cell: (context) => {
return flexRenderComponent(
MyCustomComponent,
{
injector, // Optional injector
inputs: {
// Mandatory input since we are using `input.required()
content: context.row.original.rowProperty,
// cellType? - Optional input
},
outputs: {
clickEvent: () => {
// Do something
}
}
}
)
},
},
]
}
```

Underneath, this utilizes
the [ViewContainerRef#createComponent](https://angular.dev/api/core/ViewContainerRef#createComponent) api.
Therefore, you should declare your custom inputs using the @Input decorator or input/model signals.

You can still access the table cell context through the `injectFlexRenderContext` function, which returns the context
value based on the props you pass to the `FlexRenderDirective`.

```ts

@Component({
// ...
})
class CustomCellComponent {
// context of a cell component
readonly context = injectFlexRenderContext<CellContext<TData, TValue>>();
// context of a header/footer component
readonly context = injectFlexRenderContext<HeaderContext<TData, TValue>>();
}
```

Alternatively, you can render a component into a specific column header, cell, or footer by passing the component type
to the corresponding column definitions. These column definitions will be provided to the `flexRender` directive along
with the `context`.

```ts
class AppComponent {
columns: ColumnDef<Person>[] = [
{
id: 'select',
header: () => TableHeadSelectionComponent<Person>,
cell: () => TableRowSelectionComponent<Person>,
},
]
}
```

```angular-html
<ng-container
*flexRender="
header.column.columnDef.header;
props: header.getContext();
let headerCell
"
>
{{ headerCell }}
</ng-container>
```

Properties of `context` provided in the `flexRender` directive will be accessible to your component.
You can explicitly define the context properties required by your component.
In this example, the context provided to flexRender is of type HeaderContext.
Input signal `table`, which is a property of HeaderContext together with `column` and `header` properties,
is then defined to be used in the component. If any of the context properties are
needed in your component, feel free to use them. Please take note that only input signal is supported,
when defining access to context properties, using this approach.

```angular-ts
@Component({
template: `
<input
type="checkbox"
[checked]="table().getIsAllRowsSelected()"
[indeterminate]="table().getIsSomeRowsSelected()"
(change)="table().toggleAllRowsSelected()"
/>
`,
// ...
})
export class TableHeadSelectionComponent<T> {
//column = input.required<Column<T, unknown>>()
//header = input.required<Header<T, unknown>>()
table = input.required<Table<T>>()
}
```

#### Rendering a TemplateRef

In order to render a TemplateRef into a specific column header/cell/footer, you can pass the TemplateRef into the column
Expand Down Expand Up @@ -171,101 +311,3 @@ class AppComponent {
]
}
```

#### Rendering a Component

To render a Component into a specific column header/cell/footer, you can pass a `FlexRenderComponent instantiated with
your `ComponentType, with the ability to include optional parameters such as inputs and an injector.

```ts
import {FlexRenderComponent} from "@tanstack/angular-table";

class AppComponent {
columns: ColumnDef<unknown>[] = [
{
id: 'customCell',
header: () => new FlexRenderComponent(
CustomCellComponent,
{}, // optional inputs
injector // optional injector
),
cell: () => this.customCell(),
},
]
}
```

Underneath, this utilizes
the [ViewContainerRef#createComponent](https://angular.dev/api/core/ViewContainerRef#createComponent) api.
Therefore, you should declare your custom inputs using the @Input decorator or input/model signals.

You can still access the table cell context through the `injectFlexRenderContext` function, which returns the context
value based on the props you pass to the `FlexRenderDirective`.

```ts
@Component({
// ...
})
class CustomCellComponent {
// context of a cell component
readonly context = injectFlexRenderContext<CellContext<TData, TValue>>();
// context of a header/footer component
readonly context = injectFlexRenderContext<HeaderContext<TData, TValue>>();
}
```

Alternatively, you can render a component into a specific column header, cell, or footer by passing the component type
to the corresponding column definitions. These column definitions will be provided to the `flexRender` directive along with the `context`.

```ts
import {FlexRenderComponent} from "@tanstack/angular-table";

class AppComponent {
columns: ColumnDef<Person>[] = [
{
id: 'select',
header: () => TableHeadSelectionComponent<Person>,
cell: () => TableRowSelectionComponent<Person>,
},
]
}
```

```angular2html
<ng-container
*flexRender="
header.column.columnDef.header;
props: header.getContext();
let headerCell
"
>
{{ headerCell }}
</ng-container>
```

Properties of `context` provided in the `flexRender` directive will be accessible to your component.
You can explicitly define the context properties required by your component.
In this example, the context provided to flexRender is of type HeaderContext.
Input signal `table`, which is a property of HeaderContext together with `column` and `header` properties,
is then defined to be used in the component. If any of the context properties are
needed in your component, feel free to use them. Please take note that only input signal is supported,
when defining access to context properties, using this approach.

```angular-ts
@Component({
template: `
<input
type="checkbox"
[checked]="table().getIsAllRowsSelected()"
[indeterminate]="table().getIsSomeRowsSelected()"
(change)="table().toggleAllRowsSelected()"
/>
`,
// ...
})
export class TableHeadSelectionComponent<T> {
//column = input.required<Column<T, unknown>>()
//header = input.required<Header<T, unknown>>()
table = input.required<Table<T>>()
}
```
6 changes: 3 additions & 3 deletions examples/angular/row-selection/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import {
ColumnDef,
createAngularTable,
FlexRenderComponent,
flexRenderComponent,
FlexRenderDirective,
getCoreRowModel,
getFilteredRowModel,
Expand Down Expand Up @@ -43,10 +43,10 @@ export class AppComponent {
{
id: 'select',
header: () => {
return new FlexRenderComponent(TableHeadSelectionComponent)
return flexRenderComponent(TableHeadSelectionComponent)
},
cell: () => {
return new FlexRenderComponent(TableRowSelectionComponent)
return flexRenderComponent(TableRowSelectionComponent)
},
},
{
Expand Down
9 changes: 6 additions & 3 deletions packages/angular-table/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
],
"scripts": {
"clean": "rimraf ./build",
"test:types": "tsc --noEmit",
"test:types": "tsc --noEmit && vitest --typecheck",
"test:lib": "vitest",
"test:lib:dev": "vitest --watch",
"build": "ng-packagr -p ng-package.json -c tsconfig.build.json && rimraf ./build/lib/package.json"
Expand All @@ -53,11 +53,14 @@
"tslib": "^2.6.2"
},
"devDependencies": {
"@analogjs/vite-plugin-angular": "^1.3.1",
"@analogjs/vite-plugin-angular": "^1.11.0",
"@analogjs/vitest-angular": "^1.11.0",
"@angular/core": "^17.3.9",
"@angular/platform-browser": "^17.3.9",
"@angular/platform-browser-dynamic": "^17.3.9",
"ng-packagr": "^17.3.0"
"ng-packagr": "^17.3.0",
"typescript": "5.4.5",
"vitest": "^1.6.0"
},
"peerDependencies": {
"@angular/core": ">=17"
Expand Down
Loading
Loading