ngx-cfo
v1.6.3
Published
A component library built on top of [AG Grid Community](https://www.ag-grid.com/) and the [Kirby Design System](https://cookbook.kirby.design/), providing a ready-to-use data table with persistent state, row selection, column management, and more.
Readme
ngx-cfo
A component library built on top of AG Grid Community and the Kirby Design System, providing a ready-to-use data table with persistent state, row selection, column management, and more.
Installation
npm install ngx-cfoAG Grid is bundled as a dependency — no separate ag-grid-community or ag-grid-angular install needed.
Components
<cfo-data-table>
A full-featured data table wrapping AG Grid with built-in search, column visibility/reordering panel, row selection panel, and automatic state persistence (column order, visibility, filters, sort, and quick-filter text are saved to localStorage/sessionStorage per tableId).
Required inputs
| Input | Type | Description |
|---|---|---|
| tableId | string | Unique ID used to persist table state across sessions |
| colDefs | ColDef<T>[] | AG Grid column definitions |
Optional inputs
| Input | Type | Default | Description |
|---|---|---|---|
| height | string | '500px' | Height of the table. Accepts any valid CSS value ('600px', '100%', 'calc(100vh - 200px)') |
| rowData | readonly T[] | [] | Row data (client-side row model) |
| rowModelConfig | RowModelConfig | { type: 'clientSide' } | Row model configuration — use { type: 'infinite', datasourceFactory } for server-side data |
| getRowId | GetRowIdFunc<T> | — | AG Grid getRowId function |
| loading | boolean | false | Shows a loading overlay over the grid |
| selection | boolean \| CfoSelectionConfig<T> | — | Enables row selection checkboxes. Pass true for defaults or a config object |
| quickFilter | boolean \| CfoQuickFilterConfig | — | Enables the search input. Pass true for defaults or { placeholder } |
| overlay | CfoOverlayConfig | — | Customises the no-rows/no-match overlay texts (all default to Danish) |
| sidebar | boolean \| CfoSidebarConfig<T> | — | Enables the slide-out sidebar. Pass true for the columns panel, or a config object |
| gridOptions | CfoGridOptions<T> | — | Passthrough for any AG Grid options. Merges with the component's own defaults — consumer values take precedence. Only columnDefs, rowData, rowModelType, rowSelection, theme, initialState, overlayComponentSelector, and getRowId are blocked |
Feature matrix
| Feature | How to enable |
|---|---|
| Kirby card + AG Grid theme | Always on |
| Danish no-rows/no-match overlays | Always on (texts customisable via [overlay]) |
| State persistence (columns, sort, filter, quick-filter) | Always on (keyed by tableId) |
| Opinionated defaultColDef | Always on (overridable via [gridOptions]) |
| Segmented control | [segment]="{ items: [...] }" |
| Search input | [quickFilter]="true" or [quickFilter]="{ placeholder: 'Search...' }" |
| Columns panel + "Visning" button | [sidebar]="true" or [sidebar]="{ columnsPanel: { title: 'Columns' } }" |
| Row selection checkboxes | [selection]="true" or [selection]="{ isRowSelectable: (r) => r.active }" |
| Selected rows panel | [sidebar]="{ selectedRowsPanel: { rowLabel: (r) => r.name } }" |
CfoSegmentConfig
| Property | Type | Description |
|---|---|---|
| items | SegmentItem[] | Segment items to display — each has id: string and text: string |
Example:
import type { CfoSegmentConfig, SegmentItem } from 'ngx-cfo';
segments: CfoSegmentConfig = {
items: [
{ id: 'active', text: 'Aktive' },
{ id: 'archived', text: 'Arkiverede' },
]
};
onSegmentChange(item: SegmentItem) {
this.rowData = item.id === 'active' ? this.activeRows : this.archivedRows;
}<cfo-data-table
[segment]="segments"
(segmentChange)="onSegmentChange($event)"
[rowData]="rowData"
...
/>CfoSelectionConfig<T>
| Property | Type | Description |
|---|---|---|
| isRowSelectable | (row: T) => boolean | Predicate to conditionally disable checkboxes |
| tooltipValueGetter | (params: ITooltipParams<T>) => string \| null | Tooltip on checkboxes — pair with isRowSelectable to explain why a row cannot be selected |
CfoSidebarConfig<T>
| Property | Type | Description |
|---|---|---|
| columnsPanel | CfoColumnsPanelConfig | Enables the column visibility/reorder panel (also shows the "Visning" toolbar button) |
| selectedRowsPanel | CfoSelectedRowsPanelConfig<T> | Enables the selected rows panel in the sidebar |
CfoSelectedRowsPanelConfig<T>
| Property | Type | Default | Description |
|---|---|---|---|
| rowLabel | (row: T) => string | required | Label function for rows shown in the panel |
| panelTitle | string | 'Valgte modparter' | Title of the selected rows panel |
| transferButtonText | string | 'Overfør til mit overblik' | Text of the transfer button |
Outputs
| Output | Type | Description |
|---|---|---|
| gridApiReady | GridApi<T> | Fires once when the AG Grid API is ready — use for imperative AG Grid calls (api.exportDataAsCsv(), etc.) |
| selectedRowsTransferred | readonly T[] | Fires when the transfer button is clicked, with the currently selected rows |
| quickFilterChanged | string | Fires on every quick-filter text change — use for server-side filtering with infinite row model |
| segmentChange | SegmentItem | Fires when the user selects a segment — use to swap rowData or colDefs |
Basic usage (client-side)
import { DataTableComponent } from 'ngx-cfo';
import type { ColDef } from 'ngx-cfo';
@Component({
imports: [DataTableComponent],
template: `
<cfo-data-table
tableId="my-table"
[colDefs]="colDefs"
[rowData]="rows"
/>
`
})
export class MyComponent {
rows = [{ name: 'Alice', amount: 100 }];
colDefs: ColDef[] = [{ field: 'name' }, { field: 'amount' }];
}With search, selection and sidebar
<cfo-data-table
tableId="my-table"
[colDefs]="colDefs"
[rowData]="rows"
[quickFilter]="true"
[selection]="{ isRowSelectable: canSelect }"
[sidebar]="{
columnsPanel: {},
selectedRowsPanel: { rowLabel: rowLabel }
}"
(selectedRowsTransferred)="onTransfer($event)"
/>gridOptions examples
// Enable pagination
[gridOptions]="{ pagination: true, paginationPageSize: 25 }"
// Override default column settings
[gridOptions]="{ defaultColDef: { sortable: false, minWidth: 100 } }"
// Enable master/detail
[gridOptions]="{ masterDetail: true, detailCellRendererParams: { ... } }"Infinite (server-side) row model
import { DataTableComponent } from 'ngx-cfo';
import type { InfiniteDatasourceFactory } from 'ngx-cfo';
datasourceFactory: InfiniteDatasourceFactory = (quickFilterText) => ({
getRows: (params) => {
myApi.fetchRows({ filter: quickFilterText, ...params }).then(({ rows, total }) => {
params.successCallback(rows, total);
});
}
});
rowModelConfig = { type: 'infinite' as const, datasourceFactory: this.datasourceFactory };<cfo-data-table
tableId="server-table"
[colDefs]="colDefs"
[rowModelConfig]="rowModelConfig"
[quickFilter]="true"
(quickFilterChanged)="onFilter($event)"
/>Height
AG Grid requires a defined height to render. Set it via the height input:
<!-- Fixed height (default: 500px) -->
<cfo-data-table height="600px" ... />
<!-- Fill remaining viewport -->
<cfo-data-table height="calc(100vh - 200px)" ... />
<!-- Inside a flex parent -->
<div style="display: flex; flex-direction: column; height: 100vh">
<header style="height: 60px">...</header>
<cfo-data-table style="flex: 1; min-height: 0" height="100%" ... />
</div><cfo-page-shell>
A page layout component with tab navigation.
| Input | Type | Default | Description |
|---|---|---|---|
| tabs | { label: string; path: string }[] | required | Tab items to display |
| title | string | — | Optional page title |
| selectedTabIndex | number | 0 | Initially selected tab index |
<cfo-page-shell title="My Page" [tabs]="[{ label: 'Overview', path: 'overview' }]">
<router-outlet />
</cfo-page-shell><cfo-slide-out-panel>
A slide-out side panel, animating in from the right.
| Input | Type | Default | Description |
|---|---|---|---|
| open | boolean | false | Whether the panel is open |
| width | string | '20rem' | CSS width of the panel |
<cfo-slide-out-panel [open]="isOpen">
<p>Panel content</p>
</cfo-slide-out-panel>Types
Component config types
import type {
CfoGridOptions,
CfoSelectionConfig,
CfoQuickFilterConfig,
CfoOverlayConfig,
CfoSidebarConfig,
CfoColumnsPanelConfig,
CfoSelectedRowsPanelConfig,
CfoRowMenuItem,
CfoRowMenuParams,
CfoMessagesFlagParams,
CfoMessage,
CfoAmountWithProgressParams,
CfoSegmentConfig,
SegmentItem,
} from 'ngx-cfo';AG Grid types
All commonly needed AG Grid types are re-exported from ngx-cfo — no need to import from ag-grid-community directly:
import type {
ColDef,
ColGroupDef,
GetRowIdFunc,
GridApi,
GridOptions,
GridState,
ICellRendererParams,
IDatasource,
IRowNode,
ITooltipParams,
RowSelectedEvent,
SelectionChangedEvent,
ValueFormatterParams,
ValueGetterParams,
} from 'ngx-cfo';<cfo-data-table>
A full-featured data table wrapping AG Grid with built-in search, column visibility/reordering panel, row selection panel, and automatic state persistence (column order, visibility, filters, sort, and quick-filter text are saved to localStorage/sessionStorage per tableId).
Required inputs
| Input | Type | Description |
|---|---|---|
| tableId | string | Unique ID used to persist table state across sessions |
| colDefs | ColDef<T>[] | AG Grid column definitions |
| selectedRowLabel | (row: T) => string | Label function for rows shown in the selected rows panel |
Optional inputs
| Input | Type | Default | Description |
|---|---|---|---|
| rowData | readonly T[] | [] | Row data (client-side row model) |
| rowModelConfig | RowModelConfig | { type: 'clientSide' } | Row model configuration — use { type: 'infinite', datasourceFactory } for server-side data |
| getRowId | GetRowIdFunc<T> | — | AG Grid getRowId function |
| loading | boolean | false | Shows a loading overlay over the grid |
| quickFilterPlaceholder | string | 'Søg' | Placeholder text for the search input |
| columnPanelTitle | string | 'Kolonner' | Title of the column visibility/reorder panel |
| selectedRowsPanelTitle | string | 'Valgte modparter' | Title of the selected rows panel |
| selectedRowsTransferButtonText | string | 'Overfør til mit overblik' | Text of the transfer button in the selected rows panel |
| noRowsOverlayTitle | string | 'Ingen data i tabellen' | Title shown when there are no rows |
| noRowsOverlaySubtitle | string | 'Der er ingen rækker at vise endnu.' | Subtitle shown when there are no rows |
| noMatchingRowsOverlayTitle | string | 'Ingen matchende rækker' | Title shown when no rows match the filter |
| noMatchingRowsOverlaySubtitle | string | 'Prøv at justere din søgning eller dine filtre.' | Subtitle shown when no rows match the filter |
| isRowSelectable | (row: T) => boolean | — | Predicate to conditionally disable row selection checkboxes |
| selectionTooltipValueGetter | (params: ITooltipParams<T>) => string \| null | — | Tooltip text for selection checkboxes — pair with isRowSelectable to explain why a row cannot be selected |
| gridOptions | Omit<GridOptions<T>, ...> | — | Passthrough for any AG Grid options not covered by the component's own inputs |
Outputs
| Output | Type | Description |
|---|---|---|
| gridApiReady | GridApi<T> | Fires once when the AG Grid API is ready |
| selectedRowsTransferred | readonly T[] | Fires when the transfer button is clicked, with the currently selected rows |
| quickFilterChanged | string | Fires on every quick-filter text change — use this for server-side filtering with infinite row model |
Basic usage (client-side)
import { DataTableComponent } from 'ngx-cfo';
import { ColDef } from 'ag-grid-community';
@Component({
imports: [DataTableComponent],
template: `
<cfo-data-table
tableId="my-table"
[colDefs]="colDefs"
[rowData]="rows"
[selectedRowLabel]="rowLabel"
/>
`
})
export class MyComponent {
rows = [{ name: 'Alice', amount: 100 }];
colDefs: ColDef[] = [{ field: 'name' }, { field: 'amount' }];
rowLabel = (row: typeof this.rows[0]) => row.name;
}Infinite (server-side) row model
import { DataTableComponent, InfiniteDatasourceFactory } from 'ngx-cfo';
datasourceFactory: InfiniteDatasourceFactory = (quickFilterText) => ({
getRows: (params) => {
myApi.fetchRows({ filter: quickFilterText, ...params }).then(({ rows, total }) => {
params.successCallback(rows, total);
});
}
});
rowModelConfig = { type: 'infinite' as const, datasourceFactory: this.datasourceFactory };<cfo-data-table
tableId="server-table"
[colDefs]="colDefs"
[rowModelConfig]="rowModelConfig"
[selectedRowLabel]="rowLabel"
(quickFilterChanged)="onFilter($event)"
/><cfo-page-shell>
A page layout component with tab navigation.
| Input | Type | Default | Description |
|---|---|---|---|
| tabs | { label: string; path: string }[] | required | Tab items to display |
| title | string | — | Optional page title |
| selectedTabIndex | number | 0 | Initially selected tab index |
<cfo-page-shell title="My Page" [tabs]="[{ label: 'Overview', path: 'overview' }]">
<router-outlet />
</cfo-page-shell><cfo-slide-out-panel>
A slide-out side panel, animating in from the right.
| Input | Type | Default | Description |
|---|---|---|---|
| open | boolean | false | Whether the panel is open |
| width | string | '20rem' | CSS width of the panel |
<cfo-slide-out-panel [open]="isOpen">
<p>Panel content</p>
</cfo-slide-out-panel>Cell renderers
These can be used directly in AG Grid colDef.cellRenderer.
ActionCellRendererComponent
Renders a Kirby icon button for row actions (e.g. opening a context menu).
import { ActionCellRendererComponent } from 'ngx-cfo';
colDefs: ColDef[] = [
{ headerName: '', cellRenderer: ActionCellRendererComponent, width: 56 }
];ProgressBarCellRendererComponent
Renders a color-coded progress bar (green < 80%, yellow 80–99%, red ≥ 100%). Expects a numeric cell value (0–100+). Shows a loading state for null/undefined and an error state for NaN.
import { ProgressBarCellRendererComponent } from 'ngx-cfo';
{ field: 'utilization', cellRenderer: ProgressBarCellRendererComponent }emptyTextRenderer
A function renderer that shows a greyed-out italic "Ingen data" placeholder for empty/null values.
import { emptyTextRenderer } from 'ngx-cfo';
{ field: 'note', cellRenderer: emptyTextRenderer }CfoRowMenuCellRendererComponent
Renders a "more" icon button (⋯) that opens a Kirby dropdown menu for per-row actions. Supports clickable actions, router-link navigation items, dividers, and labelled sections. Items can be conditionally disabled or hidden per row.
import { CfoRowMenuCellRendererComponent } from 'ngx-cfo';
import type { CfoRowMenuParams } from 'ngx-cfo';
colDefs: ColDef<Customer>[] = [
{ field: 'name' },
{
headerName: '',
colId: 'row-menu',
pinned: 'right',
lockPinned: true,
width: 56,
resizable: false,
sortable: false,
cellRenderer: CfoRowMenuCellRendererComponent,
cellRendererParams: {
items: (row) => [
{ label: 'Rediger', icon: 'edit', click: (r) => this.edit(r) },
{ type: 'link', label: 'Se detaljer', icon: 'list', routerLink: (r) => ['/customers', r.id] },
{ type: 'divider' },
{
type: 'section', label: 'Avanceret',
items: [
{
label: 'Slet', icon: 'trash', click: (r) => this.delete(r),
disabled: (r) => !r.canDelete,
description: 'Kræver admin-rettighed',
},
],
},
]
} satisfies CfoRowMenuParams<Customer>
}
];CfoRowMenuItem<TData> union
| Type | Required fields | Description |
|---|---|---|
| 'action' (default) | label, click | Clickable item that invokes a function |
| 'link' | label, routerLink | Angular router navigation |
| 'divider' | — | Visual separator |
| 'section' | label, items | Labelled group header followed by its items |
| 'info' | label | Non-interactive read-only item — use for metadata like "Edited by" |
All non-divider items support:
| Field | Type | Description |
|---|---|---|
| icon | string | Kirby icon name shown at the start |
| disabled | boolean \| ((row: T) => boolean) | Disables the item ('action' and 'link' only) |
| description | string \| ((row: T) => string) | Help text shown as a subtitle below the label |
| hidden | boolean \| ((row: T) => boolean) | Hides the item entirely |
Link items also accept:
| Field | Type | Description |
|---|---|---|
| queryParams | (row: T) => Record<string, unknown> | Optional query params factory |
Example with 'info':
{ type: 'divider' },
{ type: 'info', label: 'Sidst redigeret af', icon: 'person', description: (r) => r.editedBy }CfoMessagesFlagCellRendererComponent
Renders a text value with an optional kirby-flag badge showing the count of messages on the row. The flag color reflects the worst severity across all messages.
| Severity | Flag color | Notes |
|---|---|---|
| error | danger (red) | White text override applied automatically |
| warning | warning (yellow) | |
| info | semi-light (grey) | |
| success | success (green) | |
When there are no messages the flag is not rendered.
import { CfoMessagesFlagCellRendererComponent } from 'ngx-cfo';
import type { CfoMessagesFlagParams, CfoMessage } from 'ngx-cfo';
{
field: 'selskabNavn',
cellRenderer: CfoMessagesFlagCellRendererComponent,
cellRendererParams: {
messagesGetter: (row: MyRow) => row.messages,
} satisfies CfoMessagesFlagParams<MyRow>
}CfoMessagesFlagParams<T>
| Property | Type | Description |
|---|---|---|
| messagesGetter | (row: T) => CfoMessage[] | Returns the messages for a row |
CfoMessage
| Property | Type | Description |
|---|---|---|
| severity | 'success' \| 'info' \| 'warning' \| 'error' | Severity level — determines flag color |
| title | string | Message text |
CfoAmountWithProgressCellRendererComponent
Renders a formatted amount with an optional thin progress bar above it. Different from ProgressBarCellRendererComponent — the cell value is the amount (not a percentage), and the percentage comes from a separate field via percentageGetter.
┌──────────────────────┐
│ ████████░░░░░░░░░░ │ ← progress bar (hidden when percentageGetter returns undefined)
│ 1.234.567 kr. │ ← right-aligned, formatted by formatter()
└──────────────────────┘import { CfoAmountWithProgressCellRendererComponent } from 'ngx-cfo';
import type { CfoAmountWithProgressParams } from 'ngx-cfo';
{
field: 'restgaeld',
cellRenderer: CfoAmountWithProgressCellRendererComponent,
cellRendererParams: {
percentageGetter: (row: MyRow) => row.restgaeldPct,
formatter: (value) => value == null ? '' : `${value.toLocaleString('da-DK')} kr.`,
} satisfies CfoAmountWithProgressParams<MyRow>
}CfoAmountWithProgressParams<T>
| Property | Type | Description |
|---|---|---|
| percentageGetter | (row: T) => number \| undefined | Returns fill % (0–100). Return undefined to hide the bar entirely |
| formatter | (value: number \| null) => string | Formats the cell value into a display string |
Running end-to-end tests
For end-to-end (e2e) testing, run:
ng e2eAngular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
Additional Resources
For more information on using the Angular CLI, including detailed command references, visit the Angular CLI Overview and Command Reference page.
