-
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
make it possible to render svelte-components inside a shadow dom #5870
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
Conversation
Thanks for this. I'll hopefully get a chance to review it soon. I do agree about stylesTarget though, the name seems a bit clunky. Perhaps we need to think on that a bit. |
This would be awesome if it gets merged. My use case is similar - I am building a web extension that injects stuff into other webpages and I need to prevent the page styles leaking into my app (tried using an iframe, but that's a pain). As for |
This is just perfect @ivanhofer! I'm working on multiple projects where the addition of Thus I hope this PR would be merged! Is there anything I could do to help move this forward? |
Hi @tepose, I think we could raise some more attention to this PR by linking other related issues. I saw a few open issues already, but haven't got the time to link all of them. Maybe you could help me with that? |
I would like to apologize for the inconvenience, we are working on these
issues.
…On Wed, Feb 24, 2021 at 8:15 AM lseguin1337 ***@***.***> wrote:
***@***.**** commented on this pull request.
------------------------------
In src/runtime/internal/style_manager.ts
<#5870 (comment)>:
> @@ -31,7 +31,7 @@ export function create_rule(node: Element & ElementCSSInlineStyle, a: number, b:
const name = `__svelte_${hash(rule)}_${uid}`;
const doc = node.ownerDocument as ExtendedDoc;
active_docs.add(doc);
- const stylesheet = doc.__svelte_stylesheet || (doc.__svelte_stylesheet = doc.head.appendChild(element('style') as HTMLStyleElement).sheet as CSSStyleSheet);
+ const stylesheet = doc.__svelte_stylesheet || (doc.__svelte_stylesheet = append_empty_stylesheet().sheet as CSSStyleSheet);
https://github.com/sveltejs/svelte/pull/4998/files
—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
<#5870 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/ARJ6A4KVS3XKCMPNPJZQPDTTAUQZHANCNFSM4V2RK36A>
.
|
@ivanhofer, I tried installing your branch as an npm module, but running it using Snowpack I get this error:
Not sure if it really is an issue though. Installing Also, after installing
Thank you for the effort put into this! |
hi @tepose, |
@ivanhofer, I regret mentioning it without first checking with the master branch. In fact, it is the same when installing from sveltejs' master also. So ignore me on this matter! |
You could open a new issue, if this is a general svelte-problem :) |
Adding a new parameter const root = (target.getRootNode ? target.getRootNode() : target.ownerDocument) // Check for getRootNode because IE is still supported
const style_append_target = (root.host ? root : root.head) |
This PR will also resolve this ticket: #1825 |
I now have removed the @lseguin1337 I now pass the |
Shouldn't <svelte:head>
<link rel="stylesheet" href="tutorial/dark-theme.css">
</svelte:head> Would not result in the expected behavior. (Or at least you'd have to be aware, whether this Svelte component will be rendered in a shadow root or not, which might not be the design idea behind the PR) |
At least in my code, For the multiple uses of Things like |
@hgiesel I agree with @ADantes. It should be the component's author responsibility to make sure the component works inside the shadow dom. If there is a need for that component to be inside a shadow dom, the author will take it into account and don't use I created this PR because I needed a solution to render my svelte-application on any possible website without fearing my styles would be overwritten by the website's global styles. If I would have full control over the host-websites, I don't think I would need the shadow dom. |
this pr looks amazing: it answers to a very basic need for custom elements to work with nested components @antony it's been around for 4 months: is there any reason why it hasn't been merged ? cheers |
Very likely because none of us actually use custom elements, and as such we haven't had a chance to look at it. Have you tried this fork? does it work? It would be good to hear from some people who are using this, so that we know all bases are covered. |
I use it myself in production since ~3 months. |
I'm happy to say that as of today, it's also used in production by Amedia's editorial developer team. A huge thank you @ivanhofer! |
@antony I am building my application with this fork since a few months. How can we get this PR merged? Is something still missing? If you have an own application, you could test it in a few minutes: file: import App from './App.svelte';
+ const target = document.body;
+ const root = target.attachShadow({ mode: 'open' });
+
const app = new App({
- target: document.body,
+ target: root,
props: {
name: 'world'
}
});
export default app; configure your import svelte from 'rollup-plugin-svelte'
export default {
plugins: [
svelte({
emitCss: false,
}),
]
} |
Context: app target is the shadow dom, which is hosted by a document>body>div. The following correction makes it work for me for @@ -31,7 +31,7 @@ export function create_rule(node: Element & ElementCSSInlineStyle, a: number, b:
const name = `__svelte_${hash(rule)}_${uid}`;
- const doc = node.ownerDocument as ExtendedDoc;
+ const doc = (node.getRootNode && (node.getRootNode() as ShadowRoot).host ? node.getRootNode() : node.ownerDocument) as ExtendedDoc; What do you think ? You can test is on my fork pfz/svelte. |
any ETA when this can get merged? |
@ivanhofer @Conduitry @jacksteamdev hi, sorry, I can't get this to work at all and there seems to be no documentation. I have my own custom element that renders a svelte component into a shadow root - but the styles are still attached to the document head :( this.#svelteComponent = new opts.component({ target: this.#shadow, props: svelteProperties }) Any ideas why your PR here doesn't work for me? /**
* Add a prefix to a camel-case property key.
*/
const addPrefix = (key, prefix) => {
return key.replace(/(^[a-z])/gi, function (g) { return `${prefix}${g.toUpperCase()}` })
}
/**
* Remove a prefix from a camel-case property key.
*/
const removePrefix = (key, prefix) => {
return key.replace(new RegExp(`^${prefix}`, 'gi'), '').replace(/(^[A-Z])/g, function (g) { return g.toLowerCase() });
}
/**
* Convert from kebab-case to camel-case.
*/
const kebabToCamelCase = (key) => {
return key.replace(/-([a-z])/g, function (g) {
return g[1].toUpperCase()
})
}
/**
* Convert camel-case to kebab-case.
*/
const camelToKebabCase = (key) => {
return key.replace(/([a-z][A-Z])/g, function (g) {
return g[0] + '-' + g[1].toLowerCase()
})
}
/**
* Create a custom element wrapper for a given svelte component.
*/
export default function (opts) {
/**
* The custom element wrapper.
*/
class Wrapper extends HTMLElement {
/**
* Internal properties,
* prefixed to prevent collision with HTMLElement properties.
*/
#properties = {}
#svelteComponent = null
#shadow = null
/**
* Initialize the custom element wrapper.
*/
constructor() {
// Always call super first in constructor.
super()
// Attach the shadow dom.
this.#shadow = this.attachShadow({ mode: 'open' });
// Subscribe to host properties.
this.#subscribeToProperties('host', opts.properties);
// Prefix custom properties.
const prefixedCustomProperties = Object
.entries(opts.customProperties || {})
.reduce((a, [key, value]) => (
{ ...a, [addPrefix(key, 'ds')]: value }
), {})
// Subscribe to custom properties.
this.#subscribeToProperties('custom', prefixedCustomProperties);
}
/**
* Subscribe to the custom element attributes.
* Attributes will override host properties.
*/
static get observedAttributes() {
return (
opts.attributes.map((attr) =>
camelToKebabCase(attr)
) || []
)
}
/**
* Forward custom element attribute value changes to the svelte element.
* Kebab-case attributes are mapped to standard properties in camel-case and prefixed with 'host'.
* `aria-label` attribute becomes `hostAriaLabel` svelte property.
*/
attributeChangedCallback(name, oldValue, newValue) {
if (this.#svelteComponent && newValue != oldValue) {
this.#svelteComponent.$set({
[addPrefix(kebabToCamelCase(name), 'host')]: newValue,
})
}
}
/**
* Forward custom element property value changes to the svelte element.
* Custom properties prefixed with `ds` will become camel-case svelte properties without the `ds` prefix.
* `dsAriaLabel` custom property becomes `ariaLabel` svelte property.
* Standard properties will become camel-case svelte properties prefixed with `host`.
* `ariaLabel` standard property becomes `hostAriaLabel` svelte property.
* TODO implement
*/
#propertyChangedCallback(type, name, oldValue, newValue) {
// Remember the new property value.
this.#properties[name] = newValue
// Make sure the svelte component exists and the value has actually changed.
// TODO this could be improved by a deep compare of old and new value.
if (this.#svelteComponent && newValue != oldValue) {
// Map to the correct svelte property.
const sveltePropertyName = type === 'host'
// Host properties are prefixed with `host` within the svelte component.
? addPrefix(name, "host")
// Custom properties are not prefixed in the svelte component.
: removePrefix(name, "ds");
// Set the new property value on the svelte component.
this.#svelteComponent.$set({ [sveltePropertyName]: newValue })
}
}
/**
* Subscribe to properties.
*/
#subscribeToProperties(type, properties) {
// Iterate through all properties.
const propertyMap = Object.entries(properties).reduce(
// Create a getter and setter for each possible property.
(a, [key, value]) => ({
...a,
[`${key}`]: {
configurable: true,
enumerable: true,
get: () => {
const currentValue = Object.prototype.hasOwnProperty.call(
this.#properties,
key
)
? this.#properties[key]
: value
return currentValue
},
set: (newValue) => {
const oldValue = Object.prototype.hasOwnProperty.call(
this.#properties,
key
)
? this.#properties[key]
: value
this.#propertyChangedCallback(type, key, oldValue, newValue)
},
},
}),
{}
)
// Apply the getters and setters to the custom element.
Object.defineProperties(this, propertyMap)
}
/**
* Create the svelte element when the custom element connects.
*/
connectedCallback() {
// Map the default properties and custom properties to the correct svelte properties.
const svelteProperties =
Object
// Host properties are prefixed with `host` within the svelte component.
.entries(opts.properties)
// Custom properties are not prefixed in the svelte component.
.reduce((a, [key, value]) => ({ ...a, [addPrefix(key, 'host')]: value }), opts.customProperties || {});
// Map the current attributes for the custom element to the host properties.
Array.from(this.attributes).forEach(
(attr) =>
(svelteProperties[addPrefix(kebabToCamelCase(attr.name), 'host')] = attr.value)
)
// Svelte stuff.
svelteProperties.$$scope = {}
// Add the custom element as host element to the properties.
svelteProperties.hostElement = this
// Create and render the svelte element.
this.#svelteComponent = new opts.component({ target: this.#shadow, props: svelteProperties })
}
/**
* Clean up when the custom element disconnects.
*/
disconnectedCallback() {
// Destroy the svelte element when removed from dom.
try {
this.#svelteComponent.$destroy()
} catch (err) { }
}
}
/**
* Register the custom element.
*/
if (!window.customElements.get(opts.tagname)) {
window.customElements.define(opts.tagname, Wrapper)
} else {
console.log(`Trying to define already defined ${opts.tagname}`)
}
} |
Actually this works (styles are rendered in the shadow root), so there must be something wrong with my original implementation I posted in the previous comment. So nothing to worry about I guess ;) import MyOtherComponent from './my-other-component.svelte';
class CustomElement extends HTMLElement {
#shadow = null
constructor() {
super()
this.#shadow = this.attachShadow({ mode: 'open' });
}
connectedCallback() {
new MyOtherComponent({
target: this.#shadow,
props: {
myTitle: 'Shadow DOM',
}
});
}
}
if (!window.customElements.get('my-other-component')) {
window.customElements.define('my-other-component', CustomElement)
} else {
console.log(`Trying to define already defined 'my-other-component'`)
} |
@BerndWessels have you set the compiler option |
@ivanhofer not exactly sure what was wrong, but this |
BTW we are building independent and individual components as custom elements rendering svelte inside the shadow dom. What I noticed is that you use I wonder if you could change from Please consider it if possible, thanks. |
@BerndWessels setting the If you render the component multiple times, the styles should get only injected once into the dom (once per shadow root if you have multiple roots). If you want to change this behavior, you should create a new issue. |
@ivanhofer thanks, I created an issue that explains it in much more detail - please have a look at it here #6719 |
Great work @ivanhofer. Is |
@jakobrosenberg if you use |
@ivanhofer thanks. Any idea how I would manually inject the stylesheet into the shadow dom? |
@jakobrosenberg just like you would with a normal DOM: // entry point of your Svelte application
import App from './App.svelte'
const target = document.body
const root = target.attachShadow({ mode: 'open' })
const app = new App({
target: root,
props: {
name: 'world'
}
})
// inject styles
const linkElement = document.createElement('link')
linkElement.setAttribute('rel', 'stylesheet')
linkElement.setAttribute('href', 'build/bundle.css')
root.appendChild(linkElement)
export default app The Hope this helps :) |
There is an important catch. For the time of writing |
I'm having a problem using Tailwind CSS inside a shadow DOM in Svelte. If I <style lang="postcss">
@tailwind base;
@tailwind components;
@tailwind utilities;
</style> The Any hints please? Thanks. |
I think that tailwind declarations must be "global". For example, you should put <style global lang="postcss"> in your layout-like or root component. Then your component injected in a shadow DOM would inject compiled style in the right place for the whole component structure (app?) |
Hi @kizivat, please ask help and support questions on the svelte discord - https://discord.gg/svelte rather than attaching them to merged and closed pull requests. |
css: true
This PR slightly increases the output size for a single component, but the size gets smaller and smaller the more components are used.
Will not effect generated output when using the compiler option
css: false
.With this PR it is possible to use Svelte inside a shadow dom.
Usage
With a few changes to
main.js
it is now possible to render a Svelte-component/-application inside a shadow root, so you can take advantage of style encapsulation.I currently build my application with svelte from this branch to take advantage of the style encapsulation from the shadow dom.
Notes
append_styles
function makes it possible to save a few bytes.