Doing JS events in LiveView

Hi! I am playing with LiveView and I have a use-case where I want to send JS desktop notifications when something happens on server.
LiveView is absolutely fantastic for changing the HTML but I have no idea how to call JS function. If I put a script tag in LiveView, it is not evaluated.

Any ideas?

3 Likes

Havenā€™t played with liveview yet, but maybe you could use that : https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver ? It should work independently.

I see, if there isnā€™t anything more obvious, then Iā€™ll try opening another channel. Letā€™s see how channels work with LiveView :smiley:

That sounds like a bug! ^.^;
Report it? :slight_smile:

Javascript interop isnā€™t supported just yet. Itā€™s a planned feature.

7 Likes

I think I wasnā€™t specific enough. If I put a script inside a LiveView tag, it is evaluated only once when the page loads. I wanted it to evaluate every time I receive an event from the server. I tried putting the script in a tag conditionally so that it appears with other elements in the view. I was just checking if it is evaluated every time it appears, but it is not the case (and that is probably not a bug but a feature :slight_smile: )

Thatā€™s what I mean. If the script tag is dynamically added ā€˜afterā€™ page load then it should be executed, it seems odd that it isnā€™t.

I have used LiveView for personal project, it is POC right now. I like the way it is used, the simplicity of the solution.

Current implementation evaluates only differences in HTML layout. Tags appear only after they are changed.

I needed to update SVG properties for the page, that uses LiveView. They are recalculated after the page is rendered at Front-end, so I have created tag, that triggers this update. SVG is used to inteconnect HTML elements. Initially connections have the length of 0, so they are invisible. JS calculates the proper coordinates and sizes for connections.

When page is loaded the first time, connections are drawn properly. As basic SVG structure is rendered inside LiveView template, connection positions are redrawn on each template update, and the length for each connection is dropped to 0.

So the solution is to update connections after each change in LiveView. This can be done by calling proper JS method. The simplest solution for me is to periodically recalculate connections (though it does not provide smooth animation).

Another way to achieve this, is to force LiveView to rerender JS script tag. Currently, this can be achieved by conditional render. I have observed, that if there will be no changes in final HTML, then JS will not be updated. This means, that to trigger an event inside browser for current LiveView version, script tag needs to be removed, and then added again. To achieve this, script tag needs to appear in a different place of the layout (for example inside the

tag for one render, and outside it for the next one). This causes script to be evaluated.

For my POC, I have created a counter value in assigns, and increment it on each socket update. This value is used to render script tag in different places of the layout.

Also, if you need to trigger some event after LiveView is connected, you can use conditional render and socket.connected? value. When page is rendered on the server (before being passed to client), its value is false, and it becomes true after the client connects to the page. I am using this to trigger initial SVG update, after the socket is connected.

SVG is updated through stimulus.js, so it works Ok, after the page is rendered on the client, but breaks after the LiveSocket is connected (as properties are dropped to their initial values). Thatā€™s why I have used the previously described approach.

Update:
So, the basic idea for this solution is:

  • script tag needs to be rendered in multiple places to be evaluated again
  • depending on the required update frequency, browser can use
    • JS setInterval() for recurring updates in the browser, and do not try to call updates from LiveView.
    • Use socket.connected? for conditional templates for triggering action after LiveView is connected the first time.
    • Use some value from assigns (for example counter for continuous updates, or some other property, like boolean value that shows if some value is selected), to render tag in different places.

This solutions are valid now, and possibly better solution will be added in future.

2 Likes

LiveView uses morphdom, which uses innerHTML. Browsers donā€™t load script tags inserted with innerHTML.

5 Likes

You can use the channel property of the liveview node the same way you do with phoenixā€™s Socket, the channel name is in the id tag of the view node in the DOM. Iā€™ve used it to send payloads to the liveview agent with javascript and Iā€™m pretty sure you can await msgs and call your functions with it.

1 Like

Could you explain how you did this? In my particular use case I need to have more control over the client-side events, and so far Iā€™ve resorted to using a separate channel and communicating with the LiveView through PubSub.

Based on your comment I got as far as:

  • getting the particular LiveView channel topic from the elementā€™s id (and prepending ā€œlv:ā€)
  • going through the liveSocketā€™s channel array and filtering by topic
  • sending my own custom message that way

But it feels a bit convoluted, and so far I havenā€™t figured out how to wait for the join before I try to send messages. Is there a much simpler way to do this that Iā€™m missing?

EDIT: come to think of it, any events Iā€™ll be sending will be after the channel has been joined, so this solution is pretty okay. Iā€™m mostly curious what you mean with " You can use the channel property of the liveview node" because at first I assumed you meant that I could simply do a querySelector(ā€œdata[]ā€).push() or something like that.

Thats exactly what I did:

window.onload = () => {
  const navView = document.getElementsByTagName('nav')[0].parentElement.id

  window.addEventListener('hashchange', function () {
    livesocket.views[navView].channel.push('event', {
      event: 'hashroute',
      type: null,
      value: window.location.hash.substring(1)
    })
  })
}

And youā€™re right, on the server side of things it felt convoluted at first, but not so much after reading phoenix_live_view.js. I figured itā€™d be ok for now until the func is added to liveview. It is a workaround afterall.

2 Likes

Awesome, thanks. That might be a bit cleaner than my approach.

1 Like

Using Chrome Developer tools on

http://palegoldenrod-grown-ibis.gigalixirapp.com/bear_game

it is actually using this code path

and scripts will execute with Range.createContextualFragment.

Try this in the console of a fresh tab:

let range = document.createRange()
let fragment = range.createContextualFragment(`<script>alert(1)</script>`)
document.documentElement.appendChild(fragment)

https://bugs.webkit.org/show_bug.cgi?id=12234

2 Likes

Yep, this is working.

<%= if @open, do: raw("<script>alert('lolz')</script>"), else: raw("") %>
1 Like

Yo, I ended up having to do this to apply changes on the DOM if itā€™s any use to you.

navView.channel.push('event', {
  event: 'hashroute',
  type: null,
  value: window.location.hash.substring(1)
}, 20000).receive("ok", diff => {
  navView.update(diff)
})

Could you maybe outline what you are doing now (liveview controller and template) I lost trackā€¦ but I think itā€™s what I need

The function at the bottom is what I use to send info from the client / javascript to the live view actor:

window.LiveSocket = new LiveSocket("/live")
window.LiveSocket.connect()

window.onload = () => {
  window.LiveSocket.pushLiveEvent('nav', 'hashchange', window.location.hash.substring(1))
}

window.addEventListener('hashchange', function () {
  window.LiveSocket.pushLiveEvent('nav', 'hashchange', window.location.hash.substring(1))
})

window.LiveSocket.pushLiveEvent = function (query, event, value) {
  const view = window.LiveSocket.views[document.querySelector(query).parentElement.id]

  view.channel.push('event', {
    event: event,
    type: null,
    value: value
  }, 20000).receive("ok", diff => { view.update(diff) })
}

And in the actor:

def handle_event("hashchange", route, socket) do
  ...
end

I havenā€™t needed to send info from actor to client yet, so I have nothing on that. The live view client joins all the actor channels on connect() so Iā€™m not sure how I would add callbacks to them without forking/modifying the source.

3 Likes

Yoy, innerHTML? Isnā€™t that pretty inefficient? o.O
Even Drab only sends what specifically needs to change in the DOM tree without sending anything like an HTML string after initial loadā€¦ o.O

Plus wouldnā€™t that reset things to an initial state when they already have other state, like a textfield?

Does this work with the last version of LiveView ? Iā€™ve receive the diff, but although the URL changes, nothing is rendered on the page.