Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom element without shadow DOM #1748

Closed
nxtwrld opened this issue Sep 18, 2018 · 39 comments
Closed

Custom element without shadow DOM #1748

nxtwrld opened this issue Sep 18, 2018 · 39 comments

Comments

@nxtwrld
Copy link

nxtwrld commented Sep 18, 2018

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
Copy link

bestguy commented Nov 18, 2018

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
Copy link
Author

nxtwrld commented Nov 27, 2018

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
Copy link

vigie commented Mar 14, 2019

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
Copy link

vigie commented Mar 18, 2019

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
Copy link

morewry commented Apr 22, 2019

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:

  • Vanilla form participation
  • Effective integration of form elements with existing frameworks and libraries (AngularJS, Formik, Redux Forms, etc.)
  • ARIA structures that span light and shadow DOMs
  • Usage of existing CSS frameworks that rely on shipping single stylesheets of global styles
  • A fatal incompatibility between the "official" Shadow DOM polyfill and AngularJS

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
Copy link

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
Copy link

Is there any progress?

@marcus-sa
Copy link

@pedroalexisruiz
Copy link

Are you still working on this?

@fsodano
Copy link

fsodano commented Dec 12, 2019

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

@fvybiral
Copy link

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
Copy link

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
Copy link

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?

@crisward crisward mentioned this issue Feb 2, 2020
4 tasks
@antony antony added the has pr label Feb 7, 2020
@cedeber
Copy link

cedeber commented Feb 9, 2020

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
Copy link

crisward commented Mar 4, 2020

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
Copy link

rac0316 commented Mar 21, 2020

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

@cedeber
Copy link

cedeber commented Mar 21, 2020

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: cedeber/frontafino@1e1742d

@AutoSponge
Copy link

AutoSponge commented Apr 7, 2020

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
Copy link

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
Copy link

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
Copy link

@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

This comment has been minimized.

@varun-etc
Copy link

varun-etc commented Aug 7, 2020

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

@milahu
Copy link
Contributor

milahu commented Aug 26, 2020

build file here..

please share your source files

@varun-etc
Copy link

build file here..

please share your source files

source files github link

@crisward
Copy link

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
Copy link

S

@BerndWessels
Copy link

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
Copy link

jgile commented Jun 2, 2021

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

@AutoSponge
Copy link

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.

@pngwn pngwn added popular more than 20 upthumbs and removed triage: has pr labels Jun 26, 2021
@ksgolding
Copy link

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 <svelte:options tag="my-custom-thing" /> is present, and there are no <style> tags, default to not adding a shadowdom. Seems like this might help preserve "intent", as I can see further confusion when both are present .. (though, traditional css rules have worked pretty well so far..)

Not sure if this is possible, just a thought.

I am another that would like support for this..

@Youhan
Copy link

Youhan commented Dec 13, 2021

I use TailwindCSS and it would be great to have Svelte-driven custom elements that can be styled but with a global CSS.

@honzahana
Copy link

honzahana commented Feb 1, 2022

Could you please help me? I would like to use Svelte Custom Elements together with TailwinCSS, but there is a problem with Shadow Dom. Thank you

@plesiecki
Copy link

like @Youhan said, yes, please 🙏

@TheCymaera
Copy link
Contributor

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?

Would it be better to set this inside <svelte:options> instead of compiler options, so we can configure it on a per-component basis?

@johndeighan
Copy link

I would like a clarification on this feature. Shadow DOM is usually presented as a way to prevent CSS leaking into a component, and to prevent CSS leaking out of a component. Along with a lot of other people, I would rather that CSS DOES leak into a component (or at least have that option). I'm assuming that when using Svelte, there is NEVER a problem with CSS leaking OUT of a component since Svelte adds mangled class names. Is that correct?

P.S. This thread started in 2018. I'm very surprised that it doesn't seem to be resolved yet. But... I don't know anything about Svelte internals - maybe it's just such a difficult thing to implement???

@baseballyama baseballyama added this to the 4.x milestone Feb 26, 2023
@patricknelson
Copy link

patricknelson commented May 1, 2023

For those watching this issue but not already aware, it appears this is slated for implementation in Svelte v4 via #8457! 🎉 It appears that migration from @crisward's svelte-tag library above to Svelte v4's custom elements implementation should be fairly minimal, so in the meantime, I'll be using that.

For migration from svelte-tag to Svelte v4, instead of this in your entrypoint:

import SvelteTag from 'svelte-tag'
import MyComponent from './MyComponent.svelte'
new SvelteTag({ component: MyComponent, tagname: "my-component" ) // etc...

You would still keep the import MyComponent ... in your entrypoint and just would need to add export ... for each importend component to ensure the compiler/bundler includes it (and doesn't "tree shake" it out), e.g.

import MyComponent from './MyComponent.svelte'
export { MyComponent }

And then, finally add the standard <svelte:options tag="my-counter" shadowdom="open" /> option to the top of the applicable .svelte component files, making sure to include the new shadowdom="open" attribute. Also, of course there are other methods if importing Svelte custom elements into your project (e.g. directly referencing **.svelte components in build.rollopOptions in the Vite config currently seems to work fine already in v3, as long as they're all custom elements). But again this is just a quick/simple migration from svelte-tag to v4 via an entrypoint.

Edit: Disclaimer: I'm new to Svelte myself, so there's a good chance I'm wrong on some specifics with migration and compatibility, so please refer to #8457 for compatibility and breaking changes! 😅

dummdidumm added a commit that referenced this issue May 2, 2023
This is an overhaul of custom elements in Svelte. Instead of compiling to a custom element class, the Svelte component class is mostly preserved as-is. Instead a wrapper is introduced which wraps a Svelte component constructor and returns a HTML element constructor. This has a couple of advantages:

- component can be used both as a custom element as well as a regular component. This allows creating one wrapper custom element and using regular Svelte components inside. Fixes #3594, fixes #3128, fixes #4274, fixes #5486, fixes #3422, fixes #2969, helps with sveltejs/kit#4502
- all components are compiled with injected styles (inlined through Javascript), fixes #4274
- the wrapper instantiates the component in `connectedCallback` and disconnects it in `disconnectedCallback` (but only after one tick, because this could be a element move). Mount/destroy works as expected inside, fixes #5989, fixes #8191
- the wrapper forwards `addEventListener` calls to `component.$on`, which allows to listen to custom events, fixes #3119, closes #4142 
- some things are hard to auto-configure, like attribute hyphen preferences or whether or not setting a property should reflect back to the attribute. This is why `<svelte:options customElement={..}>` can also take an object to modify such aspects. This option allows to specify whether setting a prop should be reflected back to the attribute (default `false`), what to use when converting the property to the attribute value and vice versa (through `type`, default `String`, or when `export let prop = false` then `Boolean`), and what the corresponding attribute for the property is (`attribute`, default lowercased prop name). These options are heavily inspired by lit: https://lit.dev/docs/components/properties. Closes #7638, fixes #5705
- adds a `shadowdom` option to control whether or not encapsulate the custom element. Closes #4330, closes #1748 

Breaking changes:
- Wrapped Svelte component now stays as a regular Svelte component (invokeing it like before with `new Component({ target: ..})` won't create a custom element). Its custom element constructor is now a static property named `element` on the class (`Component.element`) and should be regularly invoked through setting it in the html.
- The timing of mount/destroy/update is different. Mount/destroy/updating a prop all happen after a tick, so `shadowRoot.innerHTML` won't immediately reflect the change (Lit does this too). If you rely on it, you need to await a promise
@dummdidumm
Copy link
Member

Closed via #8457 via the new shadow option, to be released in Svelte 4

@patricknelson
Copy link

patricknelson commented Jun 10, 2023

FWIW, I forked svelte-tag to create svelte-retag in order to implement some of what @dummdidumm implemented above in Svelte 3 (and more), including:

But importantly, in contrast to both Svelte 3 & 4, it also has:

  • Vite HMR compatibility (no “already defined” errors when saving)
  • What I’m calling IIFE support, i.e.: Early execution without deferred modules to reduce or even eliminate CLS (Cumulative Layout Shift)
  • Nesting (e.g. within default slot)
  • Context support

It’s hard to underscore how important that last point is when in the context of custom elements. This can be a pain when combining Svelte, custom elements and slots, particularly since we cannot know what slots are available to a custom element until we’ve waited for DOMContentLoaded (which can feel like ages, especially for large or complex document). With this you can both make your Svelte components + custom elements available ahead of time and properly handle slots (with nesting) immediately. This proactively renders your component and then rerenders it during parsing to account for newly parsed content.

This should hopefully not only reduce CLS but also TTI (time to interactive) without the “jank” usually seen when loading pages that compartmentalize component frameworks into custom elements. 😊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.