Phoenix 1.6
Mix.install([
{:jason, "~> 1.4"},
{:kino, "~> 0.9", override: true},
{:youtube, github: "brooklinjazz/youtube"},
{:hidden_cell, github: "brooklinjazz/hidden_cell"}
])
Navigation
Home Report An Issue PortfolioPhoenix 1.7Phoenix 1.6
The latest release candidate for Phoenix 1.7 makes several significant changes to Phoenix such as the removal of Phoenix.View and the addition of Tailwind.
This course will focus on Phoenix version 1.6, as most Phoenix projects will use 1.6 or older. In the future we will migrate to Phoenix 1.7. If you are curious to see the changes in 1.7, you can read our Phoenix 1.7 guide.
Overview
The Phoenix Framework is the most popular web development framework for Elixir. Using Phoenix, we can build rich interactive and real-time web applications quickly.
By default, Phoenix separates the application into several layers using Model-View-Controller (MVC) architecture.
- Model: Manages the data and business logic of the application.
- View: Represents visual information.
- Controller: Handles requests and manipulates the model/view to respond to the user.
> source: https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller
Under the hood, Phoenix uses the Plug specification for composing a web application with functions. Plugs are functions that transform a conn
(connection) data structure.
flowchart LR
subgraph Input
CP[conn, params]
end
subgraph Output
C[conn]
end
Output[conn]
P[Plug Function]
Input --> P --> Output
Phoenix sets up a pipeline of plugs and uses the transformed conn
Plug.Conn struct to return a response to the client.
Plug uses Cowboy, a small and fast HTTP web server. Cowboy uses Ranch to manage the underlying TCP connections for the web server.
flowchart LR
P[Phoenix]
PG[Plug]
C[Cowboy]
R[Ranch]
P --> PG --> C --> R
click P "https://hexdocs.pm/phoenix/overview.html"
click PG "https://hexdocs.pm/plug/readme.html"
click C "https://github.com/ninenines/cowboy"
click R "https://github.com/ninenines/ranch"
Phoenix breaks the complexity of our application into several layers with different responsibilities. Separating an application into layers makes it easier to reason about and collaborate on complex applications.
- Endpoint: The boundary layer of the application.
- Router: Routes the request to the correct controller.
- Controller: Handles the request—generally, the controller delegates to the Model and View to manipulate business logic and return a response.
- Model: Contains the application’s business logic, often separated into the Context and the Schema if the application has a data layer.
- View: Handles returning the response, may delegate to a template.
- Template: Builds a response, typically using HEEx (HTML + Embedded Elixir) to programmatically build an HTML web page using both HTML and Elixir.
Install Phoenix
To use Phoenix, we need to install it and several prerequisite programs. At this point in the course, you should already have Erlang, Elixir, and PostgreSQL installed.
You’ll also need to install the Hex package manager. Hex manages elixir dependencies.
$ mix local.hex
Install the phx_new
dependency, which we’ll use to generate a Phoenix project.
$ mix archive.install hex phx_new
After the release of Phoenix 1.7, you may need to specifically install Phoenix 1.6 using the following command.
$ mix archive.install hex phx_new 1.6.15
Phoenix projects reload the project anytime a project file changes.
macOS and Windows users will have this feature by default, but students using GNU/Linux or Windows + WSL must install inotify-tools
to use this feature.
Consult the inotify-tools documentation for installation instructions.
Installing inotify-tools
is optional but highly recommended.
If you have any issues, consult the Phoenix Installation Guide or speak with your instructor.
Create A Phoenix App
The phx_new
dependency provides the mix phx.new
task, which we can use to generate a new Phoenix project.
We’re going to create a counter project which stores an in-memory integer we can increment and display using HTTP requests.
Run the following in your command line to create a new Phoenix project.
$ mix phx.new counter --no-ecto
This should create the following files.
* creating counter/config/config.exs
* creating counter/config/dev.exs
* creating counter/config/prod.exs
* creating counter/config/runtime.exs
* creating counter/config/test.exs
* creating counter/lib/counter/application.ex
* creating counter/lib/counter.ex
* creating counter/lib/counter_web/controllers/error_json.ex
* creating counter/lib/counter_web/endpoint.ex
* creating counter/lib/counter_web/router.ex
* creating counter/lib/counter_web/telemetry.ex
* creating counter/lib/counter_web.ex
* creating counter/mix.exs
* creating counter/README.md
* creating counter/.formatter.exs
* creating counter/.gitignore
* creating counter/test/support/conn_case.ex
* creating counter/test/test_helper.exs
* creating counter/test/counter_web/controllers/error_json_test.exs
* creating counter/lib/counter/repo.ex
* creating counter/priv/repo/migrations/.formatter.exs
* creating counter/priv/repo/seeds.exs
* creating counter/test/support/data_case.ex
* creating counter/lib/counter_web/controllers/error_html.ex
* creating counter/test/counter_web/controllers/error_html_test.exs
* creating counter/lib/counter_web/components/core_components.ex
* creating counter/lib/counter_web/controllers/page_controller.ex
* creating counter/lib/counter_web/controllers/page_html.ex
* creating counter/lib/counter_web/controllers/page_html/home.html.heex
* creating counter/test/counter_web/controllers/page_controller_test.exs
* creating counter/lib/counter_web/components/layouts/root.html.heex
* creating counter/lib/counter_web/components/layouts/app.html.heex
* creating counter/lib/counter_web/components/layouts.ex
* creating counter/assets/vendor/topbar.js
* creating counter/lib/counter/mailer.ex
* creating counter/lib/counter_web/gettext.ex
* creating counter/priv/gettext/en/LC_MESSAGES/errors.po
* creating counter/priv/gettext/errors.pot
* creating counter/assets/css/app.css
* creating counter/assets/js/app.js
* creating counter/assets/tailwind.config.js
* creating counter/priv/static/robots.txt
* creating counter/priv/static/images/phoenix.png
* creating counter/priv/static/favicon.ico
When prompted to install dependencies, type Y
and press enter.
This runs mix deps.get
and mix deps.compile
.
Fetch and install dependencies? [Yn] Y
* running mix deps.get
* running mix deps.compile
Project Structure
Open the new counter
project in your code editor.
Phoenix projects use Mix, so this folder structure should feel familiar minus a few extra or modified files.
├── _build
├── assets
├── config
├── deps
├── lib
│ ├── counter
│ ├── counter.ex
│ ├── counter_web
│ └── counter_web.ex
├── priv
├── test
├── formatter.exs
├── .gitignore
├── mix.exs
├── mix.lock
└── README.md
For a complete overview of each file and folder, see the Phoenix Documentation on Directory Structure. We’ll walk through the purpose of each folder and file as they become relevant to our counter project.
First, we’ll focus on the /lib
folder containing our application code.
/lib
is split into two subdirectories, one for the business logic of our application and one that handles the web server side of our application.
For example, in the counter
project, there should be a /lib/counter
folder and a lib/counter_web
folder.
/lib/counter
will hold our counter’s business logic, such as storing and incrementing the count. The lib/counter_web
folder will hold the counter’s web-related logic for accepting and responding to HTTP requests from clients.
Start Phoenix
We can start the Phoenix web server by running the following command from the /counter
folder in your command line.
$ mix phx.server
Troubleshooting
It’s common to encounter issues when starting Phoenix for the first time. Typically students run into issues with Postgres.
Linux users will often encounter an issue where the postgresql
service is not running. You can solve this problem with the following command.
sudo service postgresql start
Alternatively, you may have a permissions issue where the PostgreSQL user does not have the default username and password. You can resolve this by ensuring there is a postgres
user with a postgres
password.
While not a magic solution, the following may solve your problem.
$ sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'postgres';"
$ sudo server postgresql restart
These are two very common issues, however you may encounter an unexpected error. Please speak with your instructor if you encounter any issues to get support.
Phoenix Lifecycle
When you start a Phoenix project, it runs the Counter.Application.start/2
function in lib/counter/application.ex
, which starts several workers under a supervisor.
@impl true
def start(_type, _args) do
children = [
# Start the Telemetry supervisor
CounterWeb.Telemetry,
# Start the PubSub system
{Phoenix.PubSub, name: Counter.PubSub},
# Start the Endpoint (http/https)
CounterWeb.Endpoint
# Start a worker by calling: Counter.Worker.start_link(arg)
# {Counter.Worker, arg}
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Counter.Supervisor]
Supervisor.start_link(children, opts)
end
The CounterWeb.Endpoint
module is the boundary where all requests to your application start. Now that the Phoenix server is running, we
can visit http://localhost:4000 to view the Phoenix home page.
A few things happen when we navigate to http://localhost:4000.
- The browser makes an HTTP GET request.
-
The
CounterWeb.Endpoint
module inlib/counter_web/endpoint.ex
creates an initial Plug.Conn struct which will be transformed in a pipeline of functions. -
The
CounterWeb.Router
module inlib/counter_web/router.ex
routes the request to a controller. -
The
CounterWeb.PageController
module inlib/counter_web/controllers/page_controller.ex
module handles the request and response, and delegates to a view to render the response. -
The
CounterWeb.PageView
module inlib/counter_web/views/page_view.ex
renders a response using a template.heex
file. -
The template in
index.html.heex
uses the Phoenix template language HEEx (HTML + Embedded Elixir) to build an HTML web page which will be the response to GET request. - The HTML response is sent back to the browser.
sequenceDiagram
autonumber
participant B as Browser
participant E as Endpoint
participant R as Router
participant C as Controller
participant V as View
participant T as Template
B->>E: GET Request
E->>R: Set Up Plug Pipeline with Conn
R->>C: Route Request to Controller
C->>V: Handle request and delegate to View
V->>T: Build response using template
T->>E: Build HTML web page
E->>B: HTML response sent back to browser.
Router
To handle an HTTP request from the client, we need to define a route.
Routes accept incoming client requests provided a particular path and determine how to handle the request. Routes are defined in the lib/counter_web/router.ex
file.
The Phoenix.Router module defines several macros for handling HTTP requests.
- post/4 handle an HTTP POST request.
- get/4 handle an HTTP GET request.
- put/4 handle an HTTP PUT request.
- patch/4 handle an HTTP PATCH request.
- resources/2 handle a standard matrix of HTTP requests.
We’ll focus on get/4
and post/4
as they are the most common.
The initial router.ex
file has many macros such as scope/3
, pipeline/3
, plug/2
, pipe_through/1
and get/3
.
defmodule CounterWeb.Router do
use CounterWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, {CounterWeb.LayoutView, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", CounterWeb do
pipe_through :browser
get "/", PageController, :index
end
# Other scopes may use custom stacks.
# scope "/api", CounterWeb do
# pipe_through :api
# end
# Enables LiveDashboard only for development
#
# If you want to use the LiveDashboard in production, you should put
# it behind authentication and allow only admins to access it.
# If your application does not have an admins-only section yet,
# you can use Plug.BasicAuth to set up some basic authentication
# as long as you are also using SSL (which you should anyway).
if Mix.env() in [:dev, :test] do
import Phoenix.LiveDashboard.Router
scope "/" do
pipe_through :browser
live_dashboard "/dashboard", metrics: CounterWeb.Telemetry
end
end
# Enables the Swoosh mailbox preview in development.
#
# Note that preview only shows emails that were sent by the same
# node running the Phoenix server.
if Mix.env() == :dev do
scope "/dev" do
pipe_through :browser
forward "/mailbox", Plug.Swoosh.MailboxPreview
end
end
end
By default, the router defines a :browser
pipeline for handling requests using a browser, and an :api
pipeline for handling direct HTTP requests.
Our GET request from the browser hits the following route. The scope/3
macro defines a base url "/"
and automatically aliases all controllers so we can use PageController
instead of CounterWeb.PageController
.
scope "/", CounterWeb do
pipe_through :browser
get "/", PageController, :index
end
The pipe_through :browser
macro calls all of the plugs inside of the :browser
pipeline. These plugs handle some necessary boilerplate code, including security features and rendering a root layout in lib/counter_web/templates/root.html.heex
shared by every page.
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, {CounterWeb.LayoutView, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
The get/3
macro sets up a route "/"
which is for http://localhost:4000/. It then delegates to the PageController.index/2
function.
For our counter application, we will set up our own "/counter"
route, which delegates to a CounterController
module’s :index
action to return a count response to the user. An action is a function that accepts a conn
struct and any parameters from the client request.
scope "/", CounterWeb do
pipe_through :browser
get "/", PageController, :index
get "/count", CounterController, :index
end
We’ll define the CounterController
in the next section. First, run the following command to see the project’s routes. We should see the new /counter
route.
$ mix phx.routes
page_path GET / CounterWeb.PageController :index
counter_path GET /count CounterWeb.CounterController :index
live_dashboard_path GET /dashboard Phoenix.LiveDashboard.PageLive :home
live_dashboard_path GET /dashboard/:page Phoenix.LiveDashboard.PageLive :page
live_dashboard_path GET /dashboard/:node/:page Phoenix.LiveDashboard.PageLive :page
* /dev/mailbox Plug.Swoosh.MailboxPreview []
websocket WS /live/websocket Phoenix.LiveView.Socket
longpoll GET /live/longpoll Phoenix.LiveView.Socket
longpoll POST /live/longpoll Phoenix.LiveView.Socket
Now when we visit http://localhost:4000/count, we’ll see the following error because the CounterController
module does not exist.
Controllers
A controller determines the response to send back to a user.
The CounterWeb.PageController
in lib/counter_web/controllers/page_controller.ex
calls the render/3
macro which delegates to a view to render some HTML.
defmodule CounterWeb.PageController do
use CounterWeb, :controller
def index(conn, _params) do
render(conn, "index.html")
end
end
Notice that the function name index/2
matches the atom defined in counter/lib/counter_web/router.ex
.
get "/", PageController, :index
Make sure to add the following line to your router file.
get "/count", CounterController, :index
To make our counter, we need to define the CounterController
controller. Create a file lib/counter_web/controllers/counter_controller.ex
with the following content.
defmodule CounterWeb.CounterController do
use CounterWeb, :controller
def index(conn, _params) do
end
end
The router calls this CounterController.index/2
function to handle the request and response.
The first argument in the index/2
function is the Plug.Conn being transformed by the plug pipeline.
The second argument is a map of any query parameters included in the request.
The Phoenix.Controller module provides several macros to return a response to the user. Here are a few of the most commonly used.
- html/2 return a manual HTML response.
- json/2 return JSON.
- redirect redirect the client to another url.
- render/3 return HTML from a template file.
- text/2 return a text string.
For example, we can use the text/2
macro to return a text response.
defmodule CounterWeb.CounterController do
use CounterWeb, :controller
def index(conn, _params) do
text(conn, "Hello, world!")
end
end
Now when we visit http://localhost:4000/count we should see the "Hello, world!"
response.
Your Turn
Use the text/2
, html/2
, json/2
, and redirect/2
macros to return a response from your CounterController.index/2
function. You may copy-paste each of the following examples one at a time.
text(conn, "Hello, world!")
html(conn, "Hello, world!
")
json(conn, %{"key" => "value"})
redirect(conn, to: "/")
redirect(conn, external: "https://elixir-lang.org")
Feel free to experiment with these macros to understand them better.
Query Params
Under the hood, Phoenix converts query params into an Elixir map before reaching the controller. For example, if you visit http://localhost:4000?message=hello the second argument to
index/2
will be %{"message" => "hello"}
.
Let’s verify this. Now our index/2
function will return the "message"
query parameter if it exists, and otherwise return "Hello, world!"
.
defmodule CounterWeb.CounterController do
use CounterWeb, :controller
def index(conn, params) do
message = Map.get(params, "message", "Hello, world!")
text(conn, message)
end
end
Now, if you visit http://localhost:4000/count?message=hello, the page should display "hello"
.
Views
While we can return a response directly in the controller, it’s conventional to use the render/3
macro to delegate to a view to build the response.
Replace lib/counter_web/controllers/counter_countroller.ex
with the following content.
defmodule CounterWeb.CounterController do
use CounterWeb, :controller
def index(conn, params) do
render(conn, "index.html")
end
end
We’ll see the following error if we visit http://localhost:4000/count.
By convention, a CounterController
expects a CounterView
to exist. The name of the view should match the name of the controller.
Create a file lib/counter_web/views/counter_view.ex
with the following content.
defmodule CounterWeb.CounterView do
use CounterWeb, :view
end
Now, if we visit http://localhost:4000/count, we’ll see the following error.
The CounterView
expects a matching template file to exist.
Templates
Phoenix uses HEEx (HTML + Embedded Elixir) templates to render web pages. HEEx allows us to write HTML with embedded Elixir code.
Create a lib/counter_web/templates/counter/index.html.heex
file with the following content.
Hello, world!
Now we should see the following page when we visit http://localhost:4000/count.
Root Layout
The Phoenix Framework header comes from Hello, world!lib/counter_web/templates/layout/root.html.heex
.
This file contains the root of our HTML document. Our content
is rendered in the @inner_content
value.
<%= live_title_tag assigns[:page_title] || "Counter", suffix: " · Phoenix Framework" %>
- Get Started
<%= if function_exported?(Routes, :live_dashboard_path, 2) do %>
- <%= link "LiveDashboard", to: Routes.live_dashboard_path(@conn, :home) %>
<% end %>
<%= @inner_content %>
The :put_root_layout
plug in the router automatically configures this file as a wrapper to all template files, where @inner_content
will be the HEEx defined by the template file.
We can confirm this by commenting out the plug in lib/counter_web/router.ex
.
Removing the plug is for demonstration purposes only. We don’t recommend you remove this in your project.
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
# plug :put_root_layout, {CounterWeb.LayoutView, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
Now the response only contains our content.
We want to keep the root layout because it contains some helpful HTML boilerplate, so instead, we can remove the header from the lib/counter_web/templates/layout/root.html.heex
file.
Remove the html tag, and its contents in `lib/counter_web/templates/layout/root.html.heex`. ```elixir <%= live_title_tag assigns[:page_title] || "Counter", suffix: " · Phoenix Framework" %> <%= @inner_content %> ``` ### Styling In `lib/counter_web/templates/root.html.heex`, there is a
tag to an assets/app.css
file.
This app.css
is the root CSS file for styling. Here we can create our CSS styles, import other CSS styles, or modify/remove the default styles.
The app.css
file also imports the assets/phoenix.css
file, which contains additional default styles.
We’ll keep these default styles for this lesson, but we have the power to remove or modify them.
EEx
We can embed Elixir in our template files using the following syntax.
<%= expression %>
Where expression
is our Elixir code, for example, replace lib/counter_web/templates/counter/index.html.heex
with the following.
<%= 1 + 1 %>
We should see 2
as the new response when we visit http://localhost:4000/count.
We can write essentially any Elixir expression. So, for example, we could write an if
statement to render different HTML depending on some condition.
<%= if DateTime.utc_now().hour > 12 do %>
<p>Good afternoon!</p>
<% else %>
<p>Good morning!</p>
<% end %>
Or a loop using the for
comprehension. Often we use this to create multiple elements based on a collection.
<%= for int <- 1..10 do %>
<p><%= int %></p>
<% end %>
Notice that all expressions which output a value must use the =
symbol.
View Functions
The view compiles template files, so we can call functions defined in the view inside of the template.
Create a greeting/0
function in the CounterView
module in lib/counter_web/views/counter_view.ex
. For now it will simply return "Hello, world!"
.
defmodule CounterWeb.CounterView do
use CounterWeb, :view
def greeting do
"Hello, world!"
end
end
Then use the greeting/0
function in the lib/counter_web/templates/counter/index.html.heex
template file.
<h1><%= greeting() %></h1>
Assigns
Controllers can pass values in an assigns
argument. We can display these values in the template using the @
shortcut.
Add the count value in lib/counter_web/controllers/counter_controller.ex
. For now, it will be 0
.
defmodule CounterWeb.CounterController do
use CounterWeb, :controller
def index(conn, _params) do
render(conn, "index.html", count: 0)
end
end
And use the @count
value in the template file lib/counter_web/templates/counter/index.html.heex
.
<h1>The current count is: <%= @count %></h1>
Visit http://localhost:4000/count, and you should see the following response.
Model (Counter Implementation)
Business logic belongs in the lib/counter
folder, not the lib/counter_web
folder.
In Phoenix, we group common behavior into a context. Contexts expose an API for a set of related behavior. How we group, the behavior may change as the business logic of an application grows. There is no strict set of rules for grouping behavior into a context, though there are many differing opinions within the Phoenix community.
Count Context
For example, we can create a Count
context responsible for retrieving and exposing the current count of the counter application.
Each context has a main file under the lib/counter
folder, which matches the name of the context, so our Count
context module belongs in a lib/counter/count.ex
file.
The context will expose an increment/1
and get/0
function for returning and incrementing a count stored in memory.
Create the lib/counter/count.ex
file with the following content.
defmodule Counter.Count do
def get do
# implementation
end
def increment(increment_by \\ 1) do
# implementation
end
end
Counter Server
We group sub-modules related to a context inside a folder named after the context. We group each sub-module under the main namespace for the context.
For example, we’ll create a Counter.Count.CounterServer
module in a lib/counter/count/count_server.ex
file. This module defines the GenServer that will store the count in memory and expose handlers for retrieving and incrementing the count.
defmodule Counter.Count.CounterServer do
use GenServer
def start_link(_opts) do
GenServer.start_link(__MODULE__, 0, name: __MODULE__)
end
@impl true
def init(count) do
{:ok, count}
end
@impl true
def handle_call(:get, _from, count) do
{:reply, count, count}
end
@impl true
def handle_call({:increment, increment_by}, _from, count) do
{:reply, count + increment_by, count + increment_by}
end
end
Now we can implement the get/0
and increment/1
functions in the Count
module.
defmodule Counter.Count do
def get do
GenServer.call(Counter.Count.CounterServer, :get)
end
def increment(increment_by \\ 1) do
GenServer.call(Counter.Count.CounterServer, {:increment, increment_by})
end
end
Start The Counter Server
The lib/counter/application.ex
defines our Elixir application and the services that are part of our application. We can start our workers and supervisors in this file.
Add the Counter.Count.CounterServer
module to the start/2
function in application.ex
.
def start(_type, _args) do
children = [
# Start the Telemetry supervisor
CounterWeb.Telemetry,
# Start the PubSub system
{Phoenix.PubSub, name: Counter.PubSub},
# Start the Endpoint (http/https)
CounterWeb.Endpoint,
# Start a worker by calling: Counter.Worker.start_link(arg)
# {Counter.Worker, arg}
{Counter.Count.CounterServer, []}
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: Counter.Supervisor]
Supervisor.start_link(children, opts)
end
Stop your server with CTRL+C if it is already running, then run the following command.
$ iex -S mix phx.server
This command starts your server and compiles the project files into the IEx shell so we can interact with project modules manually.
Call the Counter.Count
context directly from the IEx shell and ensure it works as expected.
iex> Counter.Count.get()
0
iex> Counter.Count.increment()
1
iex> Counter.Count.get()
1
Connect The Counter
Now we have a working counter. We’ll retrieve the count from the CounterWeb.CounterController
and then return the current count as the response to the client.
defmodule CounterWeb.CounterController do
use CounterWeb, :controller
def index(conn, _params) do
count = Counter.Count.get()
render(conn, "index.html", count: count)
end
end
Visit http://localhost:4000/count to see the current count in the response.
Increment The Count
We’ll have clients send an HTTP POST request to increment the count.
First, use the post/4
macro to create a new post /increment
route in lib/counter_web/router.ex
.
scope "/", CounterWeb do
pipe_through :browser
get "/", PageController, :index
get "/count", CounterController, :index
post "/count", CounterController, :update
end
A client will make an HTTP POST request to the /count
router which triggers the CounterController.update/2
function.
Create an update/2
function in lib/counter_web
, which increments the count and redirects the user back to the /count
page to refresh the count.
def update(conn, _params) do
Counter.Count.increment()
redirect(conn, to: "/count")
end
Forms
We can use HTML Forms to send HTTP POST requests from within a web page.
The Phoenix.HTML.Form module makes it easier to create HTML forms.
We’ll use the form_for/3
macro to create an HTML form, and the submit/1
macro to create a submit button for the form.
Replace lib/counter_web/templates/counter/index.html.heex
with the following content.
<h1>The current count is: <%= @count %></h1>
<%= form_for @conn, "/count", fn _f -> %>
<%= submit "Increment" %>
<% end %>
We’ll dive deeper into forms in a later lesson. For now, know that the form_for/3
macro generates the following HTML.
Increment
The Cross-Site Request Forgery (CSRF) security token helps prevent CSRF attacks. Otherwise, this form sends a POST request to the /count
route.
Visit http://localhost:4000/count and press the increment button. The count should increment on the page.
Body Params
We can include body parameters in the HTTP POST request by adding inputs to the form. See Phoenix.HTML.Form Functions for a full list of available inputs.
Let’s add a number input. counter_web/templates/count/index.html.heex
should contain the following content.
<h1>The current count is: <%= @count %></h1>
<%= form_for @conn, "/count", fn f -> %>
<%= number_input f, :increment %>
<%= submit "Increment" %>
<% end %>
The :increment
atom is the key of the value associated with the input.
We can retrieve the value in the form using conn.body_params
in the controller.
Let’s inspect conn.body_params
in the CounterController
.
def update(conn, _params) do
IO.inspect(conn.params)
Counter.Count.increment()
redirect(conn, to: "/count")
end
Enter an integer such as 2
in the number input, then click the increment button on http://localhost:4000/count.
We’ll see the following in the command line.
%{
"_csrf_token" => "Fh8FNgE7Pi0HIHdBLjYRJS5WAhx1ZQI1Fe_qJpnWLM6sXZAFKesVM2zP",
"increment" => "2"
}
The _csrf_token
comes from the hidden input to validate the request, and the increment
comes from our form input.
Notice that the "increment"
key matches the name of the field in the form and that the value is a string, not an integer.
We need to parse the integer from the string and use it to increment the count. If the string does not contain an integer, we’ll increment by 1.
def update(conn, _params) do
increment =
case Integer.parse(conn.params["increment"]) do
{integer, _remainder_of_binary} ->
integer
_ ->
1
end
Counter.Count.increment(increment)
redirect(conn, to: "/count")
end
Now, if you visit http://localhost/count and enter an integer in the number input, it should increment the count by that number.
Form Query Params
The form_for/3
macro provides several conveniences under the hood. For example, by using @conn
with the form, the inputs will automatically receive default values from query params.
For example, visit http://localhost:4000/count?increment=5, and the number input will have 5
as its value.
Currently, Submitting the form resets the number input to a blank value.
To preserve this value, we can pass query params when we call the redirect/3
macro in the CounterController
.
def update(conn, _params) do
increment =
case Integer.parse(conn.params["increment"]) do
{integer, _binary} ->
integer
_ ->
1
end
Counter.Count.increment(increment)
redirect(conn, to: "/count?increment=#{increment}")
end
Now the form’s increment value will be preserved when we submit the form.
Route Helpers
We’ve been using static routes for demonstration purposes. However, we recommend using Path Helpers instead because we can change the routes of our application without causing breaking changes.
Path helpers are dynamically defined functions that return the route for the corresponding controller action.
For example, we can use the Routes.counter_path(@conn, :update)
to return the "/count"
route for the CounterController.update/2
function.
You can find the available routes (and therefore the available Routes
functions) using the following command.
mix phx.routes
Let’s replace the "/count"
value in lib/counter_web/templates/count/index.html.heex
.
<h1>The current count is: <%= @count %></h1>
<%= form_for @conn, Routes.counter_path(@conn, :update), fn f -> %>
<%= number_input f, :increment %>
<%= submit "Increment" %>
<% end %>
And the "/count"
value with query parameters in lib/counter_web/controllers/counter_controller.ex
.
def update(conn, _params) do
increment =
case Integer.parse(conn.params["increment"]) do
{integer, _remainder_of_binary} ->
integer
_ ->
1
end
Counter.Count.increment(increment)
redirect(conn, to: Routes.counter_path(conn, :update, increment: increment))
end
Further Reading
For more on Phoenix, consider the following resources.
Commit Your Progress
DockYard Academy now recommends you use the latest Release rather than forking or cloning our repository.
Run git status
to ensure there are no undesirable changes.
Then run the following in your command line from the curriculum
folder to commit your progress.
$ git add .
$ git commit -m "finish Phoenix 1.6 reading"
$ git push
We’re proud to offer our open-source curriculum free of charge for anyone to learn from at their own pace.
We also offer a paid course where you can learn from an instructor alongside a cohort of your peers. We will accept applications for the June-August 2023 cohort soon.