Как стать автором
Обновить

Улучшаем формы или Веб-Восемь-Ноль-Сто-Три

Время на прочтение13 мин
Количество просмотров1.3K
Итак, мы выбрали браузер вместо отдельно стоящего толстого клиента. Пользователь очень хочет вводить данные. Однако, обычные формы плохие. Потому, что:



Данные и 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 заработал:

imagewww.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 формы.

image

Вообщем, с формой можно сделать практически все что угодно: показать, очистить, загрузить данными, сделать полный disable и обратно enable, отправить на сервер и ждать успешной отправки.
Наконец, убрать экземпляр формы со страницы. Опять-таки, мы можем на странице без труда создать несколько экземпляров одной и той же формы.
К примеру, можно в таких формах показывать неудачные отправки на сервер.

Вот такое вот решение.


Жду замечаний и предложений. Если найду время — сделаю syntax highlight кода на JavaScript (а почему нет под рукой на Хабре??).

Теги:
Хабы:
+19
Комментарии52

Публикации

Изменить настройки темы

Истории

Работа

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн