DevExtreme jQuery/JS - Custom Data Sources
DevExtreme provides the CustomStore component to load and edit data from any data source unsupported out of the box. This article describes how to configure this component.
Client-Side Data Processing
To process data on the client, load all data from the server in the load function. You should also switch the CustomStore to the raw loadMode in all UI components except DataGrid, TreeList, PivotGrid, and Scheduler, in which this mode is already enabled:
jQuery
$(function() { $("#listContainer").dxList({ dataSource: new DevExpress.data.CustomStore({ key: "ID", loadMode: "raw", // omit in the DataGrid, TreeList, PivotGrid, and Scheduler load: function() { return $.getJSON("https://mydomain.com/MyDataService") .fail(function() { throw "Data loading error" }); } }) }); });
Angular
<dx-list [dataSource]="customDataSource"> </dx-list>
import { Component } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { lastValueFrom } from 'rxjs'; import CustomStore from 'devextreme/data/custom_store'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { customDataSource: CustomStore; constructor(private http: HttpClient) { this.customDataSource = new CustomStore({ key: 'ID', loadMode: 'raw', // omit in the DataGrid, TreeList, PivotGrid, and Scheduler load: () => { return lastValueFrom(this.http.get('https://mydomain.com/MyDataService')) .catch(() => { throw 'Data loading error' }); } }); } }
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { HttpClientModule } from '@angular/common/http'; import { AppComponent } from './app.component'; import { DxListModule } from 'devextreme-angular'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, HttpClientModule, DxListModule ], providers: [ ], bootstrap: [AppComponent] }) export class AppModule { }
Vue
<template> <DxList :data-source="customDataSource" /> </template> <script> import 'devextreme/dist/css/dx.light.css'; import DxList from 'devextreme-vue/list'; import 'whatwg-fetch'; function handleErrors(response) { if (!response.ok) { throw Error(response.statusText); } return response; } const customDataSource = new CustomStore({ key: 'ID', loadMode: 'raw', // omit in the DataGrid, TreeList, PivotGrid, and Scheduler load: () => { return fetch('https://mydomain.com/MyDataService') .then(handleErrors) .then(response => response.json()); .catch(() => { throw 'Network error' }); } }); export default { components: { DxList }, data() { return { customDataSource } } } </script>
React
import React from 'react'; import 'devextreme/dist/css/dx.light.css'; import List from 'devextreme-react/list'; import 'whatwg-fetch'; function handleErrors(response) { if (!response.ok) { throw Error(response.statusText); } return response; } const customDataSource = new CustomStore({ key: 'ID', loadMode: 'raw', // omit in the DataGrid, TreeList, PivotGrid, and Scheduler load: () => { return fetch('https://mydomain.com/MyDataService') .then(handleErrors) .then(response => response.json()); .catch(() => { throw 'Network error' }); } }); class App extends React.Component { render() { return ( <List dataSource={customDataSource} /> ); } } export default App;
Server-Side Data Processing
The communication between the CustomStore and the server is organized as follows:
- The CustomStore sends data processing settings to the server.
- The server processes data according to these settings and sends the processed dataset back.
Each setting carries information about data operation (sorting, filtering, etc.) and is present only if this operation is enabled or declared as remote. Configure remoteOperations in the DataGrid, TreeList, and PivotGridDataSource UI components or remoteFiltering in the Scheduler UI component to declare remote operations. For other UI components, use DataSource properties to enable operations.
The following example shows a CustomStore that sends data processing settings to the server. The settings are passed as the loadOptions parameter to the load function. loadOptions may contain empty settings if an operation is disabled or local. In this example, the forEach
loop filters out such settings:
jQuery
$(function() { var customDataSource = new DevExpress.data.CustomStore({ key: "ID", load: function(loadOptions) { var d = $.Deferred(); var params = {}; [ "filter", "group", "groupSummary", "parentIds", "requireGroupCount", "requireTotalCount", "searchExpr", "searchOperation", "searchValue", "select", "sort", "skip", "take", "totalSummary", "userData" ].forEach(function(i) { if(i in loadOptions && isNotEmpty(loadOptions[i])) { params[i] = JSON.stringify(loadOptions[i]); } }); $.getJSON("https://mydomain.com/MyDataService", params) .done(function(response) { d.resolve(response.data, { totalCount: response.totalCount, summary: response.summary, groupCount: response.groupCount }); }) .fail(function() { throw "Data loading error" }); return d.promise(); }, // Needed to process selected value(s) in the SelectBox, Lookup, Autocomplete, and DropDownBox // byKey: function(key) { // var d = new $.Deferred(); // $.get('https://mydomain.com/MyDataService?id=' + key) // .done(function(result) { // d.resolve(result); // }); // return d.promise(); // } }); $("#dataGridContainer").dxDataGrid({ dataSource: customDataSource, remoteOperations: { groupPaging: true } }); }); function isNotEmpty(value) { return value !== undefined && value !== null && value !== ""; }
Angular
import { Component } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { lastValueFrom } from 'rxjs'; import DataSource from 'devextreme/data/data_source'; import CustomStore from 'devextreme/data/custom_store'; import { LoadOptions } from 'devextreme/data'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'], providers: [HttpClient] }) export class AppComponent { customDataSource: DataSource; constructor(private http: HttpClient) { const isNotEmpty = (value: unknown) => value !== undefined && value !== null && value !== ''; this.customDataSource = new DataSource({ store: new CustomStore({ key: 'ID', load: (loadOptions: LoadOptions) => { let params: HttpParams = new HttpParams(); [ 'filter', 'group', 'groupSummary', 'parentIds', 'requireGroupCount', 'requireTotalCount', 'searchExpr', 'searchOperation', 'searchValue', 'select', 'sort', 'skip', 'take', 'totalSummary', 'userData', ].forEach(function (i) { const optionValue = loadOptions[i as keyof LoadOptions]; if (i in loadOptions && isNotEmpty(optionValue)) { params = params.set(i, JSON.stringify(optionValue)); } }); return lastValueFrom( this.http.get( 'https://mydomain.com/MyDataService', { params } ) ).then((response: any) => { return { data: response?.data, totalCount: response?.totalCount, summary: response?.summary, groupCount: response?.groupCount, }; }).catch(() => { throw 'Data loading error'; }); }, // Needed to process selected value(s) in the SelectBox, Lookup, Autocomplete, and DropDownBox // byKey: (key: number) => { // return lastValueFrom( // this.http.get(`$https://mydomain.com/MyDataService?id=${key}`) // ); // }, }), }); } }
<dx-data-grid [dataSource]="customDataSource"> <dxo-remote-operations [groupPaging]="true"> </dxo-remote-operations> </dx-data-grid>
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { HttpClientModule } from '@angular/common/http'; import { DxDataGridModule } from 'devextreme-angular'; @NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, HttpClientModule, DxDataGridModule ], providers: [ ], bootstrap: [AppComponent] }) export class AppModule { }
Vue
<template> <DxDataGrid :data-source="customDataSource"> <DxRemoteOperations :group-paging="true" /> </DxDataGrid> </template> <script> import 'devextreme/dist/css/dx.light.css'; import DxDataGrid, { DxRemoteOperations } from 'devextreme-vue/data-grid'; import CustomStore from 'devextreme/data/custom_store'; import 'whatwg-fetch'; const isNotEmpty = (value) => value !== undefined && value !== null && value !== ''; function handleErrors(response) { if (!response.ok) { throw Error(response.statusText); } return response; } const customDataSource = new CustomStore({ key: 'ID', load: (loadOptions) => { let params = '?'; [ 'filter', 'group', 'groupSummary', 'parentIds', 'requireGroupCount', 'requireTotalCount', 'searchExpr', 'searchOperation', 'searchValue', 'select', 'sort', 'skip', 'take', 'totalSummary', 'userData' ].forEach(function(i) { if(i in loadOptions && isNotEmpty(loadOptions[i])) { params += `${i}=${JSON.stringify(loadOptions[i])}&`; } }); params = params.slice(0, -1); return fetch(`https://mydomain.com/MyDataService${params}`) .then(handleErrors) .then(response => response.json()) .then(response => { return { data: response.data, totalCount: response.totalCount, summary: response.summary, groupCount: response.groupCount }; }) .catch(() => { throw 'Network error' }); }, // Needed to process selected value(s) in the SelectBox, Lookup, Autocomplete, and DropDownBox // byKey: (key) => { // return fetch(`https://mydomain.com/MyDataService?id=${key}`) // .then(handleErrors); // } }); export default { components: { DxDataGrid, DxRemoteOperations }, data() { return { customDataSource } } } </script>
React
import React from 'react'; import 'devextreme/dist/css/dx.light.css'; import DataGrid, { RemoteOperations } from 'devextreme-react/data-grid'; import CustomStore from 'devextreme/data/custom_store'; import 'whatwg-fetch'; const isNotEmpty = (value) => value !== undefined && value !== null && value !== ''; function handleErrors(response) { if (!response.ok) { throw Error(response.statusText); } return response; } const customDataSource = new CustomStore({ key: 'ID', load: (loadOptions) => { let params = '?'; [ 'filter', 'group', 'groupSummary', 'parentIds', 'requireGroupCount', 'requireTotalCount', 'searchExpr', 'searchOperation', 'searchValue', 'select', 'sort', 'skip', 'take', 'totalSummary', 'userData' ].forEach(function(i) { if(i in loadOptions && isNotEmpty(loadOptions[i])) { params += `${i}=${JSON.stringify(loadOptions[i])}&`; } }); params = params.slice(0, -1); return fetch(`https://mydomain.com/MyDataService${params}`) .then(handleErrors) .then(response => response.json()) .then(response => { return { data: response.data, totalCount: response.totalCount, summary: response.summary, groupCount: response.groupCount }; }) .catch(() => { throw 'Network error' }); }, // Needed to process selected value(s) in the SelectBox, Lookup, Autocomplete, and DropDownBox // byKey: (key) => { // return fetch(`https://mydomain.com/MyDataService?id=${key}`) // .then(handleErrors); // } }); class App extends React.Component { render() { return ( <DataGrid dataSource={customDataSource}> <RemoteOperations groupPaging={true}> </DataGrid> ); } } export default App;
When the server receives the data processing settings, it should apply them to the dataset and send back an object with the following structure:
{ data: [{ key: "Group 1", items: [ ... ], // subgroups or data objects if there are no further subgroups (check group.isExpanded = true) // is null if group.isExpanded = false count: 3, // count of items in this group; required only when items = null summary: [30, 20, 40] // group summary results }, ... ], totalCount: 200, // if requireTotalCount = true summary: [170, 20, 20, 1020], // total summary results groupCount: 35 // if requireGroupCount = true }
If the server did not receive the group setting, the structure should be different:
{ data: [ ... ], // data objects totalCount: 200, // if requireTotalCount = true summary: [170, 20, 20, 1020] // total summary results }
Edit Data
To implement data editing in the CustomStore, add the insert, remove, and update functions:
jQuery
$(function() { var customDataSource = new DevExpress.data.CustomStore({ key: "ID", load: function(loadOptions) { // ... }, insert: function(values) { var deferred = $.Deferred(); $.ajax({ url: "https://mydomain.com/MyDataService/", method: "POST", data: JSON.stringify(values) }) .done(deferred.resolve) .fail(function(e){ deferred.reject("Insertion failed"); }); return deferred.promise(); }, remove: function(key) { var deferred = $.Deferred(); $.ajax({ url: "https://mydomain.com/MyDataService/" + encodeURIComponent(key), method: "DELETE" }) .done(deferred.resolve) .fail(function(e){ deferred.reject("Deletion failed"); }; return deferred.promise(); }, update: function(key, values) { var deferred = $.Deferred(); $.ajax({ url: "https://mydomain.com/MyDataService/" + encodeURIComponent(key), method: "PUT", data: JSON.stringify(values) }) .done(deferred.resolve) .fail(function(e){ deferred.reject("Update failed"); }; return deferred.promise(); } }); // ... });
Angular
import { Component } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import CustomStore from 'devextreme/data/custom_store'; import { lastValueFrom } from 'rxjs'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'] }) export class AppComponent { customDataSource: CustomStore; constructor(private http: HttpClient) { this.customDataSource = new CustomStore({ key: 'ID', load: (loadOptions) => { // ... }, insert: (values) = > { return lastValueFrom(this.http.post('https://mydomain.com/MyDataService', JSON.stringify(values))) .catch(() => { throw 'Insertion failed' }); }, remove: (key) => { return lastValueFrom(this.http.delete('https://mydomain.com/MyDataService/' + encodeURIComponent(key))) .catch(() => { throw 'Deletion failed' }); }, update: (key, values) => { return lastValueFrom(this.http.put('https://mydomain.com/MyDataService/' + encodeURIComponent(key), JSON.stringify(values))) .catch(() => { throw 'Update failed' }); } }); } }
Vue
<template> <!-- ... --> </template> <script> // ... import CustomStore from 'devextreme/data/custom_store'; import 'whatwg-fetch'; function handleErrors(response) { if (!response.ok) { throw Error(response.statusText); } return response; } const customDataSource = new CustomStore({ key: 'ID', load: (loadOptions) => { // ... }, insert: (values) => { return fetch('https://mydomain.com/MyDataService', { method: 'POST', body: JSON.stringify(values), headers:{ 'Content-Type': 'application/json' } }) .then(handleErrors) .catch(() => { throw 'Network error' }); }, remove: (key) => { return fetch(`https://mydomain.com/MyDataService/${encodeURIComponent(key)}`, { method: 'DELETE' }) .then(handleErrors) .catch(() => { throw 'Network error' }); }, update: (key, values) => { return fetch(`https://mydomain.com/MyDataService/${encodeURIComponent(key)}`, { method: 'PUT', body: JSON.stringify(values), headers:{ 'Content-Type': 'application/json' } }) .then(handleErrors) .catch(() => { throw 'Network error' }); } }); export default { components: { // ... }, data() { return { customDataSource } } } </script>
React
// ... import CustomStore from 'devextreme/data/custom_store'; import 'whatwg-fetch'; function handleErrors(response) { if (!response.ok) { throw Error(response.statusText); } return response; } const customDataSource = new CustomStore({ key: 'ID', load: (loadOptions) => { // ... }, insert: (values) => { return fetch('https://mydomain.com/MyDataService', { method: 'POST', body: JSON.stringify(values), headers:{ 'Content-Type': 'application/json' } }) .then(handleErrors) .catch(() => { throw 'Network error' }); }, remove: (key) => { return fetch(`https://mydomain.com/MyDataService/${encodeURIComponent(key)}`, { method: 'DELETE' }) .then(handleErrors) .catch(() => { throw 'Network error' }); }, update: (key, values) => { return fetch(`https://mydomain.com/MyDataService/${encodeURIComponent(key)}`, { method: 'PUT', body: JSON.stringify(values), headers:{ 'Content-Type': 'application/json' } }) .then(handleErrors) .catch(() => { throw 'Network error' }); } }); class App extends React.Component { render() { return ( {/* ... */} ); } } export default App;
If you have technical questions, please create a support ticket in the DevExpress Support Center.