I had a use case where I wanted to disable a button when the LiveView has lost its connection and restored its enabled/disabled state when the connection recovers. I was able to achieve this using the socket connection lifecycle bindings in conjunction with JS.set_attribute/1
and JS.remove_attribute/1
.
However, I also wanted to be able to control the buttons enabled/disabled state via a disabled
attribute. It seems that, when using the aforementioned JS
functions in the lifecycle events bindings, this no longer functions correctly; that is, the disabled
HTML attribute is not added/removed, even though the value of the corresponding function component attribute is changing.
Does anyone know if this is the expected behaviour, or if I’m just missing something?
I have prepared a sample application to demonstrate what I’m describing:
Application.put_env(:sample, SamplePhoenix.Endpoint,
http: [ip: {127, 0, 0, 1}, port: 5001],
server: true,
live_view: [signing_salt: "aaaaaaaa"],
secret_key_base: String.duplicate("a", 64)
)
Mix.install([
{:jason, "1.4.4"},
{:plug_cowboy, "2.7.2"},
{:phoenix, "1.7.19"},
{:phoenix_live_view, "1.0.4"},
])
defmodule SamplePhoenix.ErrorView do
def render(template, _), do: Phoenix.Controller.status_message_from_template(template)
end
defmodule SamplePhoenix.CoreComponents do
use Phoenix.Component
alias Phoenix.LiveView.JS
@doc """
Renders a button.
## Examples
<.button>Send!</.button>
<.button phx-click="go" class="ml-2">Send!</.button>
"""
attr :type, :string, default: nil
attr :class, :string, default: nil
attr :disabled, :boolean, default: nil
attr :rest, :global, include: ~w(form name value)
slot :inner_block, required: true
def button(assigns) do
~H"""
<button
type={@type}
class={[
"phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3",
"text-sm font-semibold leading-6 text-white active:text-white/80",
@class
]}
disabled={IO.inspect(@disabled, label: "=====button===== disabled")}
{@rest}
>
{render_slot(@inner_block)}
</button>
"""
end
@doc """
Renders a button that is disabled when disconnected and has its
enabled/disabled state restored when reconnected.
## Examples
<.button>Send!</.button>
<.button phx-click="go" class="ml-2">Send!</.button>
"""
attr :type, :string, default: nil
attr :class, :string, default: nil
attr :disabled, :boolean, default: nil
attr :rest, :global, include: ~w(form name value)
slot :inner_block, required: true
def button2(assigns) do
~H"""
<button
type={@type}
class={[
"phx-submit-loading:opacity-75 rounded-lg bg-zinc-900 hover:bg-zinc-700 py-2 px-3",
"text-sm font-semibold leading-6 text-white active:text-white/80",
@class
]}
disabled={IO.inspect(@disabled, label: "=====button2===== disabled")}
phx-connected={
case @disabled do
true -> JS.set_attribute({"disabled", "true"})
_ -> JS.remove_attribute("disabled")
end
}
phx-disconnected={JS.set_attribute({"disabled", "true"})}
{@rest}
>
{render_slot(@inner_block)}
</button>
"""
end
end
defmodule SamplePhoenix.SampleLive do
use Phoenix.LiveView, layout: {__MODULE__, :live}
import SamplePhoenix.CoreComponents
def mount(_params, _session, socket) do
{:ok, assign(socket, :disabled, false)}
end
def render("live.html", assigns) do
~H"""
<script src="https://cdn.jsdelivr.net/npm/phoenix@1.7.19/priv/static/phoenix.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/phoenix_live_view@1.0.4/priv/static/phoenix_live_view.min.js"></script>
<script>
const liveSocket = new window.LiveView.LiveSocket("/live", window.Phoenix.Socket);
liveSocket.connect();
</script>
<style>
* { font-size: 1.1em; }
</style>
<div>
<%= @inner_content %>
</div>
"""
end
def render(assigns) do
~H"""
<div>
<.button type="button" disabled={@disabled}>No JS</.button>
<.button2 type="button" disabled={@disabled}>JS.(set|remove)_attribute</.button2>
</div>
<div>
<button phx-click="enable">Enable</button>
<button phx-click="disable">Disable</button>
</div>
"""
end
def handle_event("enable", _params, socket) do
{:noreply, assign(socket, :disabled, false)}
end
def handle_event("disable", _params, socket) do
{:noreply, assign(socket, :disabled, true)}
end
end
defmodule Router do
use Phoenix.Router
import Phoenix.LiveView.Router
pipeline :browser do
plug :accepts, ["html"]
end
scope "/", SamplePhoenix do
pipe_through :browser
live_session :default do
live "/", SampleLive, :index
end
end
end
defmodule SamplePhoenix.Endpoint do
use Phoenix.Endpoint, otp_app: :sample
socket "/live", Phoenix.LiveView.Socket
plug Router
end
{:ok, _} = Supervisor.start_link([SamplePhoenix.Endpoint], strategy: :one_for_one)
Process.sleep(:infinity)
As can be seen in this application,
- The first button (i.e., labelled “No JS”) is enabled/disabled when the corresponding button is clicked
- The second button (i.e., labelled “JS.(set|remove)_attribute”) remains enabled regardless of whether the enabled/disabled buttons are clicked
- However, if the connection is lost (e.g., by stopping the application), this button will be automatically disabled; when the connection is restored (e.g., by restarting the application) its previous enabled/disabled state will also be restored
In the meantime, I’ve worked around this using a client hook, but I’d be interested to know if anyone else has a more satisfying solution!