Un tutorial completo para principiantes paso a paso para construir una lista de tareas en Phoenix.
100% funcional. 0% JavaScript. Solo HTML , CSS y Elixir . Rápido y mantenible.
Las listas de tareas son familiares para la mayoría de las personas; Hacemos listas todo el tiempo. Construir una lista de tareas desde cero es una excelente manera de aprender Elixir / Phoenix porque la UI / UX es simple , por lo que podemos centrarnos en la implementación.
Para el equipo @dwyl esta aplicación/tutorial es una muestra de cómo la representación del lado del servidor ( con mejora progresiva del lado del cliente ) puede proporcionar un excelente equilibrio entre la efectividad del desarrollador ( funciones de envío rápidas ), UX y accesibilidad . Las páginas renderizadas tardan menos de 5 ms para responder, por lo que el UX es rápido . En Fly.io: phxtodo.fly.dev Los tiempos de respuesta de ida y vuelta son menores de 200 ms para todas las interacciones, por lo que se siente como una aplicación representada del lado del cliente.
Un tutorial de la lista de tareas que muestra un principiante completo sobre cómo crear una aplicación en Elixir/Phoenix desde cero.
Prueba la versión Fly.io. Agregue algunos elementos a la lista y pruebe la funcionalidad.

Incluso con un viaje redondo HTTP completo para cada interacción, el tiempo de respuesta es rápido . Preste atención a cómo Chrome | Firefox | Safari espera la respuesta del servidor antes de volver a reproducir la página. La antigua réplica de la página completa de Yesteryear se ha ido . ¡Los navegadores modernos hacen de manera inteligente los cambios! ¡Entonces el UX se aproxima a "Nativo"! En serio, prueba la aplicación Fly.io en tu teléfono y mira.
En este tutorial estamos utilizando el CSS ToDomVC para simplificar nuestra interfaz de usuario. ¡Esto tiene varias ventajas que se minimizan la cantidad de CSS que tenemos que escribir! También significa que tenemos una guía de la cual las características deben implementarse para lograr una funcionalidad completa.
Nota : Nos encanta
CSSpor su increíble poder/flexibilidad, pero sabemos que no a todos les gusta. Ver: Aprender-Tachyons#¡Por qué lo último que queremos es perder mucho tiempo conCSSen un tutorialPhoenix!
Este tutorial es para cualquiera que esté aprendiendo a Elixir/Phoenix. No se supone/esperada experiencia previa con Phoenix. Hemos incluido todos los pasos necesarios para construir la aplicación.
Si te atascas en cualquier paso, ¡abra un problema en GitHub, donde estamos felices de ayudarte a despegarte! Si cree que cualquier línea de código puede usar un poco más de explicación/claridad, ¡no dude en informarnos ! Sabemos lo que es ser un principiante, ¡puede ser frustrante cuando algo no tiene sentido! ¡Hacer preguntas en GitHub ayuda a todos a aprender!
¡Por favor danos comentarios! Estrata el repositorio si lo encontró útil.
Antes de intentar construir la lista de TODO, asegúrese de tener todo lo que necesita instalado en su computadora. Ver: Requisitos previos
Una vez que haya confirmado que tiene instalado Phoenix y PostgreSQL, intente ejecutar la aplicación terminada .
localhost Antes de comenzar a construir su propia versión de la aplicación TODO List, ejecute la versión terminada en su localhost para confirmar que funciona.
Clon el proyecto de GitHub:
git clone [email protected]:dwyl/phoenix-todo-list-tutorial.git && cd phoenix-todo-list-tutorialInstalar dependencias y configurar la base de datos:
mix setupInicie el servidor Phoenix:
mix phx.server Visite localhost:4000 en su navegador web.
Deberías ver:

Ahora que tiene la aplicación de ejemplo terminada ejecutándose en su localhost ,
Vamos a construirlo desde cero y comprender todos los pasos.
Al ejecutar la aplicación de ejemplo terminada en localhost , si desea probar el botón login , deberá obtener un AUTH_API_KEY . [1 minuto] Ver: Obtenga su AUTH_API_KEY
Si ejecutaste la aplicación terminada en tu localhost (¡ y realmente deberías! ),
Deberá cambiar un directorio antes de comenzar el tutorial:
cd ..
¡Ahora estás listo para construir !
En su terminal, cree una nueva aplicación Phoenix usando el siguiente comando mix :
mix phx.new app --no-dashboard --no-gettext --no-mailer Cuando se le solicite que instale dependencias, tipo Y seguido de Enter .
Nota : Esos indicadores después del nombre de la
appson solo para evitar la creación de archivos que no necesitamos para este simple ejemplo. Ver: hexdocs.pm/phoenix/mix.tasks.phx.new
Cambie en el recién creado Directorio app ( cd app ) y asegúrese de tener todo lo que necesita:
mix setupInicie el servidor Phoenix:
mix phx.server Ahora puede visitar localhost:4000 en su navegador web. Deberías ver algo similar a:

Apague el servidor Phoenix Ctrl + c .
Ejecute las pruebas para asegurarse de que todo funcione como se esperaba:
mix testDeberías ver:
Compiling 16 files (.ex)
Generated app app
17:49:40.111 [info] Already up
...
Finished in 0.04 seconds
3 tests, 0 failuresHabiendo establecido que la aplicación Phoenix funciona como se esperaba, ¡pasemos a crear algunos archivos!
items Al crear una lista básica de TODO solo necesitamos un esquema: items . Más tarde podemos agregar listas y etiquetas separadas para organizar/clasificar nuestros items , pero por ahora esto es todo lo que necesitamos.
Ejecute el siguiente comando generador para crear la tabla de elementos:
mix phx.gen.html Todo Item items text:string person_id:integer status:integer Estrictamente hablando, solo necesitamos los campos text y status , pero como sabemos que queremos asociar elementos con personas (_later en el tutorial), estamos agregando el campo ahora .
Verá la siguiente salida:
* 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
¡Eso creó un montón de archivos! Algunos de los cuales no necesitamos estrictamente.
Podríamos crear manualmente solo los archivos que necesitamos , pero esta es la forma "oficial" de crear una aplicación CRUD en Phoenix, por lo que lo estamos utilizando para la velocidad.
Nota : los contextos de Phoenix denotados en este ejemplo como
Todo, son " módulos dedicados que exponen y agrupan la funcionalidad relacionada ". Sentimos que complican innecesariamente aplicaciones básicas de Phoenix con capas de "interfaz" y realmente deseamos poder evitarlas. Pero dado que están horneados en los generadores, y al creador del marco le gustan , tenemos una opción: estar a bordo con contextos o crear manualmente todos los archivos en nuestros proyectos de Phoenix. ¡Los generadores son una forma mucho más rápida de construir! ¡ Abrácelos , incluso si terminas teniendo quedeletealgunos archivos no utilizados en el camino!
¡No vamos a explicar cada uno de estos archivos en esta etapa en el tutorial porque es más fácil entender los archivos a medida que está construyendo la aplicación! El propósito de cada archivo quedará claro a medida que avanza a través de la edición.
/items al router.ex Siga las instrucciones anotadas por el generador para agregar los resources "/items", ItemController al router.ex .
Abra el archivo lib/app_web/router.ex y localice la línea: scope "/", AppWeb do . Agregue la línea al final del bloque. p.ej:
scope "/" , AppWeb do
pipe_through :browser
get "/" , PageController , :index
resources "/items" , ItemController # this is the new line
end Su archivo router.ex debería verse así: router.ex#L20
Ahora, como sugirió el terminal, ejecute mix ecto.migrate . ¡Esto terminará de configurar las tablas de la base de datos y ejecutará las migraciones necesarias para que todo funcione correctamente!
En este punto, ya tenemos una lista funcional de TODO ( si estábamos dispuestos a usar la interfaz de usuario de Phoenix predeterminada ).
Intente ejecutar la aplicación en su localhost : ejecute las migraciones generadas con mix ecto.migrate y luego el servidor con:
mix phx.server
Visite: http: // localhost: 4000/elementos/nuevos e ingrese algunos datos.

Haga clic en el botón "Guardar elemento" y será redirigido a la página "Mostrar": http: // localhost: 4000/elementos/1

Esta no es una experiencia de usuario atractiva (UX), ¡pero funciona ! Aquí hay una lista de elementos, una "lista de tareas". Puede visitar esto haciendo clic en el botón Back to items o accediendo a la siguiente URL http: // localhost: 4000/elementos.

¡Mejoremos el UX usando el HTML y CSS de ToDomVC!
Para recrear el ToDomvc UI/UX, tomemos prestado el código HTML directamente del ejemplo.
Visite: http://todomvc.com/examples/vanillajs Agregue un par de elementos a la lista. Luego, inspeccione la fuente con las herramientas de desarrollo de su navegador. p.ej:

Haga clic con el botón derecho en la fuente que desee (por ejemplo: <section class="todoapp"> ) y seleccione "Editar como html":

Una vez que el HTML para la <section> sea editable, seleccione y cópielo.

El código HTML es:
< 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 > Convirtamos este HTML en una plantilla de elixir incrustada ( EEx ).
Nota : La razón por la que estamos copiando este
HTMLdel Inspector Elementos del Browser en lugar de directamente desde la fuente en GitHub:examples/vanillajs/index.htmles que esta es una "aplicación de una sola página", por lo que<ul class="todo-list"></ul>solo se pobla en el navegador. Copiarlo de las herramientas de desarrollo del navegador es la forma más fácil de obtener elHTMLcompleto .
index.html.eex Abra el archivo lib/app_web/controllers/item_html/index.html.eex y desplácese hacia la parte inferior.
Luego ( sin eliminar el código que ya está allí ) Pegue el código HTML desde el que obtuvimos ToDomVC.
EG:
/lib/app_web/controllers/item_html/index.html.eex#L27-L73
Si intenta ejecutar la aplicación ahora y visite http: // localhost: 4000/elementos/
Verá esto ( sin el CSS de ToDomVC ):

Obviamente, eso no es lo que queremos, ¡así que obtengamos el CSS de ToDomvc y guárdelo en nuestro proyecto!
/assets/css Visite http://todomvc.com/examples/vanillajs/node_modules/todomvc-app-css/index.csss
y guarde el archivo en /assets/css/todomvc-app.css .
por ejemplo: /assets/css/todomvc-app.css
todomvc-app.css en app.scss Abra el archivo de assets/css/app.scss y reemplácelo con lo siguiente:
/* This file is for your main application css. */
/* @import "./phoenix.css"; */
@import "./todomvc-app.css" ; por ejemplo: /assets/css/app.scss#L4
Abra su archivo lib/app_web/components/layouts/app.html.heex y reemplace el contenido con el siguiente 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
Después:lib/app_web/components/layouts/app.html.heex
<%= @inner_content %> es donde se presentará la aplicación TODO.
NOTA : La etiqueta
<script>se incluye fuera de la convención. Sin embargo, no escribiremosJavaScripten este tutorial. Lograremos una paridad del 100% con TODDOMVC, sin escribir una línea deJS. No "odiamos"JS, de hecho, tenemos un tutorial "hermana" que construye la misma aplicación enJS: DWYL/JavaScript-Todo-List-Tutorial, ¡solo queremos recordarle que no necesita ningunaJSpara crear una aplicación web completamente funcional con un gran UX!
Con la plantilla de diseño guardada, el archivo CSS ToDomVC guardado en /assets/css/todomvc-app.css y el todomvc-app.css importado en app.scss , su página /items ahora debería verse así:

Entonces, nuestra lista de TODO está empezando a verse a ToDomVC, pero sigue siendo solo una lista ficticia.
Para obtener datos item en la plantilla ToDomVC, necesitaremos agregar algunas funciones. Cuando creamos el proyecto y generamos el modelo item , se creó un controlador (ubicado en lib/app_web/controllers/item_controller.ex ) y también un componente/vista (ubicado en lib/app_web/controllers/item_html.ex ). Este componente/vista es lo que controla efectivamente la representación del contenido dentro del directorio lib/app_web/controllers/item_html que jugamos con Prior.
Sabemos que necesitamos hacer cambios en la interfaz de usuario, por lo que vamos a agregar algunas funciones en este componente (que es similar a la parte de vista del paradigma MVC).
Esta es nuestra primera oportunidad de hacer un poco de desarrollo impulsado por las pruebas (TDD).
Cree un nuevo archivo con la ruta test/app_web/controllers/item_html_test.exs .
Escriba el siguiente código en el archivo:
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 ejemplo, /test/app_web/controllers/item_html_test.exs
Si intenta ejecutar este archivo de prueba:
mix test test/app_web/controllers/item_html_test.exsVerá el siguiente error (¡porque la función aún no existe!):
** (UndefinedFunctionError) function AppWeb.ItemHTML.checked/1 is undefined or private
Abra el archivo lib/app_web/controllers/item_html.ex y escriba las funciones para que pasen las pruebas.
Así es como implementamos las funciones. Su archivo item_html.ex ahora debería verse como el siguiente.
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
endVuelva a ejecutar las pruebas y ahora deberían pasar:
mix test test/app_web/controllers/item_html_test.exsDeberías ver:
....
Finished in 0.1 seconds
4 tests, 0 failuresAhora que hemos creado estas dos funciones de vista, y nuestras pruebas están pasando, ¡ usémoslas en nuestra plantilla!
Abra el archivo lib/app_web/controllers/item_html/index.html.eex y localice la línea:
< ul class =" todo-list " > Reemplace el contenido del <ul> con lo siguiente:
< %= 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 ejemplo: lib/app_web/controllers/item_html/index.html.heex#L43-L53
Con esos dos archivos guardados, si ejecuta la aplicación ahora: mix phx.server y visite http: // localhost: 4000/elementos.
Verá los items reales que creó en el paso 2.2 arriba:

Ahora que tenemos nuestros elementos que representan en el diseño de ToDomVC, trabajemos en la creación de nuevos elementos en el estilo de "aplicación de página única".
En la actualidad, nuestro formulario de "artículo nuevo" está disponible en: http: // localhost: 4000/elementos/nuevo ( como se indica en el paso 2 arriba )
Queremos que la persona pueda crear un nuevo elemento sin tener que navegar a una página diferente. Para lograr ese objetivo, incluiremos la plantilla lib/app_web/controllers/item_html/new.html.heex ( parcial ) dentro de la plantilla lib/app_web/controllers/item_html/index.html.heex . p.ej:
Antes de que podamos hacer eso, necesitamos ordenar la plantilla new.html.heex para eliminar los campos que no necesitamos .
Abra lib/app_web/controllers/item_html/new.html.heex y simplifíquelo solo al campo esencial :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
Después:/lib/app_web/controllers/item_html/new.html.heex
Necesitamos cambiar adicionalmente el estilo de la etiqueta <.input> . Con Phoenix, dentro del archivo lib/app_web/components/core_components.ex , los estilos se definen para componentes previamente construidos (que es el caso con <.input> ).
Para cambiar esto para que use el mismo estilo que ToDomVC, ubique la siguiente línea.
def input ( assigns ) do
Cambie el atributo de clase con la clase new-todo . Esta función debería parecerse a la siguiente.
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 También necesitamos cambiar los estilos actions dentro de simple_form . En el mismo archivo, busque def simple_form(assigns) do y cámbielo para que parezca así:
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 Si ejecuta la aplicación Phoenix ahora y visite http: // localhost: 4000/elementos/nuevo verá el campo de entrada :text único y no es un botón "Guardar":

No se preocupe, aún puede enviar el formulario con la tecla Ingres (retorno). Sin embargo, si intenta enviar el formulario ahora, no funcionará porque eliminamos dos de los campos requeridos por los changeset . Arreglemos eso.
items para establecer valores default Dado que hemos eliminado dos de los campos ( :person_id y :status ) del new.html.eex , debemos asegurarnos de que haya valores predeterminados para estos en el esquema. Abra el archivo lib/app/todo/item.ex y reemplace el contenido con lo siguiente:
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 Aquí estamos actualizando el schema "Elementos" para establecer un valor predeterminado de 0 para person_id y status . Y en el changeset/2 estamos eliminando el requisito de person_id y status . De esa manera, nuestro nuevo formulario item se puede enviar solo con el campo text .
Eg: /lib/app/todo/item.ex#L6-L7
Ahora que tenemos valores default para person_id y status si envía el formulario /items/new , tendrá éxito.
index/2 en ItemController Para entrar en línea en el formulario de elemento nuevo ( new.html.eex ) en la plantilla index.html.eex , necesitamos actualizar el AppWeb.ItemController.index/2 para incluir un conjunto de cambios.
Abra el archivo lib/app_web/controllers/item_controller.ex y actualice la función index/2 a lo siguiente:
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
After: /lib/app_web/controllers/item_controller.ex#L9-L10
No verá ningún cambio en la interfaz de usuario o pruebas después de este paso. Simplemente pase a 5.3 donde ocurre el momento "aha".
new.html.eex Inside index.html.eex Ahora que hemos realizado todo el trabajo de preparación, el siguiente paso es representar la plantilla new.html.eex ( parcial ) index.html.eex .
Abra el archivo lib/app_web/controllers/item_html/index.html.heex y localice la línea:
< input class =" new-todo " placeholder =" What needs to be done? " autofocus ="" >Reemplácelo con esto:
< % = new ( Map . put ( assigns , :action , ~p " /items/new " ) ) % > Analicemos lo que acabamos de hacer. Estamos incrustando el new.html.heex parcial dentro del archivo index.html.heex . Estamos haciendo esto llamando a la función new/2 dentro de item_controller.ex . Esta función se refiere a la página en los items/new y renderiza el archivo new.html.heex . Por lo tanto, ¿por qué llamamos a esta función para incrustarse con éxito?
Antes: /lib/app_web/controllers/item_html/index.html.heex#L36
After: /lib/app_web/controllers/item_html/index.html.heex#L36
Si ejecuta la aplicación ahora y visita: http: // localhost: 4000/elementos
Puede crear un elemento escribiendo su texto y enviarlo con la tecla Intro (retorno).

La redireccionamiento a la plantilla "Show" está "OK", pero podemos hacer una mejor UX redirigiendo a la plantilla index.html . Afortunadamente, esto es tan fácil como actualizar una sola línea en el código.
redirect en create/2 Abra el archivo lib/app_web/controllers/item_controller.ex y localice la función create . Específicamente la línea:
|> redirect ( to: ~p " /items/ #{ item } " )Actualizar la línea a:
|> redirect ( to: ~p " /items/ " ) Antes: /lib/app_web/controllers/item_controller.ex#L22
After: /lib/app_web/controllers/item_controller.ex#L23
Ahora, cuando creamos un nuevo item somos redirigidos a la plantilla index.html :

item_controller_test.exs para redirigir al index Los cambios que hemos realizado a los archivos new.html.heex y los pasos anteriores han roto algunas de nuestras pruebas automatizadas. Deberíamos arreglar eso.
Ejecute las pruebas:
mix testVerá la siguiente salida:
Finished in 0.08 seconds (0.03s async, 0.05s sync)
23 tests, 3 failures
Abra el archivo test/app_web/controllers/item_controller_test.exs y localice describe "new item" y describe "create item" . Cambia estos dos a lo siguiente.
Reemplace la prueba:
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 actualizado:
/test/app_web/controllers/item_controller_test.exs#L34-L55
Si vuelve a ejecutar las pruebas, mix test ahora todo volverá a pasar.
......................
Finished in 0.2 seconds (0.09s async, 0.1s sync)
22 tests, 0 failuresHasta ahora, la funcionalidad principal de la interfaz de usuario de ToDomVC está funcionando, podemos crear nuevos elementos y aparecen en nuestra lista. En este paso, vamos a mejorar la interfaz de usuario para incluir el recuento de elementos restantes en la esquina inferior izquierda.
Abra el archivo test/app_web/controllers/item_html_test.exs y cree las siguientes dos pruebas:
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 ejemplo: test/app_web/controllers/item_html_test.exs#L14-L26
Estas pruebas fallarán porque la función ItemHTML.remaining_items/1 no existe.
Haga que las pruebas pasen agregando el siguiente código al archivo 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 por ejemplo, /lib/app_web/controllers/item_html#L15-L17
Ahora que las pruebas están pasando, use el remaining_items/1 en la plantilla index.html . Abra el archivo lib/app_web/controllers/item_html/index.html.eex y localice la línea de código:
< span class =" todo-count " > < strong > 1 </ strong > item left </ span >Reemplácelo con esta línea:
< span class =" todo-count " > < %= remaining_items(@items) % > items left </ span > Esto solo invoca la función ItemHTML.remaining_items/1 con la lista de @items que devolverá el recuento de enteros de los elementos restantes que aún no se han "hecho".
Eg: /lib/app_web/controllers/item_html/index.html.eex#L60
¡En este punto, los elementos (restantes) contrarrestan en la parte inferior izquierda de la interfaz de usuario de ToDomvc está funcionando !
Agregue un new elemento a su lista y vea el aumento del recuento:

¡Eso fue bastante fácil, intentemos algo un poco más avanzado!
Tómese un descanso y tome un vaso de agua fresco, ¡la siguiente sección será intensa !
status de un elemento TODO a 1 Una de las funciones centrales de una lista de TODO es alternar el status de un item de 0 a 1 ("completo").
En nuestro esquema, un item completo tiene el status de 1 .
Vamos a necesitar dos funciones en nuestro controlador:
toggle_status/1 alterna el estado de un elemento, por ejemplo, 0 a 1 y 1 a 0.toggle/2 La función del controlador para las solicitudes HTTP para alternar el estado de un elemento. Abra el archivo test/app_web/controllers/item_controller_test.exs . Vamos a hacer algunos cambios aquí para que podamos agregar pruebas a las funciones que mencionamos anteriormente. Vamos a importar App.Todo dentro de item_controller_test.exs y arreglar las constantes Crear y atribuir para crear elementos simulados. Asegúrese de que el comienzo del archivo se vea así.
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 agregando atributos Item fijo para que se utilicen más tarde en las pruebas. Estamos especificando Item public porque luego agregaremos autenticación a esta aplicación.
Después de esto, ubique la función defp create_item()/1 dentro del mismo archivo. Cámbielo para que se vea así.
defp create_item ( _ ) do
item = item_fixture ( @ create_attrs )
% { item: item }
end Vamos a usar esta función para crear objetos Item para usar en las pruebas que vamos a agregar. Hablando de eso, ¡hagamos eso! Agregue el siguiente fragmento al archivo.
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 Eg: /test/app_web/controllers/item_controller_test.exs#L64-L82
Abra el archivo lib/app_web/controllers/item_controller.ex y agregue las siguientes funciones:
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 Eg: /lib/app_web/controllers/item_controller.ex#L64-L76
Las pruebas aún fallarán en este punto porque la ruta que estamos invocando en nuestra prueba aún no existe. ¡Arreglemos eso!
get /items/toggle/:id que invoca toggle/2 Abra lib/app_web/router.ex y localice los resources "/items", ItemController . Agregue una nueva línea:
get "/items/toggle/:id" , ItemController , :toggle por ejemplo: /lib/app_web/router.ex#L21
Ahora nuestras pruebas finalmente pasarán:
mix testDeberías ver:
22:39:42.231 [info] Already up
...........................
Finished in 0.5 seconds
27 tests, 0 failurestoggle/2 cuando se hace clic en una casilla de verificación en index.html Ahora que nuestras pruebas están pasando, es hora de usar toda esta funcionalidad que hemos estado construyendo en la interfaz de usuario. Abra el archivo /lib/app_web/controllers/item_html/index.html.heex y localice la línea:
< %= if item.status == 1 do % >
...
< % else % >
...
< % end % >Reemplácelo con lo siguiente:
< %= 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 % > Cuando se hace clic en este enlace, se invoca el punto final get /items/toggle/:id :
Eso a su vez desencadena el controlador toggle/2 que definimos anteriormente.
Antes:
/lib/app_web/controllers/item_html/index.html.heex#L40
Después:/lib/app_web/controllers/item_html/index.html.heex#L47-L57
.checked app.scss Desafortunadamente, las etiquetas <a> (que se generan con <.link> ) no pueden tener un :checked , por lo que los estilos predeterminados de ToDOMVC que funcionaron en la etiqueta <input> no funcionarán para el enlace. Por lo tanto, necesitamos agregar un par de líneas de CSS a nuestra app.scss .
Abra el archivo de assets/css/app.scss y agregue las siguientes líneas:
. 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;
} Después de guardar el archivo, debe tener: /assets/css/app.scss#L8
Y cuando ve la aplicación, la funcionalidad de alternativa funciona como se esperaba:

Nota de implementación : No estamos utilizando deliberadamente un JavaScript en este tutorial porque estamos demostrando cómo hacer una aplicación renderizada al 100% del lado del servidor. Esto siempre funciona incluso cuando JS está deshabilitado en el navegador o el dispositivo es súper antiguo y no tiene un navegador web moderno. Podríamos haber agregado fácilmente un atributo onclick a la etiqueta <input> , por ejemplo:
< input < %= checked(item) % > type="checkbox" class="toggle"
onclick="location.href='
< %= Routes.item_path(@conn, :toggle, item.id) % > ';" > Pero onclick es JavaScript y no necesitamos recurrir a JS .
El <a> (enlace) es un enfoque no JS perfectamente semántico para alternar item.status .
todo Si "completa" o revertir la operación, el orden de las TODOS podría diferir entre estas operaciones. Para mantener esto consistente, obtengamos todos los artículos todo en el mismo orden.
Inside lib/app/todo.ex , cambie list_items/0 a lo siguiente.
def list_items do
query =
from (
i in Item ,
select: i ,
order_by: [ asc: i . id ]
)
Repo . all ( query )
end Al buscar los artículos todo y ordenarlos, ¡garantizamos que el UX se mantiene consistente!
La pieza final de funcionalidad que necesitamos agregar a nuestra interfaz de usuario es la capacidad de editar el texto de un elemento.
Al final de este paso, tendrá una edición en línea funcionando:

La razón para requerir dos clics para editar un elemento es para que las personas no editen accidentalmente un elemento mientras se desplazan. Por lo tanto, tienen que hacer clic/tocar deliberadamente dos veces para editar.
En la especificación de ToDomVC, esto se logra creando un oyente de eventos para el evento de doble clic y reemplazando el elemento <label> con un <input> . Estamos tratando de evitar usar JavaScript en nuestra aplicación Phoenix renderizada del lado del servidor ( por ahora ), por lo que queremos usar un enfoque alternativo. Afortunadamente, podemos simular el evento de doble clic usando solo HTML y CSS . Ver: https://css-tricks.com/double-click-in-css ( recomendamos leer esa publicación y la demostración para comprender completamente cómo funciona este CSS !)
Nota : La implementación de CSS no es un verdadero doble clic, una descripción más precisa sería "dos clics" porque los dos clics pueden ocurrir con un retraso arbitrario. es decir, el primer clic, seguido de 10 segundos, espera y segundo clic tendrá el mismo efecto que dos clics en rápida sucesión. Si desea implementar hacer doble clic, consulte: github.com/dwyl/javascript-todo-list-tutorial#52-1uble-click
¡Vamos a seguir con eso! Abra el archivo lib/app_web/controllers/item_html/index.html.heex y localice la línea:
< % = new ( Map . put ( assigns , :action , ~p " /items/new " ) ) % >Reemplazarlo con:
< % = 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 % > Aquí, estamos revisando si estamos editando un elemento y presentando un enlace en lugar del formulario. Hacemos esto para evitar tener múltiples formularios en la página. Si no estamos editando un artículo, renderize el new.html.heex como antes. Con esto, si el usuario está editando un elemento, puede "salir del modo de edición" haciendo clic en el enlace que se representa.
por ejemplo: lib/app_web/controllers/item_html/index.html.heex#L30-L38
A continuación, todavía en el archivo index.html.eex , localice la línea:
< %= for item < - @items do % > Reemplace la etiqueta completa <li> con el siguiente código.
< 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 ejemplo: lib/app_web/controllers/item_html/index.html.heex#L46-L79
Hemos hecho algunas cosas aquí. Cambiamos el botón de alternancia fuera de la etiqueta <div class="view> . Además, hemos cambiado el texto con un Bloque de declaraciones de bloque if else .
Si el usuario no está editando, se representa un enlace ( <a> ) que, cuando se hace clic, permite al usuario ingresar el modo "Editar". Por otro lado, si el usuario está editando , representa el archivo edit.html.heex .
Hablando de eso, editemos edit.html.heex para que haga lo que queremos: un campo de texto que, una vez que se Enter , edita el elemento de retención de referencia.
< .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 editar Para habilitar el efecto de doble clic CSS para ingresar al modo edit , necesitamos agregar el siguiente CSS a nuestro archivo de 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 ejemplo: assets/css/app.css#L13-L32
Además, dado que nuestro marcado es ligeramente diferente al marcado de ToDomVC, necesitamos agregar un poco más de CSS para mantener la UI 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 */
} Así es como debería verse su archivo app.scss al final de este paso: assets/css/app.css#L34-L71
ItemController.edit/2 Para habilitar la edición en línea, necesitamos modificar la función edit/2 . Abra el archivo lib/app_web/controllers/item_controller.ex y reemplace la función edit/2 con lo siguiente:
def edit ( conn , params ) do
index ( conn , params )
end Además, dado que estamos pidiendo a nuestra función index/2 que maneje la edición, necesitamos actualizar 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, necesitamos manejar el envío del formulario para actualizar un elemento (que se representa en edit.html.heex ). Cuando presionamos Enter , el controlador update/2 se llama Inside lib/app_web/controllers/item_controller.ex . Queremos permanecer en la misma página después de actualizar el artículo.
Entonces, cámbielo para que se vea así.
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 Su archivo item_controller.ex ahora debería verse así: lib/app_web/controllers/item_controller.ex
ItemControllerTestEn nuestra búsqueda para construir una aplicación de una sola página, ¡rompimos algunas pruebas! Eso está bien. Son fáciles de arreglar.
Abra el archivo test/app_web/controllers/item_controller_test.exs y localice la prueba con el siguiente texto.
test "renders form for editing chosen item"
Y cámbielo para que parezca lo siguiente.
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 Cuando ingresamos el "Modo de edición del temporizador", creamos <a> un enlace para volver a /items , como hemos implementado previamente. Esta etiqueta tiene el texto "Haga clic aquí para crear un nuevo elemento", que es lo que estamos afirmando.
por ejemplo: test/app_web/controllers/item_controller_test.exs#L37-L39
A continuación, localice la prueba con la siguiente descripción:
describe "update item"Actualice el bloque al siguiente 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 ejemplo: test/app_web/controllers/item_controller_test.exs#L67-L80
Hemos actualizado las rutas a las que se redirige la aplicación después de actualizar un elemento. Dado que estamos construyendo una aplicación de una sola página, esa ruta se refiere a la ruta /items/ URL.
Si ejecuta las pruebas ahora, deberían pasar nuevamente:
mix test
23:08:01.785 [info] Already up
...........................
Finished in 0.5 seconds
27 tests, 0 failures
Randomized with seed 956565
index.html Ahora que tenemos funciones toggle y edit , finalmente podemos eliminar el diseño predeterminado de Phoenix (tabla) de la plantilla lib/app_web/controllers/item_html/index.html.heex .

Abra el archivo lib/app_web/controllers/item_html/index.html.eex y elimine todo el código antes de la línea:
< section class =" todoapp " > por ejemplo: lib/app_web/controllers/item_html/index.html.heex
Tu aplicación ahora debería verse así: 
Desafortunadamente, al eliminar el diseño predeterminado, hemos "roto" las pruebas.
Abra el archivo test/app_web/controllers/item_controller_test.exs y localice la prueba que tiene la siguiente descripción:
test "lists all items"Actualizar la afirmación de:
assert html_response ( conn , 200 ) =~ "Listing Items"A:
assert html_response ( conn , 200 ) =~ "todos" por ejemplo: test/app_web/controllers/item_controller_test.exs#L14
Ahora que la funcionalidad Core (Crear, Editar/Actualizar, Eliminar) está funcionando, podemos agregar las mejoras finales de la interfaz de usuario. En este paso, vamos a agregar la navegación/filtrado del pie de página.

La vista "All" es el valor predeterminado. El "activo" es todos los elementos con status==0 . "Completado" son todos los elementos con status==1 .
/:filterAntes de comenzar, agregemos una prueba unitaria. Queremos mostrar elementos filtrados de acuerdo con el filtro elegido.
Abra test/app_web/controllers/item_controller_test.exs y localice describe "index" do . En este bloque, agregue la siguiente prueba. Verifica si el elemento se muestra correctamente cuando se cambia el filtro.
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 ejemplo: test/app_web/controllers/item_controller_test.exs#L21-L32
Abra lib/app_web/router.ex y agregue la siguiente ruta:
get "/items/filter/:filter" , ItemController , :index Eg: /lib/app_web/router.ex#L23
index/2 para enviar filter a la vista/plantilla Abra el archivo lib/app_web/controllers/item_controller.ex y localice la función index/2 . Reemplace la invocación de render/3 al final del index/2 con lo siguiente:
render ( conn , "index.html" ,
items: items ,
changeset: changeset ,
editing: item ,
filter: Map . get ( params , "filter" , "all" )
) por ejemplo: lib/app_web/controllers/item_controller.ex#L17-L22
Map.get(params, "filter", "all") establece el valor predeterminado de nuestro filter en "todos", así que cuando index.html se representa, muestre "todos los elementos".
filter/2 Ver Para filtrar los elementos por su estado, necesitamos crear una nueva función.
Abra el archivo lib/app_web/controllers/item_html.ex y cree la función filter/2 de la siguiente manera:
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 ejemplo: lib/app_web/controllers/item_html.ex#L19-L26
Esto nos permitirá filtrar los elementos en el siguiente paso.
index.html Use la función filter/2 para filtrar los elementos que se muestran. Abra el archivo lib/app_web/controllers/item_html/index.html.heex y localice la línea de bucle for :
< % = for item <- @ items do % >Reemplazarlo con:
< % = for item <- filter ( @ items , @ filter ) do % > por ejemplo: lib/app_web/controllers/item_html/index.html.heex#L18
Esto invoca la función filter/2 que definimos en el paso anterior que pasa en la lista de @items y el seleccionado @filter .
A continuación, ubique el <footer> y reemplace el contenido de <ul class="filters"> con el siguiente 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 agregando condicionalmente la clase selected de acuerdo con el valor de asignación de @filter .
EG: /lib/app_web/controllers/item_html/index.html.heex#L62-L98
Al final de este paso, tendrá un filtro de pie de página que funciona completamente:

Podemos cubrir rápidamente esta función que agregamos con una pequeña prueba unitaria. Abra test/app_web/controllers/item_html_test.exs y agregue lo siguiente.
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¿Y deberías terminar con esta función? ¡Increíble trabajo!
Casi hemos terminado con nuestra implementación de Phoenix de ToDomVC. Lo último que debe implementar es "claro completado".
Abra su archivo lib/app_web/router.ex y agregue la siguiente ruta:
get "/items/clear" , ItemController , :clear_completed Tu scope "/" ahora debería verse como lo siguiente:
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 En el archivo lib/app_web/controllers/item_controller.ex agregue el siguiente 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 ejemplo: lib/app_web/controllers/item_controller.ex#L87-L93
Esto utiliza la práctica función update_all/3 para actualizar todos los elementos que coinciden con la query . En nuestro caso, buscamos todos items que pertenecen a person_id==0 y tengan status==1 .
No estamos eliminando los elementos, sino que estamos actualizando su estado a 2 , lo que para los propósitos de nuestro ejemplo significa que están "archivados".
Nota : Esta es una guía útil para
update_all: https://adamdelong.com/bulk-update-ecto
Finalmente, en lib/app_web/controllers/item_html/index.html.eex desplácese a la parte inferior del archivo y reemplace la línea:
< button class = "clear-completed" style = "display: block;" >
Clear completed
< / button >Con:
< a class = "clear-completed" href = "/items/clear" >
Clear completed
[ < % = Enum . count ( filter ( @ items , "completed" ) ) % > ]
< / a > por ejemplo: lib/app_web/controllers/item_html/index.html.heex#L104-L107
Lo último que debemos hacer es actualizar la función filter/2 dentro de lib/app_web/controllers/item_html.ex . Dado que status = 2 ahora se refiere a un estado archivado , queremos devolver cualquier cosa que no esté archivada.
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?
¡Y eso es todo! 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 ? ¡Estás de suerte! 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! ☀️