Мне нравится как сделан github, поэтому я решил показать, как можно реализовать элементы управления в его стиле. Самый простой элемент (он встречается там достаточно часто) можно найти на странице проекта (если у вас есть проекты на github, то вы им безусловно пользовались), например строка с описанием проекта. По ней можно кликнуть мышкой, в результате чего появится форма, в которой можно редактировать это описание. Несколько обобщив концепцию данного элемента можно получить следующую схему:
- Элемент модели данных (хранящийся на сервере) описывается с помощью фрагмента разметки
- Пользователь может активировать процесс редактирования этого элемента с помощью клика мышкой или иным образом
- В результате изначальная разметка скрывается и вместо неё появляется форма для ввода данных
- Пользователь может либо сохранить внесённые изменения, либо отказаться от них, в результате чего форма для ввода данных скрывается и снова появляется изначальная разметка, но модифицированная на основе введённых данных
Используемое мной решение заключается в том, что бы не заниматься "тонким редактированием" разметки, а производить полное её замещение на основе шаблонов, реализованных с помощью cl-closure-template: изменилась модель - создали новую разметку и подставили её вместо старой.
Для начала потребуются следующие шаблоны (которые в основном повторяют соответствующую разметку, используемую в github на момент написания данного поста):
{namespace example.githubway.view}Эти шаблоны принимают следующие аргументы:
// Представление текстового элемента
{template editable-text}
<div class="editable-text" json="{$json}">
<p>
{$value}
<em class="edit-text">click to edit</em>
</p>
</div>
{/template}
// Форма для редактирование элемента
{template edit-text}
<form method="post" action="{$saveLink}" json="{$json}">
<input type="text" value="{$value}" name="value"></input>
<div class="form-actions">
<input type="submit" class="minibutton save" value="Save"></input>
<span class="fakelink cancel">cancel</span>
</div>
</form>
{/template}
- value - значение элемента
- saveLink - URL, который может использоваться для обновления значения элемента с помощью POST-запроса
- json - а вот это любопытно, это предыдущие аргументы в формате JSON. Зачем это надо? Напомню, что cl-closure-template позволяет создавать шаблоны, доступные как на сервеной, так и на клиентской стороне. Я хочу, что бы JavaScript-код получил полную информацию об элементе модели, которую он сможет в последующем использовать для передачи в эти же шаблоны для генерации новой разметки. Для этого я сохраняю эти данные в атрибуте json корневого элемента генерируемой разметки, что существенно упрощает последующую обработку.
function EObject (node) {Данный код использует библиотеке jquery и плагины jquery.form и jquery.json. Теперь, создать код для редактирования текстового элемента очень просто:
// node - узел DOM-дерева, представляющего элемент данных
if (node) {
this.node = node;
}
}
// Возвращает описание элемента модели в виде
// пригодном для генерации разметки с помощью шаблонов
EObject.prototype.modelData = function () {
var data = $.evalJSON(this.node.attr("json"))
data.json = $.toJSON(data);
return data;
};
// Метод для генерации основной разметки
EObject.prototype.toHTML = function (data) {
throw "Method toHTML not implemented";
};
// Метод для генерации формы редактирования
EObject.prototype.editForm = function (data) {
throw "Method editForm not implemented";
};
// Вспомогательный метод, позволяющий заменить разметку элемента
// таким образом, что бы в объекте осталась ссылка на актуальный
// элемент DOM-дерева
EObject.prototype.replaceHTML = function (html) {
this.node.after(html);
this.node = this.node.next();
this.node.prev().remove();
};
// Заменяет основное представление на форму редактирования
EObject.prototype.startEdit = function () {
this.replaceHTML(this.editForm(this.modelData()));
var obj = this;
$('.cancel:first', this.node).click(function (evt) { obj.endEdit(); });
this.node.ajaxForm({
dataType: 'json',
success: function (data) { obj.endEdit(data)},
error: function () { alert("Не удалось сохранить данные"); obj.endEdit() }
});
};
// Заменяет форму редактирования на основной вариант разметки
EObject.prototype.endEdit = function (data) {
this.replaceHTML(this.toHTML(data || this.modelData()));
// Похоже на хак, но мне кажется нормальным решением.
// Фактически, просто ре-инициализация объекта, которая,
// например, приведёт к активизации нужных обработчиков событий
this.constructor(this.node);
};
function EditableText (node) {И наконец инициализация при загрузке документа:
if (node) {
// вызываем конструктор базового класса
EObject.prototype.constructor.call(this, node);
// Инициируем вызов процедуру редактирования по клику мышкой
var obj = this;
this.node.click(function (evt) { obj.startEdit(); });
}
}
// Инициализируем прототип
EditableText.prototype = new EObject;
// Необходимо, что бы иметь возможность создать новый класс, наследующий от EditableText
EditableText.prototype.constructor = EditableText;
// Генерируем разметку с помощью ранее описанного шаблона editable-text
EditableText.prototype.toHTML = example.githubway.view.editableText;
// Генерируем форму редактирования с помощью ранее описанного шаблона edit-text
EditableText.prototype.editForm = example.githubway.view.editText;
$(document).ready(function () {Теперь серверная часть. Для демонстрации я использую очень простое приложение с одной страницей, на которой показывается имя и email некоего человека. Пользователь приложения может отредактировать их в стиле github. Код данного приложения зависит от:Сначала определим очень простую модель
// Тоже довольно любопытно. Просто создаём новые объекты "в никуда".
// Но благодаря привязке этих объектов к DOM-дереву через обработчики
// событий они останутся "жить" и будут выполнять свою работу
$(".editable-text").each( function (i, node) { new EditableText($(node)); })
});
(defparameter *name* "Ivan Petrov")Здесь функции name-to-json и email-to-json генерируют plist, подходящий для использования с шаблонами, описанными в начале.
(defparameter *email* "Ivan.Petrov@example.com")
(defun with-json (&rest args)
(list* :json (json:encode-json-plist-to-string args)
args))
(defun name-to-json ()
(with-json :value *name*
:save-link (genurl 'save-name)))
(defun email-to-json ()
(with-json :value *email*
:save-link (genurl 'save-email)))
Описываем маршрут для основной страницы:
(define-route main ("")Ради экономии места здесь я не буду приводить текст шаблона example.githubway.view:page, укажу лишь, что в нём есть следующие строчки:
(example.githubway.view:page (list :name (name-to-json)
:email (email-to-json))))
{call editable-text data="$name" /}Теперь определяем обработчики для обновления данных об имени и email, ради упрощения какая-либо обработка ошибок опущена:
{call editable-text data="$email" /}
(define-route save-name ("api/name" :method :post :content-type "application/json")Вот и всё. Полный код данного приложения я включил в состав closure-template и посмотреть его можно здесь.
(setf *name*
(hunchentoot:post-parameter "value"))
(json:encode-json-plist-to-string (name-to-json)))
(define-route save-email ("api/email" :method :post :content-type "application/json")
(setf *email*
(hunchentoot:post-parameter "value"))
(json:encode-json-plist-to-string (email-to-json)))
Продолжение следует...
Комментариев нет:
Отправить комментарий