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/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: https://ibb.co/Nms7qHk 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

earth10
Hi, I’m just starting to build a side-project with Elixir and Phoenix and doing some basic test with Elixir alone. What strikes me is th...
New
LegitStack
I’m trying to make a websocket server in Phoenix or raw Elixir. I heard about gun, I think I could use cowboy, but since I’m not that sma...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers' Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
hariharasudhan94
lets say i have a sample like a = 20; b = 10; if (a &gt; b) do {:ok, "a"} end if (a &lt; b) do {:ok, b} end if (a == b) do {:ok, "eq...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
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

Other popular topics Top

marius95
Hello everyone, I try to use an Javascript Event Handler in my root.html.leex file. Therefore I created a function in the app.js file: ...
New
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
New
AstonJ
Posting this to see if we can make things easier for people to get into Neovim. If you use Neovim and have a favourite distro please let ...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
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
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
marick
I had some trouble figuring out how to make many-to-many associations work. Once I got it working, I wrote a blog post. Because I'm a nov...
New
sergio
Kind of like when jquery came out, it was super necessary. Existing drag and drop libraries have a bunch of baggage to support old browse...
New

We're in Beta

About us Mission Statement