Custom DataSource
A custom data source is an instance class that extends from the DataSource abstract class.
Last updated
A custom data source is an instance class that extends from the DataSource abstract class.
Last updated
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));
}
}
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.
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.
The REST API receives the filtering and sorting event data to process them behind the REST API.
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.
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:
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.
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.
<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));
}
}
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.