Skip to content

In CustomElements, DOM #shadow-root slot semantics are not being honored, Vue is stepping on them #49

Open
@smallscript

Description

@smallscript

TL;DR
Issue:
vue-web-component-wrapper prevents a WebComponent #shadow-root from containing DOM rendered slot elements.

In its implementation, the wrap function it provides is focused on creating CustomElements tags that behave as VueComponents; rather than focusing on behaving as WebComponents defined from a VueComponent that integrates, with shadow-DOM semantics, as a direct child-element in their #shadow-root definition.

I've written a Vue vue-web-component-plugin (<=) , using a different design, that supports DOM WebComponent slot behavior. It is approximately ~100 lines of self-contained ready to use Javascript .msj module code.

Some of the other differences from wrap are:

  • seamless Vue.component registration via Vue.component(..)
  • any ancestral $root vue is inherited/shared, or if none, then a $root vue is created as needed
  • lazy Vue integration of DOM upgraded custom HTMLElements; reducing temporal side-effects.
  • vueDefinition key shadowCss support for #shadow-root encapsulated css definitions

For slots to work, I had to enhance Vue with a way to honor DOM semantics for #shadow-root slot elements.

Note:
The following discussion assumes some familiarity with the DOM concepts slot, light DOM and shadow DOM.

For more detail on what wrap does, and why it is different from the plugin I mentioned above, I will briefly present a minimal description of how Vue VueComponents work and describe a few browser terms for clarity.

The Vue system has VueComponents (similar to classes) that are based on vueDefinitions associated with a tagName using Vue.component(tagName, vueDefinition). I will be referring to these VueComponents as HTML macros, from here on.

When you create a new $root vue using new Vue(vueDefinition) with a el: member in the vueDefinition (or manually invoke its' $mount), Vue will walk the childElements and recursively perform macro substitution of all the elements whose tagName matches the VueComponents you registered using Vue.component(tagName, vueDefinition). It will continue to perform this behavior based on reactive change tracking from that point on.

It does this using the render function defined on the vueDefinition.

If no render function is defined, it will look for a template to compile into a render function. If no template is defined, it will extract the innerHTML of the corresponding tagName element, and use that as the template.

This makes Vue a nicely designed and powerful reactive HTML macro (aka VueComponent) system, where arguments to the tagName element macros are provided in their attributes, and all rendered macro results are reactively tracked for efficient management of change propagation (observe and react patterns from Smalltalk). In that same system templates and (innerHTML) support {{..}} substitution rules.

The browser DOM allows you to define CustomElements and WebComponents.

To enable that, it provides the customElements API to allow registering a Javascript class as the behavior for an element with a hyphenated tagName.

A custom element once registered is a new type of browser DOM element; depending on your browser, custom elements technology is the same technique by which it internally implements a number of the intrinsic browser elements with closed #shadow-roots.

A custom element is not a macro and thus can't do some of the things Vue VueComponents can, but a WebComponent using a shadow DOM #shadow-root can provide some capabilities that Vue VueComponents cannot. This can be specifically seen in how it uses DOM slot elements, and how it lazily upgrades tagName elements when a corresponding custom element is registered, and in how it encapsulates the #shadow-root elements including css.

When you issue the call:

customElements.define(tagName, behavior_class, options)

you are defining a CustomElement. If that CustomElement will utilize a #shadow-root then you are defining a WebComponent. A WebComponent uses slot elements within the #shadow-root for slotting (transposing) the innerHTML (DOM Nodes) contained within any HTML that has that tagName element.

What's the problem, why should I think about these things?

Given that background we now have the foundation to understand why wrap does not provide expected WebComponent #shadow-root semantics and behavior, but does allow self-contained CustomElement isolated $root wrappering of Vue VueComponent vueDefinitions.

Phew, sorry you had to read all that. It is a lot of pedantically crafted explanatory discussion for saying DOM slot elements in a WebComponent's shadowRoot don't work in Vue, and that you can't use WebComponents (defined by a vueDefinition) properly in Vue without some modifications to Vue. Specifically, we need a way to tell Vue to treat appropriately marked slot elements, as DOM slot elements and not as Vue slot macros.

Basically, Vue thinks that any slot elements it sees within a vue subtree of VNodes are slot elements that it should always use as its (Vue's) own macro element directives for performing transpositional replacement of innerHTML content into a render/template definition.

That behavior leads to a direct conflict with the DOM semantics of slot elements in #shadow-roots.

In other words Vue grabs all slot elements wherever it sees them, which with wrap defined CustomElements includes their #shadow-root. Therefore the DOM never gets to see them in a wrapped WebComponents #shadow-root and can't perform the expected DOM slotting semantics.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions