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:

PropertyTypePresent whenDescription
offsetnumberAlwaysZero-based row offset. The index of the first row the grid needs. Use for SQL OFFSET or array slicing.
limitnumberAlwaysMaximum number of rows to return. Equals config.pageSize (default 50).
sortsSortConfig[]AlwaysActive sort configurations in priority order. Each entry is { key: string; dir: 'asc' | 'desc' }. Empty array means no sort.
filtersFilterStateWhen 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.
pagenumberscrollMode = '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:

PropertyTypeDescription
rowsT[]The rows for this page or batch. Length should be <= limit. Return an empty array to signal the end of data.
totalCountnumberTotal 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:

PropertyTypeDescription
offsetnumberStart index of the rows needed.
limitnumberNumber of rows requested.
sortsSortConfig[]Active sorts at the time of the fetch.
filtersFilterStateActive user filters (not yet merged with groupFilter).
pagenumber1-based page number (virtual-paging mode only).
groupFilterFilterState 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.