So LiveView is more than capable of simple forms. I think the generators do make it appear more complex than it really is due to the way the form is embedded in a modal. If you can code up a simple form using “dead view”, it is actually a lot simpler to code them up with LiveViews.
I typically just code them from scratch, but I have built up a library of components to reduce a lot of the boilerplate.
Essentially, you just need to implement the render function for the template (or have your separate template file), the mount function (which needs to set up the changeset for the form), an event handler to validate (typically wired to the phx-change on the form) and an event handler for form submit. I generally combine the template containing the HTML form into the main LiveView file module by implementing the render
function.
An (untested) complete example might looks something like this - I’m sure better developers than me will pick holes in it, but it works for me:
defmodule MyWeb.Admin.Units.UnitEditLive do
use MyWeb, :live_view
alias MyWeb.Live.FormComponents
@impl true
def render(assigns) do
~H"""
<.form let={f} for={@changeset} phx-change="validate" phx-submit="save">
<%= hidden_inputs_for(f) %>
<FormComponents.text_input form={f} field={:name} />
<FormComponents.text_input form={f} field={:symbol} />
<FormComponents.text_input form={f} field={:description} />
<div class="mt-8 flex-shrink-0 flex justify-end">
<%= live_patch("Cancel",
to: Routes.live_path(@socket, MyWeb.Admin.Units.UnitsIndexLive),
class: "btn-plain"
) %>
<%= submit("Save", phx_disable_with: "Saving...", class: "btn-small mr-2") %>
</div>
</.form>
"""
end
@impl true
def mount(params, _session, socket) do
{thing, action} =
case params do
%{"id" => id} ->
{MyApp.Thingz.get!(id), :update}
_ ->
{%{}, :create}
end
changeset = MyApp.Thingz.change(thing)
socket =
socket
|> assign(changeset: changeset)
|> assign(action: action)
{:ok, socket}
end
@impl true
def handle_event("validate", %{"form" => params}, socket) do
changeset = MyApp.Thingz.change(params)
socket =
socket
|> assign(changeset: changeset)
{:noreply, socket}
end
def handle_event("save", %{"form" => params}, socket) do
result = case action do
:update -> MyApp.Thingz.update(params)
:create -> MyApp.Thingz.create(params)
end
socket = case result do
{:ok, updated_thing} ->
# probably some redirect to the index
push_redirect(socket, to: Routes....)
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, changeset: changeset)}
end
{:noreply, socket}
end
end
I create a separate dumb index liveview that has links to the edit view. You will need a route defined for the index view, and two pointing to the “edit” form - one to edit and one to create, e.g.
live "/admin/units", Admin.Units.UnitsIndexLive
live "/admin/units/:id/edit", Admin.Units.UnitEditLive
live "/admin/units/new", Admin.Units.UnitEditLive
Once you get into the swing of it, you can bang them out pretty quickly, but the first one or two will take a little longer while you figure out the recipe.
If you build some basic components for common form controls (e.g. text input with error helper), there doesn’t have to be much boilerplate. You can take a look at some sample components to rapidly build forms here: phoenix/components.ex at master · phoenixframework/phoenix · GitHub (depends on the new version of Phoenix/LiveView). The above example might use a basic text input component like this:
defmodule MyWeb.Live.FormComponents do
use Phoenix.Component
import Phoenix.HTML.Form
import MyWeb.ErrorHelpers
def text_input(assigns) do
f = assigns.form
field = assigns.field
assigns =
assigns
|> assign_new(:input_class, fn -> "input w-48" end)
|> assign_new(:label, fn -> humanize(assigns[:field]) end)
|> assign_new(:disabled, fn -> false end)
|> assign_new(:value, fn -> Phoenix.HTML.Form.input_value(f, field) end)
~H"""
<div class="mb-2 ml-2">
<%= label @form, @field, @label, class: "input-lbl" %>
<%= text_input @form, @field, class: "input w-48", disabled: @disabled, value: @value %>
<%= error_tag @form, @field %>
</div>
"""
end
end