Как сделать таблицу в Ext JS 6

Ext JS предоставляет для гридов поддержку следующих возможностей:
  • пагинация;
  • сортировка;
  • фильтрация;
  • поиск;
  • редактирование строк и ячеек;
  • группировка;
  • позиционируемая инструментальная панель;
  • буферизованная прокрутка;
  • изменение размеров и скрытие столбцов;
  • grouped header;
  • множественная сортировка;
  • row expander.

В modern-тулките для грида используется класс Ext.grid.Grid. В классическом тулките для таблицы используется класс Ext.grid.Panel.

Простой пример

Для начала создадим модель и простой стор с захардкоденными данными.
Ext.define('Product', {
    extend: 'Ext.data.Model',
    fields: ['id', 'productname', 'desc', 'price']
});

var productStore = Ext.create('Ext.data.Store', {
    model: 'Product',
    data: [{
        id: 'P1',
        productname: 'Ice Pop Maker',
        desc: 'Create fun and healthy treats anytime',
        price: '$16.33'
    }, {
        id: 'P2',
        productname: 'Stainless Steel Food Jar',
        desc: 'Thermos double wall vacuum insulated food jar',
        price: '$14.87'
    }, {
        id: 'P3',
        productname: 'Shower Caddy',
        desc: 'Non-slip grip keeps your caddy in place',
        price: '$17.99'
    }, {
        id: 'P4',
        productname: 'VoIP Phone Adapter',
        desc: 'Works with Up to Four VoIP Services Across One Phone Port',
        price: '$47.50'
    }]
});

Теперь для вышеприведенного стора создадим грид с помощью класса Ext.grid.Panel. Для каждой колонки могут быть указаны свойства flex и width.
Ext.create('Ext.grid.Panel', {
    renderTo: Ext.getBody(),
    store: productStore,
    width: 600,
    title: 'Products',
    columns: [{
        text: 'Id',
        dataIndex: 'id',
        hidden: true
    }, {
        text: 'Name',
        width: 150,
        dataIndex: 'productname'
    }, {
        text: 'Description',
        dataIndex: 'desc',
        sortable: false,
        flex: 1
    }, {
        text: 'price',
        width: 100,
        dataIndex: 'price'
    }]
});
Для столбца 'Description' свойству flex установлено значение 1, поэтому столбец будет занимать все оставшееся пространство ширины, которое не было использовано двумя другими столбцами. Свойство dataIndex привязывает столбец к соответствующему полю в модели данных. Через свойство hidden столбец можно сделать скрытым.


Сортировка


За возможность сортировки столбца отвечает свойство sortable.

По умолчанию сортировка делается на клиентской стороне. Для того чтобы сделать сортировку на серверной стороне надо у стора включить свойство remoteSort. Тогда стор будет посылать информацию о сортировке (поле и порядок сортировки) на сервер в параметрах запроса.

Renderer

Конфиг renderer для столбца может использоваться для определения того, как визуализируются данные для столбца. 

Пример отображения денежного символа для столбца price:
Ext.create('Ext.grid.Panel', {
    renderTo: Ext.getBody(),
    store: productStore,
    width: 600,
    title: 'Products',
    columns: [{
        text: 'Id',
        dataIndex: 'id',
        hidden: true
    }, {
        text: 'Name',
        width: 150,
        dataIndex: 'productname'
    }, {
        text: 'Description',
        dataIndex: 'desc',
        sortable: false,
        flex: 1
    }, {
        text: 'price',
        width: 100,
        dataIndex: 'price',

        renderer: function(value) {
            return Ext.String.format('${0}', value);
        }
    }]
});


renderer можно использовать для отображения HTML. Можно добавить URL-ы и отображать картинки.

Фильтрация

Фильтрация добавляется к гриду через плагин Ext.grid.filters.Filters (ptype: gridfilters).
Ext.create('Ext.grid.Panel', {
    renderTo: Ext.getBody(),
    store: productStore,
    plugins: 'gridfilters',
    width: 600,
    title: 'Products',
    columns: [{
        text: 'Id',
        dataIndex: 'id',
        hidden: true
    }, {
        text: 'Name',
        width: 150,
        dataIndex: 'productname',
        filter: 'string'
    }, {
        text: 'Description',
        dataIndex: 'desc',
        sortable: false,
        flex: 1,
        filter: {
            type: 'string',
            itemDefaults: {
                emptyText: 'Search for…'
            }
        }
    }, {
        text: 'price',
        width: 100,
        dataIndex: 'price'
    }]
});


Для каждого столбца можно задать тип фильтра (string, bool, и т.д.) и дополнительные настройки для поискового поля (например emptyText). Фильтры могут добавлять как при создании грида так и после создания.

Пагинация

Пример добавления пейджинг-тулбара  Ext.toolbar.Paging (xtype: pagingtoolbar).
Ext.create('Ext.grid.Panel', {
    renderTo: Ext.getBody(),
    store: productStore,
    width: 600,
    title: 'Products',
    columns: [{
        text: 'Id',
        dataIndex: 'id',
        hidden: true
    }, {
        text: 'Name',
        width: 150,
        dataIndex: 'productname'
    }, {
        text: 'Description',
        dataIndex: 'desc',
        sortable: false,
        flex: 1
    }, {
        text: 'price',
        width: 100,
        dataIndex: 'price'
    }],
    dockedItems: [{
        xtype: 'pagingtoolbar',
        store: productStore,
        dock: 'bottom',
        displayInfo: true
    }]
});

Соответствующий код для стора:
var productStore = Ext.create('Ext.data.Store', {
    model: 'Product',
    pageSize: 10,
    autoLoad: true,
    proxy: {
        type: 'ajax',
        url: 'data.json',
        reader: {
            type: 'json',
            rootProperty: 'data',
            totalProperty: 'total'
        }
    }
});
Свойство rootProperty говорит, где искать записи. Свойство totalProperty говорит, где узнать общее количество записей. Это свойство используется педжинг-тулбаром. Свойство pageSize используется для ограничения количестве записей в ответе на запрос. Пример запроса:
http://localhost:8000/data.json?page=1&start=0&limit=10


Редактирование ячеек

Для поддержки редактирования ячеек надо добавить к гриду плагин Ext.grid.plugin.CellEditing. Также необходимо добавить редакторы к столбцам. Редактором может быть текстовое поле или например выпадающий список.
Ext.create('Ext.grid.Panel', {
    renderTo: Ext.getBody(),
    store: productStore,

    plugins: ['cellediting', 'gridfilters'],

    width: 600,
    title: 'Products',

    columns: [{
        text: 'Id',
        dataIndex: 'id',
        hidden: true
    }, {
        text: 'Name',
        width: 150,
        dataIndex: 'productname',
        filter: 'string',
        editor: {
            allowBlank: false,
            type: 'string'
        }
    }, {
        text: 'Description',
        dataIndex: 'desc',
        sortable: false,
        flex: 1
    }, {
        text: 'Price',
        width: 100,
        dataIndex: 'price'
    }, {
        text: 'Type',
        width: 100,
        dataIndex: 'type',
        editor: new Ext.form.field.ComboBox({
            typeAhead: true,
            triggerAction: 'all',
            store: [
                ['Bath', 'Bath'],
                ['Kitchen', 'Kitchen'],
                ['Electronic', 'Electronic'],
                ['Food', 'Food'],
                ['Computer', 'Computer']
            ]
        })
    }]
});
В вышеприведенном примере для столбца Type данные редактора захардкодены, но они также могут получаться с сервера. Т.е. редактор надо для этого привязать к стору.


В качестве редакторов можно использовать компоненты для выбора даты, галочки, радио-кнопки, числовые поля и т.д. Кроме того на редакторы можно навешивать валидацию.

После редактирования записи не сохраняются на сервер. Для того чтобы автоматически сохранялись надо у стора включить свойство autosync. Чтобы сохраняться вручную надо у стора вызывать методы save или sync. Можно добавить кнопку Сохранить в шапку грида и вызывать у соответствующего стора метод save или sync.

Маленькая красная точка в верхнем левом углу ячейки говорит о том, что эта ячейка обновилась.

Редактирование строк

При редактировании ячеек редактируется только одна ячейка. При редактировании строк за раз редактируется вся строка. Соответствующий плагин, который надо подключить называется  Ext.grid.plugin.RowEditing. Поэтому если использовать вместо строки  ['rowediting','gridfilters'] строку ['cellediting','gridfilters'] то результат будет таким:

Группировка

Для того чтобы произвести группировку надо у стора указать свойство groupField. Еще надо гриду назначить Ext.grid.feature.Feature.
var productStore = Ext.create('Ext.data.Store', {
    model: 'Product',
    pageSize: 10,
    autoLoad: true,
    proxy: {
        type: 'ajax',
        url: 'data.json',
        reader: {
            type: 'json',
            rootProperty: 'data',
            totalProperty: 'total'
        }
    },
    groupField: 'type'
});

Ext.create('Ext.grid.Panel', {
    renderTo: Ext.getBody(),
    store: productStore,
    width: 600,
    title: 'Products',
    features: [{
        id: 'group',
        ftype: 'grouping',
        groupHeaderTpl: '{name}',
        hideGroupedHeader: true,
        enableGroupingMenu: false
    }],
    columns: [{
        text: 'Id',
        dataIndex: 'id',
        hidden: true
    }, {
        text: 'Name',
        width: 150,
        dataIndex: 'productname'
    }, {
        text: 'Description',
        dataIndex: 'desc',
        sortable: false,
        flex: 1,
        groupable: false
    }, {
        text: 'Price',
        width: 100,
        dataIndex: 'price'
    }, {
        text: 'Type',
        width: 100,
        dataIndex: 'type'
    }]
});


Отображение меню группировки для столбца включается и выключается свойством enableGroupingMenu. Можно выключить группировку для столбца через свойство groupable.


Шаблон группировки может использоваться для внесения дополнительной информации (количество элементов в группе). Например, если заменить строку groupHeaderTpl : '{name}' на groupHeaderTpl : '{columnName}: {name} ({rows.length} Item{[values.rows.length > 1 ? "s" : ""]})' то результат будет таким:

Сводная таблица (pivot grid)


При использовании pivot grid необходимо предоставить две вещи: axis и aggregates. Первый используется для того чтобы указать что будет по строкам и столбцам. А второй для групповых вычислений.

leftAxis: [{
    width: 80,
    dataIndex: 'employee',
    header: 'Employee'
}]

topAxis: [{
    dataIndex: 'cat',
    header: 'Category',
    direction: 'ASC'
}]
Для axis можно задавать сортировку, направление сортировки, фильтр.

aggregate: [{
    measure: 'amount',
    header: 'Expense',
    aggregator: 'sum',
    align: 'right',
    width: 85,
    renderer: Ext.util.Format.numberRenderer('0,000.00')
}]
Агрегатором может быть: sum, avg, min, max.

В pivot grid можно также указывать renderer для отображения данных в заданном формате.
renderer: function(value, meta, record) {
    return Ext.util.Format.number(value, '0,000.00');
}

Стор для pivot grid:
var store = new Ext.data.JsonStore({
    proxy: {
        type: 'ajax',
        url: 'expense.json',
        reader: {
            type: 'json',
            rootProperty: 'rows'
        }
    },
    autoLoad: true,
    fields: [{
        name: 'id',
        type: 'int'
    }, {
        name: 'employee',
        type: 'string'
    }, {
        name: 'amount',
        type: 'int'
    }, {
        name: 'date',
        type: 'date',
        dateFormat: 'd/m/Y'
    }, {
        name: 'cat',
        type: 'string'
    }, {
        name: 'year',
        convert: function(v, record) {
            return Ext.Date.format(record.get('date'), "Y");
        }
    }]
});

Далее приводится пример pivot grid. leftAxis зафиксирована. topAxis динамична. Из выпадающего списка можно выбрать значения для topAxis.
var pivotGrid = Ext.create('Ext.pivot.Grid', {
    renderTo: Ext.getBody(),
    title: 'Pivot Grid - Employee Expense Claims',
    height: 600,
    width: 700,
    enableLocking: false,
    viewConfig: {
        trackOver: true,
        stripeRows: false
    },

    tbar: [{
        xtype: 'combo',
        fieldLabel: 'Select report',
        flex: 1,
        editable: false,
        value: '1',
        store: [
            ['1', 'How much an employee claimed in total?'],
            ['2', 'What are the expense amounts of each employee in each
category?'],
            ['3', 'How much an employee claimed in a specific year?']
        ],
        listeners: {
            select: function(combo, records, eOpts) {
                switch (records.get('field1')) {
                case '1':
                    pivotGrid.reconfigurePivot({
                        topAxis: []
                    });
                    break;
                case '2':
                    pivotGrid.reconfigurePivot({
                        topAxis: [{
                            dataIndex: 'cat',
                            header: 'Category',
                            direction: 'ASC'
                        }]
                    });
                    break;

                case '3':
                    pivotGrid.reconfigurePivot({
                        topAxis: [{
                            dataIndex: 'year',
                            header: 'Year',
                            direction: 'ASC'
                        }]
                    });
                    break;
                }
            }
        }
    }],

    store: store,

    aggregate: [{
        measure: 'amount',
        header: 'Expense',
        aggregator: 'sum',
        align: 'right',
        width: 85,
        renderer: Ext.util.Format.numberRenderer('0,000.00')
    }],

    leftAxis: [{
        width: 80,
        dataIndex: 'employee',
        header: 'Employee'
    }],

    topAxis: []
});