Полный учебник для начинающих для создания списка Todo в Фениксе.
100% функциональный. 0% JavaScript. Просто HTML , CSS и Elixir . Быстрый и поддерживаемый.
Списки TODO знакомы большинству людей; Мы делаем списки все время. Создание списка Todo с нуля - отличный способ изучить Elixir / Phoenix , потому что пользовательский интерфейс / UX прост , поэтому мы можем сосредоточиться на реализации.
Для команды @dwyl это приложение/учебник - это демонстрация того, как рендеринг на стороне сервера ( с прогрессивным улучшением на стороне клиента ) может обеспечить отличный баланс между эффективностью разработчика ( быстрое функции доставки ), UX и доступность . На страницах сервера страницы требуется менее 5 мс, чтобы ответить, поэтому UX быстрая . На Fly.io: phxtodo.fly.dev Время отклика об обратном пути в раунде составляет 200 мс для всех взаимодействий, так что это похоже на приложение для рендеринга на стороне клиента.
Учебник по списку TODO, в котором показывает полный новичка, как построить приложение в Elixir/Phoenix с нуля.
Попробуйте версию fly.io. Добавьте несколько элементов в список и протестируйте функциональность.

Даже с полной http-обратной поездкой для каждого взаимодействия время отклика быстро . Обратите внимание на то, как Chrome | Firefox | Safari ждет ответа с сервера перед повторным рендерингом страницы. Старая полная страница обновления прошлых лет исчез . Современные браузеры разумно делают только изменения! Таким образом, UX приближается к «местному»! Серьезно, попробуйте приложение Fly.io на своем телефоне и посмотрите!
В этом уроке мы используем CSS ToDomvc для упрощения нашего пользовательского интерфейса. Это имеет несколько преимуществ самых больших из которых минимизирует, сколько CSS мы должны написать! Это также означает, что у нас есть руководство, к которому необходимо реализовать функции для достижения полной функциональности.
Примечание : мы любим
CSSза его невероятную силу/гибкость, но мы знаем, что не всем нравится. См.: Учите-тачины#Почему последнее , что мы хотим,-это тратить тонны времени наCSSв учебном пособииPhoenix!
Этот урок для всех, кто учится в Elixir/Phoenix. Ожидается/ожидается никакого предыдущего опыта работы с Фениксом. Мы включили все шаги, необходимые для создания приложения.
Если вы застряли на каком -либо шаге, пожалуйста, откройте проблему на Github, где мы рады помочь вам отклеиться! Если вы чувствуете, что любая строка кода может использовать немного больше объяснений/ясности, пожалуйста, не стесняйтесь сообщить нам! Мы знаем , что значит быть новичком, это может разочаровать, когда что -то не имеет смысла! Задавать вопросы о GitHub помогает всем учиться!
Пожалуйста, дайте нам отзыв! Светь репо, если вы нашли его полезным.
Прежде чем попытаться построить список TODO, убедитесь, что у вас есть все, что вам нужно установить на компьютере. Смотрите: предпосылки
После того, как вы подтвердили, что у вас установлена Phoenix & PostgreSQL, попробуйте запустить готовое приложение.
localhost Прежде чем начать создавать свою собственную версию приложения Todo List, запустите готовую версию на localhost чтобы подтвердить, что она работает.
Клонировать проект от GitHub:
git clone [email protected]:dwyl/phoenix-todo-list-tutorial.git && cd phoenix-todo-list-tutorialУстановите зависимости и настройте базу данных:
mix setupЗапустите сервер Phoenix:
mix phx.server Посетите localhost:4000 в вашем веб -браузере.
Вы должны увидеть:

Теперь, когда у вас есть готовое пример приложения, работающего на вашем localhost ,
Давайте построим его с нуля и поймем все шаги.
При запуске готового примера примера на localhost , если вы хотите попробовать кнопку login , вам нужно будет получить AUTH_API_KEY . [1 минута] См.: Получите свой AUTH_API_KEY
Если вы запустили готовое приложение на своем localhost ( и вы действительно должны! ),
Вам нужно будет сменить каталог перед началом учебника:
cd ..
Теперь вы готовы построить !
В вашем терминале создайте новое приложение Phoenix, используя следующую команду mix :
mix phx.new app --no-dashboard --no-gettext --no-mailer При предложении установить зависимости, введите Y , за которым следует enter .
Примечание . Эти флаги после имени
appпросто во избежание создания файлов, которые нам не нужны для этого простого примера. См.: Hexdocs.pm/phoenix/mix.tasks.phx.new
Изменить в недавно созданный каталог app ( cd app ) и убедитесь, что у вас есть все, что вам нужно:
mix setupЗапустите сервер Phoenix:
mix phx.server Теперь вы можете посетить localhost:4000 в своем веб -браузере. Вы должны увидеть что -то похожее на:

Выключите сервер Phoenix ctrl + c .
Запустите тесты, чтобы убедиться, что все работает так, как и ожидалось:
mix testВы должны увидеть:
Compiling 16 files (.ex)
Generated app app
17:49:40.111 [info] Already up
...
Finished in 0.04 seconds
3 tests, 0 failuresУстановив, что приложение Phoenix работает, как и ожидалось, давайте перейдем к созданию некоторых файлов!
items При создании основного списка TODO нам нужна только одна схема: items . Позже мы можем добавить отдельные списки и теги для организации/классификации наших items , но сейчас это все, что нам нужно.
Запустите следующую команду генератора для создания таблицы элементов:
mix phx.gen.html Todo Item items text:string person_id:integer status:integer Строго говоря, нам нужны только поля text и status , но, поскольку мы знаем, что хотим связать элементы с людьми (_later в учебном пособии), мы добавляем поле сейчас .
Вы увидите следующий вывод:
* creating lib/app_web/controllers/item_controller.ex
* creating lib/app_web/controllers/item_html/edit.html.heex
* creating lib/app_web/controllers/item_html/index.html.heex
* creating lib/app_web/controllers/item_html/new.html.heex
* creating lib/app_web/controllers/item_html/show.html.heex
* creating lib/app_web/controllers/item_html.ex
* creating test/app_web/controllers/item_controller_test.exs
* creating lib/app/todo/item.ex
* creating priv/repo/migrations/20221205102303_create_items.exs
* creating lib/app/todo.ex
* injecting lib/app/todo.ex
* creating test/app/todo_test.exs
* injecting test/app/todo_test.exs
* creating test/support/fixtures/todo_fixtures.ex
* injecting test/support/fixtures/todo_fixtures.ex
Add the resource to your browser scope in lib/app_web/router.ex:
resources "/items", ItemController
Remember to update your repository by running migrations:
$ mix ecto.migrate
Это создало кучу файлов! Некоторые из которых нам не нужны .
Мы могли бы вручную создать только те файлы, которые нам нужны , но это «официальный» способ создания приложения CRUD в Фениксе, поэтому мы используем его для скорости.
ПРИМЕЧАНИЕ . В этом примере в этом примере, как
Todo, представляют собой « выделенные модули, которые обнажают и группируют функциональность, связанные с функциональностью ». Мы чувствуем, что они излишне усложняют основные приложения Phoenix со слоями «интерфейса», и мы действительно хотим, чтобы мы могли их избежать. Но, учитывая, что они запекаются в генераторах, и создатель фреймворта любит их, у нас есть выбор: либо сесть на борт с контекстами, либо вручную создавать все файлы в наших проектах Phoenix. Генераторы - гораздо более быстрый способ построить! Примите их, даже если вам придетсяdeleteнесколько неиспользованных файлов по пути!
Мы не собираемся объяснять каждый из этих файлов на этом этапе в учебном пособии, потому что легче понять файлы, когда вы создаете приложение! Цель каждого файла станет ясной по мере продвижения через их редактирование.
/items ресурсы в router.ex Следуйте инструкциям, отмеченным генератором, чтобы добавить resources "/items", ItemController на router.ex .
Откройте файл lib/app_web/router.ex и найдите строку: scope "/", AppWeb do . Добавьте линию в конце блока. например:
scope "/" , AppWeb do
pipe_through :browser
get "/" , PageController , :index
resources "/items" , ItemController # this is the new line
end Ваш файл router.ex должен выглядеть следующим образом: router.ex#L20
Теперь, как предположил терминал, запустите mix ecto.migrate . Это завершит настройку таблиц базы данных и запустит необходимые миграции, так что все работает правильно!
На этом этапе у нас уже есть функциональный список TODO ( если мы готовы использовать пользовательский интерфейс Phoenix по умолчанию ).
Попробуйте запустить приложение на вашем localhost : запустите сгенерированные миграции с помощью mix ecto.migrate , затем сервер с:
mix phx.server
Посетите: http: // localhost: 4000/items/new и введите некоторые данные.

Нажмите кнопку «Сохранить элемент», и вы будете перенаправлены на страницу «Показать»: http: // localhost: 4000/items/1

Это не привлекательный пользовательский опыт (UX), но это работает ! Вот список элементов - «список Todo». Вы можете посетить это, нажав кнопку Back to items или, получив доступ к следующему URL http: // localhost: 4000/элементы.

Давайте улучшим UX, используя HTML и CSS ToDomvc!
Чтобы воссоздать UMODVC UI/UX, давайте заимствуем HTML -код непосредственно из примера.
Посетите: http://todomvc.com/examples/vanillajs Добавьте в список несколько предметов. Затем проверьте источник, используя инструменты Dev вашего браузера. например:

Щелкните правой кнопкой мыши по желаемому источнику (например: <section class="todoapp"> ) и выберите «Изменить как html»:

Как только HTML для <section> будет редактируется, выберите его и скопируйте его.

HTML -код:
< section class =" todoapp " >
< header class =" header " >
< h1 > todos </ h1 >
< input class =" new-todo " placeholder =" What needs to be done? " autofocus ="" />
</ header >
< section class =" main " style =" display: block; " >
< input id =" toggle-all " class =" toggle-all " type =" checkbox " />
< label for =" toggle-all " > Mark all as complete </ label >
< ul class =" todo-list " >
< li data-id =" 1590167947253 " class ="" >
< div class =" view " >
< input class =" toggle " type =" checkbox " />
< label > Learn how to build a Todo list in Phoenix </ label >
< button class =" destroy " > </ button >
</ div >
</ li >
< li data-id =" 1590167956628 " class =" completed " >
< div class =" view " >
< input class =" toggle " type =" checkbox " />
< label > Completed item </ label >
< button class =" destroy " > </ button >
</ div >
</ li >
</ ul >
</ section >
< footer class =" footer " style =" display: block; " >
< span class =" todo-count " > < strong > 1 </ strong > item left </ span >
< ul class =" filters " >
< li >
< a href =" #/ " class =" selected " > All </ a >
</ li >
< li >
< a href =" #/active " > Active </ a >
</ li >
< li >
< a href =" #/completed " > Completed </ a >
</ li >
</ ul >
< button class =" clear-completed " style =" display: block; " >
Clear completed
</ button >
</ footer >
</ section > Давайте преобразуем этот HTML в встроенный шаблон эликсира ( EEx ).
Примечание . Причина , по которой мы копируем этот
HTMLиз инспектора элементов браузера, вместо того, чтобы непосредственно из источника на GitHub:examples/vanillajs/index.htmlзаключается в том, что это «приложение для одной страницы», поэтому<ul class="todo-list"></ul>только заполняется в броузере. Копирование его из инструментов Dev Browser - самый простой способ получить полныйHTML.
index.html.eex Откройте файл lib/app_web/controllers/item_html/index.html.eex и прокрутите внизу.
Затем ( без удаления кода, который уже есть ) вставьте код HTML который мы поставляли из ToDomvc.
Например:
/lib/app_web/controllers/item_html/index.html.eex#L27-L73
Если вы попытаетесь запустить приложение сейчас и посетите http: // localhost: 4000/item/items/
Вы увидите это ( без CSS -CSS ):

Это, очевидно, не то, что мы хотим, так что давайте получим CSS -CSS и сохраним его в нашем проекте!
/assets/css Посетите http://todomvc.com/examples/vanillajs/node_modules/todomvc-app-css/index.css
и сохранить файл в /assets/css/todomvc-app.css .
Например: /assets/css/todomvc-app.css
todomvc-app.css в app.scss Откройте файл assets/css/app.scss и замените его следующим образом:
/* This file is for your main application css. */
/* @import "./phoenix.css"; */
@import "./todomvc-app.css" ; Например: /assets/css/app.scss#L4
Откройте свой файл lib/app_web/components/layouts/app.html.heex и замените содержимое следующим кодом:
<!DOCTYPE html >
< html lang =" en " >
< head >
< meta charset =" utf-8 " />
< meta http-equiv =" X-UA-Compatible " content =" IE=edge " />
< meta name =" viewport " content =" width=device-width, initial-scale=1.0 " />
< title > Phoenix Todo List </ title >
< link rel =" stylesheet " href = {~p "/assets/app.css"} />
< script defer type =" text/javascript " src = {~p "/assets/app.js"} > </ script >
</ head >
< body >
< main role =" main " class =" container " >
< %= @inner_content % >
</ main >
</ body >
</ html >До:
lib/app_web/components/layouts/app.html.eex
После:lib/app_web/components/layouts/app.html.heex
<%= @inner_content %> - это то, где будет отображаться приложение TODO.
Примечание : тег
<script>включен из соглашения. Тем не менее, мы не будем писать никакогоJavaScriptв этом уроке. Мы достигнем 100% паритета функции с ToDomvc, не написав линиюJS. Мы не «ненавидим»JS, на самом деле у нас есть учебник «сестры», который создает то же приложение вJS: Dwyl/JavaScript-Todo-List-Tutorial, что мы просто хотим напомнить вам, что вам не нужноJS, чтобы создать полностью функциональное веб-приложение с отличным UX!
С сохраненным шаблоном макета файл ToDomvc CSS, сохраненный в /assets/css/todomvc-app.css и todomvc-app.css импортированную в app.scss , теперь ваша страница /items должны выглядеть так:

Таким образом, наш список Todo начинает выглядеть как ToDomvc, но это все еще просто фиктивный список.
Чтобы отобразить данные item в шаблоне ToDomvc, нам нужно будет добавить несколько функций. Когда мы создали проект и сгенерировали модель item , был создан контроллер (расположен в lib/app_web/controllers/item_controller.ex ) и компонент/представление (также расположен в lib/app_web/controllers/item_html.ex ). Этот компонент/представление - это то, что эффективно управляет рендерингом содержимого внутри каталога lib/app_web/controllers/item_html который мы возились с предварительным.
Мы знаем, что нам нужно внести изменения в пользовательский интерфейс, поэтому мы собираемся добавить несколько функций в этом компоненте (что сродни точке зрения парадигмы MVC).
Это наш первый шанс сделать небольшую тестовую разработку (TDD).
Создайте новый файл с помощью test/app_web/controllers/item_html_test.exs .
Введите следующий код в файл:
defmodule AppWeb.ItemHTMLTest do
use AppWeb.ConnCase , async: true
alias AppWeb.ItemHTML
test "complete/1 returns completed if item.status == 1" do
assert ItemHTML . complete ( % { status: 1 } ) == "completed"
end
test "complete/1 returns empty string if item.status == 0" do
assert ItemHTML . complete ( % { status: 0 } ) == ""
end
end Например: /test/app_web/controllers/item_html_test.exs
Если вы попытаетесь запустить этот тестовый файл:
mix test test/app_web/controllers/item_html_test.exsВы увидите следующую ошибку (потому что функция еще не существует!):
** (UndefinedFunctionError) function AppWeb.ItemHTML.checked/1 is undefined or private
Откройте файл lib/app_web/controllers/item_html.ex и напишите функции, чтобы пройти тесты.
Вот как мы реализовали функции. Ваш файл item_html.ex теперь должен выглядеть следующим образом.
defmodule AppWeb.ItemHTML do
use AppWeb , :html
embed_templates "item_html/*"
# add class "completed" to a list item if item.status=1
def complete ( item ) do
case item . status do
1 -> "completed"
_ -> "" # empty string means empty class so no style applied
end
end
endПереуратить тесты, и теперь они должны пройти:
mix test test/app_web/controllers/item_html_test.exsВы должны увидеть:
....
Finished in 0.1 seconds
4 tests, 0 failuresТеперь, когда мы создали эти две функции представления, и наши тесты проходят, давайте используем их в нашем шаблоне!
Откройте файл lib/app_web/controllers/item_html/index.html.eex и найдите строку:
< ul class =" todo-list " > Замените содержимое <ul> на следующее:
< %= for item < - @items do % >
< li data-id = {item.id} class = {complete(item)} >
< div class =" view " >
< %= if item.status == 1 do % >
< input class =" toggle " type =" checkbox " checked />
< % else % >
< input class =" toggle " type =" checkbox " />
< % end % >
< label > < %= item.text % > </ label >
< .link
class="destroy"
href={~p"/items/#{item}"}
method="delete"
>
</ .link >
</ div >
</ li >
< % end % > Например: lib/app_web/controllers/item_html/index.html.heex#L43-L53
С помощью этих двух файлов сохранились, если вы запускаете приложение сейчас: mix phx.server и посетите http: // localhost: 4000/items.
Вы увидите реальные items вы создали на шаге 2.2 выше:

Теперь, когда у нас есть наши элементы рендеринга в макете ToDomvc, давайте поработаем над созданием новых элементов в стиле «приложения для одной страницы».
В настоящее время наша форма «Новый элемент» доступна по адресу: http: // localhost: 4000/item/new ( как отмечено на шаге 2 выше )
Мы хотим, чтобы человек мог создать новый элемент, не перемещаясь на другую страницу. Чтобы достичь этой цели, мы включим шаблон lib/app_web/controllers/item_html/new.html.heex ( частичный ) внутри шаблона lib/app_web/controllers/item_html/index.html.heex . например:
Прежде чем мы сможем это сделать, нам нужно укоренить шаблон new.html.heex , чтобы удалить поля, которые нам не нужны .
Давайте откроем lib/app_web/controllers/item_html/new.html.heex и упростите его только в необходимое поле :text :
< . simple_form :let = { f } for = { @ changeset } action = { ~p " /items " } >
< . input
field = { { f , :text } }
type = "text"
placeholder = "what needs to be done?"
/ >
< :actions >
< . button style = "display:none" > Save Item< / . button >
< / :actions >
< / . simple_form >
До:
/lib/app_web/controllers/item_html/new.html.heex
After:/lib/app_web/controllers/item_html/new.html.heex
Нам нужно дополнительно изменить стиль тега <.input> . С Phoenix, внутри файла lib/app_web/components/core_components.ex , стили определяются для предварительно созданных компонентов (что имеет место с <.input> ).
Чтобы изменить это, чтобы он использовал тот же стиль, что и ToDomvc, найдите следующую строку.
def input ( assigns ) do
Измените атрибут класса с помощью класса new-todo . Эта функция должна выглядеть следующим образом.
def input ( assigns ) do
~H """
<div phx-feedback-for={@name}>
<.label for={@id}><%= @label %></.label>
<input
type={@type}
name={@name}
id={@id || @name}
value={@value}
class={[
input_border(@errors),
"new-todo"
]}
{@rest}
/>
<.error :for={msg <- @errors}><%= msg %></.error>
</div>
"""
end Нам также необходимо изменить стили actions внутри simple_form . В том же файле def simple_form(assigns) do и измените его так:
def simple_form ( assigns ) do
~H """
<.form :let={f} for={@for} as={@as} {@rest}>
<div>
<%= render_slot(@inner_block, f) %>
<div :for={action <- @actions}>
<%= render_slot(action, f) %>
</div>
</div>
</.form>
"""
end Если вы запустите приложение Phoenix сейчас и посетите http: // localhost: 4000/itse/new, вы увидите поля Single :text Input и No «Сохранить»:

Не волнуйтесь, вы все равно можете отправить форму с ключом Enter (return). Однако, если вы попытаетесь отправить форму сейчас, она не сработает, потому что мы удалили два поля, необходимые для changeset ! Давайте исправим это.
items , чтобы установить значения default Учитывая, что мы удалили два поля ( :person_id и :status ) из new.html.eex , мы должны убедиться, что в схеме есть значения по умолчанию. Откройте файл lib/app/todo/item.ex и замените содержимое следующим:
defmodule App.Todo.Item do
use Ecto.Schema
import Ecto.Changeset
schema "items" do
field :person_id , :integer , default: 0
field :status , :integer , default: 0
field :text , :string
timestamps ( )
end
@ doc false
def changeset ( item , attrs ) do
item
|> cast ( attrs , [ :text , :person_id , :status ] )
|> validate_required ( [ :text ] )
end
end Здесь мы обновляем schema «элементы», чтобы установить значение по умолчанию 0 как для person_id , так и status . И в changeset/2 мы удаляем требование для person_id и status . Таким образом, наша новая форма item может быть отправлена только с помощью text поля.
Например: /lib/app/todo/item.ex#L6-L7
Теперь, когда у нас есть значения default для person_id и status , если вы отправите /items/new форму, она будет успешной.
index/2 в ItemController Чтобы встроить новую форму элемента ( new.html.eex ) в шаблоне index.html.eex , нам нужно обновить AppWeb.ItemController.index/2 чтобы включить набор изменений.
Откройте файл lib/app_web/controllers/item_controller.ex и обновите функцию index/2 до следующего:
def index ( conn , _params ) do
items = Todo . list_items ( )
changeset = Todo . change_item ( % Item { } )
render ( conn , "index.html" , items: items , changeset: changeset )
end До: /lib/app_web/controllers/item_controller.ex
After: /lib/app_web/controllers/item_controller.ex#L9-L10
Вы не увидите никаких изменений в пользовательском интерфейсе или тестах после этого шага. Просто перейдите к 5.3, где происходит момент "AHA".
new.html.eex внутри index.html.eex Теперь, когда мы проделали всю подготовку, следующим шагом является то, чтобы представить new.html.eex ( частичный ) внутри index.html.eex шаблон.
Откройте файл lib/app_web/controllers/item_html/index.html.heex и найдите строку:
< input class =" new-todo " placeholder =" What needs to be done? " autofocus ="" >Замените его этим:
< % = new ( Map . put ( assigns , :action , ~p " /items/new " ) ) % > Давайте разберем то, что мы только что сделали. Мы внедряем new.html.heex index.html.heex Мы делаем это, вызывая функцию new/2 внутри item_controller.ex . Эта функция относится к странице в items/new и видит файл new.html.heex . Следовательно, почему мы называем эту функцию успешно внедренным?
До: /lib/app_web/controllers/item_html/index.html.heex#L36
After: /lib/app_web/controllers/item_html/index.html.heex#L36
Если вы запускаете приложение сейчас и посетите: http: // localhost: 4000/items
Вы можете создать элемент, набрав свой текст и отправите его с ключом Enter (return).

Передача на шаблон «Показать» - это «ОК», но мы можем сделать лучше, перенаправляясь, чтобы вернуться в шаблон index.html . К счастью, это так же просто, как обновить одну строку в коде.
redirect в create/2 Откройте файл lib/app_web/controllers/item_controller.ex и найдите функцию create . В частности, линия:
|> redirect ( to: ~p " /items/ #{ item } " )Обновить строку:
|> redirect ( to: ~p " /items/ " ) До: /lib/app_web/controllers/item_controller.ex#L22
After: /lib/app_web/controllers/item_controller.ex#L23
Теперь, когда мы создаем новый item , мы перенаправлены на шаблон index.html :

item_controller_test.exs для перенаправления в index Изменения, которые мы внесли в файлы new.html.heex и приведенные выше шаги, нарушили некоторые из наших автоматизированных тестов. Мы должны это исправить.
Запустите тесты:
mix testВы увидите следующий вывод:
Finished in 0.08 seconds (0.03s async, 0.05s sync)
23 tests, 3 failures
Откройте файл test/app_web/controllers/item_controller_test.exs и найдите describe "new item" и describe "create item" . Измените эти два на следующее.
Замените тест:
describe "new item" do
test "renders form" , % { conn: conn } do
conn = get ( conn , ~p " /items/new " )
assert html_response ( conn , 200 ) =~ "what needs to be done?"
end
end
describe "create item" do
test "redirects to show when data is valid" , % { conn: conn } do
conn = post ( conn , ~p " /items " , item: @ create_attrs )
assert % { } = redirected_params ( conn )
assert redirected_to ( conn ) == ~p " /items/ "
end
test "errors when invalid attributes are passed" , % { conn: conn } do
conn = post ( conn , ~p " /items " , item: @ invalid_attrs )
assert html_response ( conn , 200 ) =~ "can't be blank"
end
endОбновленный код:
/test/app_web/controllers/item_controller_test.exs#L34-L55
Если вы заново запустите тесты mix test теперь все пройдут снова.
......................
Finished in 0.2 seconds (0.09s async, 0.1s sync)
22 tests, 0 failuresДо сих пор основная функциональность пользовательского интерфейса ToDomvc работает, мы можем создавать новые элементы, и они появляются в нашем списке. На этом этапе мы собираемся улучшить пользовательский интерфейс, чтобы включить количество оставшихся предметов в левом нижнем углу.
Откройте файл test/app_web/controllers/item_html_test.exs и создайте следующие два теста:
test "remaining_items/1 returns count of items where item.status==0" do
items = [
% { text: "one" , status: 0 } ,
% { text: "two" , status: 0 } ,
% { text: "done" , status: 1 }
]
assert ItemHTML . remaining_items ( items ) == 2
end
test "remaining_items/1 returns 0 (zero) when no items are status==0" do
items = [ ]
assert ItemHTML . remaining_items ( items ) == 0
end Например: test/app_web/controllers/item_html_test.exs#L14-L26
Эти тесты не будут сбой, поскольку функция ItemHTML.remaining_items/1 не существует.
Сделайте тесты пройти , добавив следующий код в файл lib/app_web/controllers/item_html.ex :
# returns integer value of items where item.status == 0 (not "done")
def remaining_items ( items ) do
Enum . filter ( items , fn i -> i . status == 0 end ) |> Enum . count
end Например: /lib/app_web/controllers/item_html#L15-L17
Теперь, когда тесты проходят, используйте remaining_items/1 в шаблоне index.html . Откройте файл lib/app_web/controllers/item_html/index.html.eex и найдите строку кода:
< span class =" todo-count " > < strong > 1 </ strong > item left </ span >Замените его этой линией:
< span class =" todo-count " > < %= remaining_items(@items) % > items left </ span > Это просто вызывает функцию ItemHTML.remaining_items/1 со списком @items , которая вернет целое число оставшихся элементов, которые еще не были «сделаны».
Например: /lib/app_web/controllers/item_html/index.html.eex#L60
На этом этапе (оставшиеся) элементы противостоят левому нижнему отдела пользовательского интерфейса ToDomvc!
Добавьте new элемент в свой список и посмотрите увеличение количества:

Это было достаточно легко, давайте попробуем что -нибудь более продвинутое!
Сделайте перерыв и возьмите себе свежий стакан воды, следующий раздел будет интенсивным !
status элемента Todo до 1 Одной из основных функций списка TODO является переключение status item от 0 до 1 («полное»).
В нашей схеме заполненный item имеет status 1 .
Нам понадобится две функции в нашем контроллере:
toggle_status/1 переключает статус элемента, например: от 1 до 1 и 1 до 0.toggle/2 Функция обработчика для HTTP -запросов, чтобы переключить состояние элемента. Откройте файл test/app_web/controllers/item_controller_test.exs . Мы собираемся внести некоторые изменения здесь, чтобы мы могли добавить тесты к функциям, которые мы упоминали ранее. Мы собираемся импортировать App.Todo внутри item_controller_test.exs и исправить константы создания и атрибута для создания фиктивных элементов. Убедитесь, что начало файла выглядит так.
defmodule AppWeb.ItemControllerTest do
use AppWeb.ConnCase
alias App.Todo
import App.TodoFixtures
@ create_attrs % { person_id: 42 , status: 0 , text: "some text" }
@ public_create_attrs % { person_id: 0 , status: 0 , text: "some public text" }
@ completed_attrs % { person_id: 42 , status: 1 , text: "some text completed" }
@ public_completed_attrs % { person_id: 0 , status: 1 , text: "some public text completed" }
@ update_attrs % { person_id: 43 , status: 1 , text: "some updated text" }
@ invalid_attrs % { person_id: nil , status: nil , text: nil }
Мы добавляем атрибуты фиксированного Item , чтобы позже использоваться в тестах. Мы указываем public Item , потому что позже мы добавим аутентификацию в это приложение.
После этого найдите функцию defp create_item()/1 в том же файле. Измените это так, чтобы это выглядело так.
defp create_item ( _ ) do
item = item_fixture ( @ create_attrs )
% { item: item }
end Мы будем использовать эту функцию для создания объектов Item для использования в тестах, которые мы собираемся добавить. Кстати, давайте сделаем это! Добавьте следующий фрагмент в файл.
describe "toggle updates the status of an item 0 > 1 | 1 > 0" do
setup [ :create_item ]
test "toggle_status/1 item.status 1 > 0" , % { item: item } do
assert item . status == 0
# first toggle
toggled_item = % { item | status: AppWeb.ItemController . toggle_status ( item ) }
assert toggled_item . status == 1
# second toggle sets status back to 0
assert AppWeb.ItemController . toggle_status ( toggled_item ) == 0
end
test "toggle/2 updates an item.status 0 > 1" , % { conn: conn , item: item } do
assert item . status == 0
get ( conn , ~p ' /items/toggle/ #{ item . id } ' )
toggled_item = Todo . get_item! ( item . id )
assert toggled_item . status == 1
end
end Например: /test/app_web/controllers/item_controller_test.exs#L64-L82
Откройте файл lib/app_web/controllers/item_controller.ex и добавьте в него следующие функции:
def toggle_status ( item ) do
case item . status do
1 -> 0
0 -> 1
end
end
def toggle ( conn , % { "id" => id } ) do
item = Todo . get_item! ( id )
Todo . update_item ( item , % { status: toggle_status ( item ) } )
conn
|> redirect ( to: ~p " /items " )
end Например: /lib/app_web/controllers/item_controller.ex#L64-L76
Тесты все равно пройдут на этом этапе, потому что маршрут, который мы используем в нашем тесте, еще не существует. Давайте исправим это!
get /items/toggle/:id маршрут, который вызывает toggle/2 Откройте lib/app_web/router.ex и найдите линейные resources "/items", ItemController . Добавьте новую линию:
get "/items/toggle/:id" , ItemController , :toggle Например: /lib/app_web/router.ex#L21
Теперь наши тесты наконец пройдут:
mix testВы должны увидеть:
22:39:42.231 [info] Already up
...........................
Finished in 0.5 seconds
27 tests, 0 failurestoggle/2 когда флажок нажимается в index.html Теперь, когда наши тесты проходят, пришло время использовать всю эту функциональность, которую мы строили в пользовательском интерфейсе. Откройте файл /lib/app_web/controllers/item_html/index.html.heex и найдите строку:
< %= if item.status == 1 do % >
...
< % else % >
...
< % end % >Замените его следующим образом:
< %= if item.status == 1 do % >
< .link href={~p"/items/toggle/#{item.id}"}
class="toggle checked" >
type="checkbox"
</ .link >
< % else % >
< .link href={~p"/items/toggle/#{item.id}"}
type="checkbox"
class="toggle" >
</ .link >
< % end % > Когда эта ссылка нажимается, вызывает get /items/toggle/:id конечная точка,
Это, в свою очередь, запускает обработчик toggle/2 который мы определили выше.
До:
/lib/app_web/controllers/item_html/index.html.heex#L40
After:/lib/app_web/controllers/item_html/index.html.heex#L47-L57
.checked CSS в app.scss К сожалению, теги <a> (которые генерируются с помощью <.link> ) не могут иметь :checked псевдо -селектор, поэтому стили по умолчанию todomvc, которые работали над тегом <input> , не будут работать для ссылки. Таким образом, нам нужно добавить пару строк CSS в нашу app.scss .
Откройте файл assets/css/app.scss и добавьте в него следующие строки:
. todo-list li . checked + label {
background-image : url ( 'data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E' );
background-repeat : no-repeat;
} После сохранения файла у вас должно быть: /assets/css/app.scss#L8
И когда вы просматриваете приложение, функциональность переключения работает, как и ожидалось:

Примечание к реализации : мы очень намеренно не используем JavaScript в этом уроке, потому что мы демонстрируем, как сделать 100% -ное приложение на сервере. Это всегда работает, даже когда JS отключен в браузере, или устройство очень старое и не имеет современного веб -браузера. Мы могли бы легко добавить атрибут onclick в тег <input> , например:
< input < %= checked(item) % > type="checkbox" class="toggle"
onclick="location.href='
< %= Routes.item_path(@conn, :toggle, item.id) % > ';" > Но onclick - это JavaScript , и нам не нужно прибегать к JS .
<a> (ссылка)-это совершенно семантический подход без JS к переключению item.status .
todo Если вы «завершаете» или возвращаете операцию, порядок TODOS может различаться между этими операциями. Чтобы сохранить это последовательным, давайте возьмем все элементы todo в том же порядке.
Inside lib/app/todo.ex , измените list_items/0 на следующее.
def list_items do
query =
from (
i in Item ,
select: i ,
order_by: [ asc: i . id ]
)
Repo . all ( query )
end Приобретая предметы todo и заказывая их, мы гарантируем, что UX остается последовательным!
Последняя функциональность , которую мы должны добавить в наш пользовательский интерфейс, - это возможность редактировать текст элемента.
В конце этого шага у вас будет работать в линейном редактировании:

Причина , по которой требуется два щелчка для редактирования элемента, заключается в том, что люди не редактируют предмет при прокрутке. Поэтому они должны сознательно щелкнуть/нажать дважды , чтобы редактировать.
В Spec ToDomvc это достигается путем создания слушателя событий для события с двойным щелчком и заменив элемент <label> на <input> . Мы пытаемся избежать использования JavaScript в нашем приложении на стороне сервера ( на данный момент ), поэтому мы хотим использовать альтернативный подход. К счастью, мы можем имитировать событие двойного щелчка, используя Just HTML и CSS . См.: https://css-tricks.com/double-click-in-css ( мы рекомендуем прочитать этот пост и демонстрацию, чтобы полностью понять, как работает этот CSS !)
ПРИМЕЧАНИЕ . Реализация CSS не является истинным двойным щелчком, более точным описанием будет «два щелчка», потому что два щелчка могут произойти с произвольной задержкой. т.е. первый щелчок, за которым следует 10 секунд, и второй щелчок будет иметь тот же эффект, что и два щелчка в быстрой последовательности. Если вы хотите реализовать True Double Click, см.
Давайте продолжим! Откройте файл lib/app_web/controllers/item_html/index.html.heex и найдите строку:
< % = new ( Map . put ( assigns , :action , ~p " /items/new " ) ) % >Замените его:
< % = if @ editing . id do % >
<. link href = { ~p " /items " }
method = "get"
class= "new-todo" >
Click here to create a new item!
< / . link >
< % else % >
< % = new ( Map . put ( assigns , :action , ~p " /items/new " ) ) % >
< % end % > Здесь мы проверяем, если мы редактируем элемент, и рендеринг ссылки вместо формы. Мы делаем это, чтобы избежать нескольких форм на странице. Если мы не редактируем предмет, отобразите new.html.heex как и раньше. При этом, если пользователь редактирует элемент, он может «выйти из режима редактирования», нажав на отображаемой ссылку.
Например: lib/app_web/controllers/item_html/index.html.heex#L30-L38
Далее, все еще в файле index.html.eex , найдите строку:
< %= for item < - @items do % > Замените весь тег <li> на следующий код.
< li data - id = { item . id } class= { complete ( item ) } >
< % = if item . status == 1 do % >
<. link href = { ~p " /items/toggle/ #{ item . id } " }
class= "toggle checked" >
type = "checkbox"
< / . link >
< % else % >
< . link href = { ~p " /items/toggle/ #{ item . id } " }
type = "checkbox"
class= "toggle" >
< / . link >
< % end % >
< div class = "view" >
< % = if item . id == @ editing . id do % >
< % = edit (
Map . put ( assigns , :action , ~p " /items/ #{ item . id } /edit " )
|> Map . put ( :item , item )
) % >
< % else % >
< . link href = { ~p " /items/ #{ item } /edit " } class= "dblclick" >
< label > < % = item . text % > < / label >
< / . link >
< span > < / span > < ! -- used for CSS Double Click -- >
< % end % >
< . link
class = "destroy"
href = { ~p " /items/ #{ item } " }
method= "delete"
>
< / . link >
< / div >
< /li> Например: lib/app_web/controllers/item_html/index.html.heex#L46-L79
Мы сделали несколько вещей здесь. Мы изменили кнопку переключения за пределами <div class="view> Tag. Кроме того, мы изменили текст с помощью операторов блока, if else .
Если пользователь не редактирует, ссылка ( <a> ) отображается, которая при нажатии позволяет пользователю вводить режим «Редактировать». С другой стороны, если пользователь редактирует , он отображает файл edit.html.heex .
Говоря об этом, давайте отредактируем edit.html.heex , чтобы он отдавал то, что мы хотим: текстовое поле, которое, как только Enter , нажимается, редактирует элемент ссылки.
< .simple_form :let={f} for={@changeset} method="put" action={~p"/items/#{@item}"} >
< .input
field={{f, :text}}
type="text"
placeholder="what needs to be done?"
class="new-todo"
/>
< :actions >
< .button
style="display: none;"
type="submit" >
Save
</ .button >
</ :actions >
<!-- submit the form using the Return/Enter key -->
</ .simple_form >CSS для редактирования Чтобы включить эффект CSS двойной щелчок, чтобы ввести режим edit , нам нужно добавить следующие CSS в наш файл assets/css/app.scss :
. dblclick {
position : relative; /* So z-index works later, but no surprises now */
}
. dblclick + span {
position : absolute;
top : -1 px ; /* these negative numbers are to ensure */
left : -1 px ; /* that the <span> covers the <a> */
width : 103 % ; /* Gotta do this instead of right: 0; */
bottom : -1 px ;
z-index : 1 ;
}
. dblclick + span : active {
left : -9999 px ;
}
. dblclick : hover {
z-index : 2 ;
} Например: assets/css/app.css#L13-L32
Кроме того, поскольку наша разметка немного отличается от разметки ToDomvc, нам нужно добавить немного больше CSS, чтобы поддерживать постоянный пользовательский интерфейс:
. todo-list li . toggle + div > a > label {
background-image : url ( 'data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E' );
background-repeat : no-repeat;
background-position : center left;
}
. todo-list li . checked + div > a > label
{
background-image : url ( 'data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E' );
background-repeat : no-repeat;
background-position : center left;
}
. toggle {
width : 10 % ;
z-index : 3 ; /* keep the toggle checkmark above the rest */
}
a . new-todo {
display : block;
text-decoration : none;
}
. todo-list . new-todo {
border : 1 px # 1abc9c solid;
}
. view a , . view a : visited {
display : block;
text-decoration : none;
color : # 2b2d2f ;
}
. todo-list li . destroy {
text-decoration : none;
text-align : center;
z-index : 3 ; /* keep the delete link above the text */
} Это то, как должен выглядеть ваш файл app.scss в конце этого шага: assets/css/app.css#L34-L71
ItemController.edit/2 Чтобы включить встроенное редактирование, нам необходимо изменить функцию edit/2 . Откройте файл lib/app_web/controllers/item_controller.ex и замените функцию edit/2 на следующее:
def edit ( conn , params ) do
index ( conn , params )
end Кроме того, учитывая, что мы просим нашу функцию index/2 для обработки редактирования, нам нужно обновить index/2 :
def index ( conn , params ) do
item = if not is_nil ( params ) and Map . has_key? ( params , "id" ) do
Todo . get_item! ( params [ "id" ] )
else
% Item { }
end
items = Todo . list_items ( )
changeset = Todo . change_item ( item )
render ( conn , "index.html" , items: items , changeset: changeset , editing: item )
end Наконец, нам нужно обработать представление формы, чтобы обновить элемент (который отображается в edit.html.heex ). Когда мы нажимаем Enter , обработчик update/2 называется внутри lib/app_web/controllers/item_controller.ex . Мы хотим остаться на той же странице после обновления элемента.
Итак, измените это так, чтобы это выглядело так.
def update ( conn , % { "id" => id , "item" => item_params } ) do
item = Todo . get_item! ( id )
case Todo . update_item ( item , item_params ) do
{ :ok , _item } ->
conn
|> redirect ( to: ~p " /items/ " )
{ :error , % Ecto.Changeset { } = changeset } ->
render ( conn , :edit , item: item , changeset: changeset )
end
end Ваш файл item_controller.ex теперь должен выглядеть следующим образом: lib/app_web/controllers/item_controller.ex
ItemControllerTestВ нашем стремлении создать одно страницу приложения мы разбили несколько тестов! Это нормально. Их легко исправить.
Откройте файл test/app_web/controllers/item_controller_test.exs и найдите тест со следующим текстом.
test "renders form for editing chosen item"
И изменить это, чтобы это выглядело следующим образом.
test "renders form for editing chosen item" , % { conn: conn , item: item } do
conn = get ( conn , ~p " /items/ #{ item } /edit " )
assert html_response ( conn , 200 ) =~ "Click here to create a new item"
end Когда мы вводим в «Редактировать режим таймера», мы создаем <a> ссылку для возврата в /items , как мы ранее реализовали. Этот тег имеет текст «Нажмите здесь, чтобы создать новый элемент», который мы утверждаем.
Например: test/app_web/controllers/item_controller_test.exs#L37-L39
Затем найдите тест со следующим описанием:
describe "update item"Обновите блок по следующему кусочке кода.
describe "update item" do
setup [ :create_item ]
test "redirects when data is valid" , % { conn: conn , item: item } do
conn = put ( conn , ~p " /items/ #{ item } " , item: @ update_attrs )
assert redirected_to ( conn ) == ~p " /items/ "
conn = get ( conn , ~p " /items/ " )
assert html_response ( conn , 200 ) =~ "some updated text"
end
test "errors when invalid attributes are passed" , % { conn: conn , item: item } do
conn = put ( conn , ~p " /items/ #{ item } " , item: @ invalid_attrs )
assert html_response ( conn , 200 ) =~ "can't be blank"
end
end Например: test/app_web/controllers/item_controller_test.exs#L67-L80
Мы обновили пути, которые приложение перенаправляет после обновления элемента. Поскольку мы создаем одностраничное приложение, этот путь относится к /items/ URL-пути.
Если вы запускаете тесты сейчас, они должны пройти снова:
mix test
23:08:01.785 [info] Already up
...........................
Finished in 0.5 seconds
27 tests, 0 failures
Randomized with seed 956565
index.html Теперь, когда у нас работают функции toggle и edit , мы можем, наконец, удалить макет Phoenix (таблица) по умолчанию из шаблона lib/app_web/controllers/item_html/index.html.heex .

Откройте файл lib/app_web/controllers/item_html/index.html.eex и удалите весь код перед строкой:
< section class =" todoapp " > Например: lib/app_web/controllers/item_html/index.html.heex
Ваше приложение теперь должно выглядеть так: 
К сожалению, удалив макет по умолчанию, мы «сломали» тесты.
Откройте файл test/app_web/controllers/item_controller_test.exs и найдите тест, который имеет следующее описание:
test "lists all items"Обновите утверждение из:
assert html_response ( conn , 200 ) =~ "Listing Items"К:
assert html_response ( conn , 200 ) =~ "todos" Например: test/app_web/controllers/item_controller_test.exs#L14
Теперь, когда работает функциональность Core (Create, Edit/Update, Delete), мы можем добавить окончательные улучшения пользовательского интерфейса. На этом этапе мы собираемся добавить навигацию/фильтрацию нижней части нижней части.

Представление «Все» - по умолчанию. «Актив» - это все элементы со status==0 . «Завершенные» - это все элементы со status==1 .
/:filterПеред началом давайте добавим модульный тест. Мы хотим показать фильтрованные элементы в соответствии с выбранным фильтром.
Откройте test/app_web/controllers/item_controller_test.exs и найдите describe "index" do . В этом блоке добавьте следующий тест. Он проверяет, правильно ли показан элемент при изменении фильтра.
test "lists items in filter" , % { conn: conn } do
conn = post ( conn , ~p " /items " , item: @ public_create_attrs )
# After creating item, navigate to 'active' filter page
conn = get ( conn , ~p " /items/filter/active " )
assert html_response ( conn , 200 ) =~ @ public_create_attrs . text
# Navigate to 'completed page'
conn = get ( conn , ~p " /items/filter/completed " )
assert ! ( html_response ( conn , 200 ) =~ @ public_create_attrs . text )
end Например: test/app_web/controllers/item_controller_test.exs#L21-L32
Откройте lib/app_web/router.ex и добавьте следующий маршрут:
get "/items/filter/:filter" , ItemController , :index например: /lib/app_web/router.ex#L23
index/2 для отправки filter для просмотра/шаблона Откройте файл lib/app_web/controllers/item_controller.ex и найдите функцию index/2 . Замените вызов render/3 в конце index/2 на следующее:
render ( conn , "index.html" ,
items: items ,
changeset: changeset ,
editing: item ,
filter: Map . get ( params , "filter" , "all" )
) Например: lib/app_web/controllers/item_controller.ex#L17-L22
Map.get(params, "filter", "all") Устанавливает значение по умолчанию нашего filter «все», поэтому, когда index.html отображается, покажите элементы «все».
filter/2 Чтобы фильтровать элементы по их статусу, нам нужно создать новую функцию.
Откройте файл lib/app_web/controllers/item_html.ex и создайте функцию filter/2 следующим образом:
def filter ( items , str ) do
case str do
"items" -> items
"active" -> Enum . filter ( items , fn i -> i . status == 0 end )
"completed" -> Enum . filter ( items , fn i -> i . status == 1 end )
_ -> items
end
end Например: lib/app_web/controllers/item_html.ex#L19-L26
Это позволит нам отфильтровать элементы на следующем шаге.
index.html Используйте функцию filter/2 , чтобы фильтровать отображаемые элементы. Откройте файл lib/app_web/controllers/item_html/index.html.heex и найдите строку for цикла:
< % = for item <- @ items do % >Замените его:
< % = for item <- filter ( @ items , @ filter ) do % > Например: lib/app_web/controllers/item_html/index.html.heex#L18
Это вызывает функцию filter/2 которую мы определили на предыдущем шаге, проходящем в списке @items и выбранной @filter .
Далее найдите The <footer> и замените содержимое <ul class="filters"> на следующий код:
< li >
< % = if @ filter == "items" do % >
<a href = "/items/filter/items" class= "selected" >
All
< / a >
< % else % >
<a href = "/items/filter/items" >
All
< / a >
< % end % >
< / li >
< li >
< % = if @ filter == "active" do % >
<a href = "/items/filter/active" class= 'selected' >
Active
[ < % = Enum . count ( filter ( @ items , "active" ) ) % > ]
< / a >
< % else % >
<a href = "/items/filter/active" >
Active
[ < % = Enum . count ( filter ( @ items , "active" ) ) % > ]
< / a >
< % end % >
< / li >
< li >
< % = if @ filter == "completed" do % >
<a href = "/items/filter/completed" class= 'selected' >
Completed
[ < % = Enum . count ( filter ( @ items , "completed" ) ) % > ]
< / a >
< % else % >
<a href = "/items/filter/completed" >
Completed
[ < % = Enum . count ( filter ( @ items , "completed" ) ) % > ]
< / a >
< % end % >
< / li > Мы условно добавляем selected класс в соответствии со значением назначения @filter .
например: /lib/app_web/controllers/item_html/index.html.heex#L62-L98
В конце этого шага у вас будет полностью функционирующий фильтр нижнего колонтитула:

Мы можем быстро охватить эту функцию, которую мы добавили с помощью небольшого модульного теста. Откройте test/app_web/controllers/item_html_test.exs и добавьте следующее.
test "test filter function" do
items = [
% { text: "one" , status: 0 } ,
% { text: "two" , status: 0 } ,
% { text: "three" , status: 1 } ,
% { text: "four" , status: 2 } ,
% { text: "five" , status: 2 } ,
% { text: "six" , status: 1 } ,
]
assert length ( ItemHTML . filter ( items , "items" ) ) == 4
assert length ( ItemHTML . filter ( items , "active" ) ) == 2
assert length ( ItemHTML . filter ( items , "completed" ) ) == 2
assert length ( ItemHTML . filter ( items , "any" ) ) == 4
endИ вы должны покончить с этой функцией? Потрясающая работа!
Мы почти закончили с нашей реализацией Phoenix toDomvc. Последнее, что нужно реализовать, - это «четкое завершено».
Откройте файл lib/app_web/router.ex и добавьте следующий маршрут:
get "/items/clear" , ItemController , :clear_completed Ваша scope "/" теперь должна выглядеть следующим образом:
scope "/" , AppWeb do
pipe_through :browser
get "/" , PageController , :home
get "/items/toggle/:id" , ItemController , :toggle
get "/items/clear" , ItemController , :clear_completed
get "/items/filter/:filter" , ItemController , :index
resources "/items" , ItemController
end В файле lib/app_web/controllers/item_controller.ex добавьте следующий код:
import Ecto.Query
alias App.Repo
def clear_completed ( conn , _param ) do
person_id = 0
query = from ( i in Item , where: i . person_id == ^ person_id , where: i . status == 1 )
Repo . update_all ( query , set: [ status: 2 ] )
# render the main template:
index ( conn , % { filter: "all" } )
end Например: lib/app_web/controllers/item_controller.ex#L87-L93
Это использует удобную функцию update_all/3 для обновления всех элементов, которые соответствуют query . В нашем случае мы ищем все items , которые принадлежат person_id==0 и имеют status==1 .
Мы не удаляем элементы, скорее, мы обновляем их статус 2 что для целей нашего примера означает, что они «архивированы».
Примечание : это полезное руководство для
update_all: https://adamdelong.com/bulk-update-ecto
Наконец, в прокрутке lib/app_web/controllers/item_html/index.html.eex до нижней части файла и замените строку:
< button class = "clear-completed" style = "display: block;" >
Clear completed
< / button >С:
< a class = "clear-completed" href = "/items/clear" >
Clear completed
[ < % = Enum . count ( filter ( @ items , "completed" ) ) % > ]
< / a > Например: lib/app_web/controllers/item_html/index.html.heex#L104-L107
Последнее, что нам нужно сделать, это обновить функцию filter/2 внутри lib/app_web/controllers/item_html.ex . Поскольку status = 2 теперь относится к архивному состоянию, мы хотим вернуть все, что не архивировано.
Change the filter/2 function so it looks like so.
def filter ( items , str ) do
case str do
"items" -> Enum . filter ( items , fn i -> i . status !== 2 end )
"active" -> Enum . filter ( items , fn i -> i . status == 0 end )
"completed" -> Enum . filter ( items , fn i -> i . status == 1 end )
_ -> Enum . filter ( items , fn i -> i . status !== 2 end )
end
endAt the end of this section your Todo List should have the "Clear completed" function working:

It's useful to have tests cover this feature. Open test/app_web/controllers/item_controller_test.exs . Alongside the constants, on top of the file, add the following line.
@completed_attrs %{person_id: 42, status: 1, text: "some text completed"}
We will use this to create an item that is already completed, so we can test the "clear completed" functionality.
Add the next lines to test the clear_completed/2 function.
describe "clear completed" do
setup [ :create_item ]
test "clears the completed items" , % { conn: conn , item: item } do
# Creating completed item
conn = post ( conn , ~p " /items " , item: @ public_completed_attrs )
# Clearing completed items
conn = get ( conn , ~p " /items/clear " )
items = conn . assigns . items
[ completed_item | _tail ] = conn . assigns . items
assert conn . assigns . filter == "all"
assert completed_item . status == 2
end
test "clears the completed items in public (person_id=0)" , % { conn: conn , item: item } do
# Creating completed item
conn = post ( conn , ~p " /items " , item: @ public_completed_attrs )
# Clearing completed items
conn = get ( conn , ~p " /items/clear " )
items = conn . assigns . items
[ completed_item | _tail ] = conn . assigns . items
assert conn . assigns . filter == "all"
assert completed_item . status == 2
end
endAt this point we already have a fully functioning Phoenix Todo List. There are a few things we can tidy up to make the App even better!
If you are the type of person to notice the tiny details, you would have been itching each time you saw the " 1 items left " in the bottom left corner:

Open your test/app_web/controllers/item_html_test.exs file and add the following test:
test "pluralise/1 returns item for 1 item and items for < 1 <" do
assert ItemHTML . pluralise ( [ % { text: "one" , status: 0 } ] ) == "item"
assert ItemHTML . pluralise ( [
% { text: "one" , status: 0 } ,
% { text: "two" , status: 0 }
] ) == "items"
assert ItemHTML . pluralise ( [ % { text: "one" , status: 1 } ] ) == "items"
end eg: test/app_web/controllers/item_html_test.exs#L28-L35
This test will obviously fail because the AppWeb.ItemHTML.pluralise/1 is undefined. Let's make it pass!
Open your lib/app_web/controllers/item_html.ex file and add the following function definition for pluralise/1 :
# pluralise the word item when the number of items is greater/less than 1
def pluralise ( items ) do
# items where status < 1 is equal to Zero or Greater than One:
case remaining_items ( items ) == 0 || remaining_items ( items ) > 1 do
true -> "items"
false -> "item"
end
end eg: lib/app_web/controllers/item_html.ex#L28-L35
Note : we are only pluralising one word in our basic Todo App so we are only handling this one case in our
pluralise/1function. In a more advanced app we would use a translation tool to do this kind of pluralising. See: https://hexdocs.pm/gettext/Gettext.Plural.html
Finally, use the pluralise/1 in our template. Open lib/app_web/controllers/item_html/index.html.heex
Locate the line:
< span class = "todo-count" > < % = remaining_items ( @ items ) % > items left < /span>And replace it with the following code:
< span class = "todo-count" >
< % = remaining_items ( @ items ) % > < % = pluralise ( @ items ) % > left
< /span> eg: lib/app_web/controllers/item_html/index.html.heex#L61
At the end of this step you will have a working pluralisation for the word item/items in the bottom left of the UI:

If you visit one of the TodoMVC examples, you will see that no <footer> is displayed when there are no items in the list: http://todomvc.com/examples/vanillajs

At present our App shows the <footer> even if their are Zero items: ?

This is a visual distraction/clutter that creates unnecessary questions in the user's mind. Let's fix it!
Open your lib/app_web/controllers/item_html.ex file and add the following function definition unarchived_items/1 :
def got_items? ( items ) do
Enum . filter ( items , fn i -> i . status < 2 end ) |> Enum . count > 0
end eg: lib/app_web/controllers/item_html.ex#L37-L39
Now use got_items?/1 in the template.
Wrap the <footer> element in the following if statement:
< % = if got_items? ( @ items ) do % >
< % end % > eg: lib/app_web/controllers/item_html/index.html.heex#L58
The convention in Phoenix/Elixir ( which came from Ruby/Rails ) is to have a ? (question mark) in the name of functions that return a Boolean ( true/false ) result.
At the end of this step our <footer> element is hidden when there are no items:

/ to ItemController.index/2 The final piece of tidying up we can do is to change the Controller that gets invoked for the "homepage" ( / ) of our app. Currently when the person viewing the Todo App
visits http://localhost:4000/ they see the lib/app_web/controllers/page_html/home.html.eex template:

This is the default Phoenix home page ( minus the CSS Styles and images that we removed in step 3.4 above ). It does not tell us anything about the actual app we have built, it doesn't even have a link to the Todo App! Let's fix it!
Open the lib/app_web/router.ex file and locate the line:
get "/" , PageController , :index Update the controller to ItemController .
get "/" , ItemController , :index eg: lib/app_web/router.ex#L20
Now when you run your App you will see the todo list on the home page:

Unfortunately, this update will "break" the page test. Run the tests and see:
1) test GET / (AppWeb.PageControllerTest)
test/app_web/controllers/page_controller_test.exs:4
Assertion with = ~ failed
code: assert html_response(conn, 200) = ~ " Welcome to Phoenix! "
left: " <!DOCTYPE html>n<html lang= " en " >n <head>n ... " Given that we are no longer using the Page Controller, View, Template or Tests, we might as well delete them from our project!
git rm lib/app_web/controllers/page_controller.ex
git rm lib/app_web/controllers/page_html.ex
git rm lib/app_web/page_html/home.html.heex
git rm test/app_web/controllers/page_controller_test.exs Deleting files is good hygiene in any software project. Don't be afraid to do it, you can always recover files that are in your git history.
Re-run the tests:
mix test
You should see them pass now:
...........................
Finished in 0.5 seconds
27 tests, 0 failures
Given that our Phoenix Todo List App is 100% server rendered, older browsers will perform a full page refresh when an action (create/edit/toggle/delete) is performed. This will feel like a "blink" in the page and on really slow connections it will result in a temporary blank page ! Obviously, that's horrible UX and is a big part of why Single Page Apps (SPAs) became popular; to avoid page refresh, use Turbo !
Get the performance benefits of an SPA without the added complexity of a client-side JavaScript framework. When a link is clicked/tapped, Turbolinks automatically fetches the page, swaps in its <body> , and merges its <head> , all without incurring the cost of a full page load.
Luckily, adding Turbo will require just a simple copy and paste! Check the unpkg files to fetch the latest CDN package.
We now need to add the following line to lib/app_web/components/layouts/app.html.heex and lib/app_web/components/layouts/root.html.heex .
< script src =" https://unpkg.com/browse/@hotwired/[email protected]/dist/turbo.es2017-esm.js " > </ script > This will install the UMD builds from Turbo without us needing to install a package using npm . Neat, huh?
И это все! Now when you deploy your server rendered Phoenix App, it will feel like an SPA! Try the Fly.io demo again: phxtodo.fly.dev Feel that buttery-smooth page transition.
Currently, our application occurs in the same page. However, there is a route that we don't use and is also aesthetically incompatible with the rest of our app.

If we check lib/app_web/controllers/item_controller.ex , you might notice the following function.
def show ( conn , % { "id" => id } ) do
item = Todo . get_item! ( id )
render ( conn , :show , item: item )
end This serves the GET /items/:id route. We could do the same as we did with edit and render index . However, let's do something different so we learn a bit more about routes.
If we head on to router.ex , and locate the line:
resources "/items" , ItemControllerWe can change it to this.
resources "/items" , ItemController , except: [ :show ] We are saying that we want to keep all the routes in ItemController except the one related to the show action.
We can now safely delete it from item_controller.ex , as we don't need it any more.
Your files should look like the following.
eg: /lib/router.ex#L19-L29 lib/app_web/controllers/item_controller.ex
Currently, the application allows anyone to access it and manage todo items . Wouldn't it be great if we added authentication so each person could check their own list?
We created a dedicated authentication guide: /auth.md to help you set this up. You will soon find out this is extremely easy ?.
Deployment to Fly.io takes a few minutes, but has a few "steps", we suggest you follow the speed run guide: https://fly.io/docs/elixir/getting-started/
Once you have deployed you will will be able to view/use your app in any Web/Mobile Browser.
eg: https://phxtodo.fly.dev
xs

Our Phoenix server currently only returns HTML pages that are server-side rendered . This is already awesome but we can make use of Phoenix to extend its capabilities.
What if our server also responded with JSON ? You're in luck! We've created small guide for creating a REST API : api.md
If you found this example useful, please ️ the GitHub repository so we ( and others ) know you liked it!
If you want to learn more Phoenix and the magic of LiveView , consider reading our beginner's tutorial: github.com/dwyl/ phoenix-liveview-counter-tutorial
Thank you for learning with us! ☀