dgState

The dgState operator is an RxJS operator designed to manage Clarity Datagrid state transformations with intelligent debouncing and duplicate filtering. It assesses changes in datagrid filters and handles API trigger events efficiently. When filters are modified, the operator introduces a 500ms debounce before initiating the API call, preventing excessive requests during rapid typing. For actions such as sorting or pagination, the API call is made promptly without delay. The operator also provides an optional parameter to control the use of distinctUntilChanged. When enabled (default: true), it ensures that no API call is initiated if the datagrid state remains the same, preventing redundant requests.

Examples

Example 1: Basic Usage

Use dgState in your observable pipeline to manage datagrid state changes efficiently:

import {convertToHttpParams, dgState} from 'clr-lift';
import {Component, inject} from '@angular/core';
import {BehaviorSubject, combineLatest} from 'rxjs';
import {distinctUntilChanged, filter, map, share, switchMap} from 'rxjs/operators';
import {AsyncState, createAsyncState} from 'ngx-lift';
import {ClrDatagridStateInterface} from '@clr/angular';
import {isEqual} from 'ngx-lift';

@Component({})
export class UserDatagridComponent {
  selectedItems: User[] = [];
  userService = inject(UserService);

  private dgBS = new BehaviorSubject<ClrDatagridStateInterface | null>(null);
  // When the dgState parameter is set to false, it signals the execution of an API call even when the current state is identical to the previous state.
  // Conversely, emission is suppressed when dgState is true, thanks to the application of distinctUntilChanged.
  private dgState$ = this.dgBS.pipe(dgState(false));

  usersState$ = combineLatest([this.dgState$, this.userService.refresh$]).pipe(
    switchMap(([state]) => {
      const params = convertToHttpParams(state); // if convertToHttpParams doesn't fit your need, use your own utils to convert state
      return this.userService.getUsers(params).pipe(createAsyncState());
    }),
    share(),
  );

  total$ = this.usersState$.pipe(
    filter((state) => Boolean(state.data)),
    distinctUntilChanged<AsyncState<PaginationResponse<User>, HttpErrorResponse>>(isEqual),
    map((res) => res.data?.info?.total),
  );

  refresh(state: ClrDatagridStateInterface) {
    this.dgBS.next(state);
  }
}

Assume we have an API returning the following shape of data for a server-driven datagrid:

{
  "results": [
    {
      "gender": "male",
      "name": {
        "first": "Johan",
        "last": "Lemoine"
      },
      "email": "john@gmail.com"
    }
    // ... more users
  ],
  "info": {
    "pageSize": 10, // display 10 items per page
    "page": 2, // current page is 2
    "total": 100
  }
}

Example 2: Template Implementation with Type Safety

Use async pipe with @if for better type safety:

<!-- angular v17+ user-datagrid.component.html -->
@if ({usersState: usersState$ | async, total: total$ | async}; as vm) {
  <button class="btn btn-outline" (click)="userService.refreshList()" [disabled]="vm.usersState?.isLoading">
    Refresh
  </button>

  <clr-datagrid
    class="min-h-[200px]"
    (clrDgRefresh)="refresh($event)"
    [clrDgLoading]="vm.usersState?.isLoading === true"
    [clrDgSelectionType]="'single'"
    [(clrDgSelected)]="selectedItems"
  >
    <clr-dg-column [clrDgField]="'firstName'">First Name</clr-dg-column>
    <clr-dg-column [clrDgField]="'lastName'">Last Name</clr-dg-column>
    <clr-dg-column [clrDgField]="'email'">Email</clr-dg-column>
    <clr-dg-column [clrDgField]="'gender'">Gender</clr-dg-column>

    <clr-dg-placeholder>No data found</clr-dg-placeholder>

    @for (user of vm.usersState?.data?.results; track user.id.value) {
      <clr-dg-row [clrDgItem]="user">
        <clr-dg-cell>{{ user.name.first }}</clr-dg-cell>
        <clr-dg-cell>{{ user.name.last }}</clr-dg-cell>
        <clr-dg-cell>{{ user.email }}</clr-dg-cell>
        <clr-dg-cell>{{ user.gender }}</clr-dg-cell>
      </clr-dg-row>
    }

    <clr-dg-footer>
      @if (!vm.total) {
        No items
      } @else {
        {{ pagination.firstItem + 1 }} - {{ pagination.lastItem + 1 }} of {{ vm.total }} items
      }
      <clr-dg-pagination #pagination [clrDgPageSize]="10" [clrDgTotalItems]="vm.total || 0" />
    </clr-dg-footer>
  </clr-datagrid>

  @if (vm.usersState?.error; as error) {
    <cll-alert [error]="error" class="mb-4" />
  }
}

Example 3: Alternative Template Implementation

Use the async pipe directly in your template:

<div>
  <button class="btn btn-outline" (click)="userService.refreshList()" [disabled]="(usersState$ | async)?.isLoading">
    Refresh
  </button>
</div>

<clr-datagrid
  class="min-h-[200px]"
  (clrDgRefresh)="refresh($event)"
  [clrDgLoading]="(usersState$ | async)?.isLoading === true"
  [clrDgSelectionType]="'single'"
  [(clrDgSelected)]="selectedItems"
>
  <clr-dg-column [clrDgField]="'firstName'">First Name</clr-dg-column>
  <clr-dg-column [clrDgField]="'lastName'">Last Name</clr-dg-column>
  <clr-dg-column [clrDgField]="'email'">Email</clr-dg-column>
  <clr-dg-column [clrDgField]="'gender'">Gender</clr-dg-column>

  <clr-dg-placeholder>No data found</clr-dg-placeholder>

  @for (user of (usersState$ | async)?.data?.results; track user.id.value) {
    <clr-dg-row [clrDgItem]="user">
      <clr-dg-cell>{{ user.name.first }}</clr-dg-cell>
      <clr-dg-cell>{{ user.name.last }}</clr-dg-cell>
      <clr-dg-cell>{{ user.email }}</clr-dg-cell>
      <clr-dg-cell>{{ user.gender }}</clr-dg-cell>
    </clr-dg-row>
  }

  <clr-dg-footer>
    @if (total$ | async) {
      {{ pagination.firstItem + 1 }} - {{ pagination.lastItem + 1 }} of {{ total$ | async }} items
    } @else {
      No items
    }
    <clr-dg-pagination #pagination [clrDgPageSize]="10" [clrDgTotalItems]="(total$ | async) || 0" />
  </clr-dg-footer>
</clr-datagrid>

@if ((usersState$ | async)?.error; as error) {
  <cll-alert [error]="error" class="mb-4" />
}

Example 4: Live Example

Below is a working example of a server-driven datagrid using the dgState operator:

Single selection header
Use left or right key to resize the column
Use left or right key to resize the column
Use left or right key to resize the column
Use left or right key to resize the column
 / 10

API Reference

dgState

RxJS operator for handling Clarity Datagrid state transformations with debouncing and optional distinctUntilChanged behavior.

Signature

dgState(enableDistinctUntilChanged?: boolean): UnaryFunction<
  Observable<ClrDatagridStateInterface | null>,
  Observable<ClrDatagridStateInterface | null>
>

Parameters

  • enableDistinctUntilChanged?: boolean

    (Optional) Whether to enable distinctUntilChanged filtering. Defaults to true. When true, suppresses duplicate states. When false, allows API calls even for identical states.

Returns

A RxJS pipe function that transforms an observable of ClrDatagridStateInterface | null.

Behavior

  • Adds a 500ms debounce when filters change (to avoid excessive API calls during rapid typing)
  • Makes API calls promptly for sorting or pagination actions (no debounce)
  • Optionally filters out duplicate consecutive states when enableDistinctUntilChanged is true
  • Starts with a null state to handle initial load scenarios