Skip to main content

Scott Nath Frontend Architect

Scott Nath's picture
GitHubdev.toLinkedInMastodon
Header image for article 'Profile Components: SSR with Declarative Shadow DOM'

Profile Components: SSR with Declarative Shadow DOM

Enhancing Profile Components with Declarative Shadow DOM and showing examples for Server Side Rendering


With the official release of shadowrootmode-supporting Firefox v123, it’s time to upgrade the profile components with Declarative Shadow DOM functionality!

tl;dr


What is this?

Four new methods have been added to allow generating a template tag with shadowrootmode containing the generated HTML and styles of a profile component. This was fairly trivial to add due to the Javascript, HTML and CSS living in separate files. (see 👷 DX: Separate files for Javascript, HTML, and CSS).

Each of the four components now have a dsd method which can be used to generate something like this:

<template shadowrootmode="open">
  <styles>(...css styles for GitHub component)</styles>
  <section (...rest of generated HTML)</section>
</template>

This generated template can be used server-side when generating HTML output, or client-side after page load. Although…doing this client-side is weird frankly…like…they’re web components, just use them as they were written…who knows though, maybe someone has a use for that feature 🤷.

How to use the Declarative Shadow DOM methods

Below are some examples of usage of the GitHub component (see GitHub DSD docs). Usage of the DEV components is pretty much the same - check out the dev.to DSD docs for DEV usage examples.

The main value of these is when create HTML before it is rendered by the browser. Then when the browser renders it, the browser converts the DSD into actual Shadow DOM. These components are only presentational and do not have functionality added by the customElement - so DSD or regular web component usage results in the same thing!

SSR (Server Side Rendering) HTML in Node.js

// import from npm module
import { dsd } from 'profile-components/github-utils';

const repos = JSON.stringify([
  'scottnath/profile-components',
  'storydocker/storydocker'
]);

const generatedTemplate = await dsd({
  login: 'scottnath',
  avatar_url: profilePic.src,
  repos
},true);

/**
generatedTemplate contains:
<template shadowrootmode="open">
  <styles>(...css styles for GitHub component)</styles>
  <section (...rest of generated HTML)</section>
</template>
*/

// use this variable in a file pre-render to have a DSD-pre-populated element
const componentHTML = `<github-user>${generatedTemplate}</github-user>`;

Server side render in an Astro component

---
import {dsd} from 'profile-components/github-utils';

const repos = JSON.stringify(['scottnath/profile-components', 'storydocker/storydocker']);
const declaredDOM = await dsd({
  login: 'scottnath',
  repos
},true)
---

<github-user
  data-theme="light_high_contrast"
  set:html={declaredDOM}>
</github-user>

Client side rendering via unpkg

<!-- add empty elements to HTML -->
<github-repository></github-repository>
<hr />
<github-user></github-user>

<script type="module">
  // import from unpkg
  import {
    user,
    repo,
  } from 'https://unpkg.com/profile-components/dist/github-utils.js';

  // repo has it's own DSD method:
  const dsdRepo = repo.dsd;

  /**
    * Polyfill for Declarative Shadow DOM which, when triggered, converts
    *  the template element into actual shadow DOM.
    * This is only needed when injecting _after_ page is loaded
    * @see https://developer.chrome.com/docs/css-ui/declarative-shadow-dom#polyfill
    */
  const triggerAttachShadowRoots = () => {
    (function attachShadowRoots(root) {
      root
        .querySelectorAll('template[shadowrootmode]')
        .forEach((template) => {
          const mode = template.getAttribute('shadowrootmode');
          const shadowRoot = template.parentNode.attachShadow({ mode });
          shadowRoot.appendChild(template.content);
          template.remove();
          attachShadowRoots(shadowRoot);
        });
    })(document);
  };

  /**
    * Uses the "dsd" method to generate DSD, add the string of DSD content
    *  to the element, then trigger the polyfill to convert the template
    */
  const injectDSD = async () => {
    const dsdHTML = await dsd({ username: 'scottnath' }, true);
    document.querySelector('github-user').innerHTML = dsdHTML;
    // now that the HTML is async-created, the polyfill can convert it
    triggerAttachShadowRoots();
  };
  injectDSD();

  /**
    * Uses the "dsdRepo" method to generate DSD, add the string of DSD content
    *  to the element, then trigger the polyfill to convert the template
    */
  const injectRepoDSD = async () => {
    const dsdHTML = await dsdRepo(
      { full_name: 'scottnath/profile-components' },
      true
    );
    document.querySelector('github-repository').innerHTML = dsdHTML;
    // now that the HTML is async-created, the polyfill can convert it
    triggerAttachShadowRoots();
  };
  injectRepoDSD();
</script>