Data Source
The grid communicates with your backend through the GridDataSource<T> interface. Implement fetchRows() in any class or plain object and pass it to the [dataSource] input. The grid calls this method whenever it needs more data — on initial load, on sort/filter change, on page navigation, or when the user scrolls close to the bottom.
GridDataSource<T>
import { Observable } from 'rxjs';
import {
GridDataSource,
FetchParams,
FetchResult,
} from '@gridcraft/angular';
// Implement as a class, an injectable service, or a plain object literal
class MyDataSource implements GridDataSource<MyRow> {
fetchRows(params: FetchParams): Observable<FetchResult<MyRow>> {
// Return an Observable that emits exactly once with { rows, totalCount }
}
}fetchRows must return an Observable<FetchResult<T>>. The grid subscribes and unsubscribes automatically — there is no manual lifecycle to manage. If a new fetch is triggered before the previous one completes (e.g. user changes sort mid-load), the grid cancels the in-flight subscription.
FetchParams
The object passed to fetchRows() on every call:
| Property | Type | Present when | Description |
|---|---|---|---|
| offset | number | Always | Zero-based row offset. The index of the first row the grid needs. Use for SQL OFFSET or array slicing. |
| limit | number | Always | Maximum number of rows to return. Equals config.pageSize (default 50). |
| sorts | SortConfig[] | Always | Active sort configurations in priority order. Each entry is { key: string; dir: 'asc' | 'desc' }. Empty array means no sort. |
| filters | FilterState | When filterMode is set | Map of active column filters: Record<columnKey, { op: FilterOperator; value: any } | null>. Entries with a null value represent cleared filters — you can ignore them. When row grouping is active with inner-scroll, the group filter is merged here automatically. |
| page | number | scrollMode = 'virtual-paging' | 1-based page number. Only present in virtual-paging mode. Use alongside offset and limit to fill the correct sparse array slot. |
FetchResult<T>
The shape your fetchRows() Observable must emit:
| Property | Type | Description |
|---|---|---|
| rows | T[] | The rows for this page or batch. Length should be <= limit. Return an empty array to signal the end of data. |
| totalCount | number | Total number of rows matching the current sort/filter (before pagination). Used to calculate hasMore, page count, and the stats bar. |
Minimal implementation
import { of } from 'rxjs';
import { GridDataSource, FetchParams, FetchResult } from '@gridcraft/angular';
const ALL_ROWS: Employee[] = [/* ... */];
// Plain object — no class needed
const dataSource: GridDataSource<Employee> = {
fetchRows(params: FetchParams): Observable<FetchResult<Employee>> {
const { offset, limit } = params;
return of({
rows: ALL_ROWS.slice(offset, offset + limit),
totalCount: ALL_ROWS.length,
});
},
};HTTP implementation
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
import { GridDataSource, FetchParams, FetchResult } from '@gridcraft/angular';
interface Employee {
id: number;
firstName: string;
lastName: string;
department: string;
salary: number;
}
@Injectable({ providedIn: 'root' })
export class EmployeeDataSource implements GridDataSource<Employee> {
constructor(private http: HttpClient) {}
fetchRows(params: FetchParams): Observable<FetchResult<Employee>> {
const httpParams = new HttpParams()
.set('offset', params.offset)
.set('limit', params.limit)
.set('sorts', JSON.stringify(params.sorts))
.set('filters', JSON.stringify(params.filters ?? {}));
return this.http.get<FetchResult<Employee>>('/api/employees', { params: httpParams });
}
}Handling filters in fetchRows
The filters object is a flat map where each key is a GridColumn.key (or GridColumn.sortKey when set). Each value is a { op: FilterOperator; value: any } object, or null for a cleared filter. Map op to your backend's query syntax:
import { FetchParams, FilterState, FilterOperator } from '@gridcraft/angular';
function applyFilters<T>(rows: T[], filters: FilterState | undefined): T[] {
if (!filters) return rows;
return rows.filter(row =>
Object.entries(filters).every(([key, filter]) => {
if (!filter) return true; // null = cleared filter, skip
const value = (row as any)[key];
const { op, value: term } = filter;
switch (op as FilterOperator) {
case 'contains': return String(value).toLowerCase().includes(String(term).toLowerCase());
case 'equals': return value === term;
case 'startsWith': return String(value).toLowerCase().startsWith(String(term).toLowerCase());
case 'endsWith': return String(value).toLowerCase().endsWith(String(term).toLowerCase());
case 'notContains': return !String(value).toLowerCase().includes(String(term).toLowerCase());
case '=': return Number(value) === Number(term);
case '!=': return Number(value) !== Number(term);
case '>': return Number(value) > Number(term);
case '>=': return Number(value) >= Number(term);
case '<': return Number(value) < Number(term);
case '<=': return Number(value) <= Number(term);
case 'before': return new Date(value) < new Date(term);
case 'after': return new Date(value) > new Date(term);
default: return true;
}
})
);
}
// Usage in fetchRows:
fetchRows(params: FetchParams): Observable<FetchResult<Employee>> {
const filtered = applyFilters(ALL_ROWS, params.filters);
return of({
rows: filtered.slice(params.offset, params.offset + params.limit),
totalCount: filtered.length,
});
}Row grouping & inner-scroll
When config.grouping.groupBy is a string key, the grid uses per-group lazy loading in infinite scroll mode. When the user scrolls to the bottom of an open group, the grid automatically merges a group filter into params.filters before calling fetchRows(). Your data source does not need special handling — just apply params.filters as normal.
For example, when the user has opened the "Engineering" group and scrolls to the bottom, the grid calls fetchRows with:
// When the user opens the "Engineering" group and scrolls to the bottom,
// the grid calls fetchRows with params like:
{
offset: 20, // already have 20 Engineering rows loaded
limit: 50,
sorts: [{ key: 'lastName', dir: 'asc' }],
filters: {
// Any user-applied filters are merged here too:
salary: { op: '>=', value: 50000 },
// The group filter is merged in automatically:
department: { op: 'equals', value: 'Engineering' },
},
}
// Your fetchRows implementation handles this like any other filter —
// no special casing needed. The grid manages group state internally.Client-side sorting
By default (config.sortMode: 'server'), the grid forwards the active sort state via params.sorts and expects your backend to order the result. When you set sortMode: 'client', the grid sorts the loaded rows in memory — params.sorts is always [] so your fetchRows() implementation does not need to handle sorting at all. Client sort is best for in-memory datasets such as static arrays or editable grids.
Static data (no data source)
For small, fully-loaded datasets you don't need an HTTP service. Wrap your array in an Observable and pass it directly to the data source contract:
import { of } from 'rxjs';
import { GridDataSource } from '@gridcraft/angular';
const ROWS = [
{ id: 1, name: 'Alice', role: 'Engineer' },
{ id: 2, name: 'Bob', role: 'Designer' },
];
// Wrap a static array as a data source
const staticDs: GridDataSource = {
fetchRows: (p) => of({
rows: ROWS.slice(p.offset, p.offset + p.limit),
totalCount: ROWS.length,
}),
};
// Or skip [dataSource] entirely for very small datasets and manage rows manually
// by using the grid's [rows] input (static data mode — no fetch cycle). Pair this with config.sortMode: 'client' so clicking column headers sorts in memory without re-calling fetchRows().
FetchMoreEvent (advanced)
The grid's body component emits a FetchMoreEvent internally that the root GridcraftComponent handles for you. If you are extending or wrapping the grid components directly, this is the shape of that event:
| Property | Type | Description |
|---|---|---|
| offset | number | Start index of the rows needed. |
| limit | number | Number of rows requested. |
| sorts | SortConfig[] | Active sorts at the time of the fetch. |
| filters | FilterState | Active user filters (not yet merged with groupFilter). |
| page | number | 1-based page number (virtual-paging mode only). |
| groupFilter | FilterState | Set by the body when triggering an inner-scroll group fetch. The root component merges this into filters before calling dataSource.fetchRows() — your data source never sees it separately. |