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

feature/issue 119 constructor props #120

Merged
merged 5 commits into from
Oct 28, 2023
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
70 changes: 68 additions & 2 deletions docs/pages/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,76 @@ The benefit is that this hint can be used to defer loading of these scripts by u

## 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!
WCC provide a couple mechanisms for data loading.

For example, you could preload a counter component with an initial counter state
### Constructor Props

Often for frameworks that might have their own needs for data loading and orchestration, a top level "constructor prop" can be provided to `renderToString` as the final param. The prop will then be passed to the custom element's `constructor` when loading the module URL.

<!-- eslint-disable no-unused-vars -->
```js
const request = new Request({ /* ... */ });
const { html } = await renderToString(new URL(moduleUrl), false, request);
```

This pattern plays really nice with file-based routing and SSR!
```js
export default class PostPage extends HTMLElement {
constructor(request) {
super();

const params = new URLSearchParams(request.url.slice(request.url.indexOf('?')));
this.postId = params.get('id');
}

async connectedCallback() {
const { postId } = this;
const post = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`).then(resp => resp.json());
const { title, body } = post;

this.innerHTML = `
<h2>${title}</h2>
<p>${body}</p>
`;
}
}
```

### Data Loader

To support component-level data loading and hydration scenarios, a file with a custom element definition can also export a `getData` function to inject into the custom elements constructor at build time. This can be serialized right into the component's Shadow DOM!

For example, you could preload a counter component with an initial counter state, which would also come through the `constructor`.

<!-- eslint-disable no-unused-vars -->
```js
class Counter extends HTMLElement {
constructor(props = {}) {
super();

this.count = props.count;
this.render();
}

// ....

render() {
return `
<template shadowroot="open">
<script type="application/json">
${JSON.stringify({ count: this.count })}
</script>

<div>
<button id="inc">Increment</button>
<span>Current Count: <span id="count">${this.count}</span></span>
<button id="dec">Decrement</button>
</div>
</template>
`;
}
}

export async function getData() {
return {
count: Math.floor(Math.random() * (100 - 0 + 1) + 0)
Expand Down
12 changes: 8 additions & 4 deletions src/wcc.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ async function getTagName(moduleURL) {
return tagName;
}

async function initializeCustomElement(elementURL, tagName, attrs = [], definitions = [], isEntry) {
async function initializeCustomElement(elementURL, tagName, attrs = [], definitions = [], isEntry, props = {}) {
if (!tagName) {
const depth = isEntry ? 1 : 0;
registerDependencies(elementURL, definitions, depth);
Expand All @@ -138,7 +138,11 @@ async function initializeCustomElement(elementURL, tagName, attrs = [], definiti
? customElements.get(tagName)
: (await import(pathname)).default;
const dataLoader = (await import(pathname)).getData;
const data = dataLoader ? await dataLoader() : {};
const data = props
? props
: dataLoader
? await dataLoader(props)
: {};

if (element) {
const elementInstance = new element(data); // eslint-disable-line new-cap
Expand All @@ -160,11 +164,11 @@ async function initializeCustomElement(elementURL, tagName, attrs = [], definiti
}
}

async function renderToString(elementURL, wrappingEntryTag = true) {
async function renderToString(elementURL, wrappingEntryTag = true, props = {}) {
const definitions = [];
const elementTagName = wrappingEntryTag && await getTagName(elementURL);
const isEntry = !!elementTagName;
const elementInstance = await initializeCustomElement(elementURL, undefined, undefined, definitions, isEntry);
const elementInstance = await initializeCustomElement(elementURL, undefined, undefined, definitions, isEntry, props);

const elementHtml = elementInstance.shadowRoot
? elementInstance.getInnerHTML({ includeShadowRoots: true })
Expand Down
43 changes: 43 additions & 0 deletions test/cases/constructor-props/constructor-props.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Use Case
* Run wcc against a custom element passing in constructor props.
*
* User Result
* Should return the expected HTML output based on the fetched content from constructor props.
*
* User Workspace
* src/
* index.js
*/

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 = 'Custom Element w/ constructor props';
const postId = 1;
let dom;

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

dom = new JSDOM(html);
});

describe(LABEL, function() {
it('should have a heading tag with the postId', function() {
expect(dom.window.document.querySelectorAll('h1')[0].textContent).to.equal(`Fetched Post ID: ${postId}`);
});

it('should have a second heading tag with the title', function() {
expect(dom.window.document.querySelectorAll('h2')[0].textContent).to.equal('sunt aut facere repellat provident occaecati excepturi optio reprehenderit');
});

it('should have a heading tag with the body', function() {
expect(dom.window.document.querySelectorAll('p')[0].textContent.startsWith('quia et suscipit')).to.equal(true);
});
});
});
19 changes: 19 additions & 0 deletions test/cases/constructor-props/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export default class PostPage extends HTMLElement {
constructor(postId) {
super();

this.postId = postId;
}

async connectedCallback() {
const { postId } = this;
const post = await fetch(`https://jsonplaceholder.typicode.com/posts/${postId}`).then(resp => resp.json());
const { id, title, body } = post;

this.innerHTML = `
<h1>Fetched Post ID: ${id}</h1>
<h2>${title}</h2>
<p>${body}</p>
`;
}
}
Loading