Reputation: 68
Problem summary I am using webcomponents in elm 0.19.1. Goal is for the webcomponent to emit an event if the webcomponent attribute "requeststate" changes. The event is emitted (I can see in the the logging in the webcomponent constructor), but elm is not firing the 'WebcomponentEvent' correctly. Problem is on both windows 10 and ubuntu 16.04, firefox and chrome. Didnot test older elm versions.
Steps to reproduce:
Opening the elm-debugger or clicking the 'request' button again will magically make the event fire in elm. Strange. Also I have made the branch 'tom-experiment'. In this branch I got the event from webcomponent-click working, but only if I directly click on the webcomponent itself.
The importance of this problem Why do I want to trigger an event on a webcomponent by changing an attribute? Goal of this approach is also JavaScript interop. For example I can now use this webcomponent to create a date-time or uuid or do some other javascript magic. This way I can work around ports completely. A solution for this problem might settle the entire Javascript interop discussion !
Code
This is my Main.elm:
module Main exposing (..)
import Browser
import Html
import Html.Attributes
import Html.Events
import Json.Decode
main =
Browser.sandbox
{ init = init
, view = view
, update = update
}
type alias Model =
{ request : Bool
, received: Bool
}
init : Model
init = { request = False
, received = False
}
type Msg
= Request
| Reset
| WebcomponentEvent
update : Msg -> Model -> Model
update msg model =
case msg of
WebcomponentEvent ->
{ model | received = True}
Request ->
{ model | request = True }
Reset ->
{ model | request = False , received = False}
view : Model -> Html.Html Msg
view model =
Html.div []
[ Html.button [Html.Events.onClick Request] [Html.text "Request"]
, Html.div
[]
[ Html.text "requested:"
, Html.text (if model.request then "true" else "false")
]
, Html.div
[]
[ Html.text "received:"
, Html.text (if model.received then "true" else "false")
]
, Html.button [Html.Events.onClick Reset] [Html.text "Reset"]
, Html.node "webcomponent-test"
[ Html.Events.on "created" (Json.Decode.succeed WebcomponentEvent)
, Html.Attributes.attribute "requeststate" (if model.request == True then "requested" else "idle")
] []
]
This is the webcomponent.js
class Webcomponent extends HTMLElement {
static get observedAttributes() {
return [
"requeststate"
];
}
attributeChangedCallback(name, oldValue, newValue) {
switch (name) {
case "requeststate":
if (newValue === "requested") {
console.log(`requested in webcomponent triggered`);
const customEvent = new CustomEvent('created', {detail: ""});
this.dispatchEvent(customEvent);
}
}
}
constructor() {
super();
this.addEventListener('created', function (e) {
console.log("event triggered as sensed by javascript: ", e.detail, e);
}, false, true);
}
}
customElements.define('webcomponent-test', Webcomponent);
this is my index.html
<!doctype html>
<html>
<head>
<script type="module" src="./webcomponent.js"></script>
<script src="./elm.js"></script>
</head>
<body>
<div id="elm_element"></div>
<script>
var app = Elm.Main.init({
node: document.getElementById('elm_element')
});
</script>
</body>
</html>
Upvotes: 4
Views: 491
Reputation: 21173
There is another potential cause:
This attributeChangedCallback
runs before the element is connected to the DOM
attributeChangedCallback(name, oldValue, newValue) {
switch (name) {
case "requeststate":
if (newValue === "requested") {
console.log(`requested in webcomponent triggered`);
const customEvent = new CustomEvent('created', {detail: ""});
this.dispatchEvent(customEvent);
}
}
}
Add
connectedCallback(){
console.log("Now I am ready to emit Events");
}
to verify your dispatchEvent
doesn't run too soon.
requestAnimationFrame
(or setTimeout
) are workarounds to wait till the Event Loop is empty (and thus connectedCallback ran)
(I don't know your use case) You could also test for oldValue === null
or this.isConnected
in your attributeChangedCallback
Also note you probably need bubbles:true
and composed:true
on that CustomEvent when shadowDOM is involved.
Upvotes: 3
Reputation: 176
This was discussed on the Elm Slack. The issue ended up being a timing issue. The resolution is to change from
this.dispatchEvent(customEvent)
to
requestAnimationFrame(() => this.dispatchEvent(customEvent))
in the custom element, as can be seen in this ellie https://ellie-app.com/cqGkT6xgwqKa1.
Thanks to @antew for the final solution.
Upvotes: 7