Powered by AppSignal & Oban Pro

Phoenix 1.7

reading/phoenix_1.7.livemd

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

Return Home Report An Issue

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.

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

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.

  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.PageHTML (View) module in lib/counter_web/controllers/page_html.ex renders a response using a template .heex file.
  6. The template in lib/counter_web/controllers/page_html/home.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.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 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 %>

Good afternoon!

<% else %>

Good morning!

<% end %> ``` Or a loop using the
forcomprehension. Often we use this to create multiple elements based on a collection. ```elixir <%= for int <- 1..10 do %>

<%= int %>

<% end %> ``` Notice that all expressions which output a value must use the
=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.