Сложные интерфейсы на javascript вместе c Yahoo UI. Часть 18

Эта статья завершит рассказ об одном из самых "больших" и полезных компонентов в библиотеке Yahoo UI - компоненте DataTable. DataTable служит для отображения на веб-страницах информации в форме таблиц. В последних двух статьях я рассказал почти обо всех возможностях DataTable. Остались нераскрытыми только те функции DataTable, которые связаны с редактированием содержимого таблицы.

В прошлой статье я рассказывал, как можно настроить правила выделения строк в таблице. Как разрешить выделять одну строку или целый их диапазон, как реализовать динамическую подсветку строки, над которой в этот момент времени находится курсор. Все это было подготовительными шагами для того, чтобы превратить DataTable из средства только отображения табличных данных в инструмент, позволяющий редактировать данные в таблице и даже отправлять информацию назад на сервер (сохранять ее в БД). Разрешив пользователю выделять в конкретный момент времени только одну строку, я хочу реализовать модель редактирования содержимого таблицы с помощью специальной (моей) панели. Эта панель (состоящая из множества текстовых полей, наборов радиокнопок, падающих списков) будет расположена ниже самой таблицы с записями. Как только пользователь выделит любую запись в таблице, то элементы управления панели редактирования будут наполнены информацией. А когда пользователь переходит (выделяет) другую запись в таблице, то необходимо изменить внешний вид таблицы и отправить запрос сохранения отредактированной записи на сервер. Каждый из этих шагов имеет свои подводные камни и может превратиться не в одну сотню строк кода, если делать все качественно. К примеру, должно ли редактирование содержимого некоторого поля на панели внизу таблицы приводить к одновременному изменению содержимого соответствующего столбца таблицы? Следует ли блокировать навигацию пользователя по таблице до тех пор, пока отправленный запрос сохранения исправленной записи на сервер не вернулся с подтверждением "да, все правки были сохранены". Или, может быть, нужно ставить запрос на сохранение в асинхронную очередь — так, чтобы пользователь продолжал работу, а мы тем временем могли бы накопить "пачку правок", чтобы послать их на сервер все вместе. Такой вариант имеет смысл в случае, если количество правок очень велико, равно как и затраты времени на их обработку. Где выполнять валидацию введенных пользователем данных на предмет их корректности, как ее грамотно разделить между клиентом и сервером — так, чтобы не нагружать сервер множеством "мелких" запросов, возникающих по мере того, как пользователь редактирует информацию? Как быть, если некоторые из полей записи представлены в виде падающих списков, значения которых берутся из базы данных? Следует ли кэшировать эти списки на время всего сеанса работы, а может, перезагружать с каким-то интервалом времени или при начале редактирования очередной записи? Следует ли после отправки запроса на сохранение данных на сервер загружать с него же обновленный перечень записей (то, что могло быть параллельно отредактировано другим пользователем) или изолировать сеансы разных пользователей друг от друга?

Я оставлю все это за кадром и сосредоточусь на ключевых особенностях именно YUI: о том, как назначить обработчик события "выделена строка", как узнать то, какая строка выделена, как извлечь из нее информацию для всех колонок и как "на лету", без перезагрузки DataTable, изменять содержимое ячеек.
Начнем с простого: создадим заготовку панели редактирования записи. Это будут четыре текстовых поля с идентификаторами (id), имена которых совпадают с именами полей таблицы:

<div id="editorPanel">
fio: <input type="text" id="fio" /> <br />
birthday: <input type="text" id="birthday" /> <br />
sex: <input type="text" id=" sex" /> <br />
salary: <input type="text" id="salary" /> <br /> </div>

Следующим шагом я создам функцию, которая будет получать извещения о том, что пользователь выделил какую-то строку таблицы:
table.subscribe("rowSelectEvent", onSelectRow);

Функция, обрабатывающая события "onSelectRow", устроена очень просто: в качестве параметров она получает ссылку на объект el (строка таблицы) и объект record, внутри которого хранится ассоциативный массив со всеми полями записи. Осталось только найти на html-странице текстовые поля, соответствующие каждой из колонок таблицы, и наполнить их содержимым.
function onSelectRow (oArgs){
var el = oArgs['el'];
record = oArgs['record'];
YAHOO.util.Dom.get('fio').value = record.getData ('fio');
// и так для остальных полей }

Результат выполнения примера показан на рис.1. Пример не идеален, так как при переходе по страницам paginator-а событие "запись была изменена" не генерируется и, следовательно, панель редактирования отображает устаревшие данные. В следующем примере я подписываюсь на сообщение renderEvent, которое выбрасывается всякий раз, когда таблица перерисовывается (при начальной загрузке данных в DataTable, а также при переходе по страницам, изменении направления сортировки). Устройство же функции onRender тривиально: я получаю ссылки на все текстовые поля в панели редактирования и очищаю их.

table.subscribe("renderEvent", onRender);
function onRender (oArgs){
YAHOO.util.Dom.get('fio').value = '';
// и так все остальные поля }

Рассказанного мною уже должно хватить на реализацию простенькой редактируемой DataTable, выполняющей асинхронную отправку изменений на сервер. То есть внутри функции, обрабатывающей событие rowSelectEvent, необходимо проверить условие, что ранее уже была выбрана запись и то, что те значения, которые сейчас находятся в текстовых полях панели редактирования, отличаются от оригинальных значений записи таблицы. Если это так (запись была изменена), то нужно сформировать ajax-запрос на сервер (об использовании функции YAHOO.util.Connect.asyncRequest я рассказывал в статье № 5). Недостаток подобного алгоритма очевиден: в случае, если асинхронно выполняемая операция сохранения была неудачна, то пользователь уже перешел на другую запись и, следовательно, потерял все свои правки. Давайте лучше заблокируем DataTable до тех пор, пока с сервера не придет ответ, подтверждающий завершение операции сохранения. К сожалению, среди всего множества событий, "выбрасываемых" DataTable, нет события "до перехода на новую запись". Такого события, чтобы мы могли внутри функции обработчика "до перехода на другую запись" проверить ряд условий и отменить (блокировать) переход, пока сохранение не будет завершено. С другой стороны, реализовать блокировку перехода на запись легко с помощью следующего алгоритма:

var lastSelected = null;
var selectAfterSaving = null;

function onSelectRow (oArgs){
var dom = YAHOO.util.Dom;
var mustLock = false;
record = oArgs['record'];
if (requstedToSelectAfterSaving != null) return;
if (lastSelected != null){
oldData = this.getRecord (lastSelected).getData();
mustLock = ( dom.get('fio').value != oldData['fio'] || dom.get('birthday').value != oldData['birthday'] || dom.get('sex').value !=
oldData['sex'] || dom.get('salary').value != oldData['salary'])
}
if (mustLock){
selectAfterSaving = this.getLastSelectedRecord ();
alert ('saving data');
var lastSelected2 = lastSelected;
lastSelected = null;
this.unselectAllRows ();
this.selectRow ( lastSelected2 );
this.disable (); }
else{
dom.get('fio').value = record.getData ('fio');
dom.get('birthday').value = record.getData ('birthday');
dom.get('sex').value = record.getData ('sex');
dom.get('salary').value = record.getData ('salary');
lastSelected = this.getLastSelectedRecord ();
} }

Код кажется большим и сложным, но на самом деле все построено вокруг двух переменных — lastSelected и selectAfterSaving. У нас есть две записи: "старая" и "новая". Изначально была выделена "старая" запись, а ее идентификатор сохранен в переменную lastSelected. Теперь пользователь хочет выделить, то есть перейти на "новую" запись. Проверим, можно ли это пользователю разрешить, или переход нужно блокировать на время сохранения? Функция обработчик события "выделена запись" с помощью вызова this.getRecord (lastSelected) получит из DataTable содержимое "старой" записи. Потом сверит значения полей "старой записи" с теми значениями, которые введены в текстовые поля. Если пользователь ничего не отредактировал (значения совпали), то я наполняю текстовые поля панели редактирования новыми данными и сохраняю как "старую" запись ту, на которую только что был выполнен переход (getLastSelectedRecord).

В противном случае последовательность шагов сложнее. В переменную selectAfterSaving я сохраняю номер записи, на которую пользователь хочет перейти ("новая" запись). Ее номер мне нужен по двум причинам. Во-первых, когда я завершу ajax-вызов, сохраняющий данные на сервер (в примере он имитируется вызовом alert), то было бы неплохо завершить переход на ту запись, на которую пользователь и хотел перейти ("новую запись"). Второе назначение переменной selectAfterSaving заключается в блокировке бесконечной рекурсии. Которая неизбежно возникнет, если я из обработчика события "была выделена другая запись" попробую выделить запись, ведь это в свою очередь должно привести к выбрасыванию события "запись была изменена". Внутри функции обработки события "выделена запись" я должен четко разграничивать два случая: запись была выделена пользователем, и запись была выделена программно, из javascript, в ходе операции сохранения изменений в DataTable. Зачем мне нужно выделять еще какую-то запись? Прежде всего, не "какую-то" запись, а "старую запись". Так как YUI извещает нас о том, что выполняется переход на запись, фактически после того, как он уже состоялся, то нам нужен механизм "отката изменений", то есть нужно вернуться (выделить) старую запись. Если этого не сделать, то в случае, когда при сохранении изменений произошел сбой, может возникнуть несогласованность интерфейса: когда визуально в DataTable выделена (подсвечена) запись, не имеющая никакого отношения к той, которую не удалось сохранить. После того как отработает ajax-запрос, я снимаю текущее выделение и снова выделяю предыдущую запись (ведь ее номер мы ранее сохранили).

Завершающим штрихом будет визуальное изменение DataTable, например, ее цвета на серый disabled и запрет на любые действия с таблицей. Для этого служит (правда, не слишком удачно) вызов метода disable. Почему метод disable работает неудачно? Хотя внешний вид таблицы изменился, равно как было заблокировано и выделение записей, сортировка столбцов, изменение их ширины (для того, чтобы вернуть DataTable в рабочее состояние, используйте вызов enable). Так вот, проблема в том, что если таблица использует paginator для отображения большого количества записей, то внешний вид и функциональность paginator-а никак не изменятся от применения disable на "родительском" компоненте DataTable. Получается довольно забавный эффект, показанный на рис.2. Больших проблем этот "бажок" YUI не вызывает, так как на момент отправки запроса обойтись просто затемнением DataTable не получается: некрасиво и не понятно клиенту, что там происходит с интерфейсом. Я предпочитаю делать так: помещаю и DataTable, и paginator внутрь общего блока div. Затем, перед началом отправки запроса, я меняю стилевое оформление контейнера div так, чтобы создать эффект затемнения, закрывающий и DataTable и paginator, а посередине div-а размещается gif картинка с анимацией, например, часов, подсказывающая о том, что сейчас идет выполнение запроса сохранения данных и нужно немного подождать. Какая информация будет возвращаться из php- скрипта, обслуживающего DataTable, решается в каждом случае индивидуально. Самый простой вариант, когда javascript код в браузере просто получает одно из двух значений: либо saved, либо failed (была операция сохранения удачной или нет). В некоторых случаях вместе с признаком успешности завершения операции возвращается содержимое записи.

Но зачем, ведь те же самые данные мы отправили минутой ранее на сервер для сохранения? Для "больших" систем характерно хранение данных в таблицах, обслуживаемых триггерами или хранимыми процедурами. Триггер вызывается при выполнении операций сохранения записей в таблице и может изменить данные перед их действительным сохранением. А это значит, что данные, которые "ушли" на сервер, могут быть совсем не теми, что были сохранены в таблицах БД. Еще более сложные действия предстоят, когда пользователь изменил значение поля, к которому применена сортировка, например, сменил ФИО сотрудника с "Иванов" на "Сидоров". В этом случае отредактированная запись не должна отображаться на текущей странице DataTable: нам нужно вернуть с сервера сразу 10 записей (содержимое пересортированной страницы). Не уходя далеко от рассмотрения YUI DataTable, давайте задумаемся над тем, как динамически изменять информацию, отображаемую в таблице, без ее перезагрузки. Рассмотрим эту задачу на примере функции "живого редактирования". То есть по мере ввода пользователем нового значения для какого-то из полей таблицы в текстовом поле панели редактирования, внешний вид связанной с этим полем колонки также должен меняться. Делается все очень просто: назначим каждому из текстовых полей в панели редактирования функцию, реагирующую на изменения такого текстового поля:

YAHOO.util.Event.addListener("fio", "keyup", onChangeFIO );
Функция onChangeFIO вызывается всякий раз, когда содержимое текстового поля изменяется при нажатии любой из клавиш. Есть несколько способов изменить информацию в DataTable, например, так:
var data = table.getRecord(lastSelected).getData();
data ['fio'] = YAHOO.util.Dom.get('fio').value;
table.updateRow (lastSelected, data);

Здесь и далее предполагается что lastSelected — это идентификатор той записи, значение поля fio для которой мы и хотим поменять. Функции updateRow получает первым параметром номер записи, подлежащей изменению, а второй параметр задает ассоциативный массив (объект) с новыми значениями полей. Внешний вид ячеек строки незамедлительно будет изменен. Небольшой недостаток использования updateRow в том, что зарегистрированные слушатели событий получат извещение о том, что "таблица была изменена", и могут оказаться не готовы к этому. Есть и второй прием, в котором мы изменяем два объекта: сначала RecordSet, привязанный к DataTable (так, чтобы если в дальнейшем мы запросим для записи значения ее полей, то сможем получить их измененные величины). И вторым шагом нужно изменить внешний вид ячейки таблицы:

var reqTd = {record:lastSelected, column: table.getColumn ('fio') }
table.getTdLinerEl(reqTd).innerHTML = YAHOO.util.Dom.get('fio').value;
table.getRecord(lastSelected).setData('fio', YAHOO.util.Dom.get('fio').value);
table.getRecordSet().updateRecordValue (selected, 'fio', YAHOO.util.Dom.get('fio').value);

Первая строка создает объект, хранящий номер записи и ссылку на колонку fio. Вызов метода getTdLinerEl вернет ссылку на ячейку таблицы, значение которой (innerHTML) я хочу изменить. Естественно, что в этом случае я теряю возможности по автоматическому форматированию содержимого ячейки в зависимости от ее типа данных, которые предоставляет YUI (formatter-ы). Третья и четвертая строка служат для изменения содержимого RecordSet-а и по своему поведению идентичны. Какой подход использовать — с раздельным изменением ячеек DataTable или с помощью функции updateRow – решать вам самим.

Я хочу завершить рассказ о DataTable, показав, как можно выполнять редактирование ячеек таблицы inline. То есть при выделении ячейки таблицы, ее внешний вид меняется, и на экране показывается компонент-редактор. Привязать к любой из колонок таблицы специализированный редактор очень просто. Когда мы создаем массив объектов, описывающий характеристики колонок таблицы, то наряду с такими уже знакомыми нам свойствами, как функция форматирования содержимого ячейки, признак того, можно ли сортировать колонку, мы можем указать свойство editor (редактор ячейки). В следующем примере (результат выполнения см. на рис.3) я показываю таблицу с редактором полей типа "дата-время", еще редактор в виде диалогового окна с двумя переключателями радиокнопками для выбора пола сотрудника. Есть также редактор в виде диалогового окна с набором checkbox-ов, с выпадающим списком вариантов и многое другое:

// как обычно, описываем колонки таблицы
var columns = [
{key:"fio", sortable:true},
{key:"birthday", sortable:true, formatter:YAHOO.widget.DataTable.formatDate, editor: new YAHOO.widget.DateCellEditor() },
{key:"sex", editor: new YAHOO.widget.RadioCellEditor({radioOptions:["male","female"], disableBtns:true}) },
{key:"salary", sortable:true} ];
// а по выделению ячейки нужно показать редактор ячейки
table.subscribe("cellClickEvent", table.onEventShowCellEditor);

black-zorro@tut.by black-zorro.com


Компьютерная газета. Статья была опубликована в номере 04 за 2009 год в рубрике программирование

©1997-2022 Компьютерная газета