peck

peck

Liveview Multistep Forms (Wizards) Field Validation

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

Most Liked

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

@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.

f0rest8

f0rest8

I was able to follow the similar multi-step form guide with success after changing the <%= if @current_step == 1 do %> <% end %> lines in my new.html.leex template to:

# Using Tailwind CSS
<div class="<%= unless @current_step == 1, do: "hidden" %>">
  # form fields for step 1 here
</div>

<div class="<%= unless @current_step == 2, do: "hidden" %>">
  # form fields for step 2 here
</div>

<div class="<%= unless @current_step == 3, do: "hidden" %>">
  # form fields for step 3 here
</div>

This then allows the form inputs to not be reset when the Live View switches between steps.

I also applied IO.inspect to verify that my changeset wasn’t removing my previous inputs and it works!

:heart:

Where Next?

Popular in Questions Top

9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
chrisalley
ExUnit now has describe blocks which is a welcome addition coming from RSpec. In the docs, it states that nested hierarchies of describe ...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
New
minhajuddin
I have seen a lot of code which picks the first element from a list using Enum.at(0) instead of List.first. Is there a reason why people ...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: The documentation above suggests that while ...
New
JDanielMartinez
Hi! May someone helps me, please! I have two apps into an umbrella project: the first one is Database, which manages queries, and the se...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New
komlanvi
Hi everyone, I was playing with phoenix liveView but I run into an issue. I have a form and want to validate each input text when the te...
New

Other popular topics Top

chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
AngeloChecked
What learn first? Rust or Elixir Hi Elixir community! I’m here because i want learn a new language. I’m a junior developer and mainly i ...
New
gausby
I asked this very same question on twitter and got some interesting feedback, but I thought it would be a good question to ask here as we...
1207 39523 209
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
saif
Hello everyone, Long time lurker first time poster here. I’ve recently begun working on Elixir full-time again! :raised_hands: It’s been...
New
dogweather
I wrote this comment on r/haskell, and it’s not popular there. :wink: But I think I’m on to something… Haskell reminds me of Java, and e...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New

We're in Beta

About us Mission Statement