Как стать автором
Обновить

Пишем TodoMVC на dap. Часть 2

Время на прочтение16 мин
Количество просмотров1.7K
Это вторая, заключительная, часть туториала, в котором мы пишем 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, и прятать элемент элемент средствами 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-программа пишется методом проб и ошибок — вот эти все «посмотрим, что получилось», «вроде работает, но есть нюанс» и т.п. На самом деле это не так. Все эти нюансы вполне очевидны и предсказуемы при написании кода. Но я подумал, что будет полезно на примере этих нюансов показать, зачем в правилах присутствует то или иное решение и почему делается так, а не иначе.

Задавайте, как говорится, вопросы.
Теги:
Хабы:
Всего голосов 8: ↑2 и ↓6-1
Комментарии14

Публикации

Истории

Работа

Ближайшие события

AdIndex City Conference 2024
Дата26 июня
Время09:30
Место
Москва
Summer Merge
Дата28 – 30 июня
Время11:00
Место
Ульяновская область