Это вторая, заключительная, часть туториала, в котором мы пишем TodoMVC-клиент с помощью минималистичного реактивного js-фреймворка dap.
Краткое содержание первой части: мы получили с сервера список дел в формате JSON, построили из него HTML-список, добавили возможность редактирования названия и признака завершенности для каждого дела, и реализовали уведомление сервера об этих редактированиях.
Осталось реализовать: удаление произвольных дел, добавление новых дел, массовую установку/сброс и фильтрацию дел по признаку завершенности и функцию удаления всех завершенных дел. Этим мы и займемся. Финальный вариант клиента, к которому мы придем в этой статье, можно посмотреть здесь.
Вариант, на котором мы остановились в прошлый раз, можно освежить в памяти здесь.
Вот его код:
Сейчас здесь всего полсотни строк, но к концу статьи их станет вдвое больше — аж 100. Будет много HTTP запросов к серверу, поэтому откройте, пожалуйста, инструменты разработчика (в Хроме это, как вы помните, Ctrl+Shift+I) — там будет интересна в первую очередь вкладка Network, и во вторую — Console. Также не забываем просматривать код каждой версии нашей странички — в Хроме это Ctrl+U.
Тут я должен сделать небольшое лирическое отступление. Если вы не читали первую часть туториала, я бы рекомендовал все же начать с нее. Если вы ее читали, но ничего не поняли — лучше прочитать еще раз. Как показывают комментарии к предыдущим двум моим статьям, синтаксис и принцип работы dap не всегда сразу понятны неподготовленному читателю. Еще статья не рекомендуется к прочтению лицам, испытывающим дискомфорт при виде не си-подобного синтаксиса.
![](https://habrastorage.org/r/w780q1/webt/wj/-n/br/wj-nbrh5eflirkcich69glgnooo.jpeg)
Эта, вторая, часть туториала будет чуть сложней и интересней, чем первая. [TODO: попросить token найти в интернетах картинку с взрывающимся мозгом школьника].
С вашего позволения, нумерацию глав продолжу с ч.1. Там мы досчитали до 7. Итак,
Для удаления дела из списка есть кнопка
А вот с удалением элемента с экрана возможны варианты. Можно было бы просто ввести еще одну переменную состояния, скажем,
И это бы как бы работало. Но было бы читерством. К тому же, дальше по курсу у нас будут фильтры и счетчики активных и завершенных дел (то, что находится в
Областью определения переменной
Важно. Если при тестировании вдруг список дел не загружается — вполне возможно, кто-то их все просто удалил (это общедоступный сервер, и происходить там может что угодно).
В таком случае, пожалуйста, зайдите на полнофункциональный пример, и создайте несколько дел, чтобы было с чем экспериментировать.
Смотрим. Здесь
Дело вот в чем. Как вы помните, конвертор
Чтобы исправить ситуацию и избежать блокировки непричастных элементов, отложим первоначальную загрузку данных до момента, когда все уже отрисовалось. Для этого не будем сразу же загружать в переменную
Так она не будет ничего блокировать и весь шаблон отработает — пусть пока и с пустым «списком дел». Зато теперь, с нескучным начальным экраном, можно спокойно модифицировать
Этот элемент имеет u-правило, которое выглядит точно так же, как и то блокирующее, от которого мы отказались, но здесь есть одно принципиальное отличие.
Напомню, что d-правило (от down) — это правило генерации элемента, которое исполняется при построении шаблона сверху вниз, от родителя к потомкам; а u-правила (от up) — это правила реакции, исполняемые в ответ на событие, всплывающее снизу вверх, от потомка к родителю.
Так вот, если переменной что-то (в т.ч. «ничто») присваивается в d-правиле, это означает ее объявление и инициализацию в области видимости данного элемента и его потомков (в dap реализованы вложенные области видимости, как и в JS). Присваивание же в up-правилах означает модификацию переменной, объявленной ранее в области видимости. Объявление и инициализация переменных в d-правиле позволяет родителю передавать потомкам вниз по иерархии информацию, необходимую для построения, а модификация — позволяет передавать наверх обновления этой информации и таким образом инициировать соответствующую перестройку всех элементов, от нее зависящих.
Элемент
Потребитель переменной
Итак, теперь у нас список дел является переменной состояния в
Теперь мы можем
Надо сделать так, чтобы соответствующий объект удалялся и из переменной
Сам по себе dap не предоставляет никаких особых средств для манипуляций с данными. Манипуляции можно прекрасно писать в функциях на JS, а dap-правила просто доставляют им данные и забирают результат. Напишем JS-функцию удаления объекта из массива, не зная его номер. Например, такую:
Можно, наверно, написать и что-то более эффективное, но речь сейчас не про это. Вряд ли нашему приложению придется работать со списками дел из миллионов пунктов. Важно только то, что функция возвращает новый объект-массив, а не просто удаляет элемент из того что есть.
Чтобы сделать эту функцию доступной из dap-правил, ее нужно добавить в секцию
но ничто не мешает определить эту функцию прямо внутри
Теперь мы можем обращаться к этому конвертору из dap-правил
Здесь мы сначала формируем объект, который в JS-нотации соответствует
Аналогичным образом делаем добавление нового дела в список, с помощью элемента
Правило реакции элемента
Здесь юный читатель может спросить: зачем при добавления элемента в массив использовать
Смотрим, что получилось Дела добавляются и удаляются нормально, соответствующие запросы серверу отправляются исправно (вы же держите вкладку Network открытой все это время, верно?). Но что если мы захотим изменить название или статус свежедобавленного дела? Проблема в том, что для уведомления сервера об этих изменениях нам потребуется
На самом деле, вся необходимая информация о деле содержится в ответе сервера на POST-запрос, и корректней было бы новый объект-дело создавать не просто из пользовательского ввода, а из ответа сервера, и в
Смотрим — окей, теперь все отрабатывается корректно. Уведомления серверу о редактировании свежесозданных дел уходят правильные.
Можно было бы на этом и остановиться, но… Но если приглядеться, то все же можно заметить небольшую задержку между вводом названия нового дела и моментом его появления в списке. Эту задержку хорошо заметно, если включить имитацию медленной сети. Как вы уже догадались, дело в запросе к серверу: сначала мы запрашиваем данные для нового дела от сервера, и только после их получения модифицируем
Это еще одна особенность отработки асинхронных конверторов в dap: если результат асинхронного конвертора не используется (а именно — ничему не присваивается), значит его завершения можно не ждать — и исполнение правила не блокируется. Это часто бывает полезно: возможно, вы заметили, что при удалении дел из списка — они исчезают с экрана мгновенно, не дожидаясь результата DELETE-запроса. Особенно это заметно, если быстро удалять несколько дел подряд и отслеживать запросы в панели Network.
Но, поскольку результат запроса POST мы используем — присваиваем его контексту
Итак, сначала просто добавляем в список заготовку, содержащую только
Правило генерации элемента
Смотрим. Другое дело! Даже при медленной сети список дел обновляется мгновенно, а уведомление сервера и подгрузка недостающих данных происходят в фоновом режиме, уже после отрисовки обновленного списка.
В элементе
В данном случае нас интересует только поле
Ок, в массиве
Смотрим: Клик по общей галке выравнивает все индивидуальные галки, и сервер уведомляется соответствующими PATCH-запросами. Норм.
Кроме собственно списка дел, приложение должно еще иметь возможность фильтрации дел по признаку завершенности и показывать счетчики завершенных и незавершенных дел. Разумеется, для фильтрации мы будем банально использовать все тот же метод
Но сначала нужно позаботиться о том, чтобы поле
Важный момент здесь в том, что контекстом данных каждого дела является сам объект-дело, который лежит в массиве
Чтобы показывать в списке только дела с нужным признаком завершенности
Проверяем. Фильтры работают исправно. Есть нюанс в том, что названия фильтров выводятся слитно, т.к. здесь мы чуть отступили от DOM-структуры оригинала и выбились из CSS. Но к этому вернемся чуть позже.
Чтобы показать счетчики завершенных и активных дел, просто отфильтруем
В таком виде счетчики показывают корректные значения при начальной загрузке, но не реагируют на последующие изменения завершенности дел (при кликах по галкам). Дело в том, что клики по галкам, меняя состояние каждого отдельного дела, не меняют состояние
Теперь все работает как надо, счетчики обновляются корректно.
Пакетное удаление дел в TodoMVC реализуется так же некошерно, как и пакетная модификация — множественными запросами. Ну что же, вздохнем, разведем руками, и выполним по DELETE-запросу для каждого завершенного дела — а они у нас уже все есть в
Смотрим: создаем несколько ненужных дел, помечаем их галками и удаляем. Вкладка Network покажет весь ужас подобного подхода к пакетным операциям.
Вернемся к выбору фильтров. В оригинальном примере выбранный фильтр отражается в адресной строке после #. При изменении #-фрагмента в адресной строке вручную или при навигации — изменяется и выбранный фильтр. Это позволяет заходить на страницу приложения по URL с уже выбранным фильтром дел.
Писать в
А инициализировать переменную
Событие hashchange генерируется браузером при изменении #-фрагмента в адресной строке. Правда, почему-то только
Смотрим: переключаем фильтры, отслеживаем изменения в адресной строке, заходим по ссылкам с #Active, #All, #Completed. Все работает. Но вернемся к оригиналу. Там, похоже, выбор фильтра так и реализован — переходами по ссылкам. Хоть это и не слишком практично, но сделаем так же и мы — для полноты эксперимента
И чтобы выбранный фильтр выделялся, добавим оператор условной стилизации
В таком виде функционал нашего dap-приложения уже полностью (насколько я могу судить) соответствует тому, что делает оригинал.
Мне не очень нравится, что в оригинале форма курсора не меняется над активными элементами, поэтому допишем в
Так мы хотя бы будем видеть, где можно кликнуть.
Ах, да! Еще осталось написать большими буквами слово «todos». Но тут я, пожалуй, позволю себе наконец-то проявить немного фантазии и креатива, и вместо просто «todos» напишу «dap todos»
Вау. Теперь наше приложение можно считать законченным, а туториал состоявшимся (если вы честно дочитали до этих строк).
Возможно, при чтении у вас возникло впечатление, что dap-программа пишется методом проб и ошибок — вот эти все «посмотрим, что получилось», «вроде работает, но есть нюанс» и т.п. На самом деле это не так. Все эти нюансы вполне очевидны и предсказуемы при написании кода. Но я подумал, что будет полезно на примере этих нюансов показать, зачем в правилах присутствует то или иное решение и почему делается так, а не иначе.
Задавайте, как говорится, вопросы.
Краткое содержание первой части: мы получили с сервера список дел в формате JSON, построили из него HTML-список, добавили возможность редактирования названия и признака завершенности для каждого дела, и реализовали уведомление сервера об этих редактированиях.
Осталось реализовать: удаление произвольных дел, добавление новых дел, массовую установку/сброс и фильтрацию дел по признаку завершенности и функцию удаления всех завершенных дел. Этим мы и займемся. Финальный вариант клиента, к которому мы придем в этой статье, можно посмотреть здесь.
Вариант, на котором мы остановились в прошлый раз, можно освежить в памяти здесь.
Вот его код:
'#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("*@ todos:query"
,'LI'.d("$completed=.completed $editing= $patch=; a!"
,'INPUT.toggle type=checkbox'
.d("#.checked=.completed")
.ui("$patch=($completed=#.checked)")
,'LABEL.view'
.d("? $editing:!; ! .title")
.e("dblclick","$editing=`yes")
,'INPUT.edit'
.d("? $editing; !! .title@value")
.ui("$patch=(.title=#.value)")
.e("blur","$editing=")
,'BUTTON.destroy'.d("")
)
.a("!? $completed $editing")
.u("? $patch; (@method`PATCH .url:dehttp headers $patch@):query $patch=")
)
)
,'#footer'.d(""
,'#todo-count'.d("")
,'UL#filters'.d(""
,'LI'.d("")
)
,'#clear-completed'.d("")
)
)
.DICT({
todos : "//todo-backend-express.herokuapp.com/",
headers: {"Content-type":"application/json"}
})
.FUNC({
convert:{
dehttp: url=>url.replace(/^https?\:/,'')
}
})
.RENDER()
Сейчас здесь всего полсотни строк, но к концу статьи их станет вдвое больше — аж 100. Будет много HTTP запросов к серверу, поэтому откройте, пожалуйста, инструменты разработчика (в Хроме это, как вы помните, Ctrl+Shift+I) — там будет интересна в первую очередь вкладка Network, и во вторую — Console. Также не забываем просматривать код каждой версии нашей странички — в Хроме это Ctrl+U.
Тут я должен сделать небольшое лирическое отступление. Если вы не читали первую часть туториала, я бы рекомендовал все же начать с нее. Если вы ее читали, но ничего не поняли — лучше прочитать еще раз. Как показывают комментарии к предыдущим двум моим статьям, синтаксис и принцип работы dap не всегда сразу понятны неподготовленному читателю. Еще статья не рекомендуется к прочтению лицам, испытывающим дискомфорт при виде не си-подобного синтаксиса.
![](https://habrastorage.org/webt/wj/-n/br/wj-nbrh5eflirkcich69glgnooo.jpeg)
Эта, вторая, часть туториала будет чуть сложней и интересней, чем первая. [TODO: попросить token найти в интернетах картинку с взрывающимся мозгом школьника].
С вашего позволения, нумерацию глав продолжу с ч.1. Там мы досчитали до 7. Итак,
8. Делаем список дел переменной состояния
Для удаления дела из списка есть кнопка
BUTTON.destroy
. Удаление заключается в отправке серверу DELETE-запроса и собственно удалении с глаз долой соответствующего элемента UL#todo-list > LI
со всем содержимым. С отправкой DELETE-запроса все понятно: ,'BUTTON.destroy'.ui("(@method`DELETE .url:dehttp):query")
А вот с удалением элемента с экрана возможны варианты. Можно было бы просто ввести еще одну переменную состояния, скажем,
$deleted
, и прятать элемент элемент средствами CSS, включая ему CSS-класс deleted
,'LI'.d("$completed=.completed $editing= $patch= $deleted=; a!"
// Переменная $deleted как признак "удаленности"
...
,'BUTTON.destroy'.d("(@method`DELETE .url:dehttp):query $deleted=`yes")
// включили $deleted - вроде как бы удалили
)
.a("!? $completed $editing $deleted") // а в CSS прописать .deleted{display:none}
И это бы как бы работало. Но было бы читерством. К тому же, дальше по курсу у нас будут фильтры и счетчики активных и завершенных дел (то, что находится в
#footer
). Поэтому, лучше будем сразу удалять объект из списка дел по-честному, «физически». То есть нам нужна возможность модифицировать сам массив, который мы изначально получили от сервера — значит, этот массив тоже должен стать переменной состояния. Назовем ее $todos
.Областью определения переменной
$todos
нужно выбрать общего предка всех элементов, которые будут к этой переменной обращаться. А обращаться к ней будут и INPUT#new-todo
из #header
, и счетчики из #footer
, и собственно UL#todo-list
. Общий предок у них у всех — это корневой элемент шаблона, #todoapp
. Следовательно, в его d-правиле и будем определять переменную $todos
. Там же сразу и загрузим в нее данные с сервера. И строить список UL#todo-list
тоже теперь будем из нее:'#todoapp'.d("$todos=todos:query" // Объявляем переменную $todos и загружаем в нее данные
...
,'UL#todo-list'.d("*@ $todos" // Строим список уже из $todos
Важно. Если при тестировании вдруг список дел не загружается — вполне возможно, кто-то их все просто удалил (это общедоступный сервер, и происходить там может что угодно).
В таком случае, пожалуйста, зайдите на полнофункциональный пример, и создайте несколько дел, чтобы было с чем экспериментировать.
Смотрим. Здесь
$todos
объявлена в d-правиле элемента #todoapp
и сразу же инициализирована нужными данными. Вроде бы все работает, но появилась одна неприятная особенность. Если сервер долго отвечает на запрос (Chrome позволяет смоделировать такую ситуацию: на вкладке Network инструментов разработчика можно выбрать разные режимы имитации медленных сетей), то наша новая версия приложения до завершения запроса выглядит несколько печально — на экране нет ничего, кроме каких-то CSS-артефактов. Такая картина определенно не добавит энтузиазма пользователю. Хотя предыдущая версия этим не страдала — до получения данных на странице отсутствовал только сам список, но другие элементы появлялись сразу, не дожидаясь данных.Дело вот в чем. Как вы помните, конвертор
:query
— асинхронный. Асинхронность эта выражается в том, что до завершения запроса блокируется только исполнение текущего правила, то есть генерация элемента, которому, собственно, запрашиваемые данные и нужны (что логично). Генерация же других элементов не блокируется. Поэтому, когда к серверу обращался UL#todo-list
— блокировался только он, но не #header
и не #footer
, которые отрисовывались сразу. Теперь же завершения запроса ждет весь #todoapp
.9. Отложенная загрузка данных
Чтобы исправить ситуацию и избежать блокировки непричастных элементов, отложим первоначальную загрузку данных до момента, когда все уже отрисовалось. Для этого не будем сразу же загружать в переменную
$todos
данные, а сначала просто проинициализируем ее «ничем»'#todoapp'.d("$todos=" // Объявляем переменную $todos и инициализируем ее "ничем"
Так она не будет ничего блокировать и весь шаблон отработает — пусть пока и с пустым «списком дел». Зато теперь, с нескучным начальным экраном, можно спокойно модифицировать
$todos
, загрузив-таки в нее список дел. Для этого добавим к #todoapp
вот такого потомка ,'loader'
.u("$todos=todos:query") // модифицируем $todos, загружая в нее данные с сервера
.d("u") // запустить реакцию (u-правило) сразу после генерации
Этот элемент имеет u-правило, которое выглядит точно так же, как и то блокирующее, от которого мы отказались, но здесь есть одно принципиальное отличие.
Напомню, что d-правило (от down) — это правило генерации элемента, которое исполняется при построении шаблона сверху вниз, от родителя к потомкам; а u-правила (от up) — это правила реакции, исполняемые в ответ на событие, всплывающее снизу вверх, от потомка к родителю.
Так вот, если переменной что-то (в т.ч. «ничто») присваивается в d-правиле, это означает ее объявление и инициализацию в области видимости данного элемента и его потомков (в dap реализованы вложенные области видимости, как и в JS). Присваивание же в up-правилах означает модификацию переменной, объявленной ранее в области видимости. Объявление и инициализация переменных в d-правиле позволяет родителю передавать потомкам вниз по иерархии информацию, необходимую для построения, а модификация — позволяет передавать наверх обновления этой информации и таким образом инициировать соответствующую перестройку всех элементов, от нее зависящих.
Элемент
loader
, будучи потомком #todoapp
, в своем u-правиле модифицирует переменную $todos
, загружая в нее данные с сервера, что вызывает автоматическую перегенерацию всех элементов-потребителей этой переменной (и только их, что важно!). Потребители переменной — это элементы, d-правила которых содержат эту переменную в качестве rvalue, т.е. те, кто читают эту переменную (с учетом области видимости) при построении.Потребитель переменной
$todos
у нас сейчас один — тот самый UL#todo-list
, который, соответственно, и будет перестроен после загрузки данных. ,'UL#todo-list'.d("*@ $todos" // вот он, потребитель переменной $todos
Итак, теперь у нас список дел является переменной состояния в
#todoapp
, при этом не блокируя первоначальной отрисовки шаблона.10. Удаление и добавление дел
Теперь мы можем
$todos
всячески модифицировать. Начнем с удаления элементов. У нас уже есть кнопка-крестик BUTTON.destroy
, которая пока просто отсылает серверу запросы на удаление ,'BUTTON.destroy'.ui("(@method`DELETE .url:dehttp):query")
Надо сделать так, чтобы соответствующий объект удалялся и из переменной
$todos
— а поскольку это будет модификацией, то UL#todo-list
, как потребитель этой переменной, автоматически перестроится, но уже без удаленного элемента.Сам по себе dap не предоставляет никаких особых средств для манипуляций с данными. Манипуляции можно прекрасно писать в функциях на JS, а dap-правила просто доставляют им данные и забирают результат. Напишем JS-функцию удаления объекта из массива, не зная его номер. Например, такую:
const remove = (arr,tgt)=> arr.filter( obj => obj!=tgt );
Можно, наверно, написать и что-то более эффективное, но речь сейчас не про это. Вряд ли нашему приложению придется работать со списками дел из миллионов пунктов. Важно только то, что функция возвращает новый объект-массив, а не просто удаляет элемент из того что есть.
Чтобы сделать эту функцию доступной из dap-правил, ее нужно добавить в секцию
.FUNC
, но перед этим решить, как мы хотим ее вызывать. Самый простой вариант в данном случае, пожалуй, вызвать ее из конвертора, принимающего объект { todos, tgt }
и возвращающего отфильтрованный массив.FUNC({
convert:{
dehttp: url => url.replace(/^https?\:/,''), // это здесь еще с первой части туториала
remove: x => remove(x.todos,x.tgt) // удалить объект из массива
}
})
но ничто не мешает определить эту функцию прямо внутри
.FUNC
(я уже говорил, что .FUNC
— это на самом деле обычный JS-метод, а его аргумент — обычный JS-объект?) .FUNC({
convert:{
dehttp: url => url.replace(/^https?\:/,''),
remove: x => x.todos.filter( todo => todo!=x.tgt )
}
})
Теперь мы можем обращаться к этому конвертору из dap-правил
,'BUTTON.destroy'
.ui("$todos=($todos $@tgt):remove (@method`DELETE .url:dehttp):query")
Здесь мы сначала формируем объект, который в JS-нотации соответствует
{ todos, tgt:$ }
, передаем его конвертору :remove
, описанному в .FUNC
, а полученный отфильтрованный результат возвращаем в $todos
, таким образом модифицируя ее. Здесь $
— это контекст данных элемента, тот объект-дело из массива $todos
, на котором построен шаблон. После символа @
указывается псевдоним (alias) аргумента. Если @
отсутствует, то используется собственное имя аргумента. Это похоже на недавнее нововведение ES6 — property shorthand.Аналогичным образом делаем добавление нового дела в список, с помощью элемента
INPUT#new-todo
и POST-запроса ,'INPUT#new-todo placeholder="What needs to be done?" autofocus'
.ui("$=(#.value@title) (@method`POST todos@url headers $):query $todos=($todos $@tgt):insert #.value=")
...
.FUNC({
convert:{
dehttp: url => url.replace(/^https?\:/,''),
remove: x => x.todos.filter( todo => todo!=x.tgt ), // удалить объект из массива
insert: x => x.todos.concat( [x.tgt] ) // добавить объект в массив
}
})
Правило реакции элемента
INPUT#new-todo
на стандартное UI-событие (для элементов INPUT
стандартным dap считает событие change
) включает: чтение пользовательского ввода из свойства value
этого элемента, формирование локального контекста $
с этим значением в качестве поля .title
, отправку контекста $
серверу методом POST, модификацию массива $todos
добавлением контекста $
в качестве нового элемента и наконец, очистку свойства value
элемента INPUT
.Здесь юный читатель может спросить: зачем при добавления элемента в массив использовать
concat()
, если это можно сделать с помощью обычного push()
? Опытный же читатель сразу поймет в чем дело, и напишет свой вариант ответа в комментариях.Смотрим, что получилось Дела добавляются и удаляются нормально, соответствующие запросы серверу отправляются исправно (вы же держите вкладку Network открытой все это время, верно?). Но что если мы захотим изменить название или статус свежедобавленного дела? Проблема в том, что для уведомления сервера об этих изменениях нам потребуется
.url
, который назначает этому делу сервер. Мы, когда дело создавали, его .url
не знали, соответственно, корректный PATCH-запрос на изменение сформировать не можем.На самом деле, вся необходимая информация о деле содержится в ответе сервера на POST-запрос, и корректней было бы новый объект-дело создавать не просто из пользовательского ввода, а из ответа сервера, и в
$todos
добавлять уже этот объект — со всей предоставляемой сервером информацией, в том числе и полем .url
,'INPUT#new-todo placeholder="What needs to be done?" autofocus'
.ui("$todos=($todos (@method`POST todos@url headers (#.value@title)):query@tgt ):insert #.value=")
Смотрим — окей, теперь все отрабатывается корректно. Уведомления серверу о редактировании свежесозданных дел уходят правильные.
Можно было бы на этом и остановиться, но… Но если приглядеться, то все же можно заметить небольшую задержку между вводом названия нового дела и моментом его появления в списке. Эту задержку хорошо заметно, если включить имитацию медленной сети. Как вы уже догадались, дело в запросе к серверу: сначала мы запрашиваем данные для нового дела от сервера, и только после их получения модифицируем
$todos
. Следующим шагом мы эту ситуацию постараемся исправить, но сначала обращу ваше внимание на другой интересный момент. Если мы вернемся чуть назад, к предыдущему варианту, то заметим: хотя запрос там тоже присутствует, но новое дело добавляется в список моментально, не дожидаясь окончания запроса // это предыдущая версия правила, :query тоже присутствует
.ui("$=(#.value@title) (@method`POST todos@url headers $):query $todos=($todos $@tgt):insert #.value=")
Это еще одна особенность отработки асинхронных конверторов в dap: если результат асинхронного конвертора не используется (а именно — ничему не присваивается), значит его завершения можно не ждать — и исполнение правила не блокируется. Это часто бывает полезно: возможно, вы заметили, что при удалении дел из списка — они исчезают с экрана мгновенно, не дожидаясь результата DELETE-запроса. Особенно это заметно, если быстро удалять несколько дел подряд и отслеживать запросы в панели Network.
Но, поскольку результат запроса POST мы используем — присваиваем его контексту
$
— то приходится ждать его завершения. Поэтому нужно найти другой способ модифицировать $todos
до исполнения POST-запроса. Решение: все-таки сначала создать новый объект-дело и сразу добавить его в $todos
, дать списку отрисоваться и только потом, после отрисовки, если у дела отсутствует .url
(то есть дело только что создано), выполнить POST-запрос, и его результат наложить на контекст данных этого дела.Итак, сначала просто добавляем в список заготовку, содержащую только
.title
,'INPUT#new-todo placeholder="What needs to be done?" autofocus'
.ui("$todos=($todos (#.value@title)@tgt):insert #.value=")
Правило генерации элемента
UL#todo-list > LI
уже содержит оператор a!
, запускающий a-правило после первичной отрисовки элемента. Туда же можем добавить и запуск POST-запроса при отсутствии .url
. Для инъекции дополнительных полей в контекст в dap имеется оператор &
.a("!? $completed $editing; ? .url:!; & (@method`POST todos@url headers $):query")
Смотрим. Другое дело! Даже при медленной сети список дел обновляется мгновенно, а уведомление сервера и подгрузка недостающих данных происходят в фоновом режиме, уже после отрисовки обновленного списка.
11. Галку всем!
В элементе
#header
присутствует кнопка массовой установки/сброса признака завершенности для всех дел в списке. Для массового присвоения значений полям элементов массива просто пишем еще один конвертор, :assign
, и применяем его к $todos
по клику на INPUT#toggle-all
,'INPUT#toggle-all type=checkbox'
.ui("$todos=($todos (#.checked@completed)@src):assign")
...
assign: x => x.todos && x.todos.map(todo => Object.assign(todo,x.src))
В данном случае нас интересует только поле
.completed
, но легко видеть что таким конвертором можно массово менять значения любых полей элементов массива.Ок, в массиве
$todos
галочки переключаются, теперь надо уведомить о сделанных изменениях сервер. В оригинальном примере это делается отсылкой PATCH-запросов для каждого дела — не слишком эффективная стратегия, но это уже не от нас зависит. Ок, для каждого дела отправляем PATCH-запрос .ui("*@ $todos=($todos (#.checked@completed)@src):assign; (@method`PATCH .url:dehttp headers (.completed)):query")
Смотрим: Клик по общей галке выравнивает все индивидуальные галки, и сервер уведомляется соответствующими PATCH-запросами. Норм.
12. Фильтрация дел по признаку завершенности
Кроме собственно списка дел, приложение должно еще иметь возможность фильтрации дел по признаку завершенности и показывать счетчики завершенных и незавершенных дел. Разумеется, для фильтрации мы будем банально использовать все тот же метод
filter()
, предоставляемый самим JS.Но сначала нужно позаботиться о том, чтобы поле
.completed
каждого дела всегда соответствовало действительности, и обновлялось при клике индивидуальную галку дела вместе с переменной $completed
. Раньше это нам не было важно, но теперь будет. ,'INPUT.toggle type=checkbox'
.d("#.checked=.completed")
.ui("$patch=(.completed=$completed=#.checked) $recount=()")
// поле .completed теперь тоже нужно поддерживать в актуальном состоянии
Важный момент здесь в том, что контекстом данных каждого дела является сам объект-дело, который лежит в массиве
$todos
. Не какая-то отдельная копия, или связанная конструкция, а сам объект собственной персоной. И все обращения к полям .title
, .completed
, .url
— как на чтение, так и на запись — применяются непосредственно к этому объекту. Поэтому, чтобы фильтрация массива $todos
работала корректно, нам нужно, чтобы завершенность дела отражалось не только галкой на экране, но и в поле .completed
объекта-дела.Чтобы показывать в списке только дела с нужным признаком завершенности
.completed
, будем просто фильтровать $todos
в соответствии с выбранным фильтром. Выбранный фильтр — это, как вы уже догадались, еще одна переменная состояния нашего приложения, так ее и назовем: $filter
. Для фильтрации $todos
в соответствии с выбранным $filter
пойдем по накатанной дорожке и просто добавим еще один конвертор, вида {список, фильтр}=>отфильтрованный список, а названия и фильтрующие функции будем брать из «ассоциативного массива» (то бишь, обычного JS-объекта) todoFilters
const todoFilters={
"All": null,
"Active": todo => !todo.completed,
"Completed": todo => !!todo.completed
};
'#todoapp'.d("$todos= $filter=" // добавляем переменную $filter
...
,'UL#todo-list'.d("* ($todos $filter):filter"
...
,'UL#filters'.d("* filter" // константу filter с названиями фильтров берем из .DICT
,'LI'
.d("! .filter")
.ui("$filter=.") // такая запись эквивалентна "$filter=.filter"
)
...
.DICT({
...
filter: Object.keys(todoFilters) //["All","Active","Completed"]
})
.FUNC({
convert:{
...
filter: x =>{
const
a = x.todos,
f = x.filter && todoFilters[x.filter];
return a&&f ? a.filter(f) : a;
}
}
})
Проверяем. Фильтры работают исправно. Есть нюанс в том, что названия фильтров выводятся слитно, т.к. здесь мы чуть отступили от DOM-структуры оригинала и выбились из CSS. Но к этому вернемся чуть позже.
13. Счетчики завершенных и активных дел.
Чтобы показать счетчики завершенных и активных дел, просто отфильтруем
$todos
соответствующими фильтрами и покажем длины получившихся массивов ,'#footer'.d("$active=($todos @filter`Active):filter $completed=($todos @filter`Completed):filter"
,'#todo-count'.d("! (active $active.length)format") // подставляем length в текстовый шаблон active
...
,'#clear-completed'.d("! (completed $completed.length)format")
)
...
.DICT({
...
active: "{length} items left",
completed: "Clear completed items ({length})"
})
В таком виде счетчики показывают корректные значения при начальной загрузке, но не реагируют на последующие изменения завершенности дел (при кликах по галкам). Дело в том, что клики по галкам, меняя состояние каждого отдельного дела, не меняют состояние
$todos
— модификация элемента массива не является модификацией самого массива. Поэтому нам нужен дополнительный сигнал о необходимости переучета дел. Таким сигналом может стать дополнительная переменная состояния, которая модифицируется каждый раз, когда требуется переучет. Назовем ее $recount
. Объявим в d-правиле общего предка, будем обновлять при кликах по галкам, а элемент #footer
сделаем ее потребителем — для этого достаточно просто упомянуть эту переменную в его d-правиле'#todoapp'.d("$todos= $filter= $recount=" // объявляем $recount в общей области видимости
...
,'INPUT.toggle type=checkbox'
.d("#.checked=.completed")
.ui("$patch=(.completed=$completed=#.checked) $recount=()")
// присваиваем $recount новый пустой объект
...
,'#footer'.d("$active=($todos @filter`Active):filter $completed=($todos @filter`Completed):filter $recount" // упоминаем $recount
Теперь все работает как надо, счетчики обновляются корректно.
14. Удаление всех завершенных дел.
Пакетное удаление дел в TodoMVC реализуется так же некошерно, как и пакетная модификация — множественными запросами. Ну что же, вздохнем, разведем руками, и выполним по DELETE-запросу для каждого завершенного дела — а они у нас уже все есть в
$completed
. Соответственно, в $todos
после удаления завершенных дел должно остаться то, что уже есть в $active
,'#clear-completed'
.d("! (completed $completed.length)format")
.ui("$todos=$active; *@ $completed; (@method`DELETE .url:dehttp):query")
Смотрим: создаем несколько ненужных дел, помечаем их галками и удаляем. Вкладка Network покажет весь ужас подобного подхода к пакетным операциям.
15. Состояние в адресной строке
Вернемся к выбору фильтров. В оригинальном примере выбранный фильтр отражается в адресной строке после #. При изменении #-фрагмента в адресной строке вручную или при навигации — изменяется и выбранный фильтр. Это позволяет заходить на страницу приложения по URL с уже выбранным фильтром дел.
Писать в
location.hash
можно оператором urlhash
, например, в a-правиле элемента #todoapp
(или любого его потомка), которое будет исполняться при каждом обновлении $filter
.a("urlhash $filter")
А инициализировать переменную
$filter
значением из адресной строки и потом обновлять по событию hashchange можно с помощью псевдо-конвертора :urlhash
, который возвращает текущее состояние location.hash
(без #).d("$todos= $filter=:urlhash $recount="
.e("hashchange","$filter=:urlhash")
Событие hashchange генерируется браузером при изменении #-фрагмента в адресной строке. Правда, почему-то только
window
и document.body
могут слушать это событие. Чтобы отслеживать это событие из элемента #todoapp
, придется добавить в его d-правило оператор listen
, который подписывает элемент на ретрансляцию событий от объекта window
'#todoapp'
.a("urlhash $filter")
.e("hashchange","$filter=:urlhash")
.d("$todos= $filter=:urlhash $recount=; listen @hashchange"
Смотрим: переключаем фильтры, отслеживаем изменения в адресной строке, заходим по ссылкам с #Active, #All, #Completed. Все работает. Но вернемся к оригиналу. Там, похоже, выбор фильтра так и реализован — переходами по ссылкам. Хоть это и не слишком практично, но сделаем так же и мы — для полноты эксперимента
,'UL#filters'.d("* filter"
,'LI'.d(""
,'A'.d("!! (`# .filter)concat@href .filter@")
)
)
И чтобы выбранный фильтр выделялся, добавим оператор условной стилизации
!?
, который будет добавлять элементу CSS-класс selected
, если значение в поле .filter
его контекста равно значению переменной $filter
,'A'.d("!! (`# .filter)concat@href .filter@; !? (.filter $filter)eq@selected")
В таком виде функционал нашего dap-приложения уже полностью (насколько я могу судить) соответствует тому, что делает оригинал.
16. Пара завершающих штрихов
Мне не очень нравится, что в оригинале форма курсора не меняется над активными элементами, поэтому допишем в
head
нашего HTML-документа такой стиль [ui=click]{cursor:pointer}
Так мы хотя бы будем видеть, где можно кликнуть.
Ах, да! Еще осталось написать большими буквами слово «todos». Но тут я, пожалуй, позволю себе наконец-то проявить немного фантазии и креатива, и вместо просто «todos» напишу «dap todos»
,'H1'.d("","dap todos")
Вау. Теперь наше приложение можно считать законченным, а туториал состоявшимся (если вы честно дочитали до этих строк).
В заключение
Возможно, при чтении у вас возникло впечатление, что dap-программа пишется методом проб и ошибок — вот эти все «посмотрим, что получилось», «вроде работает, но есть нюанс» и т.п. На самом деле это не так. Все эти нюансы вполне очевидны и предсказуемы при написании кода. Но я подумал, что будет полезно на примере этих нюансов показать, зачем в правилах присутствует то или иное решение и почему делается так, а не иначе.
Задавайте, как говорится, вопросы.