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

First 10 of 17 Posts Switch mode

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

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
spammy
I’m looking to build a personal workflow to quickly deploy web applications written in elixir/phoenix, for local consumption (ie not on t...
New
dli
Before I dive in myself, did anyone successfully sprinkle Hologram into their existing LiveView app? Looking for hints regarding: Addi...
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
bottlenecked
Hi all, I wanted to ask how the community is dealing with post-release steps. Today we have Ecto migrations, which make sure that the db...
New
rahultumpala
Hello, I have an Elixir backend that implements a custom protocol over TCP. I want to load test the backend and assess the performance o...
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
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
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
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New

We're in Beta

About us Mission Statement