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

Support custom element with nested components without errors #3594

Closed
marlonmleite opened this issue Sep 19, 2019 · 22 comments · Fixed by #8457
Closed

Support custom element with nested components without errors #3594

marlonmleite opened this issue Sep 19, 2019 · 22 comments · Fixed by #8457
Labels
custom element popular more than 20 upthumbs

Comments

@marlonmleite
Copy link

I have only 1 customElement and inside it there are several internal components of the Svelte. These internal/nested components cannot be 1 web component.

By setting the customElement option in the rollup, he forced me to put <svelte:options tag={null} /> on all internal components, but that seems to be wrong, as they should not be 1 customElement.

After setting up, I'm getting the following errors for internal components marked as tag={null}:

image

@pbastowski
Copy link

Same problem here. It seems that tag={null} does not work as documented in Svelte components imported into a parent component that has a tag="parent-component".

The below does not work

ParentComponent.svelte

<svelte:options tag="parent-component" />
<h1>Parent Component</h1>
<ImportedComponent />

ImportedComponent.svelte

<svelte:options tag={null} />
<h1>Imported Component</h1>
<slot />

However, if I give the imported component a tag name tag="imported-component" and then use the new custom element inside the parent component <imported-component></imported-component> then everything works.

This works, but it's not what I want

ParentComponent.svelte

<svelte:options tag="parent-component" />
<h1>Parent</h1>
<imported-component></imported-component>

ImportedComponent.svelte

<svelte:options tag="imported-component" />
<h1>Important Component</h1>
<slot />

But, I don't want to generate all my components as custom elements. I just want the top level element to be generated as a custom element and the rest of my Svelte components to be used within the app as normal Svelte components (not custom elements).

@TehShrike
Copy link
Member

TehShrike commented Jan 16, 2020

I have run into this as well. I've taken to prefixing the child components tag names with the parent name, just to reduce the chances of causing a collision.

<svelte:options tag="parent-component-imported-component" />

@morewry
Copy link

morewry commented Feb 6, 2020

I want it without having to go through the custom element interface to interact with the imported component. So that I can define one web component built with five Svelte components. The way it works currently is all or nothing if the compiler option for customElements is on. They all six have to be web components defined in global scope. (#3520)

@jawish
Copy link

jawish commented May 10, 2020

I solved this by using a combination of Rollup and file extensions.

Custom Element components get the extension .wc.svelte. Rollup then sets the customElement config depending on the file extension.

svelte({ customElement: true, include: /\.wc\.svelte$/ }),
svelte({ customElement: false, exclude: /\.wc\.svelte$/ }),

This works smoothly with local and NPM imported components.

Demo: https://jawish.github.io/svelte-customelement-rollup/

@jamesopstad
Copy link

Just found this issue after struggling with the same problem. I don't think that the current all or nothing approach to compiling custom elements is particularly useful. I would find it much more useful if the choice could be made on a per-component basis based on the presence of <svelte:options tag="element-name"/>. Nested components should be normal Svelte components unless they specify otherwise. With this approach, it doesn't seem that the compiler option would be necessary either.

@jamesopstad
Copy link

Thinking about this a bit more, I imagine the main obstacle is how to deal with css. How difficult would it be for a custom element to inline the styles of all child elements until it hits another custom element? For example:

<custom-element-a> // inlines styles from custom-element-a and SvelteComponentA
 <SvelteComponentA>
  <custom-element-b> // inlines styles from custom-element-b and SvelteComponentB
   <SvelteComponentB/>
  </custom-element-b>
 </SvelteComponentA>
</custom-element-a>

There are quite a few open issues related to this e.g. #4274, #3520, #2605.

@morewry
Copy link

morewry commented May 13, 2020

Inlined or embedded?

"inlined"

<div style="padding: 10px;"></div>

"embedded"

<style>
  div {
    padding: 10px;
  }
</style>

"(externally) linked"

<link rel="stylesheet" href="styles.css" />

"(externally css) imported"

@import "styles.css";

@jamesopstad
Copy link

jamesopstad commented May 13, 2020

Embedded. I meant inlined in the sense that the styles should be included as a string within the custom element, to be added to the shadow dom. This way the user of the custom element only needs to import a single file.

@MonkeyAndres
Copy link

I just created a recipe for using Svelte and web components interchangeably in a codebase. And also comes with Storybook and i18n configured.
More info: https://github.com/redradix/svelte-custom-element-template

@jamesopstad
Copy link

This looks great. Thanks for sharing. I wonder if this functionality could eventually be included as a web component template in the newly announced @sveltejs/kit. What I'd love to see is a Snowpack powered development environment for creating web components that encapsulate Svelte component trees and embed their css (as you have done here).

@akauppi
Copy link

akauppi commented Jan 18, 2021

While this was filed as a separate issue (merging of tag={null} and that actually not working, even for a single subcomponent), I feel the comments have made it a duplicate of #4228

For future comments, it may be best to keep them in one issue.

@replace5
Copy link

I solved this by using a combination of Rollup and file extensions.

Custom Element components get the extension .wc.svelte. Rollup then sets the customElement config depending on the file extension.

svelte({ customElement: true, include: /\.wc\.svelte$/ }),
svelte({ customElement: false, exclude: /\.wc\.svelte$/ }),

This works smoothly with local and NPM imported components.

Demo: https://jawish.github.io/svelte-customelement-rollup/

style will lose, fix it with:

// hack: custom-elment with sub comonents will lose style, manual set it
let elm = document.quqerySlector("xx-element")
function setShadowStyle(host, styleContent) {
	var style = document.createElement( 'style' )
	style.innerHTML = styleContent;
	host.shadowRoot.appendChild( style );
}
setShadowStyle(elm, '@import "./build/component.css"');

@pbastowski
Copy link

pbastowski commented Apr 15, 2021

I solved this by using a combination of Rollup and file extensions.

Custom Element components get the extension .wc.svelte. Rollup then sets the customElement config depending on the file extension.

svelte({ customElement: true, include: /\.wc\.svelte$/ }),
svelte({ customElement: false, exclude: /\.wc\.svelte$/ }),

This works smoothly with local and NPM imported components.

Demo: https://jawish.github.io/svelte-customelement-rollup/

If you're using Svelte with Vite the lines above don't work, but those below do. It appears that customElement must be nested within compilerOptions.

plugins: [
    ...,
    svelte({ compilerOptions: { customElement: true, }, include: /\.wc\.svelte$/, }),
    svelte({ compilerOptions: { customElement: false, }, exclude: /\.wc\.svelte$/, }),
]

@dohoangnam1103
Copy link

dohoangnam1103 commented Jun 22, 2021

svelte({
	compilerOptions: {
		dev: !production,
		customElement: true,
		tag: null,
	},
	include: 'src/custom-component/**/*.svelte',
	preprocess: preprocess()
}),
svelte({
	compilerOptions: {
		dev: !production,
		customElement: false,
	},
	include: 'src/component/**/*.svelte',
	preprocess: preprocess()
}),

I tried putting the config as above to use the normal component inside the custom element component.
Everything seems fine.
However, the css style inside the normal component is not compiled. :(

@remjx
Copy link

remjx commented Jun 22, 2021

@dohoangnam1103 what do you mean it is not compiled? My CSS is working with that solution.

I'm importing CSS like this:

onMount(() => {
   const id = 'my-app-css'
    const style = `<style data-description="${id}">
	@import url('${cssUrl}'); // absolute URL to .css file generated by svelte css output plugin
	</style>`;
	if (document.querySelector(`style[data-description="${id}"]`)) {
		return; // it already exists
	}
	document.head.insertAdjacentHTML('beforeend', style);
}

I'm not setting tag: null

My preprocess uses svelte-preprocess:

sveltePreprocess({
	babel: true,
	postcss: {
		plugins: [require('autoprefixer')],
	},
})

I don't know if that will help in any way, I'm just sharing what's working for me.

@pngwn pngwn added the popular more than 20 upthumbs label Jun 26, 2021
@code3z
Copy link

code3z commented Feb 7, 2022

um please fix this

I love svelte, but I spent like an hour figuring out that it doesn't work like in the docs

at least fix the docs pls

@enyo
Copy link

enyo commented Feb 11, 2022

Maybe this can be of help to someone looking into this issue. I wrote an article on how to export individual svelte components as Web Components while still using Svelte as usual. I hope it can be useful https://www.colorglare.com/svelte-components-as-web-components-b400d1253504

@SonyStone
Copy link

Okay, now someone explain to me how to do the same thing, but now with tailwind css?

@Lachee
Copy link

Lachee commented Jan 6, 2023

Trying to use nested components and finding their CSS just gets ignored when including via the component instead of the custom tag.

Would be awesome if svelte could look ahead and just bundle them all as just one component for me

@lingster
Copy link

Okay, now someone explain to me how to do the same thing, but now with tailwind css?

I think using twind, as described here: https://gourav.io/blog/tailwind-in-shadow-dom works for me...

This discussion was also useful:

tailwindlabs/tailwindcss#1935

@brucejo75
Copy link

Regards to #3594 (comment)

svelte({ customElement: true, include: /\.wc\.svelte$/ }),
svelte({ customElement: false, exclude: /\.wc\.svelte$/ }),

Alternate naming from here: #4228 (comment)

svelte({  // web components
 include: /\/[a-z][^/]+\.svelte$/,
 compilerOptions: {
   customElement: true 
 }
}),
svelte({  // normal Svelte classes
 include: /\/[A-Z][^/]+\.svelte$/,
 compilerOptions: {
   customElement: false
 }
}),

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, to be released in Svelte 4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
custom element popular more than 20 upthumbs
Projects
None yet
Development

Successfully merging a pull request may close this issue.