From e75658ca3bc93c9219eac66dde21c8b275e369f0 Mon Sep 17 00:00:00 2001 From: Sterling Nichols Date: Sun, 11 Oct 2020 14:58:20 -0700 Subject: [PATCH 1/5] 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; } }; From d7dc9efbab7a7635564c8108882e9e8e846b357a Mon Sep 17 00:00:00 2001 From: Cherif BOUCHELAGHEM Date: Tue, 13 Oct 2020 20:33:48 +0100 Subject: [PATCH 2/5] Add eslint check, Travis CI and couple of fixes to make ESLINT happy --- .eslintrc | 18 ++++++++++++++++++ .npmrc | 1 + .travis.yml | 12 ++++++++++++ build.js | 1 - package.json | 15 +++++++++++++-- react-to-webcomponent-test.js | 2 -- react-to-webcomponent.js | 1 - 7 files changed, 44 insertions(+), 6 deletions(-) create mode 100644 .eslintrc create mode 100644 .npmrc create mode 100644 .travis.yml 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/.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..1a5ed53 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: node_js +node_js: node +addons: + firefox: latest + sauce_connect: true +dist: xenial +services: + - xvfb +before_script: + - npm run http-server & + - sleep 2 +script: npm run ci 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..968cb57 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,7 +20,9 @@ "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", @@ -24,7 +31,11 @@ "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 35d001e..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); diff --git a/react-to-webcomponent.js b/react-to-webcomponent.js index d9aebf6..7e913f1 100644 --- a/react-to-webcomponent.js +++ b/react-to-webcomponent.js @@ -13,7 +13,6 @@ var define = { set: function(newValue) { value = newValue; this[renderSymbol](); - return true; } }); receiver[renderSymbol](); From 0e080a975dbf7d5d138a22567e45ddb41e15bb7a Mon Sep 17 00:00:00 2001 From: Cherif BOUCHELAGHEM Date: Tue, 13 Oct 2020 20:45:40 +0100 Subject: [PATCH 3/5] Fix CI script --- .travis.yml | 4 ---- package.json | 3 ++- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1a5ed53..04c1fba 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,11 +2,7 @@ language: node_js node_js: node addons: firefox: latest - sauce_connect: true dist: xenial services: - xvfb -before_script: - - npm run http-server & - - sleep 2 script: npm run ci diff --git a/package.json b/package.json index 968cb57..db7f7c6 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "testee": "^0.9.0" }, "scripts": { - "build": "node build.js", + "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", From 45173700162486739d0b48502449826ab8a40e45 Mon Sep 17 00:00:00 2001 From: Cherif BOUCHELAGHEM Date: Tue, 13 Oct 2020 20:48:36 +0100 Subject: [PATCH 4/5] Fix indentation --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index db7f7c6..375929c 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,8 @@ "testee": "^0.9.0" }, "scripts": { - "build": "node build.js", - "ci": "npm run test", + "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", From cf33735ceaf5f17e0bb4777f73a08a8e003e3696 Mon Sep 17 00:00:00 2001 From: Cherif BOUCHELAGHEM Date: Fri, 16 Oct 2020 15:29:53 +0100 Subject: [PATCH 5/5] Trigger Build