I'm trying to create a custom element with most of the javascript encapsulated/referenced in the template/html itself. How can I make that javascript from the template/element to be executed in the shadow dom? Below is an example to better understand the issue. How can I make the script from template.innerHTML
(<script>alert("hello"); console.log("hello from tpl");</script>
) to execute?
Currently I get no alert or logs into the console. I'm testing this with Chrome.
class ViewMedia extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({mode: 'closed'});
var template = document.createElement( 'template' );
template.innerHTML = '<script>alert("hello"); console.log("hello from tpl")';
shadow.appendChild( document.importNode( template.content, true ) );
}
}
customElements.define('x-view-media', ViewMedia);
<x-view-media />
I'm trying to create a custom element with most of the javascript encapsulated/referenced in the template/html itself. How can I make that javascript from the template/element to be executed in the shadow dom? Below is an example to better understand the issue. How can I make the script from template.innerHTML
(<script>alert("hello"); console.log("hello from tpl");</script>
) to execute?
Currently I get no alert or logs into the console. I'm testing this with Chrome.
class ViewMedia extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({mode: 'closed'});
var template = document.createElement( 'template' );
template.innerHTML = '<script>alert("hello"); console.log("hello from tpl")';
shadow.appendChild( document.importNode( template.content, true ) );
}
}
customElements.define('x-view-media', ViewMedia);
<x-view-media />
Share
Improve this question
edited Mar 25, 2020 at 11:55
sergeykish
1811 silver badge10 bronze badges
asked Dec 10, 2018 at 2:12
mikemike
6391 gold badge9 silver badges25 bronze badges
2
- Actually your example works. – Supersharp Commented Dec 10, 2018 at 16:20
- 2 Possible duplicate of Script inside shadow dom not working – Supersharp Commented Dec 10, 2018 at 16:43
2 Answers
Reset to default 3The reason this fails is because importNode
does not evaluate scripts that were imported from another document, which is essentially what's happening when you use innerHTML
to set the template content. The string you provide is parsed into a DocumentFragment
which is considered a separate document. If the template element was selected from the main document, the scripts would be evaluated as expected :
<template id="temp">
<script> console.log('templated script'); </script>
</template>
<div id="host"></div>
<script>
let temp = document.querySelector('#temp');
let host = document.querySelector('#host');
let shadow = host.attachShadow({ mode:'closed' });
shadow.append(document.importNode(temp.content, true));
</script>
One way to force your scripts to evaluate would be to import them using a contextual fragment :
<div id="host"></div>
<script>
let host = document.querySelector('#host');
let shadow = host.attachShadow({ mode:'closed' });
let content = `<script> console.log(this); <\/script>`;
let fragment = document.createRange().createContextualFragment(content);
shadow.append(document.importNode(fragment, true));
</script>
But, this breaks encapsulation as the scripts inside your shadowRoot will actually be evaluated in the global scope with no access to your closed shadow dom. The method that I came up with to deal with this issue is to loop over each script in the shadow dom and evaluate it with the shadowRoot as it's scope. You can't just pass the host object itself as the scope because you'll lose access to the closed shadowRoot. Instead, you can access the ShadowRoot.host
property which would be available as this.host
inside the embedded scripts.
class TestElement extends HTMLElement {
#shadowRoot = null;
constructor() {
super();
this.#shadowRoot = this.attachShadow({ mode:'closed' });
this.#shadowRoot.innerHTML = this.template
}
get template() {
return `
<style>.passed{color:green}</style>
<div id="test"> TEST A </div>
<slot></slot>
<script>
let a = this.querySelector('#test');
let b = this.host.firstElementChild;
a && a.classList.add('passed');
b && (b.style.color = 'green');
<\/script>
`;
}
get #scripts() {
return this.#shadowRoot.querySelectorAll('script');
}
#scopedEval = (script) =>
Function(script).bind(this.#shadowRoot)();
#processScripts() {
this.#scripts.forEach(
s => this.#scopedEval(s.innerHTML)
);
}
connectedCallback() {
this.#processScripts();
}
}
customElements.define('test-element', TestElement);
<test-element>
<p> TEST B </p>
</test-element>
Do not use this technique with an open shadowRoot as you will leave your ponent vulnerable to script injection attacks. The browser prevents arbitrary code execution for a reason: to keep you and your users safe. Do not inject untrusted content into your shadow dom with this enabled, only use this to evaluate your own scripts or trusted libraries, and ideally avoid this trick if at all possible. There are almost always better ways to execute scripts that interact with your shadow dom, like scoping all your logic into your custom element definition.
Side note: Element.setHTML is a much safer method for importing untrusted content which is ing soon as part of the HTML Sanitizer API.
A few points:
- Browsers no longer allow you to add script via
innerHTML
- There is no sand-boxing of script within the DOM a web ponent like there is in an iFrame.
You can create script blocks using
var el = document.createElement('script');
and then adding them as child elements.
class ViewMedia extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({mode: 'closed'});
const s = document.createElement('script');
s.textContent = 'alert("hello");';
shadow.appendChild(s);
}
}
customElements.define('x-view-media', ViewMedia);
<x-view-media></x-view-media>