A component here is one self-contained chunk of HTML: a
<style> with @scope, the markup, and a
<script>. The page fetches it and mounts it with
streamHTMLUnsafe({ runScripts: true }), so the component's
script runs as it streams in and wires up its own behavior: no framework,
no build step.
This is the imperative path. Declarative streaming (the other demos) would
inject the markup and scoped styles but not run the
script, which is exactly why runScripts: true exists.
The component streams in automatically after a short delay; its scoped styles apply and its counter button works, proving the script ran.
<style>
@scope {
.widget { padding: 1rem; background: #eef2ff; }
button { color: #4338ca; }
}
</style>
<div class="widget">
<h3>Counter</h3>
<button>Clicked <span class="count">0</span>×</button>
</div>
<script type="module">
// module scope, runs with runScripts: true
const w = document.querySelector(".widget");
let n = 0;
w.querySelector("button").onclick = () =>
(w.querySelector(".count").textContent = ++n);
</script>
const res = await fetch("./stream"); // the server streams the component
// runScripts: true lets the component's <script> run as it streams in
res.body
.pipeThrough(new TextDecoderStream())
.pipeTo(mount.streamHTMLUnsafe({ runScripts: true }));
Registering service worker…
The frame loads app.html, which on load fetches the
component from the service worker (./stream, streamed after
a short delay) and pipes it into the mount with
streamHTMLUnsafe({ runScripts: true }).
Because runScripts is on, the component's
<script> runs as it arrives and wires up the button,
and @scope keeps its styles local to the mount. The log
line "✓ component <script> ran" is printed by that script.