Feature

Basic LiveView form validation

Preview

Inline form validation basically comes out of the box if you generate a new LiveView app. However, if you want to try it out or add it afterwards, this feature can show you how you can very easily implement it.

lib/phoenix_features_web/live/components/form_validation_simple.ex
defmodule PhoenixFeaturesWeb.Components.FormValidationSimple do
  use PhoenixFeaturesWeb, :live_component

  alias PhoenixFeatures.Products
  alias PhoenixFeatures.Products.Product

  @impl true
  def update(assigns, socket) do
    product = %Product{} # Or get it from assigns or the database

    changeset = Products.change_product(product)

    {:ok,
      socket
      |> assign(assigns)
      |> assign(:changeset, changeset)
      |> assign(:product, product)
    }
  end

  @impl true
  def handle_event("validate", %{"product" => product_params}, socket) do
    changeset =
      socket.assigns.product
      |> Products.change_product(product_params)
      |> Map.put(:action, :validate)

    {:noreply, assign(socket, :changeset, changeset)}
  end

  def handle_event("save", %{"product" => product_params}, socket) do
    changeset = Products.change_product(%Product{})

    {:noreply, assign(socket, :changeset, changeset)}
  end
end
<%= f = form_for @changeset, "#",
  id: @id,
  class: "block",
  phx_target: @myself,
  phx_change: "validate",
  phx_submit: "save" %>

  <div class="mb-4">
    <%= label f, :name, class: "inline-block mb-2" %>
    <%= text_input f, :name, class: "block w-full h-auto py-2 px-3 text-base font-normal leading-normal text-gray-700 border border-gray-400 rounded duration-150 transition-colors transition-shadow ease-in-out hover:border-gray-500 focus:border-indigo-200 outline-none" %>
    <%= error_tag f, :name %>
  </div>

  <div class="mb-4">
    <%= label f, :price, class: "inline-block mb-2" %>
    <%= text_input f, :price, class: "block w-full h-auto py-2 px-3 text-base font-normal leading-normal text-gray-700 border border-gray-400 rounded duration-150 transition-colors transition-shadow ease-in-out hover:border-gray-500 focus:border-indigo-200 outline-none" %>
    <%= error_tag f, :price %>
  </div>

  <div class="mb-4 pl-5">
    <%= checkbox f, :published, class: "absolute p-0 mt-1 -ml-5" %>
    <%= label f, :published, class: "form-check-label" %>
    <%= error_tag f, :published %>
  </div>

  <div class="mb-4">
    <%= label f, :description %>
    <%= textarea f, :description, class: "block w-full h-auto py-2 px-3 text-base font-normal leading-normal text-gray-700 border border-gray-400 rounded duration-150 transition-colors transition-shadow ease-in-out hover:border-gray-500 focus:border-indigo-200 outline-none" %>
    <%= error_tag f, :description %>
  </div>

  <div class="mt-4 mb-2">
    <%= submit "Save", phx_disable_with: "Saving...", class: "font-normal border border-transparent py-2 px-3 text-base rounded border border-blue-600 bg-blue-600 text-white hover:border-blue-700 hover:bg-blue-700" %>
  </div>
</form>
defmodule PhoenixFeaturesWeb.Components.FormValidationSimpleTest do
  use PhoenixFeaturesWeb.ConnCase
  import Phoenix.LiveViewTest

  alias PhoenixFeatures.Products

  describe "FormValidationSimple" do
    test "shows the component", %{conn: conn} do
      {:ok, view, _html} = live(conn, Routes.demos_show_path(conn, :show, "form_validation_simple"))

      assert view |> element("#form_validation_simple") |> has_element?()
    end

    test "with invalid input it displays an error message", %{conn: conn} do
      {:ok, view, _html} = live(conn, Routes.demos_show_path(conn, :show, "form_validation_simple"))

      html = view
             |> element("form")
             |> render_change(%{"product" => %{"name" => "a"}})

      assert html =~ "invalid-feedback"
      assert html =~ "should be at least 3 character(s)"
    end

    test "with valid input it doesnt display error messages", %{conn: conn} do
      {:ok, view, _html} = live(conn, Routes.demos_show_path(conn, :show, "form_validation_simple"))

      html = view
             |> element("form")
             |> render_change(%{"product" => %{"name" => "Some name", "price" => 100}})

      refute html =~ "invalid-feedback"
    end
  end
end