Сложные интерфейсы на javascript вместе Yahoo UI. Часть 15

December 29, 2008

В своеобразный “джентльменский набор” любой javascript-библиотеки, предназначенной для проектирования “богатых” пользовательских интерфейсов, входит компоненты для отображения табличных данных. Конечно, для большинства “домашних” сайтов нет необходимости отображать большие объемы информации в виде таблиц. Но для бизнес-приложений (учет товаров и их движения, кадровый учет …) таблицы обязательны. И не просто таблицы, а удобные: с поддержкой сортировки информации по клику на заголовке столбца, с поддержкой изменения широты столбцов, с возможностью гибко настроить внешний вид вплоть до каждой отдельной ячейки.

Сегодняшняя статья в серии будет посвящена компоненту DataTable. Однако, перед тем, как я приведу примеры кода, создающего таблицу, нужно задуматься о том, как и откуда берутся данные, отображаемые в таблице. По прошлым статьям серии нам уже знакомо, что YUI активно использует концепцию отделения информации (ее поставщика) от средств визуализации. Так, когда я рассказывал о компоненте AutoComplete (текстовое поле с выпадающим списком подсказок), то упоминал, что информацию для AutoComplete поставляли специализированные классы DataSource-ы. Которые использовали информацию как размещенную на самой странице (массивы JSON объектов), так и умели загружать ее в различных форматах с сервера (XML, JSON, CSV). В каждом из этих случаев мы должны были создать специальный объект “дескриптор”, который описывал структуру данных. Т.е. правила, по которым из XML-документа, объекта JSON, извлекались лишь те фрагменты информации, которые и нужны были для отображения в AutoComplete. Такой экскурс в историю полезен, т.к. почти все идеи, изученные тогда, пригодятся и при изучении DataTable. Начнем с простого примера, в котором данные для таблицы находятся на самой html-странице в виде массива JSON. Первым шагом я использую давно знакомый нам модуль yui-loader для загрузки кода класса DataTable (модуль datatable). Затем создаю в теле html-страницы блок div, который будет играть роль контейнера, в который yui поместит сгенерированный html-код таблицы.
  1. <div id="tableplaceholder"></div>
Теперь нужно определить JSON-массив с данными (каждая запись содержит сведения о сотруднике и включает поля ФИО, дата рождения, пол):
  1. var kadry = [ 
  2.   {fio:"Jim", birthdate:new Date(1999, 1, 1), sex: "male"}, 
  3.   {fio:"Janet", birthdate:new Date(1999, 2, 2), sex: "female"}, 
  4. ];
Теперь этот массив объектов подается на вход конструктору класса YAHOO.util.DataSource. Этот класс является универсальным поставщиком данных. Поэтому я сразу же после создания объекта указываю его тип (свойство responseType) и формат данных (свойство responseSchema):
  1. var ds = new YAHOO.util.DataSource(kadry); 
  2. ds.responseType = YAHOO.util.DataSource.TYPE_JSARRAY; 
  3. ds.responseSchema = {fields: ["fio","birthdate","sex"]};
Для того, чтобы данные отобразить в виде таблицы нужен еще шаг: описать то, как поля данных (responseSchema) должны быть отображены на визуальные колонки. Т.е. какие должны быть заголовки столбцов, поддерживается ли сортировка данных при клике на заголовке столбца, и, самое важное, правила форматирования содержимого ячейки. Так, вторая колонка (birthdate) задает дату рождения сотрудника. Очевидно, что дату можно форматировать по-разному: год-месяц-день, день-месяц-год, также мы можем использовать для записи года две цифры или четыре. Итак, следующим шагом я создаю массив колонок таблицы и их свойств:
  1. var columns = [ 
  2.   {key:"fio", sortable:true, resizeable:true, sortOptions:{defaultDir:YAHOO.widget.DataTable.CLASS_DESC}}, 
  3.   {key:"birthdate", formatter:YAHOO.widget.DataTable.formatDate, sortable:true ,resizeable:true}, 
  4.   {key:"sex",  sortable:true, resizeable:true}, 
  5. ];
Свойство key совпадает с именем какого-либо из полей записи, остальные же свойства управляют внешним видом и поведением колонки. Назначение свойств, показанных в примере выше, очевидно из их названия: resizeable – разрешает пользователю изменять ширину колонки, sortable – включает режим сортировки, а sortOptions управляют всем, что связано с сортировкой данного столбца, и состоит из еще нескольких свойств. Название defaultDir может ввести в заблуждение, и может показаться, что в примере выше таблица будет изначально (при первом отображении) уже отсортирована по убыванию поля ФИО, но это не так. Когда я выполняю клик по любой из колонок таблицы, содержимое сортируется по-возрастанию значений в этой колонки. Параметр же defaultDir переопределяет это правило и данные сортируются по-убыванию (в любом случае, повторные ”клики” по столбцу будут “переворачивать” направление сортировки). DataTable позволяет указать специальную функцию, принимающую решения о правилах сравнения двух записей. Это нужно не только в тех случаях, когда DataTable содержит большее количество столбцов, чем полей записи (т.е. часть колонок являются виртуальными или вычисляемыми), но и когда правила сортировки по полю являются сложными. Например, при сравнении строк нам то нужно, а то не нужно учитывать регистр. Или, сортируя поля с единым ФИО сотрудников, мы хотим выделить из строки ФИО только отдельную часть (только имя или только отчество) и сравнение выполнять именно по ней. Так я и сделаю в следующем примере (с помощью метода split разбиваю строку ФИО сотрудника на части):
  1. function sortByFIO(rec_a, rec_b, desc) {  
  2.  surname_a = rec_a.getData("fio").split(" ");
  3.  surname_b = rec_b.getData("fio").split(" ");
  4.  return YAHOO.util.Sort.compare (surname_a[2], surname_b[2]); 
  5. };
Функция пользовательской сортировки получает в качестве первых двух параметров ссылки на пару сравниваемых записей. Именно записей, а не значений отдельных полей, т.е. вы можете выполнять сортировку на основании сравнения более чем одного поля: при клике на колонке с названием отдела, мы можем выполнить сортировку отделов по алфавиту, а затем в рамках каждого из отделов – еще одну сортировку, уже по ФИО сотрудника. Третий параметр функции сравнения – это булева переменная, признак того, что сортировка будет идти в порядке убывания (если переменная равна false, то сортировка идет по возрастанию). Результатом работы функции будет одно из трех чисел: -1 в случае если “a” < “b”, 0 – когда записи “a” и “b” равны, и +1 – когда “b” < “a”. Конечно, все это верно только, если “флажок” desc “выключен”, в противном случае результаты сравнения нужно инвертировать. Записи, которые были изначально поданы на вход конструктору класса DataSource, YUI поместил внутрь специального класса “запись”. Устройство этого класса примитивно и может показаться, что в нем есть только пара методов getData и setData (оба принимают в качестве параметров имя поля). Однако, кроме этого yui каждой из записей назначает уникальный идентификатор (получить ее значение можно, вызвав метод getId). Этот идентификатор полностью совпадает с идентификатором, назначаемым строке “tr” таблицы (“yui-rec2”). А, учитывая, что этот идентификатор остается неизменным при любых сортировках (и назначается на стадии загрузки данных), то можно создать сортировку, которая “сбрасывает” порядок записей в начальное состояние, совпадающее с тем, в котором они были загружены с сервера. Особое внимание обратите на то, что я финальную стадию сравнения (само сравнение фамилий) делегировал методу YAHOO.util.Sort.compare. Этот метод используется самим YUI для сортировки полей, когда вы не предоставили собственную функцию сравнения. И сделал я это не только из желания написать код на пару строк короче. Всегда в программировании нужно учитывать нестандартные ситуации: так значение ФИО для какого-то из сотрудников могло не содержать три компонента имени, отчества и фамилии. Значит мне нужно проверить, что значение surname_a[2] существует, а если это не так, то расположить записи с такими сотрудниками либо самыми первыми, либо в конце списка. Подобную проверку и делает метод YAHOO.util.Sort.compare (с учетом значения “флажка” desc). Последний шаг - назначать функцию сравнения sortByFIO полю ФИО:
  1. var columns = [ 
  2.  {key:"fio", sortable:true, resizeable:true, sortOptions: {sortFunction: sortByFIO} }, 
  3. ];
Теперь осталось только завершить создание DataTable, вызвав его конструктор и передав как параметры ссылки на ранее созданный html-блок “tableplaceholder”, на массив с описаниями колонок и на источник данных (DataSource):

var table = new YAHOO.widget.DataTable("tableplaceholder", columns, ds, {caption:"Employes"});

Внешний вид получившейся таблицы показан на рис. 1.



Первое улучшение, которое я внесу, – это заменю названия заголовков столбцов. По-умолчанию заголовки столбцов равны именам соответствующих полей записи. Если же это не так, то укажите название столбца как значение свойства label:
  1. var columns = [
  2.   {label: "ФИО", key:"fio" },
  3.   ....
Для того, чтобы управлять внешним видом колонок можно использовать css-стили. В следующем примере я создам два css-стиля:
  1. <style>
  2.  .red_column{ color: red;}
  3.  .green_column{ color: green;}
  4. </style>
А теперь осталось только указать их имена при создании определений для колонок DataTable:
  1. var columns = [ 
  2.   {className: "red_column", key:"fio", sortable:true, resizeable:true}, 
  3.   {className: "green_column", key:"birthdate"} 
  4. ];
Для того, чтобы задать начальную ширину колонки, используйте свойство width, а свойство minWidth служит для того, чтобы ограничить действия пользователя, не дав ему уменьшить ширину колонки (если, конечно, для нее включен режим resizeable) меньше допустимой величины (по-умолчанию это 10px). Если же, изменение оформления для всей колонки кажется слишком “грубым” средством, то можно воспользоваться приемом, показанным еще в статье посвященной календарю. Так yui сгенерировал уникальные стили для каждой из строк таблицы и для каждой из ячеек строки. К примеру, первая запись (тег “tr”) имеет идентификатор yui-rec0 (цифра 0 для первой строки, цифра 1 – для второй …). Также нулевой строке назначен css-класс “yui-dt-even” (для четных строк – even, для нечетных - odd). Для того, чтобы назначить особое оформление для первой и последней из строк таблицы, создайте или переопределите css-стили “yui-dt-first” и “yui-dt-last”. Для того, чтобы узнать идентификаторы и css-стили для всех частей сгенерированной DataTable я рекомендую воспользоваться плагином к firefox firebug.

Следующим шагом “навести немножко блеску” будет создание функции форматирования содержимого ячейки. В примере выше для каждого из сотрудников было задано поле дня рождения. Для того, чтобы управлять форматированием этой даты, я назначил специальную функцию:
  1. formatter: YAHOO.widget.DataTable.formatDate
Кроме formatDate в yui есть еще парочка функций, некоторые из них умеют формировать внешний вид ячейки таблицы состоящий не только из текстовой надписи, но и из таких элементов управления, как checkbox, select, также можно управлять параметрами форматирования. Так в примере выше я нигде явно не указывал то, какой формат даты нужно использовать, а YUI самостоятельно выбрал формат “месяц/день/год”. Давайте поменяем его на более привычный нам “день.месяц.год”. Формат можно установить как на стадии создания таблицы, так и в любой последующим момент, например, при нажатии на кнопку:
  1. table.set ('dateOptions', {format:" %Y.%m.%d ", locale:"en"} )
  2. table.render ();
Первым шагом я установил как значение конфигурационной переменной dateOptions и формат даты, и используемую локаль. Метод render необходим для того, чтобы перерисовать таблицу, в противном случае внешний вид ячеек с датой не изменится. Символы “%m”, “%Y”, которые я использовал, не берутся из ниоткуда: я советую обратиться к исходным кодам YUI модуля datasource. В нем есть настоящий справочник того, какие специальные символы есть и что они обозначают. Также внутри модуля datasource определяется класс YAHOO.util.DateLocale с правилами форматирования даты и ее компонент. От этого класса были наследованы классы локалей для ‘en’, ‘en-US’, ‘en-GB’, ‘en-AU’. Так, что если я захочу задать формат даты, в котором выводится месяц не цифрами, а полным названием на русском языке, то необходимо создать свой класс локали, к счастью это очень просто (тут же выполняется и регистрация новой локали, так чтобы YUI мог в любой момент ее найти):
  1. YAHOO.util.DateLocale['ru'] = YAHOO.lang.merge(YAHOO.util.DateLocale, {
  2.     a: ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб'],
  3.     A: ['Воскресенье', 'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота'],
  4.     b: ['Янв', 'Фев', 'Март', 'Апр', 'Май', 'Июнь', 'Июль', 'Август', 'Сен', 'Окт', 'Ноябрь', 'Декабрь'],
  5.     B: ['Январь', 'Февраль', 'Март', 'Апрель', 'Май', 'Июнь', 'Июль', 'Август', 'Сентябрь', 'Октябрь', 'Ноябрь', 'Декабрь'],
  6.     c: '%Y.%b.%d %H:%M (%a)',
  7.     p: ['', ''], P: ['', ''],
  8.     x: '%Y.%m.%d',
  9.     X: '%H %M' 
  10.   }
  11. );
За что отвечают такие поля класса локали как “a”, “A”, “b”, “B” очевидно из примера. Что касается, поля “c”, то оно кодирует традиционный для страны способ записи и даты и времени. Поле “x” кодирует формат, состоящий только из даты, без времени, а поле “X” – наоборот, только время, без даты. Что касается “p” и “P”, то они используются в англоговорящих странах для кодирования букв “AM”, “PM”. Т.к. в нашей стране особое обозначение для времени “до и после полудня” не используются, то я оставил значения свойств “p” и “P” пустыми. Теперь снова попробуем задать формат столбца с датой. То, что получится вы можете увидеть на рис. 2.


  1. table.set ('dateOptions', {format:"%c", locale:"ru"} )
  2. table.render ();
Кроме форматирования дат, YUI предоставляет встроенные средства форматирования чисел. Например, если я добавлю в список полей описывающих сотрудника поле с размером его зарплаты:
  1. var kadry = [ 
  2. {fio:"Jim Manna A", salary: 1000.724}, 
  3. ]
Теперь, при создании массива объектов, описывающий внешний вид колонок таблицы, я указываю, что поле salary будет форматироваться как число (не забудьте упомянуть про новое поля, когда задается значение свойства responseSchema для источника данных DataSource):
  1. var columns = [ 
  2.  {key:"salary",  sortable:true, resizeable:true, formatter:YAHOO.widget.DataTable.formatCurrency}, 
  3. ];
  4.  
  5. ds.responseSchema = {fields: ["fio","birthdate","sex", "salary"] };
Последним шагом, будет указание того, как выполняется форматирование чисел. Мне нужно указать символ валюты (размещается перед самим числом), затем символ, используемый для разделения целой и дробной части числа, количество знаков для представления этой дробной части, и символ-разделитель тысячных разрядов целой части:
  1. table.set("currencyOptions", {prefix: ' $ ', decimalPlaces:2, decimalSeparator:",", thousandsSeparator:"`" });
Из простых функций форматирования в YUI можно упомянуть formatEmail и formatLink. Обе они помещают входное значение внутрь тега “a”. Так что клик пользователя по ячейке приведет к переходу браузера по новому адресу, или открытию окошка почтового клиента. Еще столь же бесполезным является функция, представляющая значение ячейки в виде кнопки (YAHOO.widget.DataTable.formatButton), checkbox-а (YAHOO.widget.DataTable.formatCheckbox) или в виде радио-кнопки (YAHOO.widget.DataTable.formatRadio). Почему я говорю, что функции бесполезны? Все дело в том, что хотя и кнопки, и checkbox-ы широко используются при построении интерфейса, но требуют точной настройки своего внешнего вида и поведения. Так если вам нужно создать радиокнопку для выбора одной строки из многих, или кнопку, которая вызывает какую-то специальную функцию (да хоть открытия диалогового окошка со сведениями о выбранном сотруднике), то встроенных возможностей YUI уже не хватает. И нам приходится при описании объекта колонки таблицы уже ссылаться на свою собственную функцию форматирования, например, так:
  1. // функция форматирования
  2. function custom_format(el, oRecord, oColumn, oData) {
  3.   el.innerHTML = "<input type='radio' name='"+oColumn.key+"' value='"+oData+"' />" + oData;
  4. }
  5. // и пример ее назначения столбцу таблицы
  6. var columns = [ 
  7.  {key:"fio", formatter: custom_format }
  8. ];
Входные параметры для функции форматирования очевидны: первым (el) идет ссылка на html-элемент внутрь которого и нужно поместить сгенерированный вами фрагмент html представления ячейки. Второй и третий параметр хранят ссылки на объект записи и объект колонки (из него я извлек имя поля “fio” и присваиваю его радиокнопке). Последний параметр – собственно, информация, которую нужно отобразить в ячейке. Функция форматирования вызывается всякий раз, когда таблица перерисовывается (вызывается функция render), а значит, что оформление ячеек таблицы может меняться в любой момент времени. Я завершу сегодняшний материал тем, что расскажу об еще одной встроенной в YUI datatable функции форматирования (на сей раз довольно полезной) - YAHOO.widget.DataTable.formatDropdown. Ее назначение – сформировать в каждой из ячеек таблицы падающий список с вариантами. Идеальным полигоном для применения такой функции форматирования будет поле “пол сотрудника”.
  1. {key:"sex",  dropdownOptions: ["male", "female"] , formatter: YAHOO.widget.DataTable.formatDropdown},
  2.  ...
Значения для выпадающего списка я задаю тут же, в описании колонки. В простейшем случае, если и надпись, и значение “value” (т.е. то, что попадет на сервер после отправки формы содержащей внутри себя таблицу) совпадают, то можете перечислить эти надписи как массив строк. Если же надпись и “value” отличны, то используется не массив строк, а массив объектов состоящих из двух свойств value и text:
  1. dropdownOptions: [{value:"male", text: "мужской" }, {value:"female", text: "женский"} ] , 
  2.  ...
Как выглядит ячейка таблицы с такой функцией форматирования показано на рис. 3.