# ShadowRoots and \`Browser.Dom\`

**URL:** <https://discourse.elm-lang.org/t/shadowroots-and-browser-dom/7791>\
**Category:** Learn\
**Created:** [September 30, 2021, 11:56pm UTC](https://discourse.elm-lang.org/t/shadowroots-and-browser-dom/7791 "2021-09-30T23:56:46Z")\
**Posts on this page:** 7\
**Page:** 1

<div class="post-metadata">

**Author:** ![theotherben](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/theotherben/32/2186_2.png) [@theotherben](https://discourse.elm-lang.org/u/theotherben)\
**Post date:** [September 30, 2021, 11:56pm UTC](https://discourse.elm-lang.org/t/shadowroots-and-browser-dom/7791/1 "2021-09-30T23:56:46Z")

</div>

I’m experimenting with putting Elm apps inside of shadow roots in custom elements, and almost everything is smooth — with the exception of `Browser.Dom` functions. When running an Elm app inside of a shadow root, you’d expect `Browser.Dom.getElement` to only have access to nodes inside the shadow root, and be unable to access anything outside.

However, the opposite seems to be true – try [this Ellie](https://ellie-app.com/frXqYMyRvxba1) and search for the `outsideShadow` and `insideShadow` elements. You’ll see that `outsideShadow` is findable, but `insideShadow` is not.

It appears that `getElement` and its kin use `document.getElementById` (see [here](https://github.com/elm/browser/blob/1.0.2/src/Elm/Kernel/Browser.js#L298)), instead of doing something like:

```auto
app.node.getRootNode().getElementByID(...)

```

Has anyone run into this limitation before? Have you found any good workarounds?

---

<div class="post-metadata">

**Author:** ![wolfadex](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/wolfadex/32/2461_2.png) [@wolfadex](https://discourse.elm-lang.org/u/wolfadex)\
**Post date:** [October 1, 2021, 3:05am UTC](https://discourse.elm-lang.org/t/shadowroots-and-browser-dom/7791/2 "2021-10-01T03:05:43Z")

</div>

I wasn’t aware this was an issue, but I bet you code modify the compiled output to instead use the root node.

---

<div class="post-metadata">

**Author:** ![jreusch](https://avatars.discourse-cdn.com/v4/letter/j/b5e925/32.png) [@jreusch](https://discourse.elm-lang.org/u/jreusch)\
**Post date:** [October 1, 2021, 8:30am UTC](https://discourse.elm-lang.org/t/shadowroots-and-browser-dom/7791/3 "2021-10-01T08:30:01Z")

</div>

I was having the same issues ~1 year ago, and ended up re-implementing the `Browser.Dom` functions I needed using ports.

Edit as a follow up to your message:

Yes, we are still using it, but we have a custom-built request/response setup instead of using elm-porter. (This library was released shortly after we had this problem. But I’m sure it’s great!). It has been running smoothly in production for this whole year now, and I don’t remember any immediate problems we had that were related to this setup. It could definitely be improved - for example by using Promise-returning functions instead of a callback setup using ports directly, but we actually only ever needed 4 different effects.

My main complaint currently is that the Elm runtime does not have an easy way to shut it down - so currently we just live with leaking memory and producing runtime errors somewhere deep in the virtual dom if the custom element gets removed.

Our Javascript side looks a little bit like this:

```auto
const shadow = this.attachShadow({ mode: 'open' })

const styles = document.createElement('style')
styles.textContent = css.toString()

shadow.appendChild(styles)

const wrapper = document.createElement('div')
shadow.appendChild(wrapper)

const app = ElmApp.init({
    node: wrapper,
    flags: {
        // ..
    }
})

function withElement (id, data, callback) {
    requestAnimationFrame(() => {
        const el = shadow.getElementById(data)
        if (el) {
            app.ports.incoming.send({
                id,
                data: callback(el)
            })
        }
    })
}

app.ports.outgoing.subscribe(({ id, tag, data }) => {
    if (tag === 'getViewportOf') {
        withElement(id, data, el => ({
            scene: {
                width: el.scrollWidth,
                height: el.scrollHeight
            },
            viewport: {
                x: el.scrollLeft,
                y: el.scrollTop,
                width: el.clientWidth,
                height: el.clientHeight
            }
        }))
    } else if (tag === 'focus') {
        // ...
    }
})

```

in Elm, we then use a custom version of `Browser.element` to do all the plumbing:

```elm
port module Runtime exposing
    ( Effect
    , RuntimeProgram
    , attempt
    , batch
    , cmd
    , element
    , emitEvent
    , fail
    , js
    , map
    , none
    , perform
    )

import Browser
import Dict exposing (Dict)
import Html exposing (Html, div, h1, pre, text)
import Html.Attributes exposing (class)
import Json.Decode as Dec exposing (Decoder, Value)
import List.Extra as List
import Task exposing (Task)

-- MAKING PROGRAMS

type RuntimeModel model msg
    = RuntimeModel model (InternalModel msg)

type RuntimeMsg msg
    = A msg
    | R InternalMsg

type alias RuntimeProgram flags model msg =
    Program flags (RuntimeModel model msg) (RuntimeMsg msg)

element :
    { init : flags -> ( model, Effect msg )
    , update : msg -> model -> ( model, Effect msg )
    , view : model -> Html msg
    , subscriptions : model -> Sub msg
    }
    -> RuntimeProgram flags model msg
element { init, update, view, subscriptions } =
    Browser.element
        { init = runtimeInit init
        , update = runtimeUpdate update
        , view = runtimeView view
        , subscriptions = runtimeSubscriptions subscriptions
        }

-- EFFECT TYPE

type Effect msg
    = Error String
    | SendToJs String Value (Decoder msg)
    | EmitEvent String Value
    | SendToElm (Cmd msg)
    | Batch (List (Effect msg))

map : (msg1 -> msg2) -> Effect msg1 -> Effect msg2
map f effect =
    case effect of
        Error msg ->
            Error msg

        EmitEvent tag data ->
            EmitEvent tag data

        SendToJs tag value responseDecoder ->
            SendToJs tag value (Dec.map f responseDecoder)

        SendToElm theCmd ->
            SendToElm (Cmd.map f theCmd)

        Batch effects ->
            Batch (List.map (map f) effects)

-- EFFECTS

none : Effect msg
none =
    Batch []

batch : List (Effect msg) -> Effect msg
batch =
    Batch

fail : String -> Effect msg
fail =
    Error

cmd : Cmd msg -> Effect msg
cmd =
    SendToElm

perform : (a -> msg) -> Task Never a -> Effect msg
perform toMsg task =
    SendToElm <| Task.perform toMsg task

attempt : (Result err value -> msg) -> Task err value -> Effect msg
attempt toMsg task =
    SendToElm <| Task.attempt toMsg task

js : String -> Value -> Decoder msg -> Effect msg
js =
    SendToJs

emitEvent : String -> Value -> Effect msg
emitEvent =
    EmitEvent

-- INTERNAL RUNTIME

port events : { name : String, data : Value } -> Cmd msg

port outgoing : { id : Int, tag : String, data : Value } -> Cmd msg

port incoming : (Value -> msg) -> Sub msg

type InternalModel msg
    = Running (RunningModel msg)
    | Errored String

type alias RunningModel msg =
    { nextId : Int
    , requestsInFlight : Dict Int (Decoder msg)
    }

type InternalMsg
    = Recieved Value

runtimeInit : (flags -> ( model, Effect msg )) -> flags -> ( RuntimeModel model msg, Cmd (RuntimeMsg msg) )
runtimeInit init flags =
    let
        ( model, effect ) =
            init flags

        emptyModel =
            Running
                { nextId = 1
                , requestsInFlight = Dict.empty
                }

        ( internalModel, effectCmd ) =
            handleEffects emptyModel effect
    in
    ( RuntimeModel model internalModel, effectCmd )

runtimeUpdate :
    (msg -> model -> ( model, Effect msg ))
    -> RuntimeMsg msg
    -> RuntimeModel model msg
    -> ( RuntimeModel model msg, Cmd (RuntimeMsg msg) )
runtimeUpdate update runtimeMsg ((RuntimeModel appModel internalModel) as model) =
    case runtimeMsg of
        A appMsg ->
            let
                ( newAppModel, effect ) =
                    update appMsg appModel

                ( newInternalModel, effectCmd ) =
                    handleEffects internalModel effect
            in
            ( RuntimeModel newAppModel newInternalModel, effectCmd )

        R internalMsg ->
            case internalModel of
                Running runningModel ->
                    let
                        ( newInternalModel, effectCmd ) =
                            runningUpdate runningModel internalMsg
                    in
                    ( RuntimeModel appModel newInternalModel, effectCmd )

                Errored _ ->
                    ( model, Cmd.none )

runningUpdate : RunningModel msg -> InternalMsg -> ( InternalModel msg, Cmd (RuntimeMsg msg) )
runningUpdate model msg =
    case msg of
        Recieved value ->
            case Dec.decodeValue (Dec.field "id" Dec.int) value of
                Ok id ->
                    case Dict.get id model.requestsInFlight of
                        Just handler ->
                            case Dec.decodeValue (Dec.field "data" handler) value of
                                Ok appMsg ->
                                    ( Running
                                        { model
                                            | requestsInFlight =
                                                Dict.remove id model.requestsInFlight
                                        }
                                      -- We COULD just immediately call the App update here,
                                      -- but I think its better so see the message explicitely in the debugger and stuff
                                    , Task.perform A (Task.succeed appMsg)
                                    )

                                Err decodeError ->
                                    ( Errored <| Dec.errorToString decodeError
                                    , Cmd.none
                                    )

                        Nothing ->
                            ( Errored <| "Recieved a javascript message with id " ++ String.fromInt id ++ ", which I don't know about!"
                            , Cmd.none
                            )

                Err decodeError ->
                    ( Errored <| Dec.errorToString decodeError
                    , Cmd.none
                    )

runtimeView :
    (model -> Html msg)
    -> RuntimeModel model msg
    -> Html (RuntimeMsg msg)
runtimeView view (RuntimeModel appModel internalModel) =
    case internalModel of
        Running _ ->
            Html.map A (view appModel)

        Errored errorMessage ->
            div [class "bg-red-200 p-4 text-red-800 antialiased"]
                [h1 [ class "font-bold text-4xl"] [text "Systemfehler"]
                , pre [class "mt-4 text-sm leading-none"] [text errorMessage]
                ]

runtimeSubscriptions :
    (model -> Sub msg)
    -> RuntimeModel model msg
    -> Sub (RuntimeMsg msg)
runtimeSubscriptions subscriptions (RuntimeModel appModel _) =
    Sub.batch
        [ incoming (R << Recieved)
        , Sub.map A (subscriptions appModel)
        ]

handleEffects : InternalModel msg -> Effect msg -> ( InternalModel msg, Cmd (RuntimeMsg msg) )
handleEffects model effect =
    case model of
        Running runningModel ->
            case effect of
                Error errorMessage ->
                    ( Errored errorMessage, Cmd.none )

                EmitEvent name data ->
                    ( model
                    , events
                        { name = name
                        , data = data
                        }
                    )

                SendToJs tag data responseDecoder ->
                    ( Running
                        { runningModel
                            | nextId = runningModel.nextId + 1
                            , requestsInFlight = Dict.insert runningModel.nextId responseDecoder runningModel.requestsInFlight
                        }
                    , outgoing
                        { id = runningModel.nextId
                        , tag = tag
                        , data = data
                        }
                    )

                SendToElm theCmd ->
                    ( model, Cmd.map A theCmd )

                Batch effects ->
                    List.mapAccuml handleEffects model effects
                        |> Tuple.mapSecond Cmd.batch

        Errored _ ->
            ( model, Cmd.none )

```

---

<div class="post-metadata">

**Author:** ![theotherben](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/theotherben/32/2186_2.png) [@theotherben](https://discourse.elm-lang.org/u/theotherben)\
**Post date:** [October 1, 2021, 5:31pm UTC](https://discourse.elm-lang.org/t/shadowroots-and-browser-dom/7791/4 "2021-10-01T17:31:22Z")

</div>

This is super helpful – thank you @jreusch! I’ll give it a go.

@wolfadex – that’s also an interesting idea; monkey-patching the compiled `Browser.Kernel` to point it to the right node… One minor challenge is that the [Browser kernel code](https://github.com/elm/browser/blob/1.0.2/src/Elm/Kernel/Browser.js#L298) doesn’t actually have a reference to the root node of the app in scope at all. It’d be handy if that was around! I can probably figure out a way to get that in scope, perhaps doing something like:

```auto
const elmRoot = document.createElement('div'); // This is where we'll initialize the elm.
const compiledElm = ...; // get the generated JS as a string
const monkeyPatched = compiledElm.replace('document.getElementById', 'appRoot.getRootNode().getElementById');
new Function('appRoot', compiledElm)(elmRoot);

```

---

<div class="post-metadata">

**Author:** ![theotherben](https://yyz1.discourse-cdn.com/flex035/user_avatar/discourse.elm-lang.org/theotherben/32/2186_2.png) [@theotherben](https://discourse.elm-lang.org/u/theotherben)\
**Post date:** [October 1, 2021, 5:54pm UTC](https://discourse.elm-lang.org/t/shadowroots-and-browser-dom/7791/5 "2021-10-01T17:54:50Z")

</div>

I just gave the monkey-patching approach I mentioned a try, and it does work!

[Here’s](https://github.com/bengolds/elm-shadow-dom/blob/master/index.html) a tiny repo showing the approach; it also shows how to initialize an Elm module without polluting the `window` object.

---

<div class="post-metadata">

**Author:** ![jackyjoy123](https://avatars.discourse-cdn.com/v4/letter/j/d6d6ee/32.png) [@jackyjoy123](https://discourse.elm-lang.org/u/jackyjoy123)\
**Post date:** [October 4, 2021, 12:21pm UTC](https://discourse.elm-lang.org/t/shadowroots-and-browser-dom/7791/6 "2021-10-04T12:21:48Z")

</div>

thanks for the awesome information.

---

<div class="post-metadata">

**Author:** ![system](https://canada1.discourse-cdn.com/flex035/uploads/elm_lang/original/1X/50a05e53677a2c3b47776d7abd0f113eb50193a1.png) [@system](https://discourse.elm-lang.org/u/system)\
**Post date:** [October 14, 2021, 12:21pm UTC](https://discourse.elm-lang.org/t/shadowroots-and-browser-dom/7791/7 "2021-10-14T12:21:57Z")

</div>

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.
