JavaScript, Инструменты, Технологии → Nested Grids с помощью ExtJS 3.0
Введение

Суть проблемы, рассматриваемой в данной статье заключается в том, что Grid объекты библиотеки ExtJS не предназначены для использования в контексте вложенности. В общем случае, такая задача редко становится перед разработчиком. И все же, иногда, как, например, в моем случае, с ней приходится сталкиваться. Ниже я попытаюсь поделиться накопленным опытом, и, возможно, окажу тем самым кому-нибудь неоценимую помощь, на что искренне надеюсь :). Итак, в добрый путь...
Вложенные Grids или ColumnTree
Многие могут заметить, что большинство задач можно попросту решить методом использования ColumnTree, вместо того, чтобы пытаться реализовать вложенные «сетки». Да, если вас полностью устраивает такое решение — используйте именно его. Именно для этого и предназначены ColumnTrees. Но в ряде случаев довольно тяжело смириться с фактом, что вы лишаетесь всех тех полезных функций, которые нам предоставляют Grids, а именно: сортировки, Drag&Drop колонок, фильтры и т.д. и т.п. Если они действительно необходимы, то приходится задуматься о реализации возможности вкладывать одну сетку в другую. Ниже о том, как это сделать.
Проблемы использования RowExpander
На первый взгляд, решить проблему призван плагин RowExpander из библиотеки ux. Однако, не все так просто. Данный плагин спроектирован для возможности отобразить/скрыть произвольный HTML код в строке «сетки». В частности же, при попытке встроить в строку другой грид, сталкиваемся с проблемами в работе данного плагина. Выхода, по сути 2: написать свой плагин или несколько модифицировать существующий.
Реализацию своих плагинов я оставлю на совесть тех, у кого свободного времени много, я же выбрал путь модификации. В конце-концов, никто не запрещает назвать модифицированный плагин другим именем и использовать его как нечто новое и свое. На здоровье!
Основная непрятность связана с неверным отображением иконок и некоторым, связанным с этим, неверным поведением плагина RowExpander, который находится внутри «сетки», также использующей этот плагин. Суть идеи решения проблемы заключается в том, что каждая «сетка» вложенная в другую «сетку» должна использовать свои имена стилей для определения открытости/закрытости строки. Вооружившись данной идеей довольно просто реализовать динамическое создание идентичных стилей с разными именами, напрямую зависящих от идентификатора сетки. Сделать это довольно просто. Изменим немного конструктор плагина, добавив следующий код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | if (!config.id) { config.id = Ext.id(); } Ext.apply( this, config); var css = '.x-' + this.id + '-grid3-row-collapsed .x-grid3-row-expander { background-position:0 0; }' + '.x-' + this.id + '-grid3-row-expanded .x-grid3-row-expander { background-position:-25px 0; }' + '.x-' + this.id + '-grid3-row-collapsed .x-grid3-row-body { display:none !important; }' + '.x-' + this.id + '-grid3-row-expanded .x-grid3-row-body { display:block !important; }' ; Ext.util.CSS.createStyleSheet( css, Ext.id()); this.expanderClass = 'x-grid3-row-expander'; this.rowExpandedClass = 'x-' + this.id + '-grid3-row-expanded'; this.rowCollapsedClass = 'x-' + this.id + '-grid3-row-collapsed'; |
Необходимость определения свойств expanderClass, rowExpandedClass и rowCollapsedClass связана с тем, что теперь нам будет довольно легко оперировать их значениями внутри плагина, а нам еще предстоит внести некоторые изменения в его код.
Теперь у нас есть отдельные классы стилей для каждой уникальной сетки на нашей странице. Изменим методы, связанные с определением поведения плагина, чтобы они могли использовать предопределенные нами имена стилей:
Изменим метод render:
1 | return '<div class="' + this.expanderClass + '"> </div>'; |
Также изменим методы toggleRow:
1 | this[Ext.fly(row).hasClass( this.rowCollapsedClass) ? 'expandRow' : 'collapseRow'](row); |
expandRow:
1 | Ext.fly( row).replaceClass( this.rowCollapsedClass, this.rowExpandedClass); |
и collapseRow:
1 | Ext.fly( row).replaceClass( this.rowExpandedClass, this.rowCollapsedClass); |
Все, на этом лечение описанной выше болезни плагина можно считать законченным.
Однако полное решение всех проблем с этим не приходит.
Ветки и листья дерева
Во первых, как и любое другое дерево, наше должно уметь правильным образом отражать «ветки» и «листья». Под этими понятиями я подразумеваю, что «ветка» может быть раскрыта (у нее есть потомки), а у листьев потомков нет. К сожалению, RowExpander в текущем состоянии не умеет отличать «листья» от «веток» и добавляет элементы управления открытием/закрытием во все строки «сетки». Для полноценной реализации дерева нам придется продолжить его менять.
Благо, решение не такое уж и сложное. Давайте остановимся на тезисе, что наш набор данных должен содержать признак того, является ли запись конечной («лист») или может содержать потомков («ветка»). Для этого достаточно определить в записи поле, например, с названием «is_leaf», принимающие логическое значение true или false (is_leaf = true — «лист», is_leaf = false — «ветка»).
В то же время мы не должны препятствовать плагину работать так, как он это делал и ранее. Т.е., мы будем сами определять, как и когда плагин должен работать в режиме дерева, по-умолчанию же, он будет вести себя так, как и раньше.
Для реализации подобного механизма определим публичные конфигурационные свойства actAsTree = false и treeLeafProperty = 'is_leaf'. Таким образом, при инициализации плагина можно будет указывать, должен ли плагин вести себя как дерево (фактически проверять являются ли строки «сетки» листьями дерева), а также самостоятельно задавать имя признака «листа» в записи.
В первую очередь, нам необходимо определить еще один стиль для отображения листа (по-сути, он должен просто скрыть элемент открытия/закрытия на элементах с типом is_leaf = true):
Добавим, в уже созданное определение стилей:
1 2 3 4 | var css = ... + '.x-grid-expander-leaf .x-grid3-row-expander { background: none; }' ; |
и определим свойство, которое бы мы могли использовать внутри плагина для данного имени класса стилей:
1 | this.leafClass = 'x-grid-expander-leaf'; |
Осталось дело за немногим, нужно проставить данный класс соответствующим строкам и запретить действия по открытию/закрытию строк, помеченных как листья.
Для этого изменим метод getRowClass (инициализация строк):
1 2 3 4 5 | var cssClass = this.state[record.id] ? this.rowExpandedClass : this.rowCollapsedClass; if (this.actAsTree && record.get( this.treeLeafProperty)) { cssClass = this.leafClass; } return cssClass; |
и добавим проверку в методы toggleRow, expandRow, collapseRow (запрещаем действия на листьях):
1 2 3 | if (Ext.fly(row).hasClass( this.leafClass)) { return ; } |
Проблема обработки событий
Даже после всего этого, проблемы остаются. Поведение вложенных друг в друга «сеток» оказывается довольно-таки неадекватным. Вы встретите глюки при выборе строк, сортировке и т.п. Действия на строках потомков будут проецироваться на родительские «сетки». Это все довольно неприятно, но очень легко решаемо!
На каждом вновь создаваемом потомке просто нужно отключить всплывающие события. Для этого, внесем очередное изменение в RowExpander в метод onRender, который в свою очередь выполняется в момент рендеринга самой «сетки».
1 2 3 | if (this.actAsTree) { grid.getEl().swallowEvent([ 'mouseover', 'mouseout', 'mousedown', 'click', 'dblclick' ]); }); |
Проблема с утечками памяти
И еще не все проблемы решены :). Нам еще необходимо позаботиться об удалении всех компонентов связанных со схлопнутыми сетками. Нету смысла описывать детальные изменения, тем более, что в этой области все еще может измениться не один раз. Просто смотрите в код:
Полный код измененного плагина RowExpander: RowExpander.js
Живой пример вложенных сеток: ExtJS Nested Grids Example
Заключение
Невзирая на первичные трудности с построением вложенных сеток, и, казалось бы, столкнувшись с неприспособленностью «сеток» к вложенности, довольно несложным образом удалось добиться вполне приемлемого результата, что в очередной раз свидетельствует о гибкости и расширяемости библиотеки ExtJS.
Мне же лишь остается надеяться, что данная статья поможет кому-либо побороть трудности.
Удачи в девелопменте!

20:43:15
Доброго времени суток. Сорри за глупый вопрос — а можно ли как-то во всём этом деле сделать драгндроп, как это в деревьях реализовано. /*суть задачи проста: есть менеджер главного меню сайта. сейчас на нём прикручено обычное дерево, но дерево с табличкой было б однозначно лучше*/
02:21:13
А вы уверены, что вам нужно именно вложенные сетки. Может в вашем случае проще обойтись ColumnTree? Там и драг-дроп тот же.
15:18:46
Не плохой получился плагин. Вот только демонстрационный пример в опере немножко подтармаживает, как будто загружает данные. Или это так задумано было?
09:57:43
Мне сложно что-либо ответить по существу по поводуОперы... Почему-то это именно так, но почему... желания разбираться нет. Лично я ее не использую
Хотя не сказал бы что в ней что-то сильно тормозит — проверил — работает очень даже сносно.
11:25:57
I was so pleased when I found your nested grid example as I am trying to create a grid within a grid.
I created a simple version of your code but cannot get it to work, I keep getting 'this.el.dom is null'. Any ideas you have as to why my code does not work would be appreciated.
This is my html code:
Experiment
This is my js code:
Ext.onReady( function() { var data = [ [1001,'Record 1','Text for record 1',false], [1002,'Record 2','Text for record 2',false], [1003,'Record 3','Text for record 3',false], [1004,'Record 4','Text for record 4',false], [1005,'Record 5','Text for record 5',false] ]; var getGrid = function(data,element) { var store = new Ext.data.ArrayStore({ fields: [ { name: 'Record'}, { name: 'Description'}, { name: 'Text'}, { name: 'is_leaf', type: 'bool' } ], data: data }); var expander = new Ext.ux.grid.RowExpander({ tpl: '', actAsTree: true, treeLeafProperty: 'is_leaf', listeners: { expand: function(expander, record, body, rowIndex){ getGrid(data, Ext.get(this.grid.getView().getRow(rowIndex)).child('.ux-row-expander-box')); } } }); var grid = new Ext.grid.GridPanel({ store: store, columns: [ expander, { id:'Record',header:'Record',width: 100,sortable: true, dataIndex: 'Record'}, { id:'Description',header:'Description',width: 100,sortable: true, dataIndex: 'Description'}, { id:'Text',header:'Text',width: 300,sortable: true, dataIndex: 'Text'} ], autoHeight: true, border: false, width: 100, stateful: true, stateId: 'grid', plugins: expander }); element && grid.render(element); return grid; }; var panel = new Ext.Panel({ title: 'John', autoHeight: true, items: [getGrid(data)], applyTo: 'grid-box' }); });12:44:15
Hi, John!
Yeap, there is a problem in your code. You've removed valid teplate declaration in row expander configuration, so there is no element with class .ux-row-expander-box.
Problem here:
var expander = new Ext.ux.grid.RowExpander ({ tpl: '', ...Solution is following:
var expander = new Ext.ux.grid.RowExpander ({ tpl: '<div class="ux-row-expander-box"><div>', ...Here is working example of your code: mikhailstadnik.com/ext/examples/help.html.
Best regards,
Mikhail
17:20:12
Michael,
thanks very much for your help, it is very much appreciated.
Regards,
John
11:29:22
Вы собираетесь реализовать поддержку ExtJS 3.1? (Сейчас с ним ваш компонент не работает)
10:25:07
Вы уверены, у меня работает. Какая именно проблема в вашем случае?
10:27:32
Михаил, добрый день,
Первым делом — спасибо за замечательный плагин!
Скажите, а Вы замечали, что заголовки колонок в сетке сдвинуты чуть вправо относительно данных? когда их мало, как в Вашем примере, это не очень заметно, но как только, как в моём случае, колонок много — они совершенно уползают в никуда.
17:21:18
Да. безусловно я это заметил. Но для моей задачи это абсолютно не критично. Вы уверены, что вам не подходит TreeGrid, который был зарелизен в ExtJs версии 3.1? Я модифицировал RowExpander для посроения вложенных сеток из-за доволно специфической задачи — на разных уровнях мне нужны разные сетки (т.е. разные кол-ва колонок и названия заголовков, и т.п.). Если у вас на разных уровнях структура данных не меняется — вам более подойдет именно TreeGrid. Мое решение не лучшее для построения подобной сетки. Я указал на способ решения довольно частного случая построения древовидных сеток (TreeGrid'ом его просто не решить). В моем примере на разных уровнях одинаковые данные лишь потому, что это ПРИМЕР.
11:10:15
Нет-нет, у меня как раз разные сетки на всех уровнях, в том-то и дело. Поэтому TreeGrid и не подходит.
Впрочем, в эксплорере седьмом всё нормально, только в файрфоксе глюк.
Спасибо ещё раз!