Skip to content

Commit

Permalink
config for ways to omit declarative shadow dom (lightMode) (#19)
Browse files Browse the repository at this point in the history
* config for various ways to omit declarative shadow dom

* non shadow dom styling

* includeShadowRoots config spec

* refactor out double getInnerHTML call

* refactor to lightMode config

* documentation

* quick docs update
  • Loading branch information
thescientist13 authored May 11, 2022
1 parent 9f12d4c commit 71cf93f
Show file tree
Hide file tree
Showing 10 changed files with 215 additions and 28 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ In addition, WCC hopes to provide a surface area to explore patterns around [str
## Key Features

1. Supports the following `HTMLElement` lifecycles and methods on the server side
- `constructor`
- `connectedCallback`
- `attachShadow`
- `innerHTML`
Expand Down
6 changes: 4 additions & 2 deletions build.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ async function init() {
const distRoot = './dist';
const pagesRoot = './docs/pages';
const pages = await fs.readdir(new URL(pagesRoot, import.meta.url));
const { html } = await renderToString(new URL('./docs/index.js', import.meta.url), false);
const { html } = await renderToString(new URL('./docs/index.js', import.meta.url), {
lightMode: true
});

// await fs.rm(distRoot, { recursive: true, force: true });
// await fs.mkdir('./dist', { recursive: true });
Expand Down Expand Up @@ -65,7 +67,7 @@ async function init() {
<html lang="en" prefix="og:http://ogp.me/ns#">
<head>
<title>Web Components Compiler (WCC)</title>
<title>WCC - Web Components Compiler</title>
<meta property="og:title" content="Web Components Compiler (WCC)"/>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/simple.min.css">
</head>
Expand Down
18 changes: 7 additions & 11 deletions docs/components/footer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,20 @@ const template = document.createElement('template');
template.innerHTML = `
<style>
footer {
bottom: 0;
width: 100%;
background-color: var(--accent);
min-height: 30px;
padding-top: 10px;
padding: 10px 0;
grid-column: 1 / -1;
}
footer a {
color: #efefef;
text-decoration: none;
text-align: center;
}
footer h4 {
width: 90%;
margin: 0 auto;
padding: 0;
text-align: center;
}
footer a:visited {
color: var(--text);
text-decoration: none;
}
</style>
Expand Down
9 changes: 4 additions & 5 deletions docs/components/navigation.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,21 @@ const template = document.createElement('template');

template.innerHTML = `
<style>
ul {
nav ul {
list-style-type: none;
color: #efefef;
overflow: auto;
grid-column: 1 / -1;
}
ul li {
nav ul li {
float: left;
width: 33%;
text-align: center;
margin: 10px 0;
}
ul li a:visited {
color: #efefef;
nav ul li a, nav ul li a:visited {
color: var(--text);
}
</style>
Expand Down
6 changes: 6 additions & 0 deletions docs/pages/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ This function takes a `URL` to a JavaScript file that defines a custom element,
const { html } = await renderToString(new URL('./src/index.js', import.meta.url));
```
#### Options
`renderToString` also supports a second parameter that is an object, called `options`, which supports the following configurations:
- `lightMode`: For more static outcomes (e.g. no declarative shadow DOM), this option will omit all wrapping `<template shadowroot="...">` tags when rendering out custom elements. Useful for static sites or working with global CSS libraries.
## Metadata
Expand Down Expand Up @@ -72,6 +77,7 @@ export async function getData() {
> _See our [examples page](/examples/) for more info._
## Conventions
- Make sure to define your custom elements with `customElements.define`
- Make sure to `export default` your custom element base class
- Avoid [touching the DOM in `constructor` methods](https://twitter.com/techytacos/status/1514029967981494280)
20 changes: 10 additions & 10 deletions src/wcc.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import fs from 'node:fs/promises';

let definitions;

async function renderComponentRoots(tree) {
async function renderComponentRoots(tree, includeShadowRoots = true) {
for (const node of tree.childNodes) {
if (node.tagName && node.tagName.indexOf('-') > 0) {
const { tagName } = node;
const { moduleURL } = definitions[tagName];
const elementInstance = await initializeCustomElement(moduleURL, tagName, node.attrs);

const shadowRootHtml = elementInstance.getInnerHTML({ includeShadowRoots: true });
const shadowRootHtml = elementInstance.getInnerHTML({ includeShadowRoots });
const shadowRootTree = parseFragment(shadowRootHtml);

// TODO safeguard against non-declared custom elements, e.g. using <my-element></my-element>
Expand All @@ -25,12 +25,12 @@ async function renderComponentRoots(tree) {
}

if (node.childNodes && node.childNodes.length > 0) {
await renderComponentRoots(node);
await renderComponentRoots(node, includeShadowRoots);
}

// does this only apply to `<template>` tags?
if (node.content && node.content.childNodes && node.content.childNodes.length > 0) {
await renderComponentRoots(node.content);
await renderComponentRoots(node.content, includeShadowRoots);
}
}

Expand Down Expand Up @@ -92,18 +92,18 @@ async function initializeCustomElement(elementURL, tagName, attrs = []) {
return elementInstance;
}

async function renderToString(elementURL, fragment = true) {
async function renderToString(elementURL, options = {}) {
definitions = [];
const { lightMode = false } = options;
const includeShadowRoots = !lightMode;

const elementInstance = await initializeCustomElement(elementURL);
const elementHtml = elementInstance.getInnerHTML({ includeShadowRoots: false });
const elementHtml = elementInstance.getInnerHTML({ includeShadowRoots });
const elementTree = parseFragment(elementHtml);
const finalTree = await renderComponentRoots(elementTree);

elementInstance.shadowRoot.innerHTML = serialize(finalTree);
const finalTree = await renderComponentRoots(elementTree, includeShadowRoots);

return {
html: elementInstance.getInnerHTML({ includeShadowRoots: fragment }),
html: serialize(finalTree),
metadata: definitions
};
}
Expand Down
88 changes: 88 additions & 0 deletions test/cases/config-light-mode/config-light-mode.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Use Case
* Run wcc against nested custom elements with nested declarative shadow dom and ensure no shadow is included
*
* User Result
* Should return the expected HTML output for all levels of element nesting.
*
* User Workspace
* src/
* components/
* navigation.js
* header.js
* pages/
* index.js
*
* Config
* {
* lightMode: true
* }
*/

import chai from 'chai';
import { JSDOM } from 'jsdom';
import { renderToString } from '../../../src/wcc.js';

const expect = chai.expect;

describe('Run WCC For ', function() {
const LABEL = 'Nested Custom Element w/ no using Light Mode configuration';
let dom;

before(async function() {
const { html } = await renderToString(new URL('./src/pages/index.js', import.meta.url), {
lightMode: true
});

dom = new JSDOM(html);
});

describe(LABEL, function() {
it('should not have one top level <template> with an open shadowroot', function() {
expect(dom.window.document.querySelectorAll('template[shadowroot="open"]').length).to.equal(0);
expect(dom.window.document.querySelectorAll('template').length).to.equal(0);
});

describe('static page content', function() {
it('should have the expected static content for the page', function() {
expect(dom.window.document.querySelector('h1').textContent).to.equal('Home Page');
});
});

describe('custom header element with nested navigation element', function() {
let headerContentsDom;

before(function() {
headerContentsDom = new JSDOM(dom.window.document.querySelectorAll('header')[0].innerHTML);
});

it('should have a <header> tag within the <template> shadowroot', function() {
expect(dom.window.document.querySelectorAll('header').length).to.equal(1);
});

it('should have expected content within the <header> tag', function() {
const content = headerContentsDom.window.document.querySelector('a h4').textContent;

expect(content).to.contain('My Personal Blog');
});

describe('nested navigation element', function() {
let navigationContentsDom;

before(function() {
navigationContentsDom = new JSDOM(headerContentsDom.window.document.querySelectorAll('wcc-navigation')[0].innerHTML);
});

it('should have a <nav> tag within the <template> shadowroot', function() {
expect(navigationContentsDom.window.document.querySelectorAll('nav').length).to.equal(1);
});

it('should have three links within the <nav> element', function() {
const links = navigationContentsDom.window.document.querySelectorAll('nav ul li a');

expect(links.length).to.equal(3);
});
});
});
});
});
43 changes: 43 additions & 0 deletions test/cases/config-light-mode/src/components/header.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// intentionally nested to test wcc nested dependency resolution logic
import './navigation.js';

class Header extends HTMLElement {
connectedCallback() {
if (!this.shadowRoot) {
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = this.render();
}
}

render() {
return `
<header class="header">
<div class="head-wrap">
<div class="brand">
<a href="/">
<img src="/www/assets/greenwood-logo.jpg" alt="Greenwood logo"/>
<h4>My Personal Blog</h4>
</a>
</div>
<wcc-navigation></wcc-navigation>
<div class="social">
<a href="https://github.com/ProjectEvergreen/greenwood">
<img
src="https://img.shields.io/github/stars/ProjectEvergreen/greenwood.svg?style=social&logo=github&label=github"
alt="Greenwood GitHub badge"
class="github-badge"/>
</a>
</div>
</div>
</header>
`;
}
}

export {
Header
};

customElements.define('wcc-header', Header);
27 changes: 27 additions & 0 deletions test/cases/config-light-mode/src/components/navigation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// intentionally nested in the assets/ directory to test wcc nested dependency resolution logic
const template = document.createElement('template');

template.innerHTML = `
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/artists">Artists</a></li>
<ul>
</nav>
`;

class Navigation extends HTMLElement {
connectedCallback() {
if (!this.shadowRoot) {
this.attachShadow({ mode: 'open' });
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
}
}

export {
Navigation
};

customElements.define('wcc-navigation', Navigation);
25 changes: 25 additions & 0 deletions test/cases/config-light-mode/src/pages/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import '../components/header.js';

export default class HomePage extends HTMLElement {
constructor() {
super();

if (this.shadowRoot) {
// console.debug('HomePage => shadowRoot detected!');
} else {
this.attachShadow({ mode: 'open' });
}
}

connectedCallback() {
this.shadowRoot.innerHTML = this.getTemplate();
}

getTemplate() {
return `
<wcc-header></wcc-header>
<h1>Home Page</h1>
`;
}
}

0 comments on commit 71cf93f

Please sign in to comment.