Form validation with Phoenix LiveView

This post was updated 01 May - 2020

liveview forms phoenix

Form validation with Phoenix LiveView One thing that has always been problematic is when providing live form validation for a user that interacts with a form. The problem has always been that you need to have validation logic in both backend and frontend. That is of course not optimal. However, with Phoenix LiveView, the problem with providing form validation is to a large extent solved. Let me explain how. ### STEP 1 In this tutorial I already have a project setup but I want to generate a resource for products. ``` mix phx.gen.html Products Product products name description:text price:float ``` When that is generated, follow the instructions and add the resources in the routes and run ``` mix ecto.migrate ``` I can now start the server with ``` mix phx.server ``` And navigate to `http://localhost:4000/products/new` and see the form. ![](https://res.cloudinary.com/dwvh1fhcg/image/upload/w_650,c_scale/v1579678301/tutorials/tut_3_img_1.png) ### Step 2 Next step is to customize some validation rules. For example, I want to make sure that the name needs to be of a specific length and the price needs to be larger than zero. In the file `lib/tutorial/products/product.ex`, I can add the extra validation rules in the bottom of the changeset function. ```elixir def changeset(product, attrs) do product |> cast(attrs, [:name, :description, :price]) |> validate_required([:name, :description, :price]) |> validate_length(:name, min: 2) |> validate_number(:price, greater_than: 0) end ``` ### Step 3 Now, I can stop the server and create a folder called `live` inside the `/tutorial_web/` folder. Then create the file `lib/tutorial_web/live/product_form_live.ex`. The initial content will be a very basic LiveView component. ``` defmodule TutorialWeb.ProductFormLive do use Phoenix.LiveView def mount(%{"action" => action, "csrf_token" => csrf_token}, socket) do assigns = [ conn: socket, action: action, csrf_token: csrf_token ] {:ok, assign(socket, assigns)} end def render(assigns) do TutorialWeb.ProductView.render("form.html", assigns) end end ``` Note that I expect `%{"action" => action, "csrf_token" => csrf_token}` to be included when I eventially call the LiveView component. As you can see, I am referencing a template that I have yet to create. I need to rename the `form.html.eex` to the LiveView compatible `form.html.leex`. The LiveView component are included in both when you are creating a product and editing a product. So I need to update the code in both `lib/tutorial_web/templates/product/new.html.eex` and `lib/tutorial_web/templates/product/edit.html.eex` I need to update those file to call the actual LiveView component. Replace: ```erb <%= render "form.html", Map.put(assigns, :action, Routes.product_path(@conn, :create)) %> ``` With: ```erb <%= live_render @conn, TutorialWeb.ProductFormLive, session: %{"action" => Routes.product_path(@conn, :create), "csrf_token" => Plug.CSRFProtection.get_csrf_token()} %> ``` When I start the server now, and navigate to `http://localhost:4000/products/new` I can see the error: ![](https://res.cloudinary.com/dwvh1fhcg/image/upload/w_650,c_scale/v1579678301/tutorials/tut_3_img_2.png) I need to add the changeset to the LiveView component. In the top add: ```elixir alias Videos.Products alias Videos.Products.Product def mount(%{a"action" => action, "csrf_token" => csrf_token}, socket) do assigns = [ conn: socket, action: action, csrf_token: csrf_token, changeset: Products.change_product(%Product{}) ] {:ok, assign(socket, assigns)} end ``` Now, when refreshing the page the form should be visible. However, if I enter something, I dont get the form validations. For that to work, I need to tell the component that it should validate. in `lib/tutorial_web/templates/product/form.html.leex` I need to make 2 changes. First its the `phx_change: :validate` and `csrf_token: @csrf_token`. ```erb <%= form_for @changeset, @action, [phx_change: :validate, class: "block", csrf_token: @csrf_token], fn f -> %> ... <% end %> ``` The `csrf_token` needs to be specified so that Phoenix recognises and allows the form to be submittet. And back in the LiveView component, I need to add the event handler for the validation: ```elixir def handle_event("validate", %{"product" => product_params}, socket) do changeset = %Product{} |> Product.changeset(product_params) |> Map.put(:action, :insert) {:noreply, assign(socket, changeset: changeset)} end ``` If i go to `http://localhost:4000/products/new` and test this out by typing a character in the name field I can see that it works. Kind of.. ![](https://res.cloudinary.com/dwvh1fhcg/image/upload/w_650,c_scale/v1579678301/tutorials/tut_3_img_3.png) First, one issue is that the alert error is displayed. Second, it displays the error messages for the fields I havent even touched. To fix the first issue, I will just remove the alert from the file `lib/tutorial_web/templates/product/form.html.leex`. Actually, I dont even want anyone to be able to submit the form if invalid, so I will change the submit to: ```erb <%= submit "Save", class: "btn btn-primary mr-2", disabled: !@changeset.valid? %> ``` and update CSS to: ```css .btn.disabled, .btn:disabled, input[type="submit"]:disabled { opacity: .65; @apply pointer-events-none; } ``` To fix the second issue with displaying errors in all fields, I need to open `lib/tutorial_web/views/error_helpers.ex ` and in the function `def error_tag(form, field) do` and add `, data: [phx_error_for: input_id(form, field)`: ``` def error_tag(form, field) do Enum.map(Keyword.get_values(form.errors, field), fn error -> field_name = field |> Atom.to_string() |> String.capitalize() content_tag(:span, "#{field_name} #{translate_error(error)}", class: "block mt-1 text-sm text-red-700", data: [phx_error_for: input_id(form, field)]) end) end ``` ### Final result and comments So, this now works when adding a new resource. ![](https://res.cloudinary.com/dwvh1fhcg/image/upload/w_650,c_scale/v1579678301/tutorials/tut_3_img_4.png) However, the code needs to change slightly to make it work with edit a resource so [i have a full example on Github](https://github.com/andreaseriksson/tutorials/commit/53ade0afcdaf57b0534b9f1e2369f6af3d580c0f) But basically we just need to make sure we have an existing product or a new `%Product{}`.

Related Tutorials

Published 14 Feb - 2020
Updated 01 May - 2020

Improving LiveView UX with Phoenix Channels - Tagging part 3

In the previous tutorial I set up the tagging interface. It had however a small issue. If I added a tag, it didnt really refocus on the input so I n..

Published 13 Feb - 2020
Updated 01 May - 2020

Tagging interface with Phoenix LiveView and Tailwind - Tagging part 2

In the previous tutorial, I set up the the backend for being able to add tags to products. I have also written a tutorial about adding a LiveView an..