Author
Jason Stiebs
@peregrine
Image by Annie Ruygt
We’re Fly.io and we transmute containers into VMs, running them on our hardware around the world. We have fast booting VM’s, so why not take advantage of them?
Phoenix LiveView is a server-side frontend framework and with this great power comes great responsibility on the developer. A very common source of frustration is around Forms, their assigns values and connection resets or deploys. In this post we’ll discuss how this all works and how it enables Form Auto-Recovery.
Let’s say we have this basic LiveView with a form and a query input:
defmodule FormExamples.FormsLive do
use FormExamplesWeb, :live_view
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(form: to_form(%{"query" => nil}))
}
end
def render(assigns) do
~H"""
<.simple_form for={@form} phx-submit="save" id="form" class="mb-20">
<.input field={@form[:query]} label="Search" />
</.simple_form>
"""
end
def handle_event("save", %{"query" => q}, socket) do
{:noreply,
socket
|> assign(form: to_form(%{"query" => nil}))
|> put_flash(:info, "Form submitted with query: #{q}")}
end
end
When we type into the text box and hit enter, our save callback is called and we reset the form. Before we move on, can you see the problem with this form?
On our Form submit we are assigning "query" => nil but "hello" remains in our text box. What’s going on here?
Spoiler: We did not change the assign value while we were typing the query, in LiveView’s view of the world the form‘s query value is nil, and it never changed. These implications will be discussed further in the article.
The Problem of State
LiveView and, any good frontend framework, help us organize our front end code around encapsulating state within a component. Meaning the only place we need to think about the state of a view is in our assigns, there will be no other references mutating our values outside of the component.
Unfortunately in all frameworks this a lie, or a helpful fiction. All frontend frameworks actually have to deal with three(!) separate local states and keep them in sync, for example here are the 3 states for LiveView:
- Server, where we keep track of assigns, session and the WebSocket related state.
- Browser HTML DOM state.
- Browser Internal DOM state.
In react the first state would be the Component/state/props, they still need to maintain the HTML and the internal DOM State. This is why they have so many functions around use* now to encapsulate the many different ways you might want to manage state and the dom.
In LiveView flow of state is as follows:
- Developer changes
assigns. - LiveView calculates the minimum HTML Diff changes and sends it down the WebSocket.
- LiveView.js sends the changes to the virtual DOM library
morphdomwhich alters the HTML DOM. - User changes inputs, which only changes the Browser Internal DOM State.
Knowing this maybe we can guess what went wrong with our form, and it happens right at point 1, the assigns value for the query value never changed. No diff was calculated, the browser HTML never changed so the HTML Input value never changed, all the while the Browsers Internal DOM State value did change.
Here is how we can change our example to reset the value on submit:
defmodule FormExamples.FormsLive do
use FormExamplesWeb, :live_view
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(form: to_form(%{"query" => nil}))
}
end
def render(assigns) do
~H"""
<.simple_form for={@form} phx-submit="save" phx-change="validate" id="form" class="mb-20">
<.input field={@form[:query]} label="Search" />
</.simple_form>
"""
end
def handle_event("validate", %{"query" => q}, socket) do
{:noreply, assign(socket, form: to_form(%{"query" => q}))}
end
Here we added a `phx-change` to our form, with `handle_event` function that changes the form and query `assigns` whenever the form changes. Now when we hit `submit` our input would reset to nil because every single keypress we informed LiveView of this change! We can follow the flow of state here:
1. User changes the text box, firing a `phx-change` event.
2. Handle event `assign` changes the LiveView state.
3. LiveView calculates a diff that is essentially `set input.value to assigns.query`
4. LiveView.js and `morphdom` sets the HTML DOM value of our input.
5. Which is the same as the current HTML Internal Dom value so no change happens that the user can see.
## Enter A Server Restart
Now this is where the [Auto-Recovery](https://hexdocs.pm/phoenix_live_view/0.20.14/form-bindings.html#recovery-following-crashes-or-disconnects) Magic happens when we deploy or the [process](/content/phoenix-files/a-liveview-is-a-process/index.html) restarts:
1. On re-connect a `phx-change` event is fired with the current DOM for every Form that has the same `id` as a reconnect mounts html.
2. Handle event `assign` changes the LiveView internal state with the validate params.
3. LiveView calculates a diff that is essentially `set input.value to assigns.query`.
4. LiveView.js and `morphdom` sets the HTML DOM value of our text box.
5. Which is the same as the current HTML Internal Dom value so no change happens that the user can see.
The only difference here a little bit of Magic that LiveView does when it knows it’s reconnecting to an existing DOM State. If the current DOM has a Form with the same `id`, instead of replacing it, it fires the `change` event which should alter the LiveView’s current assigns with the previous state! And there you have it—fully recoverable forms with no server-side state at all! This works assuming the Form’s `id` remains unchanged.
And if you don’t believe me I pushed a live example [here](https://form-examples.fly.dev/) with code [here](https://github.com/jeregrine/form_examples/blob/main/lib/form_examples_web/live/forms_live.ex).
## Takeaways
1. Always give each Form a unique and consistent `id`.
2. Always have a `phx-change` event for Forms that updates the server side `assign`.
By understanding its inner workings of form handling and auto-recovery mechanisms, we can unlock the full potential of LiveView to create seamless user experiences. I encourage you to check on your own forms and inputs and make sure they have `ids` and `phx-change` events wired up to give your users a better user experience!