Как стать автором
Обновить

Делаем красивый список с GroupingStore/View и ExtJS

Время на прочтение11 мин
Количество просмотров6K
Сегодня мы поговорим, как сделать на базе ExtJS красивый (и функциональный) список каких-либо данных, например, список пользователей или групп. Я применяю такой список в одном из текущих проектов (правда, там уже не настолько красивый и удобный) Такой виджет можно использовать при выводе любых данных, которые характеризуются не только тестовой строкой, но и расширенными данными, а также необходимо сопоставить какие-то действия каждому набору. Допускается динамическое обновление данных (через Store), а также сортировка и группирование — в общем, все возможности, предоставляемые компонентом Grid из ExtJS. Сразу скажу, что я буду использовать версию ExtJS 3.0, но и в предыдущем релизе, 2.3.х пример также должен быть работоспособным. Приведенный компонент является примером и никак не готовым для использование кодом, а лишь демонстрацией возможностей, вы в своих проектах можете как угодно менять и дорабатывать под свои возможности. По этой же причине к статье намеренно нет исходного кода.

Основным моментом, который я учитывал в этом компоненте — максимально сократить дистанцию между действиями, то есть, смотря на список пользователь должен получить сразу максимум нужной ему информации (в контексте списка, конечно), а если что нельзя отобразить, что информация о самом действии (возможности) должна быть также сразу видна. Именно поэтому я отказался от контекстного меню, вернее, продублировал действия иконками прямо в списке. Таким образом я устраняю необходимость в дополнительном действии ( открытие меню только для того, чтобы узнать ответ на вопрос «а что я могу еще сделать отсюда»), сразу показывая возможные действия. Исходя из этого же принципа в подсказки-тултипы вынесена только справочная информация, описывающая тот или иной пункт — она используется только если пользователь не может изначально понять назначение того или иного элемента и явно запрашивает подсказку путем наведения курсора.

Ладно, приступим к разработке. Как источник данных для списка мы будем использовать массив данных (считывая его через ArrayReader), а в качестве хранилища данных — Ext.data.GroupingStore. Этот стор позволяет использовать группировку данных по одному из полей, правда я столкнулся с интересной ситуацией с рендерингом групп, но об этом чуть позже. И так, мы передаем наши данные в стор, а он сортирует и группирует их по указанному критерию, в качестве которого используем одно из полей. Array как хранилище информации был выбран исходя из того, что мне не надо было прямое обновление данных с сервера, у нас этим занимается отдельный механизм, поэтому для простоты считаем, что все данные у нас уже есть локально.

Исходные данные имеют следующую структуру:
  • id группы, уникальный
  • иконка группы по умолчанию (имя файла)
  • имя группы
  • количество пользователей в группе
  • код группы, это для получения расширенной информации с дополнительного массива данных о группах
  • описание группы


  1. var _user_groups = [
  2.    [0,'user_home.png','Модераторы системы',2,
  3. 'system','Служебная группа для модераторов и администраторов системы'],
  4.   [10,'user_home.png','Бизнес на играх',10,'profy',''],
  5.   [20,'user_home.png','Игры под iPhone/iTouch',22,'profy',''],
  6.   [30,'user_home.png','Разработка под FreeBSD',11,'profy',''],
  7.   [40,'user_home.png','Первый филиал',20,
  8. 'filials','Приватная группа московского филиала нашей компании'],
  9.   [50,'user_home.png','О политике и не только',120,'publicusers',''],
  10.   [60,'user_home.png','Приколы на работе',99,
  11. 'publicusers','Общая группа для развлечений, юмора и просто отдыха']
  12. ];
* This source code was highlighted with Source Code Highlighter.


А так выглядит код подготовки Store. Мы задали код группы как поле для группировки, по нему потом из служебного массива получим остальные данные. К сожалению, есть некоторые сложности в работе с полем группировки — при рендеринге каждой строки скрипт будет переписывать заголовок группы и потому нам надо каждый раз его перезаписывать. Хотя есть механизм шаблонизации заголовка группы, но несмотря на примеры, у меня ничего не вышло, поэтому я переопределил функцию рендеринга заголовков вручную. Учитывайте также, что группировка возможна только по тому же полю, которое мы сортируем.

  1. var _usergroup_store = new Ext.data.GroupingStore({
  2.   reader: new Ext.data.ArrayReader({},
  3.      [{
  4.     name: 'id',
  5.     type: 'int'
  6.    }, {
  7.      name: 'group_icon',
  8.     type: 'string'
  9.    }, {
  10.     name: 'group_name',
  11.     type: 'string'
  12.    }, {
  13.     name: 'count_users',
  14.     type: 'string'
  15.    }, {
  16.     id:'group_type',
  17.     name: 'group_type',
  18.     type: 'string'
  19.    }, {
  20.     name: 'group_desc',
  21.     type: 'string'
  22.    }]),
  23.    data: _user_groups,
  24.    sortInfo: {
  25.       field: 'group_type',
  26.       direction: "ASC"
  27.    },
  28.    groupField: 'group_type',
  29.    groupOnSort: true
  30. });
* This source code was highlighted with Source Code Highlighter.


Также нам понадобится дополнительный источник данных о группах. Я реализовал его в виде объекта, где по коду группы можно получить ее описание, иконку и другие данные. Это, конечно, все можно было включить в исходный массив данных, однако эти же данные будут использовать в других местах, поэтому они вынесены отдельно.

  1. var _user_groups_cats = {
  2.    system:{
  3.     icon:'user_home.png',
  4.     title:'Системные группы',
  5.     desc:'Служебная группа только для сотрудников Wheemplay Ltd.',
  6.     isClosed:true,
  7.     isPrivate:false
  8.   },
  9.   profy:{
  10.     icon:'user_star.png',
  11.     title:'Профессиональные группы',
  12.     desc:'Группы по интересам',
  13.     isClosed:false,
  14.     isPrivate:false
  15.   },
  16.   filials:{
  17.     icon:'user_earth.png',
  18.     title:'Группы филиалов',
  19.     desc:'Частные группы филиалов',
  20.     isClosed:false,
  21.     isPrivate:true
  22.   },
  23.   publicusers:{
  24.     icon:'group.png',
  25.     title:'Общие пользовательские',
  26.     desc:'Общедоступные группы, куда может войти любой желающий',
  27.     isClosed:false,
  28.     isPrivate:false
  29.   },
  30.   my:{
  31.     icon:'user_comment.png',
  32.     title:'<strong>Мои группы</strong>',
  33.     desc:'Мои группы (в которых вы состоите)',
  34.     isClosed:true,
  35.     isPrivate:true
  36.   }
  37. }
* This source code was highlighted with Source Code Highlighter.


Кроме обычных параметров групп, у меня есть два дополнительный — является ли группа закрытой и приватной. У вас могут быть любые другие произвольные параметры. Они используются для того, чтобы для каждой группы вывести нужные иконки и действия, основываясь на параметрах. То есть, если группа закрытая, то незачем выводить иконку с действием «присоединиться к группе» и т.п.

Дополнительно, я храню в массиве коды групп, в которые входит текущий пользователь:
var _i_in_group = [0, 30, 60];

Теперь у нас есть все данные для рендеринга нашего списка. Для этого мы используем компонент Ext.grid.GridPanel, хотя его функциональность, конечно, немного избыточная для такого примера. Лучше было бы использовать что-то типа ListView, возможно, в следующей статье я попробую переделать все под него, но пока попробуем через Grid. Иначе бы пришлось придумывать самостоятельную реализацию группировки, да и будущее расширение было бы под вопросом. Так что вам придется рассматривать свой случай лично — если вам не надо все возможности именно грида, попробуйте использовать облегченный и быстрый ListView, однако там будет намного больше ручной работы.

И так, создаем компонент Ext.grid.GridPanel, в качестве источника данных используем заранее созданный нами Ext.data.GroupingStore (_usergroup_store). Основной функционал сосредоточен в описании колонок и их рендерах, о чем мы сейчас и поговорим более детально. Сначала я покажу вам описание колонок без рендеров (рендер или renderer — специальный метод, возвращающий произвольный текст или код html, который выводится в качестве значения в ячейке).

  1. columns: [{
  2.     id: 'group_name',
  3.     header: "<strong>Группы</strong>",
  4.     sortable: false,
  5.     width: 150,
  6.     dataIndex: 'group_name'
  7.      }, {
  8.     header: "Участников",
  9.     width: 35,
  10.     hidden:false,
  11.     sortable: true,
  12.     dataIndex: 'count_users'
  13.        },{
  14.     header: "действия",
  15.     width: 60,
  16.     hidden:false,
  17.     sortable: false,
  18.     dataIndex: 'count_users'
  19.      }, {
  20.     id: 'group_type',
  21.     dataIndex: 'group_type',
  22.     hidden: true,
  23.     groupable: true
  24. }]
* This source code was highlighted with Source Code Highlighter.


Как видим, группировка происходит по последнему полю — колонке group_type, которая использует данные с той же колонки в сторе (обычно я использую одинаковые названия колонок, потому id колонки совпадает с dataIndex). Но нам нет необходимости отображать эту колонку, она чисто служебная, для группировки, поэтому мы ставим параметр hidden:true, а также указываем, что мы хотим группировать по этому полю — groupable: true.

Существует два типа рендеров для каждой колонки — renderer и groupRenderer. Первый отвечает за отображение данных в ячейках, второй используется для отображения заголовка группы. Здесь кроется одна существенная сложность (хотя, видимо, это просто странное архитектурное решение разработчиков). Заголовок группы используется в качестве имени стиля этого элемента, и если мы переопределим рендер группы, например, добавив html-код, то весь этот текст пойдет в идентификатор стиля группы. Ниже показан скриншот из Firebug для иллюстрации этого момента. Я также вполне допускаю, что не до конца разобрался с механизмом группирования, так что если вы можете дополнить или поправить меня — буду благодарен.



Приступим к описанию вывода каждой колонки. Перед названием группы у нас может быть одна или две иконки. Зеленая точка показывает, открыта ли группа — если она есть, значит группа общедоступная, или же она приватная или закрытая, выводится другая иконка. Вы можете использовать собственные атрибуты групп и выводить другую информацию. Каждая иконка имеет собственный тултип, добавляемый через разметку (часто так удобнее).

  1. renderer:function(obj, x, y)
  2. {
  3. var src = '<span style="cursor:pointer;">';
  4. //для упрощения
  5. var tmp = _user_groups_cats[y.data.group_type];
  6.  
  7. if (tmp.isClosed == true)
  8. {
  9.  src = src +
  10. '<img src="/images/icons/bullet_error.png" alt="" align="absmiddle" /> ';
  11. }
  12.  
  13. if (tmp.isPrivate == true)
  14. {
  15.  src = src +
  16. ' <img src="/images/icons/bullet_key.png" alt="" align="absmiddle" /> ';
  17. }
  18.  
  19. if ((tmp.isClosed == false) && (tmp.isPrivate == false)  )
  20. {
  21.  src = src +
  22.  ' <img src="/images/icons/bullet_green.png" alt="" align="absmiddle" /> ';
  23. }
  24.  
  25. // обязательно вернуть результат
  26. return src + ' ' + obj + '</span>';
  27. }
* This source code was highlighted with Source Code Highlighter.


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

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

  1. renderer:function(obj, x, y)
  2. {
  3.  if (_user_groups_cats[y.data.group_type].isPrivate == false)
  4. {
  5.    return obj + ' учасн.';
  6. }
  7. else
  8.   return '<em>скрыто</em>';
  9. }
* This source code was highlighted with Source Code Highlighter.


Самой интересной будет третья и последняя колонка, куда мы выведем иконки-кнопки для пользовательских действий, при этом их набор будет зависеть от параметров группы. Правда, есть пользовательские расширения, RowActions и CellActions, но сегодня мы сделаем то же самое вручную (честно, я просто сначала сделал, а потом нашел эти расширения, поэтому не стал уже переделывать готовый код).

  1. renderer:function(obj, x, y)
  2. {
  3. var src = '';
  4. var tmp = _user_groups_cats[y.data.group_type];
  5.  
  6. src = src + '<img style="cursor:pointer;" ' +
  7.   'src="/images/icons/vcard.png" alt="" align="absmiddle" /> ';
  8.  
  9. // если группа не приватная и юзер в ней не состоит
  10. if ((tmp.isPrivate == false) && (_i_in_group.indexOf(y.data.id) == -1))
  11. {
  12.  src = src + '<img style="cursor:pointer;" ' +
  13.  'src="/images/icons/user_add.png" alt="" align="absmiddle" /> ';
  14. }
  15.  
  16. if ((tmp.isPrivate == false) && (tmp.isClosed == false))
  17. {
  18. src = src + '<img style="cursor:pointer;" ' +
  19. 'src="/images/icons/group.png" alt="" align="absmiddle" /> ';
  20. }
  21.  
  22. // проверю, состоит ли юзер в группе
  23. if (_i_in_group.indexOf(y.data.id) != -1)
  24. {
  25. src = src + '<img style="cursor:pointer;" ' +
  26. 'src="/images/icons/user_delete.png" alt="" align="absmiddle" /> ' +
  27. '<img style="cursor: pointer;" src="/images/icons/comments.png" alt="" align="absmiddle" /> ';
  28. }
  29.  
  30.   return src;
  31. }
* This source code was highlighted with Source Code Highlighter.


Далее у нас есть скрытая колонка, по которой идет группирование. Для нее мы переопределим рендер группы, чтобы вывести красивый заголовок. Рендер получает одно значение, то поле, которое объявлено как условие для группировки, в нашем случае, это код группы, по которому мы получим уже всю остальную информацию.

  1. groupRenderer:function(group)
  2. {
  3.  return '<span><img src="/images/icons/' + _user_groups_cats[group].icon +
  4.      '" alt="" align="absmiddle" /> ' + _user_groups_cats[group].title +
  5.      '</span>';
  6. }
* This source code was highlighted with Source Code Highlighter.


Остался последний штрих, необходимо нашей таблице указать необходимый View, в нашем случае — GroupingView.

  1. view: new Ext.grid.GroupingView({
  2.     forceFit: true,
  3.     enableNoGroups: false,
  4.     autoFill: true,
  5.     scrollOffset:0,
  6.     showGroupName: false,
  7.     groupTextTpl: '{text}'
  8. })
* This source code was highlighted with Source Code Highlighter.


Мы указали, что не может быть строк без групп, а также запретили показ названия группы, так как мы переопределили рендер группы. groupTextTpl — здесь указывается шаблон для вывода названия группы, но с ним есть сложности, так как достаточно сложно описать внутри шаблона условия и обработку глобальных значений. Поэтому мы просто выводим то, что вернула наша функция рендера и все, без какой-либо обработки.

Вот и все, в результате мы получим красивый группированый список. Для реализации пользовательских действий необходимо повесить обработчики на иконки, но с этим, думаю, вы справитесь и сами. Замечу также, что в примере намеренно скрыт заголовок таблицы, но если он включен (через конфиг грида), можно будет сортировать данные (по тех колонках, что объявлены как sortable) путем клика на заголовке. При этом группировка будет сохраняться, то есть, сортироваться будут данные внутри каждой группы индивидуально.

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


Теги:
Хабы:
Всего голосов 31: ↑28 и ↓3+25
Комментарии27

Публикации