fable-compiler / Fable.Lit

Write Fable Elmish apps with Lit
https://fable.io/Fable.Lit/
MIT License
91 stars 13 forks source link

Problem importing raw css into ShadowDom #39

Open juselius opened 2 years ago

juselius commented 2 years ago

Importing verbatim css from an external source into a LitConfig.styles element does not work:

let mycss' : {| ``default``: string |} = JsInterop.importAll "./public/some.css"
let mycss = mycss'.``default``
...
LitElement.init (fun cfg ->
  cfg.useShadowDom <- true
  cfg.styles <- [ css $"{mycss}" ]
)

This compiles just fine, but crashes at runtime:

Error: LitElement.init must be called on top of the render function
    LitElementUtil_failInit http://localhost:3000/build/client/fable_modules/Fable.Lit.1.4.1/LitElement.fs.js:24
    Decorate http://localhost:3000/build/client/fable_modules/Fable.Lit.1.4.1/LitElement.fs.js:301
    <anonymous> http://localhost:3000/build/client/OlMap.js?import&t=1653653843482:180
[client.ts:28:12](http://localhost:3000/src/client/client.ts)

As a workaround, this works:

[<Import("unsafeCSS", "lit")>]
let unsafeCSS str = jsNative

LitElement.init (fun cfg ->
  cfg.useShadowDom <- true
  cfg.styles <- [ unsafeCSS mycss ]
)

Otherwise Fable.Lit is awesome!

AngelMunoz commented 2 years ago

Can you share how are you using the component?

LitElement.init must be called on top of the render function this error if I recall correctly only happens if you don't call it in the top of your LitElement decorated function so if we could see how you were using it on your first example it would be nice

Also I would test this in the final build mode because I'm not sure it is supported

juselius commented 2 years ago

Here is how I use it:

[<LitElement("ol-map")>]
let OlMap ()  =
    Hook.useHmr hmr
    let this, props =
        LitElement.init (fun cfg ->
            cfg.useShadowDom <- true
            cfg.props <-
                {|
                    map = Prop.Of(Map.map [], attribute = "map")
                |}
            cfg.styles <- [
                css $"{olCss}"
                // unsafeCSS olCss
                css $"""
                  .map {{
                    width: 100%%;
                    height: 80vh;
                    background: #a2e7ff;
                  }}"""
            ])
    Hook.useEffect (fun () ->
        if isNull this.shadowRoot |> not then
            let target = getShadowElementById this "map"
            props.map.Value.setTarget target
    )
    html $"""<div id="map" class="map"></div>"""

When you say final build mode, you mean the output of fable --run vite build?

alfonsogarciacaro commented 2 years ago

Lit has a restriction that styles must be statically declared with the css tagged template (so they can be analyzed and ensure they don't contain malicious code): https://lit.dev/docs/components/styles/#expressions

image

The error is misleading, it seems we're missing a Promise.catch here to show the actual error: https://github.com/fable-compiler/Fable.Lit/blob/82c3286f1c380b4f467ca5f3491b599cbf6658bd/src/Lit/LitElement.fs#L246-L247

juselius commented 2 years ago

What is then the correct way of getting a css package into a (shadowed) component? I'm interfacing OpenLayers to Fable (I'll release the package in due time), and OpenLayers comes with a bunch of css, and it would be nice (but not strictly necessary) to contain it in the Lit component.

AngelMunoz commented 2 years ago

One thing that the shadow DOM has is that it was not designed to work with global styles so this tends to create a lot of friction when you try to integrate with third party style libraries.

The simplest and fastest workaround would be to just not use shadow DOM if you depend heavily on third party css or while not optimal but doable you can add a style/link tag inside your shadow DOM component and it should work properly

but for completeness the ideal thing though would be to use css import assertions please note that while css assertions are already on the spec it is only supported in chrome and thus needs a polyfill

// please note that Fable doesn't have an out of the box
// way to import css/json assertions so you need to create 
// cssImport manually somehow
let styles: obj = cssImport "dependency.css"
// inside the config function
cfg.styles <- [ css $".my-local-style {{ display: flex }}"; unbox styles]

That being said I made a project which hasn't been updated in a while that covers this use case it's called Fable.ShadowStyles and you can see me hacking my way around thanks to vite here and using them here

juselius commented 2 years ago

Thank you for your suggestions @AngelMunoz!