From e75658ca3bc93c9219eac66dde21c8b275e369f0 Mon Sep 17 00:00:00 2001 From: Sterling Nichols Date: Sun, 11 Oct 2020 14:58:20 -0700 Subject: [PATCH] Adding shadow DOM support. --- .gitignore | 1 + README.md | 17 ++++++++++++++++- react-to-webcomponent-test.js | 35 +++++++++++++++++++++++++++++++++++ react-to-webcomponent.js | 23 ++++++++++++++++++----- 4 files changed, 70 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index b947077..5c1bfd3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ dist/ +.idea/ diff --git a/README.md b/README.md index d3d62d0..a138335 100644 --- a/README.md +++ b/README.md @@ -98,13 +98,15 @@ npm i react-to-webcomponent ## API -`reactToWebComponent(ReactComponent, React, ReactDOM)` takes the following: +`reactToWebComponent(ReactComponent, React, ReactDOM, options)` takes the following: - `ReactComponent` - A react component that you want to convert to a Web Component. - `React` - A version of React (or [preact-compat](https://preactjs.com/guide/v10/switching-to-preact)) the component works with. - `ReactDOM` - A version of ReactDOM (or preact-compat) that the component works with. +- `options` - An optional set of parameters. +- `options.shadow` - Use shadow DOM rather than light DOM. A new class inheriting from `HTMLElement` is returned. This class can be directly passed to `customElements.define` as follows: @@ -138,6 +140,19 @@ class WebGreeting extends reactToWebComponent(Greeting, React, ReactDOM) customElements.define("web-greeting", WebGreeting); ``` +Components can also be implemented using [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM). + +```js +const WebGreeting = reactToWebComponent(Greeting, React, ReactDOM, { shadow: true }); + +customElements.define("web-greeting", WebGreeting); + +var myGreeting = new WebGreeting(); +document.body.appendChild(myGreeting); + +var shadowContent = myGreeting.shadowRoot.children[0]; +``` + ### How it works `reactToWebComponent` creates a constructor function whose prototype is a [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy). This acts as a trap for any property set on instances of the custom element. When a property is set, the proxy: diff --git a/react-to-webcomponent-test.js b/react-to-webcomponent-test.js index 98c17e7..35d001e 100644 --- a/react-to-webcomponent-test.js +++ b/react-to-webcomponent-test.js @@ -144,3 +144,38 @@ QUnit.test("works within can-stache and can-stache-bindings (propTypes are writa assert.equal(myWelcome.childNodes[0].innerHTML, "Hello, Bohdi", "can update"); }); + + +QUnit.test("works with shadow DOM `options.shadow === true`", function(assert) { + class Welcome extends React.Component { + render() { + return

Hello, { + this.props.name + }

; + } + } + Welcome.propTypes = { + user: PropTypes.string + }; + + class MyWelcome extends reactToWebComponent(Welcome, React, ReactDOM, { shadow: true }) {} + + customElements.define("my-shadow-welcome", MyWelcome); + + var fixture = document.getElementById("qunit-fixture"); + + var myWelcome = new MyWelcome(); + fixture.appendChild(myWelcome); + + assert.true(myWelcome.shadowRoot !== undefined, "shadow DOM is attached"); + + assert.equal(myWelcome.shadowRoot.children.length, 1, "able to render something in shadow DOM") + + var child = myWelcome.shadowRoot.childNodes[0]; + assert.equal(child.tagName, "H1", "renders the right tag name"); + assert.equal(child.innerHTML, "Hello, ", "renders the right content"); + + myWelcome.name = "Justin"; + child = myWelcome.shadowRoot.childNodes[0] + assert.equal(child.innerHTML, "Hello, Justin", "can update"); +}); diff --git a/react-to-webcomponent.js b/react-to-webcomponent.js index 8c98256..d9aebf6 100644 --- a/react-to-webcomponent.js +++ b/react-to-webcomponent.js @@ -20,12 +20,22 @@ var define = { } } -export default function(ReactComponent, React, ReactDOM) { + +/** + * Converts a React component into a webcomponent by wrapping it in a Proxy object. + * @param {ReactComponent} + * @param {React} + * @param {ReactDOM} + * @param {Object} options - Optional parameters + * @param {String?} options.shadow - Use shadow DOM rather than light DOM. + */ +export default function(ReactComponent, React, ReactDOM, options= {}) { var renderAddedProperties = {isConnected: "isConnected" in HTMLElement.prototype}; var rendering = false; // Create the web component "class" var WebComponent = function() { var self = Reflect.construct(HTMLElement, arguments, this.constructor); + self.attachShadow({ mode: 'open' }); return self; }; @@ -43,7 +53,7 @@ export default function(ReactComponent, React, ReactDOM) { // when any undefined property is set, create a getter/setter that re-renders set: function(target, key, value, receiver) { - if(rendering) { + if (rendering) { renderAddedProperties[key] = true; } @@ -57,10 +67,10 @@ export default function(ReactComponent, React, ReactDOM) { // makes sure the property looks writable getOwnPropertyDescriptor: function(target, key){ var own = Reflect.getOwnPropertyDescriptor(target, key); - if(own) { + if (own) { return own; } - if(key in ReactComponent.propTypes) { + if (key in ReactComponent.propTypes) { return { configurable: true, enumerable: true, writable: true, value: undefined }; } } @@ -83,7 +93,10 @@ export default function(ReactComponent, React, ReactDOM) { } }, this); rendering = true; - this[reactComponentSymbol] = ReactDOM.render(React.createElement(ReactComponent, data), this); + // Container is either shadow DOM or light DOM depending on `shadow` option. + const container = options.shadow ? this.shadowRoot : this; + // Use react to render element in container + this[reactComponentSymbol] = ReactDOM.render(React.createElement(ReactComponent, data), container); rendering = false; } };