Stuurpiek
Stuurpiek

Reputation: 68

Elm with webcomponents - Is this a bug or am I doing something wrong?

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

Answers (2)

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

wolfadex
wolfadex

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

Related Questions