Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/dwyl/phoenix-todo-list-tutorial
✅ Complete beginners tutorial building a todo list from scratch in Phoenix 1.7 (latest)
https://github.com/dwyl/phoenix-todo-list-tutorial
beginner beginner-friendly elixir learn learn-to-code phoenix phoenix-framework todo todomvc tutorial
Last synced: 3 days ago
JSON representation
✅ Complete beginners tutorial building a todo list from scratch in Phoenix 1.7 (latest)
- Host: GitHub
- URL: https://github.com/dwyl/phoenix-todo-list-tutorial
- Owner: dwyl
- License: gpl-2.0
- Created: 2015-06-10T21:20:36.000Z (over 9 years ago)
- Default Branch: main
- Last Pushed: 2024-10-21T17:15:22.000Z (13 days ago)
- Last Synced: 2024-10-22T06:59:19.857Z (13 days ago)
- Topics: beginner, beginner-friendly, elixir, learn, learn-to-code, phoenix, phoenix-framework, todo, todomvc, tutorial
- Language: Elixir
- Homepage: https://phxtodo.fly.dev/
- Size: 12.2 MB
- Stars: 195
- Watchers: 14
- Forks: 14
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
- freaking_awesome_elixir - Elixir - A complete beginners step-by-step tutorial for building a Todo List from scratch in Phoenix `1.5.3`. (Examples and funny stuff)
- fucking-awesome-elixir - phoenix-todo-list-tutorial - A complete beginners step-by-step tutorial for building a Todo List from scratch in Phoenix `1.5.3`. (Examples and funny stuff)
- awesome-elixir - phoenix-todo-list-tutorial - A complete beginners step-by-step tutorial for building a Todo List from scratch in Phoenix `1.5.3`. (Examples and funny stuff)
README
# Phoenix Todo List Tutorial
A complete beginners step-by-step tutorial
for building a Todo List in Phoenix.
100% functional. 0% JavaScript.
Just `HTML`, `CSS` and `Elixir`.
Fast and maintainable.![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/dwyl/phoenix-todo-list-tutorial/ci.yml?label=build&style=flat-square&branch=main)
[![codecov.io](https://img.shields.io/codecov/c/github/dwyl/phoenix-todo-list-tutorial/master.svg?style=flat-square)](http://codecov.io/github/dwyl/phoenix-todo-list-tutorial?branch=master)
[![HitCount](http://hits.dwyl.com/dwyl/phoenix-todo-list-tutorial.svg)](http://hits.dwyl.com/dwyl/phoenix-todo-list-tutorial)
[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat-square)](https://github.com/dwyl/phoenix-todo-list-tutorial/issues)
## Why? 🤷
Todo lists are familiar to most people;
we make lists all the time.
_Building_ a Todo list from scratch
is a great way to learn `Elixir`/`Phoenix`
because the **UI/UX** is **simple**,
so we can focus on implementation.For the team
[**`@dwyl`**](https://github.com/dwyl)
this app/tutorial
is a showcase of how server side rendering
(_with client side progressive enhancement_)
can provide a excellent balance between
developer effectiveness (_shipping features fast_),
UX and _accessibility_.
The server rendered pages take less than 5ms to respond
so the UX is _fast_.
On Fly.io:
[phxtodo.fly.dev](https://phxtodo.fly.dev/)
round-trip response times are sub 200ms for all interactions,
so it _feels_ like a client-side rendered App.
## What? 💭
A Todo list tutorial
that shows a complete beginner
how to build an app in Elixir/Phoenix
from scratch.### Try it on Fly.io: [phxtodo.fly.dev](https://phxtodo.fly.dev)
Try the Fly.io version.
Add a few items to the list and test the functionality.![phx-todo-list-example](https://user-images.githubusercontent.com/194400/208828566-c6986ac4-17b7-4d8d-9ff0-71b6136b8ebc.gif)
Even with a full HTTP round-trip for each interaction,
the response time is _fast_.
Pay attention to how Chrome|Firefox|Safari
waits for the response from the server before re-rendering the page.
The old full page refresh of yesteryear is _gone_.
Modern browsers intelligently render just the changes!
So the UX approximates "native"!
Seriously, try the Fly.io app on your Phone and see!### TodoMVC
In this tutorial
we are using the
[TodoMVC](https://github.com/dwyl/javascript-todo-list-tutorial#todomvc)
CSS to simplify our UI.
This has several advantages
the biggest being _minimizing_ how much CSS we have to write!
It also means we have a guide to which _features_
need to be implemented to achieve full functionality.> **Note**: we _love_ `CSS` for its incredible power/flexibility,
but we know that not everyone like it.
see: [learn-tachyons#why](https://github.com/dwyl/learn-tachyons#why)
The _last_ thing we want is to waste tons of time
with `CSS` in a `Phoenix` tutorial!
## Who? 👤
This tutorial is for
anyone who is learning to Elixir/Phoenix.
No prior experience with Phoenix is assumed/expected.
We have included _all_ the steps required to build the app.> If you get stuck on any step,
please open an
[issue](https://github.com/dwyl/phoenix-todo-list-tutorial/issues)
on GitHub where we are happy to help you get unstuck!
If you feel that any line of code can use a bit more explanation/clarity,
please don't hesitate to _inform_ us!
We _know_ what it's like to be a beginner,
it can be _frustrating_ when something does not make sense!
Asking questions on GitHub
helps _everyone_ to learn!Please give us feedback! 🙏
Star the repo if you found it helpful. ⭐
## _How_? 👩💻
### Before You Start! 💡
_Before_ you attempt to _build_ the Todo List,
make sure you have everything you need installed on you computer.
See:
[prerequisites](https://github.com/dwyl/phoenix-chat-example#0-pre-requisites-before-you-start)Once you have confirmed that you have Phoenix & PostgreSQL installed,
try running the _finished_ App.### 0. Run The _Finished_ App on Your `localhost` 💻
_Before_ you start building your own version of the Todo List App,
run the _finished_ version on your `localhost`
to confirm that it works.Clone the project from GitHub:
```sh
git clone [email protected]:dwyl/phoenix-todo-list-tutorial.git && cd phoenix-todo-list-tutorial
```Install dependencies and setup the database:
```sh
mix setup
```Start the Phoenix server:
```sh
mix phx.server
```Visit
[`localhost:4000`](http://localhost:4000)
in your web browser.You should see:
![phoenix-todo-list-on-localhost](https://user-images.githubusercontent.com/194400/83285190-bed5a580-a1d5-11ea-9154-80684cf9cc0e.gif)
Now that you have the _finished_ example app
running on your `localhost`,
let's build it from scratch
and understand all the steps.#### Auth [Optional]
When running the _finished_ example app on `localhost`,
if you want try the **`login` button**,
you will need to get an `AUTH_API_KEY`. [1 minute]
See:
[Get your `AUTH_API_KEY`](https://github.com/dwyl/auth_plug#2-get-your-auth_api_key-)### _Build_ it!
If you ran the finished app on your `localhost`
(_and you really should!_),
you will need to change up a directory before starting the tutorial:```
cd ..
```Now you are ready to _build_!
### 1. Create a New Phoenix Project 🆕
In your terminal,
create a new Phoenix app
using the following
[`mix`](https://elixir-lang.org/getting-started/mix-otp/introduction-to-mix.html)
command:```sh
mix phx.new app --no-dashboard --no-gettext --no-mailer
```When prompted to install dependencies,
type Y followed by Enter.> **Note**: those **flags** after the `app` name
> are just to avoid creating files we don't _need_
> for this simple example.
> See:
> [hexdocs.pm/phoenix/Mix.Tasks.Phx.New](https://hexdocs.pm/phoenix/Mix.Tasks.Phx.New.html)Change into the newly created `app` directory (`cd app`)
and ensure you have everything you need:```sh
mix setup
```Start the Phoenix server:
```sh
mix phx.server
```Now you can visit
[`localhost:4000`](http://localhost:4000)
in your web browser.
You should see something similar to:![welcome-to-phoenix](https://user-images.githubusercontent.com/17494745/206515843-2b4da196-f039-4caf-a4d2-fc316eabb2c5.png)
Shut down the Phoenix server ctrl+C.
Run the tests to ensure everything works as expected:
```sh
mix test
```You should see:
```sh
Compiling 16 files (.ex)
Generated app app17:49:40.111 [info] Already up
...Finished in 0.04 seconds
3 tests, 0 failures
```Having established that the Phoenix App works as expected,
let's move on to creating some files!
### 2. Create `items` Schema
In creating a basic Todo List we only need one schema: `items`.
Later we can add separate lists and tags to organise/categorise
our `items` but for now this is all we need.Run the following [generator](https://hexdocs.pm/phoenix/Mix.Tasks.Phx.Gen.Html.html)
command to create the items table:```sh
mix phx.gen.html Todo Item items text:string person_id:integer status:integer
```Strictly speaking we only _need_ the `text` and `status` fields,
but since we know we want to associate items with people
(_later in the tutorial),
we are adding the field _now_.You will see the following output:
```
* 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.exAdd 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
```That created a _bunch_ of files!
Some of which we don't strictly _need_.
We could _manually_ create _only_ the files we _need_,
but this is the "official" way of creating a CRUD App in Phoenix,
so we are using it for speed.> **Note**: Phoenix
[Contexts](https://hexdocs.pm/phoenix/contexts.html)
denoted in this example as `Todo`,
are "_dedicated modules that expose and group related functionality_."
We feel they _unnecessarily complicate_ basic Phoenix Apps
with layers of "interface" and we _really_ wish we could
[avoid](https://github.com/phoenixframework/phoenix/issues/3832) them.
But given that they are baked into the generators,
and the _creator_ of the framework
[_likes_](https://youtu.be/tMO28ar0lW8?t=376) them,
we have a choice: either get on board with Contexts
or _manually_ create all the files in our Phoenix projects.
Generators are a _much_ faster way to build!
_Embrace_ them,
even if you end up having to `delete` a few
unused files along the way!We are _not_ going to explain each of these files
at this stage in the tutorial because
it's _easier_ to understand the files
as you are _building_ the App!
The purpose of each file will become clear
as you progress through editing them.
### 2.1 Add the `/items` Resources to `router.ex`
Follow the instructions noted by the generator to
add the `resources "/items", ItemController` to the `router.ex`.Open the `lib/app_web/router.ex` file
and locate the line: `scope "/", AppWeb do`.
Add the line to the end of the block.
e.g:```elixir
scope "/", AppWeb do
pipe_through :browserget "/", PageController, :index
resources "/items", ItemController # this is the new line
end
```Your `router.ex` file should look like this:
[`router.ex#L20`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/b158abd7f0c3fbc462a4230f07b5e5e79723a258/app/lib/app_web/router.ex#L17-L22)Now, as the terminal suggested,
run `mix ecto.migrate`.
This will finish setting up the
database tables and run the
necessary migrations so
everything works properly!### 2.2 _Run_ The App!
At this point we _already_ have a functional Todo List
(_if we were willing to use the default Phoenix UI_).
Try running the app on your `localhost`:
Run the generated migrations with `mix ecto.migrate` then the server with:
```
mix phx.server
```Visit: http://localhost:4000/items/new
and input some data.![todo-list-phoenix-default-ui](https://user-images.githubusercontent.com/17494745/205615474-1eef8b42-86aa-4a0e-90c3-376221570255.png)
Click the "Save Item" button
and you will be redirected to the "show" page:
http://localhost:4000/items/1![todo-list-phoenix-default-ui-show-item](https://user-images.githubusercontent.com/17494745/205615709-922db20e-245d-4af0-a5e3-2bbaa29771b4.png)
This is not an attractive User Experience (UX),
but it _works_!
Here is a _list_ of items - a "Todo List".
You can visit this by clicking
the `Back to items` button or by
accessing the following URL
http://localhost:4000/items.![todo-list-phoenix-default-ui-show-items-list](https://user-images.githubusercontent.com/17494745/205616098-a514d2bb-af28-477a-b80a-6641c5b391a9.png)
Let's improve the UX by using the TodoMVC `HTML` and `CSS`!
### 3. Create the TodoMVC UI/UX
To recreate the TodoMVC UI/UX,
let's borrow the `HTML` code directly from the example.Visit: http://todomvc.com/examples/vanillajs
add a couple of items to the list.
Then, inspect the source
using your browser's
[Dev Tools](https://developers.google.com/web/tools/chrome-devtools/open).
e.g:![todomvc-view-source](https://user-images.githubusercontent.com/194400/82698634-0b176780-9c63-11ea-9e1d-7aa6e2328766.png)
Right-click on the source you want
(e.g: ``)
and select "Edit as HTML":![edit-as-html](https://user-images.githubusercontent.com/194400/82721624-b8679b00-9cb6-11ea-8d3f-2405f2bd301f.png)
Once the `HTML` for the `` is editable,
select it and copy it.![todomvc-html-editable-copy](https://user-images.githubusercontent.com/194400/82721711-b05c2b00-9cb7-11ea-85c2-083d2981440d.png)
The `HTML` code is:
```html
todos
Mark all as complete
-
Learn how to build a Todo list in Phoenix
-
Completed item
1 item left
Clear completed
```
Let's convert this `HTML` to an Embedded Elixir
([`EEx`](https://hexdocs.pm/eex/EEx.html)) template.
> **Note**: the _reason_ that we are copying this `HTML`
from the browser's Elements inspector
instead of _directly_ from the source
on GitHub:
[`examples/vanillajs/index.html`](https://github.com/tastejs/todomvc/blob/c50cc922495fd76cb44844e3b1cd77e35a5d6be1/examples/vanillajs/index.html#L18)
is that this is a "single page app",
so the `
only gets populated in the browser.
Copying it from the browser Dev Tools
is the easiest way to get the _complete_ `HTML`.
### 3.1 Paste the HTML into `index.html.eex`
Open the `lib/app_web/controllers/item_html/index.html.eex` file
and scroll to the bottom.
Then (_without removing the code that is already there_)
paste the `HTML` code we sourced from TodoMVC.
> e.g:
[`/lib/app_web/controllers/item_html/index.html.eex#L27-L73`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/8bcc3239d975f5b706514d1a88ea47ca57e5239a/lib/app_web/controllers/item_html/index.html.heex#L27-L73)
If you attempt to run the app now
and visit
[http://localhost:4000/items/](http://localhost:4000/items/)
You will see this (_without the TodoMVC `CSS`_):
![before-adding-css](https://user-images.githubusercontent.com/17494745/205656501-b3170bc4-c8a7-403f-9db9-2823c839f61e.png)
That's obviously not what we want,
so let's get the TodoMVC `CSS`
and save it in our project!
### 3.2 Save the TodoMVC CSS to `/assets/css`
Visit
http://todomvc.com/examples/vanillajs/node_modules/todomvc-app-css/index.css
and save the file to `/assets/css/todomvc-app.css`.
e.g:
[`/assets/css/todomvc-app.css`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/a341c2cbb5f1ad91897293f058a5d7bee6c1e1cc/assets/css/todomvc-app.css)
### 3.3 Import the `todomvc-app.css` in `app.scss`
Open the `assets/css/app.scss` file and replace it with the following:
```css
/* This file is for your main application css. */
/* @import "./phoenix.css"; */
@import "./todomvc-app.css";
```
e.g:
[`/assets/css/app.scss#L4`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/8bcc3239d975f5b706514d1a88ea47ca57e5239a/assets/css/app.css#L4)
### 3.4 _Simplify_ The Layout Template
Open your `lib/app_web/components/layouts/app.html.heex` file
and replace the contents with the following code:
```html
Phoenix Todo List
<%= @inner_content %>
```
> Before:
[`lib/app_web/components/layouts/app.html.eex`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/bddacda93ecd892fe0907210bab335e6b6e5e489/lib/app_web/templates/layout/app.html.eex)
> After:
[`lib/app_web/components/layouts/app.html.heex`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/8bcc3239d975f5b706514d1a88ea47ca57e5239a/lib/app_web/components/layouts/app.html.heex)
`<%= @inner_content %>` is where the Todo App will be rendered.
> **Note**: the `` tag is included out of convention.
However, we won't be writing _any_ `JavaScript` in this tutorial.
We will achieve 100% feature parity with TodoMVC,
without writing a line of `JS`.
We don't "hate" `JS`,
in fact we have a "sister" tutorial
that builds the _same_ App in `JS`:
[dwyl/javascript-todo-list-tutorial](https://github.com/dwyl/javascript-todo-list-tutorial)
We just want to _remind_ you
that you don't _need_ any `JS`
to build a fully functional web application
with great UX!
With the layout template saved,
the TodoMVC CSS file saved to `/assets/css/todomvc-app.css`
and the `todomvc-app.css` imported in `app.scss`,
your `/items` page should now look like this:
![items-with-todomvc-css](https://user-images.githubusercontent.com/17494745/205660411-7b199ec5-289e-4863-8a67-9c22e9dc78dd.png)
So our Todo List is starting to look like TodoMVC,
but it's still just a dummy list.
### 4. Render _Real_ Data in the TodoMVC Layout
In order to render out `item` data
in the TodoMVC template,
we are going to need to add
a few functions.
When we created the project
and generated the `item` model,
a controller was created
(located in `lib/app_web/controllers/item_controller.ex`)
and a component/view as well
(located in `lib/app_web/controllers/item_html.ex`).
This [**Component/View**](https://hexdocs.pm/phoenix/1.7.0-rc.0/components.html)
is what effectively
controls the rendering of the
contents inside the
`lib/app_web/controllers/item_html`
directory that we tinkered with prior.
We know that we need make changes to the UI,
so we are going to add a few functions in this component
(which is akin to the *View* part of the MVC paradigm).
This is our first chance to do a bit of Test Driven Development (TDD). <br />
Create a new file with the path `test/app_web/controllers/item_html_test.exs`.
Type the following code into the file:
```elixir
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
```
e.g:
[`/test/app_web/controllers/item_html_test.exs`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/8bcc3239d975f5b706514d1a88ea47ca57e5239a/test/app_web/controllers/item_html_test.exs)
If you attempt to run this test file:
```sh
mix test test/app_web/controllers/item_html_test.exs
```
You will see the following error (because the function does not yet exist!):
```
** (UndefinedFunctionError) function AppWeb.ItemHTML.checked/1 is undefined or private
```
Open the
`lib/app_web/controllers/item_html.ex` file
and write the functions to make the tests _pass_.
<br />
This is how we implemented the functions.
Your `item_html.ex` file
now should look like the following.
```elixir
defmodule AppWeb.ItemHTML do
use AppWeb, :html
embed_templates "item_html/*"
# add class "completed" to a list item if item.status=1
def complete(item) do
case item.status do
1 -> "completed"
_ -> "" # empty string means empty class so no style applied
end
end
end
```
Re-run the tests and they should now pass:
```sh
mix test test/app_web/controllers/item_html_test.exs
```
You should see:
```sh
....
Finished in 0.1 seconds
4 tests, 0 failures
```
Now that we have created these two view functions,
and our tests are passing,
let's _use_ them in our template!
Open the `lib/app_web/controllers/item_html/index.html.eex` file
and locate the line:
```html
<ul class="todo-list">
```
Replace the _contents_ of the `<ul>` with the following:
```html
<%= 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 %>
```
e.g:
[`lib/app_web/controllers/item_html/index.html.heex#L43-L53`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/3c04ee39df621cac200b4d3b45ad4045e67e388b/lib/app_web/controllers/item_html/index.html.heex#L36-L56)
With those two files saved,
if you run the app now: `mix phx.server`
and visit http://localhost:4000/items. <br />
You will see the _real_ `items` you created in step 2.2 above:
![todo-list-real-items](https://user-images.githubusercontent.com/17494745/205710983-e079f021-c91d-4be8-9a5e-0a6b0bc85fb8.png)
Now that we have our items rendering in the TodoMVC layout,
let's work on creating new items in the "single page app" style.
### 5. In-line the New Item Creation Form
At present our "New Item" form is available at:
http://localhost:4000/items/new
(_as noted in step 2 above_)
We want the person to be able to create a new item
without having to navigate to a different page.
In order to achieve that goal,
we will include the
`lib/app_web/controllers/item_html/new.html.heex`
template (_partial_)
inside the
`lib/app_web/controllers/item_html/index.html.heex`
template. e.g:
Before we can do that, we need to tidy up the `new.html.heex`
template to remove the fields we don't _need_.
Let's open `lib/app_web/controllers/item_html/new.html.heex`
and simplify it to just the essential field `:text`:
```elixir
<.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>
```
> Before:
[`/lib/app_web/controllers/item_html/new.html.heex`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/b158abd7f0c3fbc462a4230f07b5e5e79723a258/app/lib/app_web/controllers/item_html/new.html.heex) <br />
> After:
[`/lib/app_web/controllers/item_html/new.html.heex`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/8bcc3239d975f5b706514d1a88ea47ca57e5239a/lib/app_web/controllers/item_html/new.html.heex)
We need to additionally
change the style of the `<.input>` tag.
With Phoenix, inside the
`lib/app_web/components/core_components.ex` file,
the styles are defined for pre-built components
(which is the case with `<.input>`).
To change this so it uses the same style as TodoMVC,
locate the following line.
```elixir
def input(assigns) do
```
Change the class attribute
with the `new-todo` class.
This function should look like the following.
```elixir
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
```
We also need to change the `actions` styles
inside the `simple_form`.
In the same file, search for `def simple_form(assigns) do`
and change it so it looks like so:
```elixir
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
```
If you run the Phoenix App now and visit
[http://localhost:4000/items/new](http://localhost:4000/items/new)
you will see the _single_ `:text` input field and no "Save" button:
![new-item-single-text-field-no-save-button](https://user-images.githubusercontent.com/17494745/205731473-4a9ce3b2-c902-4b66-9bdc-e64165f22841.png)
Don't worry, you can still submit the form with <kbd>Enter</kbd> (Return) key.
However if you attempt to submit the form now,
it won't work because we removed two of the fields required by the `changeset`!
Let's fix that.
### 5.1 Update the `items` Schema to Set `default` Values
Given that we have removed two of the fields (`:person_id` and `:status`)
from the `new.html.eex`,
we need to ensure there are default values for these in
the schema.
Open the `lib/app/todo/item.ex` file
and replace the contents with the following:
```elixir
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
```
Here we are updating the "items" `schema`
to set a default value of `0`
for both `person_id` and `status`.
And in the `changeset/2` we are removing the _requirement_
for `person_id` and `status`.
That way our new `item` form
can be submitted with _just_ the `text` field.
e.g:
[`/lib/app/todo/item.ex#L6-L7`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/cc287975eef5ca8b738f49723fe8a03d9da52a19/lib/app/todo/item.ex#L6-L7)
Now that we have `default` values for `person_id` and `status`
if you submit the `/items/new` form,
it will succeed.
### 5.2 Update `index/2` in `ItemController`
In order to in-line the new item form (`new.html.eex`)
in the `index.html.eex` template,
we need to update the `AppWeb.ItemController.index/2`
to include a Changeset.
Open the `lib/app_web/controllers/item_controller.ex` file
and update the `index/2` function to the following:
```elixir
def index(conn, _params) do
items = Todo.list_items()
changeset = Todo.change_item(%Item{})
render(conn, "index.html", items: items, changeset: changeset)
end
```
Before:
[`/lib/app_web/controllers/item_controller.ex`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/031df4076fc4ff84fd719a3a66c6dd2495268a50/lib/app_web/controllers/item_controller.ex) <br />
After:
[`/lib/app_web/controllers/item_controller.ex#L9-L10`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/aed0b2c037ea0cdf5461b2287fc4b63d61cd7b14/lib/app_web/controllers/item_controller.ex#L9-L10)
You will not _see_ any change in the UI or tests after this step.
Just move on to 5.3 where the "aha" moment happens.
### 5.3 Render The `new.html.eex` inside `index.html.eex`
Now that we have done all the preparation work,
the next step is to render the `new.html.eex` (_partial_)
inside `index.html.eex` template.
Open the `lib/app_web/controllers/item_html/index.html.heex`
file and locate the line:
```html
<input class="new-todo" placeholder="What needs to be done?" autofocus="">
```
Replace it with this:
```elixir
<%= new(Map.put(assigns, :action, ~p"/items/new")) %>
```
Let's break down what we just did.
We are **embedding** the `new.html.heex` partial
inside the `index.html.heex` file.
We are doing this by calling the
`new/2` function inside `item_controller.ex`.
This function *pertains* to the page in the URL `items/new`
and renders the `new.html.heex` file.
Hence why we call this function to successfully embed :smile:.
Before:
[`/lib/app_web/controllers/item_html/index.html.heex#L36`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/031df4076fc4ff84fd719a3a66c6dd2495268a50/lib/app_web/templates/item/index.html.eex#L36) <br />
After:
[`/lib/app_web/controllers/item_html/index.html.heex#L36`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/3c04ee39df621cac200b4d3b45ad4045e67e388b/lib/app_web/controllers/item_html/index.html.heex#L30)
If you run the app now and visit:
[http://localhost:4000/items](http://localhost:4000/items) <br />
You can create an item by typing your text
and submit it with the <kbd>Enter</kbd> (Return) key.
<div align="center">
![todo-list-tutorial-step-5](https://user-images.githubusercontent.com/17494745/205904251-8c369d94-f3f9-43e9-b276-4b377e38cdc4.gif)
</div>
Redirecting to the "show" template
is "OK", but we can do better UX by
redirecting to back to the `index.html` template.
Thankfully this is as easy as updating a single line in the code.
### 5.4 Update the `redirect` in `create/2`
Open the `lib/app_web/controllers/item_controller.ex` file
and locate the `create` function.
_Specifically_ the line:
```elixir
|> redirect(to: ~p"/items/#{item}")
```
Update the line to:
```elixir
|> redirect(to: ~p"/items/")
```
Before:
[`/lib/app_web/controllers/item_controller.ex#L22`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/8bcc3239d975f5b706514d1a88ea47ca57e5239a/lib/app_web/controllers/item_controller.ex#L23) <br />
After:
[`/lib/app_web/controllers/item_controller.ex#L23`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/3c04ee39df621cac200b4d3b45ad4045e67e388b/lib/app_web/controllers/item_controller.ex#L23)
Now when we create a new `item`
we are redirected to the `index.html` template:
<div align="center">
![todo-list-tutorial-redirect-to-index](https://user-images.githubusercontent.com/17494745/205917351-5ccdeeed-0015-4bc5-9e67-9bd27f92f14e.gif)
</div>
### 5.5 Update `item_controller_test.exs` to redirect to `index`
The changes we've made to the `new.html.heex` files
and the steps above have broken some of our automated tests.
We ought to fix that.
Run the tests:
```sh
mix test
```
You will see the following output:
```
Finished in 0.08 seconds (0.03s async, 0.05s sync)
23 tests, 3 failures
```
Open the `test/app_web/controllers/item_controller_test.exs` file
and locate `describe "new item"`
and `describe "create item"`.
Change these two to the following.
Replace the test:
```elixir
describe "new item" do
test "renders form", %{conn: conn} do
conn = get(conn, ~p"/items/new")
assert html_response(conn, 200) =~ "what needs to be done?"
end
end
describe "create item" do
test "redirects to show when data is valid", %{conn: conn} do
conn = post(conn, ~p"/items", item: @create_attrs)
assert %{} = redirected_params(conn)
assert redirected_to(conn) == ~p"/items/"
end
test "errors when invalid attributes are passed", %{conn: conn} do
conn = post(conn, ~p"/items", item: @invalid_attrs)
assert html_response(conn, 200) =~ "can't be blank"
end
end
```
> Updated code:
[`/test/app_web/controllers/item_controller_test.exs#L34-L55`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/0c12a6bec7aeed5562a239d0dc8eea4952250cdd/test/app_web/controllers/item_controller_test.exs#L34-L55)
If you re-run the tests `mix test` the will now all pass again.
```sh
......................
Finished in 0.2 seconds (0.09s async, 0.1s sync)
22 tests, 0 failures
```
<br />
### 6. Display Count of Items in UI
So far the main functionality of the TodoMVC UI is working,
we can create new items and they appear in our list.
In this step we are going to enhance the UI to include
the count of remaining items in the bottom left corner.
Open the `test/app_web/controllers/item_html_test.exs` file
and create the following two tests:
```elixir
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
```
e.g:
[`test/app_web/controllers/item_html_test.exs#L14-L26`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/32acb54881b8296fcd9cf39666f35b4761c54cb0/test/app_web/controllers/item_html_test.exs#L14-L26)
These tests will fail because the `ItemHTML.remaining_items/1`
function does not exist.
Make the tests _pass_ by adding the following code to
the `lib/app_web/controllers/item_html.ex` file:
```elixir
# 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
```
e.g:
[`/lib/app_web/controllers/item_html#L15-L17`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/a341c2cbb5f1ad91897293f058a5d7bee6c1e1cc/lib/app_web/controllers/item_html.ex#L15-L17)
Now that the tests are passing,
_use_ the `remaining_items/1` in the `index.html` template.
Open the `lib/app_web/controllers/item_html/index.html.eex` file
and locate the line of code:
```html
<span class="todo-count"><strong>1</strong> item left</span>
```
Replace it with this line:
```html
<span class="todo-count"><%= remaining_items(@items) %> items left</span>
```
This just invokes the `ItemHTML.remaining_items/1` function
with the List of `@items` which will return the integer count
of remaining items that have not yet been "done".
E.g:
[`/lib/app_web/controllers/item_html/index.html.eex#L60`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/32acb54881b8296fcd9cf39666f35b4761c54cb0/lib/app_web/controllers/item_html/index.html.heex#L60)
At this point the (remaining) items counter
in the bottom left of the TodoMVC UI is _working_! <br />
Add a `new` item to your list and watch the count increase:
![item-count-increases-to-2](https://user-images.githubusercontent.com/17494745/205924061-36d9bd5a-7884-4ca7-8237-669a97a73e75.gif)
That was easy enough let's try something a bit more advanced! <br />
Take a break and grab yourself a fresh glass of water,
the next section is going be _intense_!
<br />
### 7. Toggle a Todo Item's `status` to `1`
One of the core functions of a Todo List is
toggling the `status` of an `item` from `0` to `1` ("complete"). <br />
In our schema a completed `item`
has the `status` of `1`.
### 7.1 Create the Controller Tests
We are going to need two functions in our controller:
1. `toggle_status/1` toggles the status of an item
e.g: 0 to 1 and 1 to 0.
2. `toggle/2` the handler function for HTTP requests
to toggle the status of an item.
Open the `test/app_web/controllers/item_controller_test.exs` file.
We are going to make some changes here
so we can add tests to the functions we
mentioned prior.
We are going to import `App.Todo`
inside `item_controller_test.exs`
and fix create and attribute constants
to create mock items.
Make sure the beginning of the
file looks like so.
```elixir
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}
```
We are adding fixed `Item` attributes
to later be used in tests.
We are specifying `public` `Item`s
because we will later add
*authentication* to this app.
After this, locate `defp create_item()/1`
function inside the same file.
Change it so it looks like so.
```elixir
defp create_item(_) do
item = item_fixture(@create_attrs)
%{item: item}
end
```
We are going to be using this function
to create `Item` objects
to use in the tests we are going to add.
Speaking of which, let's do that!
Add the following snippet to the file.
```elixir
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
```
e.g:
[`/test/app_web/controllers/item_controller_test.exs#L64-L82`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/a341c2cbb5f1ad91897293f058a5d7bee6c1e1cc/test/app_web/controllers/item_controller_test.exs#L64-L82)
<br />
### 7.2 Create the Functions to Make Tests Pass
Open the
`lib/app_web/controllers/item_controller.ex`
file and add the following functions to it:
```elixir
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
```
e.g:
[`/lib/app_web/controllers/item_controller.ex#L64-L76`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/bbaa6cad585bff76602e7f3cea6a43b8a1f08cbb/lib/app_web/controllers/item_controller.ex#L64-L76)
The tests will still _fail_ at this point because
the route we are invoking in our test does not yet exist.
Let's fix that!
<br />
### 7.3 Create `get /items/toggle/:id` Route that Invokes `toggle/2`
Open the `lib/app_web/router.ex`
and locate the line `resources "/items", ItemController`.
Add a new line:
```elixir
get "/items/toggle/:id", ItemController, :toggle
```
e.g:
[`/lib/app_web/router.ex#L21`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/bbaa6cad585bff76602e7f3cea6a43b8a1f08cbb/lib/app_web/router.ex#L21)
Now our tests will _finally_ pass:
```sh
mix test
```
You should see:
```sh
22:39:42.231 [info] Already up
...........................
Finished in 0.5 seconds
27 tests, 0 failures
```
<br />
### 7.4 Invoke the `toggle/2` When a Checkbox is clicked in `index.html`
Now that our tests are passing,
it's time actually _use_ all this functionality we have been building
in the UI.
Open the `/lib/app_web/controllers/item_html/index.html.heex` file
and locate the line:
```html
<%= if item.status == 1 do %>
...
<% else %>
...
<% end %>
```
Replace it with the following:
```html
<%= 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 %>
```
When this link is clicked
the `get /items/toggle/:id` endpoint is invoked, <br />
that in turn triggers the `toggle/2` handler
we defined above.
> Before:
[`/lib/app_web/controllers/item_html/index.html.heex#L40`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/32acb54881b8296fcd9cf39666f35b4761c54cb0/lib/app_web/controllers/item_html/index.html.heex#L40) <br />
> After:
[`/lib/app_web/controllers/item_html/index.html.heex#L47-L57`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/3329d8f2272d01641ed74c6b10adc957821bc81f/lib/app_web/controllers/item_html/index.html.heex#L47-L57)
### 7.5 Add a `.checked` CSS to `app.scss`
Unfortunately, `<a>` tags (that are generated with `<.link>`)
cannot have a `:checked` pseudo selector,
so the default TodoMVC styles that worked on the `<input>` tag
will not work for the link.
So we need to add a couple of lines of CSS to our `app.scss`.
Open the `assets/css/app.scss` file and add the following lines to it:
```css
.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;
}
```
After saving the file you should have:
[`/assets/css/app.scss#L8`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/a341c2cbb5f1ad91897293f058a5d7bee6c1e1cc/assets/css/app.css#L8)
And when you view the app,
the Toggle functionality is working as expected:
![todo-app-toggle](https://user-images.githubusercontent.com/17494745/205961019-141d4488-3856-4c1e-b846-6ef52252c7d1.gif)
**Implementation Note**: we are very deliberately
_not_ using an `JavaScript` in this tutorial
because we are demonstrating how to do a 100% server-side rendered App.
This _always_ works even when `JS` is disabled in the browser
or the device is super old and does not have a modern web browser.
We could easily have added an `onclick` attribute to the `<input>` tag,
e.g:
```html
<input <%= checked(item) %> type="checkbox" class="toggle"
onclick="location.href='
<%= Routes.item_path(@conn, :toggle, item.id) %>';">
```
But `onclick` is `JavaScript` and we don't _need_ to resort to `JS`. <br />
The `<a>` (link) is a perfectly semantic non-js approach to toggling
`item.status`.
### 7.6 Maintaining correct order of `todo` items
If you "complete" or revert the operation,
the order of the todos might differ between
these operations.
To keep this consistent,
let's fetch all the `todo` items in the same order.
Inside `lib/app/todo.ex`,
change `list_items/0` to the following.
```elixir
def list_items do
query =
from(
i in Item,
select: i,
order_by: [asc: i.id]
)
Repo.all(query)
end
```
By fetching the `todo` items and ordering them,
we guarantee the UX stays consistent!
<br />
### 8. _Edit_ an Item!
The final piece of _functionality_
we need to add to our UI
is the ability to _edit_ an item's text.
At the end of this step you will have in-line editing working:
![phoenix-todo-item-inline-editing](https://user-images.githubusercontent.com/194400/83204049-bf712c00-a142-11ea-8560-d68cf79a4fea.gif)
### 8.1 Double-Click Item Text to Edit
The _reason_ for requiring two clicks to edit an item,
is so that people don't _accidentally_ edit an item while scrolling.
So they have to deliberately click/tap _twice_ in order to edit.
In the TodoMVC spec this is achieved
by creating an event listener for the double-click event
and replacing the `<label>` element with an `<input>`.
We are trying to _avoid_ using `JavaScript`
in our server-side rendered Phoenix App (_for now_),
so we want to use an alternative approach.
Thankfully we can simulate the double-click event
using just `HTML` and `CSS`.
see: https://css-tricks.com/double-click-in-css
(_we recommend reading that post and the Demo
to fully understand how this CSS works_!)
> **Note**: the CSS implementation is not a _true_ double-click,
a more accurate description would be "two click"
because the two clicks can occur with an arbitrary delay.
i.e. first click followed by 10sec wait and second click
will have the same effect as two clicks in quick succession.
If you want to implement true double-click,
see:
[github.com/dwyl/javascript-todo-list-tutorial#52-double-click](https://github.com/dwyl/javascript-todo-list-tutorial/tree/e6736add9df1f46035f8a9d1dbdc14c71a7cdb41#52-double-click-item-label-to-edit)
Let's get on with it!
Open the
`lib/app_web/controllers/item_html/index.html.heex`
file and locate the line:
```elixir
<%= new(Map.put(assigns, :action, ~p"/items/new")) %>
```
Replace it with:
```elixir
<%= 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 %>
```
In here, we are checking if we are editing an item,
and rendering a link instead of the form.
We do this to avoid having multiple forms on the page.
If we are _not_ editing an item,
render the `new.html.heex` as before.
With this, if the user is editing an item,
he is able to "get out of editing mode"
by clicking on the link that is rendered.
e.g:
[`lib/app_web/controllers/item_html/index.html.heex#L30-L38`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/3329d8f2272d01641ed74c6b10adc957821bc81f/lib/app_web/controllers/item_html/index.html.heex#L30-L38)
Next, still in the `index.html.eex` file,
locate the line:
```html
<%= for item <- @items do %>
```
Replace the entire `<li>` tag
with the following code.
```elixir
<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>
```
e.g:
[`lib/app_web/controllers/item_html/index.html.heex#L46-L79`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/3329d8f2272d01641ed74c6b10adc957821bc81f/lib/app_web/controllers/item_html/index.html.heex#L46-L79)
We have done a few things here.
We changed the toggle button outside the
`<div class="view>` tag.
Additionally, we have changed the text
with a `if else` block statements.
If the user is not editing,
a link (`<a>`) is rendered which,
when clicked, allows the user to enter "edit" mode.
On the other hand, if the user *is editing*,
it renders the `edit.html.heex` file.
Speaking of which, let's edit `edit.html.heex`
so it renders what we want:
a text field that, once `Enter` is pressed,
edits the referring todo item.
```html
<.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>
```
<br />
### 8.2 Update `CSS` For Editing
To enable the CSS double-click effect
to enter `edit` mode,
we need to add the following CSS
to our `assets/css/app.scss` file:
```css
.dblclick {
position: relative; /* So z-index works later, but no surprises now */
}
.dblclick + span {
position: absolute;
top: -1px; /* these negative numbers are to ensure */
left: -1px; /* that the <span> covers the <a> */
width: 103%; /* Gotta do this instead of right: 0; */
bottom: -1px;
z-index: 1;
}
.dblclick + span:active {
left: -9999px;
}
.dblclick:hover {
z-index: 2;
}
```
e.g:
[`assets/css/app.css#L13-L32`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/a341c2cbb5f1ad91897293f058a5d7bee6c1e1cc/assets/css/app.css#L13-L32)
Additionally, since our markup is _slightly_ different
to the TodoMVC markup, we need to add a bit more CSS
to keep the UI consistent:
```css
.todo-list li .toggle + div > a > label {
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
background-repeat: no-repeat;
background-position: center left;
}
.todo-list li .checked + div > a > label
{
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
background-repeat: no-repeat;
background-position: center left;
}
.toggle {
width: 10%;
z-index: 3; /* keep the toggle checkmark above the rest */
}
a.new-todo {
display: block;
text-decoration: none;
}
.todo-list .new-todo {
border: 1px #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 */
}
```
This is what your `app.scss` file should look like
at the end of this step:
[`assets/css/app.css#L34-L71`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/a341c2cbb5f1ad91897293f058a5d7bee6c1e1cc/assets/css/app.css#L34-L71)
<br />
### 8.3 Update the `ItemController.edit/2` Function
In order to enable in-line editing,
we need to modify the `edit/2` function.
Open the `lib/app_web/controllers/item_controller.ex` file
and replace the `edit/2` function with the following:
```elixir
def edit(conn, params) do
index(conn, params)
end
```
Additionally, given that we are asking our `index/2` function
to handle editing, we need to update `index/2`:
```elixir
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
```
Finally, we need to handle the form submission
to update an item (that is rendered in `edit.html.heex`).
When we press `Enter`, the `update/2` handler is called
inside `lib/app_web/controllers/item_controller.ex`.
We want to stay on the same page after updating the item.
So,change it so it looks like this.
```elixir
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
```
Your `item_controller.ex` file should now look like this:
[`lib/app_web/controllers/item_controller.ex`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/3329d8f2272d01641ed74c6b10adc957821bc81f/lib/app_web/controllers/item_controller.ex)
<br />
### 8.4 Update the Tests in `ItemControllerTest`
In our quest to build a _Single_ Page App,
we broke a few tests! That's OK.
They're easy to fix.
Open the `test/app_web/controllers/item_controller_test.exs`
file and locate the test with the following text.
`test "renders form for editing chosen item"`
and change it so it looks like the following.
```elixir
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
```
When we enter the "edit timer mode",
we create `<a>` a link to return to `/items`,
as we have previously implemented.
This tag has the "Click here to create a new item" text,
which is what we are asserting.
e.g:
[`test/app_web/controllers/item_controller_test.exs#L37-L39`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/3d5d839d6053c3f6ac5140459a4c3c010d45b195/test/app_web/controllers/item_controller_test.exs#L37-L39)
Next, locate the test with the following description:
```elixir
describe "update item"
```
Update the block to the following
piece of code.
```elixir
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
```
e.g:
[`test/app_web/controllers/item_controller_test.exs#L67-L80`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/0c12a6bec7aeed5562a239d0dc8eea4952250cdd/test/app_web/controllers/item_controller_test.exs#L67-L80)
We've updated the paths the application redirects to
after updating an item.
Since we are building a single-page application,
that path pertains to the `/items/` URL path.
If you run the tests now, they should pass again:
```
mix test
23:08:01.785 [info] Already up
...........................
Finished in 0.5 seconds
27 tests, 0 failures
Randomized with seed 956565
```
<br />
### 8.5 Remove Old Template from `index.html`
Now that we have the `toggle` and `edit` features working,
we can finally remove the default Phoenix (table) layout
from the `lib/app_web/controllers/item_html/index.html.heex` template.
<img width="872" alt="phoenix-todo-list-table-layout" src="https://user-images.githubusercontent.com/194400/83200932-54245b80-a13c-11ea-92a3-6b55fc2b2652.png">
Open the `lib/app_web/controllers/item_html/index.html.eex` file
and remove all code before the line:
```html
<section class="todoapp">
```
e.g:
[`lib/app_web/controllers/item_html/index.html.heex`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/3d5d839d6053c3f6ac5140459a4c3c010d45b195/lib/app_web/controllers/item_html/index.html.heex)
Your app should now look like this:
![phoenix-todo-app-without-default-table-layout](https://user-images.githubusercontent.com/194400/83201568-afa31900-a13d-11ea-9511-aadb5988cc23.png)
Unfortunately, by removing the default layout,
we have "broken" the tests.
Open the
`test/app_web/controllers/item_controller_test.exs`
file and locate the test
that has the following description:
```elixir
test "lists all items"
```
Update the assertion from:
```elixir
assert html_response(conn, 200) =~ "Listing Items"
```
To:
```elixir
assert html_response(conn, 200) =~ "todos"
```
e.g:
[`test/app_web/controllers/item_controller_test.exs#L14`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/3d5d839d6053c3f6ac5140459a4c3c010d45b195/test/app_web/controllers/item_controller_test.exs#L14)
<br />
### 9. Footer Navigation
Now that the core (create, edit/update, delete) functionality is working,
we can add the final UI enhancements.
In this step we are going to add the footer navigation/filtering.
<img width="581" alt="phoenix-todo-footer-navigation" src="https://user-images.githubusercontent.com/194400/83204791-96ea3180-a144-11ea-954b-499a4348ef32.png">
The "All" view is the default.
The "Active" is all the items with `status==0`.
"Completed" is all items with `status==1`.
<br />
### 9.1 Create `/:filter` Route
Before starting,
let's add a unit test.
We want to show filtered items
according to the filter chosen.
Open `test/app_web/controllers/item_controller_test.exs`
and locate `describe "index" do`.
In this block, add the following test.
It checks if the item is properly being shown
when the filter is changed.
```elixir
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
```
e.g:
[`test/app_web/controllers/item_controller_test.exs#L21-L32`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/0c12a6bec7aeed5562a239d0dc8eea4952250cdd/test/app_web/controllers/item_controller_test.exs#L21-L32)
Open the `lib/app_web/router.ex` and
add the following route:
```elixir
get "/items/filter/:filter", ItemController, :index
```
e.g:
[`/lib/app_web/router.ex#L23`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/a341c2cbb5f1ad91897293f058a5d7bee6c1e1cc/lib/app_web/router.ex#L23)
<br />
### 9.2 Update the Controller `index/2` to send `filter` to View/Template
Open the `lib/app_web/controllers/item_controller.ex` file
and locate the `index/2` function.
Replace the invocation of `render/3` at the end of `index/2`
with the following:
```elixir
render(conn, "index.html",
items: items,
changeset: changeset,
editing: item,
filter: Map.get(params, "filter", "all")
)
```
e.g:
[`lib/app_web/controllers/item_controller.ex#L17-L22`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/a341c2cbb5f1ad91897293f058a5d7bee6c1e1cc/lib/app_web/controllers/item_controller.ex#L17-L22)
`Map.get(params, "filter", "all")`
sets the default value of our `filter` to "all"
so when `index.html` is rendered, show "all" items.
<br />
### 9.3 Create `filter/2` View Function
In order to filter the items by their status,
we need to create a new function. <br />
Open the `lib/app_web/controllers/item_html.ex` file
and create the `filter/2` function as follows:
```elixir
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
```
e.g:
[`lib/app_web/controllers/item_html.ex#L19-L26`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/50cce48a72d27b52cfeae158e3191e3cd1a8fe87/lib/app_web/controllers/item_html.ex#L19-L26)
This will allow us to filter the items in the next step.
<br />
### 9.4 Update the Footer in the `index.html` Template
Use the `filter/2` function to filter the items that are displayed.
Open the `lib/app_web/controllers/item_html/index.html.heex` file
and locate the `for` loop line:
```elixir
<%= for item <- @items do %>
```
Replace it with:
```elixir
<%= for item <- filter(@items, @filter) do %>
```
e.g:
[`lib/app_web/controllers/item_html/index.html.heex#L18`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/50cce48a72d27b52cfeae158e3191e3cd1a8fe87/lib/app_web/controllers/item_html/index.html.heex#L18)
This invokes the `filter/2` function we defined in the previous step
passing in the list of `@items` and the selected `@filter`.
Next, locate the the `<footer>`
and replace the _contents_
of the
`<ul class="filters">`
with the following code:
```elixir
<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>
```
We are conditionally adding the `selected` class
according to the `@filter` assign value.
e.g:
[`/lib/app_web/controllers/item_html/index.html.heex#L62-L98`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/50cce48a72d27b52cfeae158e3191e3cd1a8fe87/lib/app_web/controllers/item_html/index.html.heex#L62-L98)
At the end of this step you will have a
fully functioning footer filter:
![phoenix-todo-footer-nav](https://user-images.githubusercontent.com/194400/83212591-ea19af80-a157-11ea-9e0b-502af5cb61b2.gif)
We can quickly cover this function we added
with a small unit test.
Open `test/app_web/controllers/item_html_test.exs`
and add the following.
```elixir
test "test filter function" do
items = [
%{text: "one", status: 0},
%{text: "two", status: 0},
%{text: "three", status: 1},
%{text: "four", status: 2},
%{text: "five", status: 2},
%{text: "six", status: 1},
]
assert length(ItemHTML.filter(items, "items")) == 4
assert length(ItemHTML.filter(items, "active")) == 2
assert length(ItemHTML.filter(items, "completed")) == 2
assert length(ItemHTML.filter(items, "any")) == 4
end
```
And you should be done with this feature 😀.
Awesome job!
<br />
### 10. Clear Completed
We are _almost_ done with our Phoenix implementation of TodoMVC.
The last thing to implement is "clear completed".
Open your `lib/app_web/router.ex` file
and add the following route:
```elixir
get "/items/clear", ItemController, :clear_completed
```
Your `scope "/"` should now look like the following:
```elixir
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 the `lib/app_web/controllers/item_controller.ex`
file add the following code:
```elixir
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
```
e.g:
[`lib/app_web/controllers/item_controller.ex#L87-L93`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/a341c2cbb5f1ad91897293f058a5d7bee6c1e1cc/lib/app_web/controllers/item_controller.ex#L87-L93)
This uses the handy
[`update_all/3`](https://hexdocs.pm/ecto/2.0.0-rc.5/Ecto.Repo.html#c:update_all/3)
function to update all items that match the `query`.
In our case we searching for all `items`
that belong to `person_id==0`
and have `status==1`.
We are not _deleting_ the items,
rather we are updating their status to `2`
which for the purposes of our example means they are "archived".
> **Note**: This is a useful guide to `update_all`:
https://adamdelong.com/bulk-update-ecto
Finally, in the `lib/app_web/controllers/item_html/index.html.eex`
scroll to the bottom of the file and replace the line:
```elixir
<button class="clear-completed" style="display: block;">
Clear completed
</button>
```
With:
```elixir
<a class="clear-completed" href="/items/clear">
Clear completed
[<%= Enum.count(filter(@items, "completed")) %>]
</a>
```
e.g:
[`lib/app_web/controllers/item_html/index.html.heex#L104-L107`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/a341c2cbb5f1ad91897293f058a5d7bee6c1e1cc/lib/app_web/controllers/item_html/index.html.heex#L104-L107)
The last thing we need to do is to
update the `filter/2` function
inside `lib/app_web/controllers/item_html.ex`.
Since `status = 2` now pertains to an **archived** state,
we want to return anything that is not archived.
Change the `filter/2` function so it looks like so.
```elixir
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
end
```
At the end of this section your Todo List
should have the "Clear completed" function working:
![phoenix-todo-clear-completed](https://user-images.githubusercontent.com/194400/83244744-a3e44080-a197-11ea-90b3-5420f98bbd55.gif)
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.
```elixir
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
end
```
<br />
### 11. Tidy Up! (_Optional?_)
At 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!
### 11.1 _Pluralise_ Items Left
If you are the type of person to notice the _tiny_ details,
you would have been
[_itching_](https://en.wikipedia.org/wiki/Obsessive%E2%80%93compulsive_disorder)
each time you saw
the "***1 items left***" in the bottom left corner:
![phoenix-todo-pluralisation-BEFORE](https://user-images.githubusercontent.com/194400/83249677-dc3b4d00-a19e-11ea-8176-2f38725c3b50.png)
Open your `test/app_web/controllers/item_html_test.exs` file
and add the following test:
```elixir
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
```
e.g:
[`test/app_web/controllers/item_html_test.exs#L28-L35`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/a341c2cbb5f1ad91897293f058a5d7bee6c1e1cc/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`:
```elixir
# 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
```
e.g:
[`lib/app_web/controllers/item_html.ex#L28-L35`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/a341c2cbb5f1ad91897293f058a5d7bee6c1e1cc/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/1` function.
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:
```elixir
<span class="todo-count"><%= remaining_items(@items) %> items left</span>
```
And replace it with the following code:
```elixir
<span class="todo-count">
<%= remaining_items(@items) %> <%= pluralise(@items) %> left
</span>
```
e.g:
[`lib/app_web/controllers/item_html/index.html.heex#L61`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/6649a67639ccf7ad1b3189aefe678e3621a08341/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:
![phx-todo-pluralise-demo](https://user-images.githubusercontent.com/194400/83257010-6ab5cb80-a1ab-11ea-93e7-e95a9fb256c1.gif)
### 11.2 Hide Footer When There Are _Zero_ Items
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
![todo-mvc-vanilla-](https://user-images.githubusercontent.com/194400/83250460-0fcaa700-a1a0-11ea-8f05-f399233fad4d.png)
At present our App _shows_ the `<footer>`
even if their are Zero items: 🤦
<img width="849" alt="phoenix-todo-zero-items" src="https://user-images.githubusercontent.com/194400/83250895-b3b45280-a1a0-11ea-8f13-d54470cf278a.png">
This is a visual distraction/clutter
that creates
[unnecessary questions](https://en.wikipedia.org/wiki/Don%27t_Make_Me_Think)
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`:
```elixir
def got_items?(items) do
Enum.filter(items, fn i -> i.status < 2 end) |> Enum.count > 0
end
```
e.g:
[`lib/app_web/controllers/item_html.ex#L37-L39`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/a341c2cbb5f1ad91897293f058a5d7bee6c1e1cc/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:
```elixir
<%= if got_items?(@items) do %>
<% end %>
```
e.g:
[`lib/app_web/controllers/item_html/index.html.heex#L58`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/a341c2cbb5f1ad91897293f058a5d7bee6c1e1cc/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:
![phx-todo-footer-hidden](https://user-images.githubusercontent.com/194400/83268893-2bdd4100-a1be-11ea-88ac-f99f7e6efeda.gif)
<br />
### 11.3 Route `/` 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/`](http://localhost:4000)
they see the `lib/app_web/controllers/page_html/home.html.eex` template:
![page_template](https://user-images.githubusercontent.com/17494745/206484573-3596814d-92ce-42a0-9817-30fc16ce74cc.png)
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:
```elixir
get "/", PageController, :index
```
Update the controller to `ItemController`.
```elixir
get "/", ItemController, :index
```
e.g:
[`lib/app_web/router.ex#L20`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/a341c2cbb5f1ad91897293f058a5d7bee6c1e1cc/lib/app_web/router.ex#L20)
Now when you run your App you will see the todo list on the home page:
![todo-app-on-homepage](https://user-images.githubusercontent.com/194400/83270006-cbe79a00-a1bf-11ea-8972-91097fdabdc1.png)
Unfortunately,
this update will "break" the page test.
Run the tests and see:
```sh
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!
```sh
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
```
<br />
### 11.4 Add Turbolinks to Eliminate Page Refresh
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](https://github.com/hotwired/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`](https://unpkg.com/browse/@hotwired/[email protected]/dist/)
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`.
```html
<script src="https://unpkg.com/browse/@hotwired/[email protected]/dist/turbo.es2017-esm.js">
```
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](https://phxtodo.fly.dev/)
Feel that buttery-smooth page transition.
### 11.5 Remove unused /items/:id route
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.
```elixir
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:
```elixir
resources "/items", ItemController
```
We can change it to this.
```elixir
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.
e.g:
[`/lib/router.ex#L19-L29`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/0c12a6bec7aeed5562a239d0dc8eea4952250cdd/lib/app_web/router.ex#L19-L29)
[`lib/app_web/controllers/item_controller.ex`](https://github.com/dwyl/phoenix-todo-list-tutorial/blob/auth/lib/app_web/controllers/item_controller.ex)
### 12. Authentication (Optional)
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`](./auth.md)
to help you set this up.
You will soon find out this is extremely easy 😀.
### Deploy!
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.
e.g: https://phxtodo.fly.dev
xs
![phxtodo-fly-io](https://user-images.githubusercontent.com/194400/206772309-77056109-8e60-40ad-8e16-4e0f3140c0eb.png)
### 13. REST API (Optional)
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`**](./api.md)
### Done!
## What _Next_?
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**](https://github.com/dwyl/phoenix-liveview-counter-tutorial)
Thank you for learning with us! ☀️
## Learning
+ Learn Elixir: https://github.com/dwyl/learn-elixir
+ Learn Phoenix https://github.com/dwyl/learn-phoenix-framework
+ Phoenix Chat Tutorial:
https://github.com/dwyl/phoenix-chat-example
+ Phoenix LiveView Counter Tutorial:
https://github.com/dwyl/phoenix-liveview-counter-tutorial