hayleigh

hayleigh

Best way to handle Custom Element events in LiveView?

Hey folks! I’m the author of Lustre which is a frontend framework written in Gleam. Lustre has the ability to run components as native Custom Elements with support for all the good stuff like attributes, properties, custom events, form association, …

I’m currently working on a headless ui library and I’d like to document and encourage its use in Phoenix and LiveView apps, but I’m not LV expert and it’s a little unclear to me how one is supposed to handle events emitted by a custom element. I found a thread from last year that asks a similar question but it looks like the conversation fizzled out.

As an example, the following is a hypothetical menu that supports both keyboard and pointer interaction. When a menu item is selected (by any means) the menu will emit an event - "lustre-ui:select" - with a payload containing the value of the item selected.

<lustre-ui-menu>
  <lustre-ui-menu-item value="ignore">
    Ignore
  </lustre-ui-menu-item>
  <lustre-ui-menu-item value="block" class="danger">
    Block
  </lustre-ui-menu-item>
  <lustre-ui-menu-item value="report" class="danger">
    Report user profile
  </lustre-ui-menu-item>
</lustre-ui-menu>

In a typical client app, you would attach an event listener to the <lustre-ui-menu> element and perform whatever associated action when an item was selected, but of course what we’d like to do is send a message to our LiveView process some how.

As I understand it, LiveView only supports a fixed and predetermined set of native events for things like phx-click. What would be the recommended way to work with custom elements that emit non-standard events in LV?

Thanks y’all! :folded_hands:

Most Liked

hayleigh

hayleigh

Just like click / input / change are enough for native link / button / input / select
…doesn’t venture out to invent new ways to interact with those elements.

Just so we’re clear, native HTML <select> supports all of the things I just listed. We are in agreement that the standards set by native ui widgets should be the baseline, but I think that baseline is much harder to meet than most folks initially think.

Accessibility is about both functionality and semantics: this is why aria-* attributes exist so that we can tell the browser to replace the native semantics of an element with something else. This is important because when we use existing HTML elements, non-sighted users do not get visual cues that the thing they’re interacting with does something different compared to normal.

In the case of our menu made up of buttons, it is a common expectation of the “menu” pattern that it close once an item is selected. We were satisfied that our styled <div> with some buttons in looked like a menu but for a non-sighted user they saw a collection of controls. When they activate one, focus will be taken away from the button and (if you were diligent) moved elsewhere (and if you werent diligent they now have no focus at all).

Fortunately there are ways to communicate these semantics, (in this case role menu on the wrapper, role menuitem on the buttons). Custom elements can report these semantics natively, without need for developers to provide aria-* attributes.


On the question of why this might be of particular relevance for LV users. As i see it the current state of play is mostly one of these options:

  • Be satisfied with native HTML elements and the interfaces you can craft with them, don’t fix what isn’t broke. In lots of cases this is not just good enough but good.

  • Be unsatisfied with what native HTML elements provide and begin building pieces of interactive ui in LV using the interaction patterns LV makes simple (click, keydown, etc) similar to our hypothetical button-based menu.

  • Look to exist client-side rendering options and how you might integrate them with LV, (livesvelte, livevue, i assume theres some react version, …)

If the first option describes you that is totally valid, but is also not the kind of user to ever find a ui component library useful, so perhaps a bit moot to discuss.

For the second option: one of the best things about LV is that it provides an entry for backend devs and engineers in general that would otherwise be put off doing frontend work to actually start crafting interactive experiences on the web. The flipside of that, perhaps, is we might be more willing to make trade-offs in terms of accessibility and user experience because of different priorities, lack of experience, or lack of interest.

And I mean the third option means fully embracing JS and everything that entails - for some that means getting access to the best of both worlds, but I would guess for many of us it means interacting with an ecosystem we would really rather not be and spending time writing code in a language we would really rather prefer not to.

I think a robust custom element library meets LV exactly where it wants to be: rendering HTML, while ideally abstracting all the hard bits away :slight_smile:

spicychickensauce

spicychickensauce

In my projects, I’ve started to move away from hooks and started using the following pattern instead:

window.addEventListener("phx-x:register-events", (event) => {
    const target = event.target;
    const onScrollend = target.getAttribute("phx-x-on-scrollend");
    target.addEventListener("scrollend", (e) => {
        liveSocket.execJS(target, onScrollend);
    })
})
<div
  phx-mounted={JS.dispatch("phx-x:register-events")}
  phx-x-on-scrollend={JS.push("scrollend")}
/>

This has the advantage over hooks that you do not need to set an ID, which makes it a lot more reusable.
I think this pattern might also work quite well with custom elements.

I really would advise against using hooks for UI elements, as the ID requirement leads to having to pass down id prefixes everywhere.

Btw, thank you for trying to tackle this hard problem! I’ve spent hours searching for good custom elements headless UI libraries, but I couldn’t find any.

LostKobrakai

LostKobrakai

I’d argue the big “unknown” with webcomponents is the question if the emitted events are actually intended to be sent to the server (vs. meant to used client side). The current solution would be to have a liveview js hook somewhere up the tree, which would attach an event listener explicitly for events and use this.pushEvent in the hook to forward the data to the server.

As for (form-)inputs the best way is imo to implement ElementInternals - Web APIs | MDN to make the webcomponent a proper form enabled component. Those should then work out of the box with the form abstractions of liveview.

Last Post!

spicychickensauce

spicychickensauce

With liveview 1.1 there is now a new way to do it without using any internals, see:

So here is the new way of doing it:

window.addEventListener("phx-x:register-events", (event) => {
    const target = event.target;
    const onScrollend = target.getAttribute("phx-x-on-scrollend");
    target.addEventListener("scrollend", (e) => {
        const js = liveSocket.js();
        js.push(target, onScrollend, { value: { someData: 42 } });
    })
})
<div
  phx-mounted={JS.dispatch("phx-x:register-events")}
  phx-x-on-scrollend="scrollend"
/>

Where Next?

Popular in Questions Top

nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
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
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
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
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
sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New

Other popular topics Top

ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
saif
Hello everyone, Long time lurker first time poster here. I’ve recently begun working on Elixir full-time again! :raised_hands: It’s been...
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
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New

We're in Beta

About us Mission Statement