Phoenix 1.7
Mix.install([
{:jason, "~> 1.4"},
{:kino, "~> 0.8.0", override: true},
{:youtube, github: "brooklinjazz/youtube"},
{:hidden_cell, github: "brooklinjazz/hidden_cell"}
])
Navigation
Setup
Ensure you type the ea keyboard shortcut to evaluate all Elixir cells before starting. Alternatively you can evaluate the Elixir cells as you read.
Phoenix 1.7
Phoenix 1.7 introduced several changes such as Replacing Phoenix.View with Phoenix.Component as well as adding Tailwind by default with every Phoenix project.
This course will focus on Phoenix 1.7. However, most Phoenix projects you encounter in the industry will have been built with Phoenix 1.6 or older.
We will have some duplicate content between the Phoenix 1.6 reading material, and this lesson.
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.
Chris McCord, the creator of Phoenix, has an excellent video to demonstrate the power of Phoenix. Follow along and build a Twitter clone application in only 15 minutes.
YouTube.new("https://www.youtube.com/watch?v=MZvmYaFkNJI")
The video above uses Phoenix LiveView to create interactive and real-time features. We will cover LiveView in a future lesson.
Model-View-Controller (MVC) Architecture
Phoenix is heavily influenced by MVC architecture where an application is broken 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.
More recently, Phoenix has been breaking away from strict MVC architecture, but understanding this style of architecture will help us better understand the overall design choices behind Phoenix.
> 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
This material is up-to-date with Phoenix 1.17. If you are using a later version, you may find some code examples or instructions do not match. For the latest documentation of Phoenix see their https://hexdocs.pm/phoenix/installation.html”>Installation and https://hexdocs.pm/phoenix/up_and_running.html”>Up and Running guides.
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
If you are reading this before phoenix has released version 1.17 you may need to install the 1.7.0 release candidate.
mix archive.install hex phx_new 1.7.0-rc.0
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 teacher.
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 use the --no-ecto command to omit the Ecto Database, which we will cover in a future lesson.
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 --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.
flowchart
subgraph lib
direction LR
CW[CounterWeb]
C[Counter]
end
C --> b[business logic]
CW --> w[web application]
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 teacher 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 Ecto repository
Counter.Repo,
# Start the PubSub system
{Phoenix.PubSub, name: Counter.PubSub},
# Start Finch
{Finch, name: Counter.Finch},
# 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.Endpointmodule inlib/counter_web/endpoint.excreates an initial Plug.Conn struct which will be transformed in a pipeline of functions. -
The
CounterWeb.Routermodule inlib/counter_web/router.exroutes the request to a controller. -
The
CounterWeb.PageControllermodule inlib/counter_web/controllers/page_controller.exmodule handles the request and response, and delegates to a view to render the response. -
The
CounterWeb.PageHTML(View) module inlib/counter_web/controllers/page_html.exrenders a response using a template.heexfile. -
The template in
lib/counter_web/controllers/page_html/home.html.heexuses 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.Layouts, :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, :home
end
# Other scopes may use custom stacks.
# scope "/api", CounterWeb do
# pipe_through :api
# end
# Enable LiveDashboard and Swoosh mailbox preview in development
if Application.compile_env(:counter, :dev_routes) do
# 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).
import Phoenix.LiveDashboard.Router
scope "/dev" do
pipe_through :browser
live_dashboard "/dashboard", metrics: CounterWeb.Telemetry
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, :home
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.Layouts, :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 :count 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, :home
get "/count", CounterController, :count
end
We’ll define the CounterController in the next section. First, run the following command to see the project’s routes.
$ mix phx.routes
We should see the new /counter route.
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/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 home(conn, _params) do
# The home page is often custom made,
# so skip the default app layout.
render(conn, :home, layout: false)
end
end
Notice that the function name home/2 matches the atom defined in the router.
get "/", PageController, :home
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 count(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".
HTML 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, "count.html")
end
end
We’ll see the following error if we visit http://localhost:4000/count.
ArgumentError at GET /count
no "count" html template defined for CounterWeb.CounterHTML
By convention, a CounterController expects a CounterHTML module to exist. The name of the HTML view should match the name of the controller.
Create a file lib/counter_web/controllers/counter_html.ex with the following content.
defmodule CounterWeb.CounterHTML do
use CounterWeb, :html
end
Refresh the page and you’ll see the following error.
ArgumentError at GET /count
no "count" html template defined for CounterWeb.CounterHTML
We need to define a count template inside of our view.
defmodule CounterWeb.CounterHTML do
use CounterWeb, :html
def count(assigns) do
~H"""
Hello World!
"""
end
end
Now, if we visit http://localhost:4000/count, we’ll see the following content.
This is called a function component. Instead of using a template file, it returns the HEEx template directly using a sigil ~H. HEEx is HTML + Embedded elixir. We can use <%= %> to evaluate Elixir syntax and include HTML tags.
By default, Phoenix/Tailwind remove many of the default styles applied to HTML elements. This allows us to focus on using HTML elements semantically and apply any styles that we wish using the CSS utility framework Tailwind.
Replace CounterHTML with the following content and refresh the page. Notice that 2 + 2 evaluated as 4 and that the h1 tag is larger due to the text-3xl utility class.
defmodule CounterWeb.CounterHTML do
use CounterWeb, :html
def count(assigns) do
~H"""
Hello World!
<%= 2 + 2 %>
"""
end
end
Assigns
The controller can provide values to our view using the assigns. Modify the CounterController with the following content.
defmodule CounterWeb.CounterController do
use CounterWeb, :controller
def count(conn, _params) do
render(conn, "count.html", count: 0)
end
end
Values provided in a keyword list to the third argument of Controller.render/3 are bound to assigns in our view.
We can access assigns.count in our HTML view. Replace CounterHTML with the following content. Refresh your page and you should see The current count is 0.
defmodule CounterWeb.CounterHTML do
use CounterWeb, :html
def count(assigns) do
~H"""
The current count is: <%= assigns.count %>
"""
end
end
As a shorthand syntax we can use @ instead of assigns..
defmodule CounterWeb.CounterHTML do
use CounterWeb, :html
def count(assigns) do
~H"""
The current count is: <%= @count %>
"""
end
end
Templates
Instead of using a function component, we can write our HEEx inside of a template file.
First we alter our HTML view to use Phoenix.Component.embed_templates/2. We provide the function a folder to expose template files from.
defmodule CounterWeb.CounterHTML do
use CounterWeb, :html
embed_templates "counter_html/*"
end
Refresh the page and you’ll see the following error.
ArgumentError at GET /count
no "count" html template defined for CounterWeb.CounterHTML
We need to create a lib/counter_web/controllers/counter_html/count.html.heex file with some content. This template uses HEEx exactly the same as how our previous CounterHTML.count/1 function did.
The current count is <%= @count %>
Refresh the page and you should see the current count is 0.
Layouts
Phoenix defines two layout templates which wrap any templates that we create. For example, notice there’s already a header component on our /count page.
Root Layout
Our router.ex file uses the CounterWeb.Layout component to automatically render some HEEx templates that wrap our count.html.heex template.
plug :put_root_layout, {CounterWeb.Layouts, :root}
This renders the lib/counter_web/templates/layout/root.html.heex template which defines some meta information about our application, imports CSS styles, and renders our content using @inner_content.
<.live_title suffix=" · Phoenix Framework">
<%= assigns[:page_title] || "Counter" %>
<%= @inner_content %>
App Layout
The lib/counter_web/components/layouts/app.html.heex template contains the header rendered on every page and renders flash messages used for displaying errors.
Show File Snippet
<a href="/">
</a>
<p>
v1.7
</p>
<a href="https://twitter.com/elixirphoenix">
@elixirphoenix
</a>
<a href="https://github.com/phoenixframework/phoenix">
GitHub
</a>
<a href="https://hexdocs.pm/phoenix/overview.html">
Get Started <span>→</span>
</a>
<.flash kind={:info} title="Success!" flash={@flash} />
<.flash kind={:error} title="Error!" flash={@flash} />
<.flash
id="disconnected"
kind={:error}
title="We can't find the internet"
close={false}
autoshow={false}
phx-disconnected={show("#disconnected")}
phx-connected={hide("#disconnected")}
>
Attempting to reconnect
<%= @inner_content %>
We can provide the layout: false option in our assigns to disable the app.html.heex layout if we want to build a page from scratch. Notice this in the PageController.
defmodule CounterWeb.PageController do
use CounterWeb, :controller
def home(conn, _params) do
# The home page is often custom made,
# so skip the default app layout.
render(conn, :home, layout: false)
end
end
Styling
In Good afternoon! Good morning! <%= int %>lib/counter_web/components/layout/root.html.heex, there is a ` tag to anassets/app.cssfile. ```html ``` This finds theassets/css/app.cssfile, which contains all our CSS for the application. ```elixir @import "tailwindcss/base"; @import "tailwindcss/components"; @import "tailwindcss/utilities"; /* This file is for your main application CSS */ ``` We can use [CSS](./html_css.livemd), or [Tailwind](./tailwind.livemd) to style our HEEX HTML. ## HEEx We can embed Elixir in our template files using the following syntax. ```elixir <%= expression %> ``` Whereexpressionis our Elixir code, for example, replacelib/counterweb/templates/counter/index.html.heexwith the following. ```elixir <%= 1 + 1 %> ``` We can write essentially any Elixir expression. So, for example, we could write anifstatement to render different HTML depending on some condition. ```elixir <%= if DateTime.utc_now().hour > 12 do %> forcomprehension. Often we use this to create multiple elements based on a collection. ```elixir <%= for int <- 1..10 do %> =symbol. Expressions that don't output a value (or continue the current expression) omit the=symbol. ## Model (Counter Implementation) Business logic belongs in thelib/counterfolder, not thelib/counterwebfolder. 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 aCountcontext responsible for retrieving and exposing the current count of the counter application. Each context has a main file under thelib/counterfolder, which matches the name of the context, so ourCountcontext module belongs in alib/counter/count.exfile. The context will expose anincrement/1andget/0function for returning and incrementing a count stored in memory. Create thelib/counter/count.exfile with the following content. ```elixir 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 aCounter.Count.CounterServermodule in alib/counter/count/countserver.exfile. This module defines the GenServer that will store the count in memory and expose handlers for retrieving and incrementing the count. ```elixir 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 theget/0andincrement/1functions in theCountmodule. ```elixir 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 Thelib/counter/application.exdefines our Elixir application and the services that are part of our application. We can start our workers and supervisors in this file. Add theCounter.Count.CounterServermodule to thestart/2function inapplication.ex. ```elixir @impl true def start(_type, _args) do children = [ # Start the Telemetry supervisor CounterWeb.Telemetry, # Start the Ecto repository Counter.Repo, # Start the PubSub system {Phoenix.PubSub, name: Counter.PubSub}, # Start Finch {Finch, name: Counter.Finch}, # 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. ```sh $ 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 theCounter.Countcontext directly from the IEx shell and ensure it works as expected. ```elixir 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 theCounterWeb.CounterControllerand then return the current count as the response to the client. ```elixir defmodule CounterWeb.CounterController do use CounterWeb, :controller def count(conn, _params) do count = Counter.Count.get() render(conn, "count.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 thepost/4macro to create a new post/incrementroute inlib/counterweb/router.ex. ```elixir scope "/", CounterWeb do pipe_through :browser get "/", PageController, :home get "/count", CounterController, :count post "/count", CounterController, :increment end ``` A client will make an HTTP POST request to the/countrouter which triggers theCounterController.update/2function. Create anincrement/2function in theCounterControllermodule which increments the count and redirects the user back to the/countpage to refresh the count. ```elixir def update(conn, _params) do Counter.Count.increment() redirect(conn, to: "/count") end ``` ## Forms We can use [HTML Forms](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form) to send HTTP POST requests from within a web page. We can use raw HTML, or alternatively [Phoenix.HTML.Form](https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html) provides functions for creating form elements. Here's a form which will send a POST request to the/countroute and cause our count to increment.@connis the [Plug.Conn](https://hexdocs.pm/plug/Plug.Conn.html) struct automatically provided by theCounterController. ```elixir The current count is <%= @count %> <%= Phoenix.HTML.Form.form_for @conn, "/count", fn _f -> %> <%= Phoenix.HTML.Form.submit("Increment") %> <% end %> ``` We'll dive deeper into forms in a later lesson. For now, know that theformfor/3macro generates HTML. ```html Increment ``` The [Cross-Site Request Forgery (CSRF)](https://en.wikipedia.org/wiki/Cross-site_request_forgery) security token helps prevent CSRF attacks. Visit http://localhost:4000/count and press the increment button. The count should increment on the page. ## Body Parameters We can include body parameters in the HTTP POST request by adding inputs to the form. See [Phoenix.HTML.Form Functions](https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html#functions) for a full list of available inputs. Let's add a number input to our form incount.html.heex. ```elixir The current count is <%= @count %> <%= Phoenix.HTML.Form.form_for @conn, "/count", fn f -> %> <%= Phoenix.HTML.Form.number_input(f, :increment_by) %> <%= Phoenix.HTML.Form.submit("Increment") %> <% end %> ``` The:incrementbyatom is the key of the value associated with the input. We can retrieve the value in the form usingconn.body_paramsin the controller. Let's inspectconn.body_paramsin theCounterController. ```elixir def update(conn, _params) do IO.inspect(conn.params) Counter.Count.increment() redirect(conn, to: "/count") end ``` Enter an integer such as2in 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_by" => "2" } ``` The_csrf_tokencomes from the hidden input to validate the request, and theincrementcomes from our form input. Notice that the“increment_by”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. ```elixir def update(conn, _params) do increment_by = String.to_integer(conn.params["increment_by"]) Counter.Count.increment(increment_by) 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 Theform_for/3macro provides several conveniences under the hood. For example, by using@connwith 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 have5as 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 theredirect/3macro in theCounterController. ```elixir def update(conn, _params) do increment = String.to_integer(conn.params["increment"]) 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. ## Verified Routes Phoenix 1.7 introduced [Verified Routes](https://hexdocs.pm/phoenix/1.7.0-rc.0/Phoenix.VerifiedRoutes.html) using the~psigil. These routes replace [Path Helpers](https://hexdocs.pm/phoenix/routing.html#path-helpers). We've been using static routes for demonstration purposes, however we should use routes with~pto ensure that the route exists at compile time. Replacecounter_web/controllers/counter_html/count.html.heexwith the following content. ```elixir The current count is <%= @count %> <%= Phoenix.HTML.Form.form_for @conn, ~p"/count", fn f -> %> <%= Phoenix.HTML.Form.number_input(f, :increment_by) %> <%= Phoenix.HTML.Form.submit("Increment") %> <% end %> ``` Try changing~p”/count”to~p”not_found”` and notice that you get a compile time warning.
You can run the following in a terminal to view the warning as a compile-time error.
$ mix compile --warnings-as-errors Compiling 1 file (.ex) warning: no route path for CounterWeb.Router matches "/not_found" lib/counter_web/controllers/counter_html/count.html.heex:3: CounterWeb.CounterHTML.count/1 Compilation failed due to warnings while using the --warnings-as-errors option
## Further Reading
For more on Phoenix, consider the following resources.
Phoenix HexDocs
Plug HexDocs
* Phoenix a Web Framework for the New Web • José Valim • GOTO 2016
## Mark As Completed
```elixir
file_name = Path.basename(Regex.replace(~r/#.+/, __ENV.file, “”), “.livemd”)
save_name =
case Path.basename(__DIR) do
“reading” -> “phoenix_1.7_reading”
“exercises” -> “phoenix_1.7_exercise”
end
progress_path = __DIR <> “/../progress.json”
existing_progress = File.read!(progress_path) |> Jason.decode!()
default = Map.get(existing_progress, save_name, false)
form =
Kino.Control.form(
[
completed: input = Kino.Input.checkbox(“Mark As Completed”, default: default)
],
report_changes: true
)
Task.async(fn ->
for %{data: %{completed: completed}} <- Kino.Control.stream(form) do
File.write!(
progress_path,
Jason.encode!(Map.put(existing_progress, save_name, completed), pretty: true)
)
end
end)
form
## Commit Your Progress Run the following in your command line from the curriculum folder to track and save your progress in a Git commit. Ensure that you do not already have undesired or unrelated changes by running `git status` or by checking the source control tab in Visual Studio Code.
$ git checkout -b phoenix-1.7-reading
$ git add .
$ git commit -m “finish phoenix 1.7 reading”
$ git push origin phoenix-1.7-reading
`` Create a pull request from yourphoenix-1.7-readingbranch to yoursolutionsbranch. Please do not create a pull request to the DockYard Academy repository as this will spam our PR tracker. **DockYard Academy Students Only:** Notify your teacher by including@BrooklinJazz` in your PR description to get feedback.
You (or your teacher) may merge your PR into your solutions branch after review.
If you are interested in joining the next academy cohort, sign up here to receive more news when it is available.