lifeofdan

lifeofdan

I am having an issue where form field errors are not being displayed under the following scenario. Load the page, fill one field of the form, click “submit”. The errors are being shown on the struct but there is no error on the form fields. However, if you just reload the page and click submit without touching any of the inputs you get the “is required” errors. Validation on individual fields while filling out the form also works as expected. Here is my resource.

defmodule App.Public.Contact do
  use Ash.Resource,
    domain: App.Public,
    authorizers: [Ash.Policy.Authorizer],
    data_layer: AshPostgres.DataLayer,
    extensions: [AshJsonApi.Resource]

  require Ash.Expr

  json_api do
    type "contact"
  end

  postgres do
    table "public_contacts"
    repo App.Repo
  end

  actions do
    defaults [:read, :destroy, update: :*]

    create :send_message do
      accept [:email, :message, :name]

      validate match(
                 :email,
                 ~r<^[a-zA-Z2-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$>
               ) do
        message "invalid email"
      end
    end
  end

  policies do
    policy action(:send_message) do
      authorize_if always()
    end

    policy action_type(:read) do
      authorize_if expr(^actor(:kind) == :manager)
    end
  end

  attributes do
    uuid_primary_key :id

    attribute :email, :ci_string, allow_nil?: false, public?: true
    attribute :message, :string, allow_nil?: false, public?: true
    attribute :name, :string, allow_nil?: false, public?: true

    create_timestamp :created_at
    update_timestamp :updated_at
  end
end

Here is my form

defmodule App.ContactLive do
  use AppWeb, :live_view

  @impl true
  def render(assigns) do
    ~H"""
    <div class="grid grid-cols-1 md:grid-cols-2 gap-4 my-8">
      <div class="text-gray-700">
        <.simple_form id="contact-form" for={@form} phx-submit="submit" phx-change="validate">
          <div class="grid sm:grid-cols-2 gap-10">
            <div>
              <.input
                type="text"
                class={@disabled_loading_state}
                field={@form[:name]}
                placeholder="John Doe"
                label="Name"
              />
            </div>
            <div>
              <.input
                type="email"
                class={@disabled_loading_state}
                field={@form[:email]}
                placeholder="john@example.com"
                label="Email"
              />
            </div>
          </div>
          <.input
            class={@disabled_loading_state}
            field={@form[:message]}
            placeholder="Enter your message here..."
            label="Message"
            type="textarea"
          />
          <:actions>
            <.button phx-disable-with="Sending..." class="phx-submit-loading:cursor-not-allowed">
              Submit
            </.button>
          </:actions>
        </.simple_form>
      </div>
      <div class="mx-4 md:border-l border-gray-400 bg-gray-200 h-screen">
        <div class="text-center mx-4 lg:mx-12 md:px-4 flex items-stretch">
          <div class="md:h-72 relative">
            <div class="relative md:top-1/2">
              <h1 class="leading-none mb-16">We want to hear from you!</h1>
            </div>
          </div>
        </div>
      </div>
    </div>
    """
  end

  @impl true
  def mount(_params, _session, socket) do
    form =
      App.Public.Contact
      |> AshPhoenix.Form.for_create(:send_message, as: "contact")
      |> to_form()

    disabled_loading_state =
      "phx-submit-loading:cursor-not-allowed phx-submit-loading:bg-slate-50 phx-submit-loading:text-slate-500 phx-submit-loading:border-slate-200"

    {:ok,
     socket
     |> assign(:disabled_loading_state, disabled_loading_state)
     |> assign(:form, form)}
  end

  @impl true
  def handle_event("validate", params, socket) do
    validate = AshPhoenix.Form.validate(socket.assigns.form, params["contact"])

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

  @impl true
  def handle_event("submit", params, socket) do
    case AshPhoenix.Form.submit(socket.assigns.form, params: params["contact"]) do
      {:ok, form} ->
        {:noreply,
         socket
         |> put_flash(:info, "Thank you for your message #{form.email}!")}

      {:error, form} ->
        {:noreply, socket |> assign(form: form)}
    end
  end
end

Is this expected behaviour or is this a bug? Also, is there a way I can manually take over this error behaviour when required? Any help appreciated, this is my first attempt using phoenix liveview so I accept I could be doing something wrong.

Thanks in advance.

Showing Posts 1 to 10

zachdaniel

zachdaniel

Creator of Ash

Hmm…I think there was a recent change that only shows errors on touched fields, could that be part of the error that you’re seeing? There would be some code in your core components file like

errors = if Phoenix.Component.used_input?(field), do: field.errors, else: []

If you remove that and just do errors = field.errors, does it behave more as you’d expect?

lifeofdan

lifeofdan OP

Thank you! This doesn’t exactly give me the behaviour that I want but it puts me exactly where I need to be to get what I want. Thank you so much, I knew it had to be something simple like this!

zorn

zorn

Yup.

This replaces the phx-feedback-for logic. You can toggle the docs for the pre-rc versions to see those docs to compare.

axelson

axelson

Scenic Core Team

Also it’s worth mentioning that the released version (i.e. non-RC) of mix phx.new will create apps using an RC version of phoenix live view and also specifically include the errors = if Phoenix.Component.used_input?(field), do: field.errors, else: [] line.

carlgleisner

carlgleisner

Hi @lifeofdan, could you please elaborate on how you went from there?

There was a short exchange about this on the Discord as well but I can’t see that there were any conclusions documented.

I’m setting up a little example for myself now that I’m planning to share, but I thought that maybe you’re sitting on (way better) insights that might benefit the greater community.

Thanks in advance :slight_smile:

zachdaniel

zachdaniel

Creator of Ash

There is some interesting behavior here, and using a repro from @carlgleisner I’ve ultimately decided to open a bug on LiveView. I could be wrong, it could be something wrong with AshPhoenix.Form, but I have not been able to identify what that would be.

lifeofdan

lifeofdan OP

@carlgleisner I found the entire process quite frustrating to be honest. In order to get the behaviour I wanted I ended up just handling everything myself. I used AshPhoenix.Form to do the actual form field validation but otherwise I threw the errors and all of that myself because I couldn’t get the defaults to work the way I expected. Below is the code I used to get the behaviour I wanted. Take this with a very large grain of salt as this is my first time using phoenix forms and I could just be doing something wrong. The main things you will notice here is that I’m not using for={@form} or any field={@form[:name]} on the inputs. I’m just handling everything myself and not using any helpers because I just couldn’t get what I wanted otherwise. Below are my two relevant files, the form and the ash resource.

defmodule MyAppWeb.ContactLive do
  use MyAppWeb, :live_view

  @impl true
  def render(assigns) do
    ~H"""
    <div class="grid grid-cols-1 md:grid-cols-2 gap-4 my-8">
      <div class="text-gray-700">
        <form id="contact-form" phx-change="validate" phx-submit="submit">
          <div>
            <.input
              name="name"
              type="text"
              class={@disabled_loading_state}
              placeholder="John Doe"
              label="Name"
              value=""
              errors={@name_errors}
            />
            <.input
              name="email"
              type="text"
              class={@disabled_loading_state}
              placeholder="john@example.com"
              value=""
              label="Email"
              errors={@email_errors}
            />
          </div>
          <div>
            <.input
              name="message"
              class={@disabled_loading_state}
              placeholder="Enter your message here..."
              label="Message"
              value=""
              type="textarea"
              errors={@message_errors}
            />
          </div>
          <.button
            type="submit"
            phx-disable-with="Sending..."
            class="phx-submit-loading:cursor-not-allowed"
          >
            Submit
          </.button>
        </form>
      </div>
    """
  end

  @impl true
  def mount(_params, _session, socket) do
    form =
      MyApp.Public.Message
      |> AshPhoenix.Form.for_create(:send_message, as: "contact")
      |> to_form()

    disabled_loading_state =
      "phx-submit-loading:cursor-not-allowed phx-submit-loading:bg-slate-50 phx-submit-loading:text-slate-500 phx-submit-loading:border-slate-200"

    {:ok,
     socket
     |> assign(:disabled_loading_state, disabled_loading_state)
     |> assign(:form, form)
     |> assign(:name_errors, [])
     |> assign(:email_errors, [])
     |> assign(:message_errors, [])}
  end

  def handle_event("validate", params, socket) do
    validate = AshPhoenix.Form.validate(socket.assigns.form, params)
    target = validate.params["_target"] |> List.first()

    case target do
      "name" ->
        {:noreply, socket |> validate_name(validate.errors[:name])}

      "email" ->
        {:noreply, socket |> validate_email(validate.errors[:email])}

      "message" ->
        {:noreply, socket |> validate_message(validate.errors[:message])}

      _ ->
        {:noreply,
         socket
         |> assign(name_errors: [])
         |> assign(email_errors: [])
         |> assign(message_errors: [])}
    end
  end

  @impl true
  def handle_event("submit", params, socket) do
    validate = AshPhoenix.Form.validate(socket.assigns.form, params)

    socket =
      socket
      |> assign(form: validate)
      |> validate_name(validate.errors[:name])
      |> validate_email(validate.errors[:email])
      |> validate_message(validate.errors[:message])

    case AshPhoenix.Form.submit(socket.assigns.form) do
      {:ok, form} ->
        {:noreply,
         socket
         |> put_flash(:info, "Thank you for your message #{form.email}!")
         |> push_navigate(to: ~p"/contact")}

      {:error, form} ->
        {:noreply, socket |> assign(form: form)}
    end
  end

  defp validate_message(socket, errors) when is_nil(errors) do
    socket |> assign(message_errors: [])
  end

  defp validate_message(socket, errors) do
    {message, _} = errors
    socket |> assign(message_errors: [message])
  end

  defp validate_email(socket, errors) when is_nil(errors) do
    socket |> assign(email_errors: [])
  end

  defp validate_email(socket, errors) do
    {message, _} = errors
    socket |> assign(email_errors: [message])
  end

  defp validate_name(socket, errors) when is_nil(errors) do
    socket |> assign(name_errors: [])
  end

  defp validate_name(socket, errors) do
    {message, _} = errors
    socket |> assign(name_errors: [message])
  end
end

Ash Resource

defmodule MyApp.Public.Message do
  @moduledoc """
  Resource for creating messages from the contact form that is publicly available
  """
  use Ash.Resource,
    domain: MyApp.Public,
    data_layer: AshPostgres.DataLayer

  postgres do
    table "public_contacts"
    repo MyApp.Repo
  end

  actions do
    defaults [:read, :destroy, update: :*]

    create :send_message do
      accept [:email, :message, :name]

      validate match(
                 :email,
                 ~r<^[a-zA-Z2-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$>
               ) do
        message "invalid email"
      end
    end
  end

  attributes do
    uuid_primary_key :id

    attribute :email, :ci_string, allow_nil?: false, public?: true
    attribute :message, :string, allow_nil?: false, public?: true
    attribute :name, :string, allow_nil?: false, public?: true
    attribute :archived_at, :utc_datetime_usec, public?: true
    attribute :deleted_at, :utc_datetime_usec, public?: true

    create_timestamp :created_at, public?: true
    update_timestamp :updated_at, public?: true
  end
end
zachdaniel

zachdaniel

Creator of Ash

Hey there! So this was actually confirmed to be a bug in live_view, that is fixed in the latest main.

Link: Changing conditions of `Component.used_input?` not resulting in a rerender of form fields · Issue #3460 · phoenixframework/phoenix_live_view · GitHub

zachdaniel

zachdaniel

Creator of Ash

It’s also worth mentioning that you are technically using a release candidate of phoenix_live_view.

Although… the latest installer does in fact use that release candidate automatically, so it does kind of feel like its what they’re pushing people to do… for all intents and purposes, given that the installer installs the release candidate, it seems like 1.0.0-rc.* is the defacto “current latest version” :person_shrugging:

lifeofdan

lifeofdan OP

I checked out main and it does now, indeed, work as expected. (once I un-delete the errors = if Phoenix.Component.used_input?(field), do: field.errors, else: []). Now I can delete a ton of code :+1:. Thanks to all those who continued to follow this up even after I had decided to just go my own way.

It is also interesting that this bugfix was merged into main on Sept 9th but still hasn’t been put into a release yet. This seems like something that everyone would be running into at the moment if they have initialized a new project. Glad it is fixed though, and I guess I will run on main branch until this is merged into a release.

Where Next? Top

Trending in Questions Top

stjefim
Hello! Suppose you are building workflow (order / task / payment) processing system with the following requirements: Each workflow con...
New
jonnycharles
I’m in search of an Elixir library that offers PDF generation capabilities similar to Ruby’s Prawn. While there have been discussions abo...
New
Blokh
Hey guys, I’ve got a huge CSV ( around 10 GB ) that needs to be processed hourly Do you guys have any suggestions what is the best prac...
New
roeland
Kia ora, We have been using elixir-google-api to connect to Google Drive. However, with the updates to Tesla due to CVEs this is now bro...
New
mohsen
I’m using an Umbrella project for a Phoenix application, and I want to have one Ecto Repo and one PostgreSQL database shared by all apps....
New
subsaharancoder
I’ve followed the Phoenix LiveView file upload code here Uploads — Phoenix LiveView v1.0.0-rc.7 and so far everything works just fine wit...
New
jaybe78
Hello, I’m developing a online persistent chat system (what’s app) like using elixir/dynamodb/aws for a mobile app(flutter). The diffic...
New

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews