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

bug/issue 128 fix shadow root rendering for JSX #129

Merged
merged 11 commits into from
Jan 6, 2024
32 changes: 29 additions & 3 deletions docs/pages/docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ export default class Counter extends HTMLElement {
}

connectedCallback() {
this.render();
this.render(); // this is required
}

increment() {
Expand Down Expand Up @@ -289,6 +289,32 @@ There are of couple things you will need to do to use WCC with JSX:

> _See our [example's page](/examples#jsx) for some usages of WCC + JSX._ 👀

### Declarative Shadow DOM

To opt-in to Declarative Shadow DOM with JSX, you will need to signal to the WCC compiler your intentions so it can accurately mount from a `shadowRoot` on the client side. To opt-in, simply make a call to `attachShadow` in your `connectedCallback` method.

Using, the Counter example from above, we would amend it like so:

```js
export default class Counter extends HTMLElement {
constructor() {
super();
this.count = 0;
}

connectedCallback() {
if (!this.shadowRoot) {
this.attachShadow({ mode: 'open' }); // this is required for DSD support
this.render();
}
}

// ...
}

customElements.define('wcc-counter', Counter);
```

### (Inferred) Attribute Observability

An optional feature supported by JSX based compilation is a feature called `inferredObservability`. With this enabled, WCC will read any `this` member references in your component's `render` function and map each member instance to
Expand Down Expand Up @@ -322,6 +348,6 @@ And so now when the attribute is set on this component, the component will re-re
```

Some notes / limitations:
- Please be aware of the above linked discussion which is tracking known bugs / feature requests to all things WCC + JSX.
- Please be aware of the above linked discussion which is tracking known bugs / feature requests / open items related to all things WCC + JSX.
- We consider the capability of this observability to be "coarse grained" at this time since WCC just re-runs the entire `render` function, replacing of the `innerHTML` for the host component. Thought it is still WIP, we are exploring a more ["fine grained" approach](https://github.com/ProjectEvergreen/wcc/issues/108) that will more efficient than blowing away all the HTML, a la in the style of [**lit-html**](https://lit.dev/docs/templates/overview/) or [**Solid**'s Signals](https://www.solidjs.com/tutorial/introduction_signals).
- This automatically _reflects properties used in the `render` function to attributes_, so YMMV.
- This automatically _reflects properties used in the `render` function to attributes_, so YMMV.
2 changes: 1 addition & 1 deletion sandbox/components/card.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export default class Card extends HTMLElement {

selectItem() {
alert(`selected item is => ${this.getAttribute('title')}!`);
alert(`selected item is => ${this.title}!`);
}

connectedCallback() {
Expand Down
6 changes: 3 additions & 3 deletions sandbox/components/card.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ const styles = `
export default class CardJsx extends HTMLElement {

selectItem() {
alert(`selected item is => ${this.getAttribute('title')}!`);
alert(`selected item is => ${this.title}!`);
}

connectedCallback() {
if (!this.shadowRoot) {
console.log('NO shadowRoot detected for card.jsx!');
console.warn('NO shadowRoot detected for card.jsx!');
this.thumbnail = this.getAttribute('thumbnail');
this.title = this.getAttribute('title');

Expand All @@ -39,7 +39,7 @@ export default class CardJsx extends HTMLElement {
const { thumbnail, title } = this;

return (
<div>
<div class="card">
<style>
{styles}
</style>
Expand Down
10 changes: 2 additions & 8 deletions sandbox/components/counter-dsd.jsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
export const inferredObservability = true;

export default class CounterDsdJsx extends HTMLElement {
// having a constructor is required for inferredObservability
constructor() {
super();
this.count = 0;
}

connectedCallback() {
if (!this.shadowRoot) {
console.log('NO shadowRoot detected for counter-dsd.jsx!');
this.count = this.getAttribute('count');
console.warn('NO shadowRoot detected for counter-dsd.jsx!');
this.count = this.getAttribute('count') || 0;

// having an attachShadow call is required for DSD
this.attachShadow({ mode: 'open' });
Expand Down
2 changes: 1 addition & 1 deletion sandbox/components/counter.jsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
export const inferredObservability = true;

export default class CounterJsx extends HTMLElement {
// having a constructor is required for inferredObservability
constructor() {
super();
this.count = 0;
}

connectedCallback() {
this.count = parseInt(this.getAttribute('count'), 10) || this.count;
this.render();
}

Expand Down
49 changes: 34 additions & 15 deletions sandbox/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,33 @@
margin: 0 auto;
text-align: center;
}

button.reset {
display: block;
min-width: 5%;
margin: 0 auto;
}
</style>

<script>
function randomReset() {
return Math.floor(Math.random() * 100);
}

globalThis.document.addEventListener('DOMContentLoaded', () => {
const counterJsxResetButton = document.getElementById('counter-jsx-reset');
const counterJsxDsdResetButton = document.getElementById('counter-jsx-dsd-reset');

counterJsxResetButton.addEventListener('click', () => {
document.querySelector('sb-counter-jsx').setAttribute('count', randomReset());
});

counterJsxDsdResetButton.addEventListener('click', () => {
document.querySelector('sb-counter-dsd-jsx').setAttribute('count', randomReset());
});
});
</script>

<script>
document.write('<script src="http://' + (location.host || 'localhost').split(':')[0] +
':35729/livereload.js?snipver=1"></' + 'script>')
Expand Down Expand Up @@ -68,21 +93,16 @@ <h2>JSX + Light DOM (no JS)</h2>

<hr/>

<!-- TODO - https://github.com/ProjectEvergreen/wcc/issues/128 -->
<h2>JSX + Declarative Shadow DOM (has JS) 🚫</h2>

<details>
SSR Shadow DOM (and thus host styles) not working, see - https://github.com/ProjectEvergreen/wcc/issues/128
</details>
<h2>JSX + Declarative Shadow DOM (has JS)</h2>

<sb-card-jsx
title="iPhone 9"
thumbnail="https://i.dummyjson.com/data/products/1/thumbnail.jpg"
title="iPhone X"
thumbnail="https://i.dummyjson.com/data/products/2/thumbnail.jpg"
></sb-card-jsx>

<pre>
&lt;sb-card-jsx
title="iPhone 9" thumbnail="https://i.dummyjson.com/data/products/1/thumbnail.jpg"
title="iPhone X" thumbnail="https://i.dummyjson.com/data/products/2/thumbnail.jpg"
&gt;&lt;/sb-card-jsx&gt;
</pre>

Expand All @@ -94,6 +114,8 @@ <h2>JSX + Light DOM + inferredObservability (has JS)</h2>
count="5"
></sb-counter-jsx>

<button class="reset" id="counter-jsx-reset">Random Reset</button>

<pre>
&lt;sb-counter-jsx
count="5"
Expand All @@ -102,17 +124,14 @@ <h2>JSX + Light DOM + inferredObservability (has JS)</h2>

<hr/>

<!-- TODO - https://github.com/ProjectEvergreen/wcc/issues/128 -->
<h2>JSX + DSD + inferredObservability (has JS) 🚫</h2>

<details>
SSR Shadow DOM and inferredObservability not working, see - https://github.com/ProjectEvergreen/wcc/issues/128
</details>
<h2>JSX + DSD + inferredObservability (has JS)</h2>

<sb-counter-dsd-jsx
count="3"
></sb-counter-dsd-jsx>

<button class="reset" id="counter-jsx-dsd-reset">Random Reset</button>

<pre>
&lt;sb-counter-dsd-jsx
count="3"
Expand Down
32 changes: 21 additions & 11 deletions src/jsx-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@

const jsxRegex = /\.(jsx)$/;

// TODO same hack as definitions

Check warning on line 12 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected 'todo' comment: 'TODO same hack as definitions'

Check warning on line 12 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected ' TODO' comment: 'TODO same hack as definitions'

Check warning on line 12 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (20)

Unexpected 'todo' comment: 'TODO same hack as definitions'

Check warning on line 12 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (20)

Unexpected ' TODO' comment: 'TODO same hack as definitions'

Check warning on line 12 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (20)

Unexpected 'todo' comment: 'TODO same hack as definitions'

Check warning on line 12 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (20)

Unexpected ' TODO' comment: 'TODO same hack as definitions'

Check warning on line 12 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected 'todo' comment: 'TODO same hack as definitions'

Check warning on line 12 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected ' TODO' comment: 'TODO same hack as definitions'

Check warning on line 12 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (20)

Unexpected 'todo' comment: 'TODO same hack as definitions'

Check warning on line 12 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (20)

Unexpected ' TODO' comment: 'TODO same hack as definitions'

Check warning on line 12 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected 'todo' comment: 'TODO same hack as definitions'

Check warning on line 12 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected ' TODO' comment: 'TODO same hack as definitions'

Check warning on line 12 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected 'todo' comment: 'TODO same hack as definitions'

Check warning on line 12 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected ' TODO' comment: 'TODO same hack as definitions'

Check warning on line 12 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (20)

Unexpected 'todo' comment: 'TODO same hack as definitions'

Check warning on line 12 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (20)

Unexpected ' TODO' comment: 'TODO same hack as definitions'
// https://github.com/ProjectEvergreen/wcc/discussions/74
let string;

// TODO move to a util

Check warning on line 16 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected 'todo' comment: 'TODO move to a util'

Check warning on line 16 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected ' TODO' comment: 'TODO move to a util'

Check warning on line 16 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (20)

Unexpected 'todo' comment: 'TODO move to a util'

Check warning on line 16 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (20)

Unexpected ' TODO' comment: 'TODO move to a util'

Check warning on line 16 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (20)

Unexpected 'todo' comment: 'TODO move to a util'

Check warning on line 16 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (20)

Unexpected ' TODO' comment: 'TODO move to a util'

Check warning on line 16 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected 'todo' comment: 'TODO move to a util'

Check warning on line 16 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected ' TODO' comment: 'TODO move to a util'

Check warning on line 16 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (20)

Unexpected 'todo' comment: 'TODO move to a util'

Check warning on line 16 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (20)

Unexpected ' TODO' comment: 'TODO move to a util'

Check warning on line 16 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected 'todo' comment: 'TODO move to a util'

Check warning on line 16 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected ' TODO' comment: 'TODO move to a util'

Check warning on line 16 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected 'todo' comment: 'TODO move to a util'

Check warning on line 16 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected ' TODO' comment: 'TODO move to a util'

Check warning on line 16 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (20)

Unexpected 'todo' comment: 'TODO move to a util'

Check warning on line 16 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (20)

Unexpected ' TODO' comment: 'TODO move to a util'
// https://github.com/ProjectEvergreen/wcc/discussions/74
function getParse(html) {
return html.indexOf('<html>') >= 0 || html.indexOf('<body>') >= 0 || html.indexOf('<head>') >= 0
Expand Down Expand Up @@ -100,7 +100,7 @@
}

// onclick={() => this.deleteUser(user.id)}
// TODO onclick={(e) => { this.deleteUser(user.id) }}

Check warning on line 103 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected 'todo' comment: 'TODO onclick={(e) => {...'

Check warning on line 103 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected ' TODO' comment: 'TODO onclick={(e) => {...'

Check warning on line 103 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (20)

Unexpected 'todo' comment: 'TODO onclick={(e) => {...'

Check warning on line 103 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (20)

Unexpected ' TODO' comment: 'TODO onclick={(e) => {...'

Check warning on line 103 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (20)

Unexpected 'todo' comment: 'TODO onclick={(e) => {...'

Check warning on line 103 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (20)

Unexpected ' TODO' comment: 'TODO onclick={(e) => {...'

Check warning on line 103 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected 'todo' comment: 'TODO onclick={(e) => {...'

Check warning on line 103 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected ' TODO' comment: 'TODO onclick={(e) => {...'

Check warning on line 103 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (20)

Unexpected 'todo' comment: 'TODO onclick={(e) => {...'

Check warning on line 103 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (20)

Unexpected ' TODO' comment: 'TODO onclick={(e) => {...'

Check warning on line 103 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected 'todo' comment: 'TODO onclick={(e) => {...'

Check warning on line 103 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected ' TODO' comment: 'TODO onclick={(e) => {...'

Check warning on line 103 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected 'todo' comment: 'TODO onclick={(e) => {...'

Check warning on line 103 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (18)

Unexpected ' TODO' comment: 'TODO onclick={(e) => {...'

Check warning on line 103 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (20)

Unexpected 'todo' comment: 'TODO onclick={(e) => {...'

Check warning on line 103 in src/jsx-loader.js

View workflow job for this annotation

GitHub Actions / build (20)

Unexpected ' TODO' comment: 'TODO onclick={(e) => {...'
// TODO onclick={(e) => { this.deleteUser(user.id) && this.logAction(user.id) }}
// https://github.com/ProjectEvergreen/wcc/issues/88
if (expression.type === 'ArrowFunctionExpression') {
Expand Down Expand Up @@ -265,8 +265,26 @@

applyDomDepthSubstitutions(elementTree, undefined, hasShadowRoot);

const finalHtml = serialize(elementTree);
const transformed = acorn.parse(`${elementRoot}.innerHTML = \`${finalHtml}\`;`, {
const serializedHtml = serialize(elementTree);
// we have to Shadow DOM use cases here
// 1. No shadowRoot, so we attachShadow and append the template
// 2. If there is root from the attachShadow signal, so we just need to inject innerHTML, say in an htmx
// could / should we do something else instead of .innerHTML
// https://github.com/ProjectEvergreen/wcc/issues/138
const renderHandler = hasShadowRoot
? `
const template = document.createElement('template');
template.innerHTML = \`${serializedHtml}\`;

if(!${elementRoot}) {
this.attachShadow({ mode: 'open' });
this.shadowRoot.appendChild(template.content.cloneNode(true));
} else {
this.shadowRoot.innerHTML = template.innerHTML;
}
`
: `${elementRoot}.innerHTML = \`${serializedHtml}\`;`;
const transformed = acorn.parse(renderHandler, {
ecmaVersion: 'latest',
sourceType: 'module'
});
Expand Down Expand Up @@ -300,15 +318,7 @@
for (const line of tree.body) {
// test for class MyComponent vs export default class MyComponent
if (line.type === 'ClassDeclaration' || (line.declaration && line.declaration.type) === 'ClassDeclaration') {
const children = !line.declaration
? line.body.body
: line.declaration.body.body;
for (const method of children) {
if (method.key.name === 'constructor') {
insertPoint = method.start - 1;
break;
}
}
insertPoint = line.declaration.body.start + 1;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { renderToString } from '../../../src/wcc.js';
const expect = chai.expect;

describe('Run WCC For ', function() {
const LABEL = 'Single Custom Element using JSX';
const LABEL = 'Single Custom Element using JSX and Inferred Observability';
let fixtureAttributeChangedCallback;
let fixtureGetObservedAttributes;
let meta;
Expand Down
71 changes: 71 additions & 0 deletions test/cases/jsx-shadow-dom/jsx-shadow-dom.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Use Case
* Run wcc against a nested custom elements using JSX render function and Declarative Shadow DOM.
*
* User Result
* Should return the expected HTML and JavaScript output.
*
* User Workspace
* src/
* heading.jsx
*/
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 = 'Single Custom Element using JSX and Declarative Shadow DOM';
let dom;
let meta;

before(async function() {
const { html, metadata } = await renderToString(new URL('./src/heading.jsx', import.meta.url));

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

describe(LABEL, function() {

describe('<wcc-heading> component', function() {
let heading;

before(async function() {
heading = dom.window.document.querySelector('wcc-heading template[shadowrootmode="open"]');
});

describe('Metadata', () => {
it('should return a JSX definition in metadata', () => {
expect(Object.keys(meta).length).to.equal(1);
expect(meta['wcc-heading'].source).to.not.be.undefined;
});
});

describe('Declarative Shadow DOM (<template> tag)', () => {
it('should handle a this expression', () => {
expect(heading).to.not.be.undefined;
});
});

describe('Event Handling', () => {
it('should handle a this expression', () => {
const wrapper = new JSDOM(heading.innerHTML);
const button = wrapper.window.document.querySelector('button');

expect(button.getAttribute('onclick')).to.be.equal('this.parentElement.parentNode.host.sayHello()');
});
});

describe('Attribute Contents', () => {
it('should handle a this expression', () => {
const wrapper = new JSDOM(heading.innerHTML);
const header = wrapper.window.document.querySelector('h1');

expect(header.textContent).to.be.equal('Hello, World!');
});
});
});
});
});
27 changes: 27 additions & 0 deletions test/cases/jsx-shadow-dom/src/heading.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export default class HeadingComponent extends HTMLElement {
sayHello() {
alert(`Hello, ${this.greeting}!`);
}

connectedCallback() {
if (!this.shadowRoot) {
this.greeting = this.getAttribute('greeting') || 'World';

this.attachShadow({ mode: 'open' });
this.render();
}
}

render() {
const { greeting } = this;

return (
<div>
<h1>Hello, {greeting}!</h1>
<button onclick={this.sayHello}>Get a greeting!</button>
</div>
);
}
}

customElements.define('wcc-heading', HeadingComponent);
Loading