Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

config for ways to omit declarative shadow dom (lightMode) #19

Merged
merged 7 commits into from
May 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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/simpledotcss@2.1.0/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>
`;
}
}