Ein vollständiges Tutorial für Anfänger für den Aufbau einer Todo-Liste in Phoenix.
100% funktional. 0% JavaScript. Nur HTML , CSS und Elixir . Schnell und wartbar.
Todo -Listen sind den meisten Menschen bekannt; Wir erstellen die ganze Zeit Listen. Das Aufbau einer Todo -Liste von Grund auf ist eine großartige Möglichkeit, Elixir / Phoenix zu lernen, da die UI / UX einfach ist, sodass wir uns auf die Implementierung konzentrieren können.
Für das Team @dwyl ist diese App/Tutorial ein Präsentieren, wie das Rendering der Server -Seite ( mit progressiver Verbesserung der Client -Seite ) eine hervorragende Balance zwischen Entwickler -Effektivität ( Versandfunktionen schnell ), UX und Barrierefreiheit bieten kann. Die von Server gerenderten Seiten benötigen weniger als 5 ms, um zu reagieren, sodass die UX schnell ist. Auf fly.io: phxtodo.fly.dev Roundtrip-Reaktionszeiten sind für alle Interaktionen Unter 200 ms, so dass es sich wie eine clientseitige App anfühlt .
Ein Tutorial für das TODO -Listen, das einem vollständigen Anfänger zeigt, wie man eine App in Elixir/Phoenix von Grund auf neu erstellt.
Probieren Sie die Version von Fly.io aus. Fügen Sie der Liste ein paar Elemente hinzu und testen Sie die Funktionalität.

Selbst mit einem vollständigen HTTP-Hin- und Rückflug für jede Interaktion ist die Reaktionszeit schnell . Achten Sie darauf, wie Chrome | Firefox | Safari auf die Antwort vom Server wartet, bevor Sie die Seite erneut rendern. Die alte vollständige Auffrischung von gestern ist verschwunden . Moderne Browser machen intelligent nur die Veränderungen! Also nähert sich der UX "Native"! Im Ernst, probieren Sie die Fly.io -App auf Ihrem Telefon aus und sehen Sie!
In diesem Tutorial verwenden wir das TodomVC -CSS, um unsere Benutzeroberfläche zu vereinfachen. Dies hat mehrere Vorteile, um zu minimieren , wie viel CSS wir schreiben müssen! Dies bedeutet auch, dass wir einen Leitfaden haben, für den Merkmale implementiert werden müssen, um die volle Funktionalität zu erreichen.
Hinweis : Wir lieben
CSSfür seine unglaubliche Kraft/Flexibilität, aber wir wissen, dass nicht jeder es mögen. Siehe: Lerntachyons#Warum das Letzte , was wir wollen, ist, Tonnen von Zeit mitCSSin einemPhoenix-Tutorial zu verschwenden!
Dieses Tutorial richtet sich an alle, die Elixir/Phoenix lernen. Es werden keine früheren Erfahrungen mit Phoenix angenommen/erwartet. Wir haben alle Schritte für den Bau der App aufgenommen.
Wenn Sie in einem Schritt stecken bleiben, öffnen Sie bitte ein Problem auf Github, in dem wir Ihnen gerne dabei helfen, sich zu lösen! Wenn Sie der Meinung sind, dass eine Codezeile etwas mehr Erklärung/Klarheit verwenden kann, zögern Sie bitte nicht, uns zu informieren ! Wir wissen, wie es ist, ein Anfänger zu sein. Es kann frustrierend sein, wenn etwas keinen Sinn macht! Wenn Sie Fragen zu Github stellen, können Sie alle lernen!
Bitte geben Sie uns Feedback! Sterne das Repo, wenn du es hilfreich fand.
Bevor Sie versuchen, die TODO -Liste zu erstellen , stellen Sie sicher, dass Sie alles, was Sie benötigen, auf Ihrem Computer installiert werden. Siehe: Voraussetzungen
Sobald Sie bestätigt haben, dass Sie Phoenix und Postgresql installiert haben, leiten Sie die fertige App aus.
localhost aus Führen Sie die fertige Version in Ihrem localhost aus, um zu bestätigen, dass sie funktioniert, bevor Sie Ihre eigene Version der Todo -List -App erstellen, um zu bestätigen, dass sie funktioniert.
Klonen Sie das Projekt von GitHub:
git clone [email protected]:dwyl/phoenix-todo-list-tutorial.git && cd phoenix-todo-list-tutorialInstallieren Sie Abhängigkeiten und richten Sie die Datenbank ein:
mix setupStarten Sie den Phoenix -Server:
mix phx.server Besuchen Sie localhost:4000 in Ihrem Webbrowser.
Sie sollten sehen:

Jetzt, da Sie die fertige Beispiel -App auf Ihrem localhost laufen lassen,
Lassen Sie es uns von Grund auf neu erstellen und alle Schritte verstehen.
Wenn Sie die fertige Beispiel -App auf localhost ausführen, müssen Sie die login AUTH_API_KEY . [1 Minute] Siehe: Holen Sie sich Ihren AUTH_API_KEY
Wenn Sie die fertige App auf Ihrem localhost betrieben haben ( und Sie sollten es wirklich! ),
Sie müssen ein Verzeichnis ändern, bevor Sie mit dem Tutorial beginnen:
cd ..
Jetzt bist du bereit zu bauen !
Erstellen Sie in Ihrem Terminal eine neue Phoenix -App mit dem folgenden mix -Befehl:
mix phx.new app --no-dashboard --no-gettext --no-mailer Wenn Sie aufgefordert werden, Abhängigkeiten zu installieren, geben Sie y gefolgt von der Eingabetaste ein.
HINWEIS : Diese Flags nach dem
app-Namen vermeiden nur das Erstellen von Dateien, die wir für dieses einfache Beispiel nicht benötigen . Siehe: hexdocs.pm/phoenix/mix.tasks.phx.new
Wechseln Sie in das neu erstellte app -Verzeichnis ( cd app ) und stellen Sie sicher, dass Sie alles haben, was Sie benötigen:
mix setupStarten Sie den Phoenix -Server:
mix phx.server Jetzt können Sie localhost:4000 in Ihrem Webbrowser besuchen. Sie sollten etwas Ähnliches sehen wie:

Schalten Sie den Phoenix -Server Strg + c ab .
Führen Sie die Tests durch, um sicherzustellen, dass alles wie erwartet funktioniert:
mix testSie sollten sehen:
Compiling 16 files (.ex)
Generated app app
17:49:40.111 [info] Already up
...
Finished in 0.04 seconds
3 tests, 0 failuresNachdem die Phoenix -App wie erwartet funktioniert hat, gehen wir zum Erstellen einiger Dateien ein!
items Beim Erstellen einer grundlegenden Todo -Liste benötigen wir nur ein Schema: items . Später können wir separate Listen und Tags hinzufügen, um unsere items zu organisieren/kategorisieren, aber im Moment ist dies alles, was wir brauchen.
Führen Sie den folgenden Generatorbefehl aus, um die Tabelle der Elemente zu erstellen:
mix phx.gen.html Todo Item items text:string person_id:integer status:integer Streng genommen brauchen wir nur die text und status , aber da wir wissen, dass wir Gegenstände mit Personen (_later im Tutorial) in Verbindung bringen möchten, fügen wir jetzt das Feld hinzu.
Sie sehen die folgende Ausgabe:
* 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
Das hat eine Reihe von Dateien erstellt! Einige davon brauchen wir nicht ausschließlich.
Wir könnten manuell nur die Dateien erstellen, die wir benötigen , aber dies ist die "offizielle" Art, eine Crud -App in Phoenix zu erstellen, also verwenden wir sie für Geschwindigkeit.
HINWEIS : Phoenix -Kontexte, die in diesem Beispiel als
Todobezeichnet werden, sind " dedizierte Module, die die Funktionen und gruppenbezogene Funktionen aufdecken ." Wir glauben, dass sie unnötig grundlegende Phoenix -Apps mit "Schnittstellenschichten" komplizieren , und wir wünschen uns wirklich , dass wir sie vermeiden könnten. Angesichts der Tatsache, dass sie in die Generatoren eingebunden sind und der Schöpfer des Frameworks sie mag , haben wir die Wahl: Entweder mit Kontexten oder manuell alle Dateien in unseren Phoenix -Projekten erstellen. Generatoren sind eine viel schnellere Art zu bauen! Umarmen Sie sie, auch wenn Sie auf dem Weg ein paar unbenutzte Dateiendeletemüssen!
Wir werden in dieser Phase im Tutorial nicht jede dieser Dateien im Tutorial erklären, da es einfacher ist, die Dateien beim Erstellen der App zu verstehen! Der Zweck jeder Datei wird klar, wenn Sie durch die Bearbeitung voranschreiten.
/items zu router.ex hinzu Befolgen Sie die vom Generator angegebenen Anweisungen zum Hinzufügen der resources "/items", ItemController zum router.ex .
Öffnen Sie die lib/app_web/router.ex -Datei und suchen Sie die Zeile: scope "/", AppWeb do Fügen Sie die Linie zum Ende des Blocks hinzu. z.B:
scope "/" , AppWeb do
pipe_through :browser
get "/" , PageController , :index
resources "/items" , ItemController # this is the new line
end Ihre router.ex -Datei sollte so aussehen: router.ex#L20
Jetzt, wie das Terminal vorschlug, rennen Sie mix ecto.migrate . Dadurch wird die Datenbanktabellen eingerichtet und die erforderlichen Migrationen ausgeführt, sodass alles richtig funktioniert!
Zu diesem Zeitpunkt haben wir bereits eine funktionale Todo -Liste ( wenn wir bereit waren, die Standard -Phoenix -Benutzeroberfläche zu verwenden ).
Versuchen Sie, die App auf Ihrem localhost auszuführen: Führen Sie die generierten Migrationen mit mix ecto.migrate aus, dann den Server mit:
mix phx.server
Besuchen Sie: http: // localhost: 4000/items/neu und geben Sie einige Daten ein.

Klicken Sie auf die Schaltfläche "Element speichern" und Sie werden auf die Seite "anzeigen" umgeleitet: http: // localhost: 4000/items/1

Dies ist keine attraktive Benutzererfahrung (UX), aber es funktioniert ! Hier ist eine Liste von Elementen - eine "Todo -Liste". Sie können dies besuchen, indem Sie auf die Schaltfläche Back to items klicken oder auf die folgende URL http: // localhost: 4000/items zugreifen.

Verbessern wir die UX, indem wir das ToMVC HTML und CSS verwenden!
Um die todomvc ui/ux nachzubilden, liehen wir den HTML -Code direkt aus dem Beispiel aus.
Besuchen Sie: http://todomvc.com/examples/vanillajs Fügen Sie der Liste ein paar Elemente hinzu. Überprüfen Sie dann die Quelle mit den Entwicklerwerkzeugen Ihres Browsers. z.B:

Klicken Sie mit der rechten Maustaste auf die gewünschte Quelle (zB: <section class="todoapp"> ) und wählen Sie "AS HTML" bearbeiten:

Sobald die HTML für den <section> bearbeitet werden kann, wählen Sie es aus und kopieren Sie es.

Der HTML -Code lautet:
< 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 > Konvertieren wir diese HTML in eine eingebettete EXIXIR -Vorlage ( EEx ).
HINWEIS : Der Grund , warum wir dieses
HTMLaus dem Elementinspektor des Browsers anstelle von direkt aus der Quelle auf GitHub kopieren:examples/vanillajs/index.htmlist, dass dies eine "<ul class="todo-list"></ul>App" ist. Das Kopieren des Browser -Entwickler -Tools ist der einfachste Weg, um die vollständigeHTMLzu erhalten.
index.html.eex ein. Öffnen Sie die lib/app_web/controllers/item_html/index.html.eex -Datei und scrollen Sie nach unten.
Dann ( ohne den bereits vorhandenen Code zu entfernen ) fügen Sie den HTML -Code ein, den wir von TodomVC bezogen haben.
EG:
/lib/app_web/controllers/item_html/index.html.eex#L27-L73
Wenn Sie versuchen, die App jetzt auszuführen und http: // localhost: 4000/items/besuchen/
Sie werden dies sehen ( ohne das Todomvc CSS ):

Das ist offensichtlich nicht das, was wir wollen, also lass uns das Todomvc CSS bekommen und es in unserem Projekt retten!
/assets/css Besuchen Sie http://todomvc.com/examples/vanillajs/node_modules/todomvc-app-apps/index.css
und speichern Sie die Datei in /assets/css/todomvc-app.css .
EG: /assets/css/todomvc-app.css
todomvc-app.css in app.scss Öffnen Sie die Datei assets/css/app.scss und ersetzen Sie sie durch Folgendes:
/* This file is for your main application css. */
/* @import "./phoenix.css"; */
@import "./todomvc-app.css" ; EG: /assets/css/app.scss#L4
Öffnen Sie Ihre lib/app_web/components/layouts/app.html.heex -Datei und ersetzen Sie den Inhalt durch den folgenden Code:
<!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 >Vorher:
lib/app_web/components/layouts/app.html.eex
Nach:lib/app_web/components/layouts/app.html.heex
<%= @inner_content %> ist, wo die Todo -App gerendert wird.
Hinweis : Das
<script>-Tag ist aus der Konvention enthalten. Wir werden jedoch in diesem Tutorial keinJavaScriptschreiben. Wir werden eine 100% ige Feature -Parität mit ToMVC erreichen, ohne eine Zeile vonJSzu schreiben. Wir "hassen"JSnicht, in der Tat haben wir ein "Schwester" -Tutorial, das dieselbe App inJS: Dwyl/Javascript-Todo-List-Tutorial erstellt. Wir möchten Sie nur daran erinnern , dass Sie keineJSbrauchen , um eine voll funktionsfähige Webanwendung mit großem UX zu erstellen!
Wenn die Layout-Vorlage gespeichert ist, speichert die ToMvc-CSS-Datei, die in /assets/css/todomvc-app.css und die in app.scss importierte todomvc-app.css importiert wurde, Ihre /items sollte jetzt so aussehen:

Unsere Todo -Liste sieht also so aus wie tomvc, aber es ist immer noch nur eine Dummy -Liste.
Um item in der TOMOMVC -Vorlage auszulösen, müssen wir einige Funktionen hinzufügen. Als wir das Projekt erstellt und das item generiert haben, wurde ein Controller erstellt (befindet sich in lib/app_web/controllers/item_controller.ex ) und auch eine Komponente/Ansicht (befindet sich in lib/app_web/controllers/item_html.ex ). Diese Komponente/Ansicht steuert das Rendern des Inhalts im Verzeichnis lib/app_web/controllers/item_html mit dem wir vorher bastelten, effektiv kontrolliert.
Wir wissen, dass wir Änderungen an der Benutzeroberfläche vornehmen müssen, daher werden wir in dieser Komponente einige Funktionen hinzufügen (was dem Ansichtsteil des MVC -Paradigmas ähnelt).
Dies ist unsere erste Chance, ein bisschen Test -Treen -Entwicklung (TDD) durchzuführen.
Erstellen Sie eine neue Datei mit dem test/app_web/controllers/item_html_test.exs .
Geben Sie den folgenden Code in die Datei ein:
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 EG: /test/app_web/controllers/item_html_test.exs
Wenn Sie versuchen, diese Testdatei auszuführen:
mix test test/app_web/controllers/item_html_test.exsSie sehen den folgenden Fehler (weil die Funktion noch nicht vorhanden ist!):
** (UndefinedFunctionError) function AppWeb.ItemHTML.checked/1 is undefined or private
Öffnen Sie die lib/app_web/controllers/item_html.ex -Datei und schreiben Sie die Funktionen, damit die Tests bestehen .
So haben wir die Funktionen implementiert. Ihre item_html.ex -Datei sollte jetzt wie Folgendes aussehen.
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
endFühren Sie die Tests erneut aus und sie sollten jetzt bestehen:
mix test test/app_web/controllers/item_html_test.exsSie sollten sehen:
....
Finished in 0.1 seconds
4 tests, 0 failuresNachdem wir diese beiden Ansichtsfunktionen erstellt haben und unsere Tests bestehen, verwenden wir sie in unserer Vorlage!
Öffnen Sie die lib/app_web/controllers/item_html/index.html.eex -Datei und suchen Sie die Zeile:
< ul class =" todo-list " > Ersetzen Sie den Inhalt des <ul> durch Folgendes:
< %= 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 % > ZB: lib/app_web/controllers/item_html/index.html.heex#L43-L53
Wenn Sie mit diesen beiden Dateien gespeichert sind, führen Sie die App jetzt aus: mix phx.server und besuchen Sie http: // localhost: 4000/items.
Sie sehen die realen items die Sie in Schritt 2.2 oben erstellt haben:

Nachdem wir unsere Artikel im TodomVC -Layout rendern, lassen Sie uns daran arbeiten, neue Elemente im Stil "Single Page App" zu erstellen.
Gegenwärtig ist unser "neues Element " -Formform
Wir möchten, dass die Person in der Lage ist, ein neues Element zu erstellen, ohne zu einer anderen Seite navigieren zu müssen. Um dieses Ziel zu erreichen, werden wir die lib/app_web/controllers/item_html/new.html.heex -Vorlage ( teilweise ) in der lib/app_web/controllers/item_html/index.html.heex -Vorlage einbeziehen. z.B:
Bevor wir das tun können , müssen wir die new.html.heex Vorlage für die NEW aufräumen.
Öffnen wir lib/app_web/controllers/item_html/new.html.heex und vereinfachen Sie es nur auf das wesentliche Feld :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 >
Vorher:
/lib/app_web/controllers/item_html/new.html.heex
Nach:/lib/app_web/controllers/item_html/new.html.heex
Wir müssen zusätzlich den Stil des <.input> Tags ändern. Mit Phoenix in der Datei lib/app_web/components/core_components.ex werden die Stile für vorgefertigte Komponenten definiert (was bei <.input> der Fall ist).
Um dies zu ändern, so verwenden Sie den gleichen Stil wie bei toDomVC, finden Sie die folgende Zeile.
def input ( assigns ) do
Ändern Sie das Klassenattribut mit der new-todo Klasse. Diese Funktion sollte wie folgt aussehen.
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 Wir müssen auch die actions in der simple_form ändern. Suchen Sie in derselben Datei nach def simple_form(assigns) do und ändern Sie sie so, dass es so aussieht:
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 Wenn Sie jetzt die Phoenix -App ausführen und http: // localhost: 4000/items/neu besuchen, sehen Sie das Feld Single :text und keine "Speichern" -Taste:

Machen Sie sich keine Sorgen, Sie können das Formular dennoch mit der Eingabetaste (Rückgabe) einreichen. Wenn Sie jedoch versuchen, das Formular jetzt einzureichen, wird es nicht funktionieren, da wir zwei der vom changeset erforderlichen Felder entfernt haben! Lassen Sie uns das beheben.
items -Schema, um default festzulegen Angesichts der Tatsache, dass wir zwei der Felder ( :person_id und :status ) aus dem new.html.eex entfernt haben, müssen wir sicherstellen, dass diese im Schema Standardwerte für diese gibt. Öffnen Sie die Datei lib/app/todo/item.ex und ersetzen Sie den Inhalt durch folgende:
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 Hier aktualisieren wir das schema "Elemente", um einen Standardwert von 0 für person_id und status festzulegen. Und im changeset/2 entfernen wir die Anforderung für person_id und status . Auf diese Weise kann unser neues item nur mit dem text eingereicht werden.
EG: /lib/app/todo/item.ex#L6-L7
Nachdem wir default für person_id und status haben, wenn Sie das /items/new Formular einreichen, wird dies erfolgreich sein.
index/2 in ItemController Um das neue Elementformular ( new.html.eex ) in der index.html.eex -Vorlage zu investieren, müssen wir die AppWeb.ItemController.index/2 aktualisieren, um einen Änderungssatz einzuschließen.
Öffnen Sie die lib/app_web/controllers/item_controller.ex -Datei und aktualisieren Sie die index/2 -Funktion auf folgende:
def index ( conn , _params ) do
items = Todo . list_items ( )
changeset = Todo . change_item ( % Item { } )
render ( conn , "index.html" , items: items , changeset: changeset )
end Vorher: /lib/app_web/controllers/item_controller.ex
Nach: /lib/app_web/controllers/item_controller.ex#L9-L10
Sie werden nach diesem Schritt keine Änderung in der Benutzeroberfläche oder Tests sehen . Gehen Sie einfach weiter zu 5.3, wo der "Aha" -Moment passiert.
new.html.eex in index.html.eex Nachdem wir die gesamte Vorbereitungsarbeiten durchgeführt haben, besteht der nächste Schritt darin, die new.html.eex ( teilweise ) in index.html.eex -Vorlage zu rendern.
Öffnen Sie die lib/app_web/controllers/item_html/index.html.heex -Datei und suchen Sie die Zeile:
< input class =" new-todo " placeholder =" What needs to be done? " autofocus ="" >Ersetzen Sie es dadurch:
< % = new ( Map . put ( assigns , :action , ~p " /items/new " ) ) % > Lassen Sie uns zusammenbrechen, was wir gerade getan haben. Wir bitten das new.html.heex partiell in der index.html.heex -Datei. Wir tun dies, indem wir die new/2 -Funktion in item_controller.ex aufrufen. Diese Funktion bezieht sich auf die Seite in den URL items/new und rendert die new.html.heex -Datei. Daher nennen wir diese Funktion, um erfolgreich einzubetten?
Vorher: /lib/app_web/controllers/item_html/index.html.heex#L36
Nach: /lib/app_web/controllers/item_html/index.html.heex#L36
Wenn Sie jetzt die App ausführen und besuchen: http: // localhost: 4000/items
Sie können ein Element erstellen, indem Sie Ihren Text eingeben und ihn mit der Eingabetaste (Rückgabe) senden.

Das Umleiten in die Vorlage "Show" ist "OK", aber wir können ux besser machen, indem wir uns auf die index.html -Vorlage umgeben. Zum Glück ist dies so einfach wie die Aktualisierung einer einzelnen Zeile im Code.
redirect in create/2 Öffnen Sie die lib/app_web/controllers/item_controller.ex -Datei und suchen Sie die Funktion create . Speziell die Zeile:
|> redirect ( to: ~p " /items/ #{ item } " )Aktualisieren Sie die Zeile auf:
|> redirect ( to: ~p " /items/ " ) Vorher: /lib/app_web/controllers/item_controller.ex#L22
Nach: /lib/app_web/controllers/item_controller.ex#L23
Wenn wir nun ein neues item erstellen, werden wir in die index.html -Vorlage umgeleitet:

item_controller_test.exs , um in index umzuleiten Die Änderungen, die wir an den new.html.heex Dateien und den obigen Schritten vorgenommen haben, haben einige unserer automatisierten Tests zerstört. Wir sollten das beheben.
Führen Sie die Tests aus:
mix testSie sehen die folgende Ausgabe:
Finished in 0.08 seconds (0.03s async, 0.05s sync)
23 tests, 3 failures
Öffnen Sie die Datei test/app_web/controllers/item_controller_test.exs und suchen Sie describe "new item" und describe "create item" . Ändern Sie diese beiden nach Folgendes.
Ersetzen Sie den 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
endAktualisierter Code:
/test/app_web/controllers/item_controller_test.exs#L34-L55
Wenn Sie die Tests erneut ausführen, mix test der jetzt alle wieder passt.
......................
Finished in 0.2 seconds (0.09s async, 0.1s sync)
22 tests, 0 failuresBisher funktioniert die Hauptfunktionalität der TOMOMVC -UI, wir können neue Elemente erstellen und sie erscheinen in unserer Liste. In diesem Schritt werden wir die Benutzeroberfläche verbessern, um die Anzahl der verbleibenden Gegenstände in der unteren linken Ecke aufzunehmen.
Öffnen Sie die Datei test/app_web/controllers/item_html_test.exs und erstellen Sie die folgenden zwei Tests:
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 EG: test/app_web/controllers/item_html_test.exs#L14-L26
Diese Tests scheitern, da die Funktion ItemHTML.remaining_items/1 nicht vorhanden ist.
Lassen Sie die Tests übergeben , indem Sie den folgenden Code zur lib/app_web/controllers/item_html.ex -Datei hinzufügen:
# 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 EG: /lib/app_web/controllers/item_html#L15-L17
Verwenden Sie nun, da die Tests bestanden werden, die remaining_items/1 in der index.html -Vorlage. Öffnen Sie die lib/app_web/controllers/item_html/index.html.eex -Datei und suchen Sie die Codezeile:
< span class =" todo-count " > < strong > 1 </ strong > item left </ span >Ersetzen Sie es durch diese Zeile:
< span class =" todo-count " > < %= remaining_items(@items) % > items left </ span > Dies ruft nur auf die Funktion ItemHTML.remaining_items/1 mit der Liste der @items auf, die die Ganzzahlanzahl der verbleibenden Elemente zurückgeben, die noch nicht "fertig" wurden.
EG: /lib/app_web/controllers/item_html/index.html.eex#L60
Zu diesem Zeitpunkt funktioniert die (verbleibenden) Gegenstände im unteren Bereich der TOMOMVC -Benutzeroberfläche!
Fügen Sie Ihrer Liste einen new Artikel hinzu und sehen Sie sich die Zählung an:

Das war einfach genug, lass uns etwas weiterentwickelter ausprobieren!
Machen Sie eine Pause und schnappen Sie sich ein frisches Glas Wasser, der nächste Abschnitt wird intensiv sein!
status eines Todo -Elements auf 1 um 1 Eine der Kernfunktionen einer TODO -Liste ist die Umschaltung des status eines item von 0 auf 1 ("komplett").
In unserem Schema hat ein abgeschlossener item den status von 1 .
Wir werden zwei Funktionen in unserem Controller brauchen:
toggle_status/1 Schaltet den Status eines Elements um, z. B. 0 bis 1 und 1 bis 0.toggle/2 Die Handler -Funktion für HTTP -Anforderungen, um den Status eines Elements zu umschalten. Öffnen Sie die Datei test/app_web/controllers/item_controller_test.exs . Wir werden hier einige Änderungen vornehmen, damit wir den Funktionen, die wir zuvor erwähnt haben, Tests hinzufügen können. Wir werden App.Todo in item_controller_test.exs importieren und create create constanten beheben und zu ordnen, um Scheinelemente zu erstellen. Stellen Sie sicher, dass der Beginn der Datei so aussieht.
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 }
Wir fügen feste Item hinzu, um später in Tests verwendet zu werden. Wir geben public Item an, da wir dieser App später Authentifizierung hinzufügen.
Suchen Sie anschließend defp create_item()/1 Funktion in derselben Datei. Ändern Sie es so, dass es so aussieht.
defp create_item ( _ ) do
item = item_fixture ( @ create_attrs )
% { item: item }
end Wir werden diese Funktion verwenden, um Item zu erstellen, die in den Tests verwendet werden, die wir hinzufügen werden. Apropos, lass uns das tun! Fügen Sie der Datei den folgenden Ausschnitt hinzu.
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
Öffnen Sie die Datei lib/app_web/controllers/item_controller.ex und fügen Sie die folgenden Funktionen hinzu:
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
Die Tests werden an dieser Stelle immer noch scheitern , da die Route, die wir in unserem Test anrufen, noch nicht vorhanden ist. Lasst uns das beheben!
get /items/toggle/:id -Route, die sich umschaltet toggle/2 aufruft Öffnen Sie die lib/app_web/router.ex und suchen Sie die resources "/items", ItemController . Fügen Sie eine neue Zeile hinzu:
get "/items/toggle/:id" , ItemController , :toggle EG: /lib/app_web/router.ex#L21
Jetzt werden unsere Tests endlich bestehen:
mix testSie sollten sehen:
22:39:42.231 [info] Already up
...........................
Finished in 0.5 seconds
27 tests, 0 failurestoggle/2 auf, wenn ein Kontrollkästchen in index.html klickt wird Nachdem unsere Tests bestehen, ist es Zeit, all diese Funktionen zu verwenden , die wir in der Benutzeroberfläche aufgebaut haben. Öffnen Sie die /lib/app_web/controllers/item_html/index.html.heex -Datei und suchen Sie die Zeile:
< %= if item.status == 1 do % >
...
< % else % >
...
< % end % >Ersetzen Sie es durch Folgendes:
< %= 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 % > Wenn dieser Link klickt, wird auf den Endpunkt des get /items/toggle/:id aufgerufen.
Das wiederum löst den oben definierten toggle/2 -Handler aus.
Vorher:
/lib/app_web/controllers/item_html/index.html.heex#L40
Nach:/lib/app_web/controllers/item_html/index.html.heex#L47-L57
app.scss ein .checked CSS -CSS hinzufügen Leider können <a> Tags (die mit <.link> generiert werden) einen :checked Pseudo -Selektor haben, sodass die Standardstile, die auf dem <input> -Tag funktionierten, nicht für den Link funktioniert. Wir müssen also ein paar Zeilen von CSS zu unserer app.scss hinzufügen.
Öffnen Sie die Datei assets/css/app.scss und fügen Sie die folgenden Zeilen hinzu:
. 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;
} Nach dem Speichern der Datei sollten Sie: /assets/css/app.scss#L8 haben
Und wenn Sie die App anzeigen, funktioniert die Umschaltfunktionalität wie erwartet:

Implementierung Hinweis : Wir verwenden in diesem Tutorial sehr absichtlich kein JavaScript , da wir demonstrieren, wie eine 100% serverseitige App-App durchführt. Dies funktioniert immer , auch wenn JS im Browser deaktiviert ist oder das Gerät super alt ist und keinen modernen Webbrowser hat. Wir hätten dem <input> -Tag, z. B. leicht ein Onclick -Attribut, z. B. ein onclick -Attribut hinzufügen können:
< input < %= checked(item) % > type="checkbox" class="toggle"
onclick="location.href='
< %= Routes.item_path(@conn, :toggle, item.id) % > ';" > Aber onclick ist JavaScript und wir müssen nicht auf JS zurückgreifen.
Der <a> (Link) ist ein perfekt semantischer Nicht-JS-Ansatz für das Umschalten item.status .
todo -Elemente Wenn Sie den Vorgang "vervollständigen" oder zurückkehren, kann die Reihenfolge der Todos zwischen diesen Operationen unterscheiden. Um dies konsistent zu halten, holen wir alle todo -Elemente in derselben Reihenfolge.
Ändern Sie in lib/app/todo.ex list_items/0 in Folgendes.
def list_items do
query =
from (
i in Item ,
select: i ,
order_by: [ asc: i . id ]
)
Repo . all ( query )
end Indem wir die todo abrufen und sie bestellen, garantieren wir, dass die UX konsistent bleibt!
Das letzte Stück Funktionalität , das wir unserer Benutzeroberfläche hinzufügen müssen, ist die Fähigkeit, den Text eines Elements zu bearbeiten .
Am Ende dieses Schritts haben Sie eine Inline-Bearbeitung:

Der Grund , dass zwei Klicks zum Bearbeiten eines Elements erforderlich sind, ist, dass Personen beim Scrollen nicht versehentlich einen Artikel bearbeiten. Sie müssen also absichtlich zweimal klicken/tippen, um zu bearbeiten.
In der tOdomVC-Spezifikation wird dies erreicht, indem ein Ereignishörer für das Doppelklick-Ereignis erstellt und das <label> -Element durch ein <input> ersetzt wird. Wir versuchen, JavaScript in unserer serverseitigen Phoenix-App ( vorerst ) zu vermeiden , daher möchten wir einen alternativen Ansatz verwenden. Zum Glück können wir das Doppel-Klick-Ereignis mit nur HTML und CSS simulieren. Siehe: https://css-tricks.com/double klick-in- css
Hinweis : Die CSS-Implementierung ist kein echtes Doppelklick. Eine genauere Beschreibung wäre "zwei Klicks", da die beiden Klicks mit einer willkürlichen Verzögerung auftreten können. IE Erster Klick, gefolgt von 10 Sekundenwartungen und zweiter Klick hat den gleichen Effekt wie zwei Klicks in schneller Folge. Wenn Sie ein tatsächliches Doppelklick implementieren möchten
Lass uns damit weitermachen! Öffnen Sie die lib/app_web/controllers/item_html/index.html.heex -Datei und suchen Sie die Zeile:
< % = new ( Map . put ( assigns , :action , ~p " /items/new " ) ) % >Ersetzen Sie es durch:
< % = 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 % > Hier prüfen wir, ob wir einen Artikel bearbeiten und einen Link anstelle des Formulars rendern. Wir tun dies, um mehrere Formulare auf der Seite zu haben. Wenn wir keinen Artikel bearbeiten, rendern Sie den new.html.heex wie zuvor. Wenn der Benutzer ein Element bearbeitet, kann er "aus dem Bearbeitungsmodus herauskommen", indem er auf den wiedergegebenen Link klickt.
ZB: lib/app_web/controllers/item_html/index.html.heex#L30-L38
Suchen Sie als nächstes in der Datei index.html.eex die Zeile:
< %= for item < - @items do % > Ersetzen Sie das gesamte <li> Tag durch den folgenden Code.
< 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> ZB: lib/app_web/controllers/item_html/index.html.heex#L46-L79
Wir haben hier ein paar Dinge getan. Wir haben die Schaltfläche Umschaltungen außerhalb des <div class="view> tag. Zusätzlich haben wir den Text mit einem if else Blockanweisungen geändert.
Wenn der Benutzer nicht bearbeitet, wird ein Link ( <a> ) gerendert, der beim Klicken dem Benutzer den "Bearbeiten" -Modus eingeben kann. Wenn der Benutzer hingegen bearbeitet , wird die Datei edit.html.heex gerendert.
Apropos, lassen Sie uns edit.html.heex bearbeiten, damit wir das erwarten, was wir wollen: Ein Textfeld, das nach der Enter das überweisende ToDo -Element bearbeitet.
< .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 zur Bearbeitung aktualisieren Um den CSS-Doppel-Klick-Effekt für den Eingeben von edit zu aktivieren, müssen wir unseren assets/css/app.scss Datei die folgenden CSS hinzufügen:
. 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 ;
} ZB: assets/css/app.css#L13-L32
Da sich unser Markup etwas von der ToomVC -Markup unterscheidet, müssen wir ein bisschen mehr CSS hinzufügen, um die Benutzeroberfläche konsistent zu halten:
. 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 */
} So sollte Ihre Datei app.scss am Ende dieses Schritts aussehen: assets/css/app.css#L34-L71
ItemController.edit/2 Funktion Um die Inline-Bearbeitung zu aktivieren, müssen wir die Funktion edit/2 ändern. Öffnen Sie die lib/app_web/controllers/item_controller.ex -Datei und ersetzen Sie die Funktion edit/2 durch folgende:
def edit ( conn , params ) do
index ( conn , params )
end Angesichts der Tatsache, dass wir unsere index/2 -Funktion bitten, die Bearbeitung zu verarbeiten, müssen wir index/2 aktualisieren:
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 Schließlich müssen wir die Einreichung von Formular zur Aktualisierung eines Elements (das in edit.html.heex ) verarbeiten. Wenn wir Enter drücken, wird der update/2 -Handler in lib/app_web/controllers/item_controller.ex aufgerufen. Wir möchten auf derselben Seite bleiben, nachdem wir den Artikel aktualisiert haben.
Ändern Sie es also, damit es so aussieht.
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 Ihre item_controller.ex -Datei sollte jetzt so aussehen: lib/app_web/controllers/item_controller.ex
ItemControllerTestBei unserer Suche nach einer einzigen Seiten -App haben wir ein paar Tests gebrochen! Das ist in Ordnung. Sie sind leicht zu reparieren.
Öffnen Sie die Datei test/app_web/controllers/item_controller_test.exs und suchen Sie den Test mit dem folgenden Text.
test "renders form for editing chosen item"
Und ändern Sie es so, dass es wie folgt aussieht.
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 Wenn wir den "Timer -Modus bearbeiten" eingeben, <a> wir einen Link zur Rückkehr zu /items , wie wir zuvor implementiert haben. Dieses Tag enthält den Text "Klicken Sie hier, um einen neuen Element zu erstellen".
test/app_web/controllers/item_controller_test.exs#L37-L39
Suchen Sie als Nächstes den Test mit der folgenden Beschreibung:
describe "update item"Aktualisieren Sie den Block auf das folgende Code.
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 EG: test/app_web/controllers/item_controller_test.exs#L67-L80
Wir haben die Pfade aktualisiert, auf die die Anwendung nach der Aktualisierung eines Elements ausgeleitet wird. Da wir eine einseitige Anwendung erstellen, bezieht sich dieser Pfad auf den /items/ url-Pfad.
Wenn Sie die Tests jetzt durchführen, sollten sie erneut bestehen:
mix test
23:08:01.785 [info] Already up
...........................
Finished in 0.5 seconds
27 tests, 0 failures
Randomized with seed 956565
index.html Nachdem wir die Funktionen toggle und edit können, können wir endlich das Standard -Layout von Phoenix (Tabelle) aus dem lib/app_web/controllers/item_html/index.html.heex -Vorlage entfernen.

Öffnen Sie die lib/app_web/controllers/item_html/index.html.eex -Datei und entfernen Sie den gesamten Code vor der Zeile:
< section class =" todoapp " > EG: lib/app_web/controllers/item_html/index.html.heex
Ihre App sollte jetzt so aussehen: 
Leider haben wir durch das Entfernen des Standardlayouts die Tests "gebrochen".
Öffnen Sie die Datei test/app_web/controllers/item_controller_test.exs und suchen Sie den Test mit der folgenden Beschreibung:
test "lists all items"Aktualisieren Sie die Behauptung von:
assert html_response ( conn , 200 ) =~ "Listing Items"Zu:
assert html_response ( conn , 200 ) =~ "todos" EG: test/app_web/controllers/item_controller_test.exs#L14
Nachdem die Funktionalität der Kern- (Erstellen, Bearbeiten/Aktualisieren, Löschen) funktioniert, können wir die endgültigen UI -Verbesserungen hinzufügen. In diesem Schritt werden wir die Fußzeile Navigation/Filterung hinzufügen.

Die "All" -Ansicht ist die Standardeinstellung. Das "Aktiv" ist alle Elemente mit status==0 . "Fertig" ist alle Elemente mit status==1 .
/:filterFügen wir vor dem Start einen Unit -Test hinzu. Wir möchten gefilterte Elemente gemäß dem ausgewählten Filter anzeigen.
Öffnen Sie test/app_web/controllers/item_controller_test.exs und suchen Sie describe "index" do . Fügen Sie in diesem Block den folgenden Test hinzu. Es prüft, ob das Element ordnungsgemäß angezeigt wird, wenn der Filter geändert wird.
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 EG: test/app_web/controllers/item_controller_test.exs#L21-L32
Öffnen Sie die lib/app_web/router.ex und fügen Sie die folgende Route hinzu:
get "/items/filter/:filter" , ItemController , :index EG: /lib/app_web/router.ex#L23
index/2 um filter an die Anzeige/Vorlage zu senden Öffnen Sie die lib/app_web/controllers/item_controller.ex -Datei und suchen Sie die index/2 -Funktion. Ersetzen Sie die Aufruf von render/3 am Ende von index/2 durch Folgendes:
render ( conn , "index.html" ,
items: items ,
changeset: changeset ,
editing: item ,
filter: Map . get ( params , "filter" , "all" )
) EG: lib/app_web/controllers/item_controller.ex#L17-L22
Map.get(params, "filter", "all") legt den Standardwert unseres filter auf "alle" fest, wenn index.html gerendert wird, zeigen Sie "alle" Elemente an.
filter/2 -Ansichtsfunktion erstellen Um die Elemente nach ihrem Status zu filtern, müssen wir eine neue Funktion erstellen.
Öffnen Sie die lib/app_web/controllers/item_html.ex -Datei und erstellen Sie die Funktion der filter/2 wie folgt:
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 EG: lib/app_web/controllers/item_html.ex#L19-L26
Auf diese Weise können wir die Elemente im nächsten Schritt filtern.
index.html -Vorlage Verwenden Sie die Funktion filter/2 um die angezeigten Elemente zu filtern. Öffnen Sie die lib/app_web/controllers/item_html/index.html.heex -Datei und suchen Sie die for die Loop -Zeile:
< % = for item <- @ items do % >Ersetzen Sie es durch:
< % = for item <- filter ( @ items , @ filter ) do % > lib/app_web/controllers/item_html/index.html.heex#L18
Dadurch wird die filter/2 -Funktion aufgerufen, die wir im vorherigen Schritt in der Liste der @items und der ausgewählten @filter definiert haben.
Suchen Sie als Nächstes die <footer> und ersetzen Sie den Inhalt der <ul class="filters"> durch den folgenden Code:
< 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 > Wir addieren die selected Klasse bedingt entsprechend dem Wert @filter zugewiesen.
EG: /lib/app_web/controllers/item_html/index.html.heex#L62-L98
Am Ende dieses Schritts haben Sie einen voll funktionsfähigen Fußzellfilter:

Wir können diese Funktion schnell abdecken, die wir mit einem kleinen Einheiten -Test hinzugefügt haben. Öffnen Sie test/app_web/controllers/item_html_test.exs und fügen Sie Folgendes hinzu.
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
endUnd Sie sollten mit dieser Funktion fertig sein? Super Job!
Wir sind fast mit unserer Phoenix -Implementierung von todomvc fertig. Das Letzte, was zu implementieren ist, ist "klar abgeschlossen".
Öffnen Sie Ihre lib/app_web/router.ex -Datei und fügen Sie die folgende Route hinzu:
get "/items/clear" , ItemController , :clear_completed Ihr scope "/" sollte jetzt wie Folgendes aussehen:
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 In der Datei lib/app_web/controllers/item_controller.ex den folgenden Code hinzufügen:
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 EG: lib/app_web/controllers/item_controller.ex#L87-L93
Dadurch wird die Funktion Handy update_all/3 verwendet, um alle Elemente zu aktualisieren, die der query entsprechen. In unserem Fall suchen wir nach allen items , die zu person_id==0 gehören und status==1 haben.
Wir löschen die Elemente nicht, sondern aktualisieren ihren Status auf 2 was für die Zwecke unseres Beispiels bedeutet, dass sie "archiviert" sind.
Hinweis : Dies ist eine nützliche Anleitung zu
update_all: https://adamdelong.com/bulk-update-ecto
Schließlich in der lib/app_web/controllers/item_html/index.html.eex scrollen Sie zum Ende der Datei und ersetzen Sie die Zeile:
< button class = "clear-completed" style = "display: block;" >
Clear completed
< / button >Mit:
< a class = "clear-completed" href = "/items/clear" >
Clear completed
[ < % = Enum . count ( filter ( @ items , "completed" ) ) % > ]
< / a > ZB: lib/app_web/controllers/item_html/index.html.heex#L104-L107
Das Letzte, was wir tun müssen, ist, die filter/2 -Funktion in lib/app_web/controllers/item_html.ex zu aktualisieren. Da status = 2 jetzt einen archivierten Zustand betrifft, möchten wir alles zurückgeben, was nicht archiviert ist.
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?
Und das war's! Now when you deploy your server rendered Phoenix App, it will feel like an SPA! Try the Fly.io demo again: phxtodo.fly.dev Feel that buttery-smooth page transition.
Currently, our application occurs in the same page. However, there is a route that we don't use and is also aesthetically incompatible with the rest of our app.

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

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