Sebb
Let's make shoelace work with liveview
Motivated by the success @benkimpel had with shoelace (see: Improve Support for Web Components in Forms with "Element Adapters") and the thread by @adw632 (see: Adobe Spectrum 2 web components with LiveView) I tried it myself.
I had some success, but there are also some problems I do not know how to solve and if they are solvable.
Solved? Problem 1 - LV removes attributes set by shoelace
If you do not set all relevant attributes on an element, shoelace sets defaults, LV removes them. This can be easily solved by either wrapping all shoelace elements into function components (see Ben’s thread) or just:
SL_DEFAULTS = {
"SL-BUTTON": { "variant": "default", "size": "medium" },
"SL-AVATAR": { "shape": "circle" },
"SL-BADGE": { "variant": "neutral" },
"SL-ICON": { "library": "default" },
...
}
let liveSocket = new LiveSocket("/live", Socket, {
params: { _csrf_token: csrfToken },
dom: {
onBeforeElUpdated(_from, to) {
const sl_defaults_for_current = SL_DEFAULTS[to.tagName];
if (sl_defaults_for_current) {
Object.entries(sl_defaults_for_current).forEach(([key, value]) => {
if (!to.hasAttribute(key)) {
to.setAttribute(key, value);
}
});
}
}
}
})
this seems to work fine.
Solved? Problem 2 - LV removes state like in @open
Multiple components store their state in an @open, eg <sl-details>
<sl-details summary="Toggle Me">
Lorem ipsum ...
</sl-details>
So when you
- render → closed (
@opennot set) - toggle → opens (
@openset) - rerender → closes (
@openremoved by LV)
This can be fixed by sth like
window.addEventListener("sl-after-show", (evt) => {
liveSocket.execJS(evt.target, '[["set_attr", {"attr": ["open", true]}]]');
})
window.addEventListener("sl-after-hide", (evt) => {
liveSocket.execJS(evt.target, '[["remove_attr", {"attr": "open"}]]');
})
Or sth more sophisticated (see Ben’s thread)
Seems to work fine.
Problem 3 - LV removes elements that shoelace places into the light-DOM
This happens with <sl-breadcrumb>, code:
<sl-breadcrumb>
<sl-icon name="arrow-right" slot="separator" aria-hidden="true"></sl-icon>
<sl-breadcrumb-item>
<sl-icon slot="prefix" name="house"></sl-icon>First
</sl-breadcrumb-item>
<sl-breadcrumb-item>Second</sl-breadcrumb-item>
<sl-breadcrumb-item>Third</sl-breadcrumb-item>
</sl-breadcrumb>
This is how the breadcrumbs look like after first render (correct):

Note the arrow-icon that shoelace dynamically put into the separator slot of the item:
after a rerender it looks like:

Note the missing arrow-icon:
Problem 4 - LV removes generated classes
happens with <button-group>, code:
<sl-button-group label="Alignment">
<sl-button size="small">Left</sl-button>
<sl-button size="small">Center</sl-button>
<sl-button size="small">Right</sl-button>
</sl-button-group>
first render (correct):

note the classes:
after rerender:
classes missing:

Problem 5 - ARIA
Seems like shoelace puts some important aria attributes which LV removes, didn’t look into that.
Problem 6 - Forms
This does absolutely not work right now, see Ben’s thread.
Most Liked
Sebb
sad story.
Thank’s for all the help so far, did put me on the right track.
We’ll see what comes of it.
benkimpel
This is great. Let’s see if we can fix these.
When we were testing it out we found that adding an ID to some of the Shoelace slots helped with patching. It triggers different behavior in morphdom since the id is the nodeKey so rather than patching as children it patches directly.
There’s a more drastic option as well…
// WARNING: Lazy example. There's probably much more to it.
onBeforeElUpdated: (from, to) => {
// Effectively make sure Shoelace always wins the attr merge by assigning from
// back over it.
//
// This could be done better with a "Managed Attr" list where instead of this
// all or nothing approach below one could say:
// "Ok, for attr ABC of element XYZ we never patch it."
// or even handle some specifically...
// "For class of element XYZ we can merge the classes, but never remove"
//
// BUT I expect this would result in unnecessary renders all the time?
//
// This is why I think there are ultimately low-level changes involved if we want
// to support arbitrary WCs
//
if (from.tagName.startsWith("SL-")) {
[...to.attributes, ...from.attributes].forEach((attr) => {
to.setAttribute(attr.name, attr.value);
});
},
benkimpel
For example, this fixes the button group…
// onBeforeElUpdated
// Protect sl- classes
if (from.tagName.startsWith("SL-")) {
from.classList.forEach((cls) => {
if (cls.startsWith("sl-")) {
to.classList.add(cls);
}
});
}
But how fragile is all of this? idk
Popular in Discussions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance













