sveltejs / svelte

Cybernetically enhanced web apps
https://svelte.dev
MIT License
78.4k stars 4.1k forks source link

Custom element without shadow DOM #1748

Closed nxtwrld closed 1 year ago

nxtwrld commented 6 years ago

Hi,

I would like to get your thoughts on having a switch for customElement behaviour.

Current custom elements are based on shadow DOM which can be really handy in some cases.

But in our project we heavily depend on cascading - components style depends on its context. (e.g. Button colors are different for different types of containers)

It would be handy, if we could choose between using shadow DOM and a simpler version without it. In the similar way we attach slots in connectedCallback, we could import the component itself into the the main document.

Here is a very crude example of the custom element without shadowDOM:

<template id="my-box">
   <style>
    :host {
        border: 1px solid #666;
    }
  </style>

  <p class="red"></p>
 </template>

<script>
(function() {
  const doc = (document._currentScript || document.currentScript).ownerDocument;
  const template = doc.querySelector('#my-box');

  customElements.define('my-box', class extends HTMLElement {
    constructor() {
      super();
    }

    connectedCallback() {
      const temp = document.importNode(template.content, true);
      this.appendChild(temp);
    }
  });
})();
</script>

<style>
.red {
   color: #F00;
}
</style>

<my-box></my-box>

Would someone else also find this useful?

bestguy commented 5 years ago

Hi @nxtwrld , yes this would be really useful and important for me too. My use-case is am trying to use custom component for encapsulating some complex components, but the styles and class names need to use external CSS classes (for theming across different apps, like Bootstrap). I can't embed styles in the component. Use of shadow DOM means I can't use svelte compiler for this.

nxtwrld commented 5 years ago

I am trying to tweak the svelte compiler and add a new option

customElement : true,
shadowDom: false   // defaults to true

I will post more info when I am done and it will pass or the tests.

vigie commented 5 years ago

Hey @nxtwrld did you finish your implementation of the shadowDom option? I can pick this up if needed. I can't move forward with Svelte without this, for the reasons stated and also because we have to support IE 11 which has no support for Shadow DOM

vigie commented 5 years ago

I am trying to tweak the svelte compiler and add a new option

customElement : true,
shadowDom: false   // defaults to true

By the way, perhaps the API should instead be:

shadowDom: 'open' | 'closed' | 'none'

I understand for backward compatibility it might be best to make "open" the default, but really I think a more natural default would be "none". Since this would go into a new major version, perhaps we could consider adding this breaking change also?

morewry commented 5 years ago

Just wanna chime in, this is a good idea and there are numerous reasons to want it, especially if you can still use Svelte's own support for the <slot> functionality that is (I presume) syntactical rather than relying on real Shadow DOM.

For my use cases, this functionality is needed to work around existing limitations in and issues with Shadow DOM so that Web Components can be effectively used throughout the transitional period while incompatibilities decrease, the platform fills in the gaps, etc.

The issues I've encountered or heard of that need workarounds include:

In any case where a component's interface would require slots, but Shadow DOM in its current incarnation has issues that make it not viable to use, you're between a rock and hard place. Either you get no slots, or you get no form participation, accessibility, or styles (perhaps at all, perhaps only with major expense). This can lead to no Web Components.

In the long term, my hope and assumption is that these issues will be fixed by increased support, new specs, or evolutions to the current specs and that eventually using Shadow DOM will have none of these significant downsides.

zephraph commented 5 years ago

I ran into this same issue with a component I was building for one of our experimental projects. I went ahead and started a PR to add the the option to disable shadow DOM. I'm not super familiar with Svelte internals at this point (or WCs generally), but we'll see where we get.

timonweb commented 4 years ago

Is there any progress?

marcus-sa commented 4 years ago

https://angular.io/api/core/ViewEncapsulation

pedroalexisruiz commented 4 years ago

Are you still working on this?

fsodano commented 4 years ago

Hi there, I've opened this PR https://github.com/sveltejs/svelte/pull/4073. I was wondering if any of the maintainers can give me some feedback on the approach/whether this is something Svelte wants?

fvybiral commented 4 years ago

Hi there, problem is, when you need to create classic html form (not submited via ajax) with form element created custom element, then you need light dom. Custom element cannot contain shadowed html forms.

I think that this is most important complexity feature comparing to Stencil. StencilJS is perfect for large projects. But svelte is svelte...

Please merge...

paladinu commented 4 years ago

Use of the Light DOM is a legit use case and I want to add my voice to the number of people for whom this blocker eliminates svelte from consideration. I hope it can be resolved/merged soon.

Manzurkds commented 4 years ago

Wanted to make a small authentication component with the help of svelte. Got stuck after finding out that google reCaptcha cannot work in shadow dom. Is there any other way out? When can we expect making custom components without having shadow dom?

cedeber commented 4 years ago

I don't think we need something very complex here. We just have to consider each component as a new Svelte app. I guess the store is available too, I didn't test so far.

This is the "connect" utility I use to render a Svelte component into a CustomElement (without shadowDom):

/**
 * Connect Web Component attributes to Svelte Component properties
 * @param {string} name Name of the Web Component
 * @param {*} Component Svelte Component
 * @param {string[]} attributes Which attributes will be passed as properties
 */
export default function connect(name, Component, attributes = []) {
    return customElements.define(name, class extends HTMLElement {
        constructor() {
            super();
            this.component = undefined;
        }

        static get observedAttributes() {
            return attributes;
        }

        attributeChangedCallback(name, oldValue, newValue) {
            if (this.component && oldValue !== newValue) {
                this.component.$set({ [name]: newValue });
            }
        }

        connectedCallback() {
            let props = {};

            for (const attr of attributes) {
                props[attr] = this.getAttribute(attr) || undefined;
            }

            this.component = new Component({
                target: this,
                props,
            });
        }
    });
}
crisward commented 4 years ago

I also cobbled together something similar in the meantime, though in addition I pass in an option of shadow true/false and if true also embed a link to the svelte's compiled stylesheet.

I think the svelte maintainers should decide if they really want to compile to web-components with all their complexity, or just promote using wrappers like these. What would help here, and with integration with other frameworks would be to make top level slots accessible outside of shadow dom. I realise they'd need to be "simulated", but grabbing the content of the mounted dom node and parsing it for named slots should be feasible, though admittedly not ideal.

I have two pull requests dealing with these two issues, was hoping for some feedback, even if just to say no thanks. The slots pull request is still a WIP.

rac0316 commented 4 years ago

Hey @cedeber, can you provide a small example on how to integrate your connector?

cedeber commented 4 years ago

Hi @rac0316 Here is my Svelte boileplate: https://github.com/cedeber/eukolia/tree/master/boilerplates/rollup-svelte I modified it to integrate the connector. In this commit, you can see the needed changes: https://github.com/cedeber/eukolia/commit/1e1742ddca7fac58f02760aa11b4b3c61a560283

AutoSponge commented 4 years ago

This may be a little "crude" but it seems to work for light dom slots:

customElements.define('element-details', 
  class extends HTMLElement {
    constructor() {
      super();
      const template = document.getElementById('element-details-template').content;
      // render into light dom
      this.appendChild(template.cloneNode(true));
      // grab all slots
      const slots = this.querySelectorAll('slot');
      slots.forEach(slot => {
        // replace slots with their element counterpart
        const el = this.querySelector(`[slot="${slot.name}"]`)
        if (!el) slot.parentNode.removeChild(slot)
        slot.parentNode.replaceChild(el, slot)
      });
    }
  }
);

Adapted from the demo at https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_templates_and_slots

jindrahm commented 4 years ago

I got this which also supports slots. The only restriction is that the main slot must be wrapped in a tag (eg. <custom-button><span>{label}</span></custom-button>) if something may change within the slot.. because it changes the context of the main slot elements which svelte uses when updating.

export default class KinElement extends HTMLElement {
    constructor() {
        super();

        this.elTpl; // elementTemplate
        this.initialized = false;

        this.initTimeoutId = null;
    }

    connectedCallback() {
        this.initTimeoutId = window.setTimeout(this.init.bind(this), 0);
    }

    disconnectedCallback() {
        this.initTimeoutId = window.clearTimeout(this.initTimeoutId);
    }

    attributeChangedCallback() {
        if (this.initialized) {
            // may be defined by a child class
            this.updateElem && this.updateElem();
        }
    }

    init() {
        if (this.initialized) {
            return;
        }
        // must be defined by a child class (e.g. Button) and create this.elTpl
        this.initElem();

        this.initSlots();
        this.appendChild(this.elTpl);

        this.initialized = true;
    }

    initSlots() {
        const mainSlot = this.elTpl.querySelector('slot:not([name])');
        const namedSlots = this.elTpl.querySelectorAll('slot[name]');
        const namedSlotsMap = {};
        const mainSlotNodes = [];
        const namedSlotNodes = [];

        namedSlots.forEach(slot => {
            namedSlotsMap[slot.name] = slot;
        });

        this.childNodes.forEach(child => {
            if (child.slot) {
                namedSlotNodes.push(child);
            } else {
                mainSlotNodes.push(child);
            }
        });

        if (mainSlot) {
            mainSlotNodes.forEach((node, index)=>{
                node = this.removeChild(node);

                if (index) {
                    mainSlotNodes[index - 1].after(node);
                } else {
                    mainSlot.replaceWith(node);
                }
            });
        }

        namedSlotNodes.forEach(node => {
            const slot = namedSlotsMap[node.slot];

            node = this.removeChild(node);

            if (slot) {
                slot.replaceWith(node);
            }
        });
    }
}
varun-etc commented 4 years ago

Wanted to make a small authentication component with the help of svelte. Got stuck after finding out that google reCaptcha cannot work in shadow dom. Is there any other way out? When can we expect making custom components without having shadow dom?

I am also stuck with same issue trying develop a custom element with google reCaptcha

Wanted to make a small authentication component with the help of svelte. Got stuck after finding out that google reCaptcha cannot work in shadow dom. Is there any other way out? When can we expect making custom components without having shadow dom?

Did you got any solution for this i am also stuck with this..

Manzurkds commented 4 years ago

@varun-etc I didn't find any way with having it as a custom component. But I solved my problem by not making it as a custom component, in that way there is no shadow dom and it's as good as any other app (I have to be carefull with my class names though, so as to not override styles and also not leak them to the host app).

terrywh commented 4 years ago

any news on this ?

varun-etc commented 4 years ago

Finally i was able to achieve this in svelte you can refer build file here..

milahu commented 4 years ago

build file here..

please share your source files

varun-etc commented 4 years ago

build file here..

please share your source files

source files github link

crisward commented 3 years ago

I created a wrapper to solve this issue a while ago. Been using it internally for a few months and just quickly uploaded something to npm / github.

It supports default and named slots, attributes, shadow dom, light dom, and embedding css.

Hopefully someone else finds it useful.

BenitoJedai commented 3 years ago

S

BerndWessels commented 3 years ago

Lots of accessibility tools like Jaws do have very very very poor support for shadow dom. Like a combobox with haspopup=listbox doesn't work at all in shadow dom with Jaws. @Rich-Harris to be able to use svelte to built UI component libraries we desperately need custom elements without shadow dom. This topic hasn't been addressed for a long time. It stopped quite a few enterprises I work for to adopt svelte because of WAI compliance. Please have another look into this. Thanks

jgile commented 3 years ago

would love to see this. are there are other technologies out there that do something similar?

AutoSponge commented 3 years ago

Another use case I just encountered: rendering a custom element SVG filter. That filter can't be addressed by the rest of the page while in shadow (even if it's open). It must be rendered into light dom.

ksgolding commented 2 years ago

Just getting started with Svelte and SvelteKit and really liking it so far! One of few attractions to Svelte was support for web components. In one of our use cases, all worked well, and the ShadowDom worked as expected. In another case.. well, not so much. We have now probably spent more time fighting this then it would have taken to just create a plan web component and just NOT added the shadow dom. My issue resembles others discussed in this thread.

Anyway, an idea occurred to me. If the is present, and there are no