Server & Client Side Forms Validations With [Elixir, Phoenix, LiveView and Tailwind CSS | No Javascript]
Anthony Gonzalez
Posted on February 24, 2022
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__)
]
Go to your config/dev.exs
file and add the following to your watchers
:
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:
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:
<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:
<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
:
defmodule FormValidatorWeb.PageLive do
use FormValidatorWeb, :live_view
end
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>
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'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:
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:
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:
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:
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:
<%= 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:
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:
<%= 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:
phx-submit="save"
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
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.
Posted on February 24, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.