Делаем красивый список с GroupingStore/View и ExtJS
Сегодня мы поговорим, как сделать на базе ExtJS красивый (и функциональный) список каких-либо данных, например, список пользователей или групп. Я применяю такой список в одном из текущих проектов (правда, там уже не настолько красивый и удобный) Такой виджет можно использовать при выводе любых данных, которые характеризуются не только тестовой строкой, но и расширенными данными, а также необходимо сопоставить какие-то действия каждому набору. Допускается динамическое обновление данных (через Store), а также сортировка и группирование — в общем, все возможности, предоставляемые компонентом Grid из ExtJS. Сразу скажу, что я буду использовать версию ExtJS 3.0, но и в предыдущем релизе, 2.3.х пример также должен быть работоспособным. Приведенный компонент является примером и никак не готовым для использование кодом, а лишь демонстрацией возможностей, вы в своих проектах можете как угодно менять и дорабатывать под свои возможности. По этой же причине к статье намеренно нет исходного кода. Основным моментом, который я учитывал в этом компоненте — максимально сократить дистанцию между действиями, то есть, смотря на список пользователь должен получить сразу максимум нужной ему информации (в контексте списка, конечно), а если что нельзя отобразить, что информация о самом действии (возможности) должна быть также сразу видна. Именно поэтому я отказался от контекстного меню, вернее, продублировал действия иконками прямо в списке. Таким образом я устраняю необходимость в дополнительном действии ( открытие меню только для того, чтобы узнать ответ на вопрос «а что я могу еще сделать отсюда»), сразу показывая возможные действия. Исходя из этого же принципа в подсказки-тултипы вынесена только справочная информация, описывающая тот или иной пункт — она используется только если пользователь не может изначально понять назначение того или иного элемента и явно запрашивает подсказку путем наведения курсора.
Ладно, приступим к разработке. Как источник данных для списка мы будем использовать массив данных (считывая его через ArrayReader), а в качестве хранилища данных — Ext.data.GroupingStore. Этот стор позволяет использовать группировку данных по одному из полей, правда я столкнулся с интересной ситуацией с рендерингом групп, но об этом чуть позже. И так, мы передаем наши данные в стор, а он сортирует и группирует их по указанному критерию, в качестве которого используем одно из полей. Array как хранилище информации был выбран исходя из того, что мне не надо было прямое обновление данных с сервера, у нас этим занимается отдельный механизм, поэтому для простоты считаем, что все данные у нас уже есть локально.
- id группы, уникальный
- иконка группы по умолчанию (имя файла)
- имя группы
- количество пользователей в группе
- код группы, это для получения расширенной информации с дополнительного массива данных о группах
- описание группы
- var _user_groups = [
- [0, 'user_home.png' , 'Модераторы системы' ,2,
- 'system' , 'Служебная группа для модераторов и администраторов системы' ],
- [10, 'user_home.png' , 'Бизнес на играх' ,10, 'profy' , '' ],
- [20, 'user_home.png' , 'Игры под iPhone/iTouch' ,22, 'profy' , '' ],
- [30, 'user_home.png' , 'Разработка под FreeBSD' ,11, 'profy' , '' ],
- [40, 'user_home.png' , 'Первый филиал' ,20,
- 'filials' , 'Приватная группа московского филиала нашей компании' ],
- [50, 'user_home.png' , 'О политике и не только' ,120, 'publicusers' , '' ],
- [60, 'user_home.png' , 'Приколы на работе' ,99,
- 'publicusers' , 'Общая группа для развлечений, юмора и просто отдыха' ]
- ];
А так выглядит код подготовки Store. Мы задали код группы как поле для группировки, по нему потом из служебного массива получим остальные данные. К сожалению, есть некоторые сложности в работе с полем группировки — при рендеринге каждой строки скрипт будет переписывать заголовок группы и потому нам надо каждый раз его перезаписывать. Хотя есть механизм шаблонизации заголовка группы, но несмотря на примеры, у меня ничего не вышло, поэтому я переопределил функцию рендеринга заголовков вручную. Учитывайте также, что группировка возможна только по тому же полю, которое мы сортируем.
- var _usergroup_store = new Ext.data.GroupingStore( );
Также нам понадобится дополнительный источник данных о группах. Я реализовал его в виде объекта, где по коду группы можно получить ее описание, иконку и другие данные. Это, конечно, все можно было включить в исходный массив данных, однако эти же данные будут использовать в других местах, поэтому они вынесены отдельно.
- var _user_groups_cats =
- >
Кроме обычных параметров групп, у меня есть два дополнительный — является ли группа закрытой и приватной. У вас могут быть любые другие произвольные параметры. Они используются для того, чтобы для каждой группы вывести нужные иконки и действия, основываясь на параметрах. То есть, если группа закрытая, то незачем выводить иконку с действием «присоединиться к группе» и т.п.
Дополнительно, я храню в массиве коды групп, в которые входит текущий пользователь:
Теперь у нас есть все данные для рендеринга нашего списка. Для этого мы используем компонент Ext.grid.GridPanel, хотя его функциональность, конечно, немного избыточная для такого примера. Лучше было бы использовать что-то типа ListView, возможно, в следующей статье я попробую переделать все под него, но пока попробуем через Grid. Иначе бы пришлось придумывать самостоятельную реализацию группировки, да и будущее расширение было бы под вопросом. Так что вам придется рассматривать свой случай лично — если вам не надо все возможности именно грида, попробуйте использовать облегченный и быстрый ListView, однако там будет намного больше ручной работы.
И так, создаем компонент Ext.grid.GridPanel, в качестве источника данных используем заранее созданный нами Ext.data.GroupingStore (_usergroup_store). Основной функционал сосредоточен в описании колонок и их рендерах, о чем мы сейчас и поговорим более детально. Сначала я покажу вам описание колонок без рендеров (рендер или renderer — специальный метод, возвращающий произвольный текст или код html, который выводится в качестве значения в ячейке).
- columns: [ , , , ]
Как видим, группировка происходит по последнему полю — колонке group_type, которая использует данные с той же колонки в сторе (обычно я использую одинаковые названия колонок, потому id колонки совпадает с dataIndex). Но нам нет необходимости отображать эту колонку, она чисто служебная, для группировки, поэтому мы ставим параметр hidden:true, а также указываем, что мы хотим группировать по этому полю — groupable: true.
Существует два типа рендеров для каждой колонки — renderer и groupRenderer. Первый отвечает за отображение данных в ячейках, второй используется для отображения заголовка группы. Здесь кроется одна существенная сложность (хотя, видимо, это просто странное архитектурное решение разработчиков). Заголовок группы используется в качестве имени стиля этого элемента, и если мы переопределим рендер группы, например, добавив html-код, то весь этот текст пойдет в идентификатор стиля группы. Ниже показан скриншот из Firebug для иллюстрации этого момента. Я также вполне допускаю, что не до конца разобрался с механизмом группирования, так что если вы можете дополнить или поправить меня — буду благодарен.
Приступим к описанию вывода каждой колонки. Перед названием группы у нас может быть одна или две иконки. Зеленая точка показывает, открыта ли группа — если она есть, значит группа общедоступная, или же она приватная или закрытая, выводится другая иконка. Вы можете использовать собственные атрибуты групп и выводить другую информацию. Каждая иконка имеет собственный тултип, добавляемый через разметку (часто так удобнее).
- renderer: function (obj, x, y)
- if (tmp.isPrivate == true )
- if ((tmp.isClosed == false ) && (tmp.isPrivate == false ) )
- // обязательно вернуть результат
- return src + ' ' + obj + '</span>' ;
- >
Подробнее о ренденре и способах его задания читайте в официальной документации. В метод передаются различные данные, нас интересует только начальное значение (первый параметр), мата-данные о html-объекте, куда рендерится значение, а также объект текущей записи (третий параметр).
Вторая колонка у нас — количество участников в каждой группе. Но если группа приватная, необходимо скрыть эти данные, поэтому мы в рендере проверяем тип группы и выводит необходимое значение.
- renderer: function (obj, x, y)
Самой интересной будет третья и последняя колонка, куда мы выведем иконки-кнопки для пользовательских действий, при этом их набор будет зависеть от параметров группы. Правда, есть пользовательские расширения, RowActions и CellActions, но сегодня мы сделаем то же самое вручную (честно, я просто сначала сделал, а потом нашел эти расширения, поэтому не стал уже переделывать готовый код).
- renderer: function (obj, x, y)
- if ((tmp.isPrivate == false ) && (tmp.isClosed == false ))
- // проверю, состоит ли юзер в группе
- if (_i_in_group.indexOf(y.data.id) != -1)
- return src;
- >
Далее у нас есть скрытая колонка, по которой идет группирование. Для нее мы переопределим рендер группы, чтобы вывести красивый заголовок. Рендер получает одно значение, то поле, которое объявлено как условие для группировки, в нашем случае, это код группы, по которому мы получим уже всю остальную информацию.
- groupRenderer: function (group)