Это вторая, заключительная, часть туториала, в котором мы пишем TodoMVC-клиент с помощью минималистичного реактивного js-фреймворка dap.
Краткое содержание первой части: мы получили с сервера список дел в формате JSON, построили из него HTML-список, добавили возможность редактирования названия и признака завершенности для каждого дела, и реализовали уведомление сервера об этих редактированиях.
Осталось реализовать: удаление произвольных дел, добавление новых дел, массовую установку/сброс и фильтрацию дел по признаку завершенности и функцию удаления всех завершенных дел. Этим мы и займемся. Финальный вариант клиента, к которому мы придем в этой статье, можно посмотреть здесь.
Вариант, на котором мы остановились в прошлый раз, можно освежить в памяти здесь.
Вот его код:
Сейчас здесь всего полсотни строк, но к концу статьи их станет вдвое больше — аж 100. Будет много HTTP запросов к серверу, поэтому откройте, пожалуйста, инструменты разработчика (в Хроме это, как вы помните, Ctrl+Shift+I) — там будет интересна в первую очередь вкладка Network, и во вторую — Console. Также не забываем просматривать код каждой версии нашей странички — в Хроме это Ctrl+U.
Тут я должен сделать небольшое лирическое отступление. Если вы не читали первую часть туториала, я бы рекомендовал все же начать с нее. Если вы ее читали, но ничего не поняли — лучше прочитать еще раз. Как показывают комментарии к предыдущим двум моим статьям, синтаксис и принцип работы dap не всегда сразу понятны неподготовленному читателю. Еще статья не рекомендуется к прочтению лицам, испытывающим дискомфорт при виде не си-подобного синтаксиса.
Эта, вторая, часть туториала будет чуть сложней и интересней, чем первая. [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 не всегда сразу понятны неподготовленному читателю. Еще статья не рекомендуется к прочтению лицам, испытывающим дискомфорт при виде не си-подобного синтаксиса.
Эта, вторая, часть туториала будет чуть сложней и интересней, чем первая. [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-программа пишется методом проб и ошибок — вот эти все «посмотрим, что получилось», «вроде работает, но есть нюанс» и т.п. На самом деле это не так. Все эти нюансы вполне очевидны и предсказуемы при написании кода. Но я подумал, что будет полезно на примере этих нюансов показать, зачем в правилах присутствует то или иное решение и почему делается так, а не иначе.
Задавайте, как говорится, вопросы.
Комментарии (14)
index0h
03.01.2020 02:36это все прекрасно, но это не то, чем дап был бы интересен на фоне всего остального (ну, на мой взгляд), т.к. они по большей части в голове разработчика а не во фреймворке.
Ну как же, эти принципы в конечном итоге имеют вполне конкретную реализацию и в фреймворках тоже.
И кстати дап вообще не про функции-хелперы.
Как только dsl не хватает вы пишете функцию-хэлпер, в FUNC, DICT, и т.д. По этой причине dap в целом и про функции-хэлепры тоже.
jooher Автор
03.01.2020 10:47-1Как только dsl не хватает вы пишете функцию-хэлпер, в FUNC, DICT, и т.д.
Вот как раз эта фраза и говорит о том, что вы вообще ничего не поняли, и ищете пхп там где его нет.
Это кагбэ не упрек вам, а сигнал мне — о том, что пример с todomvc, возможно, сложноват для первого знакомства, и надо было начинать с чего-то попроще.OldVitus
06.01.2020 22:55сигнал мне — о том, что пример с todomvc, возможно, сложноват для первого знакомства, и надо было начинать с чего-то попроще
Это кагбе сигнал о том что с dap что-то не так. Задачка простая, на вечерок на любом известном фреймворке, и на неизвестном тоже :) и безо всякого фреймворка тоже на вечерок-два.И кстати дап вообще не про функции-хелперы
А про что dap?
Вы так и не рассказали в чём профит от использования именно dap, по сравнению с тем-же React или Vue, что в нём не такого как в React (кроме маленького футпринта)?
Пока, после трёх статей, у меня например впечатление, что для работы с dap необходимо родиться хотя бы на столько-же гениальным как Вы, ну или пройти обучение в Хогвардсе.
Кстати как обстоят дела с 75к ( в прямом и переносном смысле )?index0h
07.01.2020 02:07-1Кстати как обстоят дела с 75к ( в прямом и переносном смысле )?
ганебна зрада
jooher Автор
07.01.2020 13:13у меня например впечатление, что для работы с dap необходимо родиться хотя бы на столько-же гениальным как Вы, ну или пройти обучение в Хогвардсе
Вот вы зря принижаете свои интеллектуальные способности. Дап вполне доступен для понимания и программисту Сбера, и пхпшнику. Секрет в том, что читать нужно с намерением понять, а не ололо пыщпыщ.
И кстати, это вы меня минусите? Я читал, что нужно набрать -50, чтобы считаться тут нормальным человеком :) Так что вы уж не сдерживайте себя.
jooher Автор
07.01.2020 10:18как обстоят дела с 75к
Почему вы меня-то об этом спрашиваете? У вас что нового? Начальство разрешило вам апи и тз? Давайте, «я сразу в понедельник для вас начну».
Или вы как рыбка: прошло5 минутнесколько дней — и забыли, за чем дело стало?
Почему бы нет? Учитывая, что сбером пользуется каждый российский нищеброд (за отсутствием альтернатив) — а далеко не у каждого российского нищеброда телефон как у вас — думаю, многие оценят PWA Сбер-клиент весом в 75к, и что Сбер наконец-то не срет им в телефон своими 75М. Думаю, и рекламодатели найдутся. Так что я вполне готов потратить свое время.
А вы «сможете очень и очень неплохо заработать», уговорив меня потом продать этот клиент сберу. Но это не точно.
index0h
jooher Автор
Ты мог просто написать «я опять нихера не понял». Вместо этого ты потратил новогоднюю ночь и 1 января на рисование этого говнокреатива?? Побойся
богаДеда Мороза. Так и на всю жизнь без подарков остаться можно.index0h
Именно потому, что я опять все понял, я и сгенерил этот комикс, по поводу вашего "говнокреатива" (ETA кстати 2 минуты). Вы очень некрасиво слили все полимеры в прошлой статье. В этой я вижу примерно то же самое. Если хотите, можем продолжить обсуждение, у вас в принципе было время подготовиться по вашему проекту более основательно.
Если этот комментарий убережет от вашей поделки хотя бы несколько человек, он оставлен не зря.
jooher Автор
Вам, извиняюсь, посрать больше негде, кроме как в коментах? Я вас, похоже, за что-то личное задел? Или вам за пхп обидно?
Вы комиксами уберегать людей собираетесь? Или нытьем, что «в пхп так не пишут»? Впрочем, ваше право.
Давайте, расскажите, что вы поняли. Возьмите любую строчку из статьи, и напишите ее так, как «пишут в пхп». И расскажите, почему у вас хорошо, а у меня плохо.
Вот это вообще неожиданное заявление, учитывая вашу роль в планировании моего времени.
index0h
Не-не-не, воспринимайте как форму досуга, не более того.
Все состоит из мелочей. Если это сработает почему нет?
Я ждал этого момента)) не согласен — докажи мне обратное
Ваш DSL — это концентрат из функциональности разных доменов. Цена ошибки в нем очень велика, а вот с их поиском все очень плохо. Единственный плюс вашего DSL — это лаконичность.
Главная проблема, которую область (не важно на каком языке) пытается решить на протяжении уже многих лет — это уменьшение сложности. Вот эти всякие принципы типа SOLID, GRASP, DRY, YAGNI, KISS,… — это только устоявшиеся попытки решить эту проблему. Паттерны типа MVC, MVVM, Repository, Container,… — решают ту же проблему. А что у вас? Простыня и функции-хэлперы. Ваш проект — говно, именно по этой причине.
Вы же его тратили на то, что бы слиться на довольно простых вопросах. Значит какая-то роль все таки есть.
jooher Автор
Я же принял тот ваш комментарий. И да, следующие статьи будут об этом. Эта ваша претензия принимается, только относится она не к дапу, а конкретно к этому примеру, который показывает не «как строить мегапроект», а просто принципы описания реактивного взаимодействия между элементами. Чем дап собственно и отличается от миллионов других фреймворков.
А «вот эти всякие принципы», интересные вам и прочим «миддлам» — это все прекрасно, но это не то, чем дап был бы интересен на фоне всего остального (ну, на мой взгляд), т.к. они по большей части в голове разработчика а не во фреймворке. Поэтому я не с этого и начал.
(И кстати дап вообще не про функции-хелперы.)