Form validation does not block submit

I have two forms. One to create and one to delete. The form validation for the create form works and prevents submit until valid is true. For the delete form, I want users to confirm deletion by typing the name. Here, the form validation also works, if the name_confirmation doesn’t match the changeset is invalid, but the form submit is not prevented.

So to make it extra clear, submitting the create form with an invalid changeset blocks. Submitting the delete form with an invalid changeset does not.

The only difference I can find is that the create form is in a live_view, but the delete form is in a live_component.

Below are the schema, component and form for the delete with confirmation feature. I’d appreciate any pointers what I’m doing wrong.

defmodule Cloud.Platform.Stack do
  use Ecto.Schema
  import Ecto.Changeset

  @primary_key {:id, Ecto.UUID, autogenerate: true}

  schema "stacks" do
    field(:name, :string)
    field(:owner, :string)

    field(:name_confirmation, :string, virtual: true)
    field(:delete, :boolean, virtual: true)

    timestamps()
  end

  def changeset(stack, %{"delete" => "true"} = attrs) do
    stack
    |> cast(attrs, [:name, :name_confirmation])
    |> validate_required([:name, :name_confirmation])
    |> validate_confirmation(:name,
      required: true,
      message: "must match name to confirm"
    )
  end

  def changeset(stack, attrs) do
    stack
    |> cast(attrs, [:name, :owner])
    |> validate_required([:name, :owner])
    |> validate_length(:name, min: 3, max: 255)
  end
end
defmodule CloudWeb.StackLive.FormComponent do
  use CloudWeb, :live_component

  alias Cloud.Platform

  @impl true
  def update(%{stack: stack} = assigns, socket) do
    changeset = Platform.change_stack(stack)

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

  require Logger

  @impl true
  def handle_event("validate", %{"stack" => stack_params}, socket) do
    changeset =
      socket.assigns.stack
      |> Platform.change_stack(stack_params)
      |> Map.put(:action, :validate)

    Logger.debug(changeset)

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

  def handle_event("delete", _stack_params, socket) do
    case Platform.delete_stack(socket.assigns.stack) do
      {:ok, _stack} ->
        {:noreply,
         socket
         |> put_flash(:info, "Stack deleted successfully")
         |> push_redirect(to: Routes.stack_index_path(socket, :index))}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, changeset: changeset)}
    end
  end
end
<div>
  <%= if @action == :delete do %>
  <.form
    let={f}
    for={@changeset}
    id="delete-stack-form"
    phx-target={@myself}
    phx-change="validate"
    phx-submit="delete">
      <%= hidden_input f, :name, value: input_value(f, :name) %>
      <%= hidden_input f, :delete, value: true %>
      <div class="sm:rounded-md sm:overflow-hidden">
        <%= label f, :name_confirmation, class: "block text-sm font-medium text-gray-700" %>
        <div class="mt-1 flex rounded-md shadow-sm">
          <%= text_input f, :name_confirmation, class: "focus:ring-indigo-500 focus:border-indigo-500 flex-1 block w-full rounded sm:text-sm border-gray-300", placeholder: @stack.name %>
          <%= error_tag f, :name_confirmation %>
        </div>
      </div>
      <div class="px-5 py-3 text-right sm:px-6">
        <%= submit "Delete", phx_disable_with: "Deleting...", class: "inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500" %>
      </div>
   </.form>
  <% end %>
</div>

Did you change the default delete_stack/1 function? The default just deletes the Stack by id.

1 Like

Thank you, that was exactly the tip I needed! I’m just getting started with Elixir and Phoenix.

1 Like