svelte icon indicating copy to clipboard operation
svelte copied to clipboard

Custom element without shadow DOM

Open nxtwrld opened this issue 7 years ago • 34 comments

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?

nxtwrld avatar Sep 18 '18 10:09 nxtwrld

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.

bestguy avatar Nov 18 '18 23:11 bestguy

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.

nxtwrld avatar Nov 27 '18 12:11 nxtwrld

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 avatar Mar 14 '19 21:03 vigie

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?

vigie avatar Mar 18 '19 21:03 vigie

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.

morewry avatar Apr 22 '19 20:04 morewry

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.

zephraph avatar Apr 23 '19 21:04 zephraph

Is there any progress?

timonweb avatar Oct 13 '19 16:10 timonweb

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

marcus-sa avatar Oct 13 '19 16:10 marcus-sa

Are you still working on this?

pedroalexisruiz avatar Dec 11 '19 20:12 pedroalexisruiz

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?

fsodano avatar Dec 12 '19 06:12 fsodano

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...

fvybiral avatar Dec 18 '19 14:12 fvybiral

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.

paladinu avatar Jan 10 '20 20:01 paladinu

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?

Manzurkds avatar Jan 31 '20 11:01 Manzurkds

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,
            });
        }
    });
}

cedeber avatar Feb 09 '20 19:02 cedeber

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.

crisward avatar Mar 04 '20 08:03 crisward

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

rac0316 avatar Mar 21 '20 11:03 rac0316

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

cedeber avatar Mar 21 '20 14:03 cedeber

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

AutoSponge avatar Apr 07 '20 14:04 AutoSponge

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);
			}
		});
	}
}

jindrahm avatar Apr 21 '20 17:04 jindrahm

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..

varun-etc avatar Jun 04 '20 07:06 varun-etc

@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).

Manzurkds avatar Jun 04 '20 11:06 Manzurkds

any news on this ?

terrywh avatar Aug 07 '20 10:08 terrywh

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

varun-etc avatar Aug 07 '20 11:08 varun-etc

build file here..

please share your source files

milahu avatar Aug 26 '20 06:08 milahu

build file here..

please share your source files

source files github link

varun-etc avatar Aug 27 '20 05:08 varun-etc

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.

  • https://www.npmjs.com/package/svelte-tag
  • https://github.com/crisward/svelte-tag

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

Hopefully someone else finds it useful.

crisward avatar Nov 29 '20 00:11 crisward

S

BenitoJedai avatar Dec 05 '20 16:12 BenitoJedai

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

BerndWessels avatar Mar 26 '21 22:03 BerndWessels

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

jgile avatar Jun 02 '21 14:06 jgile

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.

AutoSponge avatar Jun 03 '21 12:06 AutoSponge