Fog Lit Street logo

Creating A Liveview Form

venomnert

What we'll be building

I’ll be walking you through on how to build a simple contact us form that will capture the following information about the user:

Github Repo

  • First name: String
  • Last name: String
  • Email address: String
  • Phone number: String
  • Company(Optional): String
  • Service required: String

Why use liveview for forms?

Current forms work by sending a post request to our controller that will process the request and send back an error or success message. One way or another if we aren’t using JS our form page would have to re-render in order to display the message. I’m not a fan of this, and this is where liveview can help by providing a better user experience.

LiveView Form

Building our contact us form

We'll be using Elixir 1.9 and Phoenix 1.4

  1. Create our phoenix project

      mix phx.new contact_us
      mix ecto.setup
    
  2. Let's start our project to make sure everything is setup properly

      iex -S mix phx.server
    
  3. Let's generate our context schema, controllers, view and template for our client model.

      mix phx.gen.html Accounts Client clients first_name:string last_name:string email_address:string phone_number:string company:string service:string
      mix ecto.migrate
    
  4. Let's update our router.ex to include all the CRUD endpoints for our client

      scope "/", ContactUsWeb do
        pipe_through :browser
      
        get "/", PageController, :index
        resources "/client", ClientController
      end
    

With that we have created a simple form in which a client can submit their information to us. The form has to re-render the page on each form submission in order validate the form's entry. With our basis setup, it's speedy re-render. But sometimes a page re-render is not desired, so in the next step we'll convert our regular form to liveview, which doesn't require page re-render on form submits.

Setting up liveview

Since phoenix doesn't come with liveview, we need to included it into our project and set it up. Rather than reinventing the wheel, I'm going to be following ElixirCast's video on setting up liveview along with Phoenix official setup doc.

  1. Let's add liveview in our mix.exs file:

      defp deps do
        [
          {:phoenix, "~> 1.4.10"},
          ...
          {:phoenix_live_view, "~> 0.4.0"},
        ]
      end
    

    Get the dependencies:

      mix deps.get
    
  2. Generate a signing salt

      mix phx.gen.secret 32
      XApE7hADbc0TUm3/NbIPHzIIK/1iG/lr
    

    Add the salt to our endpoint config found in config.exs

      config :contact_us, ContactUsWeb.Endpoint,
        url: [host: "localhost"],
        live_view: [
          signing_salt: "XApE7hADbc0TUm3/NbIPHzIIK/1iG/lr"
        ],
        ...
    
  3. We next add liveview flash within router.ex

      pipeline :browser do
          ...
        plug :fetch_flash
        plug Phoenix.LiveView.Flash
        ...
      end
    
  4. Updated lib/contact_us_web.ex accordingly

      ...
      def controller do
        quote do
          ...
          import Phoenix.LiveView.Controller
        end
      end
        
      def view do
        quote do
          ...
          import Phoenix.LiveView,
            only: [
              live_render: 2, 
              live_render: 3, 
              live_link: 1, 
              live_link: 2,
              live_component: 2, 
              live_component: 3, 
              live_component: 4
            ]  
        end
      end
        
      def router do
        quote do
          ...
          import Phoenix.LiveView.Router
        end
      end
    
      ...
    
  5. Let's first extract our session info currently utilized by Plug.Session , into a module attribute, because this same info will be required for our liveview socket.

    Here is the current implementation:

      defmodule ContactUsWeb.Endpoint do
      
        ... 
    
        plug Plug.Session, 
          store: :cookie,
          key: "_contact_us_key",
          signing_salt: "piqiBzEh"
      
        plug ContactUsWeb.Router
      end
    

    After extracting the session here is the following code

      defmodule ContactUsWeb.Endpoint do
      
        @session_options [
          store: :cookie,
          key: "_contact_us_key",
          signing_salt: "piqiBzEh"
        ]
      
        ...
      
        plug Plug.Session, @session_options
    
        plug ContactUsWeb.Router
      end
    
  6. Expose our liveview socket in our lib/contact_us_web/endpoint.ex

      defmodule ContactUsWeb.Endpoint do
        use Phoenix.Endpoint, otp_app: :contact_us
      
        @session_options [
          store: :cookie,
          key: "_contact_us_key",
          signing_salt: "piqiBzEh"
        ]
      
        socket "/live", Phoenix.LiveView.Socket,
        websocket: [connect_info: [session: @session_options]]
      
        ...
      
        plug Plug.Session, @session_options
      
        plug ContactUsWeb.Router
      end
    
  7. We need to include the phoenix liveview npm package in our assets/package.json

      ...
      "dependencies": {
        "phoenix": "file:../deps/phoenix",
        "phoenix_html": "file:../deps/phoenix_html",
        "phoenix_live_view": "file:../deps/phoenix_live_view"
      },
      ...
    

    Get the depenencies

      npm install --prefix assets
    
  8. Import and initialize liveview socket in app.js

      import css from "../css/app.css"
      import "phoenix_html"
      
      import {Socket} from "phoenix"
      import LiveSocket from "phoenix_live_view"
      
      let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");
      let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}});
      liveSocket.connect()
    
  9. We need to automatically generate csrf_meta_tag which will be used to setup the live view socket in the front end. Add the following snippet of code within contact_us/lib/contact_us_web/templates/layout/app.html.eex file

      <head>
        <meta charset="utf-8"/>
        <%= csrf_meta_tag() %>
        ...
      </head>
    
  10. Let's convert our form into a liveview template, first let's update the controller for /client/new route

      defmodule ContactUsWeb.ClientController do
        use ContactUsWeb, :controller
      
        alias ContactUs.Accounts
        alias Phoenix.LiveView
      
        def new(conn, _params) do
          changeset = Accounts.change_client(%Client{})
          LiveView.Controller.live_render(conn, ContactUsWeb.ClientLive.Index, session: %{})
          # render(conn, "new.html", changeset: changeset)
        end
      
        ...
      
      end
    
  11. After having setup our controller to render the liveview, we need to create the live module. Create a new module, within live directory. The full pathing is the following lib/contact_us_web/live/client_live/index.ex

      defmodule ContactUsWeb.ClientLive.Index do
        use Phoenix.LiveView
        def mount(_session, socket) do
          {:ok, socket}
        end
      
        def render(assigns) do
          ~L"""
          Greetings from Live View
          """
        end
      end
    

    Tip: Use the following command to created nested directories mkdir -p lib/contact_us/live/client_live

  12. Head on over to the url to see the newly created liveview page: http://localhost:4000/client/new

      iex -S mix phx.server
    

Implementing liveview form

We'll be picking up from the previous section.

  1. Let's first update ContactUsWeb.ClientLive.Index.render function to use the form.html template

      defmodule ContactUsWeb.ClientLive.Index do
        use Phoenix.LiveView
        alias ContactUsWeb.ClientView
      
        ...
      
        def render(assigns) do
          ClientView.render("form.html", assigns)
        end
      
      end
    
  2. Update form.html.eex to form.html.leex format.

  3. Once that's done if we re-run the server we'll get an error assign @changeset not available in eex template. Which informs us that the @changeset assign in our template isn't available. In liveview the assign within our render/1 function comes from the socket defined in the mount/2function. Currently the assign is passed within our ClientController. Since we updated the render to utilize live_render we need to pass the changeset via the socket.assign as opposed to conn.assign. Let's update our mount within the following file lib/contact_us_web/live/client_live/index.ex

      defmodule ContactUsWeb.ClientLive.Index do
        use Phoenix.LiveView
        
        alias ContactUsWeb.ClientView
        alias ContactUs.Accounts
        alias ContactUs.Accounts.Client
      
        def mount(_session, socket) do
          value = %{
            changeset: Accounts.change_client(%Client{}),
            form_data: %{                # This is the form data to be captured and utilized to create a new client
              "first_name" => "",
              "last_name" => "",
              "email_address" => "",
              "phone_number" => "",
              "company" => "",
              "service" => "",
            }
          }
          {:ok, assign(socket, value)}
        end
        ...
      end
    
  4. If we were to run the server now...we'll get another error message assign @action not available in eex template. Let's resolve this. You may have seen form_for/3 being used more commonly; however, we'll be using form_for/4 in our case. Let's take a quick look at the arguments we'll be passing to form_for/4:

    1. @changeset: this will be the argument that get's passed to your form function, argument #4
    2. action: the path you want to send the post request to
    3. options: are attributes that gets added to the form tag
    4. anonymous function: the function that will utilize the changeset to generate input fields.
      <%= form_for @changeset, "#", [phx_change: :validate, phx_submit: :save] ,fn f -> %>
        <%= if @changeset.action do %>
          <div class="alert alert-danger">
            <p>Oops, something went wrong! Please check the errors below.</p>
          </div>
        <% end %>
    
        <%= label f, :first_name %>
        <%= text_input f, :first_name, value: @form_data["first_name"] %>
        <%= error_tag f, :first_name %>
        
        ...
    
        <div>
          <%= submit "Save", phx_disable_with: "Saving..." %>
        </div>
      <% end %>
    

    For instance phx_submit: :save option will bind to our form so when user click submit we can handle it using handle_event

    Liveview form attributes

    Also while we are at it, we can remove the default changeset messaging

      <%= form_for @changeset, "#", [phx_change: :validate, phx_submit: :save], fn f -> %>
    
        <%= label f, :first_name %>
        <%= text_input f, :first_name, value: @form_data["first_name"] %>
        <%= error_tag f, :first_name %>
        
        ...
    
        <div>
          <%= submit "Save", phx_disable_with: "Saving..." %>
        </div>
      <% end %>
    
  5. Let's capture and validate our input changes and display the data we'll be working with lib/contact_us_web/live/client_live/index.ex

      def handle_event("validate", %{"client" => params} = args, socket) do
          IO.inspect(args, label: "VALIDATE DATA")
      
          {:noreply, socket}
      end
    

    And now if were to type something in our input we should see it getting logged in our console:

    validated form data

  1. Let's create a custom form validation function

    contact_us/lib/contact_us/accounts/client.ex

      defmodule ContactUs.Accounts.Client do
        
        ...
      
        def validate_form_input(%Ecto.Changeset{changes: %{phone_number: number}} = changeset) do
          validate_phone_number(number)
          |> case do
              :error ->
                {:error, changeset
                        |> Map.put(:action, :insert)
                        |> add_error(:phone_number, "Number must be valid number")}
    
              _ -> {:ok, changeset}
            end
        end
    
        defp validate_phone_number(""), do: :ok
        defp validate_phone_number(value), do: Integer.parse(value)
      end
    

    Note: Why add action: insert https://hexdocs.pm/phoenix_html/Phoenix.HTML.Form.html#module-a-note-on-errors

    Tip: Click on WS option within chrome developer network tab to see socket messages

    contact_us/lib/contact_us/accounts.ex

      defmodule ContactUs.Accounts do
    
        import Ecto.Changeset
        ... 
      
        def validate_form_input(attrs) do
          %Client{}
          |> change(format_form_input(attrs))
          |> Client.validate_form_input()
        end
    
        defp format_form_input(attrs) do
          for {k,v} <- attrs, into: %{}, do: {String.to_existing_atom(k), v}
        end
      end
    
  2. Realtime form input validation contact_us/lib/contact_us_web/live/client_live/index.ex

      def handle_event("validate", %{"client" => params} = args, socket) do
      
        # First determine which input field was updated
        target = args["_target"] |> List.last()
      
        # Then updated our internal form state with the new input field value
        form_data = socket.assigns.form_data
                    |> Map.put(target, params[target])
      
        {_, %Ecto.Changeset{} = changeset} = form_data
                                              |> Accounts.validate_form_input() # validate our form data
      
          value = %{changeset: changeset, form_data: form_data}
      
          # Return the newly updated changeset to our form.
          {:noreply, assign(socket, value)}
      end
    
  3. Let's setup our form validation on submit contact_us/lib/contact_us_web/live/client_live/index.ex

      defmodule ContactUsWeb.ClientLive.Index do
        ...
        
        alias ContactUsWeb.Router.Helpers, as: Routes
        ...
    
        def handle_event("save", %{"client" => params} = args, socket) do
          params
          |> Accounts.create_client()
          |> case do
              {:ok, _user} ->
                {:stop,
                socket
                |> put_flash(:info, "Client created")
                |> redirect(to: Routes.client_path(ContactUsWeb.Endpoint, :index))}
        
              {:error, %Ecto.Changeset{} = changeset} ->
                {:noreply, assign(socket, changeset: changeset)}
          end
        end
      end
    
  4. And that's it! For now our liveview form is complete.

Next post we'll look at how liveview work and answer the following questions?

Q: What is the signing salt used for?

Q: What is liveview flash used for?

Q: What's the difference between .eex and .leex?

Update: Sunday December 15, 2019:

  • Updated typos
  • Refactored validation function

Thank:

I would like to thank Denis and Shijith, for their feedbacks!

Resources:

  1. Phoenix Liveview Documentaiton
  2. Poetic Coding Blog
  3. Elixir Cast Liveview tutorial
  4. How to create custom changeset error
  5. Liveview component
← back to all posts