DevExtreme jQuery - Data Layer

Overview

DevExtreme includes a data layer, which is a set of complementary components that enable you to read and write data. The core data layer objects are DataSource and Stores.

The DataSource is intended to simplify working with data, whether it be an in-memory array, a local browser storage, remote service, or any custom storage. The DataSource is a stateful object that keeps sorting, grouping, filtering, and keeps data transformation properties and applies them each time data is loaded. It also provides events intended to handle changing data and the state.

The DataSource underlying data access logic is isolated in a Store. Unlike the DataSource, a Store is a stateless object implementing a universal interface for reading and modifying data. Each Store contains the same set of methods. For more information about Stores refer to the What Are Stores section of this article.

The following picture illustrates how the DataSource and Stores interact.

Data Layer

Asynchrony

jQuery

All Data transferring operations in the data layer are asynchronous, whether it be an access to a remote service or to local data. It is made to support the universal Store interface, regardless of the source of data you access. These operations return a jQuery.Promise. Use this Promise to specify callbacks for successful operation completion and for operation failure.

JavaScript
dataSource.load()
    .done(function(result) {
        //success callback
    })
    .fail(function(error) {
        //error callback
    });
Angular

All Data transferring operations in the data layer are asynchronous, whether it be an access to a remote service or to local data. It is made to support the universal Store interface, regardless of the source of data you access. These operations return a native Promise. Use this Promise to specify callbacks for successful operation completion and for operation failure.

JavaScript
dataSource.load()
    .done(function(result) {
        //success callback
    })
    .fail(function(error) {
        //error callback
    });
Vue

All Data transferring operations in the data layer are asynchronous, whether it be an access to a remote service or to local data. It is made to support the universal Store interface, regardless of the source of data you access. These operations return a native Promise. Use this Promise to specify callbacks for successful operation completion and for operation failure.

JavaScript
dataSource.load()
    .done(function(result) {
        //success callback
    })
    .fail(function(error) {
        //error callback
    });
React

All Data transferring operations in the data layer are asynchronous, whether it be an access to a remote service or to local data. It is made to support the universal Store interface, regardless of the source of data you access. These operations return a native Promise. Use this Promise to specify callbacks for successful operation completion and for operation failure.

JavaScript
dataSource.load()
    .done(function(result) {
        //success callback
    })
    .fail(function(error) {
        //error callback
    });

Creating DataSource

To create a DataSource instance, call the DataSource constructor and pass the required configuration object to this constructor. This section covers the ways to associate the required data with the DataSource instance being created. You can use one of these properties to create a DataSource instance.

From Array

Pass the required array to the DataSource constructor.

JavaScript
var dataSource = new DevExpress.data.DataSource(array);

If you need to specify other DataSource configuration properties, use another form.

JavaScript
var dataSource = new DevExpress.data.DataSource({
    // array of data
    store: array,

    // additional configuration properties
    sort: "name",
    pageSize: 10
});

In this case, the array is passed to the store property of the configuration object.

From url

If you pass a string to the DataSource constructor, the DataSource will access the data returned by the AJAX request to the URL specified by this string.

JavaScript
var dataSource = new DevExpress.data.DataSource("http://www.example.com/dataservices/jsondata");

You may also use a JSONP callback parameter.

JavaScript
var dataSource = new DevExpress.data.DataSource("http://www.example.com/dataservices/jsonpdata?callback=?");
See Also

From Store

Pass a Store instance to the DataSource constructor.

JavaScript
var store = new DevExpress.data.ArrayStore(data);
var dataSource = new DevExpress.data.DataSource(store);

If you need to specify other DataSource properties, use the following code.

JavaScript
var store = new DevExpress.data.ArrayStore(array);
var dataSource = new DevExpress.data.DataSource({
    sort: "name",
    pageSize: 10,
    store: store
});

In this example, the Store instance is passed to the store configuration property.

A Store can be specified implicitly. Two possibilities are described above (From Array and From Url). In addition, you can use the following.

  • Add the properties implementing the required data access logic to the DataSource configuration object. In this case, the CustomStore will be automatically created within the DataSource. The properties should correspond to the CustomStore configuration properties.

    JavaScript
    var dataSource = new DevExpress.data.DataSource({
        //DataSource configuration
        sort: "name",
        pageSize: 10,
        //data access logic
        load: function(loadOptions) {
            return array;
        },
        byKey: function(key) {
            return array[key];
        },
        ...
    });
  • Pass a Store configuration object. Besides the required Store configuration properties, this object should contain the type property that specifies which Store object will be created within the DataSource.

    JavaScript
    var dataSource = new DevExpress.data.DataSource({
        store: {
            type: "array",
            data: array
        }
    });

    The following Store types are available.

What Are Stores

A store is a universal data access component within the DevExtreme data layer. It includes the following methods that are required for reading and editing data:

  • load(options) - Loads data. This function accepts an object specifying sorting, grouping, filtering, and data transformation properties.
  • insert(values) - Adds a new item with the passed values.
  • remove(key) - Removes the data item the key specifies.
  • update(key, values) - Sets the new values to the data item the key specifies.
  • byKey(key, extraOptions) - Loads the data item the key specifies. The extraOptions argument requires additional implementation properties to get the required item.
  • totalCount(options) - Enables you to get the total count of items that satisfy the specified conditions without loading them. The options object can contain filter and group fields that specify filtering and grouping properties.

DevExtreme includes the following Store implementations for the most common data access scenarios:

  • ArrayStore - provides access to an in-memory array
  • LocalStore - provides access to HTML5 web storage
  • ODataStore - provides access to a remote OData service

You can implement custom data access logic as described in the Custom Sources topic if these stores are not suitable.

Reading Data

Consider data reading possibilities of the data layer on an example of an in-memory array.

JavaScript
var dataSource = new DevExpress.data.DataSource([
    { name: "item1" },
    { name: "item2" },
    { name: "item3" }
]);

The information given in this section is applicable to any data source. We use in-memory data for the sake of simplicity.

To load data, call the dataSource.load() method.

JavaScript
dataSource.load()
    .done(function(result) {
        // 'result' contains the array associated with the DataSource
    })
    .fail(function(error) {
        // handle error
    });

The DataSource allows you to specify initial data shaping properties (sort, filter, etc.).

JavaScript
var dataSource = new DevExpress.data.DataSource({
    store: [
        { name: "Charlie", value: 10 },
        { name: "Alice", value: 20 },
        { name: "Bob", value: 30 }
    ],
    filter: [ "value", ">", 15 ],
    sort: { field: "name", desc: true }
});

In the ArrayStore, LocalStore and ODataStore data shaping operations are applied in the following order: Filtering -> Sorting -> Selection -> Grouping -> Paging -> Item mapping -> Post processing.

As for the CustomStore, data shaping properties are passed to the load(options) method, which gives you an opportunity to process them in any order you need.

Paging

By default, the DataSource loads items by pages. Page size is determined by the pageSize configuration property of the DataSource.

JavaScript
var dataSource = new DevExpress.data.DataSource({
    pageSize: 30,
    . . .
});

The current page index can be accessed and modified via the pageIndex() and pageIndex(newIndex) methods respectively.

JavaScript
dataSource.pageIndex(1); // Switch to the next page
dataSource.load();

To disable paging, assign false to the paginate configuration property of the DataSource.

JavaScript
var dataSource = new DevExpress.data.DataSource({
    paginate: false,
    . . .
});

Sorting

DataSources and Stores use sort expressions to specify sorting conditions and direction.

For an example, declare a Person class.

JavaScript
var Person = function(firstName, lastName, city) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.address = {
        city: city
    };
};
Person.prototype.fullName = function() {
    return this.firstName + " " + this.lastName;
};

After the Person class is created, declare a data array holding several instances of this class.

JavaScript
var data = [
    new Person("John", "Smith", "San Francisco"),
    new Person("John", "Doe", "New York"),
    new Person("Xavier", "Gomez", "Denver"),
    new Person("Xavier", "Lee", "New Mexico")
];

Create a simple DataSource.

JavaScript
var dataSource = new DevExpress.data.DataSource(data);

The sorting condition can be specified using the sort configuration property or the sort(sortExpr) method of the DataSource. The value assigned to the property or passed to the method is a sorting expression, which can have one of the following formats.

  • A single expression

    JavaScript
    dataSource.sort("lastName");
    dataSource.load().done(function(result) {
        // 'result' contains the 'data' array items sorted by 'lastName'
    });

    To sort data in descending order, pass an object containing the getter (field or selector - they are equivalents) and desc properties to the sort method.

    JavaScript
    dataSource.sort({ getter: "lastName", desc: true });
    dataSource.load().done(function(result) {
        // 'result' contains the 'data' array items sorted by 'lastName' in descending order
    });

    Pass a function to the method if you need to sort by custom values.

    JavaScript
    dataSource.sort(function(e) {
        if(e.Position == "CEO") 
            return "!";
        else
            return e.Position;
    });
    dataSource.load().done(function(result) {
        // 'result' contains the 'data' array where CEO's are displayed at the top
        // other positions are sorted in ascending order
    });
  • Several expressions

    JavaScript
    dataSource.sort(
        "fullName", 
        { getter: "address.city", desc: true }
    );
    dataSource.load().done(function(result) {
        // 'result' contains the 'data' array items sorted by 'fullName' and then by 'address.city' in the descending order
    });

    You can pass an array of expressions instead of using several arguments.

    JavaScript
    dataSource.sort([
        "firstName",
        "lastName",
        { getter: "address.city", desc: true }
    ]);
    dataSource.load().done(function(result) {
        // 'result' contains the 'data' array items sorted by 'firstName', then by 'lastName',
        // and then by 'address.city' in the descending order
    });

Stores support the same sort expression syntax as the DataSource.

JavaScript
var arrayStore = new DevExpress.data.ArrayStore(data);
arrayStore
    .load({
        sort: [
            { getter: "firstName", desc: true },
            "lastName"
        ]
    })
    .done(function (result) {
        // 'result' contains the 'data' array items sorted by 'firstName' in the descending order and then by 'lastName'
    });

Filtering

To consider the filter expression syntax, begin by creating a sample DataSource.

NOTE
Filtering works when inputting a plain data structure only. However, if you need the filtering capability and hierarchical data, transform the plain data using the DataSource's group property.
JavaScript
var dataSource = new DevExpress.data.DataSource([
    { name: "First item", value: 5 },
    { name: "Second item", value: 7 },
    { name: "Last item", value: 3 }
]);

Filtering conditions are specified as a filter expression, which is a set of filter expressions separated by group operators. The filtering conditions can be defined via the filter configuration property or the filter(filterExpr) method of the DataSource.

Binary Filter Operations

To specify a binary filter operation, use an array containing the following items.

  1. The getter by which data items are filtered.
  2. The comparison operator. The available operators are: "=", "<>", ">", ">=", "<", "<=", "startswith", "endswith", "contains", "notcontains".
  3. The value to which the getter value is compared.

To specify filtering conditions, call the filter(filterExpr) method of the DataSource. If a filter expression consists of a single binary operation, you can either pass an array of the items described above, or you can pass those items as separate arguments.

JavaScript
dataSource.filter("value", ">", 3);
dataSource.load().done(function(result) {
    //'result' contains the "First item" and "Second item" items
});
JavaScript
dataSource.filter("name", "contains", "st");
dataSource.load().done(function(result) {
    //'result' contains the "First item" and "Last item" items
});

The second item of the filter expression array is optional. You can specify only the getter and the value. The default operator is "=".

JavaScript
dataSource.filter("value", "=", 3);

is equal to

JavaScript
dataSource.filter("value", 3);

Unary Filter Operation

The unary not operation allows you to filter out data matching some conditions.

To specify an unary filter operation, use an array containing the following items.

  1. The "!" operator.
  2. The array with matching conditions.

To specify filtering conditions, call the filter(filterExpr) method of the DataSource. Note that filter expression should be included into square brackets.

JavaScript
dataSource.filter( ["!", ["value", "=", 3]] );
dataSource.load().done(function(result) {
    //'result' contains the "First item" and "Second item" items
});
JavaScript
dataSource.filter( ["!", ["name", "contains", "st"]] );
dataSource.load().done(function(result) {
    //'result' contains the "Second item"
});

Group Filter Operations

To combine several binary operations, use group operators "and" or "or".

JavaScript
dataSource.filter([
    [ "value", ">", 3 ],
    "and",
    [ "value", "<", 7 ]
]);
dataSource.load().done(function(result) {
    //'result' contains the "First item" item
});
JavaScript
dataSource.filter([
    [ "value", "<", 4 ],
    "or",
    [ "value", ">", 6 ]
]);
dataSource.load().done(function(result) {
    //'result' contains the "Second item" and "Last item" items
});

If neighboring filter expressions are not separated by a group operator, the "and" operator is implied.

JavaScript
dataSource.filter([
    [ "value", ">", 3 ],
    "and",
    [ "value", "<", 7 ]
]);

is equal to

JavaScript
dataSource.filter([
    [ "value", ">", 3 ],
    [ "value", "<", 7 ]
]);

The operator priority depends on the implementation of the underlying Store, or you can define the operator priority enclosing the required expression into square brackets.

JavaScript
dataSource.filter([
    ["name", "notcontains", "Second"],
    "and", //calculated second
    [
        [ "value", "<", 4 ],
        "or", //calculated first
        [ "value", ">", 6 ]
    ]
]);

Custom Filter Conditions

A filter expression can also be a function that takes on an item object and returns a Boolean value that determines whether an item satisfies the filtering condition or not.

JavaScript
dataSource.filter(function(itemData) {
    return itemData.value > 3 && itemData.name != "Second item";
});
dataSource.load().done(function(result) {
    //'result' contains the "First item" item
});

Stores support the same filter expression syntax as the DataSource.

JavaScript
var arrayStore = new DevExpress.data.ArrayStore(data);
arrayStore
    .load({
        filter: [
            [ "value", "<", 5 ],
            "or",
            [ "value", ">", 5 ]
        ]
    })
    .done(function (result) {
        // 'result' contains the "Second item" and "Last item" items
    });

Search Api

NOTE
Searching works when inputting a plain data structure only. However, if you need the searching capability and hierarchical data, transform the plain data using the DataSource's group property.

In addition to filtering capabilities, the DataSource provides the search API. It is a convenient way to augment filtering by text search. The search API consists of three properties: searchExpr, searchOperation, and searchValue.

JavaScript
var dataSource = new DevExpress.data.DataSource({
    store: [
        { firstName: "John", lastName: "Doe", birthYear: 1970 },
        . . .
    ],
    filter: [ "birthYear" , "<", 2000 ],
    searchExpr: function(dataItem) {
        return dataItem.firstName + " " + dataItem.lastName;
    }
});

dataSource.searchValue("doe");
dataSource.load().done(function(result) {
    //'result' contains items whose birthYear is less then 2000 and firstName or lastName contain "doe"
});

The default search operation is "contains". It can be changed by providing the DataSource's searchOperation configuration property. You can also modify the search API properties via the appropriate methods: searchExpr(expr), searchOperation(op), and searchValue(value).

Data Transformation

The DataSource includes several ways to customize the resulting array structure, which allows you to modify each item as well as process the entire array.

Select Expressions

The DataSource supports the select property intended to specify the structure of an item of the array being loaded. The DataSource passes the value of this property to the underlying Store when loading data. Therefore, this data transformation can be performed on the server side if the Store supports it.

To consider a select expression syntax, begin by creating an array of sample data.

JavaScript
var data = [
    {
        firstName: "John",
        lastName: "Smith",
        city: "San Francisco"
    },
    {
        firstName: "Xavier",
        lastName: "Lee",
        city: "New York"
    },
    {
        firstName: "Maria",
        lastName: "Gomez",
        city: "Denver"
    }
];

You can define the select property value via the configuration object passed to the DataSource constructor.

JavaScript
var dataSource = new DevExpress.data.DataSource({
    store: data,
    select: "lastName"
});

To modify the select property, pass the new property value to the select(expr) method of the DataSource.

JavaScript
dataSource.select("firstName", "city");
dataSource.load();

A selection expression can be a getter, several getters, or a function.

  • A single getter

    JavaScript
    dataSource.select("lastName");
    dataSource.load().done(function(result) {
        //'result' contains the resulting array
    });

    The resulting array looks like the following.

    [
        { lastName: "Smith" },
        { lastName: "Lee" },
        { lastName: "Gomez" },
    ]
  • Several getters

    JavaScript
    dataSource.select("firstName", "lastName");
    dataSource.load().done(function(result) {
        //'result' contains the resulting array
    });

    The resulting array looks like the following.

    [
        { firstName: "John", lastName: "Smith" },
        { firstName: "Xavier", lastName: "Lee" },
        { firstName: "Maria", lastName: "Gomez" },
    ]

    You can pass an array of getters instead of using several arguments.

    JavaScript
    dataSource.select(["firstName", "lastName"]);
  • A function

    A select expression can also be a function that takes on an item object and returns the transformed object.

    JavaScript
    dataSource.select(function(dataItem) {
        return {
            fullName: dataItem.firstName + " " + dataItem.lastName,
            address: dataItem.city
        };
    });
    dataSource.load().done(function(result) {
        //'result' contains the resulting array
    });

    The resulting array looks like the following.

    [
        { fullName: "John Smith", address: "San Francisco" }
        { fullName: "Xavier Lee", address: "New York" }
        { fullName: "Maria Gomez", address: "Denver" }
    ]

Stores support the same select expression syntax as the DataSource.

JavaScript
var arrayStore = new DevExpress.data.ArrayStore(data);
arrayStore
    .load({
        select: [ "firstName", "lastName" ]
    })
    .done(function (result) {
        //'result' contains the resulting array
    });

Additional data transformation can be performed with the map and postProcess configuration properties of the DataSource.

Item Mapping

The map configuration property of the DataSource enables you to modify each item of the loaded array. A function passed to this property takes an initial item as a parameter and returns the processed item. This function is performed within the DataSource and is not passed to the underlying Store.

Begin with the creation of a sample array.

JavaScript
var data = [
    {
        firstName: "John",
        lastName: "Smith",
        city: "San Francisco"
    },
    {
        firstName: "Xavier",
        lastName: "Lee",
        city: "New York"
    },
    {
        firstName: "Maria",
        lastName: "Gomez",
        city: "Denver"
    }
];

Assign the required function to the map configuration property of the DataSource.

JavaScript
var dataSource = new DevExpress.data.DataSource({
    store: data,
    map: function(itemData) {
        return {
            fullName: itemData.firstName + " " + itemData.lastName,
            address: {
                city: itemData.city
            }
        }
    }
});

The dataSource in the example above will convert the initial data items into the following ones.

[
    {
        fullName: "John Smith",
        address: {
            city: "San Francisco"
        }
    },
    {
        fullName: "Xavier Lee",
        address: {
            city: "New York"
        }
    },
    {
        fullName: "Maria Gomez",
        address: {
            city: "Denver"
        }
    }
]

Post Processing

You may often need to process loaded data in a way, which does not include simple sorting, grouping, filtering or data item transformation. For instance, you may need to group items by a date they were created (Today, This week, Long time ago). The postProcess configuration property of the DataSource will help you perform this task. Assign a function that implements the required post processing algorithm to this property. The function takes on the loaded array as a parameter and returns the processed array.

The following example illustrates how to perform the task mentioned above using the postProcess property.

var data = [
    { 
        subject: "First message", 
        message: "This message has been recieved at 12/09/2013", 
        date: new Date(2013, 08, 11) 
    },
    { 
        subject: "Today message", 
        message: "This message has been recieved today",
        date: new Date() 
    },
    ...
];

Create a DataSource instance and assign the required function to the postProcess configuration property.

JavaScript
function groupByDate(data) {
    var MS_PER_DAY = 24 * 60 * 60 * 1000,
        result = [],
        todayItems = [],
        thisWeekItems = [],
        otherItems = [];

    // Fill intervals
    $.each(data, function() {
        var daysAgo = ($.now() - this.date.getTime()) / MS_PER_DAY;

        if(daysAgo < 1)
            todayItems.push(this);                
        else if(daysAgo < 7)
            thisWeekItems.push(this);                
        else
            otherItems.push(this);                
    });

    // Construct final result
    if(todayItems.length)
        result.push({ key: "Today", items: todayItems });

    if(thisWeekItems.length)
        result.push({ key: "This week", items: thisWeekItems });

    if(otherItems.length)
        result.push({ key: "Long time ago", items: otherItems });

    return result;
}

var dataSource = new DevExpress.data.DataSource({
    store: data,
    postProcess: groupByDate
});

The DataSource in the example above groups the items by the time period. The resulting array will look like the following.

[
    {
        key: "Today",
        items: [
            //today items
        ]
    },
    {
        key: "This week",
        items: [
            //this week items
        ]
    },
    {
        key: "Long time ago",
        items: [
            //items elder then a week
        ]
    }
]
NOTE
The function passed to the postProcess property is performed within the DataSource and is not passed to the underlying Store.

Grouping

In some cases, you may need to group data by certain criteria. An item of a grouped array is an object containing two fields:

  • key - a group key;
  • items - an array of items belonging to the group.
JavaScript
var groupedArray = [
    {
        key: "A",
        items: [ "Amelia", "Andrew"]
    }        
    {
        key: "B",
        items: [ "Betty", "Benjamin"]
    }
];

The DevExtreme data layer (DataSource and Stores) supports grouping. The group expression syntax is identical to the sort expression syntax.

Consider grouping on the following examples. Begin with the creation of a sample DataSource.

JavaScript
var data = [
    { name: "Amelia", birthYear: 1991, gender: "female" },
    { name: "Benjamin", birthYear: 1983, gender: "male" },
    { name: "Andrew", birthYear: 1991, gender: "male" },
    { name: "Daniela", birthYear: 1983, gender: "female" },
    { name: "Lee", birthYear: 1983, gender: "male" },
    { name: "Betty", birthYear: 1983, gender: "female" }
];
var dataSource = new DevExpress.data.DataSource(data);

The group expression is stored in the group property of the DataSource. You can get and modify the property value via the group() and group(groupExpr) methods respectively. To group the given array by 'birthYear', call the group(groupExpr) method and pass "birthYear" as an argument.

JavaScript
dataSource.group("birthYear");
dataSource.load().done(function(result) {
    //'result' contains the loaded array
});

The return value of the function that processes the loaded data contains the following array.

[
    {
        key: 1983,
        items: [
            { name: "Benjamin", birthYear: 1983, gender: "male" },
            { name: "Daniela", birthYear: 1983, gender: "female" },
            { name: "Lee", birthYear: 1983, gender: "male" },
            { name: "Betty", birthYear: 1983, gender: "female" }
        ]
    },
    {
        key: 1991,
        items: [
            { name: "Amelia", birthYear: 1991, gender: "female" },
            { name: "Andrew", birthYear: 1991, gender: "male" }
        ]
    }
]

To group by custom criteria, pass a function to the group(groupExpr) method.

JavaScript
dataSource.group(function(dataItem) {
    return dataItem.birthYear < 1990 ? "Born before 1990" : "Born after 1990";
});
dataSource.load().done(function(result) {
    //'result' contains the loaded array
});

In this case, the 'result' array includes the following items.

[
    {
        key: "Born after 1990",
        items: [
            { name: "Amelia", birthYear: 1991, gender: "female" },
            { name: "Andrew", birthYear: 1991, gender: "male" }
        ]
    },
    {
        key: "Born before 1990",
        items: [
            { name: "Benjamin", birthYear: 1983, gender: "male" },
            { name: "Daniela", birthYear: 1983, gender: "female" },
            { name: "Lee", birthYear: 1983, gender: "male" },
            { name: "Betty", birthYear: 1983, gender: "female" }
        ]
    }
]

Multi-level grouping is also supported. Pass several getters or an array of getters to the group(groupExpr) method to load an array containing subgroups.

JavaScript
dataSource.group("birthYear", "gender");
dataSource.load().done(function(result) {
    //'result' contains the loaded array
});

The loaded array looks like the following.

[
    {
        key: 1983,
        items: [
            {
                key: "female",
                items:[
                    { name: "Daniela", birthYear: 1983, gender: "female" },
                    { name: "Betty", birthYear: 1983, gender: "female" }
                ]
            },
            {
                key: "male",
                items:[
                    { name: "Benjamin", birthYear: 1983, gender: "male" },
                    { name: "Lee", birthYear: 1983, gender: "male" }
                ]
            }
        ]
    },
    {
        key: 1991,
        items: [
            {
                key: "female",
                items:[
                    { name: "Amelia", birthYear: 1991, gender: "female" }
                ]
            },
            {
                key: "male",
                items:[
                    { name: "Andrew", birthYear: 1991, gender: "male" }
                ]
            }
        ]
    }
]

Stores support the same group expression syntax as the DataSource.

JavaScript
var arrayStore = new DevExpress.data.ArrayStore(data);
arrayStore
    .load({
        group: "birthYear"
    })
    .done(function (result) {
        // 'result' contains the 'data' array grouped by the 'birthYear' property value.
    });

Data Modification

For read-write access to data, use a Store directly in addition to the DataSource. You have two opportunities to obtain a Store instance.

Create a Store instance explicitly.

JavaScript
var store = new DevExpress.data.ArrayStore({
    key: "id",
    data: [
        {
            id: 1,
            value: "value 1"
        },
        {
            id: 2,
            value: "value 2"
        }
    ]
});

Note that it is necessary to specify the key expression to identify data items.

To load data from this Store either call store.load(loadOptions) or wrap this Store with the DataSource.

If you already have an existing DataSource, you can obtain the underlying Store instance via the store() method.

Insertion

To insert a new item to the Store, call the store.insert(values) method.

JavaScript
store.insert({
        id: 3,
        value: "value 3"
    })
    .done(function(values, key) {
        //handle success
    })
    .fail(function(error) {
        //handle error
    });

Some Stores can generate a key value if you have not specified it in the values object. You may access it via the key argument of the done callback.

Update

To update a data item specified by the key, use the store.update(key, values) method.

JavaScript
store.update(1, { value: "new value" })
    .done(function(values) {
        //handle successfull updating
    })
    .fail(function(error) {
        //handle error
    });

Note that the second argument of the update(key, values) method contains only the properties whose values should be changed in the data item, but not the entire item, because it will be merged with the original item object.

In a common scenario, before updating, you need to load the required item using the store.byKey(key) method.

Removing

To remove a data item, call the store.remove(key) method.

JavaScript
store.remove(1)
    .done(function(key) {
        //handle success
    })
    .fail(function(error) {
        //handle error
    });

Integration with Push Services

Integration with push services allows the store to notify the DataSource and all the UI components bound to it about a data modification. On receiving the notification, the DataSource can reapply data processing operations, and the components can update parts of the UI in real time.

All stores provide the push(changes) method used to insert, update, and remove data objects. It accepts an array. This allows you to update data in batches.

store.push([{ type: "insert", data: data }]);
store.push([{ type: "update", data: data, key: key }]);
store.push([
    { type: "remove", key: key1 },
    { type: "remove", key: key2 }
]);

Call this method in the event handlers for client functions that can be invoked from the server.

DataGrid SignalR Demo Scheduler SignalR Demo Chart SignalR Demo

After being notified, the DataSource can reapply sorting, filtering, grouping, and other data processing settings if you set the reshapeOnPush property to true. If pushes come too frequently, specify pushAggregationTimeout to aggregate them.

View Demo

Events and Change Tracking

DataSource and Stores support events. The DataSource events are used to track the data loading state. They are used internally by the DevExtreme UI components. You can also use them for your purposes. Stores also raise events before and after each operation (loading, update, insertion, etc.).

Use one of the following approaches to handle events.

Assign a Handler to a Configuration Property

To attach a handler for a certain event to a DataSource or Data Store, use the corresponding configuration property. The properties that take on event handling functions have names starting with on.

The following example demonstrates how to use a configuration property to handle an event.

JavaScript
var dataSource = new DevExpress.data.DataSource({
    sort: "name",
    pageSize: 10,
    store: store,
    onChanged: function() {
        // Executed after the DataSource has successfully loaded data
    }
});

Attach Several Handlers Using one Method

To attach several event handlers to a DataSource or Data Store, use the on method. This method has two overloads.

  • on(eventName, eventHandler)
    This method allows you to provide several handlers for an event.

    JavaScript
    dataSource
        .on("changed", handler1)
        .on("changed", handler2)
  • on(events)
    This method allows you to subscribe to several events with one method call.

    JavaScript
    dataSource
        .on({
            "changed": handler1,
            "loadError": handler2
        })

You can find a full list of events in the Events section: DataSource events and Store events (for example, ArrayStore events).

To unsubscribe from the events that you handled using the on() method, use the off() method. This method has two overloads.

  • off(eventName, eventHandler)
    This method allows you to detach a specific event handler.

    JavaScript
    dataSource
        .off("changed", handler1)
        .off("changed", handler2)
  • off(eventName)
    This method allows you to detach all event handlers from a specific event.

    JavaScript
    dataSource
        .off("changed")

Handling Errors

DevExtreme includes the setErrorHandler utility method to help you handle errors that occur within the entire data layer. Pass an error handler to this method. The handler accepts a JavaScript Error object as a parameter.

jQuery
index.js
DevExpress.data.setErrorHandler(function (error) {
    console.log(error.message);
});

// or when using modules
import { setErrorHandler } from "data/errors";

setErrorHandler(function(error) {
    console.log(error.message);
});
Angular
app.component.ts
import { setErrorHandler } from "data/errors";

setErrorHandler(function(error) {
    console.log(error.message);
});
Vue
App.vue
import { setErrorHandler } from "data/errors";

setErrorHandler(function(error) {
    console.log(error.message);
});
React
App.js
import { setErrorHandler } from "data/errors";

setErrorHandler(function(error) {
    console.log(error.message);
});

A Store enables you to handle errors that occur only within itself. To handle Store errors, assign an error handling function to the handleError configuration property of the Store.

JavaScript
var store = new DevExpress.data.LocalStore({
    name: "MyStore",
    errorHandler: function(error) {
        console.log(error.message);
    }
};
jQuery

DevExtreme also provides an ability to handle errors that occur during operation execution. All DevExtreme data transfer operations are asynchronous and return a Promise (a jQuery.Promise) allowing you to specify both success and error callbacks.

Angular

DevExtreme also provides an ability to handle errors that occur during operation execution. All DevExtreme data transfer operations are asynchronous and return a Promise (a native Promise) allowing you to specify both success and error callbacks.

Vue

DevExtreme also provides an ability to handle errors that occur during operation execution. All DevExtreme data transfer operations are asynchronous and return a Promise (a native Promise) allowing you to specify both success and error callbacks.

React

DevExtreme also provides an ability to handle errors that occur during operation execution. All DevExtreme data transfer operations are asynchronous and return a Promise (a native Promise) allowing you to specify both success and error callbacks.

jQuery

The following example illustrates how to handle errors that occurred during the execution of the load operation of a DataSource.

JavaScript
dataSource.load()
    .done(function(result) {
        // process result
    })
    .fail(function(error) {
        console.log(error.message);
    });

Query Concept

Besides DataSource and Stores, DevExtreme data layer has one more useful concept named Query. Query is an abstract chainable interface which provides functionality to evaluate data queries.

There are several implementations of this interface, and they are used internally in some Stores as part of the load() method. While Query is considered internal instrument, one implementation, which is a wrapper over a JavaScript array, is particularly interesting, and can be used in the application code.

Here is an example:

JavaScript
var processedArray = DevExpress.data.query(inputArray)
    .filter([ [ "value", ">=", 10 ], "and", [ "value", "<=", 90 ]])
    .sortBy("lastName")
    .thenBy("firstName")
    .select("lastName", "firstName", "value")
    .toArray();

The full list of supported methods is given in the Query API reference.

View on GitHub

Getters And Setters

One of the basic data layer concepts is a Getters and Setters concept. Getters are essential tools used to specify sorting, grouping, filtering, and data transformation rules.

A getter is a function that returns a value of the predefined property of an object passed to the getter as a parameter. A setter is a function that assigns the new value to the predefined property of an object passed to the setter as a parameter. The new value is specified via the second parameter of a setter.

Create the person object to consider how to get and set its property values using getters and setters.

JavaScript
var person = {
  firstName: "John",
  lastName: "Smith",
  birthDate: new Date(1985,10,15),
  getBirthYear: function(){
    return this.birthDate.getFullYear();
  },
  address: {
    zipCode: 90007,
    state: "CA",
    city: "Los Angeles",
    getAddress: function(){
      return this.zipCode + " " + this.state + " " + this.city;
    }
  }
};

Creating Getters And Setters

DevExtreme includes methods used to create a getter and setter for the property specified as a string.

JavaScript
var getter = DevExpress.data.utils.compileGetter("firstName");
var setter = DevExpress.data.utils.compileSetter("firstName");
NOTE
In this section, we will use the compileGetter and compileSetter utility functions to demonstrate the use of getters and setters separately. However, in practice you do not need to use them when specifying sorting, filtering, etc. Use only the values passed to these methods. They too should be named "getter" and "setter".

Consider some examples to understand how getters and setters work.

To get a value of the person.firstName property, call the created getter and pass the person object to it.

JavaScript
// the getter returns "John" and assigns it to the 'name' variable
var name = getter(person); 

To assign a new value to the person.firstName property, call the created setter and pass the person object and the new value to it.

JavaScript
// assigns "Michael" to 'person.firstName'
setter(person, "Michael"); 

The property for which you want to create a getter or a setter is specified via a string passed to the compileGetter or the compileSetter method. The property string can have one of the following formats.

  • A property name
    Specifies a top-level property.

    JavaScript
    var getter = DevExpress.data.utils.compileGetter("firstName");
    var setter = DevExpress.data.utils.compileSetter("firstName");
    // the getter returns "John" and assigns it to the 'name' variable
    var name = getter(person);
    // assigns "Michael" to 'person.firstName'
    setter(person, "Michael");
  • A second and higher level property
    Specifies a second and higher level property.

    JavaScript
    var getter = DevExpress.data.utils.compileGetter("address.city");
    var setter = DevExpress.data.utils.compileSetter("address.city");
    // the getter returns "Los Angeles" and assigns it to the 'city' variable
    var city = getter(person); 
    // assigns "San Francisco" to 'person.address.city'
    setter(person, "San Francisco"); 
  • A method name
    Specifies the method whose value should be returned by the getter.

    JavaScript
    var getter = DevExpress.data.utils.compileGetter("getBirthYear");
    // the getter returns 1985 and assigns it to the 'year' variable
    var year = getter(person); 
  • An array of property and method names
    If you pass an array of strings to the compileGetter function, the compiled getter will return an object containing the specified properties. If an item of the array passed to compileGetter represents a second or higher level property, the getter will create the appropriate subobjects within the returned object.

    JavaScript
    var getter = DevExpress.data.utils.compileGetter(["firstName", "lastName", "address.city", "getBirthYear"]);
    var personData = getter(person); 

    The specified getter called for the person object will return the following object.

    {
        firstName: "John",
        lastName: "Smith",
        getBirthYear: 1985,
        address: {
            city: "Los Angeles"
        }
    }

    Note that the returned object contains the getBirthYear property that holds the value returned by the method with the same name.

  • A function
    If you pass a function to compileGetter, this function will be executed each time the getter is called. The function should have a parameter that takes on an object passed to the getter.

    JavaScript
    var getter = DevExpress.data.utils.compileGetter(function(dataItem) {
      return dataItem.firstName + " " + dataItem.lastName;
    });
    // the getter returns "John Smith" and assigns it to the 'fullName' variable
    var fullName = getter(person); 
  • "this"
    In this case, the getter returns the object itself. This getter is used to sort or group an array of items containing a simple value.

    JavaScript
    var getter = DevExpress.data.utils.compileGetter("this");
    // the getter returns "A text" and assigns it to the 'text' variable
    var text = getter("A text");

Getter And Setter Calling Options

If you assign the name of a method to a compileGetter, the getter normally returns the value returned by this method. If you need to get a reference to the method instead of its value, pass an object with the functionAsIs property set to true to the getter as a second parameter.

JavaScript
var getter = DevExpress.data.utils.compileGetter("address.getAddress");
// the getter returns a reference to the 'person.address.getAddress' function and assigns it to the 'getAddressFunction' variable
var getAddressFunction = getter(person, { functionsAsIs: true }); 

If the getter returns an object containing several functions, the functionsAsIs property affects all functions contained in this object.

When you use a setter to assign a new value to a property containing an object, the object held in the property is replaced by the new one by default.

JavaScript
var setter = DevExpress.data.utils.compileSetter("address");
setter(person, {
    city: "San Francisco",
    street: "Stanford Ave"
});

In the example above, the object held in the person.address property will contain only the city and street properties after the setter will be called. Other members will be lost.

If you need to merge the new object passed to the setter with the object held in the property, pass a properties object containing the merge property set to true to the setter.

JavaScript
var setter = DevExpress.data.utils.compileSetter("address");
setter(
    person, 
    {
        city: "San Francisco",
        street: "Stanford Ave"
    }, 
    { merge: true }
);

In this case, the address object will contain the following properties.

{
    zipCode: 90007,
    state: "CA",
    city: "San Francisco",
    street: "Stanford Ave",
    getAddress: function() {
      return this.zipCode + " " + this.state + " " + this.city;
    }
}