Skip to content

Add option to make usage of shadow dom configurable #2516

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

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/compile/Component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export default class Component {

this.walk_instance_js_post_template();

if (!compile_options.customElement) this.stylesheet.reify();
if (!compile_options.shadowDom) this.stylesheet.reify();

this.stylesheet.warn_on_unused_selectors(this);
}
Expand Down Expand Up @@ -317,7 +317,7 @@ export default class Component {

add_string(final_chunk);

css = compile_options.customElement ?
css = compile_options.shadowDom ?
{ code: null, map: null } :
this.stylesheet.render(compile_options.cssOutputFilename, true);

Expand Down
9 changes: 7 additions & 2 deletions src/compile/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ const valid_options = [
'hydratable',
'legacy',
'customElement',
'shadowDom',
'tag',
'css',
'preserveComments',
'preserveWhitespace'
];

function validate_options(options: CompileOptions, warnings: Warning[]) {
const { name, filename } = options;
const { name, filename, shadowDom, customElement } = options;

Object.keys(options).forEach(key => {
if (valid_options.indexOf(key) === -1) {
Expand All @@ -53,6 +54,10 @@ function validate_options(options: CompileOptions, warnings: Warning[]) {
toString: () => message,
});
}

if (!customElement && shadowDom) {
throw new Error(`options.shadowDom cannot be true if options.customElement is false`)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a little sanity check to save us from an invalid state.

}
}

function get_name(filename) {
Expand All @@ -74,7 +79,7 @@ function get_name(filename) {
}

export default function compile(source: string, options: CompileOptions = {}) {
options = assign({ generate: 'dom', dev: false }, options);
options = assign({ generate: 'dom', dev: false, shadowDom: options.customElement }, options);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This defaults shadowDom to whatever customElement may be set to. So if it's true, shadowDom will be true by default (which is the behavior we want for backwards compatibility).


const stats = new Stats();
const warnings = [];
Expand Down
14 changes: 8 additions & 6 deletions src/compile/render-dom/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@ export default function dom(
builder.add_line(`const ${renderer.file_var} = ${JSON.stringify(component.file)};`);
}

const css = component.stylesheet.render(options.filename, !options.customElement);

const css = component.stylesheet.render(options.filename, !options.shadowDom);
const styles = component.stylesheet.has_styles && stringify(options.dev ?
`${css.code}\n/*# sourceMappingURL=${css.map.toUrl()} */` :
css.code, { only_escape_at_symbol: true });

if (styles && component.compile_options.css !== false && !options.customElement) {
if (styles && component.compile_options.css !== false && !options.shadowDom) {
builder.add_block(deindent`
function @add_css() {
var style = @element("style");
Expand Down Expand Up @@ -64,7 +65,7 @@ export default function dom(
// TODO injecting CSS this way is kinda dirty. Maybe it should be an
// explicit opt-in, or something?
const should_add_css = (
!options.customElement &&
!options.shadowDom &&
component.stylesheet.has_styles &&
options.css !== false
);
Expand Down Expand Up @@ -443,11 +444,12 @@ export default function dom(
builder.add_block(deindent`
class ${name} extends @SvelteElement {
constructor(options) {
super();
super(${options.shadowDom ? '' : '{ use_shadow_dom: false }'});

${css.code && `this.shadowRoot.innerHTML = \`<style>${escape(css.code, { only_escape_at_symbol: true }).replace(/\\/g, '\\\\')}${options.dev ? `\n/*# sourceMappingURL=${css.map.toUrl()} */` : ''}</style>\`;`}
${css.code && options.shadowDom && `this.shadowRoot.innerHTML = \`<style>${escape(css.code, { only_escape_at_symbol: true }).replace(/\\/g, '\\\\')}${options.dev ? `\n/*# sourceMappingURL=${css.map.toUrl()} */` : ''}</style>\`;`}
${should_add_css && `if (!document.getElementById("${component.stylesheet.id}-style")) @add_css();`}

@init(this, { target: this.shadowRoot }, ${definition}, create_fragment, ${not_equal}, ${prop_names});
@init(this, { target: this${options.shadowDom ? '.shadowRoot' : ''} }, ${definition}, create_fragment, ${not_equal}, ${prop_names});
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't guess I'm 100% confident about this. It makes the target itself... I assume it's okay?

Copy link

@ldumaine ldumaine May 1, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello. Yes this should work. I just had to modify the javascript generated by Svelte: set "this"as the target in the init() method of the component, and remove "this.attachShadow({ mode: 'open' });" from SvelteElement constructor() method.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ldumaine, If the shadowDom option is disabled then the target will be this and the constructor of SvelteElement should get passed { use_shadow_dom: false } which will make this.attachShadow({ mode: 'open' }) not be called.


${dev_props_check}

Expand Down
2 changes: 1 addition & 1 deletion src/compile/render-ssr/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default function ssr(
}, options));

// TODO concatenate CSS maps
const css = options.customElement ?
const css = options.shadowDom ?
{ code: null, map: null } :
component.stylesheet.render(options.filename, true);

Expand Down
1 change: 1 addition & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export interface CompileOptions {
hydratable?: boolean;
legacy?: boolean;
customElement?: boolean;
shadowDom?: boolean;
tag?: string;
css?: boolean;

Expand Down
6 changes: 4 additions & 2 deletions src/internal/Component.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,11 @@ export function init(component, options, instance, create_fragment, not_equal, p
export let SvelteElement;
if (typeof HTMLElement !== 'undefined') {
SvelteElement = class extends HTMLElement {
constructor() {
constructor({ use_shadow_dom = true } = { use_shadow_dom: true }) {
super();
this.attachShadow({ mode: 'open' });
if (use_shadow_dom) {
this.attachShadow({ mode: 'open' });
}
}

connectedCallback() {
Expand Down
6 changes: 3 additions & 3 deletions src/parse/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ import error from '../utils/error';

interface ParserOptions {
filename?: string;
customElement?: boolean;
shadowDom?: boolean;
}

type ParserState = (parser: Parser) => (ParserState | void);

export class Parser {
readonly template: string;
readonly filename?: string;
readonly customElement: boolean;
readonly shadowDom: boolean;

index = 0;
stack: Array<Node> = [];
Expand All @@ -33,7 +33,7 @@ export class Parser {

this.template = template.replace(/\s+$/, '');
this.filename = options.filename;
this.customElement = options.customElement;
this.shadowDom = options.shadowDom;

this.html = {
start: null,
Expand Down
2 changes: 1 addition & 1 deletion src/parse/state/tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export default function tag(parser: Parser) {
? meta_tags.get(name)
: (/[A-Z]/.test(name[0]) || name === 'svelte:self' || name === 'svelte:component') ? 'InlineComponent'
: name === 'title' && parent_is_head(parser.stack) ? 'Title'
: name === 'slot' && !parser.customElement ? 'Slot' : 'Element';
: name === 'slot' && !parser.shadowDom ? 'Slot' : 'Element';
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another area I wasn't 100% sure on. From my reading slot seems to be directly related to the shadow DOM. I assume that with a custom element we'd just want the slot to be a regular element..


const element: Node = {
start,
Expand Down
2 changes: 1 addition & 1 deletion test/custom-elements/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ describe('custom-elements', function() {
if (id.endsWith('.svelte')) {
const compiled = svelte.compile(code, {
customElement: true,
dev: config.dev
...config
});

return compiled.js;
Expand Down
4 changes: 4 additions & 0 deletions test/custom-elements/samples/no-shadow-dom/_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default {
customElement: true,
shadowDom: false
};
9 changes: 9 additions & 0 deletions test/custom-elements/samples/no-shadow-dom/main.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<svelte:options tag="custom-element"/>

<h1>Hello world!</h1>

<style>
h1 {
color: blue;
}
</style>
17 changes: 17 additions & 0 deletions test/custom-elements/samples/no-shadow-dom/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as assert from 'assert';
import CustomElement from './main.svelte';

export default function (target) {
new CustomElement({
target
});

assert.equal(target.innerHTML, '<custom-element><h1 class="svelte-619mm8">Hello world!</h1></custom-element>');

const el = target.querySelector('custom-element');
const h1 = el.querySelector('h1');
const { color } = getComputedStyle(h1);

assert.equal(h1.textContent, 'Hello world!');
assert.equal(color, 'rgb(0, 0, 255)');
}
6 changes: 6 additions & 0 deletions test/js/samples/css-custom-element-no-shadow-dom/_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
options: {
customElement: true,
shadowDom: false
}
};
64 changes: 64 additions & 0 deletions test/js/samples/css-custom-element-no-shadow-dom/expected.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/* generated by Svelte vX.Y.Z */
import {
SvelteElement,
append,
detach,
element,
init,
insert,
noop,
safe_not_equal
} from "svelte/internal";

function add_css() {
var style = element("style");
style.id = 'svelte-pbe34-style';
style.textContent = "h1.svelte-pbe34{color:blue}";
append(document.head, style);
}

function create_fragment(ctx) {
var h1;

return {
c() {
h1 = element("h1");
h1.textContent = "Hello world";
this.c = noop;
h1.className = "svelte-pbe34";
},

m(target, anchor) {
insert(target, h1, anchor);
},

p: noop,
i: noop,
o: noop,

d(detaching) {
if (detaching) {
detach(h1);
}
}
};
}

class Component extends SvelteElement {
constructor(options) {
super({ use_shadow_dom: false });
if (!document.getElementById("svelte-pbe34-style")) add_css();

init(this, { target: this }, null, create_fragment, safe_not_equal, []);

if (options) {
if (options.target) {
insert(options.target, this, options.anchor);
}
}
}
}

customElements.define("custom-element", Component);

export default Component;
9 changes: 9 additions & 0 deletions test/js/samples/css-custom-element-no-shadow-dom/input.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<svelte:options tag="custom-element"/>

<h1>Hello world</h1>

<style>
h1 {
color: blue;
}
</style>