Conditional CSS classes in form tags when validating?

I am using LiveView with TailwindCSS to create an app where visitors can send a message in a component.

I have gotten almost everything to work as I would like it to:

  • the form only submits if the fields are valid
  • validation errors are only displayed after the user tries to submit the form for the first time, and on every change thereafter
  • if the form is successfully submitted, it is replaced with a confirmation message

However, I also want color of the borders around the text_input and the textarea tags to switch from gray to red while a field fails validation.

Currently, only the error tag appears below an invalid field, but the style of the field itself does not change.

Is there a way to achieve this behaviour? E.g. by adding conditional css classes to the text_input and textarea tags, so the borders for these tags turn to red while that particular field is invalid?

I have tried googling and looking through the Phoenix/LiveView docs for a solution, but have been unable to find one.

This is my template (message_form.html.heex):

<div id="message-form-container">
  <%= if @form_open do %>
  <%= f = form_for @changeset, "#",
      id: "message-form",
      phx_change: @change_action,
      phx_submit: "save",
      phx_target: @myself,
      class: "grid grid-cols-2 gap-4 w-full" %>
  <div class="flex flex-col">
    <%= text_input f, :name, placeholder: "Name", class: "rounded border-gray-400 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
    <%= error_tag f, :name %>
  </div>
  <div class="flex flex-col">
    <%= text_input f, :email, placeholder: "Email", class: "rounded border-gray-400 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
    <%= error_tag f, :email %>
  </div>
  <div class="col-span-2 flex flex-col">
    <%= textarea f, :body, placeholder: "Message", class: "rounded border-gray-400 shadow-sm focus:ring-gray-900 focus:ring-opacity-50 focus:border-gray-900" %>
    <%= error_tag f, :body %>
  </div>
  <%= submit "Send message", phx_disable_with: "Sending...", class: "block px-4 py-2 border-none shadow rounded font-semibold text-sm text-gray-50 hover:bg-blue-600 bg-blue-500 cursor-pointer" %>
  <% else %>
  <p>Message sent!</p>
  <% end %>
</div>

And this is the LiveView component code (message_form.ex)

defmodule AppWeb.Components.MessageForm do
  use AppWeb, :live_component
  alias App.Messages.Message
  alias App.Messages

  def update(_assigns, socket) do
    {:ok,
     socket
     |> assign_message()
     |> assign_changeset()
     |> assign(form_open: true)
     |> assign(change_action: nil)}
  end

  def handle_event(
        "validate",
        %{"message" => message_params},
        %{assigns: %{message: message}} = socket
      ) do
    changeset = Messages.validate_message(message, message_params)

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

  def handle_event(
        "save",
        %{"message" => message_params},
        %{assigns: %{message: message}} = socket
      ) do
    changeset = Messages.validate_message(message, message_params)

    case changeset.valid? do
      true -> {:noreply, assign(socket, form_open: false)}
      false -> {:noreply, assign(socket, changeset: changeset, change_action: "validate")}
    end
  end

  def assign_message(socket) do
    socket
    |> assign(:message, %Message{})
  end

  def assign_changeset(%{assigns: %{message: message}} = socket) do
    socket
    |> assign(:changeset, Messages.change_message(message))
  end
end
2 Likes

I figured it out, this is the solution I used:

    <%= text_input f, :name, placeholder: "Name", class: "
    #{if @changeset.errors[:name] && @change_action,
      do: "border-red-600 focus:border-red-600 focus:ring-2 focus:ring-red-600 focus:ring-opacity-50 dark:border-red-500",
      else: "border-gray-400 dark:border-gray-500 focus:border-gray-900 dark:focus:border-gray-300 focus:ring-2 focus:ring-gray-500 focus:ring-opacity-50"}
    rounded dark:bg-zinc-800 transition duration-150" %>
    <%= error_tag f, :name %>

Not sure whether to delete this post or leave it in case others are searching for the same solution. Leaving it unless somebody suggests that I delete it!

7 Likes