Arbitrary JavaScript on LiveView update

I’m about to use LiveView for the first time. I think I get how it works when updating the DOM, hiding/changing controls, etc. but is it possible to run arbitrary JavaScript when a new message is received?

We’re building a document conversion service. When the page loads, I set the title to “Loading…” Eventually, our converter returns an actual document title, at which point I want to run document.title = title or something equivalent to include the actual document’s title in the page. Titles are generated in the layout’s view, so aren’t in the LiveView itself.

Maybe there’s a better way to set the title, but is it possible to run other client-side code when certain messages are received? We have some complex client-side state that doesn’t always translate to DOM updates–libraries that need to be called on the client whenever new content is received, for instance. I think hooks may help here, but I’m not clear how, nor am I clear how to use specific hooks on specific pages rather than globally.

Thanks.

1 Like

Hi @ndarilek,

Hooks are the way to go, and they can (and actually have to be) linked to specific elements in your LiveView.

The docs cover this:
https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html#module-js-interop-and-client-controlled-dom

But you are probably better watching the video of when it was announced to get an overview:

Edit: I haven’t figured out a way to nicely manage different hooks for different pages/views yet (I’m not really that up to speed with JS - that’s why I’m here!!) - I’m sure someone else can help with that.

Currently, hooks only respond to mounted (when element under view is added and LV has finished mounting) or updated (when the DOM has been updated). It seems like you have neither and need state available client side.

One thing I would like to explore is a meta tags solution for LV, especially as it relates to live_link. Effectively, we need to propagate changes server side to the meta tags client side and I don’t think we have a solution yet.

1 Like

Hi @ndarilek,
Thinking about it some more, you could create a hidden element with a phx-hook and push the messages to that, e.g.

Template (leex) - spot to push messages content, plus JS hook

  <div hidden 
    id="msg-thing" 
    data-messages="<%= Jason.encode!(@messages) %>" 
    phx-hook="MessageChange" />

Live controller

  # Assumes messages published to a PubSub topic the LiveView subscribes to
  # Also assumes an empty list of messages is assigned to the socket on mount...
  def handle_info({:msg_added, %Message{}=msg}, socket) do
    messages = [msg | socket.assigns.messages]
    {:noreply, assign(socket, messages: messages)}
  end

JS

Hooks.MessageChange = {
  mount() { this.handleMessages(); },
  updated() { this.handleMessages(); },
  
  handleMessages() {
    var messages = JSON.parse(this.el.dataset.messages);
    // Do what you need to with messages
  }
}

I mentioned in another post about knockout js it’s super easy to use an a way to sprinkle extra ui js onto server rendered templates. I often handle default messages with server side code with it. .

You bind your live view data to a knockout model, then use that to handle things like the foreach for data display. I had created a git repo showing knockout rendering when the websocket updates.

https://elixirforum.com/t/using-knockout-js-to-bind-phoenix-websockets-to-the-ui/27145/3

In generally i only need loading text etc on big initial data loads,

I would say that in almost any templating language, you can have 2 elements, one shown if data is null… showing loading, and another to render the data if present.

Or if a div has a data bind on it, if you add default text inside that element, that will be displayed if no data is present to replace it.

In live view the websocket traffic is generally so fast and the ui update happens instantly on receiving that data, showing loading is sort of pointless after page load. I see more too adding things like ‘is typing’

Thanks folks, sorry it’s taken a while to come back around to this. I
finally had some time to dive into actually implementing this last week.

I’ve gotten pretty far with this, but I just want to check my
understanding. So essentially, when I initially load my document, I have
<title>Loading...</title> in my HTML document. Eventually my GRPC
service processes my PDF and returns a document title, at which point I
want to set <title>My Document Title</title> on my HTML document. But
I can’t wrap my entire page in a hook.

So I’m guessing the answer here is to put my document title in an
element under a phx-hook binding, something like:


<div phx-hook="DocumentShow">

   <h1 id="document_title"><%= @document.title %></h1>

</div>

Then my updated hook does something like document.title = el.querySelector("#document_title").innerHtml? All off the top of my
head, so I probably got capitalization wrong somewhere.

So in other words, if I want data from the server on which to run
arbitrary JavaScript, I have to package it in the DOM elements somehow,
then extract it in my hook. Is that accurate? If so, then thanks for
helping me figure that out. :slight_smile: I’d rather not package in a client-side
framework just yet, since so far most of my logic can run server-side,
and it’s only really this one bit of functionality outside of the
rendered template that I need this for. If I need more complexity, I’ll
definitely reach for something heavier.

Hi @ndarilek,
Conceptually you are spot on. Package your data into a DOM element that has a JS hook attached, and use it in the hook. Your version may not work as the hook has to be associated with an element that has a socket assign that is changing in order for it to fire, and also note that the hook has direct access to the element it is associated with so you don’t need to look it up with a selector. I’d personally code it along the lines of:

leex

<div id="my-id" data-title="<%= @document.title %>" phx-hook="DocumentShow"></div>

and in the JS:

Hooks.DocumentShow = {
  mounted() {
    this.updateTitle();
  },
  updated() {
     this.updateTitle();
  },
  updateTitle() {
    var title = this.el.dataset.title; // You have access to the element within the hook
    document.title = title;
  }
};
1 Like

Nice, thanks, haven’t done much with data attributes so that’s an
elegant solution.

(great idea with the hidden elements!)

I think one important point is that your client side code will only change once one of the following callbacks happens server side. It necessarily doesn’t happen “automagically”…although it may see like it ;).

So for you to trigger an update to the UI, you need to send a message to the LiveView or LiveComponent on the server and update the assigns (as mentioned above). When the websocket message gets to the client, it will diff/update/call relevant hooks to update your UI. Sorry if I’m being repetitive :pray: if this is something you are already aware of but I thought a few links might help.

LiveView Component send_update/2
example using send_update/2

As shown, it is rather tedious and I would love to help make this process more ergonomical in the future to update <head> tags. Perhaps time to start experimenting!