artem

artem

How do you control LiveView elements visibility from BOTH server-side and frontend

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?

Most Liked

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, ...})
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).

Where Next?

Popular in Questions Top

Kurisu
For example for a current url like http://localhost:4000/cosmetic/products?_utf8=✓&amp;query=perfume&amp;page=2, I would like to get: ...
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
qwerescape
Is there a way to get the call stack or stack trace at any point in the code? Not from exceptions, but an expression that returns how the...
New
mgjohns61585
Could someone help me? I’m making my first elixir program, number guessing game. I can’t figure out how to convert the user’s guess from ...
New
JulienCorb
I am trying to implement my new.html.eex file to create new posts on my website. new.html.eex: &lt;h1&gt;Create Post&lt;/h1&gt; &lt;%= ...
New
ycv005
I have followed this StackOverflow post to install the specific version of Erlang. And When I am running mix ecto.setup then getting fol...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
New

Other popular topics Top

Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 29703 241
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
New
Fl4m3Ph03n1x
About me? ( if you have nothing better to do than reading about some random guy in the internet :stuck_out_tongue: ) Hello all, this is ...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
New

We're in Beta

About us Mission Statement