A partial archive of discourse.wicg.io as of Saturday February 24, 2024.

Script tags scoped to shadow root, <script scoped src=”…”>

trusktr
2020-08-13

It’d be neat if we could scope scripts to shadow roots, like we can scope CSS to shadow roots.

F.e. inside a shadow Root’s HTML (naming bikesheddable):

<script scoped>
  function foo() {...}
  var bar = 123
</script>

Then in a custom element, for example, we could access that stuff, which is scoped to the shadow root instead of window:

this.shadowRoot.scope.foo()
console.log(this.shadowRoot.scope.bar) // 123

Use case: it would allow us to very easily wrap and scope markup managed by tools like Alpine.js or Mavo as custom elements, and in general would allow devs to be more inventive on what sorts of component systems (custom element systems) they can create.

F.e., we could make simple markup like that of Svelte with lets and functions, but still having a scope object for any given custom element (that has a shadow root).

trusktr
2020-08-13

It would mean we could take advantage of simple features of DOM that we already have. F.e. writing the following within a shadow root without muddying the global scope:

<!-- inside the shadow DOM -->
<div onclick="foo()"></div>
<script scope="shadow">
  // foo does not leak to global scope, but it can shadow a global foo var.
  function foo() {...}
</script>

The builtin onclick attribute handler of the DOM would run in scope of the shadow root, which means it’ll use any variables defined in that “lexical scope”. Any variables in that scope can shadow global variables, just like we’re accustomed to inside JavaScript functions. The only difference between this new scope and a normal function scope is that the variables are also exposed on an object on the shadow root.

Now we can imagine a custom element framework, might prescribe for a user to write HTML files like this:

<!-- my-element.html -->
<div onclick="foo()">
  <slot></slot>
</div>
<script>
  function foo() {...}
  function connected(ce) {...}
  function disconnected(ce) {...}
</script>

Then the framework takes this HTML, adds the scope="shadow" attribute to it, gets the life cycle functions from the scope object, and hooks them up to the custom element class.

Just like that we’d have an easy way to create “single-file component” format (like Vue). This would be alternative to the Declarative Custom Elements proposal, but this does not require ES Modules, and it works with the existing and simple functionality that HTML has already given us for many years (f.e. onclick="foo()"), which is something we can’t really use with custom elements.

Basically, this could allow possibilities that requires a lower learning curve for new developers.

And even CE authors could hook things up pretty easily:

// fetch the above HTML file
const html = fetch('./my-element.html')
  .then(r => r.text().replace('<script', '<script scope="shadow"'))

class MyElement extends HTMLElement {
  async connectedCallback () {
    const htm = await html
    if (!this.isConnected) return

    const root = this.attachShadow(...)
    // (Let's update the HTML parser for this)
    root.innerHTML = htm
    this.scope = root.scope
    root.scope.connected?(this)
  }

  disconnectedCallback() {
    this.scope?.disconnected?(this)
  }
}

With this you could imagine it would be trivial for an author to reduce that boilerplate to the following (with a simple monkey patch):

customeElements.define('my-element', './my-element.html')
trusktr
2020-08-13

If a script is type=module, then maybe in that case root.scope is a promise that resolves to the scope (not the module). In that case, the end user (that is using the above framework) would write:

<!-- my-element.html -->
<div onclick="foo()">
  <slot></slot>
</div>
<script>
  import something from 'somewhere'
  function foo() {...}
  function connected(ce) {...}
  function disconnected(ce) {...}
</script>

The idea here is for things like onclick="foo()" to still work in a simple way.

If we resolve the promise to a module, then how would it work? We would need to update onclick machinery to look for exports if script modules. That could work too. In that case, the user would write:

<!-- my-element.html -->
<div id="el" onclick="foo()" class="loading">
  <slot></slot>
</div>
<style>.loading {...}</style>
<script type="module">
  import something from 'somewhere'
  el.classList.remove('loading')
  export function foo() {...}
  export function connected(ce) {...}
  export function disconnected(ce) {...}
</script>

Now just imagine, the end user can use something very simple like Alpine.js to further augment that DOM to add data binding, list iteration, etc. Or they can use anything really.

dy
2020-11-18

Nice! Partial polyfill is here https://gist.github.com/dy/2124c2dfcbdd071f38e866b85436c6c5

jackyjoy123
2021-07-16

thanks for the awesome information.

collimarco
2021-09-03

I really love the idea of having a scoped attribute also for the script tags, it’s very intuitive.

Take the new ViewComponents of Ruby on Rails for example:

Instead of the ugly custom elements (suggested in the above link) we could simply use scoped CSS and JS.

Maybe if a polyfill exists we can try to use this idea right now (with Rails ViewComponents).

mkay581
2021-09-03

Could you do the same thing by throwing your script code in a js file and import it into the one you’d use it in? Taken from your example:

// code.js
export function foo() {...}
export const bar = 123

Then you import it into your component like this

import { foo, bar} from 'code.js';

class MyComponent extends HTMLElement {
  // do some stuff with foo() or bar
}

Would this not be sufficient? Or is there some special need for “scoping” that I’m missing?

jackyjoy123
2021-12-07

thanks my issue has been fixed.

tomyo
2022-10-28

Yes, for what I understand, is the possibility to reference this imported functions from within the html itself as in <button onclick="foo()">.

Currently, they only reference the global scope.

trusktr
2022-12-28

Of course that works! The purpose of scoped scripts would be that they can be written in HTML. This can be good for some reasons:

  • server side rendering sends up an HTML payload, and it simply works and run immediately, eliminates complicated custom hydration code that would need to run before anything would function. This is also one reason why declarative shadow DOM is coming out, so that an HTML payload can be fully rendered even without JavaScript. In the case of scoped scripts, their JavaScript and functionality would be ready before any custom elements are defined and executed, so the end user would be able to get a seemingly faster user experience.
  • write components HTML-first: we can write HTML first, then you add scripts later if we need. Currently, for custom elements, we must write JavaScript no matter what, which detracts from the simplicity of HTML. We should not need JavaScript to make custom elements, but should have a declarative HTML-first capability that simplifies the experience, and scoped scripts would be one step towards that (thinking like Vue and Svelte components which are HTML-first, add JS later if you want).
    • Relating to this, scoped scripts would make life simpler for tools to take the HTML-first components and output JavaScript custom element wrappers. The less our tools have to do (because more of it is standard) the better.
trusktr
2022-12-28

I forgot to mention that too @mkay581. With Declarative Shadow DOM that I linked above, scoped scripts would let us write HTML like this:

  <h1>Example: </h1>

  <template shadowroot="open">
    <!-- inside the shadow DOM -->
    <div onclick="foo()"></div>

    <script scope="shadow">
      // foo does not leak to global scope, but it can shadow a global foo var.
      function foo() {...}
    </script>

    <script scope="global">
      // bar is global
      function bar() {...}
    </script>
  </template>

  <!-- other div outside the shadow root, causes runtime error because foo() is not global -->
  <div onclick="foo()"></div>

  <!-- yet another div outside the shadow root, this works because bar() is global -->
  <div onclick="bar()"></div>

Note:

  • foo is scoped only inside of the declarative shadow root
  • this HTML may come from a server, and it will immediately run before any custom elements may even be defined (if at all), so the user may have functionality that is available sooner

We might need more rules. F.e. what is this inside the foo() function? Perhaps it is the shadowRoot itself.

mkay581
2022-12-28

I see. Thanks for explaining @trusktr and @tomyo.

Just the benefit of having custom element creation (with shadow dom) not require JavaScript is a big enough win for me to support this proposal. :raised_hands: