peck

peck

Hi all,

I’ve been building a out a multi-step join/checkout flow and I’m liking the results. I used https://www.markusbodner.com/til/2019/05/31/multi-step-form-using-phoenix-live-view/ for inspiration.

The problem I’m having is with multiple fields on the same step, and validations called for all fields at the same time. Meaning that whenever name is starting to be filled them the error message for empty email shows up. I could solve it by having either field as a separate “step” but that would tend to annoy anyone filling out the form and doesn’t work well for things like addresses.

Gif here that probably shows much clearly than my words do: LiveView hosted at ImgBB — ImgBB since Im new user and cannot upload

I feel like I must be missing something obvious, but for the life of me I cannot figure out what it might be. Thanks for any help!

relevant lib versions:

      {:phoenix, "~> 1.4.10"},
      {:phoenix_pubsub, "~> 1.1"},
      {:phoenix_ecto, "~> 4.0"},
      {:phoenix_html, "~> 2.11"},

my changes struct

defmodule LarderWeb.Order do
  import Ecto.Changeset
  defstruct [:product_id, :name, :email]

  @types %{name: :string, email: :string, product_id: :string}

  def create_changeset() do
    {%__MODULE__{}, @types}
    |> cast(%{}, Map.keys(@types))
  end

  def changeset(order, attrs) do
    order
    |> Map.replace!(:errors, [])
    |> cast(attrs, Map.keys(@types))
    |> validate_required(Map.keys(@types))
    |> validate_format(:email, ~r/@/)
  end
end

liveview form

<%= f = form_for @changeset, "#", [phx_change: :validate, phx_submit: :save, as: "order"] %>

<%= if @current_step == 1 do %>
<%= with {:ok, %{data: products} } = Stripe.Product.list(%{active: true}) do %>
<%= for p <- products do %>
    <%= label do %>
    <%= radio_button f, :product_id, p.id %>
    <div>
      <h1>
        <%= p.name %>
      </h1>
    </div>
    <% end %>
    <% end %>

    <% end %>
    <% end %>

    <%= if @current_step == 2 do %>
    <%= label f, :name %>
    <%= text_input f, :name %>
    <%= error_tag f, :name %>
<hr/>
    <%= label f, :email %>
    <%= text_input f, :email %>
    <%= error_tag f, :email %>
    <% end %>


    <%= if @current_step == 3 do %>
    Time To Checkout
    <% end %>

    <div>


    <%= if @current_step > 1 do %>
<button phx-click="prev-step">Back</button>
<% end %>

<%= if @current_step == 3 do %>
<%= submit "Submit" %>
<% else %>
<button phx-click="next-step" phx-disable-with="Continuing">Continue</button>
<% end %>
</div>
</form>

liveview

defmodule LarderWeb.LarderLive do
  use Phoenix.LiveView
  use Phoenix.HTML
  import Bamboo.Email
  alias LarderWeb.Order

  def mount(_session, socket) do
    Phoenix.PubSub.subscribe(Larder.PubSub, "test", link: true)
    changeset = Order.create_changeset()

    socket =
      socket
      |> assign(current_step: 1)
      |> assign(changeset: changeset)

    {:ok, socket}
  end


  def render(assigns) do
    LarderWeb.OrderView.render("form.html", assigns)
  end

  def handle_event("save", %{"order" => params}, socket) do
    IO.puts("in save")
    changeset = Order.changeset(Order.create_changeset(), socket.assigns.changeset.changes)
    IO.inspect(changeset, label: "SAVE CHANGESET")
    case changeset.valid? do
      true ->
        {:stop,
         socket
         |> put_flash(:info, "Order taken")
         |> redirect(to: "/")}

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

  def handle_event("validate", %{"order" => params}, socket) do
    changeset = LarderWeb.Order.changeset(socket.assigns.changeset, params) |> Map.put(:action, :insert)
    {:noreply, assign(socket, changeset: changeset)}
  end

  def handle_info(message, socket) do
    IO.puts("Got message from broadcast")
    {:noreply, socket}
  end

  def handle_event("prev-step", _value, socket) do
    new_step = max(socket.assigns.current_step - 1, 1)
    {:noreply, assign(socket, :current_step, new_step)}
  end

  def handle_event("next-step", _value, socket) do
    current_step = socket.assigns.current_step
    changeset = socket.assigns.changeset

    step_invalid =
      case current_step do
        1 -> Enum.any?(Keyword.keys(changeset.errors), fn k -> k in [:product_id] end)
          2 -> Enum.any?(Keyword.keys(changeset.errors), fn k -> k in [:name, :email] end)
        _ -> true
      end

    new_step = if step_invalid, do: current_step, else: current_step + 1
    socket =
      socket
      |> assign(current_step: new_step)

    {:noreply, socket}
  end

  def error_tag(form, field) do
    Enum.map(Keyword.get_values(form.errors, field), fn error ->
      content_tag(:span, error,
        class: "help-block",
        data: [phx_error_for: input_id(form, field)]
      )
    end)
  end

end

Showing Posts 1 to 10

qilongxu

qilongxu

Probably you need add “phx-blur” to your input field.

peck

peck OP

could you elaborate on that?

I have tried using phx-debounce with a value of blur for the input fields, and that does work to limit validation of the form until I leave a field, but as soon as the name field loses focus the email field is validated too.

I guess at the core of it I’m looking to ignore validations of fields that haven’t been “touched” by the user yet.

qilongxu

qilongxu

I mean put phx_input: “blur” to every field to be validated.

<%= if @current_step == 2 do %>
<%= label f, :name %>
<%= text_input f, :name, phx_input: “blur” %>
<%= error_tag f, :name %>
<%= label f, :email %>
<%= text_input f, :email, phx_input: “blur” %>
<%= error_tag f, :email %>
<% end %>

tme_317

tme_317

@peck Did you add phx_error_for to your error_tag function as documented here: Phoenix.LiveView — Phoenix LiveView v1.2.5

I had to do that to keep error messages from appearing on fields that haven’t been focused yet.

peck

peck OP

I get the same behavior with that added

peck

peck OP

@tme_317 I did, it is in the bottom of my LiveView file. :frowning:

Since you were able to do it that makes me think that there must be something simple that I missed.

tme_317

tme_317

Sorry I didn’t see that!

Yeah I was able to do it… instead of doing the conditional rendering approach from the article where you have <%= if @current_step == 3 do %> ... #fields ... <% end %> I am cheating by having all of the fields rendered but wrapping each step/group of fields in divs where all but the currently active step has style="display: none;". It’s a dirty hack but it works great.

I didn’t have to do that because of validation messages (rather keeping morphdom happy with my JS hooks) but this approach may work for you.

peck

peck OP

@tme_317 yeah, I think my next step here is to strip down everything to just two fields with just one “step” so I only have save and validate events to handle and see if I can get it to work the way I want.

No shame at all in display:none hack, if it works it works! but unfortunately wouldn’t help my case since I want both name/email visible at the same time.

In your project that doesn’t show validations that haven’t been touched are you using the latest release versions of phoenix and phoenix_live_view?

tme_317

tme_317

@peck Yeah… I am using the latest release of Phoenix and a recent master version of LV including the awesome live_components.

I think using the display: none; instead of conditional rendering would help you since I noticed you have a the radio button :product_id which only appears on step 1 and the fields :name and :email on step 2. When validate is called I don’t think the full valid list of params are sent since not all the fields exist in the form at the same time. You can troubleshoot by IO.inspecting the params and the changeset in your validation handle_event. Your validations required all three fields to be set.

Also what if you make a fresh changeset on each call to “validate”? In other words I wouldn’t try to recycle the changeset already existing in your assigns but would just:

  def handle_event("validate", %{"order" => params}, socket) do
    changeset = Order.changeset(%Order{}, params) |> Map.put(:action, :insert)
    {:noreply, assign(socket, changeset: changeset)}
  end

Then you can get rid of |> Map.replace!(:errors, []) in your changeset function.

peck

peck OP

@tme_317 thanks so much for helping me work through it.

Updating the Changeset seems to be the problem. My mental model of iteratively build up the changeset in the in the state of the LiveView was completely wrong (and probably not the best idea for memory size of the process). It seems the right way is to have all the fields in one form and hide them hidden_input or otherwise.

Where Next? Top

Trending in Questions Top

katta
I having some trouble figuring out if I have set myself too strict of standards for my production server. Currently I can handle 75% of r...
New
achenet
Hello, I’m trying to build a basic Phoenix web-app, and I’d like to use Tailwind. However, when I launch mix phx.server, I get an error...
New
peck
Hi everyone. My team and I have been working on a fairly modest app based around video streaming and chat, but we’ve landed a customer t...
New
bradley
I really like the adapter patterns that ecto, nebulex, waffle, etc. use and would love find something similar for a key management servic...
New
unaware8150
Hello folks! So at work, we are seeing some situations where we have to define some “fixed” strings that are used across the codebase in...
New
Cxx-mlr
I’m working on a small exercise involving update_in/3, and I came up with this solution: data = %{ name: "Periodic Table", category:...
New
Alvinkariuki
How Can I Optimise Compile Time Dependencies I have been building an elixir application for about 2 years now. Many modules and files ha...
New

Other Trending Topics Top

GenericJam
Edit: 2026 May 15 - This post is archived. Mob is alive!! Main docs: mob v0.7.11 — Documentation A bit of explanation for the slightly c...
New
budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New
KristerV
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
mudasobwa
I fully migrated to my own harness from Anthropic/Gemini and I think it’s time to share it. Welcome DSH, the DeepSeek Harness, fully writ...
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
juhalehtonen
There has been a thread to discuss the Stack Overflow Developer Survey on this forum every year since 2018, so here’s yet another one for...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews