npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

turbo-shadow

v1.2.0

Published

Provides event handling and an HTMLElement mixin for Declarative Shadow DOM in Hotwire Turbo.

Downloads

137

Readme

Turbo Shadow

Provides event handling and an HTMLElement mixin for Declarative Shadow DOM support in Hotwire Turbo.

Requires Turbo 7.2 or higher.

Quick Install

Add the NPM library to your project:

npm i turbo-shadow
# or
yarn add turbo-shadow

Add this to your JavaScript entrypoint (likely index.js) right after you import Turbo:

import * as TurboShadow from "turbo-shadow"

And add this to your HTML head (unfortunately Turbo's client-side caching will strip out all shadow roots).

<meta name="turbo-cache-control" content="no-cache" />

Now when you write a web component by subclassing HTMLElement (or some subclass of that), you can use the ShadowRootable mixin along with the shadowRootAttached promise to ensure by the time you run code within your connectedCallback, the shadow root with server-sent declarative markup has already been attached.

<!-- HTML sent from the server and intercepted by Turbo -->
<test-dsd>
  <template shadowrootmode="open">
    <p>Your message is:</p>
    <slot></slot>
  </template>

  <p>Greetings from Turbo</p>
</test-dsd>
// Client-side JavaScript
import { ShadowRootable } from "turbo-shadow"

class TestDSDElement extends ShadowRootable(HTMLElement) {
  async connectedCallback() {
    // Wait for the promise that the shadow root has been attached
    await this.shadowRootAttached

    // Shadow DOM markup is now loaded and working for the component
    console.log("The shadow root has been attached", this.shadowRoot.innerHTML)
  }
}
customElements.define("test-dsd", TestDSDElement)

Something to note: the client-side JavaScript component definition is actually optional. With Declarative Shadow DOM, you can write HTML-only shadow roots and that's perfectly fine. In fact, you don't even need to use custom elements! You can still get all the benefits of encapsulated styles and DOM that's hidden away from the parent document using most built-in HTML elements. This totally works:

<div style="padding: 10px; background: lightcyan">
  <template shadowrootmode="open">
    <p>Just some regular ol' markup.</p>
    <style>
      /* Styles are fully encapsulated because they only apply to the shadow root! */
      p {
        color: lightsalmon;
      }
    </style>
  </template>
</div>

Keep reading for further details…

Rationale for Turbo Shadow

Hotwire Turbo is an excellent JavaScript library that can take your MPA (Multi-Page Application) and make it feel more like an SPA (Single-Page Application): with fast page changes which you can augment with slick CSS transitions, frame-like support for loading and updating specific regions of a page in real-time, and a feature called Streams which can surgically alter the DOM from server-driven events.

Declarative Shadow DOM (DSD) is an emerging spec for Web Components which lets you define the "shadow DOM" template for a component using server-rendered HTML. So instead of writing out this:

<h1>Hello World from a Web Server</h1>

<howdy-folks>
  <!-- Who knows what will get rendered here? Only the client knows! :( -->
</howdy-folks>

You can write out this (or generate it automatically from a template engine of some kind):

<h1>Hello World from a Web Server</h1>

<howdy-folks>
  <template shadowrootmode="open">
    <!-- Now we get to provide the shadow DOM! -->
    <p>Isn't this amazing?!</p>

    <style>
      /* Styles are fully encapsulated because they only apply to the shadow root! */
      p { 
        color: green;
      }
    </style>
  </template>
</howdy-folks>

You would think that Declarative Shadow DOM and Turbo would be a match made in heaven! Both resolve around the centrality of HTML. But…you would be wrong. 😭

First of all, DSD is only natively supported in Chromium and (recently) WebKit browsers. You would need to use a polyfill for Firefox. However, there are no polyfills out there (that I'm aware of) which support Turbo's event system (for Drive, Frames, and Streams). And even if there were, they don't provide extra support for the custom element to get notified when a shadow root has actually been attached. Expecting it to be in place already when connectedCallback gets triggered is a no-go, because Turbo has already attached new elements to the document prior to the triggering of Turbo events. Something would need to intercept the Turbo events, run a polyfill, and then notify the elements that the shadow roots are now attached.

In addition, Turbo currently isn't directly compatible with the native DSD support, because standard HTML parsing methods in JavaScript don't support DSD for security reasons. For example, if you were to run this:

(new DOMParser()).parseFromString(htmlContainingDSD, "text/html")

Any shadow root templates in the htmlContainingDSD would be ignored…aka they'd just remain inert templates in the output node tree. To get real attached shadow DOM roots, you'd have to supply an extra argument:

(new DOMParser()).parseFromString(htmlContainingDSD, "text/html", { includeShadowRoots: true })

This is all described in the DSD spec explainer.

Will Turbo itself get updated in the future to support this? Possibly, but unlikely until this aspect of the DSD spec is itself supported by all major browsers. Until that time, you will need a Turbo-specific polyfill to handle full DSD support.

Introducing: Turbo Shadow. 😎

If you find any bugs or edge cases that need solving, please file an issue! Otherwise, the primary goal of this library is widespread stability, so I am unlikely to add any additional features in the near future.