Doing JS events in LiveView

phoenix
liveview
#1

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?

0 Likes

#2

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.

0 Likes

#3

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:

0 Likes

#4

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

0 Likes

#5

Javascript interop isn’t supported just yet. It’s a planned feature.

4 Likes

LiveView: How to trigger JS function? (Update chart.js on data change)
#6

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: )

0 Likes

#7

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.

0 Likes

#8

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: How to trigger JS function? (Update chart.js on data change)
#9

LiveView uses morphdom, which uses innerHTML. Browsers don’t load script tags inserted with innerHTML.

0 Likes

Javascript not loading properly in LiveView
#10

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.

0 Likes

#11

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.

0 Likes

#12

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.

0 Likes

#13

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

1 Like

#14

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

0 Likes

Javascript not loading properly in LiveView
#15

Yep, this is working.

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

#16

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)
})
0 Likes