Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Phoenix 1.6

reading/deprecated_phoenix_1.6.livemd

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.7

Phoenix 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.

test

> 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.

  1. The browser makes an HTTP GET request.
  2. The CounterWeb.Endpoint module in lib/counter_web/endpoint.ex creates an initial Plug.Conn struct which will be transformed in a pipeline of functions.
  3. The CounterWeb.Router module in lib/counter_web/router.ex routes the request to a controller.
  4. The CounterWeb.PageController module in lib/counter_web/controllers/page_controller.ex module handles the request and response, and delegates to a view to render the response.
  5. The CounterWeb.PageView module in lib/counter_web/views/page_view.ex renders a response using a template .heex file.
  6. 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.
  7. 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 lib/counter_web/templates/layout/root.html.heex. This file contains the root of our HTML document. Our content

Hello, world!

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.

Navigation

Home Report An Issue PortfolioPhoenix 1.7