css-render icon indicating copy to clipboard operation
css-render copied to clipboard

ShadowDom Support

Open NiewView opened this issue 3 years ago • 7 comments

Hi 07akioni,

I'm planing to use the naive-ui library in combination with custom-elements and a shadow-dom. Unfortunately this combination is currently not possible, because the styles always get mounted to the main document (https://github.com/TuSimple/naive-ui/issues/2308). So I had a look how to add support for this and came across this repository.

Since you are an active member in both repositories, I have the following questions. Do you plan to continue using css-render in naive-ui and add support for custom-elements here as well? If so, do you already have an idea how the API of css-render should look like (maybe the possibility to add a custom handler, like ssr)?

I'm asking to help out, not to ask when it's done 😉 In all cases thanks for you great work in both projects.

Best regards NiewView

NiewView avatar Jun 03 '22 11:06 NiewView

I think we may provide a style mount hook in css-render. (props: { id: string, style: string }) => void. Then user can mount the style following their intention.

However I'm not very sure how it's going to be like exactly since I'm not familiar with shadow dom (for example why is the style, how many times it's mounted, how to check if some style is already mounted).

The key points are:

  1. Make sure style applied
  2. Do not generate style multiple times if it's already mounted (by id)

07akioni avatar Jun 04 '22 16:06 07akioni

The shadow dom is mounted once, when the custom element is initiated. The shadow dom is more or a less a additional document within an HTML-Element, by this it isolates all the styles in both directions (outside styles are not applied in the shadow dom and the other way around.) This also means one webpage can have x shadow-doms.

Because of this and your second point I don't think it is enough to provide a style mount hook. Then css-render would have no way to check, if styles are already mounted. Except the plan would be, to just always call the hook and leave it to the logic within it, to check if the styles are already mounted.

NiewView avatar Jun 10 '22 11:06 NiewView

The shadow dom is mounted once, when the custom element is initiated. The shadow dom is more or a less a additional document within an HTML-Element, by this it isolates all the styles in both directions (outside styles are not applied in the shadow dom and the other way around.) This also means one webpage can have x shadow-doms.

Because of this and your second point I don't think it is enough to provide a style mount hook. Then css-render would have no way to check, if styles are already mounted. Except the plan would be, to just always call the hook and leave it to the logic within it, to check if the styles are already mounted.

I wonder if style created in a component instance can be reused in another component instance?

For example:

<custom-button />
<custom-button />

Can these 2 buttons share same style (with only 1 time style generation)

07akioni avatar Jun 10 '22 15:06 07akioni

I'm not sure, if I understand you correctly. When I build a vue app as custom element the stlyes will only be applied within the custom element, so it would look something like this

<custom-button>
  # shadow-root
    <styles>/* styles from css-render */</styles>
    <button></button>
  #end shadow-root
</custom-button>
<custom-button>
  # shadow-root
    <styles>/* styles from css-render */</styles>
    <button></button>
  #end shadow-root
</custom-button>

It is ensured, that the implementation for both custom-button is the same, because you can only register a custom-element once. But the styles need to be added within each shadow-dom separate.

If you were only talking about the generation part within css-render, I think it could be theoretically possible to share the logic. But I don't think it is a good idea, because by default the components don't know from each other and they also should be separated. There is even a shadow-dom mode, which explicitly prohibits any js mutation within a DOM from externally.

If you attach a shadow root to a custom element with mode: closed set, you won't be able to access the shadow DOM from the outside — myCustomElem.shadowRoot returns null. link

That's why I would recommend generating the styles for each element individually and not breaking the border between them.

NiewView avatar Jun 13 '22 06:06 NiewView

Current vue has some limitations. I can't access shadowRoot easily from the component. I think we may raise a feature request for vue. Then we can use the following method to reuse generated style.

https://jsbin.com/mafiyidave/3/edit?html,console,output

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
  <script src="https://unpkg.com/vue"></script>
  <script>
    const style = '.foo { color: red; }'
    let styleUrl = ''
    let styleGenerated = false
    
    const _El = Vue.defineCustomElement({
      setup () {
        const self = Vue.getCurrentInstance()
        Vue.onBeforeMount(() => {
          const linkEl = document.createElement('link')
          linkEl.rel = 'stylesheet'
          if (!styleGenerated) {
            console.log('generate style')
            styleUrl = URL.createObjectURL(new Blob([style]))
            linkEl.href = styleUrl
            styleGenerated = true
          } else {
            console.log('reuse style style')
            linkEl.href = styleUrl
          }
          self.shadowRoot.appendChild(linkEl)
        })
      },
      render () {
        return Vue.h('div', { class: 'foo' }, 'custom element')
      }
    })
    // Hack into Vue
    class El extends _El {
      constructor(...args) {
        super(...args)
        this.VueElement = Object.getPrototypeOf(Object.getPrototypeOf(this))
      }
      connectedCallback() {
        this.VueElement.connectedCallback.apply(this)
      }
      _createVNode() {
        const vnode = this.VueElement._createVNode.apply(this)
        if (!this._instance) {
          const ce = vnode.ce
          vnode.ce = instance => {
            instance.shadowRoot = this.shadowRoot
            return ce(instance)
          }
        } 
        return vnode
      }
    }
    customElements.define('custom-el', El)
  </script>
  <custom-el></custom-el>
  <custom-el></custom-el>
  <custom-el></custom-el>
</body>
</html>

07akioni avatar Jun 13 '22 18:06 07akioni

At least there's not much JS runtime expense. If possible I think it's better to benchmark normal way & style reuse way.

07akioni avatar Jun 13 '22 18:06 07akioni

ref https://github.com/vuejs/core/issues/6113

07akioni avatar Jun 14 '22 14:06 07akioni