I would like to share my unorthodox solution to a model synchronization problem I encountered in my first Elm project where I wanted an Elm element to render into multiple separate DOM nodes on a page.
I wasn’t aware of this “Bring Your Own DOM” approach but I did try a different experiment with <template> that didn’t quite work (nothing ever got rendered).
If I understand your code correctly the “elm-portal” element Elm sees is something of a proxy which forwards the custom element dom operations to the target nodes which I assume can be anywhere on the page. This is quite clever and something I’m going try out. It obviously has the benefit of not needing any patching like Elm Multiview does.
Thank you for pointing this out to me. I will see if I can make it work for my app.
Cim
Yep, that’s exactly how it works! I’ve only ever used it with Browser.application and Browser.document so I’m not certain how well it’ll work for your use case.
If you happen to use it’d I’d love to hear about how it goes. This is outside the use cases I’d thought of.
That’s the beauty, and also where the idea came from, it uses Elm’s vdom. It’s vdom stays untouched and all I do is forward requests from 1 node to another.
The whole thing came about from Ryan asking me to pair on this web component with him to help with dropdowns and tooltips. He had 80% of a solution that was using cloneNode, but that doesn’t clone properties (e.g. event listeners). As we were pairing I realized we were basically implementing our own vdom. That’s when I came up with the idea to just let Elm do all of the work and only proxy the necessary API calls between DOM nodes. That’s why the elm-portal web component is so small too, Elm realistically only uses these 5 browser functions
get childNodes() {
return this._targetNode.childNodes;
}
replaceData(...args) {
return this._targetNode.replaceData(...args);
}
removeChild(...args) {
return this._targetNode.removeChild(...args);
}
insertBefore(...args) {
return this._targetNode.insertBefore(...args);
}
appendChild(...args) {
// To cooperate with the Elm runtime
requestAnimationFrame(() => {
return this._targetNode.appendChild(...args);
});
}
and all the component does is pass values between the source (itself) & target node.
Brief aside, Elm does use a few more DOM APIs internally for things like setting the window title and attaching to the document.body. There’s some really fun stuff you can do if you provide these functions to the scope you initiate Elm in.
There are a lot of fun hacks with Elm when you’re familiar with JS, but this is one of the least hacky things I’ve ever done.
This rainy morning I did a quick test and converted my multiview example to use elm-portal.
@wolfadex 's example worked flawlessly apart from one small detail: targetId should be data-target-selector and it should be specified as a selector (i.e. #target-id-1) and not an id (i.e. target-id-1).
I’ve added the elm-portal example to my https://elm-multiview.securepub.org page along with a recommendation to prefer that approach. Over time I’ll phase it into Securepub.
One other thing: I couldn’t find any licensing for elm-portal so I left it as “unspecified”.
If that is incorrect, please let me know and I’ll update accordingly.
I’m happy you found a better solution than targetId for your project. I only used that in the gist because it was short and easy to understand. It’s very possible you’ll need other modifications as well. E.g. at work we also do our tooltip positioning within the elm-portal JS code.
Don’t worry about a license. I never considered adding a license when I made it, not sure it needs one either.