diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..6ace206 --- /dev/null +++ b/.eslintrc @@ -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" + ] +} 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/.npmrc b/.npmrc new file mode 100644 index 0000000..43c97e7 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..04c1fba --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: node_js +node_js: node +addons: + firefox: latest +dist: xenial +services: + - xvfb +script: npm run ci 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/build.js b/build.js index 653e668..288fca1 100644 --- a/build.js +++ b/build.js @@ -1,4 +1,3 @@ - var stealTools = require("steal-tools"); var globalJS = require("steal-tools/lib/build/helpers/global").js; diff --git a/package.json b/package.json index fdaafce..375929c 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,13 @@ "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", @@ -15,16 +20,23 @@ "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", diff --git a/react-to-webcomponent-test.js b/react-to-webcomponent-test.js index 98c17e7..3f4fd9b 100644 --- a/react-to-webcomponent-test.js +++ b/react-to-webcomponent-test.js @@ -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); @@ -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

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..7e913f1 100644 --- a/react-to-webcomponent.js +++ b/react-to-webcomponent.js @@ -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; }; @@ -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; } @@ -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 }; } } @@ -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; } };