Master / detail
Содержание |
Когда [форма редактирования|draw_form] содержит десяток-другой select'ов, довольно часто оказывается так, что при выборе значения в одном из них должны перерисовываться один или несколько остальных.
Пример
Сразу опишем условия модельной задачи, в рамках которой пойдёт дальнейшее описание.
Допустим, в нашей БД имеется таблица рабочих групп groups и (как всегда) таблица пользователей системы users со ссылкой id_group на groups.
Мы верстаем форму со списками выбора id_group и id_user:
{ name => 'id_group', type => 'select', values => $data -> {groups}, empty => '[Выберите группу]', }, { name => 'id_user', type => 'select', values => $data -> {users}, empty => '[Выберите пользователя]', },
данные для справочников извлекается так:
add_vocabularies ($data, groups => {}, users => {}, )
Наша цель -- сделать так, чтобы во втором select'е показывались только те пользователи, которые относятся к выбранной группе.
Шаг 1. Устанавливаем зависимость
Прежде всего, дадим системе знать, что id_user как-то зависит от id_group. Как именно -- уточним позже. А пока просто заменим первое определение на
{ name => 'id_group', type => 'select', values => $data -> {groups}, empty => '[Выберите группу]', detail => 'id_user', # <-- вот она где прописана },
Смотрим, что получилось. На первый взгляд, всё работает по-прежнему. Однако, если присмотреться внимательно, cтанет заметно, что при выборе строки в первом списке второй немного подмигивает и передёргивается.
Что происходит?
Включив любой HTTP-сниффер, нетрудно заметить, что теперь при любом изменении id_group на сервер отправляется запрос со специальными параметрами, которые не встречаются при обычной работе приложения. Вот они:
- __only_field -- имя единственного поля, которое будет перерисовываться;
- __only_form -- имя формы, которая его содержит.
В ответ сервер присылает HTML-страницу, всё содержимое которой сводится к onload-скрипту. А скрипт этот перерисовывает заказанное поле, подменяя его innerHTML на новый.
Откуда берётся HTML для обновлённого поля? Ровно оттуда же, откуда для целой страницы (его формирует [$_SKIN] по результатам работы get_item_of...), только из целого экрана вырезается единственное поле и обрамляется в скрипт.
Как сервер понимает, что ему нужно выдать не полную страницу, а обновляющий скрипт? По наличию параметров __only_form/__only_field.
Если вы когда-либо имели дело с JSF2, то тамошнее частичное обновление через AJAX -- прямой аналог того, о чём написано в этой статье.
Шаг 2. Корректируем выборку данных
Всё это интересно, но прогресс пока невелик: в $data -> {users} по-прежнему полный список пользователей, а не состав выбранной группы. Ну так отфильтруем же его. Что нам нужно? id_group? Посмотрим, что там приходит вместе с параметрами __only_form/__only_field... Так и есть: _id_group, как и в do_update, например. Значит, так и пишем:
[add_vocabularies] ($data, groups => {}, users => {filter => "id_group=$_REQUEST{_id_group}"}, )
Стоп! Легко сказать: "id_group=$_REQUEST{_id_group}". Но ведь $_REQUEST {_id_group} может быть пустым. Тогда получится битый SQL.
Шаг 3. Учитываем пустые значения
Давайте спокойно разберёмся, какие вообще возможны варианты get_item_of:
- вызов для отрисовки полного экрана:
- существующей записи:
- новой записи
- в режиме __read_only
- в режиме __edit
- вызов ради перерисовки id_user;
По поводу этого обилия возможностей можно написать соответствующий развесистый if, но можно сделать проще и красивее: прогарантировать корректное значение $_REQUEST{_id_group} во всех случаях.
Явное значение параметра фильтра
Допустим, то или иное значение $_REQUEST {_id_group} пришло в запросе. Этот случай обладает наивысшим приоритетом: клиент знает, какой id_group он хочет. В частности, он может хотеть id_group=0 -- это клик по пустой строке в списке групп. Заказывая такое, он должен получить пустой список users: ведь у вас нет users с id_group=0, (не IS NULL, а =0) не правда ли? А может быть, есть -- тогда их должны найти и найдут. Так или иначе, если $_REQUEST {_id_group} задан (не истинен -- а именно задан! Поскольку значение '0' необходимо уважать), то необходимо использовать его. Итак, начало нашей строки кода будет иметь вид:
defined $_REQUEST {_id_group} or $_REQUEST {_id_group} = ...
или, если вы уверены, что ваш код будет исполняться на Perl не ранее 5.10, то
$_REQUEST {_id_group} //= ...
Значение, сохранённое в БД
А если $_REQUEST {_id_group} всё-таки не определён? Тогда имеет смысл использовать значение из записи БД. Если её извлекли, как всегда, в переменную $data, то
defined $_REQUEST {_id_group} or $_REQUEST {_id_group} = $data -> {id_group}
$_REQUEST {_id_group} //= $data -> {id_group}
Это сгодится и для __read_only-режима, и для первого показа в __edit-режиме, пока форму ещё не трогали. Однако...
Значение по умолчанию
...запись может быть совсем новой, с пустым полем id_group. На этот случай надо использовать такое значение, чтобы оно было корректно, но никаких дочерних записей бы не нашлось:
defined $_REQUEST {_id_group} or $_REQUEST {_id_group} = $data -> {id_group} || -1;
$_REQUEST {_id_group} //= $data -> {id_group} || -1;
Вот теперь -- всё. Форма с master / detail-списками будет корректно работать во всех режимах.
Résumé
Итак, для установления зависимости между справочниками нам понадобилось ровно 3 строки кода:
1. Уточнение $_REQUEST {_id_group} в get_item_of:
defined $_REQUEST {_id_group} or $_REQUEST {_id_group} = $data -> {id_group} || -1;
2. Фильтрация справочника users с учётом уточнённого $_REQUEST {_id_group} в get_item_of:
users => {filter => "id_group=$_REQUEST{_id_group}"},
3. Объявление связи справочников в draw_item_of:
detail => 'id_user',
Дополнительные сведения
Множественное влияние
Изменение одного списка выбора может требовать перерисовки не одного, а нескольких подчинённых списков. В этом случае всё обстоит ровно так же, как описано выше, только опция detail имеет векторное значение:
detail => ['id_user', 'id_project', ...],
Косвенная и множественная зависимость
Список, упомянутый как detail, сам может иметь опцию detail. Цепочки detail-зависимостей могут быть столь длинными, сколько у вас полей на форме. Кроме того, один список может упоминаться как detail сразу у нескольких.
Но тут возникает одна небольшая проблема. Дело в том, что в вышеописанном рецепте при перерисовке подчинённого списка на сервер приходит значение только того параметра, который связан с непосредственно "кликнутым" списком. Если id_user зависит не только от id_group, но ещё и от id_role, то при выборе группы параметр $_REQUEST {_id_group} будет опредеён, а вот $_REQUEST {_id_role} -- нет. Хотя переключатель роли могли затронуть прошлым кликом и его значение отличается от $data -> {id_role}.
Но это не беда: достаточно указать зависимости опцией master:
master => ['id_group', 'id_role'],