Skip to content
Closed
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 .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules/
dist/
.idea/
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
35 changes: 35 additions & 0 deletions react-to-webcomponent-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <h1>Hello, {
this.props.name
}</h1>;
}
}
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");
});
23 changes: 18 additions & 5 deletions react-to-webcomponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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= {}) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is options = {} valid in every browser that supports Proxy? If not, we might need to write this out "old school". I don't think we are currently transpiling this file.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.
The only known* difference according to caniuse is Edge 12 and 13 supporting default function parameters and not supporting Proxy.
https://caniuse.com/mdn-javascript_functions_default_parameters
https://caniuse.com/proxy

| * (some browsers are listed in the source docs with unknown feature sets)

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;
};

Expand All @@ -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;
}

Expand All @@ -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 };
}
}
Expand All @@ -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;
}
};
Expand Down