简体   繁体   中英

How to execute javascript in shadow dom/web components?

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 />

A few points:

  1. Browsers no longer allow you to add script via innerHTML
  2. There is no sand-boxing of script within the DOM a web component 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>

The 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 component 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 coming soon as part of the HTML Sanitizer API .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM