Первая статья про dap, очевидно, не стала моим писательским успехом: подавляющее большинство коментов к ней свелись к «ниасилил» и «ниасилил, но осуждаю». А приз за самый единственный конструктивный комментарий верхнего уровня достается OldVitus, за совет продемонстрировать dap на примере TodoMVC, чтобы было с чем сравнить. Чем я в этой статье и займусь.
TodoMVC, если кто не знает, это такой стандартный UI-хелловорлд, позволяющий сравнить решения одной и той же задачи — условного «Списка дел» — средствами разных фреймворков. Задачка, при всей своей простоте (ее решение на dap влезает «в один экран»), весьма иллюстративна. Поэтому на ее примере я попробую показать, как типичные для веб-фронтенда задачи реализуются с помощью dap.
Искать и изучать формальное описание задачи я не стал, а решил просто среверсить один из примеров. Бэкенд в рамках этой статьи нам не интересен, поэтому сами мы его писать не будем, а воспользуемся одним из готовых с сайта www.todobackend.com, оттуда же возьмем и пример клиента и стандартный CSS-файл.
Для использования dap вам не нужно ничего скачивать и устанавливать. Никаких
Итак, с чистого листа.
Обычная html-заготовка, в которой подключаем CSS-файл, любезно предоставляемый сайтом www.todobackend.com и dap-движок, не менее любезно предоставляемый сайтом dap.js.org
Чтобы пользоваться стандартным CSS-файлом без переделок, будем придерживаться той же DOM-структуры, что и оригинальный пример. Открываем его в браузере Chrome, жмем Ctr+Shift+I, выбираем вкладку Elements и видим, что собственно приложение находится в элементе

Последовательно раскрывая это поддерево, переписываем его структуру в наш .html файл. Сейчас мы просто срисовываем по-быстренькому, а не пишем код, поэтому просто пишем сигнатуры элементов в 'одинарных кавычках', а в скобках их детей. Если детей нет — рисуем пустые скобочки. Следим за индентами и балансом скобок.
Oбратите внимание: повторяющиеся элементы (например, здесь это элементы
Формат сигнатур, думаю, понятен любому, кто писал руками HTML и CSS, поэтому останавливаться на нем подробно пока не буду. Скажу лишь, что теги пишутся ЗАГЛАВНЫМИ буквами, а отсутствие тега равносильно наличию тега DIV. Обилие здесь #-элементов (имеющих id) обусловлено спецификой подключаемого CSS-файла, в котором используются в основном как раз id-селекторы.
Чтобы избавить нас от лишних скобочек в коде, dap-движок внедряет прямо в
Исходя из этого нового знания, дописываем наш код так, чтобы вместо каждой открывающей скобки у нас была последовательность
Вуаля! Мы получили дерево вызовов метода
Итак, на данном этапе имеем .html-файл вот с таким содержанием:
Можно открыть его в браузере, чтобы убедиться, что DOM-элементы генерятся, CSS-стили применяются, осталось только наполнить этот шаблон данными.
Идем на страничку-оригинал, открываем в инструментах вкладку Network, включаем фильтр XHR, и смотрим, откуда берутся данные, и в каком виде.


Окей, понятненько. Список дел берется прямо из todo-backend-express.herokuapp.com в виде json-массива объектов. Замечательно.
Для получения данных в dap имеется встроенный конвертор
а саму константу
Получив массив
Обновляем эту нашу страничку в браузере и… если вы запускаете ее из файловой системы, то ничего не происходит. Проблема в том, что современные браузеры не разрешают кросс-доменные XHR-запросы из локальных документов.

Пришло время смотреть нашу страничку через http — с помощью любого локального вебсервера. Ну, или если вы пока не готовы писать dap своими руками, смотрите последовательные версии странички по моим ссылкам (не забывайте смотреть исходники — в Хроме это делается с помощью Ctrl+U)
Итак, заходим на нашу страничку по http:// и видим, что данные приходят, список строится. Отлично! Вы уже освоили операторы
Возможно, вы уже попробовали понажимать на галочки в списке дел. Сами галочки меняют цвет, но, в отличие от оригинала, родительский элемент
Добавим элементу
Подобные манипуляции с CSS-классами — вещь довольно частая, поэтому для них в dap имеется специальный оператор
Обратите внимание, делаем мы это в а-правиле (от слова accumulate). Почему не в d-правиле? Отличие между этими двумя типами правил в том, что d-правило при обновлении полностью перестраивает содержимое элемента, удаляя старое и генеря все заново, тогда как a-правило не трогает имеющееся содержимое элемента, а «дописывает» результат к тому, что уже есть. Смена отдельного атрибута элемента
Смотрим на результат. Уже лучше: нажатия на галочки меняют состояние соответствующего элемента списка дел, и в соответствии с этим состоянием меняется и визуальный стиль элемента. Но все еще есть проблема: если в списке изначально присутствовали завершенные дела — они не будут серенькими, т. к. по умолчанию a-правило не исполняется при генерации элемента. Чтобы исполнить его и при генерации, допишем в d-правило элемента
Смотрим. Окей. С состоянием
Вернемся к оригиналу. При двойном клике по названию дела включается режим редактирования, в котором это название можно поменять. Там это реализовано так, что шаблон режима просмотра «view» (с галкой, названием и кнопкой удаления) целиком прячется, а показывается элемент
Для состояния «редактирование» определим в элементе
Теперь мы можем редактировать названия дел.
Ок, в браузере мы дела редактировать уже можем, но эти изменения нужно еще и передавать на сервер. Смотрим, как это делает оригинал:

Внесенные изменения отправляются на сервер методом PATCH с неким URL вида
Здесь мы используем все тот же конвертор
Но есть нюанс. Поле
Решается это просто: если убрать из
Еще имеет смысл вынести объект headers в словарь, чтобы использовать его и в других запросах:
Ну и для полного фэншуя воспользуемся еще одним полезным свойством конвертора
Итак, смотрим. Окей, названия дел теперь меняются не только в браузере, но и на сервере. Но! Меняться-то может не только название дела, но и его состояние завершенности —
Можно элементу
А можно вынести этот PATCH-запрос «за скобки»:
Тут дело вот в чем. Правила реакции относятся к группе «up-правил», которые исполняются «снизу вверх» — от потомка к родителю, до самого корня (эта последовательность может быть прервана при необходимости). Это чем-то похоже на «всплывающие» события в DOM. Поэтому какие-то фрагменты реакции, общие для нескольких потомков, можно поручить их общему предку.
Конкретно в нашем случае выигрыш от такого делегирования не особо заметный, но если бы редактируемых полей было больше, то вынос этого громоздкого (по меркам dap, конечно) запроса в одно общее правило сильно помог бы сохранять код простым и читабельным. Так что рекомендую.
Смотрим: Теперь на сервер отправляются и изменения названия, и изменения статуса.
В следующей статье, если будет интерес, рассмотрим добавление, удаление и фильтрацию дел. А пока можно посмотреть финальный результат и другие примеры dap-кода на dap.js.org/docs
TodoMVC, если кто не знает, это такой стандартный UI-хелловорлд, позволяющий сравнить решения одной и той же задачи — условного «Списка дел» — средствами разных фреймворков. Задачка, при всей своей простоте (ее решение на dap влезает «в один экран»), весьма иллюстративна. Поэтому на ее примере я попробую показать, как типичные для веб-фронтенда задачи реализуются с помощью dap.
Искать и изучать формальное описание задачи я не стал, а решил просто среверсить один из примеров. Бэкенд в рамках этой статьи нам не интересен, поэтому сами мы его писать не будем, а воспользуемся одним из готовых с сайта www.todobackend.com, оттуда же возьмем и пример клиента и стандартный CSS-файл.
Для использования dap вам не нужно ничего скачивать и устанавливать. Никаких
npm install
и вот этого всего. Не требуется создавать никаких проектов с определенной структурой каталогов, манифестами и прочей атрибутикой IT-успеха. Достаточно текcтового редактора и браузера. Для отладки XHR-запросов может еще потребоваться веб-сервер — достаточно простейшего, типа вот этого расширения для Chrome. Весь наш фронтенд будет состоять из одного-единственного .html-файла (разумеется, ссылающегося на скрипт dap-движка и на стандартный CSS-файл TodoMVC)Итак, с чистого листа.
1. Создаем .html файл
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Todo -- dap sample</title>
<link rel="stylesheet" href="https://www.todobackend.com/client/css/vendor/todomvc-common.css"/>
<script src="https://dap.js.org/0.4.js"></script>
</head>
<body>
<script>
// здесь будет dap
</script>
</body>
</html>
Обычная html-заготовка, в которой подключаем CSS-файл, любезно предоставляемый сайтом www.todobackend.com и dap-движок, не менее любезно предоставляемый сайтом dap.js.org
2. Копируем DOM-структуру оригинального примера
Чтобы пользоваться стандартным CSS-файлом без переделок, будем придерживаться той же DOM-структуры, что и оригинальный пример. Открываем его в браузере Chrome, жмем Ctr+Shift+I, выбираем вкладку Elements и видим, что собственно приложение находится в элементе
section id="todo-app">

Последовательно раскрывая это поддерево, переписываем его структуру в наш .html файл. Сейчас мы просто срисовываем по-быстренькому, а не пишем код, поэтому просто пишем сигнатуры элементов в 'одинарных кавычках', а в скобках их детей. Если детей нет — рисуем пустые скобочки. Следим за индентами и балансом скобок.
// здесь будет dap
'#todoapp'(
'#header'(
'H1'()
'INPUT#new-todo placeholder="What needs to be done?" autofocus'()
)
'#main'(
'#toggle-all type=checkbox'()
'UL#todo-list'(
'LI'(
'INPUT.toggle type=checkbox'()
'LABEL'()
'BUTTON.destroy'()
)
)
)
'#footer'(
'#todo-count'()
'UL#filters'(
'LI'()
)
'#clear-completed'()
)
)
Oбратите внимание: повторяющиеся элементы (например, здесь это элементы
LI
) мы пишем в структуру по одному разу, даже если в оригинале их несколько; очевидно, что это массивы из одного и того же шаблона. Формат сигнатур, думаю, понятен любому, кто писал руками HTML и CSS, поэтому останавливаться на нем подробно пока не буду. Скажу лишь, что теги пишутся ЗАГЛАВНЫМИ буквами, а отсутствие тега равносильно наличию тега DIV. Обилие здесь #-элементов (имеющих id) обусловлено спецификой подключаемого CSS-файла, в котором используются в основном как раз id-селекторы.
3. Вспоминаем, что dap-программа — это Javascript
Чтобы избавить нас от лишних скобочек в коде, dap-движок внедряет прямо в
String.prototype
несколько методов (я в курсе, что внедрять свои методы в стандартные объекты — это айяйяй, но… короче, проехали), которые преобразует строку-сигнатуру в dap-шаблон. Один из таких методов — .d(rule, ...children)
. Первым аргументом он принимает правило генерации (d-правило), и остальными аргументами — произвольное число чайлдов.Исходя из этого нового знания, дописываем наш код так, чтобы вместо каждой открывающей скобки у нас была последовательность
.d(""
, а перед каждой открывающей одинарной кавычкой, кроме самой первой, была запятая. Лайфхак: можно воспользоваться автозаменой.'#todoapp'.d(""
,'#header'.d(""
,'H1'.d("")
,'INPUT#new-todo placeholder="What needs to be done?" autofocus'.d("")
)
,'#main'.d(""
,'#toggle-all type=checkbox'.d("")
,'UL#todo-list'.d(""
,'LI'.d(""
,'INPUT.toggle type=checkbox'.d("")
,'LABEL'.d("")
,'BUTTON.destroy'.d("")
)
)
)
,'#footer'.d(""
,'#todo-count'.d("")
,'UL#filters'.d(""
,'LI'.d("")
)
,'#clear-completed'.d("")
)
)
Вуаля! Мы получили дерево вызовов метода
.d
, которое уже готово трансформироваться в dap-шаблон. Пустые строки ""
— это зародыши будущих d-правил, а чайлды стали перечисленными через запятую аргументами. Формально, это уже валидная dap-программа, хоть пока и не совсем с тем выхлопом, который нам нужен. Но ее уже можно запустить! Для этого после закрывающей корневой скобки дописываем метод .RENDER()
. Этот метод, как понятно из его названия, рендерит полученный шаблон.Итак, на данном этапе имеем .html-файл вот с таким содержанием:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Todo -- dap sample</title>
<link rel="stylesheet" href="https://www.todobackend.com/client/css/vendor/todomvc-common.css"/>
<script src="https://dap.js.org/0.4.js"></script>
</head>
<body>
<script>
'#todoapp'.d(""
,'#header'.d(""
,'H1'.d("")
,'INPUT#new-todo placeholder="What needs to be done?" autofocus'.d("")
)
,'#main'.d(""
,'#toggle-all type=checkbox'.d("")
,'UL#todo-list'.d(""
,'LI'.d(""
,'INPUT.toggle type=checkbox'.d("")
,'LABEL'.d("")
,'BUTTON.destroy'.d("")
)
)
)
,'#footer'.d(""
,'#todo-count'.d("")
,'UL#filters'.d(""
,'LI'.d("")
)
,'#clear-completed'.d("")
)
)
.RENDER() // рендерим полученный dap в документ
</script>
</body>
</html>
Можно открыть его в браузере, чтобы убедиться, что DOM-элементы генерятся, CSS-стили применяются, осталось только наполнить этот шаблон данными.
4. Получаем данные
Идем на страничку-оригинал, открываем в инструментах вкладку Network, включаем фильтр XHR, и смотрим, откуда берутся данные, и в каком виде.


Окей, понятненько. Список дел берется прямо из todo-backend-express.herokuapp.com в виде json-массива объектов. Замечательно.
Для получения данных в dap имеется встроенный конвертор
:query
который асинхронно «конвертирует» URL в данные, с него полученные. Сам URL мы не будем писать прямо в правиле, а обозначим его константой todos
; тогда вся конструкция по добыче данных будет выглядеть так:todos:query
а саму константу
todos
пропишем словаре — в секции .DICT
, прямо перед .RENDER()
:'#todoapp'.d(""
...
)
.DICT({
todos : "https://todo-backend-express.herokuapp.com/"
})
.RENDER()
Получив массив
todos
, строим из него список дел: для каждого дела берем название из поля .title
и пишем его в элемент LABEL
, а из поля .completed
берем признак «завершенности» — и пишем в свойство checked
элемента-чекбокса INPUT.toggle
. Делается это так: ,'UL#todo-list'.d("*@ todos:query" // Оператор * выполняет повтор для всех элементов массива
,'LI'.d(""
,'INPUT.toggle type=checkbox'.d("#.checked=.completed") // # обозначает "этот элемент"
,'LABEL'.d("! .title") // Оператор ! просто добавляет текст в элемент
,'BUTTON.destroy'.d("")
)
)
Обновляем эту нашу страничку в браузере и… если вы запускаете ее из файловой системы, то ничего не происходит. Проблема в том, что современные браузеры не разрешают кросс-доменные XHR-запросы из локальных документов.

Пришло время смотреть нашу страничку через http — с помощью любого локального вебсервера. Ну, или если вы пока не готовы писать dap своими руками, смотрите последовательные версии странички по моим ссылкам (не забывайте смотреть исходники — в Хроме это делается с помощью Ctrl+U)
Итак, заходим на нашу страничку по http:// и видим, что данные приходят, список строится. Отлично! Вы уже освоили операторы
*
и !
, конвертор :query
, константы и доступ к полям текущего элемента массива. Посмотрите еще раз на получающийся код. Он вам все еще кажется нечитаемым?5. Добавляем состояние
Возможно, вы уже попробовали понажимать на галочки в списке дел. Сами галочки меняют цвет, но, в отличие от оригинала, родительский элемент
LI
не меняет свой стиль («завершенное дело» должно становиться серым и зачеркнутым, но этого не происходит) — дела не меняют свое состояние. А никакого состояния эти элементы пока и не имеют и, соответственно, не могут его менять. Сейчас мы это поправим.Добавим элементу
LI
состояние «завершенности». Для этого определим в его d-правиле переменную состояния $completed
. Элементу INPUT.toggle
, который может это состояние менять, назначим соответствующее правило реакции (ui-правило), которое будет устанавливать переменную $completed
в соответствии с собственным признаком checked
(«галка включена»). В зависимости от состояния $completed
элементу LI
будем либо включать, либо выключать CSS-класс «completed». ,'UL#todo-list'.d("*@ todos:query"
,'LI'.d("$completed=.completed"// Переменная состояния, инициализируем из поля .completed
,'INPUT.toggle type=checkbox'
.d("#.checked=.completed") // Начальное состояние галочки берем из данных
.ui("$completed=#.checked") // при нажатии обновляем $completed
,'LABEL'.d("! .title")
,'BUTTON.destroy'.d("")
)
.a("!? $completed") // в зависимости от значения $completed, включаем или выключаем css-класс completed
)
Подобные манипуляции с CSS-классами — вещь довольно частая, поэтому для них в dap имеется специальный оператор
!?
Обратите внимание, делаем мы это в а-правиле (от слова accumulate). Почему не в d-правиле? Отличие между этими двумя типами правил в том, что d-правило при обновлении полностью перестраивает содержимое элемента, удаляя старое и генеря все заново, тогда как a-правило не трогает имеющееся содержимое элемента, а «дописывает» результат к тому, что уже есть. Смена отдельного атрибута элемента
LI
не требует перестройки остального его содержимого, поэтому рациональней это делать именно в a-правиле.Смотрим на результат. Уже лучше: нажатия на галочки меняют состояние соответствующего элемента списка дел, и в соответствии с этим состоянием меняется и визуальный стиль элемента. Но все еще есть проблема: если в списке изначально присутствовали завершенные дела — они не будут серенькими, т. к. по умолчанию a-правило не исполняется при генерации элемента. Чтобы исполнить его и при генерации, допишем в d-правило элемента
LI
оператор a!
,'LI'.d("$completed=.completed; a!" // Сразу же после инициализации переменной $completed используем ее в a-правиле
Смотрим. Окей. С состоянием
$completed
разобрались. Завершенные дела стилизуются корректно и при начальной загрузке, и при последующих ручных переключениях.6. Редактирование названий дел
Вернемся к оригиналу. При двойном клике по названию дела включается режим редактирования, в котором это название можно поменять. Там это реализовано так, что шаблон режима просмотра «view» (с галкой, названием и кнопкой удаления) целиком прячется, а показывается элемент
INPUT class="edit"
. Мы сделаем чуть иначе — прятать будем только элемент LABEL
, т. к. остальные два элемента нам при редактировании не мешают. Просто допишем класс view
элементу LABEL
Для состояния «редактирование» определим в элементе
LI
переменную $editing
. Изначально оно (состояние) сброшено, включается по dblclick
на элементе LABEL
, а выключается при расфокусе элемента INPUT.edit
. Так и запишем: ,'LI'.d("$completed=.completed $editing=; a!" // Теперь у нас две переменные состояния
,'INPUT.toggle type=checkbox'
.d("#.checked=.completed")
.ui("$completed=#.checked")
,'LABEL.view'
.d("? $editing:!; ! .title") // Если $editing сброшена, то показываем этот элемент
.e("dblclick","$editing=`yes") // По dblclick включаем $editing
,'INPUT.edit'
.d("? $editing; !! .title@value") // Если $editing непустой
.ui(".title=#.value") // обновляем .title по событию change (ui событие по умолчанию для INPUT)
.e("blur","$editing=") // сбрасываем $editing по событию blur
,'BUTTON.destroy'.d("")
).a("!? $completed $editing") // отображаем состояния $completed и $editing в css-классе элемента 'LI'
Теперь мы можем редактировать названия дел.
7. Отправка данных на сервер
Ок, в браузере мы дела редактировать уже можем, но эти изменения нужно еще и передавать на сервер. Смотрим, как это делает оригинал:

Внесенные изменения отправляются на сервер методом PATCH с неким URL вида
http://todo-backend-express.herokuapp.com/28185
, который, очевидно, является уникальным для каждого дела. Этот URL указывается сервером в поле .url
для каждого дела, присутствующего в списке. То есть все, что от нас требуется для обновления дела на сервере — это отправить PATCH-запрос по адресу, указанному в поле .url
, с измененными данными в формате JSON: ,'INPUT.edit'
.d("? $editing; !! .title@value")
.ui(".title=#.value; (@method`PATCH .url (@Content-type`application/json)@headers (.title):json.encode@body):query")
.e("blur","$editing=")
Здесь мы используем все тот же конвертор
:query
, но в более развернутом варианте. Когда :query
применяется к простой строке, эта строка трактуется как URL и выполняется GET-запрос. Если же :query
получает сложный объект, как в данном случае, он трактует его как детальное описание запроса, содержащее поля .method
, .url
, .headers
и .body
, и выполняет запрос в соответствии с ними. Здесь мы сразу после обновления .title
отправляем серверу PATCH-запрос c этим обновленным .title
Но есть нюанс. Поле
.url
мы получаем от сервера, оно выглядит примерно так: http://todo-backend-express.herokuapp.com/28185
, то есть в нем жестко прописан протокол http:// Если наш клиент тоже открыт по http://, то все нормально. Но если клиент открыт по https:// — то возникает проблема: по соображениям безопасности браузер блокирует http-трафик от https-источника.Решается это просто: если убрать из
.url
протокол, то запрос будет проходить по протоколу страницы. Так и сделаем: напишем соответствующий конвертер — dehttp
, и будем пропускать .url
через него. Собственные конверторы (и прочий функционал) прописывается в секции .FUNC
: .ui(".title=#.value; (@method`PATCH .url:dehttp (@Content-type`application/json)@headers (.title):json.encode@body):query")
...
.FUNC({
convert:{ // конверторы - это функции с одним входом и одним выходом
dehhtp: url=>url.replace(/^https?\:/,'')// удаляем протокол из URL
}
})
Еще имеет смысл вынести объект headers в словарь, чтобы использовать его и в других запросах:
.ui(".title=#.value; (@method`PATCH .url:dehttp headers (.title):json.encode@body):query")
...
.DICT({
todos : "//todo-backend-express.herokuapp.com/",
headers: {"Content-type":"application/json"}
})
Ну и для полного фэншуя воспользуемся еще одним полезным свойством конвертора
:query
— автоматическим кодированием тела запроса в json в соответствии с заголовком Content-type:application/json
. В итоге правило будет выглядеть так: .ui(".title=#.value; (@method`PATCH .url:dehttp headers (.title)):query")
Итак, смотрим. Окей, названия дел теперь меняются не только в браузере, но и на сервере. Но! Меняться-то может не только название дела, но и его состояние завершенности —
completed
. Значит, его тоже нужно отправлять серверу.Можно элементу
INPUT.toggle
дописать аналогичный PATCH-запрос, просто вместо (.title)
отправлять (.completed)
: ,'INPUT.toggle type=checkbox'
.d("#.checked=.completed")
.ui("$completed=#.checked; (@method`PATCH .url:dehttp headers (.completed:?)):query")
А можно вынести этот PATCH-запрос «за скобки»:
,'LI'.d("$completed=.completed $editing= $patch=; a!" // $patch - "посылка" для сервера
,'INPUT.toggle type=checkbox'
.d("#.checked=.completed")
.ui("$patch=($completed=#.checked)") // кладем в $patch измененный completed
,'LABEL.view'
.d("? $editing:!; ! .title")
.e("dblclick","$editing=`yes")
,'INPUT.edit'
.d("? $editing; !! .title@value")
.ui("$patch=(.title=#.value)") // кладем в $patch измененный title
.e("blur","$editing=")
,'BUTTON.destroy'.d("")
)
.a("!? $completed $editing")
// если $patch не пустой, отправляем его серверу, потом сбрасываем
.u("? $patch; (@method`PATCH .url:dehttp headers $patch@):query $patch=")
Тут дело вот в чем. Правила реакции относятся к группе «up-правил», которые исполняются «снизу вверх» — от потомка к родителю, до самого корня (эта последовательность может быть прервана при необходимости). Это чем-то похоже на «всплывающие» события в DOM. Поэтому какие-то фрагменты реакции, общие для нескольких потомков, можно поручить их общему предку.
Конкретно в нашем случае выигрыш от такого делегирования не особо заметный, но если бы редактируемых полей было больше, то вынос этого громоздкого (по меркам dap, конечно) запроса в одно общее правило сильно помог бы сохранять код простым и читабельным. Так что рекомендую.
Смотрим: Теперь на сервер отправляются и изменения названия, и изменения статуса.
В следующей статье, если будет интерес, рассмотрим добавление, удаление и фильтрацию дел. А пока можно посмотреть финальный результат и другие примеры dap-кода на dap.js.org/docs