Итак, мы выбрали браузер вместо отдельно стоящего толстого клиента. Пользователь очень хочет вводить данные. Однако, обычные формы плохие. Потому, что:
Данные и 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 (а почему нет под рукой на Хабре??).