DevExtreme Angular - 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.

Load Data

The CustomStore configuration depends on whether data should be processed on the client or on the server.

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
index.js
$(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
app.component.html
app.component.ts
app.module.ts
<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
App.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
App.js
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;
NOTE
For performance reasons, we do not recommend that large datasets are processed on the client.

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
index.js
$(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
app.component.ts
app.component.html
app.module.ts
import { Component } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';

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) {
        const isNotEmpty = (value) => value !== undefined && value !== null && value !== '';

        this.customDataSource = new CustomStore({
            key: 'ID',
            load: (loadOptions) => {
                let params: HttpParams = new HttpParams();

                [
                    '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]);
                    }
                });

                return this.http.get('https://mydomain.com/MyDataService', { params: params })
                    .toPromise()
                    .then(response => {
                        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) => {
            //     return this.http.get('https://mydomain.com/MyDataService?id=' + key)
            //         .toPromise();
            // }
        });
    }
}
<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
App.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
App.js
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 isExpanded = true)
                                 // can be null if 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
index.js
$(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
app.component.ts
import { Component } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';

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',
            load: (loadOptions) => {
                // ...
            },
            insert: (values) = > {
                return this.http.post('https://mydomain.com/MyDataService', JSON.stringify(values))
                    .toPromise()
                    .catch(() => { throw 'Insertion failed' });
            },
            remove: (key) => {
                return this.http.delete('https://mydomain.com/MyDataService/' + encodeURIComponent(key))
                    .toPromise()
                    .catch(() => { throw 'Deletion failed' });
            },
            update: (key, values) => {
                return this.http.put('https://mydomain.com/MyDataService/' + encodeURIComponent(key), JSON.stringify(values))
                    .toPromise()
                    .catch(() => { throw 'Update failed' });
            }
        });
    }
}
Vue
App.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
App.js
// ...
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;