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

January 26, 2009

Я продолжаю рассказ об одном из наиболее полезных и сложных компонентов в библиотеке Yahoo UI. В прошлый раз я показал самые основы работы с компонентом DataTable: мы научились размещать его на странице и наполнять данными из javascript-массива. Также познакомились с методиками настройки внешнего вида колонок таблицы и отдельных ячеек (formatter-ы). Сегодняшняя статья покажет то, как загружать данные для DataTable с сервера (ajax), как реализовать постраничную прокрутку таблицы и управлять ее внешним видом.

Итак, сначала мы разберем то, как наполнить DataTable набором данных, не жестко заданных в теле html-страницы в виде массива объектов JSON, но загружаемых с сервера. Какой бы способ “поставки” данных мы не выбрали, но работать придется с классом DataSource. Этот универсальный загрузчик данных нуждается только в указании адреса php-скрипта формирующего данные, и правил того, как эти данные после загрузки нужно интерпретировать. Для следующего примера я создал на html-страничке блок div с идентификатором “tableplaceholder”, именно внутрь этого блока будет помещен сгенерированный YUI компонент таблицы. Что касается кода javascript, то он практически идентичен тому, который был показан в прошлой статье серии (все эти правила создания массива с описаниями колонок, DataSource):
  1. ds = new YAHOO.util.DataSource("load_json.php?"); 
  2. ds.responseType = YAHOO.util.DataSource.TYPE_JSON; 
  3. ds.connXhrMode = "queueRequests"; 
  4. ds.responseSchema = { 
  5.     resultsList: "users.user", 
  6.     fields: ["fio","birthday","sex","salary"]
  7. }; 
  8. var columns = [
  9.     {key:"fio", sortable:true}, 
  10.     {key:"birthday", sortable:true}, 
  11.     {key:"sex",  sortable:true}, 
  12.     {key:"salary",  sortable:true} 
  13. ];
Первым шагом я создал объект источник данных – DataSource. Единственным параметром для его конструктора выступает адрес php-скрипта, который и генерирует записи о сотрудниках некоей фирмы в формате объектов JSON (об том говорит значение свойства responseType). Еще одно свойство DataSource – connXhrMode служит для корректного разрешения возможных конфликтов данных. Например, можно ли отправлять на сервер несколько запросов на загрузку данных в DataSource и DataTable и как их обрабатывать? Так, когда свойство connXhrMode равно “queueRequests”, то новые запросы на сервер не будут посылаться до тех пор, пока не придет ответ на самый первый запрос за данными. Если же connXhrMode равно cancelStaleRequests, то мы можем отравлять запросы на сервер до того, как придет ответ на предыдущий запрос, при этом тот предыдущий запрос полагается “устаревшим”. Режим ignoreStaleResponses не приводит к отмене “старых” запросов, но просто говорит, что лишь самый последний из отправленных запросов будет учитываться. Режим allowAll не накладывает никаких ограничений на порядок отправляемых запросов и их ответов. Для того, чтобы не накладывать ограничений на php-код, точнее на структуру данных JSON, которую он формирует для javascript в браузере, мы используем привычный прием “подсказывающий” DataSource как найти в ответе сервера перечень записей для последующего отображения в таблице. Так свойство responseSchema и его параметр resultsList говорят, что внутри объекта JSON, возвращенного сервером есть свойство “users”, значением которого является еще один объект JSON, внутри которого, в свою очередь, есть свойство “user”. И именно оно и хранит массив записей о сотрудниках фирмы. Теперь нужно указать какие свойства объекта “сотрудник” нас интересуют. Для этого я использую переменную “fields” равную массиву имен полей (свойств) сотрудников. Записи сотрудников не обязаны быть “плоскими” и какая-нибудь характеристика может вычисляться в несколько этапов, например, “neat_appearance.eyes.color” (внешность – глаза - цвет). Следующий код тривиален: создаем объект таблицы DataTable, передав его конструктору имя html-элемента с заготовкой для таблицы, ссылку на подготовленных источник данных, перечисление характеристик колонок таблицы, и четвертым параметром идет объект с “всякими-разными” настройками DataTable. Хотя настроек не очень много, но собирать их в единое целое и рассказывать о них здесь и сейчас я не будут: мы познакомимся с многими из них на последующих примерах. Пока просто запомните: свойство initialRequest нужно в случае, если DataTable загружает данные с сервера, и оно представляет собой ту строку запроса (набор GET-переменных), которые мы пошлем в самый первый раз на сервер, чтобы получить список записей для первой страницы DataTable. Да-да, вы ведь не думали, что я хочу сразу же загрузить в DataTable все записи, которые у нас есть в базе данных: это глупо т.к. записей может быть очень много, а, значит, их загрузка займет много времени. Да и анализировать “сразу все сто тыщ записей” проблематично. Другое дело, если бы DataTable представлял развитые средства для группировки данных по критериям, т.е. элемент управления, объединяющий возможности дерева для представления иерархических данных, и возможности таблицы для представления характеристик этих данных, сортировки, фильтрации, быстрого поиска. Но в этом плане Yahoo UI отстает от своего бывшего младшего (теперь уже правильнее говорить старшего) братца, библиотеки ExtJs. А все javascript библиотеки находятся далеко позади возможностей представляемых библиотеками компонентов привычных тем, кто разрабатывает деловое ПО с помощью платформы .net или Delphi, но это хоть ожидаемо и понятно. Итак, пример создания DataTable:
  1. table = new YAHOO.widget.DataTable("tableplaceholder", columns, ds, 
  2.    { initialRequest:"department=managers&from=10&size=10&format=json"}
  3. );
На стороне клиента, т.е. браузера и javascript, я завершил все подготовительные действия, и осталось только привести фрагмент кода на стороне сервера. Я не создаю настоящий php-код, который бы отбирал нужную информацию из базы данных, например, mysql. Вместо этого есть имитация, которая генерирует список сотрудников случайным образом, но с учетом тех переменных, которые были переданы из javascript в php. Переменная “department” - это отдел, список сотрудников из которого нужно вернуть в браузер. Далее идет “from” - номер записи, с которой загрузка данных (это для будущего постраничного просмотра). Количество записей на страничках будущего paging-а задается переменной “size”. Переменная sort в последующем пригодится для сортировки отбираемых данных. Последняя переменная “format” служит для того, чтобы указать серверному скрипту то, в каком формате JSON, XML или старый добрый html, будет сформирован список сотрудников. Для не очень секретных сайтов, заинтересованных в том, чтобы их информация была доступна поисковым машинам, всегда нужно заботиться о наличии альтернативных форматов представления понятных роботам-поисковикам.
  1. header ('text/javascript');
  2.  
  3.  // принимаем входные переменные, правда, никаких проверок на предмет их корректности нет, но ведь это только учебный пример
  4.  $department = $_REQUEST['department'];
  5.  $sort = $_REQUEST[sort'];
  6.  $from = $_REQUEST['from'];
  7.  $size = $_REQUEST['size'];
  8.  $format = $_REQUEST['format'];
  9.  
  10.  // начинаем случайным образом формировать массив сотрудников
  11.  $rows = array ();
  12.  for ($i = $from; $i < $size+$from; $i++){
  13.    $fio = "Employee # $i from $department";
  14.    $birthday = sprintf("%02d-%02d-%04d", rand(1,30), rand(1,12), rand(1900,2000) );
  15.    $sex = rand(0,1) == 1?"male":"female" ;
  16.    $rows [] = array ('fio' => $fio, 'birthday' => $birthday,  'sex' => $sex , 'salary' => rand(0,1000) );
  17.  }
  18.  
  19.  $final = array ('users' => array ('user' => $rows) );
  20.  
  21.  print json_encode ($final);
Код тривиален: в цикле я сформировал случайным образом характеристики сотрудников некоторого гипотетического отдела. Затем массив этих объектов (имена полей совпадают с именами, задействованными при описании схемы источника данных responseSchema), был преобразован в формат JSON стандартной php-функцией json_encode. Обратите внимание на цепочку вложенности ‘users’ -> ‘user’ -> ‘массив сотрудников’, она в точности соответствует responseSchema. То, что получилось, показано на рис. 1,



и сейчас мы займемся наведением лоска на эту пока еще сырую заготовку. Первое с чем мы разберемся это управление типами данных. На рис. 1 я показал, как выполнил сортировку по полю “birthday” и полученный результат не тот, который я мог бы ожидать: 1942 год, затем 1926, 1995… В прошлой статье мы разобрали то, как настроить параметры сортировки какого-либо из столбцов, как включать или выключать сортировку, как задать начальное направление сортировки, как в особо тяжелых случаях управлять правилами сравнения содержимого полей. Может нам нужно сделать, что-то подобное? Нет: YUI отлично умеет сортировать поля содержащие даты, вот только как бы ему подсказать, что содержимое колонки birthday и есть дата? В YUI каждой из колонок я могу поставить в соответствие специальную функцию, выполняющую преобразование строки (а ведь все данные, загружаемые с сервера в виде JSON – это строки) в “реальный” тип данных, т.е. в число или дату. Никоим образом эти функции не нужно путать с функциями форматирования, т.к. последние отвечают за визуализацию уже подготовленной информации. А то, что информация и ее представление это две совершенно разные вещи – очевидно. Первым шагом я создаю специальную функцию преобразования строки вида “год-месяц-день” в стандартный для javascript объект Date:
  1. function strToDate (str){
  2.  fragments = str.split ('-')
  3.  return new Date (fragments[2], fragments[1] - 1, fragments[0]); 
  4. }
Теперь нужно при создании DataSource сослаться на эту функцию:
  1. ds.responseSchema = { 
  2.  resultsList: "users.user", 
  3.  fields: [
  4.      "fio",
  5.      {key:"birthday", parser: strToDate},
  6.      "sex",
  7.      {key:"salary", parser:"number"}
  8.  ] 
  9. };
Здесь двум полям “birthday” и “salary” назначены функции-парсеры. Для поля “salary” я использовал встроенную в DataSource функцию “number” преобразования строки в число. А для поля “birthday” я указал собственную функцию strToDate, которая получала бы строку, разбивала ее на три части по разделителю “-” и использовала эти компоненты как параметры года, месяца (напоминаю, что в javascript отсчет месяцев начинается с 0), и день для создания полноценной даты (Date). Если запустить пример, то сортировка будет уже работать корректно, но внешний вид ячейки с датой рождения сотрудника поменяется, и будет выглядеть, например, так “Wed Feb 17 1988 00:00:00 GMT+0200”. Завершающий штрих - назначить колонке “birthday” функцию форматирования, например, так:
  1. var columns = [{key:"birthday", sortable:true, formatter:YAHOO.widget.DataTable.formatDate}];
Теперь рассмотрим внимательнее процесс отправки запроса за данными на сервер. Т.к. в общем случае, это занимает некоторое время то, как будет выглядеть DataTable пока данные для нее еще не пришли? По-умолчанию, на экране показывается заготовка таблицы с заголовками колонок, а вместо данных – надпись “Loading”. Если, то нас не устраивает, то следует при создании DataTable, указать парочку конфигурационных переменных:
  1. table = new YAHOO.widget.DataTable(
  2.  "tableplaceholder", columns, ds, 
  3.  {
  4.     initialRequest:"department=managers&from=10&size=10&format=json",
  5.     MSG_LOADING: "Ждите, идет загрузка данных",
  6.     MSG_EMPTY: "Данных для отображения нет",
  7.     MSG_ERROR: "Ошибка на сервере" 
  8.  }
  9. );
MSG_LOADING - это надпись (а это может быть не просто текст, но и произвольный html-код, с картинками, стилями), которую мы видим пока данные еще не были загружены (та же надпись показывается пока выполняется сортировка, хотя обычно это происходит очень быстро и мы ничего не замечаем). Если же сервер вернул пустой набор записей, то на экране будет отображена надпись MSG_EMPTY. А в том, случае, если данные, возвращенные php-скриптом, вообще не являются как должно JSON (например, ошибка выполнения скрипта), то пользователь увидит сообщение, задаваемое переменной MSG_ERROR. Есть еще переменные MSG_SORTASC или MSG_SORTDESC – они задают текст подсказки, всплывающей при наведении мыши на заголовок столбца (изначально равны "Click to sort ascending" и "Click to sort descending").

Идем далее: сейчас табличка DataTable содержит только первые 10 записей. Как нам отобразить оставшиеся? Для реализации paging-а (постраничного отображения записей) есть два подхода: на стороне клиента и на стороне сервера. С выходом YUI версии 2.6 появился новый элемент управления Paginator. Формально он предназначен для отображения длинного перечня записей (картинок галереи, да и чего угодно) на нескольких страницах, с расположенной рядом панелью навигации (кнопки перемещения вперед и назад, к самой первой и последней страничке). На практике быстрого старта в использовании paginator-а не получится т.к. он представляет собой скорее конструктор “собери сам”, чем законченный и готовый к использованию визуальный компонент. Вы отвечаете за все: за формирование внешнего вида самой страницы и области навигации, за загрузку из внешнего источника содержимого страницы. Не знаю даже, что проще использовать Paginator или создать собственный компонент с таким же назначением? С другой стороны, если рассмотреть исходный код Paginator-а, то можно почерпнуть несколько интересных методик к написанию собственных “повторно используемых компонент”. До версии YUI 2.6 компонент DataTable имел встроенную поддержку paging-а, а начиная с 2.6 мы при создании объекта DataTable одним из параметров, наряду с описанными выше MSG_LOADING и другими, можем передать параметр paginator, значением которого и является объект YAHOO.widget.Paginator, например:
  1. // создаем paginator
  2. p = new YAHOO.widget.Paginator(
  3.    {rowsPerPage: 10,  containers : ['container1','container2']}
  4. );
  5. // и используем его 
  6. table = new YAHOO.widget.DataTable("tableplaceholder", columns, ds, 
  7.   {
  8.     initialRequest:"department=managers&from=10&size=100&format=json",
  9.     paginator: p 
  10.   } 
  11. );
Минимально необходимым является указание для paginator-а размера страницы (здесь 10 строк на страницу). По-умолчанию, будут созданы две панели навигации по страницам и размещены до и после таблицы с данными (см. рис. 2).



В примере я решил это переопределить: создал в теле html-страницы два блока div с идентификаторами “container1” и “container2” (они могут находиться где угодно) и указал на них при создании paginator-а. Теперь, стандартные панели навигации были внедрены внутрь именно этих блоков div. Следующий пример показывает многие из настраиваемых параметров внешнего вида paging-а:
  1. p = new YAHOO.widget.Paginator(
  2.  {
  3.   rowsPerPage: 10,
  4.   // надписи на кнопка перемещения по страница
  5.   firstPageLinkLabel : "<<", 
  6.   previousPageLinkLabel : "<", 
  7.   nextPageLinkLabel : " >", 
  8.   lastPageLinkLabel : ">>", 
  9.   // соответствующие им css-классы
  10.   firstPageLinkClass : "goto-first",
  11.   previousPageLinkClass : "goto-prev",
  12.   nextPageLinkClass : "goto-next",
  13.   lastPageLinkClass : "goto-last",
  14.   pageLinksContainerClass : 'pages-area',
  15.   pageLinkClass           : 'common-page',
  16.   currentPageClass        : 'current-page', 
  17.   pageLinks               : YAHOO.widget.Paginator.VALUE_UNLIMITED, 
  18.   pageLabelBuilder        : function (page,paginator) { 
  19.       r = paginator.getPageRecords(page);
  20.       return r[0]+"-"+r[1];   
  21.   }, 
  22.  
  23.   template : 
  24.     '<h3>Сведения об сотрудниках</h3>' + 
  25.     '<p>{CurrentPageReport}' + 
  26.     '{FirstPageLink} {PreviousPageLink} ' + 
  27.     '{NextPageLink} {LastPageLink}' + 
  28.     '<label>Размер страницы: {RowsPerPageDropdown}</label>' + 
  29.     '{PageLinks} </p>', 
  30.  
  31.    rowsPerPageOptions       : [ 
  32.     { value : 10, text : "10" }, 
  33.     { value : 50, text : "50" }, 
  34.     { value : 100000, text : "все" }   ], 
  35.  
  36.   pageReportTemplate : '(страница {currentPage} из {totalPages})'
  37.  } 
  38. );
Внешний вид получившегося paging-а показан на рис. 3.



Хотя из сравнения картинки и примера кода ясно, то какие переменные управляют каким аспектом внешнего вида paginatora-а, но все же пару слов сказать стоит. Внешний вид paginator-а прежде всего задается шаблоном “template”. Это произвольный фрагмент html с размеченными с помощью placeholder-ов местами, куда нужно поместить (или не поместить) составляющие paginator части. Первая часть шаблона – это сообщение о том, какая страница из скольких сейчас активна (видима). В шаблоне переменная обозначается как “CurrentPageReport” и в свою очередь управляется шаблоном “pageReportTemplate”. Вторая часть шаблона - это кнопки-ссылки перехода по страницам вперед и назад, а также к первой и последней записи (к примеру, FirstPageLink). В свою очередь, каждая из кнопок управляется двумя переменными: firstPageLinkLabel и firstPageLinkClass – это текст и стилевое оформление кнопки. Следующей частью шаблона является падающий список с вариантами размера страниц “RowsPerPageDropdown”. Само же перечисление вариантов (надпись-подсказка и число с количеством записей на страницу) управляется переменной “rowsPerPageOptions”. Завершает шаблон перечисление самих страниц “PageLinks”. Т.к. страниц может быть очень много и они могут элементарно не умещаться в отведенной для paging-а части html-страницы, то можно ограничить число одновременно показываемых ярлычков страниц с помощью переменной “pageLinks” (в примере она равна специальному значению VALUE_UNLIMITED, т.е. без ограничений). Внешний вид каждой (подчеркиваю) страниц может быть различным и формирует его функция “pageLabelBuilder”.