https://habr.com/ru/post/481680/- Разработка веб-сайтов
- JavaScript
- Программирование
- HTML
Это вторая, заключительная, часть туториала, в котором мы пишем TodoMVC-клиент с помощью минималистичного реактивного js-фреймворка
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
:
,'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, модификацию массива
$title
добавлением контекста
$
в качестве нового элемента и наконец, очистку свойства
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 имеется оператор
&
:
,'LI'.d("$completed=.completed $editing= $patch=; a!"
...
)
.a("!? $completed $editing; ? .url:!; & (@method`POST todos@url headers $):query")
.u("? $patch; (@method`PATCH .url:dehttp headers $patch@):query $patch=")
Смотрим. Другое дело! Даже при медленной сети список дел обновляется мгновенно, а уведомление сервера и подгрузка недостающих данных происходят в фоновом режиме, уже после отрисовки обновленного списка.
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-программа пишется методом проб и ошибок — вот эти все «посмотрим, что получилось», «вроде работает, но есть нюанс» и т.п. На самом деле это не так. Все эти нюансы вполне очевидны и предсказуемы при написании кода. Но я подумал, что будет полезно на примере этих нюансов показать, зачем в правилах присутствует то или иное решение и почему делается так, а не иначе.
Задавайте, как говорится, вопросы.