So after reading through the replies I believe that I’ve come up with a workable solution that can be used as a standin, but it does require a bunch of changes in different places, so let’s start with an overview of what need’s to be done:
- Put the static path as the action into the form, even for the liveview version.
- Add a hook in your JS code that replaces the action with “#”. Thus if JS is enabled we’ll stay in the LiveView, otherwise we’ll fallback to the default route.
- In the router remove the
:edit
and :new
paths, and replace them with live_path
's.
- In the controller, remove the
:edit
and :new
functions, we no longer need them. Also delete the new.html.eex
and edit.html.eex
templates, this will be handled by the LiveView in the future.
- In the controller, add a helper module where we can put the duplicate functionality, i.e. funcitons for validation, creation and updates
- Use the code from the helper module in both the Controller and the LiveView.
I.e. if we take the example from my question, we need all these (I’ve left out the validation step, as that is rather trivial to add):
defmodule SomeWeb.Router do
...
scope "/", SomeWeb do
...
# make sure that the live paths are above the resources, otherwise
# they won't match
live "/notes/:id/edit", SomeLiveView
live "/notes/new", SomeLiveView
resources "/notes", NoteController, except: [:new, :edit]
end
end
defmodule SomeWeb.SomeController do
# Contains the duplicated code and some helpers so that we generate the same
# responses/redirects. If we need to do this more often, we can probably move
# most of it to a macro and simply generate the boilerplate code
defmodule Helpers do
alias Phoenix.LiveView.Socket
alias Plug.Conn
alias SomeWeb.Router.Helpers, as: Routes
# We need those so that we can have the same redirect code for both
# Socket and Conn
alias Phoenix.Controller, as: PC
alias Phoenix.LiveView, as: PV
def create(conn_or_socket, some_params) do
case SomeContext.create(some_params) do
{:ok, some_schema} ->
reply(conn_or_socket, {:ok, some_schema, "Created successfully."})
{:error, %Ecto.Changeset{} = changeset} ->
reply(conn_or_socket, {:error, "new.html", changeset: changeset})
end
end
def update(conn_or_socket, some_id, some_params) do
some_schema = Notes.get_note!(some_id)
case SomeContext.update(some_schema, some_params) do
{:ok, some_schema} ->
reply(conn_or_socket, {:ok, some_schema, " Updated successfully."})
{:error, %Ecto.Changeset{} = changeset} ->
reply(conn_or_socket, {:error, "edit.html", changeset: changeset})
end
end
defp reply(%Conn{} = conn, {:error, template, args}), do:
PC.render(conn, template, args)
defp reply(%Socket{} = socket, {:error, _template, args}), do:
{:noreply, PV.assign(socket, args)}
defp reply(%Conn{} = conn, {:ok, some_schema, msg}), do:
redirect_success(conn, some_schema, msg, &PC.put_flash/3, &PC.redirect/2)
defp reply(%Socket{} = socket, {:ok, some_schema, msg}), do:
{:stop, redirect_success(socket, some_schema, msg, &PV.put_flash/3, &PV.redirect/2)}
defp redirect_success(conn_or_socket, some_schema, msg, put_flash, redirect) do
conn_or_socket
|> put_flash.(:info, msg)
|> redirect.(to: Routes.some_path(conn_or_socket, :show, some_schema))
end
end
...
alias FastNotesWeb.NoteController.Helpers
def create(conn, %{"note" => note_params}), do:
Helpers.create(conn, note_params)
def update(conn, %{"id" => id, "note" => note_params}), do:
Helpers.update(conn, id, note_params)
...
end
defmodule SomeLiveView do
...
def mount(%{"id" => id}, session, socket) do
changeset = SomeContext.get_changeset!(id)
do_mount(changeset, session, socket, :update)
end
def mount(%{}, session, socket), do:
do_mount(SomeContext.empty_changeset(), session, socket, :create)
defp do_mount(changeset, _session, socket, action) do
{:ok, assign(socket, endpoint: socket.endpoint, changeset: changeset, action: action)}
end
def handle_event("submit", %{"some" => some_params}, socket) do
case socket.assigns.action do
:update ->
SomeWeb.SomeController.Helpers.update(socket, socket.assigns.changeset.data.id, some_params)
:create ->
SomeWeb.SomeController.Helpers.create(socket, some_params)
end
end
...
end
some_live_view.html.leex
<% action_path = case @action do
:create -> Routes.note_path(@endpoint, :create)
:update -> Routes.note_path(@endpoint, :update, @changeset.data.id)
end %>
<%= render "form.html", Map.put(assigns, :action, action_path) %>
<span><%= link "Back", to: Routes.some_path(@endpoint, :index) %></span>
form.html.eex
<%= form_for @changeset, @action, [phx_change: :validate, phx_submit: :submit, phx_hook: "formHook"], fn f -> %>
...
<% end %>
app.js
import {Socket} from "phoenix"
import LiveSocket from "phoenix_live_view"
const hooks = {
formHook: {
mounted() {
this.el.setAttribute("action", "#");
}
}
}
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content");
const liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}, hooks: hooks});
liveSocket.connect();
Sorry for the rather long post, but so far this seems like the cleanest solution to me. Tested this with JS active/deactivated in the browser, and is working fine for me. The only thing left to do is to wrap all the boilerplate stuff about reply etc in the Helper into a macro so that we can simply write that stuff and only provide the functions for validation/creation/updates.