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