DEV Community

Cover image for Server & Client Side Forms Validations With [Elixir, Phoenix, LiveView and Tailwind CSS | No Javascript]
Anthony Gonzalez
Anthony Gonzalez

Posted on • Originally published at elixirprogrammer.com

Server & Client Side Forms Validations With [Elixir, Phoenix, LiveView and Tailwind CSS | No Javascript]

 
 

 
 

Learn how to use Elixir/Phoenix to validate forms on the client and server-side, no Javascript code is needed thanks to the LiveView library, Tailwind CSS is going to be used for the user interface.

Create New Project:

Let's start by creating a new phoenix project from scratch inside our desired directory, by going to the command line and typing the following command:

$ mix phx.new form_validator --no-dashboard && cd form_validator

Then $ mix ecto.create

Tailwind CSS Installation & Configuration:

Let's add and configure Tailwind CSS, go to the website https://hex.pm and grab the latest version of the tailwind package library, at the time of the making of this tutorial the latest version is 0.1.5, add the following to your project dependencies inside your mix.exs file:

{:tailwind, "~> 0.1.5"}

Also, in that same file under aliases add the following:

"assets.deploy": ["tailwind default --minify", ..., "phx.digest"]

Go to your config/config/exs file and add the following configuration:

config :tailwind,
  version: "3.0.23",
  default: [
    args: ~w(
      --config=tailwind.config.js
      --input=css/app.css
      --output=../priv/static/assets/app.css
    ),
    cd: Path.expand("../assets", __DIR__)
  ]
Enter fullscreen mode Exit fullscreen mode

Go to your config/dev.exs file and add the following to your watchers:

tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]}
Enter fullscreen mode Exit fullscreen mode

Get back to the command line and type in $ mix deps.get && mix tailwind.install to fetch your new dependency and install tailwind.

LiveView Template:

Run the server with the following command in your terminal:

$ iex -S mix phx.server

Open your lib/form_validator_web/router.ex file and remove the root path and change it to the following:

  scope "/", FormValidatorWeb do
    pipe_through :browser

    live "/", PageLive
  end
Enter fullscreen mode Exit fullscreen mode

Open your lib/form_validator_web/templates/layout/root.html.heex and remove the header nav, your html body tag should look like the following:

  <body class="bg-violet-500">
    <%= @inner_content %>
  </body>
Enter fullscreen mode Exit fullscreen mode

To your lib/form_validator_web/templates/layout/live.html.heex file, add the following css class to your main and p tag:

<main  class="container mx-auto max-w-full">
    <p  class="alert alert-info flex"  role="alert"
Enter fullscreen mode Exit fullscreen mode

Add the following folder to lib/form_validator_web:

live

Add the following file to that newly created live folder:

page_live.ex

Add the following to that newly created file lib/form_validator_web/live/page_live.ex:

defmodule FormValidatorWeb.PageLive do
  use FormValidatorWeb, :live_view
end
Enter fullscreen mode Exit fullscreen mode

Under lib/form_validator_web/live add the following file:

page_live.html.heex

<section class="mt-24 w-1/2 shadow flex flex-col items-center justify-center mx-auto p-6 bg-white">
  <h1 class="text-4xl font-bold italic text-gray-700">
    Create Account
  </h1>

  <p class="text-gray-500 font-semibold text-lg mt-6 text-center px-8">
  Sign up to get an account.
  </p>

  <.form
    let={f}
    for={:user}
    id="user-form"
    class="flex flex-col space-y-4 w-full px-6">

    <div class="flex flex-col">
      <%= label f, :email, class: "text-gray-400" %>
      <%= email_input f, :email, autocomplete: "off", class: "rounded shadow-sm focus:ring-opacity-50 border-gray-300 focus:border-gray-900 focus:ring-gray-900" %>
      <%= error_tag f, :email %>
    </div>

    <div class="flex flex-col">
      <%= label f, :username, class: "text-gray-400" %>
      <%= text_input f, :username, autocomplete: "off", class: "rounded shadow-sm focus:ring-opacity-50 border-gray-300 focus:border-gray-900 focus:ring-gray-900" %>
      <%= error_tag f, :username %>
    </div>

    <div class="flex flex-col">
      <%= label f, :password, class: "text-gray-400" %>
      <%= password_input f, :password, class: "rounded shadow-sm focus:ring-opacity-50 border-gray-300 focus:border-gray-900 focus:ring-gray-900" %>
      <%= error_tag f, :password %>
    </div>

    <div>
      <%= submit "Sign up", phx_disable_with: "Saving...", class: "w-full py-2 border-none shadow rounded font-semibold text-sm text-gray-50 hover:bg-violet-600 bg-violet-500" %>
    </div>

  </.form>

  <p class="text-sm px-10 text-center mt-6 text-gray-400 font-semibold">
    By signing up, you agree to our Terms , Data Policy and Cookies Policy.
  </p>
</section>
Enter fullscreen mode Exit fullscreen mode

LiveView Tests:

Add the following folder under test/form_validator_web:

live

Add the following file to that newly created folder:

page_live_test.exs

To that newly created file test/form_validator_web/live/page_live_test.exs add the following tests:

defmodule FormValidatorWeb.PageLiveTest do
  use FormValidatorWeb.ConnCase

  import Phoenix.LiveViewTest

  @create_attrs %{
    username: "somename",
    email: "test@test.com",
    password: "Password1"
  }
  @invalid_attrs %{
    username: nil,
    email: nil,
    password: nil
  }

  test "renders form title", %{conn: conn} do
    {:ok, _page_path, html} = live(conn, "/")

    assert html =~ "Create Account"
  end

  test "saves new account", %{conn: conn} do
    {:ok, page_path, _html} = live(conn, "/")

    assert page_path
           |> form("#user-form", user: @invalid_attrs)
           |> render_change() =~ "can&#39;t be blank"

    {:ok, _, html} =
        page_path
        |> form("#user-form", user: @create_attrs)
        |> render_submit()
        |> follow_redirect(conn, "/")

    assert html =~ "Account created successfully"
  end

  test "validates email", %{conn: conn} do
    invalid_email = %{email: "email"}
    {:ok, page_path, _html} = live(conn, "/")

    assert page_path
           |> form("#user-form", user: invalid_email)
           |> render_change() =~ "must have the @ sign and no spaces"
  end

  test "validates username", %{conn: conn} do
    {:ok, page_path, _html} = live(conn, "/")

    assert page_path
           |> form("#user-form", user: %{username: "a"})
           |> render_change() =~ "should be at least 5 character(s)"

    assert page_path
           |> form("#user-form", user: %{username: "user$"})
           |> render_change() =~ "Please use letters and numbers without space(only characters allowed _ . -)"
  end

  test "validates password", %{conn: conn} do
    {:ok, page_path, _html} = live(conn, "/")

    assert page_path
           |> form("#user-form", user: %{password: "a"})
           |> render_change() =~ "should be at least 6 character(s)"

    assert page_path
           |> form("#user-form", user: %{password: "."})
           |> render_change() =~ "at least one lower case character"

    assert page_path
           |> form("#user-form", user: %{password: "."})
           |> render_change() =~ "at least one upper case character"

    assert page_path
           |> form("#user-form", user: %{password: "."})
           |> render_change() =~ "at least one digit or punctuation character"
  end
end
Enter fullscreen mode Exit fullscreen mode

Phoenix Context:

Let's create a context, by going to the command line and typing the following command:

$ mix phx.gen.context Accounts User users email:string username:string password:string

Then $ mix ecto.migrate

Add your validation to your users schema changeset, open lib/form_validator/accounts/user.ex and add the following:

  def changeset(user, attrs) do
    user
    |> cast(attrs, [:email, :username, :password])
    |> validate_required([:email, :username, :password])
    |> validate_email()
    |> validate_username()
    |> validate_password()
  end

  defp validate_email(changeset) do
    changeset
    |> validate_format(:email, ~r/^[^\s]+@[^\s]+$/, message: "must have the @ sign and no spaces")
    |> validate_length(:email, max: 160)
    |> unique_constraint(:email)
  end

  defp validate_username(changeset) do
    changeset
    |> validate_format(:username, ~r/^[a-zA-Z0-9_.-]*$/, message: "Please use letters and numbers without space(only characters allowed _ . -)")
    |> validate_length(:username, min: 5, max: 30)
    |> unique_constraint(:username)
  end

  defp validate_password(changeset) do
    changeset
    |> validate_length(:password, min: 6, max: 80)
    |> validate_format(:password, ~r/[a-z]/, message: "at least one lower case character")
    |> validate_format(:password, ~r/[A-Z]/, message: "at least one upper case character")
    |> validate_format(:password, ~r/[!?@#$%^&*_0-9]/, message: "at least one digit or punctuation character")
  end
Enter fullscreen mode Exit fullscreen mode

Validate Event:

Open your liveview file lib/form_validator_web/live/page_live.ex and add the following:

  alias FormValidator.Accounts
  alias FormValidator.Accounts.User

  def mount(_params, _session, socket) do
    changeset = Accounts.change_user(%User{})

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

  def handle_event("validate", %{"user" => user_params}, socket) do
    changeset =
      %User{}
      |> Accounts.change_user(user_params)
      |> Map.put(:action, :validate)

    {:noreply, assign(socket, changeset: changeset)}
  end
Enter fullscreen mode Exit fullscreen mode

Go to your liveview template lib/form_validator_web/live/page_live.html.heex and add the form and the validate phoenix change bind event:

    for={@changeset}
    phx-change="validate"
Enter fullscreen mode Exit fullscreen mode

Now we need to fix the error tag class for our layout, open lib/form_validator_web/views/error_helpers.ex and change the error tag funtion to the following:

  def error_tag(form, field, class \\ [class: "invalid-feedback"]) do
    Enum.map(Keyword.get_values(form.errors, field), fn error ->
      content_tag(:span, translate_error(error),
        class: Keyword.get(class, :class),
        phx_feedback_for: input_name(form, field)
      )
    end)
  end
Enter fullscreen mode Exit fullscreen mode

Now we can add our own class to the error tags, go back to the lib/form_validator_web/live/page_live.html.heex liveview template file and the following class to all of the error tags:

<%= error_tag f, :email, class: "text-red-700 text-sm" %>
<%= error_tag f, :username, class: "text-red-700 text-sm" %>
<%= error_tag f, :password, class: "text-red-700 text-sm" %>
Enter fullscreen mode Exit fullscreen mode

Now let's add a function to change the inputs colors to red when there's error in our form, go back to lib/form_validator_web/views/error_helpers.ex file and add the following function:

  def error_ring(f, field) do
    with :validate <- f.source.action do
      case Keyword.fetch(f.errors, field) do
        {:ok, _} ->
          "border-red-700 focus:border-red-700 focus:ring-red-700"
        :error ->
          "border-green-500 focus:border-green-500 focus:ring-green-500"
      end
    else
      _ ->
        "border-gray-300 focus:border-gray-900 focus:ring-gray-900"
    end
  end
Enter fullscreen mode Exit fullscreen mode

Now back to our lib/form_validator_web/live/page_live.html.heex file let's remove the border classes on our inputs and add the our new function:

<%= email_input f, :email, autocomplete: "off", class: "rounded shadow-sm focus:ring-opacity-50 #{error_ring(f, :email)}" %>
<%= text_input f, :username, autocomplete: "off", class: "rounded shadow-sm focus:ring-opacity-50 #{error_ring(f, :username)}" %>
<%= password_input f, :password, class: "rounded shadow-sm focus:ring-opacity-50 #{error_ring(f, :password)}" %>
Enter fullscreen mode Exit fullscreen mode

Save Event:

Let's add our save handle event function, first open lib/form_validator_web/live/page_live.html.heex file and add the phoenix submit bind event to our form:

    phx-submit="save"
Enter fullscreen mode Exit fullscreen mode

Then back to the lib/form_validator_web/live/page_live.ex liveview add the following function:

  def handle_event("save", %{"user" => user_params}, socket) do
    case Accounts.create_user(user_params) do
      {:ok, _user} ->
        {:noreply,
         socket
         |> put_flash(:info, "Account created successfully")
         |> push_redirect(to: "/")}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, changeset: changeset)}
    end
  end
Enter fullscreen mode Exit fullscreen mode

Go to http://localhost:4000 to test your application manually as a user, and if you go to the command line and run the tests with mix test test/form_validator_web/live they should pass with no failure.

Conclusion

Thank you so much for your time, I really appreciate it. We added Tailwind CSS to our Phoenix application, and went to the steps of handling forms validations on the server-side and client-side without the need for javascript, using the phoenix change bind event and customizing the error helpers functions for our template when errors on our form.

 
 

Join The Elixir Army

Top comments (0)