DevExtreme Angular - Data Source Examples

In this article, we consider some of the most common examples demonstrating how to connect to various sources of data using a DevExtreme data layer. Regardless of the data source type, approaches to data reading and editing are the same. They are described in the Data Layer article.

In-memory Data

The most simple data layer is one that deals with in-memory arrays. DevExtreme provides an implementation of the Store interface for this purpose (ArrayStore), as well as convenient shortcuts for creating a DataSource from arrays, and a Query tool for custom queries (see Query Concept).

In this case, the data lifetime equals the lifetime of the application, but no additional setup activity is required. An in-memory DataSource is great for easily understanding examples and for prototyping. It can also be used in a real application; for example when you obtain a medium-sized array from a web service and then process it on the client side.

Here is the general form of creating a DataSource from an array.

JavaScript
var dataSource = new DevExpress.data.DataSource({
    store: {
        type: "array",
        key: "id",
        data: [
            { id: 1, value: "Item 1" }
        ]
    }
});

This notation allows you to specify any configuration properties for both the ArrayStore and DataSource. However, more compact forms are available for read-only cases (see Creating DataSource).

Local Data

For working with HTML5 Web Storage (known as window.localStorage), the data layer provides the LocalStore. It functions exactly as the ArrayStore described in the previous section, and also ensures that the data is persisted in the browser's localStorage, immediately or at regular intervals.

To create this kind of a DataSource, use the following code.

JavaScript
var dataSource = new DevExpress.data.DataSource({
    store: {
        type: "local",
        name: "MyLocalData",
        key: "id"
    }
});

The name configuration property is required to scope the data and distinguish it from other localStorage contents. There are two LocalStore properties controlling how often the underlying array is persisted: flushInterval and immediate.

OData

OData is a universal open protocol for consuming data APIs. The DevExtreme data layer provides a special Store implementation to access OData web services (ODataStore).

Use ODataStore to access one OData entity specified by the URL, or the ODataContext object to communicate with an entire OData service.

Using ODataContext

This object represents a whole OData service, it creates a number of ODataStore instances inside, so you can access separate entities. In addition, the ODataContext includes get() and invoke() methods used to invoke service operations, and the objectLink() helper method to link entities.

To create an ODataContext instance, call the ODataContext constructor with the required configuration object.

JavaScript
var context = new DevExpress.data.ODataContext({
    url: "http://www.example.com/Northwind.svc",
    errorHandler: function(error) {
        alert(error.message);
    },
    entities: {
        Categories: { 
            key: "CategoryID", 
            keyType: "Int32" 
        },
        MyCustomers: { 
            name: "Customers",
            key: "CustomerID", 
            keyType: "String" 
        }
    }
});

In the example above, ODataContext is created to access two entities (Categories and Customers) from the service located at the http://www.example.com/Northwind.svc URL.

Each sub-object of the entities configuration object defines a name and configuration settings for an ODataStore within this context instance. Two ODataStore objects are impicitly created: context.Categories to access http://www.example.com/Northwind.svc/Categories and context.MyCustomers accessing http://www.example.com/Northwind.svc/Customers. Note how in the second case we specified different names for the store and the entity by using additional name parameter.

Now, you can create a DataSources to load data.

JavaScript
var categoriesSource = new DevExpress.data.DataSource(context.Categories);

The following example illustrates how create the same DataSource providing the additional configuration.

JavaScript
var categoriesSource = new DevExpress.data.DataSource({
    store: context.Categories,
    pageSize: 5,
    sort: "CategoryName"
});    

To perform data modification, use Store objects directly:

JavaScript
context.Categories
    .update(1, { CategoryName: "Beverages" })
    .done(doneCallback)
    .fail(failCallback);

View on GitHub

Key Types

When specifying keys in the ODataStore configuration, and in the entities properties for the ODataContext, specify the appropriate key types as well. The following key types are supported out of the box: String, Int32, Int64, and Guid.

In most cases, the key expression is a single property.

JavaScript
var store = new DevExpress.data.ODataStore({
    url: "/url/to/service",
    key: "CategoryID",
    keyType: "Int32"
});

For compound keys consisting of multiple properties, the following syntax is used.

JavaScript
var store = new DevExpress.data.ODataStore({
    url: "/url/to/service",
    key: [ "OrderID", "ProductID" ],
    keyType: {
        OrderID: "Int32",
        ProductID: "Int32"
    } 
});

If you need to use a key type that is not supported by default, use the odata.keyConverters utility object to register your own key type.

JavaScript
DevExpress.data.utils.odata.keyConverters["MyType"] = function(value) { 
    return value + "MT"; //returns an URL component for 'value'
};

Edm Literals

OData defines some primitive data types which cannot be represented in JavaScript, for example Int64. To work with such values, use the EdmLiteral class. For the information on primitive data types, refer to the OData documentation.

JavaScript
dataSource.filter("Distance", "<", new DevExpress.data.EdmLiteral("100000L"));

The code snippet above shows a filter expression involving the Int64 Distance property.

The next example shows how to load an entity with the Int64 key.

JavaScript
store.byKey(new DevExpress.data.EdmLiteral("123L")).done(doneCallback);

GUIDs

GUID (Globally Unique Identifier) is another common data type you may encounter while working with OData services. The DevExtreme data layer includes the Guid class, which enables you to generate new GUIDs and work with existing GUIDs.

To create a Guid instance, call the Guid constructor. If you pass a string value specifying a GUID to the constructor, the created Guid instance will hold the specified value.

JavaScript
var guid = new DevExpress.data.Guid("bd330029-8106-6d2d-5371-f27325155e99");

If you call the constructor without arguments, a new GUID will be generated.

JavaScript
var guid = new DevExpress.data.Guid();

Associations

Consider the following ODataContext.

JavaScript
var context = new DevExpress.data.ODataContext({
    url: "http://www.example.com/Northwind.svc",
    entities: {
        Categories: { 
            key: "CategoryID", 
            keyType: "Int32" 
        },
        Products: { 
            key: "ProductID", 
            keyType: "Int32" 
        }
    }
});

Assume that each Product entity is connected to a Category via the Product.Category navigation property.

Navigation properties are usually deferred and are not loaded automatically together with the owning entity. To load both entities at once, use the expand load properties extension, specific for ODataStore.

JavaScript
var productSource = new DevExpress.data.DataSource({
    store: context.Products,
    expand: [ "Category" ]
});

The expand property is also supported by the byKey method.

JavaScript
context.Products.byKey(1, { expand: [ "Category" ] });

Another task is updating a navigation property or inserting a new entity with a navigation property, in other words creating links between entities. To accomplish this, use the objectLink(entityAlias, key) method of the ODataContext.

In the following example, the Category property of the Product entity with the key 1 is changed to the Category with the key 2.

JavaScript
context.Products.update(1, {
    Category: context.objectLink("Categories", 2)
});

Invoking Service Operations

In addition to entites, OData services may expose service operations. The ODataContext class supports this capability. For the information on service operations, refer to the OData documentation.

To invoke an operation which does not return any value, use the invoke() method.

JavaScript
context.invoke("MyAction", { param: "value" });

To invoke an operation and get its return value, use the get() method.

JavaScript
context.get("GetSomeValue", { param: "value" });

One interesting case is a service operation which supports querying on top of it. In this case, the operation may be treated as a read-only entity, and input parameters can be passed to the customQueryParams extension of the DataSource load properties.

JavaScript
var context = new DevExpress.data.ODataContext({
    entities: {
        "GetSomeValue": { 
        }
    }
});

new DevExpress.data.DataSource({
    store: context.GetSomeValue,

    // operation parameters
    customQueryParams: {
        operationParam: "value"
    }
});

A 1-Click Solution for CRUD Web API Services with Role-based Access Control via EF Core & XPO

If you target .NET for your backend API, be sure to check out Web API Service and register your free copy today. The Solution Wizard scaffolds an OData v4 Web API Service (.NET 6+) with integrated authorization & CRUD operations powered by EF Core and our XPO ORM library. You can use OAuth2, JWT or custom authentication strategies alongside tools like Postman or Swagger (OpenAPI) for API testing.

The built-in Web API Service also filters out secured server data based on permissions granted to users. Advanced/enterprise functions include audit trail, endpoints to download reports, file attachments, check validation, obtain localized captions, etc.

To use the free Solution Wizard (which creates the Web API Service) run the Universal Component Installer from the DevExpress Download Manager.

Custom Sources

Custom data access logic can be implemented using the CustomStore class. A developer should implement all data access operations in the CustomStore.

JavaScript
var myStore = new DevExpress.data.CustomStore({
    load: function(loadOptions) {
        // . . .
    },
    byKey: function(key, extra) {
        // . . .
    },
    update: function(values) {
        // . . .
    },
    . . .  
});

var dataSource = new DevExpress.data.DataSource({
    store: myStore
});

The DataSource supports a more brief syntax without introducing an explicit CustomStore instance.

JavaScript
var dataSource = new DevExpress.data.DataSource({
    load: function(loadOptions) {
        // . . .
    },
    byKey: function(key, extra) {
        // . . .
    },
    update: function(values) {
        // . . .
    },
    . . .
});

For example, the following synthetic implementation generates an infinite read-only list:

JavaScript
var infiniteListSource = new DevExpress.data.DataSource({
    load: function(loadOptions) {
        var result = [ ];
        for(var i = 0; i < loadOptions.take; i++)
            result.push({ id: 1 + loadOptions.skip + i });            
        return result;
    },
    byKey: function(key) {
        return { id: key };
    }
});

In this example, load and byKey functions are synchronous, that is why they instantly return a result.

NOTE
DevExtreme also provides store implementations for the BreezeJS and JayData data libraries. You can use them as a reference if you decide to introduce a custom store implementation for another data library.

Connect to RESTful Service

Assume that you have a web service published at a certain URL, for example http://www.example.com/service/entity1. This web service implements CRUD operations on data (Create, Read, Update, Delete) and follows the HTTP request conventions listed below.

  • GET http://www.example.com/service/entity1 request returns a list of all entities
  • GET http://www.example.com/service/entity1/123 request returns a single entity identified by the 123 key
  • POST http://www.example.com/service/entity1 adds a new entity built from the values passed in HTTP request body
  • PUT http://www.example.com/service/entity1/123 updates an entity identified by the 123 key with the values passed in HTTP request body
  • DELETE http://www.example.com/service/entity1/123 deletes an entity identified by the 123 key

Such services can have their own URL conventions and additional query-string parameters, they can use different HTTP methods, and different implementation of HTTP request body handlers. That is why the DevExtreme data layer does not provide a ready-to-use component to communicate with these services. However, the CustomStore class enables you to easily utilize any service.

For the service type described above, you can apply the following simple custom DataSource implementation.

jQuery
index.js
$(function() {
    var customDataSource = new DevExpress.data.CustomStore({
        load: (loadOptions) => {
            return $.getJSON(SERVICE_URL);
        },

        byKey: (key) => {
            return $.getJSON(SERVICE_URL + "/" + encodeURIComponent(key));
        },

        insert: (values) => {
            return $.ajax({
                url: SERVICE_URL,
                method: "POST",
                data: values
            });
        },

        update: (key, values) => {
            return $.ajax({
                url: SERVICE_URL + "/" + encodeURIComponent(key),
                method: "PUT",
                data: values
            });
        },

        remove: (key) => {
            return $.ajax({
                url: SERVICE_URL + "/" + encodeURIComponent(key),
                method: "DELETE",
            });
        },
    });
});

Note that all user functions return the result of the jQuery AJAX call, which is compatible with the jQuery.Deferred promise. In fact, you may use any promise-compatible object to connect to any asynchronous data storage; for example - to an HTML5 File API, and not necessarily to HTTP endpoints.

Angular
app.component.ts
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({
            load: (loadOptions) => {
                return lastValueFrom(httpClient.get(SERVICE_URL));
            },

            byKey: (key) => {
                return lastValueFrom(httpClient.get(SERVICE_URL + "/" + encodeURIComponent(key)));
            },

            insert: (values) => {
                return lastValueFrom(httpClient.post(SERVICE_URL, values));
            },

            update: (key, values) => {
                return lastValueFrom(httpClient.put(SERVICE_URL + encodeURIComponent(key), values));
            },

            remove: (key) => {
                return lastValueFrom(httpClient.delete(SERVICE_URL + encodeURIComponent(key)));
            },
        });
    }
}
Vue
App.vue (Options API)
App.vue (Composition API)
<template>
    <!-- ... -->
</template>

<script>
// ...
import CustomStore from 'devextreme/data/custom_store';
import 'whatwg-fetch';

const customDataSource = new CustomStore({

    load: (loadOptions) => {
        return fetch(SERVICE_URL);
    },

    byKey: (key) => {
        return fetch(SERVICE_URL + "/" + encodeURIComponent(key));
    },

    insert: (values) => {
        return fetch(SERVICE_URL, {
            method: "POST",
            body: JSON.stringify(values),
            headers: {
                'Content-Type': 'application/json'
            }
        });
    },

    update: (key, values) => {
        return fetch(SERVICE_URL + "/" + encodeURIComponent(key), {
            method: "PUT",
            body: JSON.stringify(values),
            headers: {
                'Content-Type': 'application/json'
            }
        });
    },

    remove: (key) => {
        return fetch(SERVICE_URL + "/" + encodeURIComponent(key), {
            method: "DELETE",
        });
    },
});

export default {
    components: {
        // ...
    },
    data() {
        return {
            customDataSource
        }
    }
}
</script>
<template>
    <!-- ... -->
</template>

<script setup>
// ...
import CustomStore from 'devextreme/data/custom_store';
import 'whatwg-fetch';

const customDataSource = new CustomStore({

    load: (loadOptions) => {
        return fetch(SERVICE_URL);
    },

    byKey: (key) => {
        return fetch(SERVICE_URL + "/" + encodeURIComponent(key));
    },

    insert: (values) => {
        return fetch(SERVICE_URL, {
            method: "POST",
            body: JSON.stringify(values),
            headers: {
                'Content-Type': 'application/json'
            }
        });
    },

    update: (key, values) => {
        return fetch(SERVICE_URL + "/" + encodeURIComponent(key), {
            method: "PUT",
            body: JSON.stringify(values),
            headers: {
                'Content-Type': 'application/json'
            }
        });
    },

    remove: (key) => {
        return fetch(SERVICE_URL + "/" + encodeURIComponent(key), {
            method: "DELETE",
        });
    },
});
</script>
React
App.js
// ...
import CustomStore from 'devextreme/data/custom_store';
import 'whatwg-fetch';

const customDataSource = new CustomStore({
    load: (loadOptions) => {
        return fetch(SERVICE_URL);
    },

    byKey: (key) => {
        return fetch(SERVICE_URL + "/" + encodeURIComponent(key));
    },

    insert: (values) => {
        return fetch(SERVICE_URL, {
            method: "POST",
            body: JSON.stringify(values),
            headers: {
                'Content-Type': 'application/json'
            }
        });
    },

    update: (key, values) => {
        return fetch(SERVICE_URL + "/" + encodeURIComponent(key), {
            method: "PUT",
            body: JSON.stringify(values),
            headers: {
                'Content-Type': 'application/json'
            }
        });
    },

    remove: (key) => {
        return fetch(SERVICE_URL + "/" + encodeURIComponent(key), {
            method: "DELETE",
        });
    },
});

export default function App() {
    return (
        {/* ... */}
    );
}

The load function accepts a number of loadOptions (sorting, filtering, paging, etc.). Send them to a remote storage where you can generate the resulting dataset based on these properties.

Note that certain UI components have peculiarities in the CustomStore implementation. For example, in case of the DataGrid, the load function should also return the total count of received records.

See Also

Load Data in Raw Mode

Loading data in raw mode allows you to configure the CustomStore more easily. You can use it only if all data shaping operations are supposed to be performed on the client. In raw mode, the load function should get raw, unprocessed data from the server, and the CustomStore will perform data shaping automatically, without any input from you. To switch to the raw mode, assign "raw" to the loadMode property.

jQuery
index.js
$(function() {
    var customDataSource = new DevExpress.data.CustomStore({
        loadMode: "raw", // omit in the DataGrid, TreeList, PivotGrid, and Scheduler
        load: function() {
            // ...
        },    
        // ...
    });
});
Angular
app.component.ts
import { Component } from '@angular/core';
import { HttpClient } 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({
            loadMode: "raw", // omit in the DataGrid, TreeList, PivotGrid, and Scheduler
            load: () => {
                // ...
            },
            // ...
        });
    }
}
Vue
App.vue (Options API)
App.vue (Composition API)
<template>
    <!-- ... -->
</template>

<script>
// ...
import CustomStore from 'devextreme/data/custom_store';

const customDataSource = new CustomStore({
    loadMode: "raw", // omit in the DataGrid, TreeList, PivotGrid, and Scheduler 
    load: () => {
        // ...
    },
});

export default {
    components: {
        // ...
    },
    data() {
        return {
            customDataSource
        }
    }
}
</script>
<template>
    <!-- ... -->
</template>

<script setup>
// ...
import CustomStore from 'devextreme/data/custom_store';

const customDataSource = new CustomStore({
    loadMode: "raw", // omit in the DataGrid, TreeList, PivotGrid, and Scheduler
    load: () => {
        // ...
    },
});
</script>
React
App.js
// ...
import CustomStore from 'devextreme/data/custom_store';

export default function App() {
    const customDataSource = new CustomStore({
        loadMode: "raw", // omit in the DataGrid, TreeList, PivotGrid, and Scheduler
        load: () => {
            // ...
        },
        // ...
    });

    return (
        {/* ... */}
    );
}

Note that you are not required to implement the byKey and totalCount functions in raw mode, since they will be evaluated based on the results of the load function. If, however, you do implement them, your implementation will take precedence over the default one.

Once loaded, data is stored in the cache. If you need to clear the cache at some point, call the clearRawDataCache() method.

jQuery
index.js
$(function() {
    var customDataSource = new DevExpress.data.CustomStore({  
        // ...
    });

    customDataSource.clearRawDataCache();
});
Angular
app.component.ts
import { Component } from '@angular/core';
import { HttpClient } 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({
            // ...
        });

        this.customDataSource.clearRawDataCache();
    }
}
Vue
App.vue (Options API)
App.vue (Composition API)
<template>
    <!-- ... -->
</template>

<script>
// ...
import CustomStore from 'devextreme/data/custom_store';

const customDataSource = new CustomStore({
    // ...
});

export default {
    components: {
        // ...
    },
    data() {
        return {
            customDataSource
        }
    },
    mounted() {
        customDataSource.clearRawDataCache(); 
    },
}
</script>
<template>
    <!-- ... -->
</template>

<script setup>
// ...
import CustomStore from 'devextreme/data/custom_store';

const customDataSource = new CustomStore({
    // ...
});

customDataSource.clearRawDataCache(); 
</script>
React
App.js
// ...
import CustomStore from 'devextreme/data/custom_store';

export default function App() {
    const customDataSource = new CustomStore({
        // ...
    });

    customDataSource.clearRawDataCache(); 

    return (
        {/* ... */}
    );
}

To switch data caching off, assign false to the cacheRawData property. Note that in this case, the CustomStore will reload all data on every call of the load, byKey and totalCount functions.

jQuery
index.js
$(function() {
    var customDataSource = new DevExpress.data.CustomStore({  
        // ...
        cacheRawData: false,
    });
});
Angular
app.component.ts
import { Component } from '@angular/core';
import { HttpClient } 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({
            // ...
            cacheRawData: false,
        });
    }
}
Vue
App.vue (Options API)
App.vue (Composition API)
<template>
    <!-- ... -->
</template>

<script>
// ...
import CustomStore from 'devextreme/data/custom_store';

const customDataSource = new CustomStore({
    // ...
    cacheRawData: false,
});

export default {
    components: {
        // ...
    },
    data() {
        return {
            customDataSource
        }
    },
}
</script>
<template>
    <!-- ... -->
</template>

<script setup>
// ...
import CustomStore from 'devextreme/data/custom_store';

const customDataSource = new CustomStore({
    // ...
    cacheRawData: false,
}); 
</script>
React
App.js
// ...
import CustomStore from 'devextreme/data/custom_store';

export default function App() {
    const customDataSource = new CustomStore({
        // ...
        cacheRawData: false,
    });

    return (
        {/* ... */}
    );
}

Since the CustomStore loads all data in raw mode at once, we do not recommend using it with large amounts of data. If you notice a decrease in the CustomStore performance in raw mode, consider delegating some or all data shaping operations to the server and implementing the remaining operations in the load function yourself.

Note On Same-Origin Policy

One common pitfall that occurs during communication with remote web services from JavaScript is the Same-Origin Policy. It is a security restriction enforced by web browsers that do not directly allow HTTP communication between different domains (not even between endpoints located at two different ports of the same website).

To consume a web service from JavaScript, the web service has to support the Cross-Origin Resource Sharing feature, also known as CORS.

For read-only access, instead of CORS, a web service may support the JSONP (JSON with padding) technique. Built-in DevExtreme, Data Store classes support JSONP. For example, to connect to an OData service with JSONP support, use the jsonp configuration property.

JavaScript
var store = new DevExpress.data.ODataStore({
    url: "http://www.example.com",
    jsonp: true
});