Сложные интерфейсы на javascript вместе Yahoo UI. Часть 9
Я продолжаю рассказ о возможностях javascript-библиотеки Yahoo UI. Несколько прошлых статей были посвящены различным аспектам работы с ajax: работа с формами, отправка на сервер файлов, решение проблемы crossdomain запросов. Сегодня я возвращаюсь к рассмотрению визуальных элементов управления, хотя это не означает, что про ajax больше не будет сказано ни слова: многие сложные визуальные элементы YUI открываются во всей своей красе только, если данные для них загружаются асинхронно, ajax-ом.
Первый элемент управления, о котором я расскажу, - это AutoComplete. Почти четыре года назад, я впервые столкнулся с применением ajax для разработки веб-сайтов именно на примере AutoComplete. Тогда google выпустила из недр своих лабораторий google labs экспериментальную версию страницы поиска. Размещенное на ней, текстовое поле для ввода запроса, было "живым": по мере ввода текста, внизу поля появлялся и обновлялся в режиме реального времени список с возможными продолжениями ключевых слов. Сейчас я знаком с множеством javascript библиотек с поддержкой ajax и почти в каждой из них есть своя версия элемента AutoComplete. Почти всегда, когда на сайте нужно вводить данные из большого списка вариантов (код товара, название города) имеет смысл сделать это с помощью AutoComplete. Так вы не только сэкономите для пользователя пару секунд времени, но и что более важно, вы избежите проблем связанных с неточным вводом, например, "Менск" вместо "Минск". Даже сейчас, когда я слышу "ajax", то первая ассоциация google suggest. На этом хватит лирики - давайте напишем немножко кода. Прежде всего, я подключаю к странице модуль autocomplete. И как всегда, пользуюсь для этого стандартным для YUI модулем yui-loader:
loader = new YAHOO.util.YUILoader();
loader.require(["autocomplete"]);
loader.loadOptional = true;
loader.base = 'js/';
loader.insert({ onSuccess: startApp });
Теперь я должен в html коде страницы создать "заготовку" будущего элемента управления AutoComplete. Предположим, что пользователь должен ввести в поле формы имя сотрудника. "Скелет" элемента управления состоит из одного текстового поля (там мы вводим ФИО) и блока div (а тут будет отображаться список возможных вариантов продолжения). Удобным будет поместить эти два элемента внутрь еще одного блока div, играющего роль контейнера:
<div id="employes">
<input id="fio" type="text">
<div id="fiosuggest"> </div>
</div>
Теперь внутри обработчика события от yui-loader "все готово", я создаю элемент управления AutoComplete:
// массив с данными для подсказок
var arrNames = ["Jim", "Janet", "Jack", "Ronald", "Louis"];
// источник данных
var ds = null;
// элемент управления
var aNames = null;
function startApp() {
ds = new YAHOO.widget.DS_JSArray(arrNames);
aNames = new YAHOO.widget.AutoComplete('fio','fiosuggest', ds);
aNames.typeAhead = true;
aNames.minQueryLength = 1;
}
При вызове конструктора YAHOO.widget.AutoComplete я передаю в качестве параметров имена созданных шагом ранее html-элементов, и (это самое важное) источник данных для подсказок. Источник данных представлен экземпляром объекта YAHOO.widget.DS_JSArray. Из названия класса ясно, что сами данные должны быть предварительно подготовлены в форме массива. И действительно, чуть выше объявления функции startApp, я создал переменную arrNames - массив имен сотрудников. Так и где тот самый ajax, если все данные заданы статически в коде html-страницы? В действительности, элемент управления AutoComplete отделен от ajax и соответствующего серверного скрипта, генерирующего значения для подсказок, дополнительным уровнем абстракции - объектом DataSource. Есть несколько видов источников данных: DS_JSArray, DS_JSFunction, DS_ScriptNode, DS_XHR. Все эти классы производны от общего предка и представляют собой типовые стратегии "откуда берутся данные". Условно говоря, во всех них есть метод "дай_продолжение_фразы (строка)", и этот метод возвращает возможные объекты "подсказок". Или, обращаясь к php-скрипту на сервере, или - к массиву с предопределенными значениями - не важно. Логично, что можно создать собственный класс поставщика данных, если ни один из четырех встроенных вариантов не подходит. Самый простой пример поставщика данных - это массив предопределенных значений. Обратите внимание на то, что я заменил фразу "поставщик строк-продолжений" на более абстрактную "поставщик данных". Дело в том, что фактически элементами массива для DS_JSArray могут быть не только строки, а произвольные объекты. Это открывает интересные перспективы по разработке интерфейсов, но об этом позже. Пока же внимание на последние строки кода функции startApp. Там я присваиваю значения для специальных конфигурационных переменных, управляющих внешним видом и поведением AutoComplete. Первый параметр typeAhead - рассмотрим на следующем примере. Вы ввели в текстовое поле начало имени "Ва", здесь же компонент AutoComplete нашел список возможных продолжений: "Вася", "Валера". При включенном режиме typeAhead, AutoComplete сам завершит начало фразы "Ва" до первого из списка продолжений - "Вася". Следующий конфигурационный параметр minQueryLength - задает нижнюю границы длины строки, которую должен ввести пользователь, чтобы AutoComplete загрузил список ее продолжений. Фактически вы можете поставить значение равное -1 - и тем самым отключить функцию AutoComplete на какое-то время. Подобных параметров влияющих на работу YUI AutoComplete много, но самые интересные из них это maxResultsDisplayed, queryDelay, delimChar, animHoriz, animVert, animSpeed, forceSelection, allowBrowserAutocomplete, useIFrame. Итак: maxResultsDisplayed в соответствии со своим названием задает ограничение количества подсказок, которые будут отображены после загрузки из источника данных. Параметр queryDelay задает время паузы (в секундах) с которой выполняются запросы к поставщику данных. Дело в том, что есть две стратегии работы элемента AutoComplete - либо мы будем запрашивать продолжение строки каждый раз, когда содержимое текстового поля изменится. Либо мы ждем пока в течении какого-то промежутка времени текстовое поле остается неизменным и только затем делаем запрос. Тут мы полагаем, что пользователь задумался над продолжением, а значит, нуждается в нашей помощи. Вторая стратегия является наилучшей (особенно для тех, кто быстро набирает или "сидит" на модеме). Параметр delimChar может быть одним символом или набором символов-разделителей. Представьте, что мы должны ввести в текстовое поле не одно имя сотрудника, а перечень имен. В таком случае мы говорим YUI, что delimChar равен пробелу, запятой или точке с запятой. И когда будем набирать имена, разделив их соответствующим символом, то YUI будет показывать всплывающую подсказку для каждого имени, а не только для первого. Кроме того, после выбора из списка какого-то из вариантов подсказки и нажатия клавиши "ввод", то символ разделитель будет вставлен в текстовое поле сразу после выбранного в списке значения. Конфигурационные параметры animHoriz, animVert, animSpeed управляют параметрами анимации появления на экране области с подсказками. Будет ли проиграна анимация горизонтального или вертикального разворачивания подсказки и сколько секунд это должно занять. Параметр forceSelection служит для того, чтобы фактически превратить элемент text в select. Т.е. если пользователь попробует ввести в текстовое поле значение, отсутствующее в списке возможных вариантов подсказок - продолжений, то у него ничего не получится. Параметр allowBrowserAutocomplete управляет для браузеров атрибутом autocomplete для текстовых полей. Идея autocomplete (точнее отключения этой функции очень хороша). Так если вы вообразите сценарий, когда пользователь заполняет форму с "секретной" информацией, жмет на кнопку отправления формы. А затем с помощью кнопки "назад" браузера возвращается на предыдущую страницу (а может, пользователь отлучился на пару минут, и вместо него "работает" злоумышленник). То очевидно, что не все поля должны сохранить предыдущие значения - ряд полей нужно очистить. В практике же autocomplete лучше не пользоваться, т.к. на нее могут влиять настойки браузера, еще всяческие "умные" плагины и дополнения к браузеру. Лучше всего взять контроль за очисткой "опасных" полей в свои руки, например, при получении события "страница загружена", очистить такие поля. Последний из заслуживающих упоминания параметров для компонента AutoComplete - это useIFrame. Он решает проблему с известным багом для internet explorer-ом, когда элемент управления select, размещенный рядом с AutoComplete текстовым полем будет "просвечивать" из-под блока div с подсказкой для AutoComplete. Еще к слову: если вы размещаете сразу под элементом AutoComplete какую-то информацию, то она будет невидимой: спрячется под AutoComplete. Все дело, в том, что YUI добавляет к html-элементам "скелета" AutoComplete стиль с абсолютным позиционированием, так что не пугайтесь когда на странице что-то пропало - просто сделайте побольше отступ сверху. Все конфигурационные переменные, которые я перечислил выше, можно указать либо после создания элемента AutoComplete, либо перечислив их как третий параметр конструктора:
aNames = new YAHOO.widget.AutoComplete(
'fio','fiosuggest', ds,
{
delimChar: ", ", minQueryLength: 2, allowBrowserAutocomplete: false
}
);
Теперь вернемся назад к рассмотрению классов DataSource - источников данных для AutoComplete. В некоторых случаях значения строк-подсказок не являются ни известными заранее, ни загружаемыми динамически с сервера, а генерируемыми с помощью какой-то функции. Рассмотрим пример, когда на html-странице есть текстовое поле для ввода ФИО сотрудника, но для удобства мы решили разместить на форме еще падающий список с перечнем отделов. Тогда очевидно, что мы не можем использовать как источник данных единый список имен всех сотрудников. Мы должны сначала отобрать из этого перечня тех, кто работает в выделенном отделе, и только затем выполнить поиск продолжения по первым буквам их ФИО. Для такого сценария удобно использовать источник данных типа DS_JSFunction. В качестве единственного параметра конструктору DS_JSFunction передается ссылка на функцию, которая анализирует значения элементов управления формы и возвращающей массив с нужной информацией. К сожалению, если данные для подсказки не присутствуют в странице изначально и функция должна загружать данные с сервера, то DS_JSFunction не подходит из-за своей синхронной архитектуры.
Асинхронная загрузка данных возможна с помощью либо класса DS_ScriptNode, либо DS_XHR. Про первый из них я скажу буквально пару слов, т.к. он представляет собой "обертку" над раскритикованной в прошлой статье утилитой YAHOO.util.Get (там же читайте про прием с динамическим созданием тегов script). Используйте этот источник данных, только если вам нужно загружать данные с crossdomain ресурсов, а лучше вообще не использовать, а обойтись приемом со скриптом-посредником proxy.
Класс DS_XHR (XHR - расшифровываются как XmlHttpRequest) использует для отправки запросов на сервер многократно "разжеванный" в прошлых статьях класс YAHOO.util.Connect. Я говорил, что YAHOO.util.Connect умеет загружать данные с сервера в форматах xml и json, а значит, что и данные для DS_XHR могут быть в одном из этих форматов. Так при создании объекта DS_XHR я обязательно должен указать значение конфигурационного параметра responseType:
var schema = ["что-то важное"];
ds = new YAHOO.widget.DS_XHR('jsonfinder.php', schema);
// это источник данных в виде JSON
ds.responseType = YAHOO.widget.DS_XHR.TYPE_JSON;
//это источник данных в виде XML
ds.responseType = YAHOO.widget.DS_XHR.TYPE_XML;
//это источник данных в виде "плоского" текстового файла
ds.responseType = YAHOO.widget.DS_XHR.TYPE_FLAT;
// и наконец создаем сам элемент управления AutoComplete
aNames = new YAHOO.widget.AutoComplete('fio','fiosuggest', ds, {delimChar: " ", minQueryLength: 1);
Первым параметром конструктора DS_XHR я указал путь к php-скрипту генерирующему данные, затем идет какая-то непонятная переменная schema. Дело в том, что анализ результата запроса (того, что вернул скрипт jsonfinder.php) зависит не только от его типа данных, но и от их структуры. Действительно, когда я говорю xml, то это значит лишь то, что документ xml построен по некоторым формальным правилам: все эти требования на соблюдение регистра тегов, указание значений атрибутов в кавычках, закрытые теги и т.д. Но вот какие будут использованы имена тегов, какая будет структура их вложенностей - не известно. Разработчики yahoo решили не требовать того, чтобы php-скрипт формировал какие-то специальные xml-теги, а вместо этого требуют, чтобы javascript-код передал при создании объекта DataSource ключевую информацию, нужную для интерпретации дерева xml. Предположим, что php-скрипт вернул следующую структуру:
<?xml version="1.0"?>
<results>
<employee>
<fio>Jim Tapkin</fio>
<age>12</age>
<sex>male</sex>
</employee>
<employee fio="Janet Tapkina" age="12" sex="female" />
</results>
Здесь корневой элемент results содержит две записи сотрудников, имена которых начинаются на букву "J". Момент в том, что в первом случае я задаю информацию о сотруднике, в виде вложенных подэлементов, а во втором - с помощью атрибутов. Теперь я приведу пример того, как выглядит параметр, описывающий схему данных для DS_XHR:
var schema = ["employee", "fio", "age", "sex"];
ds = new YAHOO.widget.DS_XHR('xmlsearch.php', schema);
Первый элемент массива - это имя того xml-элемента, который хранит отдельную запись результата поиска. Второй элемент "fio" говорит, какое поле записи (вложенный элемент или атрибут) будет выведен среди вариантов подсказки. А что же делают остальные параметры: age и sex (к слову, этих параметров может быть неограниченное количество). Для этого нам нужно познакомиться еще с одной идеей лежащей в основе AutoComplete - пользовательской функцией форматирования элемента списка подсказок перед отображением. Можно даже сказать, что AutoComplete построен на идеях MVC (как бы претенциозно это не звучало), с их четким отделением информации от ее визуализации. К примеру, я создам функцию, которая на основании всех трех полей записи (fio, age, sex) сформирует различное представление строки с данными: так в зависимости от пола цвет шрифта элементов списка будет либо красным, либо зеленым:
aNames = new YAHOO.widget.AutoComplete('fio','fiosuggest', ds, {delimChar: " ", minQueryLength: 1});
aNames.formatResult = function(record, query) {
var color = record[2] == "male"?"red":"green";
var sMarkup = "<span style='color:"+color+"'>"+ record[0] + " (" + record[1] + ")" + "</span>";
return (sMarkup);
};
Первым параметром функции formatResult передали массив со всеми свойствами записи (порядок совпадает с указанным в schema), второй же параметр - это строка запроса, введенная пользователем в текстовое поле. Никаких ограничений на формируемый html-код представления элемента нет: таблицы, картинки - все используйте смело.
Теперь рассмотрим то, как можно источник данных DS_XHR научить работать с JSON. Для начала я создам php-скрипт, формирующий список объектов employee:
header ('Content-type: application/json');
$employes = array (
'employes' =>
array (
array ('fio' => 'Jim Tapkin', 'age'=> 12, 'sex'=>'male'),
array ('fio' => 'Janet Tapkina', 'age'=> 13, 'sex'=>'female')
)
);
print (json_encode ($employes) );
Также как и при работе с xml для php-скрипта, формирующего json-документ, необходимо указать правильное значение типа документа (content-type). Затем я создал сложный php-массив с перечнем сотрудников и передал его для преобразования в JSON-формат функции json_encode. Результирующая строка будет выглядеть так:
{"employes":[
{"fio":"Jim Tapkin","age":12,"sex":"male"},
{"fio":"Janet Tapkina","age":13,"sex":"female"}
]}
Теперь вернемся назад к javascript и создадим точь-в-точь как раньше объект DS_XHR, с точно такими же значениями schema. В действительности, также как и при анализе xml, первый элемент массива schema задает имя того json-узла где хранится массив с записями подсказок. Это может быть сложное имя через точку или обращение к элементу массива, например, такое "department[1].employes". Остальные элементы массива schema кодируют имена свойств записей. В отличии от xml, json-документ может хранить в качестве значения какого-либо из атрибутов сложный объект. Так свойство "age" может быть не числом, а массивом из трех элементов, кодирующих дату рождения: год, месяц, день.
Последним видом источника данных для AutoComplete является "плоский" csv-файл. Считается, что php-скрипт должен вернуть очень-очень большую строку. Для анализа которой, мы выполняем разбиение ее на подстроки (отдельные записи). В качестве критерия конца записи используется символ заданный как первый элемент массива schema. Затем каждую из строк-записей нужно разделить уже на отдельные слова-поля, и здесь в качестве разделителя используется второй элемент массива schema.
Гораздо интереснее научиться настроить параметры запроса к серверу, который делает объект DS_XHR. К примеру, запрос может выглядеть так:
http://site.abc/script.php?query=J
В этом примере, при выполнении запроса передается только одна единственная переменная query равная букве "J" - то, что начал вводить пользователь. Начнем с того, что при создании объекта источника данных, можно указать значение конфигурационной переменной scriptQueryParam и тем самым заменить показанную выше переменную query на другое имя. Еще один параметр scriptQueryAppend - позволит передать кроме серверному скрипту дополнительные переменные, например, так "mode=suggest&format=json". Параметр connTimeout хранит предельную величину времени выполнения запроса к серверу (в секундах).