Nested Grids с помощью ExtJS 3.0

Введение



image

Суть проблемы, рассматриваемой в данной статье заключается в том, что Grid объекты библиотеки ExtJS не предназначены для использования в контексте вложенности. В общем случае, такая задача редко становится перед разработчиком. И все же, иногда, как, например, в моем случае, с ней приходится сталкиваться. Ниже я попытаюсь поделиться накопленным опытом, и, возможно, окажу тем самым кому-нибудь неоценимую помощь, на что искренне надеюсь :). Итак, в добрый путь…



Вложенные Grids или ColumnTree



Многие могут заметить, что большинство задач можно попросту решить методом использования ColumnTree, вместо того, чтобы пытаться реализовать вложенные «сетки». Да, если вас полностью устраивает такое решение — используйте именно его. Именно для этого и предназначены ColumnTrees. Но в ряде случаев довольно тяжело смириться с фактом, что вы лишаетесь всех тех полезных функций, которые нам предоставляют Grids, а именно: сортировки, Drag&Drop колонок, фильтры и т.д. и т.п. Если они действительно необходимы, то приходится задуматься о реализации возможности вкладывать одну сетку в другую. Ниже о том, как это сделать.

Проблемы использования RowExpander



На первый взгляд, решить проблему призван плагин RowExpander из библиотеки ux. Однако, не все так просто. Данный плагин спроектирован для возможности отобразить/скрыть произвольный HTML код в строке «сетки». В частности же, при попытке встроить в строку другой грид, сталкиваемся с проблемами в работе данного плагина. Выхода, по сути 2: написать свой плагин или несколько модифицировать существующий.

Реализацию своих плагинов я оставлю на совесть тех, у кого свободного времени много, я же выбрал путь модификации. В конце-концов, никто не запрещает назвать модифицированный плагин другим именем и использовать его как нечто новое и свое. На здоровье! :)

Основная непрятность связана с неверным отображением иконок и некоторым, связанным с этим, неверным поведением плагина RowExpander, который находится внутри «сетки», также использующей этот плагин. Суть идеи решения проблемы заключается в том, что каждая «сетка» вложенная в другую «сетку» должна использовать свои имена стилей для определения открытости/закрытости строки. Вооружившись данной идеей довольно просто реализовать динамическое создание идентичных стилей с разными именами, напрямую зависящих от идентификатора сетки. Сделать это довольно просто. Изменим немного конструктор плагина, добавив следующий код:

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';


* This source code was highlighted with Source Code Highlighter.


Необходимость определения свойств expanderClass, rowExpandedClass и rowCollapsedClass связана с тем, что теперь нам будет довольно легко оперировать их значениями внутри плагина, а нам еще предстоит внести некоторые изменения в его код.

Теперь у нас есть отдельные классы стилей для каждой уникальной сетки на нашей странице. Изменим методы, связанные с определением поведения плагина, чтобы они могли использовать предопределенные нами имена стилей:

Изменим метод render:

return '<div class="' + this.expanderClass + '"> </div>';

* This source code was highlighted with Source Code Highlighter.


Также изменим методы toggleRow:
this[Ext.fly(row).hasClass( this.rowCollapsedClass) ? 'expandRow' : 'collapseRow'](row);

* This source code was highlighted with Source Code Highlighter.


expandRow:
Ext.fly( row).replaceClass( this.rowCollapsedClass, this.rowExpandedClass);

* This source code was highlighted with Source Code Highlighter.


и collapseRow:
Ext.fly( row).replaceClass( this.rowExpandedClass, this.rowCollapsedClass);

* This source code was highlighted with Source Code Highlighter.


Все, на этом лечение описанной выше болезни плагина можно считать законченным.

Однако полное решение всех проблем с этим не приходит.

Ветки и листья дерева



Во первых, как и любое другое дерево, наше должно уметь правильным образом отражать «ветки» и «листья». Под этими понятиями я подразумеваю, что «ветка» может быть раскрыта (у нее есть потомки), а у листьев потомков нет. К сожалению, RowExpander в текущем состоянии не умеет отличать «листья» от «веток» и добавляет элементы управления открытием/закрытием во все строки «сетки». Для полноценной реализации дерева нам придется продолжить его менять.

Благо, решение не такое уж и сложное. Давайте остановимся на тезисе, что наш набор данных должен содержать признак того, является ли запись конечной («лист») или может содержать потомков («ветка»). Для этого достаточно определить в записи поле, например, с названием «is_leaf», принимающие логическое значение true или false (is_leaf = true — «лист», is_leaf = false — «ветка»).

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

Для реализации подобного механизма определим публичные конфигурационные свойства actAsTree = false и treeLeafProperty = 'is_leaf'. Таким образом, при инициализации плагина можно будет указывать, должен ли плагин вести себя как дерево (фактически проверять являются ли строки «сетки» листьями дерева), а также самостоятельно задавать имя признака «листа» в записи.

В первую очередь, нам необходимо определить еще один стиль для отображения листа (по-сути, он должен просто скрыть элемент открытия/закрытия на элементах с типом is_leaf = true):

Добавим, в уже созданное определение стилей:
var css =
  ...
  + '.x-grid-expander-leaf .x-grid3-row-expander { background: none; }'
;


* This source code was highlighted with Source Code Highlighter.


и определим свойство, которое бы мы могли использовать внутри плагина для данного имени класса стилей:
this.leafClass = 'x-grid-expander-leaf';

* This source code was highlighted with Source Code Highlighter.


Осталось дело за немногим, нужно проставить данный класс соответствующим строкам и запретить действия по открытию/закрытию строк, помеченных как листья.

Для этого изменим метод getRowClass (инициализация строк):
var cssClass = this.state[record.id] ? this.rowExpandedClass : this.rowCollapsedClass;
if (this.actAsTree && record.get( this.treeLeafProperty)) {
  cssClass = this.leafClass;
}
return cssClass;


* This source code was highlighted with Source Code Highlighter.


и добавим проверку в методы toggleRow, expandRow, collapseRow (запрещаем действия на листьях):
if (Ext.fly(row).hasClass( this.leafClass)) {
  return ;
}


* This source code was highlighted with Source Code Highlighter.


Проблема обработки событий



Даже после всего этого, проблемы остаются. Поведение вложенных друг в друга «сеток» оказывается довольно-таки неадекватным. Вы встретите глюки при выборе строк, сортировке и т.п. Действия на строках потомков будут проецироваться на родительские «сетки». Это все довольно неприятно, но очень легко решаемо!

На каждом вновь создаваемом потомке просто нужно отключить всплывающие события. Для этого, внесем очередное изменение в RowExpander в метод onRender, который в свою очередь выполняется в момент рендеринга самой «сетки».

if (this.actAsTree) {
  grid.getEl().swallowEvent([ 'mouseover', 'mouseout', 'mousedown', 'click', 'dblclick' ]);
});


* This source code was highlighted with Source Code Highlighter.


Проблема с утечками памяти



И еще не все проблемы решены :). Нам еще необходимо позаботиться об удалении всех компонентов связанных со схлопнутыми сетками. Нету смысла описывать детальные изменения, тем более, что в этой области все еще может измениться не один раз. Просто смотрите в код:

Полный код измененного плагина RowExpander: RowExpander.js
Живой пример вложенных сеток: ExtJS Nested Grids Example

Заключение



Невзирая на первичные трудности с построением вложенных сеток, и, казалось бы, столкнувшись с неприспособленностью «сеток» к вложенности, довольно несложным образом удалось добиться вполне приемлемого результата, что в очередной раз свидетельствует о гибкости и расширяемости библиотеки ExtJS.

Мне же лишь остается надеяться, что данная статья поможет кому-либо побороть трудности.

Удачи в девелопменте!

P.S. Это кросс-постинг оригинальной статьи "Nested Grids с помощью ExtJS 3.0", размещенной на моем личном блоге.
+8
11 декабря 2009, 20:51
28
Mikhus 100,9

комментарии (23)

+1
Etherial #
Исправьте пожалуйста ссылки кода плагина и примера на абсолютные пути, а то получается, что ссылки ведут на хабр :)
А пример понравился, очень здорово, нам обязательно пригодится.
0
Mikhus #
Спасибо, уже исправил :)
+1
Medal #
Заметил один небольшой баг. Если отсортировать вышестоящий грид, то вложенный закроется (если раскрыт), но минус не изменится на плюс.
0
Mikhus #
Спасибо
0
Mikhus #
Fixed
0
AstralMan #
Это жесть:
+3
AstralMan #
+2
Tronum #
Так вот ты какой «Дружественный пользовательский интерфейс»…
0
Mikhus #
Некоторые вещи хороши лишь в разумных пределах :) Ну а тут уж ваше право, если нравится — можете именно так и использовать.
+2
rSedoy #
а еще есть интересный плагин max-bazhenov.com/dev/ux.maximgb.tg/index.php там и «nested set» и «adjacency list»
0
Mikhus #
Офигительные плагины. Кинул себе в закладки. А можно у них задать разные дефинишины для колонок и полей на разных уровнях (количественно и по содержанию)?
0
pharod #
Нет, поскольку там единый datastore и единая columnmodel.
0
Mikhus #
Вот
+1
aleks_raiden #
перенесите, плиз, в блог по ExtJS, там нехватает хороших материалов
0
Mikhus #
о! а я его как-то сразу не заметил :)
0
mecommayou #
В 3.1 Этот компонент будет доступен из коробки через пару месяцев.
+1
mecommayou #
Еще бы в начало статьи сразу скриншот компонента.
0
pharod #
Не совсем этот: TreeGrid является расширением дерева, а не грида (загрузка данных будет через Ext.tree.TreeLoader, а не через Ext.data.Store).
+1
demongloom #
Есть несколько путей работы с древовидным-гридом в ExtJS. Правда не без плагинов.

Вариант 1.
Когда нужно дерево которое имеет колонки и может быть драг/дроп пересортировано.
Берем Tree, плагин ColumnTree и редактор ColumnTreeEditor.
pastebin.com/f33b993bc (мои измененные columntree и редактор).
Оригинальный плагин имел проблемы с редактированием более 1 поля.

Особенности:
Настоящее дерево. Для формирования нужны иерархические данные (многомерный массив). (http://pastebin.com/f48d4e6bf). Действия по перетасовке/перетаскиванию — родные. Гибко можно через события (onnodedrag/drop, etc) настроить логику перетаскивания.

Минусы:
Если много загрузить данных — будет рендерить все что есть, что может дать проблемы с производительностью. Но это особенность всех деревьев и их рендеринга.
Нет датастора для дерева. И соответственно обработка данных сложней без такого апи.

Вариант 2.
MaximGB tree grid.

Очень толковый плагин, но мне лично пришлось его пилить долго и упорно напильником для своего проекта. Позволяет делать древовидный грид. Есть датастор двух типов nested sets.
Поскольку с гридами в extjs работать легче чем с деревьями, то построение деревьев легче.

Огромный минус:
Писец медленный. При огромном кол-ве записей — можно застрелиться. Пришлось фиксить.

www.extjs.com/forum/showthread.php?p=380487#post380487
pastebin.com/f2fd44e13 — Еще одна моя версия для extjs 2.3, с рендерингом детей только при разворачивании родителя. Если чего то будет не хватать (ошибки), пишите в личку. Там есть пару внутренних вещей что я перегружал в extjs что-бы оно корректно работало.

Результат — стало просто летать в 10-15 раз быстрее.

Но в целом очень классная вещь, практически для 95% иерархических презентаций (списки сайтов по группам, списки продуктов по их инвентарной иерархии.) я использую этот вариант.

Вариант 3.
GroupingStore + GroupingView.

Хорош для аналитических простых списков. Типа: Грид, а грид? Нука, сгрупируй сайты мне тут по серверу. Видел варианты многоуровеневой группировки, но не использовал и ничего сказать не могу.

Минус:
Тоже медленный при больших обьемах строк. В качестве сравнения используется отрендеренная колонка (!), а не датафилд.

Тоже фиксы всякие:
pastebin.com/f7b0be3fe

Замечание:
Код представлен для версии 2.3. Не факт что он подойдет для третьей версии.

0
demongloom #
UPD:
Коды всех фиксов в заметке для версии 2.3.

«Поскольку с гридами в extjs работать легче чем с деревьями, то построение деревьев легче.»
Имел в виду что построение (заполнение данных, гламуризация внешнего вида и т.д.) грида с древовидным отображением используя плагин MaximGB tree grid легче, чем построение дерева используя Ext.Tree.

0
Mikhus #
Меня более интересует компонент в котором иерархия по сути не глубокая. А вот наботы данных разные на разных уровнях (кол-во колонок и их содержание). При этом глубина может отличаться в зависимости от условий (конфигурации). И необходимость иметь возможность фильтровать/сортировать группировать каждый уровень по отдельности. Классические деревья с этим не справляются. Простейший вариант — вложение гридов. Именно поэтому появился этот топик. А за линки — большое спасибо — информация полезная.
0
virusman #
Вот тоже интересный компонент — расширение GroupingView:
jaffa.sourceforge.net/JaffaRIATests/tests/extjs/multigroup/MultiGroup.html
0
demongloom #
Да это и есть тот самый мульти-групинг.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.