Итак, мы выбрали браузер вместо отдельно стоящего толстого клиента. Пользователь очень хочет вводить данные. Однако, обычные формы плохие. Потому, что:
Данные и HTML-код смешаны.
Нет удобного виджета с выпадающим списком и поиском по началу наименования.
Нет удобного встроенного средства грузить данные в формы.
Нет удобной возможности отправлять данные формы через AJAX.
И т.д.
Чего хочется?
Полностью отвязать HTML-код форм от данных.
Иметь возможность предзагрузки форм.
Иметь возможность гибко настраивать форму.
Иметь удобный выпадающий список с поиском по началу имени.
Отвязать контролы формы от ее окружения.
И так далее.
Начнем с конца. Какой должен быть выпадающий список? Уж конечно не как стандартный SELECT!
Во-первых, он должен использовать справочник. То есть набор пар [«ключ», «значение»]. При этом пользователю показывается строка «значение», а на сервер отправляется «ключ».
Во-вторых, он должен подключаться к INPUT вместе со справочником, при вводе символов показывать список, бегать туда-сюда и убираться при уходе фокуса с INPUT.
Для создания выпадающего списка (будем называть его dropdown для краткости) воспользуемся jQuery. Что хорошего в jQuery? Как минимум вот это:
1. удобная функция обратного вызова когда уже загружена DOM-структура документа (и можно его портить), а всякие картинки может еще и не загружены:
2. удобный поиск нужных элементов по ID
3. удобная обвязка объектом jQuery любого DOM-элемента
4. удобная привязка всяких событий
Данный вызов привязывает к DOM-элементу li 2 события: при наезжании курсора на элемент и на отъезжание.
В промежутке между этими событиями DOM-элемент получает bold-шрифт (потому что this как раз на него и указывает в обработчике события)
5. удобные методы jQuery объекта, которые применяются универсально к отдельным DOM-элементам или их наборам
И так далее.
Мы воспользуемся возможностью jQuery расширяться. За основу нашего dropdown я взял скрипт autocomplete, который при поиске в Гугле показывает похожие запросы. Его легко нагуглить. Готовое решение должно работать примерно так: при настройке формы мы должны иметь возможность привязать dropdown со справочником к контролу вот так:
some_directory это и есть справочник (некий JavaScript объект, о котором позже)
Как этого добиться? Создаем свой новый метод jQuery, который вызывается выше:
Я привел его полностью. Нетрудно видеть, что этот скрипт упорно проверяет разные опции (если их вообще нет, то создает). Опции набиваются значениями по умолчанию. Наконец, вот эта строка:
проходится по списку элементов, которые отобрал селектор и для каждого создает новый jQuery объект dropdown с помощью конструктора dropdownConstructor. Который и содержит весь нужный код и выглядит примерно так:
И пошло-поехало.
Думаю код достаточно понятен, но все же. truevalue — это ключ. Это аттрибут контрола, который хранит ключевое (не показываемое пользователю) значение. set_truevalue() задает ключ и синхронизирует отображамое значение. active() показывает выпадающий список, deactive() его прячет. move_up(), move_down() — движение по списку вверх и вниз. select_text_part() — выделение части текста.
Вот так мы привязываемся к событиями внутри конструктора:
Теперь поговорим о справочниках. Справочник — это JavaScript массив однотипных объектов (с одинаковым набором полей). Например, "
Нашему dropdown-у нужен отсортированный по возрастанию value справочник (то есть по возрастанию видимого значения, а не ключевого). Причем обычно с игнорированием регистра букв (когда пользователь вводит начало названия на клавиатуре в большинстве случаев ему все равно зажат CAPS LOCK или нет). Следовательно, этот массив надо сортировать и для этого и пришлось написать быструю сортировку на JavaScript и бинарный поиск. Ведь пользователь может ввести и часть имени. Не буду приводить сортировку, только бинарный поиск для примера:
Итак, после упорного кодирования наш замечательный dropdown заработал:
www.picamatic.com/show/2009/05/12/06/35/3618111_557x236.png
Теперь начнем разбираться с формами. Форма разделяется на три части: HTML-код, JavaScript-код настройки и список справочников, которые требуются форме для работы. Наш серверный скрипт формы будет возвращать все эти части по запросу и кроме того, он должен принимать данные и возвращать ошибки их обработки. Серверный скрипт в файле "/foo.js" выглядит так:
Почему на сервере используется JavaScript объясняется здесь: habrahabr.ru/blogs/development/48842
HTML-код это только контролы, без окружающего "<form />". Немного хитрый настроечный код формы требует два параметра: DOM-элемент формы (form) и функцию, которая возвращает загруженный справочник по его url ( jason() ).
Итак, мы хотим все это на страницу асинхронно загружать и использовать. Тут есть два соображения. Во-первых, справочники принадлежат всей странице, а не одной форме. Разные формы могут работать с одними и теми же справочниками. Во-вторых, формой можно пользоваться только после полной и успешной загрузке всех ее составляющих.
Собственно, дальше я написал асинхронный загрузчик, который кеширует загруженные данные и позволяет иерархически вкладывать загрузки друг в друга (чтобы, допустим, полностью удалить все активные загрузки формы). Затем поверх этого — загрузчик форм, который, получив список справочников, просит их загрузить и ждет их появления.
Итак, допустим у нас есть под рукой (загружены на страницу) все необходимые компоненты формы: HTML, javascript и справочники готовы. Следующий шаг — создание _экземпляра_ формы, который и есть то, что видит пользователь. Вот код, который создает экземпляр формы формы по имени form_name в DOM-элементе container. Экземпляр получает имя instance_name:
Думаю, идея понятна.
Ну и наконец: десерт. То ради чего весь этот код создавался. Вот как выглядит загрузочный код HTML-страницы, которая использует данный механизм:
Не правда ли — весьма лаконично? А вот как используется форма.
Создать экземпляр в DOM-элементе с id='foo_form':
Загрузить данные в экземпляр формы:
Ну и наконец ради чего все это нужно — отправка данных:
Функция foo_submit может выглядеть примерно так:
Метод submit() может получить от серверного скрипта набор ошибок по данным и покажет их автоматически. А при очередной отправке уберет и сделает полный disable формы.
Вообщем, с формой можно сделать практически все что угодно: показать, очистить, загрузить данными, сделать полный disable и обратно enable, отправить на сервер и ждать успешной отправки.
Наконец, убрать экземпляр формы со страницы. Опять-таки, мы можем на странице без труда создать несколько экземпляров одной и той же формы.
К примеру, можно в таких формах показывать неудачные отправки на сервер.
Вот такое вот решение.
Жду замечаний и предложений. Если найду время — сделаю syntax highlight кода на JavaScript (а почему нет под рукой на Хабре??).
Данные и HTML-код смешаны.
Нет удобного виджета с выпадающим списком и поиском по началу наименования.
Нет удобного встроенного средства грузить данные в формы.
Нет удобной возможности отправлять данные формы через AJAX.
И т.д.
Чего хочется?
Полностью отвязать HTML-код форм от данных.
Иметь возможность предзагрузки форм.
Иметь возможность гибко настраивать форму.
Иметь удобный выпадающий список с поиском по началу имени.
Отвязать контролы формы от ее окружения.
И так далее.
Начнем с конца. Какой должен быть выпадающий список? Уж конечно не как стандартный SELECT!
Во-первых, он должен использовать справочник. То есть набор пар [«ключ», «значение»]. При этом пользователю показывается строка «значение», а на сервер отправляется «ключ».
Во-вторых, он должен подключаться к INPUT вместе со справочником, при вводе символов показывать список, бегать туда-сюда и убираться при уходе фокуса с INPUT.
Для создания выпадающего списка (будем называть его dropdown для краткости) воспользуемся jQuery. Что хорошего в jQuery? Как минимум вот это:
1. удобная функция обратного вызова когда уже загружена DOM-структура документа (и можно его портить), а всякие картинки может еще и не загружены:
$(document).ready(function(){
// ... здесь можно работать с DOM
});
2. удобный поиск нужных элементов по ID
$('#some_id')
3. удобная обвязка объектом jQuery любого DOM-элемента
$(some_DOM_element)
4. удобная привязка всяких событий
$(li).hover( function() { this.style.fontWeight = 'bold'; }, function() { this.style.fontWeight = 'normal'; } )
Данный вызов привязывает к DOM-элементу li 2 события: при наезжании курсора на элемент и на отъезжание.
В промежутке между этими событиями DOM-элемент получает bold-шрифт (потому что this как раз на него и указывает в обработчике события)
5. удобные методы jQuery объекта, которые применяются универсально к отдельным DOM-элементам или их наборам
.show()
показать.hide()
спрятать.val()
получить значение контрола.val(new_value)
задать значение контролаИ так далее.
Мы воспользуемся возможностью jQuery расширяться. За основу нашего dropdown я взял скрипт autocomplete, который при поиске в Гугле показывает похожие запросы. Его легко нагуглить. Готовое решение должно работать примерно так: при настройке формы мы должны иметь возможность привязать dropdown со справочником к контролу вот так:
$(form.some_element_name).dropdown( some_directory, options ) ;
some_directory это и есть справочник (некий JavaScript объект, о котором позже)
Как этого добиться? Создаем свой новый метод jQuery, который вызывается выше:
jQuery.fn.dropdown = function(data, options)
{
options = options || {};
options.data = ((typeof data == "object") && (data.constructor == Array)) ? data : null;
options.key = options.key || "key";
options.value = options.value || "value";
options.matchCase = options.matchCase || 0;
options.list_size = options.list_size || 11;
options.containerClass = options.containerClass || "__dropdown_container_class";
options.selectedItemClass = options.selectedItemClass || "__dropdown_selected_item_class";
options.items = options.data ? clone_quicksort(options.data, options.value, options.matchCase ? less_than_compare : less_than_ignore_case_compare) : null;
options.items_hash = options.items ? hash_array(options.items, options.key) : null;
this.each( function() { var input = this; new jQuery.dropdownConstructor(input, options); } );
return this;
}
* This source code was highlighted with Source Code Highlighter.
Я привел его полностью. Нетрудно видеть, что этот скрипт упорно проверяет разные опции (если их вообще нет, то создает). Опции набиваются значениями по умолчанию. Наконец, вот эта строка:
this.each( function() { var input = this; new jQuery.dropdownConstructor(input, options); } );
проходится по списку элементов, которые отобрал селектор и для каждого создает новый jQuery объект dropdown с помощью конструктора dropdownConstructor. Который и содержит весь нужный код и выглядит примерно так:
jQuery.dropdownConstructor = function(input, options)
{
var input = input; // это DOM элемент контрола
this.control = input;
var $input = $(input); // это jQuery объект, содержащий DOM элемент контрола (для использования методов jQuery на этом элементе)
var container = document.createElement("div"); // это DIV будет содержать выпадающий список list
var $container = $(container);
var list = document.createElement("ul");
var $list = $(list);
var active = false;
var last_key_pressed = null;
var timeout = null;
var value_of_key = ""
var prev_truevalue, next_truevalue;
var that = this;
input.dropdown = that;
input.truevalue = input.truevalue || null;
container.appendChild(list); // помещаем список в контейнер
$container.hide().addClass(options.containerClass).css("position", "absolute"); // контейнер в документе пока не виден
if( options.width > 0 ) $container.css("width", options.width);
$("body").append(container); set_truevalue(input.truevalue);
function postpone(func)
{
if (timeout) clearTimeout(timeout);
timeout = setTimeout(function(){ func(); }, 25);
}
function set_truevalue(new_truevalue)
{
if (!options.items_hash[new_truevalue])
{
input.truevalue = null;
value_of_key = "";
$input.val( "" );
return;
}
input.truevalue = new_truevalue;
if (input.truevalue) value_of_key = options.items_hash[input.truevalue][options.value]; else value_of_key = "";
$input.val( value_of_key );
}
input.update_control = function() { set_truevalue(that.control.truevalue); }
function activate()
{
if (active) return;
var pos = element_position(input);
var W = (options.width > 0) ? options.width : $input.width();
$container.css( { width: parseInt(W) + "px", left: pos.x + "px", top: (pos.y + input.offsetHeight) + "px" } ).show();
var index = binary_search(options.items, value_of_key, options.value, options.matchCase ? less_than_compare : less_than_ignore_case_compare);
build_list(index - (options.list_size >> 1), index);
active = true;
}
function deactivate()
{
if (!active) return;
$container.hide();
active = false;
}
function move_up()
{
...
}
function move_down()
{
...
}
function select_text_part(start, end)
{
...
}
* This source code was highlighted with Source Code Highlighter.
И пошло-поехало.
Думаю код достаточно понятен, но все же. truevalue — это ключ. Это аттрибут контрола, который хранит ключевое (не показываемое пользователю) значение. set_truevalue() задает ключ и синхронизирует отображамое значение. active() показывает выпадающий список, deactive() его прячет. move_up(), move_down() — движение по списку вверх и вниз. select_text_part() — выделение части текста.
Вот так мы привязываемся к событиями внутри конструктора:
$input.keydown(function(e)
{
last_key_pressed = e.keyCode;
switch(e.keyCode)
{
case 38: e.preventDefault(); move_up(); break;
case 40: e.preventDefault(); move_down(); break;
case 13: case 9: select_text_part(); deactivate(); break;
default: postpone(on_change); break;
}
}).focus(function(){
activate(); select_text_part();
}).blur(function(){
deactivate();
}).mousedown( function(e) { if (!active && !input.disabled) { e.preventDefault(); $input.focus(); activate(); } } );
* This source code was highlighted with Source Code Highlighter.
Теперь поговорим о справочниках. Справочник — это JavaScript массив однотипных объектов (с одинаковым набором полей). Например, "
[ { key:'m', value:'male' }, { key: 'f', value: 'female' } ]
". По умолчанию в поле key находится ключ, а в поле value — значение. Однако, это всегда можно изменить.Нашему dropdown-у нужен отсортированный по возрастанию value справочник (то есть по возрастанию видимого значения, а не ключевого). Причем обычно с игнорированием регистра букв (когда пользователь вводит начало названия на клавиатуре в большинстве случаев ему все равно зажат CAPS LOCK или нет). Следовательно, этот массив надо сортировать и для этого и пришлось написать быструю сортировку на JavaScript и бинарный поиск. Ведь пользователь может ввести и часть имени. Не буду приводить сортировку, только бинарный поиск для примера:
function less_than_compare(val1, val2)
{
return val1 < val2;
}
function binary_search(items,value,key,compare)
{
key = key || "key";
compare = compare || less_than_compare;
var l = -1;
var r = items.length;
while (true)
{
var m = (r - l) >> 1;
if (m == 0) return r;
m += l;
if ( compare(items[m][key],value) )
l = m;
else if ( compare(value, items[m][key]) )
r = m;
else
return m;
}
}
* This source code was highlighted with Source Code Highlighter.
Итак, после упорного кодирования наш замечательный dropdown заработал:
www.picamatic.com/show/2009/05/12/06/35/3618111_557x236.png
Теперь начнем разбираться с формами. Форма разделяется на три части: HTML-код, JavaScript-код настройки и список справочников, которые требуются форме для работы. Наш серверный скрипт формы будет возвращать все эти части по запросу и кроме того, он должен принимать данные и возвращать ошибки их обработки. Серверный скрипт в файле "/foo.js" выглядит так:
function receive_request()
{
if (request._command == "jason") return "/joo.js,/boo.js";
if (request._command == "html") return "\
<tr><th>Ваше имя <td><INPUT name=user_name>\
<tr><th>Ваш пол <td><INPUT name=sex truevalue='u'>\
<tr><th>Страна происхождения <td><INPUT name=birth_country>\
<tr><th>Страна проживания <td><INPUT name=country>\
";
if (request._command == "code") return "\
enterAsTab(form, 1);\
$(form.country).dropdown( jason('/boo.js') );\
$(form.birth_country).dropdown( jason('/boo.js') );\
$(form.sex).dropdown( jason('/joo.js') );\
";
if (request._command == "post")
{
var error = "";
if (request.user_name.length == 0) error += "user_name: Укажите имя пользователя\r";
if (request.user_name.length < 5) error += "user_name: В имени должно быть как минимум 5 символов\r";
if (request.birth_country.length == 0) error += "country: Укажите страну происхождения\r";
if (request.country.length == 0) error += "country: Укажите страну проживания\r";
if (error) return error;
// .. ура ..
return "OKAY";
}
return "";
}
* This source code was highlighted with Source Code Highlighter.
Почему на сервере используется JavaScript объясняется здесь: habrahabr.ru/blogs/development/48842
HTML-код это только контролы, без окружающего "<form />". Немного хитрый настроечный код формы требует два параметра: DOM-элемент формы (form) и функцию, которая возвращает загруженный справочник по его url ( jason() ).
Итак, мы хотим все это на страницу асинхронно загружать и использовать. Тут есть два соображения. Во-первых, справочники принадлежат всей странице, а не одной форме. Разные формы могут работать с одними и теми же справочниками. Во-вторых, формой можно пользоваться только после полной и успешной загрузке всех ее составляющих.
Собственно, дальше я написал асинхронный загрузчик, который кеширует загруженные данные и позволяет иерархически вкладывать загрузки друг в друга (чтобы, допустим, полностью удалить все активные загрузки формы). Затем поверх этого — загрузчик форм, который, получив список справочников, просит их загрузить и ждет их появления.
Итак, допустим у нас есть под рукой (загружены на страницу) все необходимые компоненты формы: HTML, javascript и справочники готовы. Следующий шаг — создание _экземпляра_ формы, который и есть то, что видит пользователь. Вот код, который создает экземпляр формы формы по имени form_name в DOM-элементе container. Экземпляр получает имя instance_name:
activate: function(instance_name, form_name, container)
{
if (!form_loader.ready(form_name)) return false;
var form = document.createElement("form"); \\ для того, чтобы настроечный код формы обращался к ее DOM-элементу через form
var jason = function (url) { return jason_loader.data(url); } \\ для того, чтобы настроечный код формы обращался к справочнику через функцию jason()
container.innerHTML = '';
container.appendChild(form);
form.innerHTML = "<table class='form' >" + form_loader.html(form_name) + "</table>";
try
{
eval(form_loader.code(form_name));
}
catch (e)
{
form_loader.forms[form_name].error = 'Ошибка активации формы <i>' + form_name + '</i><BR>' + e.message;
return false;
}
this.actor[instance_name] = { type:'form', name: form_name, form_elem: form, container_elem: container, disabled: [], post: null };
return true;
}
* This source code was highlighted with Source Code Highlighter.
Думаю, идея понятна.
Ну и наконец: десерт. То ради чего весь этот код создавался. Вот как выглядит загрузочный код HTML-страницы, которая использует данный механизм:
$(document).ready(function(){
register_form("foo", "/foo.js"); // регистрация формы foo по адресу /foo.js
register_form("voo", "/voo.js"); // и еще другой формы
await(load_callback); // ждем окончания загрузки всех форм
});
function load_callback(form_name)
{
if (form_name) // при загрузке этой формы возникла ошибка
$('#report').html( $('#report').html() + '<BR>' + form_loader.error(form_name) );
else
{
// все в порядке! можно использовать формы
}
}
* This source code was highlighted with Source Code Highlighter.
Не правда ли — весьма лаконично? А вот как используется форма.
Создать экземпляр в DOM-элементе с id='foo_form':
director.activate('foo_instance', "foo", $('#foo_form')[0]);
Загрузить данные в экземпляр формы:
director.fill("foo_instance", {user_name: 'John Smith', country: 'RO' } );
Ну и наконец ради чего все это нужно — отправка данных:
director.submit("foo_instance", foo_submit);
Функция foo_submit может выглядеть примерно так:
function foo_submit(instance_name, result, error)
{
if (result)
$('#report').html('Форма ' + instance_name + ' успешно отправлена');
else if (error)
$('#report').html('При отправке формы ' + instance_name + ' произошла сетевая ошибка<BR>' + error);
else
$('#report').html('Проверьте данные и повторите отправку формы ' + instance_name);
}
* This source code was highlighted with Source Code Highlighter.
Метод submit() может получить от серверного скрипта набор ошибок по данным и покажет их автоматически. А при очередной отправке уберет и сделает полный disable формы.
Вообщем, с формой можно сделать практически все что угодно: показать, очистить, загрузить данными, сделать полный disable и обратно enable, отправить на сервер и ждать успешной отправки.
Наконец, убрать экземпляр формы со страницы. Опять-таки, мы можем на странице без труда создать несколько экземпляров одной и той же формы.
К примеру, можно в таких формах показывать неудачные отправки на сервер.
Вот такое вот решение.
Жду замечаний и предложений. Если найду время — сделаю syntax highlight кода на JavaScript (а почему нет под рукой на Хабре??).