Skip to content

Commit 5cfefeb

Browse files
authored
support rendering components in a shadow dom (#5870)
1 parent a644818 commit 5cfefeb

File tree

11 files changed

+77
-37
lines changed

11 files changed

+77
-37
lines changed

site/content/docs/03-run-time.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -949,7 +949,7 @@ The following initialisation options can be provided:
949949

950950
| option | default | description |
951951
| --- | --- | --- |
952-
| `target` | **none** | An `HTMLElement` to render to. This option is required
952+
| `target` | **none** | An `HTMLElement` or `ShadowRoot` to render to. This option is required
953953
| `anchor` | `null` | A child of `target` to render the component immediately before
954954
| `props` | `{}` | An object of properties to supply to the component
955955
| `context` | `new Map()` | A `Map` of root-level context key-value pairs to supply to the component

src/compiler/compile/render_dom/index.ts

+14-8
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,8 @@ export default function dom(
5050

5151
if (should_add_css) {
5252
body.push(b`
53-
function ${add_css}() {
54-
var style = @element("style");
55-
style.id = "${component.stylesheet.id}-style";
56-
style.textContent = "${styles}";
57-
@append(@_document.head, style);
53+
function ${add_css}(target) {
54+
@append_styles(target, "${component.stylesheet.id}", "${styles}");
5855
}
5956
`);
6057
}
@@ -486,7 +483,7 @@ export default function dom(
486483
487484
${css.code && b`this.shadowRoot.innerHTML = \`<style>${css.code.replace(/\\/g, '\\\\')}${options.dev ? `\n/*# sourceMappingURL=${css.map.toUrl()} */` : ''}</style>\`;`}
488485
489-
@init(this, { target: this.shadowRoot, props: ${init_props}, customElement: true }, ${definition}, ${has_create_fragment ? 'create_fragment' : 'null'}, ${not_equal}, ${prop_indexes}, ${dirty});
486+
@init(this, { target: this.shadowRoot, props: ${init_props}, customElement: true }, ${definition}, ${has_create_fragment ? 'create_fragment' : 'null'}, ${not_equal}, ${prop_indexes}, null, ${dirty});
490487
491488
${dev_props_check}
492489
@@ -533,12 +530,21 @@ export default function dom(
533530
name: options.dev ? '@SvelteComponentDev' : '@SvelteComponent'
534531
};
535532

533+
const optional_parameters = [];
534+
if (should_add_css) {
535+
optional_parameters.push(add_css);
536+
} else if (dirty) {
537+
optional_parameters.push(x`null`);
538+
}
539+
if (dirty) {
540+
optional_parameters.push(dirty);
541+
}
542+
536543
const declaration = b`
537544
class ${name} extends ${superclass} {
538545
constructor(options) {
539546
super(${options.dev && 'options'});
540-
${should_add_css && b`if (!@_document.getElementById("${component.stylesheet.id}-style")) ${add_css}();`}
541-
@init(this, options, ${definition}, ${has_create_fragment ? 'create_fragment' : 'null'}, ${not_equal}, ${prop_indexes}, ${dirty});
547+
@init(this, options, ${definition}, ${has_create_fragment ? 'create_fragment' : 'null'}, ${not_equal}, ${prop_indexes}, ${optional_parameters});
542548
${options.dev && b`@dispatch_dev("SvelteRegisterComponent", { component: this, tagName: "${name.name}", options, id: create_fragment.name });`}
543549
544550
${dev_props_check}

src/runtime/internal/Component.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ interface T$$ {
3838
on_destroy: any[];
3939
skip_bound: boolean;
4040
on_disconnect: any[];
41+
root:Element|ShadowRoot
4142
}
4243

4344
export function bind(component, name, callback) {
@@ -103,7 +104,7 @@ function make_dirty(component, i) {
103104
component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31));
104105
}
105106

106-
export function init(component, options, instance, create_fragment, not_equal, props, dirty = [-1]) {
107+
export function init(component, options, instance, create_fragment, not_equal, props, append_styles, dirty = [-1]) {
107108
const parent_component = current_component;
108109
set_current_component(component);
109110

@@ -128,9 +129,12 @@ export function init(component, options, instance, create_fragment, not_equal, p
128129
// everything else
129130
callbacks: blank_object(),
130131
dirty,
131-
skip_bound: false
132+
skip_bound: false,
133+
root: options.target || parent_component.$$.root
132134
};
133135

136+
append_styles && append_styles($$.root);
137+
134138
let ready = false;
135139

136140
$$.ctx = instance

src/runtime/internal/dev.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ export interface SvelteComponentDev {
105105
[accessor: string]: any;
106106
}
107107
interface IComponentOptions<Props extends Record<string, any> = Record<string, any>> {
108-
target: Element;
108+
target: Element|ShadowRoot;
109109
anchor?: Element;
110110
props?: Props;
111111
context?: Map<any, any>;

src/runtime/internal/dom.ts

+36
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,42 @@ function init_hydrate(target: NodeEx) {
125125
}
126126
}
127127

128+
export function append_styles(
129+
target: Node,
130+
style_sheet_id: string,
131+
styles: string
132+
) {
133+
const append_styles_to = get_root_for_styles(target);
134+
135+
if (!append_styles_to?.getElementById(style_sheet_id)) {
136+
const style = element('style');
137+
style.id = style_sheet_id;
138+
style.textContent = styles;
139+
append_stylesheet(append_styles_to, style);
140+
}
141+
}
142+
143+
export function get_root_for_node(node: Node) {
144+
if (!node) return document;
145+
146+
return (node.getRootNode ? node.getRootNode() : node.ownerDocument); // check for getRootNode because IE is still supported
147+
}
148+
149+
function get_root_for_styles(node: Node) {
150+
const root = get_root_for_node(node);
151+
return (root as ShadowRoot).host ? root as ShadowRoot : root as Document;
152+
}
153+
154+
export function append_empty_stylesheet(node: Node) {
155+
const style_element = element('style') as HTMLStyleElement;
156+
append_stylesheet(get_root_for_styles(node), style_element);
157+
return style_element;
158+
}
159+
160+
function append_stylesheet(node: ShadowRoot | Document, style: HTMLStyleElement) {
161+
append((node as Document).head || node, style);
162+
}
163+
128164
export function append(target: NodeEx, node: NodeEx) {
129165
if (is_hydrating) {
130166
init_hydrate(target);

src/runtime/internal/style_manager.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { element } from './dom';
1+
import { append_empty_stylesheet, get_root_for_node } from './dom';
22
import { raf } from './environment';
33

44
interface ExtendedDoc extends Document {
@@ -29,9 +29,9 @@ export function create_rule(node: Element & ElementCSSInlineStyle, a: number, b:
2929

3030
const rule = keyframes + `100% {${fn(b, 1 - b)}}\n}`;
3131
const name = `__svelte_${hash(rule)}_${uid}`;
32-
const doc = node.ownerDocument as ExtendedDoc;
32+
const doc = get_root_for_node(node) as unknown as ExtendedDoc;
3333
active_docs.add(doc);
34-
const stylesheet = doc.__svelte_stylesheet || (doc.__svelte_stylesheet = doc.head.appendChild(element('style') as HTMLStyleElement).sheet as CSSStyleSheet);
34+
const stylesheet = doc.__svelte_stylesheet || (doc.__svelte_stylesheet = append_empty_stylesheet(node).sheet as CSSStyleSheet);
3535
const current_rules = doc.__svelte_rules || (doc.__svelte_rules = {});
3636

3737
if (!current_rules[name]) {

src/runtime/internal/utils.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export function create_slot(definition, ctx, $$scope, fn) {
8989
}
9090
}
9191

92-
export function get_slot_context(definition, ctx, $$scope, fn) {
92+
function get_slot_context(definition, ctx, $$scope, fn) {
9393
return definition[1] && fn
9494
? assign($$scope.ctx.slice(), definition[1](fn(ctx)))
9595
: $$scope.ctx;

test/js/samples/collapses-text-around-comments/expected.js

+5-8
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import {
33
SvelteComponent,
44
append,
5+
append_styles,
56
attr,
67
detach,
78
element,
@@ -13,11 +14,8 @@ import {
1314
text
1415
} from "svelte/internal";
1516

16-
function add_css() {
17-
var style = element("style");
18-
style.id = "svelte-1a7i8ec-style";
19-
style.textContent = "p.svelte-1a7i8ec{color:red}";
20-
append(document.head, style);
17+
function add_css(target) {
18+
append_styles(target, "svelte-1a7i8ec", "p.svelte-1a7i8ec{color:red}");
2119
}
2220

2321
function create_fragment(ctx) {
@@ -58,9 +56,8 @@ function instance($$self, $$props, $$invalidate) {
5856
class Component extends SvelteComponent {
5957
constructor(options) {
6058
super();
61-
if (!document.getElementById("svelte-1a7i8ec-style")) add_css();
62-
init(this, options, instance, create_fragment, safe_not_equal, { foo: 0 });
59+
init(this, options, instance, create_fragment, safe_not_equal, { foo: 0 }, add_css);
6360
}
6461
}
6562

66-
export default Component;
63+
export default Component;

test/js/samples/css-media-query/expected.js

+5-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* generated by Svelte vX.Y.Z */
22
import {
33
SvelteComponent,
4-
append,
4+
append_styles,
55
attr,
66
detach,
77
element,
@@ -11,11 +11,8 @@ import {
1111
safe_not_equal
1212
} from "svelte/internal";
1313

14-
function add_css() {
15-
var style = element("style");
16-
style.id = "svelte-1slhpfn-style";
17-
style.textContent = "@media(min-width: 1px){div.svelte-1slhpfn{color:red}}";
18-
append(document.head, style);
14+
function add_css(target) {
15+
append_styles(target, "svelte-1slhpfn", "@media(min-width: 1px){div.svelte-1slhpfn{color:red}}");
1916
}
2017

2118
function create_fragment(ctx) {
@@ -41,9 +38,8 @@ function create_fragment(ctx) {
4138
class Component extends SvelteComponent {
4239
constructor(options) {
4340
super();
44-
if (!document.getElementById("svelte-1slhpfn-style")) add_css();
45-
init(this, options, null, create_fragment, safe_not_equal, {});
41+
init(this, options, null, create_fragment, safe_not_equal, {}, add_css);
4642
}
4743
}
4844

49-
export default Component;
45+
export default Component;

test/js/samples/css-shadow-dom-keyframes/expected.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ class Component extends SvelteElement {
4646
null,
4747
create_fragment,
4848
safe_not_equal,
49-
{}
49+
{},
50+
null
5051
);
5152

5253
if (options) {
@@ -58,4 +59,4 @@ class Component extends SvelteElement {
5859
}
5960

6061
customElements.define("custom-element", Component);
61-
export default Component;
62+
export default Component;

test/sourcemaps/samples/compile-option-dev/test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ const b64dec = s => Buffer.from(s, 'base64').toString();
44

55
export async function test({ assert, css, js }) {
66

7-
// We check that the css source map embedded in the js is accurate
8-
const match = js.code.match(/\tstyle\.textContent = "(.*?)(?:\\n\/\*# sourceMappingURL=data:(.*?);charset=(.*?);base64,(.*?) \*\/)?";\n/);
7+
// We check that the css source map embedded in the js is accurate
8+
const match = js.code.match(/\tappend_styles\(target, "svelte-.{6}", "(.*?)(?:\\n\/\*# sourceMappingURL=data:(.*?);charset=(.*?);base64,(.*?) \*\/)?"\);\n/);
99
assert.notEqual(match, null);
1010

1111
const [mimeType, encoding, cssMapBase64] = match.slice(2);

0 commit comments

Comments
 (0)