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" // Переменная состояния, инициализируем из одноименного поля .completed ,'LI'.d("$completed=.completed" ,'INPUT.toggle type=checkbox' // Начальное состояние галочки берем из данных .d("#.checked=.completed") // при нажатии обновляем переменную состояния $completed .ui("$completed=#.checked") ,'LABEL'.d("! .title") ,'BUTTON.destroy'.d("") ) // включаем или выключаем css-класс completed // в зависимости от значения $completed .a("!? $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' // Если $editing сброшена, то // показываем этот элемент .d("? $editing:!; ! .title") // По dblclick установить $editing // в непустое состояние .e("dblclick","$editing=`yes") ,'INPUT.edit' // Показываем если $editing не пустое .d("? $editing; !! .title@value") // обновляем .title по событию change // (ui событие по умолчанию для INPUT) .ui(".title=#.value") // сбрасываем $editing по событию blur .e("blur","$editing=") ,'BUTTON.destroy'.d("") // отображаем состояния $completed и $editing // в css-классе элемента 'LI' ).a("!? $completed $editing")
Теперь мы можем редактировать названия дел.
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-запрос «за скобки»:
// Переменная $patch - посылка для сервера ,'LI'.d("$completed=.completed $editing= $patch=; a!" ,'INPUT.toggle type=checkbox' .d("#.checked=.completed") // кладем в $patch измененный completed .ui("$patch=($completed=#.checked)") ,'LABEL.view' .d("? $editing:!; ! .title") .e("dblclick","$editing=`yes") ,'INPUT.edit' .d("? $editing; !! .title@value") // кладем в $patch измененный title .ui("$patch=(.title=#.value)") .e("blur","$editing=") ,'BUTTON.destroy'.d("") ) .a("!? $completed $editing") // если $patch не пустой, отправляем его серверу, потом сбрасываем .u("? $patch; (@method`PATCH .url:dehttp headers $patch@):query $patch=")
Тут дело вот в чем. Правила реакции относятся к группе «up-правил», которые исполняются «снизу вверх» — от потомка к родителю, до самого корня (эта последовательность может быть прервана при необходимости). Это чем-то похоже на «всплывающие» события в DOM. Поэтому какие-то фрагменты реакции, общие для нескольких потомков, можно поручить их общему предку.
Конкретно в нашем случае выигрыш от такого делегирования не особо заметный, но если бы редактируемых полей было больше, то вынос этого громоздкого (по меркам dap, конечно) запроса в одно общее правило сильно помог бы сохранять код простым и читабельным. Так что рекомендую.
Смотрим: Теперь на сервер отправляются и изменения названия, и изменения статуса.
В следующей статье, если будет интерес, рассмотрим добавление, удаление и фильтрацию дел. А пока можно еще раз посмотреть примеры на dap.js.org/docs
ссылка на оригинал статьи https://habr.com/ru/post/480912/
Добавить комментарий