Um tutorial passo a passo completo para a construção de uma lista de Todo em Phoenix.
100% funcional. 0% JavaScript. Apenas HTML , CSS e Elixir . Rápido e sustentável.
As listas de tarefas são familiares para a maioria das pessoas; Fazemos listas o tempo todo. Construir uma lista de tarefas do zero é uma ótima maneira de aprender Elixir / Phoenix porque a UI / UX é simples , para que possamos nos concentrar na implementação.
Para a equipe @dwyl este aplicativo/tutorial é uma vitrine de como a renderização do lado do servidor ( com aprimoramento progressivo do lado do cliente ) pode fornecer um excelente equilíbrio entre a eficácia do desenvolvedor ( recursos de remessa rapidamente ), UX e acessibilidade . As páginas renderizadas pelo servidor levam menos de 5ms para responder para que o UX seja rápido . On Fly.io: Phxtodo.fly.dev O tempo de resposta de ida e volta é sub-200ms para todas as interações, por isso parece um aplicativo renderizado do lado do cliente.
Um tutorial da lista de tarefas que mostra um iniciante completo como criar um aplicativo em Elixir/Phoenix do zero.
Experimente a versão Fly.io. Adicione alguns itens à lista e teste a funcionalidade.

Mesmo com uma viagem de ida e volta HTTP completa para cada interação, o tempo de resposta é rápido . Preste atenção em como o Chrome | Firefox | Safari aguarda a resposta do servidor antes de renderizar a página. A antiga página completa da atualização do passado se foi . Os navegadores modernos renderizam de maneira inteligente apenas as mudanças! Então, o UX se aproxima "nativo"! Sério, tente o aplicativo Fly.io no seu telefone e veja!
Neste tutorial, estamos usando o TODOMVC CSS para simplificar nossa interface do usuário. Isso tem várias vantagens, a maior minimizando quanto CSS temos que escrever! Isso também significa que temos um guia para o qual os recursos precisam ser implementados para obter funcionalidade completa.
Nota : Adoramos
CSSpor seu incrível poder/flexibilidade, mas sabemos que nem todo mundo gosta. Veja: Learn-Tachyons#Por que a última coisa que queremos é perder toneladas de tempo comCSSem um tutorialPhoenix!
Este tutorial é para quem está aprendendo a Elixir/Phoenix. Nenhuma experiência anterior com Phoenix é assumida/esperada. Incluímos todas as etapas necessárias para criar o aplicativo.
Se você ficar preso em qualquer passo, abra um problema no Github, onde estamos felizes em ajudá -lo a se soltar! Se você acha que qualquer linha de código pode usar um pouco mais de explicação/clareza, não hesite em nos informar ! Sabemos como é ser iniciante, pode ser frustrante quando algo não faz sentido! Fazer perguntas no Github ajuda todos a aprender!
Por favor, dê -nos feedback! Estrela o repositório se você achou útil.
Antes de tentar criar a lista de tarefas, verifique se você tem tudo o que precisa instalar no seu computador. Veja: Pré -requisitos
Depois de confirmar que você tem Phoenix & PostgreSQL instalado, tente executar o aplicativo acabado .
localhost Antes de começar a criar sua própria versão do aplicativo TODO List, execute a versão final em sua localhost para confirmar que ele funciona.
Clone o projeto do GitHub:
git clone [email protected]:dwyl/phoenix-todo-list-tutorial.git && cd phoenix-todo-list-tutorialInstale dependências e configure o banco de dados:
mix setupInicie o servidor Phoenix:
mix phx.server Visite localhost:4000 no seu navegador da web.
Você deveria ver:

Agora que você tem o aplicativo de exemplo acabado em execução em sua localhost ,
Vamos construí -lo do zero e entender todas as etapas.
Ao executar o aplicativo de exemplo acabado no localhost , se você deseja experimentar o botão login , precisará obter um AUTH_API_KEY . [1 minuto] Veja: Obtenha seu AUTH_API_KEY
Se você executou o aplicativo acabado em sua localhost ( e você realmente deveria! ),
Você precisará alterar um diretório antes de iniciar o tutorial:
cd ..
Agora você está pronto para construir !
No seu terminal, crie um novo aplicativo Phoenix usando o seguinte comando mix :
mix phx.new app --no-dashboard --no-gettext --no-mailer Quando solicitado a instalar dependências, o tipo Y seguido de Enter .
Nota : esses sinalizadores após o nome do
appsão apenas para evitar a criação de arquivos que não precisamos para este exemplo simples. Veja: hexdocs.pm/phoenix/mix.tasks.phx.new
Mude no diretório app recém -criado ( cd app ) e verifique se você tem tudo o que precisa:
mix setupInicie o servidor Phoenix:
mix phx.server Agora você pode visitar localhost:4000 no seu navegador da web. Você deve ver algo semelhante a:

Desligue o servidor Phoenix Ctrl + c .
Execute os testes para garantir que tudo funcione conforme o esperado:
mix testVocê deveria ver:
Compiling 16 files (.ex)
Generated app app
17:49:40.111 [info] Already up
...
Finished in 0.04 seconds
3 tests, 0 failuresTendo estabelecido que o aplicativo Phoenix funciona conforme o esperado, vamos criar alguns arquivos!
items Ao criar uma lista básica de TODO, precisamos apenas de um esquema: items . Mais tarde, podemos adicionar listas e tags separados para organizar/categorizar nossos items , mas por enquanto isso é tudo o que precisamos.
Execute o seguinte comando gerador para criar a tabela de itens:
mix phx.gen.html Todo Item items text:string person_id:integer status:integer Estritamente falando, precisamos apenas dos campos de text e status , mas como sabemos que queremos associar itens a pessoas (_later no tutorial), estamos adicionando o campo agora .
Você verá a seguinte saída:
* 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
Isso criou um monte de arquivos! Alguns dos quais não precisamos estritamente.
Poderíamos criar manualmente apenas os arquivos de que precisamos , mas essa é a maneira "oficial" de criar um aplicativo CRUD no Phoenix, por isso o estamos usando para velocidade.
NOTA : Os contextos de Phoenix indicados neste exemplo como
Todosão " módulos dedicados que expõem e funcionalidade relacionada ao grupo ". Sentimos que eles complicam desnecessariamente os aplicativos básicos de Phoenix com camadas de "interface" e realmente desejamos poder evitá -los. Mas, como eles são assados nos geradores, e o criador da estrutura gosta deles, temos uma escolha: embarque com contextos ou crie manualmente todos os arquivos em nossos projetos da Phoenix. Os geradores são uma maneira muito mais rápida de construir! Abrace -os, mesmo se você acabar tendo quedeletealguns arquivos não utilizados ao longo do caminho!
Não vamos explicar cada um desses arquivos nesta fase do tutorial, porque é mais fácil entender os arquivos ao criar o aplicativo! O objetivo de cada arquivo ficará claro à medida que você avança na edição.
/items ao router.ex Siga as instruções observadas pelo gerador para adicionar os resources "/items", ItemController ao router.ex .
Abra o arquivo lib/app_web/router.ex e localize a linha: scope "/", AppWeb do . Adicione a linha ao final do bloco. por exemplo:
scope "/" , AppWeb do
pipe_through :browser
get "/" , PageController , :index
resources "/items" , ItemController # this is the new line
end Seu arquivo de router.ex deve ficar assim: router.ex#L20
Agora, como o terminal sugeriu, execute mix ecto.migrate . Isso terminará de configurar as tabelas de banco de dados e executar as migrações necessárias para que tudo funcione corretamente!
Nesse ponto, já temos uma lista de tarefas funcionais ( se estivéssemos dispostos a usar a interface do usuário do Phoenix padrão ).
Tente executar o aplicativo em sua localhost : execute as migrações geradas com mix ecto.migrate e depois o servidor com:
mix phx.server
Visite: http: // localhost: 4000/itens/novo e insira alguns dados.

Clique no botão "Salvar item" e você será redirecionado para a página "Show": http: // localhost: 4000/itens/1

Esta não é uma experiência atraente do usuário (UX), mas funciona ! Aqui está uma lista de itens - uma "lista de tarefas". Você pode visitar isso clicando no botão Back to items ou acessando o seguinte URL http: // localhost: 4000/itens.

Vamos melhorar o UX usando o TODOMVC HTML e CSS !
Para recriar o TODOMVC UI/UX, vamos emprestar o código HTML diretamente do exemplo.
Visite: http://todomvc.com/examples/vanillajs adicionam alguns itens à lista. Em seguida, inspecione a fonte usando as ferramentas de desenvolvimento do seu navegador. por exemplo:

Clique com o botão direito do mouse na fonte que você deseja (por exemplo: <section class="todoapp"> ) e selecione "Editar como html":

Depois que o HTML para a <section> for editável, selecione -o e copie -o.

O código 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 > Vamos converter este HTML em um modelo de elixir ( EEx ) incorporado.
NOTA : O motivo pelo qual estamos copiando esse
HTMLdo inspetor de elementos do navegador, em vez de diretamente da fonte no Github:examples/vanillajs/index.htmlé que este é um "aplicativo de página única", então a<ul class="todo-list"></ul>só fica preenchida no Browser. Copiá -lo das ferramentas de desenvolvimento do navegador é a maneira mais fácil de obter oHTMLcompleto .
index.html.eex Abra o arquivo lib/app_web/controllers/item_html/index.html.eex e role para a parte inferior.
Em seguida ( sem remover o código que já está lá ) Cole o código HTML que adquirimos do ToDomVC.
Por exemplo:
/lib/app_web/controllers/item_html/index.html.eex#L27-L73
Se você tentar executar o aplicativo agora e visite http: // localhost: 4000/itens/
Você verá isso ( sem o TODOMVC CSS ):

Obviamente, isso não é o que queremos, então vamos obter o TODOMVC CSS e salvá -lo em nosso projeto!
/assets/css Visite http://todomvc.com/examples/vanillajs/node_modules/todomvc-app-css/index.css
e salve o arquivo em /assets/css/todomvc-app.css .
por exemplo: /assets/css/todomvc-app.css
todomvc-app.css em app.scss Abra o arquivo assets/css/app.scss e substitua -o pelo seguinte:
/* This file is for your main application css. */
/* @import "./phoenix.css"; */
@import "./todomvc-app.css" ; por exemplo: /assets/css/app.scss#L4
Abra seu arquivo lib/app_web/components/layouts/app.html.heex e substitua o conteúdo pelo seguinte código:
<!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 >Antes:
lib/app_web/components/layouts/app.html.eex
Depois:lib/app_web/components/layouts/app.html.heex
<%= @inner_content %> é onde o aplicativo TODO será renderizado.
Nota : A tag
<script>está incluída fora da convenção. No entanto, não escreveremos nenhumJavaScriptneste tutorial. Alcançaremos 100% de paridade de recursos com o TODOMVC, sem escrever uma linha deJS. Nós não "odiamos"JS, na verdade temos um tutorial de "irmã" que cria o mesmo aplicativo emJS: Dwyl/JavaScript-Todo-List-Tutorial, apenas queremos lembrá- lo de que você não precisa de nenhumJSpara criar um aplicativo da Web totalmente funcional com ótimo UX!
Com o modelo de layout salvo, o arquivo TODOMVC CSS salva em /assets/css/todomvc-app.css e na página todomvc-app.css importados em app.scss , sua página /items deve agora ficar assim:

Portanto, nossa lista de tarefas está começando a parecer TODOMVC, mas ainda é apenas uma lista fictícia.
Para renderizar os dados item no modelo TODOMVC, precisaremos adicionar algumas funções. Quando criamos o projeto e geramos o modelo item , um controlador foi criado (localizado em lib/app_web/controllers/item_controller.ex ) e também um componente/exibição (localizado em lib/app_web/controllers/item_html.ex ). Essa visualização/visualização é o que controla efetivamente a renderização do conteúdo dentro do diretório lib/app_web/controllers/item_html com o qual mexemos anterior.
Sabemos que precisamos fazer alterações na interface do usuário, por isso vamos adicionar algumas funções neste componente (o que é semelhante à parte de visualização do paradigma do MVC).
Esta é a nossa primeira chance de fazer um pouco de desenvolvimento orientado a testes (TDD).
Crie um novo arquivo com o Path test/app_web/controllers/item_html_test.exs .
Digite o seguinte código no arquivo:
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 Por exemplo: /test/app_web/controllers/item_html_test.exs
Se você tentar executar este arquivo de teste:
mix test test/app_web/controllers/item_html_test.exsVocê verá o seguinte erro (porque a função ainda não existe!):
** (UndefinedFunctionError) function AppWeb.ItemHTML.checked/1 is undefined or private
Abra o arquivo lib/app_web/controllers/item_html.ex e escreva as funções para fazer os testes passarem .
Foi assim que implementamos as funções. Seu arquivo item_html.ex agora deve parecer o seguinte.
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
endReencontre os testes e eles devem passar agora:
mix test test/app_web/controllers/item_html_test.exsVocê deveria ver:
....
Finished in 0.1 seconds
4 tests, 0 failuresAgora que criamos essas duas funções de exibição e nossos testes estão passando, vamos usá -los em nosso modelo!
Abra o arquivo lib/app_web/controllers/item_html/index.html.eex e localize a linha:
< ul class =" todo-list " > Substitua o conteúdo do <ul> pelo seguinte:
< %= 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 % > Por exemplo: lib/app_web/controllers/item_html/index.html.heex#L43-L53
Com esses dois arquivos salvos, se você executar o aplicativo agora: mix phx.server e visite http: // localhost: 4000/itens.
Você verá os items reais que você criou na Etapa 2.2 acima:

Agora que temos nossos itens renderizando no layout do TODOMVC, vamos trabalhar para criar novos itens no estilo "App de página única".
Atualmente, nosso formulário "novo item" está disponível em: http: // localhost: 4000/itens/novo ( conforme observado na etapa 2 acima )
Queremos que a pessoa possa criar um novo item sem precisar navegar para uma página diferente. Para atingir esse objetivo, incluiremos o modelo lib/app_web/controllers/item_html/new.html.heex ( parcial ) dentro do lib/app_web/controllers/item_html/index.html.heex . por exemplo:
Antes que possamos fazer isso, precisamos arrumar o modelo new.html.heex para remover os campos de que não precisamos .
Vamos abrir lib/app_web/controllers/item_html/new.html.heex e simplifique -o apenas para o campo essencial :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 >
Antes:
/lib/app_web/controllers/item_html/new.html.heex
Depois:/lib/app_web/controllers/item_html/new.html.heex
Precisamos alterar adicionalmente o estilo da tag <.input> . Com Phoenix, dentro do arquivo lib/app_web/components/core_components.ex , os estilos são definidos para componentes pré-criados (que é o caso de <.input> ).
Para alterar isso para que ele use o mesmo estilo que o TODOMVC, localize a seguinte linha.
def input ( assigns ) do
Altere o atributo de classe com a classe new-todo . Esta função deve se parecer com a seguinte.
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 Também precisamos alterar os estilos actions dentro da simple_form . No mesmo arquivo, procure def simple_form(assigns) do e altere -o para que pareça assim:
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 Se você executar o aplicativo Phoenix agora e visite http: // localhost: 4000/itens/novo, você verá o campo de entrada Single :text e nenhum botão "Salvar":

Não se preocupe, você ainda pode enviar o formulário com a tecla Enter (Return). No entanto, se você tentar enviar o formulário agora, ele não funcionará porque removemos dois dos campos exigidos pela changeset ! Vamos consertar isso.
items para definir valores default Dado que removemos dois dos campos ( :person_id e :status ) do new.html.eex , precisamos garantir que haja valores padrão para eles no esquema. Abra o arquivo lib/app/todo/item.ex e substitua o conteúdo pelo seguinte:
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 Aqui estamos atualizando o schema "itens" para definir um valor padrão de 0 para person_id e status . E na changeset/2 estamos removendo o requisito para person_id e status . Dessa forma, nosso novo formulário item pode ser enviado apenas com o campo text .
Por exemplo: /lib/app/todo/item.ex#L6-L7
Agora que temos valores default para person_id e status Se você enviar o /items/new formulário, ele terá sucesso.
index/2 no ItemController Para incluir o novo formulário de item ( new.html.eex ) no modelo index.html.eex , precisamos atualizar o AppWeb.ItemController.index/2 para incluir um alteração.
Abra o arquivo lib/app_web/controllers/item_controller.ex e atualize a função index/2 para o seguinte:
def index ( conn , _params ) do
items = Todo . list_items ( )
changeset = Todo . change_item ( % Item { } )
render ( conn , "index.html" , items: items , changeset: changeset )
end Antes: /lib/app_web/controllers/item_controller.ex
Depois: /lib/app_web/controllers/item_controller.ex#L9-L10
Você não verá nenhuma alteração na interface do usuário ou nos testes após esta etapa. Basta passar para 5.3, onde acontece o momento "Aha".
new.html.eex Inside index.html.eex Agora que fizemos todo o trabalho de preparação, a próxima etapa é renderizar o new.html.eex ( parcial ) interno index.html.eex modelo.
Abra o arquivo lib/app_web/controllers/item_html/index.html.heex e localize a linha:
< input class =" new-todo " placeholder =" What needs to be done? " autofocus ="" >Substitua por isso:
< % = new ( Map . put ( assigns , :action , ~p " /items/new " ) ) % > Vamos quebrar o que acabamos de fazer. Estamos incorporando o new.html.heex parcial dentro do arquivo index.html.heex . Estamos fazendo isso chamando a função new/2 dentro item_controller.ex . Esta função refere -se à página nos items/new URL e renderiza o arquivo new.html.heex . Portanto, por que chamamos essa função para incorporar com sucesso?
Antes: /lib/app_web/controllers/item_html/index.html.heex#L36
Depois: /lib/app_web/controllers/item_html/index.html.heex#L36
Se você executar o aplicativo agora e visitar: http: // localhost: 4000/itens
Você pode criar um item digitando seu texto e enviá -lo com a tecla Enter (Return).

Redirecionar para o modelo "show" é "OK", mas podemos fazer melhor UX redirecionando para voltar ao modelo index.html . Felizmente, isso é tão fácil quanto atualizar uma única linha no código.
redirect em create/2 Abra o arquivo lib/app_web/controllers/item_controller.ex e localize a função create . Especificamente a linha:
|> redirect ( to: ~p " /items/ #{ item } " )Atualize a linha para:
|> redirect ( to: ~p " /items/ " ) Antes: /lib/app_web/controllers/item_controller.ex#L22
Depois: /lib/app_web/controllers/item_controller.ex#L23
Agora, quando criamos um novo item somos redirecionados para o modelo index.html :

item_controller_test.exs para redirecionar para index As alterações que fizemos nos arquivos new.html.heex e as etapas acima quebraram alguns de nossos testes automatizados. Devemos consertar isso.
Execute os testes:
mix testVocê verá a seguinte saída:
Finished in 0.08 seconds (0.03s async, 0.05s sync)
23 tests, 3 failures
Abra o arquivo test/app_web/controllers/item_controller_test.exs e localize describe "new item" e describe "create item" . Altere esses dois para o seguinte.
Substitua o teste:
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
endCódigo atualizado:
/test/app_web/controllers/item_controller_test.exs#L34-L55
Se você executar novamente os testes mix test a vontade agora passou novamente.
......................
Finished in 0.2 seconds (0.09s async, 0.1s sync)
22 tests, 0 failuresAté agora, a principal funcionalidade da interface do usuário do TODOMVC está funcionando, podemos criar novos itens e eles aparecem em nossa lista. Nesta etapa, vamos aprimorar a interface do usuário para incluir a contagem de itens restantes no canto inferior esquerdo.
Abra o arquivo test/app_web/controllers/item_html_test.exs e crie os dois testes a seguir:
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 Por exemplo: test/app_web/controllers/item_html_test.exs#L14-L26
Esses testes falharão porque a função ItemHTML.remaining_items/1 não existe.
Faça os testes passarem adicionando o código a seguir ao lib/app_web/controllers/item_html.ex Arquivo:
# 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 por exemplo: /lib/app_web/controllers/item_html#L15-L17
Agora que os testes estão passando, use o remaining_items/1 no modelo index.html . Abra o arquivo lib/app_web/controllers/item_html/index.html.eex e localize a linha de código:
< span class =" todo-count " > < strong > 1 </ strong > item left </ span >Substitua -o por esta linha:
< span class =" todo-count " > < %= remaining_items(@items) % > items left </ span > Isso apenas chama a função ItemHTML.remaining_items/1 com a lista de @items que retornará a contagem inteira dos itens restantes que ainda não foram "feitos".
Por exemplo: /lib/app_web/controllers/item_html/index.html.eex#L60
Neste ponto, os itens (restantes) contador no canto inferior esquerdo da interface do usuário do TODOMVC estão funcionando !
Adicione um new item à sua lista e observe o aumento da contagem:

Isso foi fácil, vamos tentar algo um pouco mais avançado!
Faça uma pausa e pegue um copo fresco de água, a próxima seção será intensa !
status do item de TODO para 1 Uma das funções principais de uma lista de TODO é alternar o status de um item de 0 a 1 ("Complete").
Em nosso esquema, um item concluído tem o status de 1 .
Vamos precisar de duas funções em nosso controlador:
toggle_status/1 alterna o status de um item, por exemplo: 0 a 1 e 1 a 0.toggle/2 A função Handler para solicitações HTTP para alternar o status de um item. Abra o arquivo test/app_web/controllers/item_controller_test.exs . Vamos fazer algumas alterações aqui para que possamos adicionar testes às funções que mencionamos antes. Vamos importar App.Todo dentro de item_controller_test.exs e corrija constantes de criação e atributo para criar itens simulados. Verifique se o início do arquivo parece assim.
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 }
Estamos adicionando atributos de Item fixos para serem usados posteriormente nos testes. Estamos especificando Item public porque mais tarde adicionaremos autenticação a este aplicativo.
Depois disso, localize defp create_item()/1 função dentro do mesmo arquivo. Mude para que pareça assim.
defp create_item ( _ ) do
item = item_fixture ( @ create_attrs )
% { item: item }
end Usaremos essa função para criar objetos Item para usar nos testes que vamos adicionar. Falando nisso, vamos fazer isso! Adicione o seguinte snippet ao arquivo.
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 por exemplo: /test/app_web/controllers/item_controller_test.exs#L64-L82
Abra o arquivo lib/app_web/controllers/item_controller.ex e adicione as seguintes funções a ele:
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 Por exemplo: /lib/app_web/controllers/item_controller.ex#L64-L76
Os testes ainda falharão neste momento, porque a rota que estamos chamando em nosso teste ainda não existe. Vamos consertar isso!
get /items/toggle/:id que chama toggle/2 Abra o lib/app_web/router.ex e localize os resources "/items", ItemController . Adicione uma nova linha:
get "/items/toggle/:id" , ItemController , :toggle por exemplo: /lib/app_web/router.ex#L21
Agora nossos testes finalmente passarão:
mix testVocê deveria ver:
22:39:42.231 [info] Already up
...........................
Finished in 0.5 seconds
27 tests, 0 failurestoggle/2 quando uma caixa de seleção for clicada em index.html Agora que nossos testes estão passando, é hora de usar toda essa funcionalidade que estamos construindo na interface do usuário. Abra o /lib/app_web/controllers/item_html/index.html.heex e localize a linha:
< %= if item.status == 1 do % >
...
< % else % >
...
< % end % >Substitua -o pelo seguinte:
< %= 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 % > Quando este link é clicado, o ponto de extremidade get /items/toggle/:id é chamado,
Isso, por sua vez, desencadeia o manipulador toggle/2 que definimos acima.
Antes:
/lib/app_web/controllers/item_html/index.html.heex#L40
Depois:/lib/app_web/controllers/item_html/index.html.heex#L47-L57
.checked app.scss Infelizmente, as tags <a> (que são geradas com <.link> ) não podem ter um seletor de pseudo :checked , portanto, os estilos padrão do TODOMVC que funcionaram na tag <input> não funcionarão para o link. Portanto, precisamos adicionar algumas linhas de CSS ao nosso app.scss .
Abra o arquivo assets/css/app.scss e adicione as seguintes linhas a ele:
. 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;
} Depois de salvar o arquivo, você deve ter: /assets/css/app.scss#L8
E quando você visualiza o aplicativo, a funcionalidade de alternância está funcionando como esperado:

Nota de implementação : não estamos deliberadamente usando um JavaScript neste tutorial, porque estamos demonstrando como fazer um aplicativo renderizado de 100% do servidor. Isso sempre funciona mesmo quando JS é desativado no navegador ou o dispositivo é super antigo e não possui um navegador da web moderno. Poderíamos facilmente ter adicionado um atributo onclick à tag <input> , por exemplo:
< input < %= checked(item) % > type="checkbox" class="toggle"
onclick="location.href='
< %= Routes.item_path(@conn, :toggle, item.id) % > ';" > Mas onclick é JavaScript e não precisamos recorrer ao JS .
O <a> (link) é uma abordagem perfeitamente semântica de JS para alternar item.status .
todo Se você "concluir" ou reverter a operação, a ordem dos TODOs pode diferir entre essas operações. Para manter isso consistente, vamos buscar todos os itens todo na mesma ordem.
Inside lib/app/todo.ex , altere list_items/0 para o seguinte.
def list_items do
query =
from (
i in Item ,
select: i ,
order_by: [ asc: i . id ]
)
Repo . all ( query )
end Ao buscar os itens todo e encomendá -los, garantimos que o UX permaneça consistente!
A peça final de funcionalidade que precisamos adicionar à nossa interface do usuário é a capacidade de editar o texto de um item.
No final desta etapa, você terá edição em linha trabalhando:

O motivo para exigir dois cliques para editar um item é para que as pessoas não editem acidentalmente um item durante a rolagem. Portanto, eles precisam clicar/tocar deliberadamente duas vezes para editar.
Na especificação TODOMVC, isso é alcançado criando um ouvinte de evento para o evento de clique duas vezes e substituindo o elemento <label> por um <input> . Estamos tentando evitar o uso de JavaScript em nosso aplicativo Phoenix renderizado do lado do servidor ( por enquanto ), por isso queremos usar uma abordagem alternativa. Felizmente, podemos simular o evento de clique duas vezes usando apenas HTML e CSS . Veja: https://css-tricks.com/double-click-n-css ( recomendamos ler essa postagem e a demonstração para entender completamente como esse CSS funciona !)
NOTA : A implementação do CSS não é um verdadeiro clique duas vezes, uma descrição mais precisa seria "dois cliques" porque os dois cliques podem ocorrer com um atraso arbitrário. ou seja, clique primeiro, seguido por 10 segundos de espera e o segundo clique terá o mesmo efeito que dois cliques em rápida sucessão. Se você deseja implementar o verdadeiro clique duas vezes, consulte: github.com/dwyl/javascript-todo-list-tutorial#52-double click
Vamos continuar com isso! Abra o arquivo lib/app_web/controllers/item_html/index.html.heex e localize a linha:
< % = new ( Map . put ( assigns , :action , ~p " /items/new " ) ) % >Substitua -o por:
< % = 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 % > Aqui, estamos verificando se estamos editando um item e renderizando um link em vez do formulário. Fazemos isso para evitar ter vários formulários na página. Se não estivermos editando um item, renderize o new.html.heex como antes. Com isso, se o usuário estiver editando um item, ele poderá "sair do modo de edição" clicando no link renderizado.
Por exemplo: lib/app_web/controllers/item_html/index.html.heex#L30-L38
Em seguida, ainda no arquivo index.html.eex , localize a linha:
< %= for item < - @items do % > Substitua a tag <li> inteira pelo código a seguir.
< 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> Por exemplo: lib/app_web/controllers/item_html/index.html.heex#L46-L79
Fizemos algumas coisas aqui. Alteramos o botão de alternância para fora da tag <div class="view> . Além disso, alteramos o texto com uma declaração de bloco if else .
Se o usuário não estiver editando, um link ( <a> ) for renderizado que, quando clicado, permite que o usuário insira o modo "editar". Por outro lado, se o usuário estiver editando , ele renderiza o arquivo edit.html.heex .
Falando nisso, vamos editar edit.html.heex para que ele renderize o que queremos: um campo de texto que, uma vez que Enter é pressionado, edita o item de referência a ToDO.
< .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 para edição Para ativar o efeito de clique duas vezes CSS para entrar no modo edit , precisamos adicionar o seguinte arquivo CSS aos nossos 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 ;
} Por exemplo: assets/css/app.css#L13-L32
Além disso, como nossa marcação é um pouco diferente da marcação do TODOMVC, precisamos adicionar um pouco mais de CSS para manter a interface do usuário consistente:
. 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 */
} É assim que o seu arquivo app.scss deve ser no final desta etapa: assets/css/app.css#L34-L71
ItemController.edit/2 Para ativar a edição em linha, precisamos modificar a função edit/2 . Abra o arquivo lib/app_web/controllers/item_controller.ex e substitua a função edit/2 pelo seguinte:
def edit ( conn , params ) do
index ( conn , params )
end Além disso, dado que estamos pedindo à nossa função index/2 para lidar com a edição, precisamos atualizar 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 Finalmente, precisamos lidar com o envio do formulário para atualizar um item (que é renderizado em edit.html.heex ). Quando pressionamos Enter , o manipulador update/2 é chamado Inside lib/app_web/controllers/item_controller.ex . Queremos ficar na mesma página depois de atualizar o item.
Então, mude para que pareça assim.
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 Seu arquivo item_controller.ex agora deve ficar assim: lib/app_web/controllers/item_controller.ex
ItemControllerTestEm nossa busca para criar um aplicativo de uma única página, quebramos alguns testes! Isso está ok. Eles são fáceis de consertar.
Abra o arquivo test/app_web/controllers/item_controller_test.exs e localize o teste com o texto a seguir.
test "renders form for editing chosen item"
e mude para que pareça o seguinte.
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 Quando entramos no "modo de timer de edição", <a> um link para retornar /items , como implementamos anteriormente. Esta tag tem o texto "Clique aqui para criar um novo item", que é o que estamos afirmando.
Por exemplo: test/app_web/controllers/item_controller_test.exs#L37-L39
Em seguida, localize o teste com a seguinte descrição:
describe "update item"Atualize o bloco para a seguinte parte do código.
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 Por exemplo: test/app_web/controllers/item_controller_test.exs#L67-L80
Atualizamos os caminhos que o aplicativo redireciona depois de atualizar um item. Como estamos construindo um aplicativo de uma página, esse caminho refere-se ao caminho /items/ URL.
Se você executar os testes agora, eles devem passar novamente:
mix test
23:08:01.785 [info] Already up
...........................
Finished in 0.5 seconds
27 tests, 0 failures
Randomized with seed 956565
index.html Agora que temos os recursos toggle e edit funcionando, podemos finalmente remover o layout Phoenix (tabela) padrão do lib/app_web/controllers/item_html/index.html.heex Modelo.

Abra o arquivo lib/app_web/controllers/item_html/index.html.eex e remova todo o código antes da linha:
< section class =" todoapp " > Por exemplo: lib/app_web/controllers/item_html/index.html.heex
Seu aplicativo agora deve ficar assim: 
Infelizmente, ao remover o layout padrão, "quebramos" os testes.
Abra o arquivo test/app_web/controllers/item_controller_test.exs e localize o teste que possui a seguinte descrição:
test "lists all items"Atualize a afirmação de:
assert html_response ( conn , 200 ) =~ "Listing Items"Para:
assert html_response ( conn , 200 ) =~ "todos" Por exemplo: test/app_web/controllers/item_controller_test.exs#L14
Agora que a funcionalidade CORE (CREATE, EDIT/ATUALIZAÇÃO, DELETE) está funcionando, podemos adicionar os aprimoramentos finais da interface do usuário. Nesta etapa, vamos adicionar a navegação/filtragem do rodapé.

A visualização "All" é o padrão. O "ativo" é todos os itens com status==0 . "Concluído" é todos os itens com status==1 .
/:filterAntes de começar, vamos adicionar um teste de unidade. Queremos mostrar itens filtrados de acordo com o filtro escolhido.
Open test/app_web/controllers/item_controller_test.exs e locate describe "index" do . Neste bloco, adicione o seguinte teste. Ele verifica se o item está sendo mostrado corretamente quando o filtro é alterado.
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 Por exemplo: test/app_web/controllers/item_controller_test.exs#L21-L32
Abra o lib/app_web/router.ex e adicione a seguinte rota:
get "/items/filter/:filter" , ItemController , :index por exemplo: /lib/app_web/router.ex#L23
index/2 para enviar filter para visualizar/modelo Abra o arquivo lib/app_web/controllers/item_controller.ex e localize a função index/2 . Substitua a invocação de render/3 no final do index/2 pelo seguinte:
render ( conn , "index.html" ,
items: items ,
changeset: changeset ,
editing: item ,
filter: Map . get ( params , "filter" , "all" )
) Por exemplo: lib/app_web/controllers/item_controller.ex#L17-L22
Map.get(params, "filter", "all") define o valor padrão do nosso filter como "tudo", para que index.html seja renderizado, mostre itens "todos".
filter/2 Função de exibição Para filtrar os itens por seu status, precisamos criar uma nova função.
Abra o arquivo lib/app_web/controllers/item_html.ex e crie a função filter/2 da seguinte forma:
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 Por exemplo: lib/app_web/controllers/item_html.ex#L19-L26
Isso nos permitirá filtrar os itens na próxima etapa.
index.html Use a função filter/2 para filtrar os itens exibidos. Abra o arquivo lib/app_web/controllers/item_html/index.html.heex e localize a linha for LOOP:
< % = for item <- @ items do % >Substitua -o por:
< % = for item <- filter ( @ items , @ filter ) do % > Por exemplo: lib/app_web/controllers/item_html/index.html.heex#L18
Isso chama a função filter/2 que definimos na etapa anterior passando na lista de @items e no @filter selecionado.
Em seguida, localize o <footer> e substitua o conteúdo do <ul class="filters"> pelo seguinte código:
< 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 > Estamos adicionando condicionalmente a classe selected de acordo com o valor @filter Atribuir.
Por exemplo: /lib/app_web/controllers/item_html/index.html.heex#L62-L98
No final desta etapa, você terá um filtro de rodapé em pleno funcionamento:

Podemos cobrir rapidamente essa função que adicionamos com um pequeno teste de unidade. Abra test/app_web/controllers/item_html_test.exs e adicione o seguinte.
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
endE você deve terminar com esse recurso?. Trabalho incrível!
Quase terminamos a implementação do Phoenix do TODOMVC. A última coisa a implementar é "claro concluído".
Abra seu arquivo lib/app_web/router.ex e adicione a seguinte rota:
get "/items/clear" , ItemController , :clear_completed Seu scope "/" agora deve parecer o seguinte:
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 No arquivo lib/app_web/controllers/item_controller.ex Adicione o seguinte código:
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 Por exemplo: lib/app_web/controllers/item_controller.ex#L87-L93
Isso usa a função Handy update_all/3 para atualizar todos os itens que correspondem à query . No nosso caso, pesquisamos todos items que pertencem a person_id==0 e temos status==1 .
Não estamos excluindo os itens, mas estamos atualizando seu status para 2 , que para os fins do nosso exemplo significa que eles estão "arquivados".
NOTA : Este é um guia útil para
update_all: https://adamdelong.com/bulk-update-ecto
Finalmente, no lib/app_web/controllers/item_html/index.html.eex role na parte inferior do arquivo e substitua a linha:
< button class = "clear-completed" style = "display: block;" >
Clear completed
< / button >Com:
< a class = "clear-completed" href = "/items/clear" >
Clear completed
[ < % = Enum . count ( filter ( @ items , "completed" ) ) % > ]
< / a > Por exemplo: lib/app_web/controllers/item_html/index.html.heex#L104-L107
A última coisa que precisamos fazer é atualizar a função filter/2 dentro de lib/app_web/controllers/item_html.ex . Como status = 2 agora refere -se a um estado arquivado , queremos devolver qualquer coisa que não seja arquivada.
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?
E é isso! 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 ? Você está com sorte! 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! ☀️