artem

artem

Hi all

I am still learning Elixir, Phoenix and LiveView. I love the way I can describe the state in my LiveView and it is automagically rendered in frontend. However, right now I am playing with the features where latency happens to be quite important in a couple of places.

I am implementing a tiny sound recorder and I’d really like to hide the “start recording” microphone button ASAP, before the LiveView on server side reacts - so that user wouldn’t click it second time, also fast UI responsiveness looks nice.

Well, that’s where JS module comes into play and I can easily and quickly hide the button with this:

<button
      id="microphone-button"
      class={[
        "fixed bottom-3 ...",
        microphone_button_should_be_visible?(@recording_state) || "hidden"
        ]}
      phx-click={microphone_clicked_js()}
    >
 
 def microphone_button_should_be_visible?(recording_state) do
    recording_state == nil || recording_state == :ready
  end

 def microphone_clicked_js(js \\ %JS{}) do
    js
    |> JS.push("microphone_clicked")
    |> JS.add_class("hidden", to: "#microphone-button")
...
end

As you see I have microphone_button_should_be_visible? function for server side to compute when button should be visible, but since I am pretty sure it is to be hidden on click, I speculatively add hidden class for it not waiting for server side to react.

Unhiding problem
Problem happens when the recording is done and I want to show the microphone button again, so user could initiate the next recording session. My live view correctly sets recording_state assign (I verified it) and microphone_button_should_be_visible? should work correctly, but button never appears unless I use JS to remove “hidden” class.

Could somebody, please, help me understand what’s happening here?

  • Is LiveView-via-heex-based adding-removing “hidden” class not working because JS.add_class somehow added a “distinctively different” attribute that is not controlled by LiveView state anymore?

  • Or does LiveView somehow fail to compute DOM diff when some parts of it are manipulated by JS and I need to somehow explicitly ask it to rerender a particular control?

  • Or am I missing something even more basic? How would you approach the problem?

Showing Posts 1 to 7

caleb-bb

caleb-bb

Can you inspect the html after you click? I suspect that you’ll see TWO instances of the hidden class on that element while it’s hidden. If only one of them gets removed, that would explain what’s happening.

IF that’s the case, then I suggest adding a conditional to your JS event handler to check if the element already has the hidden class before adding it.

artem

artem OP

One instance of “hidden” only. I start suspecting that possibly JS.remove class kind’a removes the DOM leaf that LiveView was supposed to update, so nothing is updated from live view then :confused:

I hope I am not the first one who wants to manage UI mostly from LiveView, but with some speculative speedups via JS library-based manipulations, so hopefully somebody has solved this kind of issue already.

D4no0

D4no0

I have a feeling that the class list is not tracked correctly when liveview wants to patch.

Try and move the entire list for class microphone-button to assigns, in this way when you will change that assign it will patch the entire list.

codeanpeace

codeanpeace

… JS commands are DOM-patch aware, so operations applied by the JS APIs will stick to elements across patches from the server.
source: Phoenix.LiveView.JS

Yup, it’s a feature that can seem like a bug in certain use cases since the typical scenario would have the hide/show and corresponding show/hide be from client driven interactions.

What you could do is push an event via push_event/2 from the server to tell the client to show the record button again with a client side hook using vanilla JS or JS.exec/liveSocket.execJS + JS.show in a data attribute.

# HEEX
<button ... phx-hook="MicButton" data-show={JS.show()}>
  Record
</button>

# LiveView/LiveComponent
push_event(socket, "show-mic-button", %{})

# app.js
let Hooks = {}
Hooks.MicButton = {
  mounted(){
    this.handleEvent("show-mic-button", () =>
      this.liveSocket.execJS(this.el, this.el.getAttribute("data-show"))
    )
  }
}

let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, ...})
artem

artem OP

Oh, so class added or removed by JS is supposed to stay forever regardless of the server side assigns changes.. not quite expected by me to be honest - I thought that the whole main point of JS module is to speculatively update UI while waiting for the “true” state coming over websocket :slight_smile:

If I get it correctly if I ever changed element visibility via JS adding/removing “hidden” class, I have to stick with JS controling their visibility forever. Well, if it is this way, then it is this way. In my particular case, I’ll probably skip using “speculative JS” - too much hassle.

  • Just to double check: is there possibly a workaround?
  • Does anybody change element visibility speculatively on JS while waiting for the true server state over websocket? Is there a way to do it with some simple(ish) code?
D4no0

D4no0

I think this is working as expected, to visualize this in a more clear way, let’s go back to how we would do this in the old days:

class= "fixed bottom-3 <%= microphone_button_should_be_visible?(@recording_state) || "hidden" %>",

It is clear that the only thing the assign knows is how to generate some text in a place, it has no actual concept of dom interaction with class tag like JS. The liveview also can’t track any modifications that are made by JS, so you create an invariant that the server has no idea about.

As I proposed above, try to keep the entire class list in the assign, so it actually rewrites the broken state created by JS with a valid server-side state. I cannot confirm this works 100%, because the patching mechanism is internal and might have changed. It would take a lot of time for me to recreate your setup, so it would be nice if you tried that and came back with a response.

endoooo

endoooo

if I understand correctly what you want, you don’t need the JS.add_class/1. you can rely on the added phx-click-loading class. remove the |> JS.add_class(...) line, and adjust your button class to "fixed bottom-3 ... phx-click-loading:hidden".

from Elixir documentation on Bindings:

All phx- event bindings apply their own css classes when pushed

actually, you don’t even need the microphone_clicked_js/1 function. you can just do phx-click="microphone_clicked", which will immediately add the "phx-click-loading" class in the clicked element, and remove it when your event handler finishes its job (and we can use the "phx-click-loading:hidden" syntax thanks to the custom Phoenix Tailwind variants).

JS.push/1 is necessary only if you want to enhance the push event (adding the phx-click-loading class to a different element, for example).

— All posts loaded —

Where Next? Top

Trending in Questions Top

katta
I having some trouble figuring out if I have set myself too strict of standards for my production server. Currently I can handle 75% of r...
New
achenet
Hello, I’m trying to build a basic Phoenix web-app, and I’d like to use Tailwind. However, when I launch mix phx.server, I get an error...
New
kpanic
Hi everyone, I am toying with the idea of building a “match maker” for giving personal help to people that wants to start coding. I sta...
New
Cxx-mlr
I’m working on a small exercise involving update_in/3, and I came up with this solution: data = %{ name: "Periodic Table", category:...
New
ChrisAmelia
I’ve got trouble wrapping my head around the order in which functions are called in this snippet (from Phoenix’s authentication): toke...
New
dillonoconnor
Is there any way to avoid the Hologram compiler running when using iex? It seems like the front-end code could potentially be disregarded...
New
thiagogsr
** (ArgumentError) expected :max_attempts to be a positive integer, got: {:@, [line: 10, column: 19], [{:max_attempts, [line: 10, column:...
New

Other Trending Topics Top

GenericJam
Edit: 2026 May 15 - This post is archived. Mob is alive!! Main docs: mob v0.7.11 — Documentation A bit of explanation for the slightly c...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New
KristerV
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews