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
18 changes: 18 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"env": {
"es6": true,
"browser": true,
"node": true
},
"extends": "eslint:recommended",
"parser": "babel-eslint",
"parserOptions": {
"ecmaFeatures": {
"jsx": true,
"experimentalObjectRestSpread": true
}
},
"plugins": [
"react"
]
}
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/
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package-lock=false
8 changes: 8 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
language: node_js
node_js: node
addons:
firefox: latest
dist: xenial
services:
- xvfb
script: npm run ci
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
1 change: 0 additions & 1 deletion build.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

var stealTools = require("steal-tools");

var globalJS = require("steal-tools/lib/build/helpers/global").js;
Expand Down
16 changes: 14 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,37 @@
"dependencies": {},
"devDependencies": {
"@webcomponents/custom-elements": "^1.2.4",
"babel-eslint": "^10.1.0",
"can-stache": "^4.17.20",
"can-stache-bindings": "^4.10.9",
"detect-cyclic-packages": "^1.1.0",
"eslint": "^7.11.0",
"eslint-plugin-react": "^7.21.4",
"http-server": "^0.11.1",
"preact": "^8.5.2",
"preact-compat": "^3.19.0",
"prop-types": "^15.7.2",
"react": "^16.9.0",
"react-dom": "^16.9.0",
"steal": "^2.2.2",
"steal-qunit": "^2.0.0",
"steal-tools": "^2.2.2"
"steal-tools": "^2.2.2",
"test-saucelabs": "0.0.6",
"testee": "^0.9.0"
},
"scripts": {
"build": "node build.js",
"ci": "npm run test",
"preversion": "npm test && npm run build",
"version": "git commit -am \"Update version number\" && git checkout -b release && git add -f dist/",
"postpublish": "git push --tags && git checkout master && git branch -D release && git push origin master",
"release:patch": "npm version patch && npm publish",
"release:minor": "npm version minor && npm publish",
"release:major": "npm version major && npm publish"
"release:major": "npm version major && npm publish",
"test": "npm run detect-cycle && npm run eslint && npm run testee",
"detect-cycle": "detect-cyclic-packages",
"eslint": "eslint ./*.js",
"testee": "testee test.html --browsers firefox"
},
"repository": {
"type": "git",
Expand Down
37 changes: 35 additions & 2 deletions react-to-webcomponent-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,8 @@ QUnit.test("works with attributes set with propTypes", function(assert) {

var myGreeting = new MyGreeting();

var oldError = console.error;
console.error = function(message) {
assert.ok(message.includes("required"), "got a warning with required");
oldError = console.error;
}
fixture.appendChild(myGreeting);

Expand Down Expand Up @@ -144,3 +142,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");
});
24 changes: 18 additions & 6 deletions react-to-webcomponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,28 @@ var define = {
set: function(newValue) {
value = newValue;
this[renderSymbol]();
return true;
}
});
receiver[renderSymbol]();
}
}

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

Expand All @@ -43,7 +52,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 +66,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 +92,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