Skip to content
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
79 changes: 73 additions & 6 deletions docs/pages/docs.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,92 @@
# Documentation


## API

### `renderToString`
### renderToString

This function takes a `URL` to a JavaScript file that defines a custom element, and returns the static HTML output of its rendered contents.

```js
const { html } = await renderToString(new URL('./src/index.js', import.meta.url));
```

#### Options
```js
// index.js
import './components/footer.js';
import './components/header.js';

const template = document.createElement('template');

template.innerHTML = `
<style>
:root {
--accent: #367588;
}
</style>

<wcc-header></wcc-header>

<main>
<h1>My Blog Post</h1>
</main>

<wcc-footer></wcc-footer>
`;

class Home extends HTMLElement {

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

export default Home;
```

`renderToString` also supports a second parameter that is an object, called `options`, which supports the following configurations:
### renderFromHTML

This function takes a string of HTML and an array of any top-level custom elements used with `import`, and returns the static HTML output of the rendered content.

```js
const { html } = await renderToString(`
<html>
<head>
<title>WCC</title>
</head>
<body>
<wcc-header></wcc-header>
<h1>Home Page</h1>
<wcc-footer></wcc-footer>
</body>
</html>
`,
[
new URL('./src/components/footer.js', import.meta.url),
new URL('./src/components/header.js', import.meta.url)
]);
```

### Options

`renderToString` and `renderFromHTML` also supports a second and third parameter respectively, that is an object, called `options`
```js
// default values
{
lightMode: false
}
```

It supports the following configuration(s):

- `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

`renderToString` returns not only HTML, but also metadata about all the custom elements registered as part of rendering the top level custom element.
`renderToString` and `renderFromHTML` return not only HTML, but also metadata about all the custom elements registered as part of rendering the top level custom element.

```js
const { metadata } = await renderToString(new URL('./src/index.js', import.meta.url));
Expand Down Expand Up @@ -61,7 +128,7 @@ The benefit is that this hint can be used to defer loading of these scripts by u
> _See our [examples page](/examples/) for more info._


## `getData`
## Data

To further support SSR and hydration scenarios where data is involved, a file with a custom element definition can also export a `getData` function to inject into the custom elements constructor at server render time, as "props". This can be serialized right into the component's Shadow DOM!

Expand Down
25 changes: 22 additions & 3 deletions src/wcc.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import './dom-shim.js';

import * as acorn from 'acorn';
import * as walk from 'acorn-walk';
import { parseFragment, serialize } from 'parse5';
import { parse, parseFragment, serialize } from 'parse5';

import fs from 'node:fs/promises';

Expand Down Expand Up @@ -94,6 +94,7 @@ async function initializeCustomElement(elementURL, tagName, attrs = []) {

async function renderToString(elementURL, options = {}) {
definitions = [];

const { lightMode = false } = options;
const includeShadowRoots = !lightMode;

Expand All @@ -108,8 +109,26 @@ async function renderToString(elementURL, options = {}) {
};
}

// TODO renderToStream
async function renderFromHTML(html, elements = [], options = {}) {
definitions = [];

const { lightMode = false } = options;
const includeShadowRoots = !lightMode;

for (const url of elements) {
await initializeCustomElement(url);
}

const elementTree = parse(html);
const finalTree = await renderComponentRoots(elementTree, includeShadowRoots);

return {
html: serialize(finalTree),
metadata: definitions
};
}

export {
renderToString
renderToString,
renderFromHTML
};
101 changes: 101 additions & 0 deletions test/cases/render-from-html/render-from-html.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Use Case
* Run wcc against nested custom elements with declarative shadow dom from an HTML string.
*
* User Result
* Should return the expected HTML output for all levels of element nesting.
*
* User Workspace
* src/
* components/
* navigation.js
* header.js
*/
import chai from 'chai';
import { JSDOM } from 'jsdom';
import { renderFromHTML } from '../../../src/wcc.js';

const expect = chai.expect;

describe('Run WCC For ', function() {
const LABEL = 'Using renderFromHTML';
let dom;
let assetMetadata;

before(async function() {
const { html, metadata } = await renderFromHTML(`
<html>
<head>
<title>WCC</title>
</head>
<body>
<wcc-header></wcc-header>
<h1>Home Page</h1>
</body>
</html>
`, [
new URL('./src/components/header.js', import.meta.url)
]);

dom = new JSDOM(html);
assetMetadata = metadata;
});

describe(LABEL, function() {
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('wcc-header template[shadowroot="open"]')[0].innerHTML);
});

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

it('should have expected content within the <header> tag', function() {
const content = headerContentsDom.window.document.querySelector('header 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 template[shadowroot="open"]')[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);
});
});
});

describe(LABEL, function() {
it('should have three custom elements in the asset graph', function() {
expect(Object.keys(assetMetadata).length).to.equal(2);
});

it('should have the correct attributes for each asset', function() {
Object.entries(assetMetadata).forEach((asset) => {
expect(asset[0]).to.not.be.undefined;
expect(asset[1].instanceName).to.not.be.undefined;
expect(asset[1].moduleURL).to.not.be.undefined;
});
});
});
});
});
40 changes: 40 additions & 0 deletions test/cases/render-from-html/src/components/header.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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 default Header;

customElements.define('wcc-header', Header);
27 changes: 27 additions & 0 deletions test/cases/render-from-html/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);