Server & Client Side Forms Validations With [Elixir, Phoenix, LiveView and Tailwind CSS | No Javascript]
Tags: elixir, tutorial, programming, webdev
Published: 2022-02-24 21:33:43

&nbsp;
&nbsp;

{% youtube sJMPLoqIS7M %}

&nbsp;
&nbsp;


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:

```elixir
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__)
  ]
```

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

```elixir
tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]}
```

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:

```elixir
  scope "/", FormValidatorWeb do
    pipe_through :browser

    live "/", PageLive
  end
```

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:

```html
  <body class="bg-violet-500">
    <%= @inner_content %>
  </body>
```

To your `lib/form_validator_web/templates/layout/live.html.heex` file, add the following css class to your main and p tag:
```html
<main  class="container mx-auto max-w-full">
	<p  class="alert alert-info flex"  role="alert"
```

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`:

```elixir
defmodule FormValidatorWeb.PageLive do
  use FormValidatorWeb, :live_view
end
```

Under `lib/form_validator_web/live` add the following file:

`page_live.html.heex`

```html
<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>
```

### 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:

```elixir
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
```

### 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:

```elixir
  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
```

### Validate Event:

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

```elixir
  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
```

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:

```elixir
    for={@changeset}
    phx-change="validate"
```

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:

```elixir
  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
```

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:

```elixir
<%= 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" %>
```
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:

```elixir
  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
```
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:

```elixir
<%= 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)}" %>
```

### 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:

```elixir
    phx-submit="save"
```

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

```elixir
  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
```

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.


&nbsp;
&nbsp;


[![Join The Elixir Army](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cv2q172ztuekdtw82vo9.png)](https://elixirprogrammer.com)