Добрый день, уважаемые хабрапользователи.
С недавних пор я занимаюсь разработкой на Ext JS 4 и Zend framework 2.
Пользуясь случаем, хотелось бы создать небольшую серию статей, «на пальцах» освещаюшую некоторые основные компоненты Ext JS 4, без которых не обойдется ни одно приложение на Ext JS (простите, серия,- громко сказано,- пишу из песочницы).
Итак, Grid, часть 1.
Компонент Grid позволяет создавать очень гибкие и функциональные таблицы с пагинацией, сортировкой, фильтрацией, поддерживающие неограниченную вложенность, шаблоны, пользовательские рендерреры, множество плагинов, событийную модель, а также все операции CRUD используя ajax, rest, html5, cookies, сессии и многое другое.
В первой статье я собираюсь рассмотреть все базовые принципы работы с grid (все примеры я постарался сделать максимально простыми, стремясь к тому, чтобы из них нельзя было убрать ни строчки кода).
Самая простая таблица Ext.grid и Ext.data.ArrayStore
<code> Ext.onReady(function() { var myData = [ ['3m Co', 71.72, 0.02, 0.03, '9/1 12:00am'], ['Alcoa Inc', 29.01, 0.42, 1.47, '9/1 12:00am'], //.................. ['Verizon Communications', 35.57, 0.39, 1.11, '9/1 12:00am'], ['Wal-Mart Stores, Inc.', 45.45, 0.73, 1.63, '9/1 12:00am'] ]; var store = Ext.create('Ext.data.ArrayStore', { fields: ['company','price','change','pctChange','lastChange'], data: myData }); Ext.create('Ext.grid.Panel', { store: store, columns: [ { text : 'Company', dataIndex: 'company' }, { text : 'Price', dataIndex: 'price' }, { text : 'Change', dataIndex: 'change' }, { text : '% Change', dataIndex: 'pctChange' }, { text : 'Last Updated', dataIndex: 'lastChange' } ], height: 350, width: 600, title: 'Простейшая статическая таблица grid', renderTo: 'grid1' }); }); </code>
В основе лежит максимально упрощенный пример с сайта разработчиков.
Чтобы заставить данный код работать, создадим файл index.html (или любой другой) со следующим содержимым:
<code> <!DOCTYPE html> <html> <head> <title></title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <link rel="stylesheet" href="/resources/css/ext-all.css" type="text/css" /> <script src="/ext-all.js" type="text/javascript"></script> <script src="/examples/grid1.js" type="text/javascript"></script> </head> <body> <p>Простая таблица grid и хранилище Ext.data.ArrayStore</p> <div id="grid1"></div> </body> </html> </code>
Не буду заострять внимание на этом шаге. Ознакомиться со структурой можно на хабе
Вернемся к коду!
Практически каждое приложение Ext JS 4 начинается с Ext.onReady(function(){...}. Те, кто использовал, или использует jQuery свои приложения начинают с чего-то вроде: jQuery(document).ready(function(){...}),- цель обоих записей одна — подождать, пока загрузится DOM и файлы фреймворка.
Первым делом мы создали обычный неассоциативный двухуровневый массив myData с данными прямо в коде.
Затем с помощью конструкции Ext.create создали объект класса Ext.data.ArrayStore,- простое хранилище, позволяющее работать с обыкновенными javascript массивами, такими как наш myData.
Объект можно создать и более привычным способом,- с помощью оператора new, например new Ext.data.ArrayStore(...), но тогда бы нам пришлось вручную подгрузить файл с классом Ext.data.ArrayStore.
Благо на этот случай в Ext JS предусмотрена конструкция Ext.require:
<code> Ext.require([ 'Ext.grid.*', 'Ext.data.*', ]); </code>
Так, например, вы можете подгрузить все файлы классов для областей видимости Ext.grid и Ext.data.
Задача объекта store,- определить поля,- некую структуру данных и загрузить сами эти данные.
Последний шаг — определение объекта Ext.grid.Panel.
В конструктор Ext.grid.Panel мы передаем объект Store,- для работы с данными, в нем же определяем колонки таблицы grid и параметры панели:
высоту (height), ширину (width), заголовок (title) и id html-контейнера (renderTo), куда Ext JS будет рендерить нашу таблицу.
Вот и все!
Все, что мы проделали, достаточно для того, чтобы создать простейшую таблицу Grid, просто выводящую несколько строк с данными и сортировкой по умолчанию.
Забегая вперед отмечу, что количество полей в хранилище не обязательно должно быть равным количеству колонок в grid,- они лишь определяют структуру данных, которые можно выводить, как угодно и где вам вздумается.
В результате работы данного скрипта мы получим простейшую статическую табличку grid:
Live demo
Заменим Ext.data.ArrayStore на Ext.data.Store, чтобы было удобнее работать с данными.
Grid и Ext.data.Store
<code> Ext.onReady(function() { var myData = [ { company: 'Alcoa Inc', price: 71.72, change: 0.02, pctChange: 0.03, lastChange: '9/1 12:00am' }, { company: 'Altria Group Inc', price: 83.81, change: 0.28, pctChange: 0.34, lastChange: '9/1 12:00am' }, { company: 'American International Group, Inc.', price: 81.82, change: 0.12, pctChange: 0.63, lastChange: '9/1 12:00am' } ]; var store = Ext.create('Ext.data.Store', { fields: ['company','price','change','pctChange','lastChange'], data: myData }); var grid = Ext.create('Ext.grid.Panel', { store: store, columns: [ { text : 'Company', dataIndex: 'company' }, { text : 'Price', dataIndex: 'price' }, { text : 'Change', dataIndex: 'change' }, { text : '% Change', dataIndex: 'pctChange' }, { text : 'Last Updated', dataIndex: 'lastChange' } ], height: 350, width: 600, title: 'Простейшая статическая таблица grid', renderTo: 'grid2' }); }); </code>
Теперь данные в myData представляют собой коллекцию объектов (хэшей), с которой мы собираемся работать дальше.
На экране мы видим все ту же таблицу (разве что в ней меньше данных и у нее другой заголовок).
Теперь добавим модель данных в наше приложение.
Grid, Ext.data.Store и Ext.data.Model
<code> Ext.onReady(function() { var myData = [ { company: 'Alcoa Inc', price: 71.72, change: 0.02, pctChange: 0.03, lastChange: '9/1 12:00am' }, { company: 'Altria Group Inc', price: 83.81, change: 0.28, pctChange: 0.34, lastChange: '9/1 12:00am' }, { company: 'American International Group, Inc.', price: 81.82, change: 0.12, pctChange: 0.63, lastChange: '9/1 12:00am' } ]; Ext.define('Model', { extend: 'Ext.data.Model', fields: ['company','price','change','pctChange','lastChange'] }); var store = Ext.create('Ext.data.Store', { model: 'Model', data: myData }); var grid = Ext.create('Ext.grid.Panel', { store: store, columns: [ { text : 'Company', dataIndex: 'company' }, { text : 'Price', dataIndex: 'price' }, { text : 'Change', dataIndex: 'change' }, { text : '% Change', dataIndex: 'pctChange' }, { text : 'Last Updated', dataIndex: 'lastChange' } ], height: 350, width: 600, title: 'Простейшая статическая таблица grid', renderTo: 'grid3' }); }); </code>
Пару слов о модели: если хранилище Store — коллекция данных, то модель — это один экземпляр этих данных. На первый взгляд модель может показаться избыточной, однако без нее мы не сможем простым способом работать с многоуровневыми вложенными данными.
Пришло время избавиться от хранения данных в коде, для этого нам понадобится Proxy.
Grid и Proxy
<code> Ext.onReady(function() { Ext.define('myModel', { extend: 'Ext.data.Model', fields: ['company','price','change','pctChange','lastChange'] }); var store = Ext.create('Ext.data.Store', { model: 'myModel', proxy: { type: 'ajax', url: '/examples/data/data.json', reader: { type: 'json', root: 'data' } }, autoLoad: true }); var grid = Ext.create('Ext.grid.Panel', { store: store, columns: [ { text : 'Company', dataIndex: 'company' }, { text : 'Price', dataIndex: 'price' }, { text : 'Change', dataIndex: 'change' }, { text : '% Change', dataIndex: 'pctChange' }, { text : 'Last Updated', dataIndex: 'lastChange' } ], height: 350, width: 600, title: 'Простейшая статическая таблица grid', renderTo: 'grid4' }); }); </code>
С этого момента начинается самое интересное,- мы убрали жестко закодированные данные из нашей таблицы grid и определили прокси в нашем хранилище.
Объект прокси позволяет очень удобно совершать любые CRUD-процедуры, используя ajax, rest или же локальные хранилища на абстрактном уровне, использовать специальные ридеры, для расшифровки данных (подробнее в следующей статье).
Мы будем использовать ajax прокси, который автоматически (autoLoad: true) подгрузит данные по указанному url-адресу (запрос по умолчанию выполняется методом GET), а указанный ридер попытается расшифровать их из json-формата. root: 'data' указывает ридеру на то, что корневой узел с данными имеет ключ «data».
Важный момент,- по умолчанию grid отображает лишь 25 строк, чтобы это исправить, нужно добавить в инициализацию нашего хранилища свойство pageSize, вот так:
<code> var store = Ext.create('Ext.data.Store', { model: 'myModel', pageSize: 50, proxy: { type: 'ajax', url: '/examples/data/data.json', reader: { type: 'json', root: 'data' } }, autoLoad: true }); </code>
Чтобы включить пагинацию в таблице grid достаточно добавить в инициализацию grid объект Ext.PagingToolbar в какую-нибудь «панель инструментов», например в bbar:
<code> var grid = Ext.create('Ext.grid.Panel', { store: store, columns: [ { text : 'Company', dataIndex: 'company' }, { text : 'Price', dataIndex: 'price' }, { text : 'Change', dataIndex: 'change' }, { text : '% Change', dataIndex: 'pctChange' }, { text : 'Last Updated', dataIndex: 'lastChange' } ], height: 350, width: 600, title: 'Простейшая статическая таблица grid', renderTo: 'grid5', bbar: Ext.create('Ext.PagingToolbar', { store: store }) }); </code>
Теперь у нас есть пагинация:
Live demo
Обращу внимание на то, что все колонки таблиц Grid по умолчанию поддерживают локальную сортировку, если не указано обратное. Если необходимо сортировать данные на сервере, необходимо указать это в нашем хранилище с помощью свойства «remoteSort: true»:
<code> Ext.onReady(function() { Ext.define('myModel', { extend: 'Ext.data.Model', fields: ['company','price','change','pctChange','lastChange'] }); var store = Ext.create('Ext.data.Store', { model: 'myModel', proxy: { type: 'ajax', url: '/examples/data/data.json', reader: { type: 'json', root: 'data' } }, autoLoad: true, remoteSort: true }); var grid = Ext.create('Ext.grid.Panel', { store: store, columns: [ { text : 'Company', dataIndex: 'company' }, { text : 'Price', dataIndex: 'price' }, { text : 'Change', dataIndex: 'change' }, { text : '% Change', dataIndex: 'pctChange' }, { text : 'Last Updated', dataIndex: 'lastChange' } ], height: 350, width: 600, title: 'Простейшая статическая таблица grid', renderTo: 'grid6', bbar: Ext.create('Ext.PagingToolbar', { store: store }) }); }); </code>
Теперь при попытке сортировки вы можете увидеть в firebug следующую картину:
Теперь давайте посмотрим, как можно реализовать фильтрацию данных.
Фильтры в таблицах grid
<code> Ext.Loader.setPath('Ext.ux', '/public/ux'); Ext.require([ 'Ext.ux.grid.FiltersFeature', ]); Ext.onReady(function() { Ext.define('myModel', { extend: 'Ext.data.Model', fields: ['company','price','change','pctChange','lastChange'] }); var store = Ext.create('Ext.data.Store', { model: 'myModel', proxy: { type: 'ajax', url: '/public/examples/data/data.json', reader: { type: 'json', root: 'data' } }, autoLoad: true, remoteSort: true }); var filters = { ftype: 'filters', encode: true, local: false }; var grid = Ext.create('Ext.grid.Panel', { store: store, features: [filters], columns: [ { text : 'Company', dataIndex: 'company', filter: { type: 'string' } }, { text : 'Price', dataIndex: 'price', filter: { type: 'int' } }, { text : 'Change', dataIndex: 'change', filter: { type: 'float' } }, { text : '% Change', dataIndex: 'pctChange', filter: { type: 'float' } }, { text : 'Last Updated', dataIndex: 'lastChange', filter: { type: 'date' } } ], height: 350, width: 600, title: 'Фильтрация полей grid на сервере', renderTo: 'grid8', bbar: Ext.create('Ext.PagingToolbar', { store: store }) }); }); </code>
Впервые в коде нашей таблицы grid появилась запись:
<code> Ext.Loader.setPath('Ext.ux', '/public/ux'); Ext.require([ 'Ext.ux.grid.FiltersFeature', ]); </code>
Эта запись заставляет Ext JS подгрузить класс Ext.ux.grid.FiltersFeature из директории /public/ux/grid, который необходим для работы фильтров в таблице grid.
Еще одно новшество,- инициализация объекта filters, содержащего конфигурацию наших фильтров:
<code> var filters = { ftype: 'filters', encode: true, local: false }; </code>
Здесь нас интересуют свойства encode и local. Как вы, навероное, уже догадались, свойство local: false заставляет отправить значения фильтров на сервер, а encode: true, позволяет получать данные фильтров на сервере в виде массива, а не строки-json.
Итак, фильтрация сконфигурирована, осталось указать в колонках таблицы: какой именно фильтр использовать колонке:
<code> filter: { type: 'string' } </code>
В нашем примере использованы фильтры: string, int, float и date,- работу каждого из них можно увидеть кликнув по заголовку колонки в таблице, выбрать элемент Filters
и указать его значение, после чего сработает событие onChange данные таблицы обновятся. Вы можете увидеть в firebug, как Ext JS выполняет ajax-запрос на сервер.
Live demo (простите, данные не меняются при использовании фильтров,- закешировал фронтэнд в хлам, т.к. боюсь хабраэффекта)
Завершая статью, хочется немного приукрасить нашу таблицу.
Во первых укажем типы для полей в модели:
<code> fields: [ {name: 'weight', type: 'int'}, {name: 'company', type: 'string'}, {name: 'price', type: 'float'}, {name: 'change', type: 'float'}, {name: 'pctChange', type: 'float'}, {name: 'lastChange', type: 'string'} ] </code>
Во вторых хватит запрашивать данные GET'ом:
<code> Ext.create('Ext.data.Store', { pageSize: 10, model: 'myModel', proxy: { type: 'ajax', url: '/examples/data/data2.json', actionMethods: { read: 'POST' }, reader: { type: 'json', root: 'data', totalProperty: 'totalCount' } }, autoLoad: true, remoteSort: true }); </code>
Сделаем колонки таблицы резиновыми:
<code> { text : '% Change', flex:1, dataIndex: 'pctChange' }, </code>
Попробуем создать объект хранилища неявно:
<code> store: Ext.data.StoreManager.lookup('gridStore'), </code>
Создадим простейший обработчик для отображения колонки grid,- пусть он покажется наивным.
<code> renderer: function(value){ if(value > 75){ value = '<span style="color:#ff0000;">' + value + '</span>'; } else { value = '<span style="color:#00ff00;">' + value + '</span>'; } return value; } </code>
И на последок, добавим информацию об общем количестве записей таблицы в наш пагинатор:
<code> bbar: Ext.create('Ext.PagingToolbar', { store: store, displayInfo: true, displayMsg: 'Displaying topics {0} - {1} of {2}', emptyMsg: "No topics to display" }) </code>
(данные об общем колличестве приходят в json, как указано в totalProperty в нашем хранилище).
Собираем воедино:
<code> Ext.Loader.setPath('Ext.ux', '/ux'); Ext.require([ 'Ext.ux.grid.FiltersFeature', ]); Ext.onReady(function() { Ext.define('myModel', { extend: 'Ext.data.Model', fields: [ {name: 'weight', type: 'int'}, {name: 'company', type: 'string'}, {name: 'price', type: 'float'}, {name: 'change', type: 'float'}, {name: 'pctChange', type: 'float'}, {name: 'lastChange', type: 'string'} ] }); Ext.create('Ext.data.Store', { pageSize: 10, model: 'myModel', proxy: { type: 'ajax', url: '/examples/data/data2.json', actionMethods: { read: 'POST' }, reader: { type: 'json', root: 'data', totalProperty: 'totalCount' } }, autoLoad: true, remoteSort: true }); var filters = { ftype: 'filters', encode: true, local: false }; var grid = Ext.create('Ext.grid.Panel', { store: Ext.data.StoreManager.lookup('gridStore'), features: [filters], columns: [ { text : 'Weight', dataIndex: 'weight', width: 50, sortable: false, filter: { type: 'int' } }, { text : 'Company', dataIndex: 'company', flex:1, filter: { type: 'string' } }, { text : 'Price', dataIndex: 'price', flex:1, renderer: function(value){ if(value > 75){ value = '<span style="color:#ff0000;">' + value + '</span>'; } else { value = '<span style="color:#00ff00;">' + value + '</span>'; } return value; } }, { text : 'Change', flex:1, dataIndex: 'change' }, { text : '% Change', flex:1, dataIndex: 'pctChange' }, { text : 'Last Updated', flex:1, dataIndex: 'lastChange', filter: { type: 'date' } } ], height: 350, width: 600, title: 'Наводим красоту...', renderTo: 'grid9', bbar: Ext.create('Ext.PagingToolbar', { store: store, displayInfo: true, displayMsg: 'Displaying topics {0} - {1} of {2}', emptyMsg: "No topics to display" }) }); }); </code>
и видим красивую таблицу grid с сортировкой и фильтрацией на сервере, пагинацией и простейшим custom-renderer'ом:
Послесловие.
Уважаемые хабрапользователи, очень надеюсь на вашу лояльность,- это моя первая статья. Уважаемое НЛО,- прошу прощения за репост моей статьи, изначально написал для хабра, но захотелось кинуть ссылку на демо (и еще кое-что). О неточностях, косяках, замечаниях, просьбах и т.д. прошу писать в личку. Если эта статья кого-то заинтересовала, буду рад продолжить писать по Ext JS 4,- в планах написать статью-продолжение по Ext JS grid (плагины. шаблоны, события, связка с propertygrid, CRUD и ридеры...), а также осветить другие аспекты фреймворка Ext JS 4. Спасибо за внимание.
Автор: spendlively