Un tutoriel étape par étape complet pour la construction d'une liste de TODO à Phoenix.
100% fonctionnel. 0% javascript. Juste HTML , CSS et Elixir . Rapide et maintenable.
Les listes de TODO sont familières à la plupart des gens; Nous faisons des listes tout le temps. La construction d'une liste de togne à partir de zéro est un excellent moyen d'apprendre Elixir / Phoenix car l' interface utilisateur / UX est simple , nous pouvons donc nous concentrer sur la mise en œuvre.
Pour l'équipe @dwyl cette application / tutoriel est une vitrine de la façon dont le rendu côté serveur ( avec l'amélioration progressive du côté client ) peut fournir un excellent équilibre entre l'efficacité du développeur ( fonctionnalités d'expédition rapide ), UX et l'accessibilité . Les pages rendues du serveur prennent moins de 5 ms pour répondre, donc l'UX est rapide . Sur fly.io: phxtodo.fly.dev Les temps de réponse aller-retour sont inférieurs à 200 ms pour toutes les interactions, donc cela ressemble à une application rendue côté client.
Un tutoriel de la liste TODO qui montre un débutant complet comment créer une application dans Elixir / Phoenix à partir de zéro.
Essayez la version Fly.io. Ajoutez quelques éléments à la liste et testez les fonctionnalités.

Même avec un aller-retour HTTP complet pour chaque interaction, le temps de réponse est rapide . Faites attention à la façon dont Chrome | Firefox | Safari attend la réponse du serveur avant de rediriger la page. L'ancien rafraîchissement complet d'antan a disparu . Les navigateurs modernes ne rendent intelligemment que les changements! Donc, l'UX se rapproche de "natif"! Sérieusement, essayez l'application Fly.io sur votre téléphone et voyez!
Dans ce tutoriel, nous utilisons le TODOMVC CSS pour simplifier notre interface utilisateur. Cela présente plusieurs avantages le plus grand étant de minimiser la quantité de CSS que nous devons écrire! Cela signifie également que nous avons un guide à laquelle les fonctionnalités doivent être implémentées pour atteindre les fonctionnalités complètes.
Remarque : Nous aimons
CSSpour sa puissance / flexibilité incroyable, mais nous savons que tout le monde ne l'aime pas. Voir: Apprendre les tachyons # Pourquoi la dernière chose que nous voulons, c'est perdre des tonnes de temps avecCSSdans un tutorielPhoenix!
Ce tutoriel s'adresse à tous ceux qui apprennent à Elixir / Phoenix. Aucune expérience antérieure avec Phoenix n'est supposée / attendue. Nous avons inclus toutes les étapes nécessaires pour créer l'application.
Si vous êtes coincé sur une étape, veuillez ouvrir un problème sur GitHub où nous sommes heureux de vous aider à vous décoller! Si vous pensez que n'importe quelle ligne de code peut utiliser un peu plus d'explication / clarté, n'hésitez pas à nous informer ! Nous savons ce que c'est que d'être un débutant, cela peut être frustrant quand quelque chose n'a pas de sens! Poser des questions sur Github aide tout le monde à apprendre!
Veuillez nous donner des commentaires! Étaler le référentiel si vous l'avez trouvé utile.
Avant d'essayer de construire la liste TODO, assurez-vous d'avoir tout ce dont vous avez besoin sur votre ordinateur. Voir: Prérequis
Une fois que vous avez confirmé que Phoenix et PostgreSQL ont été installés, essayez d'exécuter l'application finie .
localhost Avant de commencer à construire votre propre version de l'application ToDo List, exécutez la version finie de votre localhost pour confirmer qu'elle fonctionne.
Clone le projet de GitHub:
git clone [email protected]:dwyl/phoenix-todo-list-tutorial.git && cd phoenix-todo-list-tutorialInstallez les dépendances et configurez la base de données:
mix setupDémarrez le serveur Phoenix:
mix phx.server Visitez localhost:4000 dans votre navigateur Web.
Vous devriez voir:

Maintenant que vous avez l'application d'exemple fini en cours d'exécution sur votre localhost ,
Construisons-le à partir de zéro et comprenons toutes les étapes.
Lorsque vous exécutez l'application d'exemple fini sur localhost , si vous souhaitez essayer le bouton login , vous devrez obtenir un AUTH_API_KEY . [1 minute] Voir: Obtenez votre AUTH_API_KEY
Si vous avez dirigé l'application finie sur votre localhost ( et vous devriez vraiment! ),
Vous devrez modifier un répertoire avant de commencer le tutoriel:
cd ..
Maintenant, vous êtes prêt à construire !
Dans votre terminal, créez une nouvelle application Phoenix à l'aide de la commande mix suivante:
mix phx.new app --no-dashboard --no-gettext --no-mailer Lorsque vous êtes invité à installer des dépendances, le type Y est suivi de Entrée .
Remarque : ces drapeaux après le nom de
appsont juste pour éviter de créer des fichiers dont nous n'avons pas besoin pour cet exemple simple. Voir: hexdocs.pm/phoenix/mix.tasks.phx.new
Changez dans le répertoire app nouvellement créé ( cd app ) et assurez-vous d'avoir tout ce dont vous avez besoin:
mix setupDémarrez le serveur Phoenix:
mix phx.server Vous pouvez maintenant visiter localhost:4000 dans votre navigateur Web. Vous devriez voir quelque chose de similaire à:

Arrêtez le serveur Phoenix Ctrl + c .
Exécutez les tests pour s'assurer que tout fonctionne comme prévu:
mix testVous devriez voir:
Compiling 16 files (.ex)
Generated app app
17:49:40.111 [info] Already up
...
Finished in 0.04 seconds
3 tests, 0 failuresAprès avoir établi que l'application Phoenix fonctionne comme prévu, passons à la création de certains fichiers!
items En créant une liste de TODO de base, nous n'avons besoin que d'un seul schéma: items . Plus tard, nous pouvons ajouter des listes et des balises distinctes pour organiser / catégoriser nos items mais pour l'instant, c'est tout ce dont nous avons besoin.
Exécutez la commande du générateur suivant pour créer la table des éléments:
mix phx.gen.html Todo Item items text:string person_id:integer status:integer À strictement parler, nous n'avons besoin que des champs text et status , mais comme nous savons que nous voulons associer des éléments à des personnes (_later dans le tutoriel), nous ajoutons le champ maintenant .
Vous verrez la sortie suivante:
* 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
Cela a créé un tas de fichiers! Dont certains dont nous n'avons pas strictement besoin .
Nous pourrions créer manuellement uniquement les fichiers dont nous avons besoin , mais c'est la façon "officielle" de créer une application crud à Phoenix, donc nous l'utilisons pour la vitesse.
Remarque : les contextes phoenix désignés dans cet exemple comme
Todosont des « modules dédiés qui exposent et regroupent les fonctionnalités liées ». Nous pensons qu'ils compliquent inutilement les applications Phoenix de base avec des couches d '"interface" et nous souhaitons vraiment que nous puissions les éviter. Mais étant donné qu'ils sont cuits dans les générateurs, et le créateur du framework les aime , nous avons le choix: soit montez à bord avec des contextes ou créez manuellement tous les fichiers de nos projets Phoenix. Les générateurs sont un moyen beaucoup plus rapide de construire! Embrassez- les, même si vous finissez par devoirdeletequelques fichiers inutilisés en cours de route!
Nous n'allons pas expliquer chacun de ces fichiers à ce stade du tutoriel, car il est plus facile de comprendre les fichiers pendant que vous créez l'application! Le but de chaque fichier deviendra clair au fur et à mesure que vous progressez en les modifiant.
/items à router.ex Suivez les instructions notées par le générateur pour ajouter les resources "/items", ItemController au router.ex .
Ouvrez le fichier lib/app_web/router.ex et localisez la ligne: scope "/", AppWeb do . Ajoutez la ligne à la fin du bloc. Par exemple:
scope "/" , AppWeb do
pipe_through :browser
get "/" , PageController , :index
resources "/items" , ItemController # this is the new line
end Votre fichier router.ex devrait ressembler à ceci: router.ex#L20
Maintenant, comme l'a suggéré le terminal, exécutez mix ecto.migrate . Cela terminera la configuration des tables de base de données et exécutera les migrations nécessaires pour que tout fonctionne correctement!
À ce stade, nous avons déjà une liste de TODO fonctionnelle ( si nous étions prêts à utiliser l'interface utilisateur de Phoenix par défaut ).
Essayez d'exécuter l'application sur votre localhost : exécutez les migrations générées avec mix ecto.migrate puis le serveur avec:
mix phx.server
Visitez: http: // localhost: 4000 / items / nouveau et saisissez certaines données.

Cliquez sur le bouton "Enregistrer l'élément" et vous serez redirigé vers la page "Afficher": http: // localhost: 4000 / items / 1

Ce n'est pas une expérience utilisateur attrayante (UX), mais cela fonctionne ! Voici une liste d'éléments - une "liste de todo". Vous pouvez visiter ceci en cliquant sur le bouton Back to items ou en accédant à l'URL suivante http: // localhost: 4000 / items.

Amélilons l'UX en utilisant les HTML et CSS TODOMVC!
Pour recréer le TODOMVC UI / UX, emprunlons le code HTML directement à l'exemple.
Visitez: http://todomvc.com/examples/vanillajs Ajoutez quelques éléments à la liste. Ensuite, inspectez la source à l'aide des outils de développement de votre navigateur. Par exemple:

Cliquez avec le bouton droit sur la source que vous souhaitez (par exemple: <section class="todoapp"> ) et sélectionnez "Modifier en tant que HTML":

Une fois que le HTML de la <section> est modifiable, sélectionnez-le et copiez-le.

Le code HTML est:
< 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 > Converons ce HTML en un modèle d'élixir ( EEx ) intégré.
Remarque : La raison pour laquelle nous copions ce
HTMLà partir de l'inspecteur des éléments du navigateur au lieu de directement à partir de la source sur GitHub:examples/vanillajs/index.htmlest qu'il s'agit d'une "application de page unique", donc le<ul class="todo-list"></ul>ne fait que remplir dans le soliseur. Le copier à partir des outils de développement du navigateur est le moyen le plus simple d'obtenir leHTMLcomplet .
index.html.eex Ouvrez le fichier lib/app_web/controllers/item_html/index.html.eex et faites défiler vers le bas.
Ensuite ( sans supprimer le code qui est déjà là ) collez le code HTML à partir de TODOMVC.
par exemple:
/lib/app_web/controllers/item_html/index.html.eex#L27-L73
Si vous essayez d'exécuter l'application maintenant et visitez http: // localhost: 4000 / items /
Vous verrez cela ( sans le TODOMVC CSS ):

Ce n'est évidemment pas ce que nous voulons, alors obtenons le TODOMVC CSS et enregistrons-le dans notre projet!
/assets/css Visitez http://todomvc.com/examples/vanillyjs/node_modules/todomvc-app-css/index.css
et enregistrez le fichier sur /assets/css/todomvc-app.css .
par exemple: /assets/css/todomvc-app.css
todomvc-app.css dans app.scss Ouvrez le fichier assets/css/app.scss et remplacez-le par les suivants:
/* This file is for your main application css. */
/* @import "./phoenix.css"; */
@import "./todomvc-app.css" ; par exemple: /assets/css/app.scss#L4
Ouvrez votre fichier lib/app_web/components/layouts/app.html.heex et remplacez le contenu par le code suivant:
<!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 >Avant:
lib/app_web/components/layouts/app.html.eex
Après:lib/app_web/components/layouts/app.html.heex
<%= @inner_content %> est l'endroit où l'application TODO sera rendue.
Remarque : la balise
<script>est incluse hors convention. Cependant, nous n'écrirons aucunJavaScriptdans ce tutoriel. Nous atteindrons une parité à 100% avec TODOMVC, sans écrire une ligne deJS. Nous ne détessons pasJS, en fait, nous avons un tutoriel "sœur" qui construit la même application dansJS: Dwyl / Javascript-Todo-List-tutorial, nous voulons simplement vous rappeler que vous n'avez pas besoin deJSpour créer une application Web entièrement fonctionnelle avec Great UX!
Avec le modèle de mise en page enregistré, le fichier TODOMVC CSS enregistré sur /assets/css/todomvc-app.css et le todomvc-app.css importé dans app.scss , votre page /items devrait maintenant ressembler à ceci:

Donc, notre liste de TODO commence à ressembler à TODOMVC, mais ce n'est toujours qu'une liste factice.
Afin de rendre les données item dans le modèle ToDomvc, nous devrons ajouter quelques fonctions. Lorsque nous avons créé le projet et généré le modèle item , un contrôleur a été créé (situé dans lib/app_web/controllers/item_controller.ex ) et un composant / vue également (situé dans lib/app_web/controllers/item_html.ex ). Ce composant / vue est ce qui contrôle efficacement le rendu du contenu dans le répertoire lib/app_web/controllers/item_html que nous avons bricolé avec Prior.
Nous savons que nous avons besoin d'apporter des modifications à l'interface utilisateur, nous allons donc ajouter quelques fonctions dans ce composant (qui s'apparente à la partie de la vue du paradigme MVC).
C'est notre première chance de faire un peu de développement axé sur les tests (TDD).
Créez un nouveau fichier avec le Path test/app_web/controllers/item_html_test.exs .
Tapez le code suivant dans le fichier:
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 par exemple: /test/app_web/controllers/item_html_test.exs
Si vous essayez d'exécuter ce fichier de test:
mix test test/app_web/controllers/item_html_test.exsVous verrez l'erreur suivante (car la fonction n'existe pas encore!):
** (UndefinedFunctionError) function AppWeb.ItemHTML.checked/1 is undefined or private
Ouvrez le fichier lib/app_web/controllers/item_html.ex et écrivez les fonctions pour faire passer les tests.
C'est ainsi que nous avons implémenté les fonctions. Votre fichier item_html.ex devrait maintenant ressembler à ce qui suit.
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
endRe-tourne les tests et ils devraient maintenant passer:
mix test test/app_web/controllers/item_html_test.exsVous devriez voir:
....
Finished in 0.1 seconds
4 tests, 0 failuresMaintenant que nous avons créé ces deux fonctions de vue et que nos tests passent, utilisons -les dans notre modèle!
Ouvrez le fichier lib/app_web/controllers/item_html/index.html.eex et localisez la ligne:
< ul class =" todo-list " > Remplacez le contenu des <ul> par les éléments suivants:
< %= 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 % > par exemple: lib/app_web/controllers/item_html/index.html.heex#L43-L53
Avec ces deux fichiers enregistrés, si vous exécutez l'application maintenant: mix phx.server et visitez http: // localhost: 4000 / items.
Vous verrez les vrais items que vous avez créés à l'étape 2.2 ci-dessus:

Maintenant que nos articles ont rendu dans la disposition ToDomvc, travaillons sur la création de nouveaux éléments dans le style "application à page unique".
À l'heure actuelle, notre formulaire "nouvel article" est disponible sur: http: // localhost: 4000 / articles / nouveau ( comme indiqué à l'étape 2 ci-dessus )
Nous voulons que la personne puisse créer un nouvel élément sans avoir à naviguer vers une autre page. Afin d'atteindre cet objectif, nous inclurons le modèle lib/app_web/controllers/item_html/new.html.heex ( partiel ) à l'intérieur du modèle lib/app_web/controllers/item_html/index.html.heex . Par exemple:
Avant de pouvoir le faire, nous devons ranger le modèle new.html.heex pour supprimer les champs dont nous n'avons pas besoin .
Ouvrir lib/app_web/controllers/item_html/new.html.heex et le simplifions dans le champ essentiel :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 >
Avant:
/lib/app_web/controllers/item_html/new.html.heex
After:/lib/app_web/controllers/item_html/new.html.heex
Nous devons en outre modifier le style de la balise <.input> . Avec Phoenix, à l'intérieur du fichier lib/app_web/components/core_components.ex , les styles sont définis pour les composants prédéfinis (ce qui est le cas avec <.input> ).
Pour changer cela afin qu'il utilise le même style que TODOMVC, localisez la ligne suivante.
def input ( assigns ) do
Modifiez l'attribut de classe avec la classe new-todo . Cette fonction devrait ressembler à ce qui suit.
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 Nous devons également modifier les styles actions à l'intérieur du simple_form . Dans le même fichier, recherchez def simple_form(assigns) do and Changez-le pour que cela ressemble:
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 vous exécutez l'application Phoenix maintenant et visitez http: // localhost: 4000 / items / nouveau, vous verrez le champ de saisie unique :text et pas de bouton "Enregistrer":

Ne vous inquiétez pas, vous pouvez toujours soumettre le formulaire avec ENTER (return). Cependant, si vous essayez de soumettre le formulaire maintenant, cela ne fonctionnera pas parce que nous avons supprimé deux des champs requis par l' changeset ! Fixons cela.
items pour définir les valeurs default Étant donné que nous avons supprimé deux des champs ( :person_id et :status ) du new.html.eex , nous devons nous assurer qu'il existe des valeurs par défaut pour celles-ci dans le schéma. Ouvrez le fichier lib/app/todo/item.ex et remplacez le contenu par ce qui suit:
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 Ici, nous mettons à jour le schema "éléments" pour définir une valeur par défaut de 0 pour person_id et status . Et dans le changeset/2 nous supprimons l' exigence de person_id et status . De cette façon, notre nouveau formulaire item peut être soumis avec le champ text .
par exemple: /lib/app/todo/item.ex#L6-L7
Maintenant que nous avons des valeurs default pour person_id et status Si vous soumettez le /items/new formulaire, il réussira.
index/2 dans ItemController Afin de mettre en ligne le nouveau formulaire d'élément ( new.html.eex ) dans le modèle index.html.eex , nous devons mettre à jour l' AppWeb.ItemController.index/2 pour inclure un ensemble de modifications.
Ouvrez le fichier lib/app_web/controllers/item_controller.ex et mettez à jour la fonction index/2 à ce qui suit:
def index ( conn , _params ) do
items = Todo . list_items ( )
changeset = Todo . change_item ( % Item { } )
render ( conn , "index.html" , items: items , changeset: changeset )
end Avant: /lib/app_web/controllers/item_controller.ex
Après: /lib/app_web/controllers/item_controller.ex#L9-L10
Vous ne verrez aucun changement dans l'interface utilisateur ou les tests après cette étape. Passez à 5.3 où le moment "aha" se produit.
new.html.eex à l'intérieur index.html.eex Maintenant que nous avons effectué tous les travaux de préparation, l'étape suivante consiste à rendre le modèle new.html.eex ( partiel ) à l'intérieur index.html.eex .
Ouvrez le fichier lib/app_web/controllers/item_html/index.html.heex et localisez la ligne:
< input class =" new-todo " placeholder =" What needs to be done? " autofocus ="" >Remplacez-le par ceci:
< % = new ( Map . put ( assigns , :action , ~p " /items/new " ) ) % > Décomposons ce que nous venons de faire. Nous intégrons le partiel new.html.heex dans le fichier index.html.heex . Nous le faisons en appelant la fonction new/2 dans item_controller.ex . Cette fonction concerne la page dans les items/new et rend le fichier new.html.heex . Par conséquent, pourquoi nous appelons cette fonction à intégrer avec succès ?.
Avant: /lib/app_web/controllers/item_html/index.html.heex#L36
Après: /lib/app_web/controllers/item_html/index.html.heex#L36
Si vous exécutez l'application maintenant et visitez: http: // localhost: 4000 / articles
Vous pouvez créer un élément en tapant votre texte et le soumettre avec la touche Entrée (retour).

La redirection vers le modèle "Show" est "OK", mais nous pouvons faire mieux UX en redirigeant vers le modèle index.html . Heureusement, c'est aussi simple que de mettre à jour une seule ligne dans le code.
redirect dans create/2 Ouvrez le fichier lib/app_web/controllers/item_controller.ex et localisez la fonction create . Plus précisément la ligne:
|> redirect ( to: ~p " /items/ #{ item } " )Mettez à jour la ligne vers:
|> redirect ( to: ~p " /items/ " ) Avant: /lib/app_web/controllers/item_controller.ex#L22
Après: /lib/app_web/controllers/item_controller.ex#L23
Maintenant, lorsque nous créons un nouvel item nous sommes redirigés vers le modèle index.html :

item_controller_test.exs pour rediriger vers index Les modifications que nous avons apportées aux fichiers new.html.heex et les étapes ci-dessus ont cassé certains de nos tests automatisés. Nous devons résoudre ce problème.
Exécutez les tests:
mix testVous verrez la sortie suivante:
Finished in 0.08 seconds (0.03s async, 0.05s sync)
23 tests, 3 failures
Ouvrez le fichier test/app_web/controllers/item_controller_test.exs et localisez describe "new item" et describe "create item" . Changer ces deux à ce qui suit.
Remplacez le test:
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
endCode mis à jour:
/test/app_web/controllers/item_controller_test.exs#L34-L55
Si vous réécoutez le mix test tests, le sera maintenant tous passer à nouveau.
......................
Finished in 0.2 seconds (0.09s async, 0.1s sync)
22 tests, 0 failuresJusqu'à présent, la principale fonctionnalité de l'interface utilisateur TODOMVC fonctionne, nous pouvons créer de nouveaux éléments et ils apparaissent dans notre liste. Dans cette étape, nous allons améliorer l'interface utilisateur pour inclure le nombre d'éléments restants dans le coin inférieur gauche.
Ouvrez le fichier test/app_web/controllers/item_html_test.exs et créez les deux tests suivants:
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 par exemple: test/app_web/controllers/item_html_test.exs#L14-L26
Ces tests échoueront car la fonction ItemHTML.remaining_items/1 n'existe pas.
Faites passer les tests en ajoutant le code suivant au fichier 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 par exemple: /lib/app_web/controllers/item_html#L15-L17
Maintenant que les tests passent, utilisez le modèle remaining_items/1 dans le modèle index.html . Ouvrez le fichier lib/app_web/controllers/item_html/index.html.eex et localisez la ligne de code:
< span class =" todo-count " > < strong > 1 </ strong > item left </ span >Remplacez-le par cette ligne:
< span class =" todo-count " > < %= remaining_items(@items) % > items left </ span > Cela invoque simplement la fonction ItemHTML.remaining_items/1 avec la liste de @items qui renverra le nombre d'entités d'éléments restants qui n'ont pas encore été "faits".
Par exemple: /lib/app_web/controllers/item_html/index.html.eex#L60
À ce stade, le compteur (restant) des éléments en bas à gauche de l'interface utilisateur ToDomvc fonctionne !
Ajoutez un new élément à votre liste et regardez l'augmentation du nombre:

C'était assez facile essayons quelque chose d'un peu plus avancé!
Faites une pause et prenez-vous un verre d'eau frais, la section suivante va être intense !
status à 1 L'une des fonctions principales d'une liste de tâches est de basculer l' status d'un item de 0 à 1 ("complet").
Dans notre schéma, un item terminé a le status de 1 .
Nous allons avoir besoin de deux fonctions dans notre contrôleur:
toggle_status/1 bascule l'état d'un élément par exemple: 0 à 1 et 1 à 0.toggle/2 La fonction de gestionnaire pour les demandes HTTP pour basculer l'état d'un élément. Ouvrez le fichier test/app_web/controllers/item_controller_test.exs . Nous allons apporter des modifications ici afin que nous puissions ajouter des tests aux fonctions que nous avons mentionnées avant. Nous allons importer App.Todo dans item_controller_test.exs et corriger les constantes de création et d'attribut pour créer des éléments simulés. Assurez-vous que le début du fichier ressemble.
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 }
Nous ajoutons des attributs Item fixe pour être utilisés ultérieurement dans les tests. Nous spécifions Item public car nous ajouterons plus tard l'authentification à cette application.
Après cela, localisez defp create_item()/1 fonction dans le même fichier. Changez-le pour que ça ressemble.
defp create_item ( _ ) do
item = item_fixture ( @ create_attrs )
% { item: item }
end Nous allons utiliser cette fonction pour créer des objets Item à utiliser dans les tests que nous allons ajouter. En parlant de cela, faisons ça! Ajoutez l'extrait suivant dans le fichier.
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 par exemple: /test/app_web/controllers/item_controller_test.exs#L64-L82
Ouvrez le fichier lib/app_web/controllers/item_controller.ex et ajoutez-y les fonctions suivantes:
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 par exemple: /lib/app_web/controllers/item_controller.ex#L64-L76
Les tests échoueront toujours à ce stade car l'itinéraire que nous invoquons dans notre test n'existe pas encore. Fixons cela!
get /items/toggle/:id Route qui invoque toggle/2 Ouvrez le lib/app_web/router.ex et localisez les resources "/items", ItemController . Ajouter une nouvelle ligne:
get "/items/toggle/:id" , ItemController , :toggle par exemple: /lib/app_web/router.ex#L21
Maintenant, nos tests passeront enfin :
mix testVous devriez voir:
22:39:42.231 [info] Already up
...........................
Finished in 0.5 seconds
27 tests, 0 failurestoggle/2 lorsqu'une case à cocher est cliquée dans index.html Maintenant que nos tests passent, il est temps d'utiliser toutes ces fonctionnalités que nous avons construites dans l'interface utilisateur. Ouvrez /lib/app_web/controllers/item_html/index.html.heex et localisez la ligne:
< %= if item.status == 1 do % >
...
< % else % >
...
< % end % >Remplacez-le par ce qui suit:
< %= 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 % > Lorsque ce lien est cliqué sur le point de terminaison get /items/toggle/:id est invoqué,
Cela déclenche à son tour le maître toggle/2 que nous avons défini ci-dessus.
Avant:
/lib/app_web/controllers/item_html/index.html.heex#L40
After:/lib/app_web/controllers/item_html/index.html.heex#L47-L57
.checked app.scss Malheureusement, les balises <a> (qui sont générées avec <.link> ) ne peuvent pas avoir un sélecteur de pseudo :checked , donc les styles TODOMVC par défaut qui ont fonctionné sur la balise <input> ne fonctionneront pas pour le lien. Nous devons donc ajouter quelques lignes de CSS à nos app.scss .
Ouvrez le fichier assets/css/app.scss et ajoutez-y les lignes suivantes:
. 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;
} Après avoir enregistré le fichier, vous devriez avoir: /assets/css/app.scss#L8
Et lorsque vous visualisez l'application, la fonctionnalité de bascule fonctionne comme prévu:

Remarque d'implémentation : nous n'utilisons pas très délibérément un JavaScript dans ce tutoriel car nous montrons comment effectuer une application renvoyée à 100%. Cela fonctionne toujours même lorsque JS est désactivé dans le navigateur ou que l'appareil est super ancien et n'a pas de navigateur Web moderne. Nous aurions facilement pu ajouter un attribut onclick à la balise <input> , par exemple:
< input < %= checked(item) % > type="checkbox" class="toggle"
onclick="location.href='
< %= Routes.item_path(@conn, :toggle, item.id) % > ';" > Mais onclick est JavaScript et nous n'avons pas besoin de recourir à JS .
Le <a> (Link) est une approche non sémantique non sémantique de la mise en œuvre de item.status .
todo Si vous «complétez» ou réintégrez l'opération, l'ordre des Todos peut différer entre ces opérations. Pour garder cela cohérent, récupérons tous les éléments todo dans le même ordre.
À l'intérieur lib/app/todo.ex , modifiez list_items/0 à ce qui suit.
def list_items do
query =
from (
i in Item ,
select: i ,
order_by: [ asc: i . id ]
)
Repo . all ( query )
end En récupérant les articles todo et en les commandant, nous garantissons que l'UX reste cohérent!
La dernière fonctionnalité que nous devons ajouter à notre interface utilisateur est la possibilité de modifier le texte d'un élément.
À la fin de cette étape, vous aurez le travail d'édition en ligne:

La raison pour nécessiter deux clics pour modifier un élément, est que les gens ne modifient pas accidentellement un élément pendant le défilement. Ils doivent donc délibérément cliquer / taper deux fois pour modifier.
Dans la spécification TODOMVC, cela est réalisé en créant un écouteur d'événements pour l'événement à double clic et en remplaçant l'élément <label> par un <input> . Nous essayons d' éviter d'utiliser JavaScript dans notre application Phoenix de notre serveur ( pour l'instant ), nous voulons donc utiliser une approche alternative. Heureusement, nous pouvons simuler l'événement à double clic en utilisant Just HTML et CSS . Voir: https://css-tricks.com/double-click-in-css ( nous vous recommandons de lire ce post et la démo pour bien comprendre comment ce CSS fonctionne !)
Remarque : L'implémentation CSS n'est pas un vrai double-clic, une description plus précise serait "à deux clics" car les deux clics peuvent se produire avec un retard arbitraire. IE First Click suivi de 10 secondes d'attente et de deuxième clic aura le même effet que deux clics en succession rapide. Si vous souhaitez implémenter un vrai double clic, voir: github.com/dwyl/javascript-todo-list-tutorial#52-double-click
Allons-y! Ouvrez le fichier lib/app_web/controllers/item_html/index.html.heex et localisez la ligne:
< % = new ( Map . put ( assigns , :action , ~p " /items/new " ) ) % >Remplacez-le par:
< % = 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 % > Ici, nous vérifions si nous modifions un élément et rendons un lien au lieu du formulaire. Nous le faisons pour éviter d'avoir plusieurs formulaires sur la page. Si nous ne modifions pas un élément, rendez le new.html.heex comme auparavant. Avec cela, si l'utilisateur modifie un élément, il est en mesure de "sortir du mode d'édition" en cliquant sur le lien qui est rendu.
par exemple: lib/app_web/controllers/item_html/index.html.heex#L30-L38
Ensuite, toujours dans le fichier index.html.eex , localisez la ligne:
< %= for item < - @items do % > Remplacez l'ensemble de la balise <li> par le code suivant.
< 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> par exemple: lib/app_web/controllers/item_html/index.html.heex#L46-L79
Nous avons fait quelques choses ici. Nous avons changé le bouton de bascule à l'extérieur de la balise <div class="view> . De plus, nous avons changé le texte avec A if else Block Instructions.
Si l'utilisateur n'est pas éditant, un lien ( <a> ) est rendu qui, lorsqu'il est cliqué, permet à l'utilisateur de saisir le mode "Modifier". D'un autre côté, si l'utilisateur modifie , il rend le fichier edit.html.heex .
En parlant de cela, modifions edit.html.heex pour que cela rend ce que nous voulons: un champ de texte qui, une fois Enter en appuyant, modifie l'élément de référence.
< .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 pour l'édition Pour activer l'effet CSS à double clic pour entrer en mode edit , nous devons ajouter le CSS suivant à notre fichier 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 ;
} par exemple: assets/css/app.css#L13-L32
De plus, comme notre balisage est légèrement différent du balisage TODOMVC, nous devons ajouter un peu plus de CSS pour garder l'interface utilisateur cohérent:
. 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 */
} C'est à quoi ressemble votre fichier app.scss à la fin de cette étape: assets/css/app.css#L34-L71
ItemController.edit/2 Afin d'activer l'édition en ligne, nous devons modifier la fonction edit/2 . Ouvrez le fichier lib/app_web/controllers/item_controller.ex et remplacez la fonction edit/2 par ce qui suit:
def edit ( conn , params ) do
index ( conn , params )
end De plus, étant donné que nous demandons à notre fonction index/2 de gérer l'édition, nous devons mettre à jour 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 Enfin, nous devons gérer la soumission de formulaire pour mettre à jour un élément (qui est rendu dans edit.html.heex ). Lorsque nous appuyons sur Enter , le gestionnaire update/2 est appelé dans lib/app_web/controllers/item_controller.ex . Nous voulons rester sur la même page après la mise à jour de l'élément.
Alors, changez-le pour que cela ressemble à ceci.
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 Votre fichier item_controller.ex devrait maintenant ressembler à ceci: lib/app_web/controllers/item_controller.ex
ItemControllerTestDans notre quête pour créer une application à une seule page, nous avons cassé quelques tests! C'est OK. Ils sont faciles à réparer.
Ouvrez le fichier test/app_web/controllers/item_controller_test.exs et localisez le test avec le texte suivant.
test "renders form for editing chosen item"
Et changez-le pour que cela ressemble à ce qui suit.
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 Lorsque nous entrons le "Modifier le mode Timer", nous créons <a> un lien pour revenir aux /items , comme nous l'avons déjà implémenté. Cette balise a le texte "cliquez ici pour créer un nouvel élément", ce que nous affirmons.
par exemple: test/app_web/controllers/item_controller_test.exs#L37-L39
Ensuite, localisez le test avec la description suivante:
describe "update item"Mettez à jour le bloc vers le morceau de code suivant.
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 par exemple: test/app_web/controllers/item_controller_test.exs#L67-L80
Nous avons mis à jour les chemins de main vers lesquels l'application redirige après la mise à jour d'un élément. Étant donné que nous construisons une application à une seule page, ce chemin se rapporte au chemin /items/ URL.
Si vous exécutez les tests maintenant, ils devraient reprendre:
mix test
23:08:01.785 [info] Already up
...........................
Finished in 0.5 seconds
27 tests, 0 failures
Randomized with seed 956565
index.html Maintenant que nous avons le fonctionnement toggle et edit , nous pouvons enfin supprimer la disposition par défaut de Phoenix (table) du modèle lib/app_web/controllers/item_html/index.html.heex .

Ouvrez le fichier lib/app_web/controllers/item_html/index.html.eex et supprimez tout le code avant la ligne:
< section class =" todoapp " > par exemple: lib/app_web/controllers/item_html/index.html.heex
Votre application devrait maintenant ressembler à ceci: 
Malheureusement, en supprimant la disposition par défaut, nous avons "cassé" les tests.
Ouvrez le fichier test/app_web/controllers/item_controller_test.exs et localisez le test qui a la description suivante:
test "lists all items"Mettez à jour l'affirmation de:
assert html_response ( conn , 200 ) =~ "Listing Items"À:
assert html_response ( conn , 200 ) =~ "todos" par exemple: test/app_web/controllers/item_controller_test.exs#L14
Maintenant que la fonctionnalité Core (créer, modifier / mettre à jour, supprimer) fonctionne, nous pouvons ajouter les améliorations finales de l'interface utilisateur. Dans cette étape, nous allons ajouter la navigation / filtrage de pied de page.

La vue "tout" est la valeur par défaut. Le "Active" est tous les éléments avec status==0 . "Terminé" est tous les éléments avec status==1 .
/:filter RouteAvant de commencer, ajoutons un test unitaire. Nous voulons afficher les éléments filtrés selon le filtre choisi.
Ouvrez test/app_web/controllers/item_controller_test.exs et Locate describe "index" do . Dans ce bloc, ajoutez le test suivant. Il vérifie si l'élément est correctement affiché lorsque le filtre est modifié.
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 par exemple: test/app_web/controllers/item_controller_test.exs#L21-L32
Ouvrez la lib/app_web/router.ex et ajoutez la route suivante:
get "/items/filter/:filter" , ItemController , :index par exemple: /lib/app_web/router.ex#L23
index/2 pour envoyer filter à afficher / modèle Ouvrez le fichier lib/app_web/controllers/item_controller.ex et localisez la fonction index/2 . Remplacez l'invocation de render/3 à la fin de index/2 par ce qui suit:
render ( conn , "index.html" ,
items: items ,
changeset: changeset ,
editing: item ,
filter: Map . get ( params , "filter" , "all" )
) par exemple: lib/app_web/controllers/item_controller.ex#L17-L22
Map.get(params, "filter", "all") définit la valeur par défaut de notre filter à "tout" donc lorsque index.html est rendu, affichez "tous" des éléments.
filter/2 Afin de filtrer les éléments par leur statut, nous devons créer une nouvelle fonction.
Ouvrez le fichier lib/app_web/controllers/item_html.ex et créez la fonction filter/2 comme suit:
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 par exemple: lib/app_web/controllers/item_html.ex#L19-L26
Cela nous permettra de filtrer les éléments à l'étape suivante.
index.html Utilisez la fonction filter/2 pour filtrer les éléments affichés. Ouvrez le fichier lib/app_web/controllers/item_html/index.html.heex et localisez la ligne de boucle for
< % = for item <- @ items do % >Remplacez-le par:
< % = for item <- filter ( @ items , @ filter ) do % > par exemple: lib/app_web/controllers/item_html/index.html.heex#L18
Cela invoque la fonction filter/2 que nous avons définie à l'étape précédente, passant dans la liste de @items et le @filter sélectionné.
Ensuite, localisez le <footer> et remplacez le contenu du <ul class="filters"> par le code suivant:
< 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 > Nous ajoutons conditionnellement la classe selected en fonction de la valeur d'attribution @filter .
par exemple: /lib/app_web/controllers/item_html/index.html.heex#L62-L98
À la fin de cette étape, vous aurez un filtre de pied de page entièrement fonctionnel:

Nous pouvons rapidement couvrir cette fonction que nous avons ajoutée avec un petit test unitaire. Ouvrez test/app_web/controllers/item_html_test.exs et ajoutez ce qui suit.
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
endEt vous devriez en avoir fini avec cette fonctionnalité ?. Travail génial!
Nous avons presque fini avec notre implémentation Phoenix de TODOMVC. La dernière chose à mettre en œuvre est "claire terminée".
Ouvrez votre fichier lib/app_web/router.ex et ajoutez la route suivante:
get "/items/clear" , ItemController , :clear_completed Votre scope "/" devrait maintenant ressembler à ce qui suit:
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 Dans le fichier lib/app_web/controllers/item_controller.ex ajoutez le code suivant:
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 par exemple: lib/app_web/controllers/item_controller.ex#L87-L93
Cela utilise la fonction Handy update_all/3 pour mettre à jour tous les éléments qui correspondent à la query . Dans notre cas, nous recherchons tous items qui appartiennent à person_id==0 et avons status==1 .
Nous ne supprimons pas les éléments, nous mettons plutôt à la mise à jour leur statut à 2 ce qui, aux fins de notre exemple, signifie qu'ils sont "archivés".
Remarque : Ceci est un guide utile pour
update_all: https://adamdelong.com/bulk-update-ecti
Enfin, dans le lib/app_web/controllers/item_html/index.html.eex faites défiler vers le bas du fichier et remplacez la ligne:
< button class = "clear-completed" style = "display: block;" >
Clear completed
< / button >Avec:
< a class = "clear-completed" href = "/items/clear" >
Clear completed
[ < % = Enum . count ( filter ( @ items , "completed" ) ) % > ]
< / a > par exemple: lib/app_web/controllers/item_html/index.html.heex#L104-L107
La dernière chose que nous devons faire est de mettre à jour la fonction filter/2 dans lib/app_web/controllers/item_html.ex . Étant donné que status = 2 concerne désormais un état archivé , nous voulons renvoyer tout ce qui n'est pas archivé.
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?
And that's it! 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 ? Vous avez de la chance! 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! ☀️