DkGrid
  • Overview
  • Getting Started
    • Quick Start Tutorial
  • Fundamentals
    • Grid Structure
      • Grid Size
      • Header
      • Row Filter
      • Rows
      • Panels
      • Gutters
      • Edition Toolbar
      • Paginator
      • Borders
      • Column Context Menu
      • Waiting Indicator
    • DataSource
      • In-Memory DataSource
      • Custom DataSource
    • Theming
  • Columns
    • Column Definitions
    • Column Grouping
    • Column Sizing
    • Column Moving
    • Column Pinning
  • Rows
    • Row Sorting
    • Row Selection
    • Row Grouping
    • Row Pagination
  • Filtering
    • Column Filters
    • Filter types
    • In-line Filters
  • Editing
    • Row Edition
    • Edition templates
    • Edition validation
    • Multiple rows edition
  • Reference
    • DkGrid API
      • DkGridColumnDef
      • DkGridSorting
      • DkGridSelection
      • DkGridRowGrouping
      • DkGridPagination
      • DkGridWaitingIndicator
      • DkGridFactoryDataSource
      • DkGridFilter
      • DkGridEdition
    • Components
      • DkGridComponent
      • DkGridColumnComponent
    • Classes
      • DataSource
      • Columns
      • Rows
      • Filtering
      • Editing
    • Interfaces
      • Columns
      • Sorting
      • Row Grouping
      • Filtering
      • Editing
    • Injection Tokens
      • Grid Structure
      • Filtering
      • Editing
      • Theming
    • Type aliases
      • DataSource
      • Columns
      • Selection
      • Filtering
      • Editing
    • Type Guards
Powered by GitBook
On this page
  • DataSource decorators
  • Summary
  • Complete code for this section
  1. Fundamentals
  2. DataSource

Custom DataSource

A custom data source is an instance class that extends from the DataSource abstract class.

PreviousIn-Memory DataSourceNextTheming

Last updated 3 years ago

We name Custom DataSource when you provide a data source that extends from the abstract class DataSource.

When the DikeGrid receives this type of data source, it connects to this DataSource and creates an instance of the type DikeGridDataSource, wrapping every data entry into a DikeGridDataRowEntry row.

Same as In-Memory DataSource, after wrapping operation, the DikeGrid assigns and emits a unique id. It also sets a unique id for every row.

See the following code snippet:

<dike-grid id="grid-custom-datasource" height="600px" #grid="dkgGrid"
    (dataDeliveryIdChange)="onDataDeliveryIdChange($event)">
</dike-grid>
@Component({
  selector: 'custom-data-source',
  templateUrl: './custom-data-source.component.html',
  styleUrls: ['./custom-data-source.component.scss'],

  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CustomDataSourceComponent implements OnInit, AfterViewInit, OnDestroy {
  // Retrieve the DikeGridComponent<T> instance from the view:
  @ViewChild('grid') dikeGrid: DikeGridComponent<Employee>;
 
  ngAfterViewInit(): void {
    // Once the DikeGrid intance is retrieved from the view, we create the DataSource instance:
    Promise.resolve().then(() => {
      this.dkgDataSource = new CustomDataSource(this.sampleData, this.dikeGrid.filter,
        this.dikeGrid.sorting, this.dikeGrid.pagination, this.dikeGrid.waitingIndicator);
    });
  }
  
  onDataDeliveryIdChange(id: string): void {
    console.log(`Data delivery id: ${id}`);
  } 
}
import { DataSource } from '@angular/cdk/collections';
import { PageEvent } from '@angular/material/paginator';

import { combineLatest, defer, merge, Observable, Subject, Subscription } from 'rxjs';
import { map, switchMap, tap } from 'rxjs/operators';

import { DikeColumnSortEvent, DikeFilterable, DikeGridFilter, DikeGridPagination,
    DikeGridSorting, DikeGridWaitingIndicator
} from '@dikesoft/angular-data-grid';

import { Employee } from 'app/mock-api/common/employees/data.model';
import { SampleData } from 'app/services/sample-data.service';

export class CustomDataSource extends DataSource<Employee> {

  private dataSetSubject: Subject<Employee[]>;
  private gridSubscription: Subscription = Subscription.EMPTY;

  constructor(
    private sampleData: SampleData,
    private gridFilter: DikeGridFilter<Employee>,
    private gridSorting: DikeGridSorting<Employee>,
    private gridPagination: DikeGridPagination,
    private gridIndicator: DikeGridWaitingIndicator) {

    super();

    this.dataSetSubject = new Subject<Employee[]>();
    this.setGridEventSubscription();
  }

  connect(): Observable<Employee[]> {
    return this.dataSetSubject.asObservable();
  }

  disconnect(): void {
    this.gridSubscription.unsubscribe();
  }

  private initialized(): Observable<void> {
    return defer(() => new Observable<void>((observer) => {
      observer.next();
      observer.complete();
    }));
  }

  private page(entries: Employee[]): Employee[] {
    // If paginator is not displayed, return the original entries:
    if (!this.gridPagination.matPaginator) {
      return entries;
    }

    const startIndex = this.gridPagination.matPaginator.pageIndex * this.gridPagination.matPaginator.pageSize;

    return entries.slice(startIndex, startIndex + this.gridPagination.matPaginator.pageSize);
  }

  private setGridEventSubscription(): void {
    // Prepare the filter observable:
    const gridFiltering$: Observable<DikeFilterable<Employee>[]> = merge(
      this.gridFilter.filterChange,
      this.gridFilter.clearFilterChange,
      this.initialized()

    ).pipe(
      map(() => {
        const validFilterables: DikeFilterable<Employee>[] = [ ];

        // Get all the valid filterables:
        this.gridFilter.filterables.forEach((filterable: DikeFilterable<Employee>) => {
          if (!!filterable.filter && filterable.filter.length > 0) {
            validFilterables.push({ ...filterable });
          }
        });

        return validFilterables;
      })
    );

    // Prepare the sorting observable:
    const gridSorting$: Observable<DikeColumnSortEvent<Employee>> = merge(this.gridSorting.sortChange, this.initialized()).pipe(
      map(() => this.gridSorting.getCurrentSortable())
    );

    /**
     * Prepare the pagination event:
     *
     * Since the paginator could be hidden at runtime, we listen the paginatorChange
     * observable.
     *
     * If the paginator is displayed, we listen to the pageChange observable. Otherwise,
     * we return a void observable.
     *
     */
    const gridPaging$: Observable<PageEvent | void> = merge(this.gridPagination.paginatorChange, this.initialized())
      .pipe(
        switchMap(paginator => !!paginator ? merge(paginator.page, this.initialized()) : this.initialized()
      )
    );

    // Execute the actual REST invocation:
    const gridServerData$: Observable<Employee[]> = combineLatest([gridFiltering$, gridSorting$]).pipe(
      tap(() => this.gridIndicator.loadingIndicator()),
      // Cancel the previous invocation and make a new one:
      switchMap(([filterables, sorting]) => this.sampleData.searchEmployees(filterables, sorting)),
    );

    // For the sake of brevity, pagination is done in the client side:
    const pagedData$ = combineLatest([gridServerData$, gridPaging$]).pipe(
      tap(([entries]) => {
        if (!!this.gridPagination.matPaginator && this.gridPagination.matPaginator.length !== entries.length) {
          this.gridPagination.matPaginator.length = entries.length;
          this.gridPagination.matPaginator.firstPage();
        }
      }),
      map(([entries]) => this.page(entries))
    );

    this.gridSubscription.unsubscribe();
    this.gridSubscription = pagedData$.pipe(
      // Once the data set arrives, we hide the loading indicator:
      tap(() => this.gridIndicator.hideWaitingIndicator())

    ).subscribe(entries => this.dataSetSubject.next(entries));
  }
}
import { HttpClient } from '@angular/common/http';

import { Observable } from 'rxjs';
import { delay } from 'rxjs/operators';

import { DikeFilterable, DikeColumnSortEvent } from '@dikesoft/angular-data-grid';
import { Employee } from 'app/mock-api/common/employees/data.model';

@Injectable({
  providedIn: 'root'
})
export class SampleData {

  constructor(
    private httpClient: HttpClient) { }

  getEmployees(sampleSize: number): Observable<Employee[]> {
    return this.httpClient.get<Employee[]>('api/common/employees', { params: { sampleSize } }).pipe(delay(350));
  }

  searchEmployees(filterables: DikeFilterable<Employee>[], sortedColumn: DikeColumnSortEvent<Employee>): Observable<Employee[]> {
    return this.httpClient.post<Employee[]>('api/common/employees', { filterables, sortedColumn }).pipe(delay(350));
  }
}
  1. After querying the DikeGrid instance from the component's view, we create a CustomDataSource object passing in all the dependencies to implement the logic for this data source.

  2. We obtain the data from a REST API. This API is a mock service that runs in memory. That is why we delay every invocation 350 milliseconds.

  3. The REST API receives the filtering and sorting event data to process them behind the REST API.

  4. For the sake of brevity, we execute the pagination operation in the client after receiving the response from the REST API.

Be aware that the DikeGrid will wrap every response from the REST API assigning new ids for every row and a unique id for the whole data set. The DikeGrid also resets the edition and selection operations. It means that the DikeGrid removes the history of changes and deselects rows.

DataSource decorators

Once you assign your custom data source to the DikeGrid instance, it connects to that data source.

See the following diagram:

The DikeGridDataSource wraps every data entry into a row of the type DikeGridDataRowEntry.

If you have allowed the edition operation, the DikeGrid creates the following decorators:

DikeRefreshingDataSource

This decorator always wraps a DikeGridDataSource instance. It pushes the rows when the user has updated them.

DikeEditionStateDataSource

It splits the set of rows depending on their status, Modified, Deleted, or Editing.

DikeEditionFilteringDataSource

This decorator manages all the filtering operations over the edited rows.

The DikeGrid creates the edition decorators only if you allow edition because the DikeGrid manages all the edition operations in memory.

You can not assign custom decorators when you provide a Custom DataSource.

Row grouping operation is not allowed when you provide a Custom DataSource.

Summary

You can provide a custom data source to the DikeGrid. For example, a custom data source could take remote data entries.

The DikeGrid manages the edition operation by adding the corresponding decorators.

Be aware that you can not group rows or assign custom decorators.

Complete code for this section

<div class="flex-none w-56 flex flex-col m-2 items-center">
    <button mat-raised-button
        class="flex-none w-56 my-2"
        color="primary"
        (click)="onClearFilter()">Clear Filter
    </button>
</div>

<dike-grid id="grid-custom-datasource" height="600px" #grid="dkgGrid"
    [displayRowId]="gridProperties.displayRowId"
    [gridElevation]="gridProperties.matElevation"
    [gridElevationValue]="gridProperties.elevationValue"
    [striped]="gridProperties.stripeRows"
    [verticalRowLines]="gridProperties.verticalRowLines"
    
    (dataDeliveryIdChange)="onDataDeliveryIdChange($event)"
    
    [allowSelection]="gridProperties.allowSelection"
    [allowRowFiltering]="gridProperties.allowRowFilter"
    [allowSorting]="gridProperties.allowSorting"
    [allowPagination]="gridProperties.allowPagination"
    [pageSize]="gridProperties.pageSize"
    
    (filterChange)="onFilterChange($event)"
    (editionFilterChange)="onEditionFilterChange($event)"
    
    (selectionChange)="onSelectionChange($event)"
    (editionRowChange)="onEditionRowChange($event)"
    (updateRowChange)="onUpdateRowChange($event)"
    (cancelRowEditionChange)="onCancelRowEditionChange($event)"
    (removeRowChange)="onRemoveRowChange($event)"
    (restoreRowChange)="onRestoreRowChange($event)"
    
    allowEdition
    [editionMode]="gridProperties.editionMode"
    
    [datasource]="dkgDataSource">

    <dike-grid-column
        fieldName="employeeId"
        headerText="Employee Id"
        dataType="Text"
        width="350"
        sortable>
    </dike-grid-column>

    <dike-grid-column
        fieldName="country"
        headerText="Country"
        dataType="Text"
        width="250"
        sortable>
    </dike-grid-column>
    
    <dike-grid-column
        fieldName="completeNameGroup"
        headerText="Complete Name">

        <dike-grid-column
            fieldName="firstName"
            headerText="Name"
            dataType="Text"
            width="150"
            sortable
            editable>
        </dike-grid-column>

        <dike-grid-column
            fieldName="lastName"
            headerText="Surname"
            dataType="Text"
            width="150"
            sortable
            editable>
        </dike-grid-column>

    </dike-grid-column>
    
    <dike-grid-column
        fieldName="gender"
        headerText="Gender"
        dataType="Binary"
        width="130"
        sortable>
    </dike-grid-column>

    <dike-grid-column
        fieldName="age"
        headerText="Age"
        dataType="Numeric"
        contentAlign="center"
        width="100"
        sortable
        editable>
    </dike-grid-column>
    
    <dike-grid-column
        fieldName="email"
        headerText="Email"
        dataType="Text"
        width="300"
        sortable
        editable>
    </dike-grid-column>

    <dike-grid-column
        fieldName="hireDate"
        headerText="Hire Date"
        dataType="Date"
        sortable
        editable>
    </dike-grid-column>

</dike-grid>
    
import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core';
import { Subscription } from 'rxjs';

import { DikeFilterable, DikeGridComponent, DikeGridDataRowEntry, DikeGridDataSourceInput } from '@dikesoft/angular-data-grid';
import { DikeGridProperties } from 'app/core/config/dike-grid.properties';
import { Employee } from 'app/mock-api/common/employees/data.model';
import { CustomDataSource } from 'app/modules/admin/datasource/custom-data-source/custom-datasource';

import { DikeGridConfig } from 'app/services/dike-grid.config.service';
import { SampleData } from 'app/services/sample-data.service';

@Component({
  selector: 'custom-data-source',
  templateUrl: './custom-data-source.component.html',
  styleUrls: ['./custom-data-source.component.scss'],

  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CustomDataSourceComponent implements OnInit, AfterViewInit, OnDestroy {
  // Retrieve the DikeGridComponent<T> instance from the view:
  @ViewChild('grid') dikeGrid: DikeGridComponent<Employee>;

  dkgDataSource: DikeGridDataSourceInput<Employee>;
  gridProperties: DikeGridProperties;

  private changeGridPropertiesSubscription: Subscription = Subscription.EMPTY;

  constructor(
    private cdr: ChangeDetectorRef,
    private gridConfig: DikeGridConfig,
    private sampleData: SampleData) { }

  ngOnInit(): void {
    // Listening to any config property change:
    this.setChangeGridPropertiesSubscription();
  }

  ngAfterViewInit(): void {
    // Once the DikeGrid intance is retrieved from the view, we create the DataSource instance:
    Promise.resolve().then(() => {
      this.dkgDataSource = new CustomDataSource(this.sampleData, this.dikeGrid.filter,
        this.dikeGrid.sorting, this.dikeGrid.pagination, this.dikeGrid.waitingIndicator);
    });
  }

  ngOnDestroy(): void {
    this.changeGridPropertiesSubscription.unsubscribe();
  }

  onDataDeliveryIdChange(id: string): void {
    console.log(`Data delivery id: ${id}`);
  }

  onFilterChange(filterable: DikeFilterable<Employee>): void {
    console.log('onFilterChange => ', filterable);
  }

  onEditionFilterChange(filterable: DikeFilterable<Employee>): void {
    console.log('onEditionFilterChange => ', filterable);
  }

  onSelectionChange(rows: DikeGridDataRowEntry<Employee>[]): void {
    console.log('Selection change: ', rows);
  }

  onEditionRowChange(row: DikeGridDataRowEntry<Employee>): void {
    console.log('Edition row change: ', row);
  }

  onUpdateRowChange(rows: DikeGridDataRowEntry<Employee> | DikeGridDataRowEntry<Employee>[]): void {
    console.log('Update row(s) change: ', rows);
  }

  onCancelRowEditionChange(rows: DikeGridDataRowEntry<Employee> | DikeGridDataRowEntry<Employee>[]): void {
    console.log('Cancel row(s) edition change: ', rows);
  }

  onRemoveRowChange(rows: DikeGridDataRowEntry<Employee> | DikeGridDataRowEntry<Employee>[]): void {
    console.log('Remove row(s) change: ', rows);
  }

  onRestoreRowChange(rows: DikeGridDataRowEntry<Employee> | DikeGridDataRowEntry<Employee>[]): void {
    console.log('Restore row(s) change: ', rows);
  }

  onClearFilter(): void {
    console.log('Clear Filter => ', this.dikeGrid.filter.clearFilter());
  }

  private setChangeGridPropertiesSubscription(): void {
    this.changeGridPropertiesSubscription.unsubscribe();
    this.changeGridPropertiesSubscription = this.gridConfig.configChange.subscribe((props: DikeGridProperties) => {
      this.gridProperties = props;
      this.cdr.markForCheck();
    });
  }
}
import { DataSource } from '@angular/cdk/collections';
import { PageEvent } from '@angular/material/paginator';

import { combineLatest, defer, merge, Observable, Subject, Subscription } from 'rxjs';
import { map, switchMap, tap } from 'rxjs/operators';

import { DikeColumnSortEvent, DikeFilterable, DikeGridFilter, DikeGridPagination,
    DikeGridSorting, DikeGridWaitingIndicator
} from '@dikesoft/angular-data-grid';

import { Employee } from 'app/mock-api/common/employees/data.model';
import { SampleData } from 'app/services/sample-data.service';

export class CustomDataSource extends DataSource<Employee> {

  private dataSetSubject: Subject<Employee[]>;
  private gridSubscription: Subscription = Subscription.EMPTY;

  constructor(
    private sampleData: SampleData,
    private gridFilter: DikeGridFilter<Employee>,
    private gridSorting: DikeGridSorting<Employee>,
    private gridPagination: DikeGridPagination,
    private gridIndicator: DikeGridWaitingIndicator) {

    super();

    this.dataSetSubject = new Subject<Employee[]>();
    this.setGridEventSubscription();
  }

  connect(): Observable<Employee[]> {
    return this.dataSetSubject.asObservable();
  }

  disconnect(): void {
    this.gridSubscription.unsubscribe();
  }

  private initialized(): Observable<void> {
    return defer(() => new Observable<void>((observer) => {
      observer.next();
      observer.complete();
    }));
  }

  private page(entries: Employee[]): Employee[] {
    // If paginator is not displayed, return the original entries:
    if (!this.gridPagination.matPaginator) {
      return entries;
    }

    const startIndex = this.gridPagination.matPaginator.pageIndex * this.gridPagination.matPaginator.pageSize;

    return entries.slice(startIndex, startIndex + this.gridPagination.matPaginator.pageSize);
  }

  private setGridEventSubscription(): void {
    // Prepare the filter observable:
    const gridFiltering$: Observable<DikeFilterable<Employee>[]> = merge(
      this.gridFilter.filterChange,
      this.gridFilter.clearFilterChange,
      this.initialized()

    ).pipe(
      map(() => {
        const validFilterables: DikeFilterable<Employee>[] = [ ];

        // Get all the valid filterables:
        this.gridFilter.filterables.forEach((filterable: DikeFilterable<Employee>) => {
          if (!!filterable.filter && filterable.filter.length > 0) {
            validFilterables.push({ ...filterable });
          }
        });

        return validFilterables;
      })
    );

    // Prepare the sorting observable:
    const gridSorting$: Observable<DikeColumnSortEvent<Employee>> = merge(this.gridSorting.sortChange, this.initialized()).pipe(
      map(() => this.gridSorting.getCurrentSortable())
    );

    /**
     * Prepare the pagination event:
     *
     * Since the paginator could be hidden at runtime, we listen the paginatorChange
     * observable.
     *
     * If the paginator is displayed, we listen to the pageChange observable. Otherwise,
     * we return a void observable.
     *
     */
    const gridPaging$: Observable<PageEvent | void> = merge(this.gridPagination.paginatorChange, this.initialized())
      .pipe(
        switchMap(paginator => !!paginator ? merge(paginator.page, this.initialized()) : this.initialized()
      )
    );

    // Execute the actual REST invocation:
    const gridServerData$: Observable<Employee[]> = combineLatest([gridFiltering$, gridSorting$]).pipe(
      tap(() => this.gridIndicator.loadingIndicator()),
      // Cancel the previous invocation and make a new one:
      switchMap(([filterables, sorting]) => this.sampleData.searchEmployees(filterables, sorting)),
    );

    // For the sake of brevity, pagination is done in the client side:
    const pagedData$ = combineLatest([gridServerData$, gridPaging$]).pipe(
      tap(([entries]) => {
        if (!!this.gridPagination.matPaginator && this.gridPagination.matPaginator.length !== entries.length) {
          this.gridPagination.matPaginator.length = entries.length;
          this.gridPagination.matPaginator.firstPage();
        }
      }),
      map(([entries]) => this.page(entries))
    );

    this.gridSubscription.unsubscribe();
    this.gridSubscription = pagedData$.pipe(
      // Once the data set arrives, we hide the loading indicator:
      tap(() => this.gridIndicator.hideWaitingIndicator())

    ).subscribe(entries => this.dataSetSubject.next(entries));
  }
}
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

import { Observable } from 'rxjs';
import { delay } from 'rxjs/operators';

import { DikeFilterable, DikeColumnSortEvent } from '@dikesoft/angular-data-grid';
import { Employee } from 'app/mock-api/common/employees/data.model';

@Injectable({
  providedIn: 'root'
})
export class SampleData {

  constructor(
    private httpClient: HttpClient) { }

  getEmployees(sampleSize: number): Observable<Employee[]> {
    return this.httpClient.get<Employee[]>('api/common/employees', { params: { sampleSize } }).pipe(delay(350));
  }

  searchEmployees(filterables: DikeFilterable<Employee>[], sortedColumn: DikeColumnSortEvent<Employee>): Observable<Employee[]> {
    return this.httpClient.post<Employee[]>('api/common/employees', { filterables, sortedColumn }).pipe(delay(350));
  }
}
Custom DataSource decorators