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
Trending in Questions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #blog-post
- #phoenix_html
- #iex
- #graphql
- #ai
- #genstage
- #elixirconf-us
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #security
- #hex










First 10 of 17 Posts
qilongxu
Probably you need add “phx-blur” to your input field.
peck
could you elaborate on that?
I have tried using
phx-debouncewith a value ofblurfor the input fields, and that does work to limit validation of the form until I leave a field, but as soon as thenamefield loses focus theemailfield 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
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
@peck Did you add
phx_error_forto yourerror_tagfunction as documented here: Phoenix.LiveView — Phoenix LiveView v1.2.5I had to do that to keep error messages from appearing on fields that haven’t been focused yet.
peck
I get the same behavior with that added
peck
@tme_317 I did, it is in the bottom of my LiveView file.
Since you were able to do it that makes me think that there must be something simple that I missed.
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 hasstyle="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
@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
@peck Yeah… I am using the latest release of Phoenix and a recent
masterversion of LV including the awesomelive_components.I think using the
display: none;instead of conditional rendering would help you since I noticed you have a the radio button:product_idwhich only appears on step 1 and the fields:nameand:emailon 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:
Then you can get rid of
|> Map.replace!(:errors, [])in your changeset function.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_inputor otherwise.