From 5cfefeb6e72f8085e418150b644cdc4b4f6f260d Mon Sep 17 00:00:00 2001 From: Hofer Ivan Date: Wed, 21 Jul 2021 21:51:31 +0200 Subject: [PATCH] support rendering components in a shadow dom (#5870) --- site/content/docs/03-run-time.md | 2 +- src/compiler/compile/render_dom/index.ts | 22 +++++++----- src/runtime/internal/Component.ts | 8 +++-- src/runtime/internal/dev.ts | 2 +- src/runtime/internal/dom.ts | 36 +++++++++++++++++++ src/runtime/internal/style_manager.ts | 6 ++-- src/runtime/internal/utils.ts | 2 +- .../expected.js | 13 +++---- test/js/samples/css-media-query/expected.js | 14 +++----- .../css-shadow-dom-keyframes/expected.js | 5 +-- .../samples/compile-option-dev/test.js | 4 +-- 11 files changed, 77 insertions(+), 37 deletions(-) diff --git a/site/content/docs/03-run-time.md b/site/content/docs/03-run-time.md index d7bf3757df..13a3966c82 100644 --- a/site/content/docs/03-run-time.md +++ b/site/content/docs/03-run-time.md @@ -949,7 +949,7 @@ The following initialisation options can be provided: | option | default | description | | --- | --- | --- | -| `target` | **none** | An `HTMLElement` to render to. This option is required +| `target` | **none** | An `HTMLElement` or `ShadowRoot` to render to. This option is required | `anchor` | `null` | A child of `target` to render the component immediately before | `props` | `{}` | An object of properties to supply to the component | `context` | `new Map()` | A `Map` of root-level context key-value pairs to supply to the component diff --git a/src/compiler/compile/render_dom/index.ts b/src/compiler/compile/render_dom/index.ts index 7894c42115..b3ee1e91b1 100644 --- a/src/compiler/compile/render_dom/index.ts +++ b/src/compiler/compile/render_dom/index.ts @@ -50,11 +50,8 @@ export default function dom( if (should_add_css) { body.push(b` - function ${add_css}() { - var style = @element("style"); - style.id = "${component.stylesheet.id}-style"; - style.textContent = "${styles}"; - @append(@_document.head, style); + function ${add_css}(target) { + @append_styles(target, "${component.stylesheet.id}", "${styles}"); } `); } @@ -486,7 +483,7 @@ export default function dom( ${css.code && b`this.shadowRoot.innerHTML = \`\`;`} - @init(this, { target: this.shadowRoot, props: ${init_props}, customElement: true }, ${definition}, ${has_create_fragment ? 'create_fragment' : 'null'}, ${not_equal}, ${prop_indexes}, ${dirty}); + @init(this, { target: this.shadowRoot, props: ${init_props}, customElement: true }, ${definition}, ${has_create_fragment ? 'create_fragment' : 'null'}, ${not_equal}, ${prop_indexes}, null, ${dirty}); ${dev_props_check} @@ -533,12 +530,21 @@ export default function dom( name: options.dev ? '@SvelteComponentDev' : '@SvelteComponent' }; + const optional_parameters = []; + if (should_add_css) { + optional_parameters.push(add_css); + } else if (dirty) { + optional_parameters.push(x`null`); + } + if (dirty) { + optional_parameters.push(dirty); + } + const declaration = b` class ${name} extends ${superclass} { constructor(options) { super(${options.dev && 'options'}); - ${should_add_css && b`if (!@_document.getElementById("${component.stylesheet.id}-style")) ${add_css}();`} - @init(this, options, ${definition}, ${has_create_fragment ? 'create_fragment' : 'null'}, ${not_equal}, ${prop_indexes}, ${dirty}); + @init(this, options, ${definition}, ${has_create_fragment ? 'create_fragment' : 'null'}, ${not_equal}, ${prop_indexes}, ${optional_parameters}); ${options.dev && b`@dispatch_dev("SvelteRegisterComponent", { component: this, tagName: "${name.name}", options, id: create_fragment.name });`} ${dev_props_check} diff --git a/src/runtime/internal/Component.ts b/src/runtime/internal/Component.ts index 0d1d65cbdc..e706928af3 100644 --- a/src/runtime/internal/Component.ts +++ b/src/runtime/internal/Component.ts @@ -38,6 +38,7 @@ interface T$$ { on_destroy: any[]; skip_bound: boolean; on_disconnect: any[]; + root:Element|ShadowRoot } export function bind(component, name, callback) { @@ -103,7 +104,7 @@ function make_dirty(component, i) { component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31)); } -export function init(component, options, instance, create_fragment, not_equal, props, dirty = [-1]) { +export function init(component, options, instance, create_fragment, not_equal, props, append_styles, dirty = [-1]) { const parent_component = current_component; set_current_component(component); @@ -128,9 +129,12 @@ export function init(component, options, instance, create_fragment, not_equal, p // everything else callbacks: blank_object(), dirty, - skip_bound: false + skip_bound: false, + root: options.target || parent_component.$$.root }; + append_styles && append_styles($$.root); + let ready = false; $$.ctx = instance diff --git a/src/runtime/internal/dev.ts b/src/runtime/internal/dev.ts index 73fd32bf19..c20559f603 100644 --- a/src/runtime/internal/dev.ts +++ b/src/runtime/internal/dev.ts @@ -105,7 +105,7 @@ export interface SvelteComponentDev { [accessor: string]: any; } interface IComponentOptions = Record> { - target: Element; + target: Element|ShadowRoot; anchor?: Element; props?: Props; context?: Map; diff --git a/src/runtime/internal/dom.ts b/src/runtime/internal/dom.ts index c1909dafe2..7af2691785 100644 --- a/src/runtime/internal/dom.ts +++ b/src/runtime/internal/dom.ts @@ -125,6 +125,42 @@ function init_hydrate(target: NodeEx) { } } +export function append_styles( + target: Node, + style_sheet_id: string, + styles: string +) { + const append_styles_to = get_root_for_styles(target); + + if (!append_styles_to?.getElementById(style_sheet_id)) { + const style = element('style'); + style.id = style_sheet_id; + style.textContent = styles; + append_stylesheet(append_styles_to, style); + } +} + +export function get_root_for_node(node: Node) { + if (!node) return document; + + return (node.getRootNode ? node.getRootNode() : node.ownerDocument); // check for getRootNode because IE is still supported +} + +function get_root_for_styles(node: Node) { + const root = get_root_for_node(node); + return (root as ShadowRoot).host ? root as ShadowRoot : root as Document; +} + +export function append_empty_stylesheet(node: Node) { + const style_element = element('style') as HTMLStyleElement; + append_stylesheet(get_root_for_styles(node), style_element); + return style_element; +} + +function append_stylesheet(node: ShadowRoot | Document, style: HTMLStyleElement) { + append((node as Document).head || node, style); +} + export function append(target: NodeEx, node: NodeEx) { if (is_hydrating) { init_hydrate(target); diff --git a/src/runtime/internal/style_manager.ts b/src/runtime/internal/style_manager.ts index 8060e65a5d..a646c9b916 100644 --- a/src/runtime/internal/style_manager.ts +++ b/src/runtime/internal/style_manager.ts @@ -1,4 +1,4 @@ -import { element } from './dom'; +import { append_empty_stylesheet, get_root_for_node } from './dom'; import { raf } from './environment'; interface ExtendedDoc extends Document { @@ -29,9 +29,9 @@ export function create_rule(node: Element & ElementCSSInlineStyle, a: number, b: const rule = keyframes + `100% {${fn(b, 1 - b)}}\n}`; const name = `__svelte_${hash(rule)}_${uid}`; - const doc = node.ownerDocument as ExtendedDoc; + const doc = get_root_for_node(node) as unknown 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(node).sheet as CSSStyleSheet); const current_rules = doc.__svelte_rules || (doc.__svelte_rules = {}); if (!current_rules[name]) { diff --git a/src/runtime/internal/utils.ts b/src/runtime/internal/utils.ts index 9a37261723..f487732b77 100644 --- a/src/runtime/internal/utils.ts +++ b/src/runtime/internal/utils.ts @@ -89,7 +89,7 @@ export function create_slot(definition, ctx, $$scope, fn) { } } -export function get_slot_context(definition, ctx, $$scope, fn) { +function get_slot_context(definition, ctx, $$scope, fn) { return definition[1] && fn ? assign($$scope.ctx.slice(), definition[1](fn(ctx))) : $$scope.ctx; diff --git a/test/js/samples/collapses-text-around-comments/expected.js b/test/js/samples/collapses-text-around-comments/expected.js index bd1f3ce3d2..63a32bbf38 100644 --- a/test/js/samples/collapses-text-around-comments/expected.js +++ b/test/js/samples/collapses-text-around-comments/expected.js @@ -2,6 +2,7 @@ import { SvelteComponent, append, + append_styles, attr, detach, element, @@ -13,11 +14,8 @@ import { text } from "svelte/internal"; -function add_css() { - var style = element("style"); - style.id = "svelte-1a7i8ec-style"; - style.textContent = "p.svelte-1a7i8ec{color:red}"; - append(document.head, style); +function add_css(target) { + append_styles(target, "svelte-1a7i8ec", "p.svelte-1a7i8ec{color:red}"); } function create_fragment(ctx) { @@ -58,9 +56,8 @@ function instance($$self, $$props, $$invalidate) { class Component extends SvelteComponent { constructor(options) { super(); - if (!document.getElementById("svelte-1a7i8ec-style")) add_css(); - init(this, options, instance, create_fragment, safe_not_equal, { foo: 0 }); + init(this, options, instance, create_fragment, safe_not_equal, { foo: 0 }, add_css); } } -export default Component; \ No newline at end of file +export default Component; diff --git a/test/js/samples/css-media-query/expected.js b/test/js/samples/css-media-query/expected.js index f477670059..d743432206 100644 --- a/test/js/samples/css-media-query/expected.js +++ b/test/js/samples/css-media-query/expected.js @@ -1,7 +1,7 @@ /* generated by Svelte vX.Y.Z */ import { SvelteComponent, - append, + append_styles, attr, detach, element, @@ -11,11 +11,8 @@ import { safe_not_equal } from "svelte/internal"; -function add_css() { - var style = element("style"); - style.id = "svelte-1slhpfn-style"; - style.textContent = "@media(min-width: 1px){div.svelte-1slhpfn{color:red}}"; - append(document.head, style); +function add_css(target) { + append_styles(target, "svelte-1slhpfn", "@media(min-width: 1px){div.svelte-1slhpfn{color:red}}"); } function create_fragment(ctx) { @@ -41,9 +38,8 @@ function create_fragment(ctx) { class Component extends SvelteComponent { constructor(options) { super(); - if (!document.getElementById("svelte-1slhpfn-style")) add_css(); - init(this, options, null, create_fragment, safe_not_equal, {}); + init(this, options, null, create_fragment, safe_not_equal, {}, add_css); } } -export default Component; \ No newline at end of file +export default Component; diff --git a/test/js/samples/css-shadow-dom-keyframes/expected.js b/test/js/samples/css-shadow-dom-keyframes/expected.js index 4d188201eb..5d65949488 100644 --- a/test/js/samples/css-shadow-dom-keyframes/expected.js +++ b/test/js/samples/css-shadow-dom-keyframes/expected.js @@ -46,7 +46,8 @@ class Component extends SvelteElement { null, create_fragment, safe_not_equal, - {} + {}, + null ); if (options) { @@ -58,4 +59,4 @@ class Component extends SvelteElement { } customElements.define("custom-element", Component); -export default Component; \ No newline at end of file +export default Component; diff --git a/test/sourcemaps/samples/compile-option-dev/test.js b/test/sourcemaps/samples/compile-option-dev/test.js index bf240a5a89..f169ab8832 100644 --- a/test/sourcemaps/samples/compile-option-dev/test.js +++ b/test/sourcemaps/samples/compile-option-dev/test.js @@ -4,8 +4,8 @@ const b64dec = s => Buffer.from(s, 'base64').toString(); export async function test({ assert, css, js }) { - // We check that the css source map embedded in the js is accurate - const match = js.code.match(/\tstyle\.textContent = "(.*?)(?:\\n\/\*# sourceMappingURL=data:(.*?);charset=(.*?);base64,(.*?) \*\/)?";\n/); + // We check that the css source map embedded in the js is accurate + const match = js.code.match(/\tappend_styles\(target, "svelte-.{6}", "(.*?)(?:\\n\/\*# sourceMappingURL=data:(.*?);charset=(.*?);base64,(.*?) \*\/)?"\);\n/); assert.notEqual(match, null); const [mimeType, encoding, cssMapBase64] = match.slice(2);