From 76390e485756f37a1d356d739641743d76f6d5e3 Mon Sep 17 00:00:00 2001 From: Ray Nicholus Date: Fri, 23 Dec 2016 23:58:29 -0600 Subject: [PATCH 01/14] support for CE tag name instead of constructor --- src/index.js | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/src/index.js b/src/index.js index f5ea9c4..78d7c4e 100644 --- a/src/index.js +++ b/src/index.js @@ -26,12 +26,20 @@ function syncEvent(node, eventName, newEventHandler) { } } -export default function (CustomElement, opts) { +export default function (CustomElementOrTagName, opts) { opts = assign({}, defaults, opts); - if (typeof CustomElement !== 'function') { - throw new Error('Given element is not a valid constructor'); + if (typeof CustomElementOrTagName !== 'function' && typeof CustomElementOrTagName !== 'string') { + throw new Error('Given element is not a valid constructor or tag name'); } - const tagName = (new CustomElement()).tagName; + + let CustomElementConstructor = CustomElementOrTagName + if (typeof CustomElementOrTagName === 'string') { + CustomElementConstructor = document.createElement(CustomElementOrTagName).constructor + } + + const tagName = typeof CustomElementOrTagName === 'string' + ? CustomElementOrTagName + : (new CustomElementOrTagName()).tagName; const displayName = pascalCase(tagName); const { React, ReactDOM } = opts; @@ -50,25 +58,27 @@ export default function (CustomElement, opts) { const node = ReactDOM.findDOMNode(this); Object.keys(props).forEach(name => { if (name === 'children' || name === 'style') { - return; - } + return; + } - if (name.indexOf('on') === 0 && name[2] === name[2].toUpperCase()) { - syncEvent(node, name.substring(2), props[name]); - } else { - node[name] = props[name]; - } - }); + if (name.indexOf('on') === 0 && name[2] === name[2].toUpperCase()) { + syncEvent(node, name.substring(2), props[name]); + } else { + node[name] = props[name]; + } + }); } render() { return React.createElement(tagName, { style: this.props.style }, this.props.children); } } - const proto = CustomElement.prototype; + const proto = typeof CustomElementOrTagName === 'string' + ? document.createElement(tagName).constructor.prototype + : CustomElementOrTagName.prototype; Object.getOwnPropertyNames(proto).forEach(prop => { Object.defineProperty(ReactComponent.prototype, prop, Object.getOwnPropertyDescriptor(proto, prop)); - }); +}); return ReactComponent; } From 9519f449ca300f1d712f71acd121c22369f656b8 Mon Sep 17 00:00:00 2001 From: Ray Nicholus Date: Sat, 24 Dec 2016 00:08:50 -0600 Subject: [PATCH 02/14] tests won't run with anything older than node 6 --- package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 5f3a012..2063e6d 100644 --- a/package.json +++ b/package.json @@ -50,5 +50,8 @@ "dist", "src" ], - "version": "2.0.2" + "version": "2.0.2", + "engines": { + "node": ">=6.0.0" + } } From 194096db591bcdff19bd8e3988fae74d34d80d45 Mon Sep 17 00:00:00 2001 From: Ray Nicholus Date: Sat, 24 Dec 2016 21:57:12 -0600 Subject: [PATCH 03/14] allow attributes to be attached to the CE --- src/index.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/index.js b/src/index.js index 78d7c4e..81a61c6 100644 --- a/src/index.js +++ b/src/index.js @@ -63,6 +63,12 @@ export default function (CustomElementOrTagName, opts) { if (name.indexOf('on') === 0 && name[2] === name[2].toUpperCase()) { syncEvent(node, name.substring(2), props[name]); + } else if (name.indexOf('attr-') === 0) { + let attrValue = props[name] + if (typeof attrValue === 'object') { + attrValue = JSON.stringify(attrValue) + } + node.setAttribute(name.substring(5), attrValue) } else { node[name] = props[name]; } From 42aa5ab3d267e99d63c24329fcdd32031c898f58 Mon Sep 17 00:00:00 2001 From: Ray Nicholus Date: Sat, 24 Dec 2016 22:04:18 -0600 Subject: [PATCH 04/14] allow "is" prop to be attached on render for type extension CEs --- src/index.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 81a61c6..5c5ed44 100644 --- a/src/index.js +++ b/src/index.js @@ -75,7 +75,14 @@ export default function (CustomElementOrTagName, opts) { }); } render() { - return React.createElement(tagName, { style: this.props.style }, this.props.children); + return React.createElement( + tagName, + { + is: this.props.is, + style: this.props.style + }, + this.props.children + ); } } From 9e2d77067dc1b56da72eee2c5f88aab8ba061745 Mon Sep 17 00:00:00 2001 From: Ray Nicholus Date: Sat, 24 Dec 2016 22:06:18 -0600 Subject: [PATCH 05/14] typescript: first param for exported function is now a union type (CE constructor or a string/tag-name) --- src/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.d.ts b/src/index.d.ts index 49ab457..d62cf1b 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -3,4 +3,4 @@ import * as React from 'react'; type CustomElementCtor = { new(...args: any[]): HTMLElement }; type ReactComponentCtor = { new(...args: any[]): React.Component }; type Options = { React?: any, ReactDOM?: any }; -export default function (CustomElement: CustomElementCtor, opts: Options = {}): ReactComponentCtor; +export default function (CustomElementOrTagName: CustomElementCtor | string, opts: Options = {}): ReactComponentCtor; From 82f6734a8bfe439f89618a19bf576c6773fd5555 Mon Sep 17 00:00:00 2001 From: Ray Nicholus Date: Sat, 24 Dec 2016 23:11:50 -0600 Subject: [PATCH 06/14] more unit tests --- src/index.js | 2 +- test/unit.js | 1 + test/unit/attributes.js | 38 ++++++++++++++ test/unit/children.js | 30 +++++++++-- test/unit/display-name.js | 10 ++++ test/unit/webcomponent-proto-funcs.js | 75 +++++++++++++++++++-------- 6 files changed, 130 insertions(+), 26 deletions(-) create mode 100644 test/unit/attributes.js diff --git a/src/index.js b/src/index.js index 5c5ed44..63b5549 100644 --- a/src/index.js +++ b/src/index.js @@ -64,7 +64,7 @@ export default function (CustomElementOrTagName, opts) { if (name.indexOf('on') === 0 && name[2] === name[2].toUpperCase()) { syncEvent(node, name.substring(2), props[name]); } else if (name.indexOf('attr-') === 0) { - let attrValue = props[name] + let attrValue = props[name]; if (typeof attrValue === 'object') { attrValue = JSON.stringify(attrValue) } diff --git a/test/unit.js b/test/unit.js index da475f0..1135fe4 100644 --- a/test/unit.js +++ b/test/unit.js @@ -1,3 +1,4 @@ +import './unit/attributes'; import './unit/children'; import './unit/events'; import './unit/display-name'; diff --git a/test/unit/attributes.js b/test/unit/attributes.js new file mode 100644 index 0000000..7835b5e --- /dev/null +++ b/test/unit/attributes.js @@ -0,0 +1,38 @@ +import reactify from '../../src/index'; +import React from 'react'; +import ReactDOM from 'react-dom'; + +let x = 0; + +function createComponent() { + const tagName = `x-attributes-${x++}`; + + return { + constructor: document.registerElement(tagName, { + prototype: Object.create(HTMLElement.prototype), + }), + tagName: tagName + }; +} + +function getReactifiedComponentByConstructor() { + return reactify(createComponent().constructor); +} + +function getReactifiedComponentByTagName() { + return reactify(createComponent().tagName); +} + +describe('attributes', () => { + it('should pass on properties that start with "attr-" as attributes', () => { + const Comp = getReactifiedComponentByConstructor(); + const comp = ReactDOM.render(, window.fixture); + expect(ReactDOM.findDOMNode(comp).getAttribute('data-test')).to.equal('test-data'); + }); + + it('should pass on properties that start with "attr-" as attributes using tagName based reactification', () => { + const Comp = getReactifiedComponentByTagName(); + const comp = ReactDOM.render(, window.fixture); + expect(ReactDOM.findDOMNode(comp).getAttribute('data-test')).to.equal('test-data'); + }); +}); diff --git a/test/unit/children.js b/test/unit/children.js index 4b7b61e..332b2fc 100644 --- a/test/unit/children.js +++ b/test/unit/children.js @@ -3,16 +3,38 @@ import React from 'react'; import ReactDOM from 'react-dom'; let x = 0; + function createComponent() { - return reactify(document.registerElement(`x-children-${x++}`, { - prototype: Object.create(HTMLElement.prototype), - }), { React, ReactDOM }); + const tagName = `x-children-${x++}`; + + return { + constructor: document.registerElement(tagName, { + prototype: Object.create(HTMLElement.prototype), + }), + tagName: tagName + }; +} + +function getReactifiedComponentByConstructor() { + return reactify(createComponent().constructor, { React, ReactDOM }); +} + +function getReactifiedComponentByTagName() { + return reactify(createComponent().tagName, { React, ReactDOM }); } describe('children', () => { it('should pass on children', () => { - const Comp = createComponent(); + const Comp = getReactifiedComponentByConstructor(); + const comp = ReactDOM.render(, window.fixture); + expect(ReactDOM.findDOMNode(comp).tagName).to.match(/^X-CHILDREN/); + expect(ReactDOM.findDOMNode(comp).firstChild.tagName).to.equal('CHILD'); + }); + + it('should pass on children using tagName based reactification', () => { + const Comp = getReactifiedComponentByTagName(); const comp = ReactDOM.render(, window.fixture); + expect(ReactDOM.findDOMNode(comp).tagName).to.match(/^X-CHILDREN/); expect(ReactDOM.findDOMNode(comp).firstChild.tagName).to.equal('CHILD'); }); }); diff --git a/test/unit/display-name.js b/test/unit/display-name.js index eca1540..fee99d7 100644 --- a/test/unit/display-name.js +++ b/test/unit/display-name.js @@ -10,4 +10,14 @@ describe('display-name', () => { ReactDOM.render(React.createElement(Comp), window.fixture); expect(Comp.displayName).to.equal('XDisplayName_1'); }); + + it('should be a PasalCased version of the tagName when reactifying using CE tagName', () => { + document.registerElement('x-display-name-2', { + prototype: Object.create(HTMLElement.prototype), + }); + + const Comp = reactify('x-display-name-2', { React, ReactDOM }); + ReactDOM.render(React.createElement(Comp), window.fixture); + expect(Comp.displayName).to.equal('XDisplayName_2'); + }); }); diff --git a/test/unit/webcomponent-proto-funcs.js b/test/unit/webcomponent-proto-funcs.js index 532bf29..272014b 100644 --- a/test/unit/webcomponent-proto-funcs.js +++ b/test/unit/webcomponent-proto-funcs.js @@ -2,44 +2,77 @@ import reactify from '../../src/index'; let x = 0; function createComponent() { - return reactify(document.registerElement(`x-webcomponent-proto-funcs-${x++}`, { - prototype: Object.create(HTMLElement.prototype, { - prop: { - value: 'prop', - }, - foo: { - value() { - return 'bar'; + const tagName = `x-webcomponent-proto-funcs-${x++}`; + return { + constructor: document.registerElement(tagName, { + prototype: Object.create(HTMLElement.prototype, { + prop: { + value: 'prop', }, - }, - getProp: { - value() { - return this.prop; + foo: { + value() { + return 'bar'; + }, }, - }, - getter: { - get() { - throw new Error('should not throw when reactifying'); + getProp: { + value() { + return this.prop; + }, }, - }, + getter: { + get() { + throw new Error('should not throw when reactifying'); + }, + }, + }), }), - })); + tagName: tagName + }; +} + +function getReactifiedComponentByConstructor() { + return reactify(createComponent().constructor); } +function getReactifiedComponentByTagName() { + return reactify(createComponent().tagName); +} + + describe('Webcomponent prototype functions', () => { it('should be callable on React component', () => { - const Comp = createComponent(); + const Comp = getReactifiedComponentByConstructor(); + expect(Comp.prototype.foo()).to.equal('bar'); + }); + + it('should return prop', () => { + const Comp = getReactifiedComponentByConstructor(); + expect(Comp.prototype.getProp()).to.equal('prop'); + }); + + it('should not invoke getters', () => { + // If this functionality fails, calling createComponent() should cause the error to be thrown. + const Comp = getReactifiedComponentByConstructor(); + + // We expect it to throw here to make sure we've written our test correctly. + expect(() => Comp.prototype.getter).to.throw(); + }); +}); + +describe('Webcomponent prototype functions - reactified with tagName', () => { + it('should be callable on React component', () => { + const Comp = getReactifiedComponentByTagName(); expect(Comp.prototype.foo()).to.equal('bar'); }); it('should return prop', () => { - const Comp = createComponent(); + const Comp = getReactifiedComponentByTagName(); expect(Comp.prototype.getProp()).to.equal('prop'); }); it('should not invoke getters', () => { // If this functionality fails, calling createComponent() should cause the error to be thrown. - const Comp = createComponent(); + const Comp = getReactifiedComponentByTagName(); // We expect it to throw here to make sure we've written our test correctly. expect(() => Comp.prototype.getter).to.throw(); From bd5e3c70d20c03811dd0406cf33e7e19602e71dd Mon Sep 17 00:00:00 2001 From: Ray Nicholus Date: Sat, 24 Dec 2016 23:13:44 -0600 Subject: [PATCH 07/14] attr- & tagName support should be part of 2.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2063e6d..e99c04b 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "dist", "src" ], - "version": "2.0.2", + "version": "2.1.0", "engines": { "node": ">=6.0.0" } From 270e646b9a7b2bba1ef6e5a802630d363440d1f8 Mon Sep 17 00:00:00 2001 From: Ray Nicholus Date: Sun, 25 Dec 2016 22:50:35 -0600 Subject: [PATCH 08/14] document HTML import, attribute, and type-extension support --- README.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/README.md b/README.md index 3c9fc4f..282b2ed 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ Converts web components into React components so that you can use them as first - Listen for custom events triggered from web components declaratively using the standard `on*` syntax. - Passes React `props` to web components as properties instead of attributes. - Works with any web component library that uses a standard native custom element constructor, not just Skate or native web components. +- Also supports custom elements that have been loaded using HTML imports, as well as type-extension elements. ## Usage @@ -22,6 +23,9 @@ const CustomElement = window.customElements.define('my-component', MyComponent); // Reactify it! export default reactify(CustomElement); + +// You can also Reactify it using the tag name. +export default reactify('my-component'); ``` Usage with [SkateJS](https://github.com/skatejs/skatejs) is pretty much the same, Skate just makes defining your custom element easier: @@ -68,6 +72,67 @@ When you pass down props to the web component, instead of setting attributes lik ``` +### Reactifying web components loaded using HTML import +Custom elements that depend on HTML imports (which are described in another part of the Web Components specification) were previously a bit trickier to integrate into a React project. But this `react-integration` library combined with [the `web-components` Webpack loader](https://github.com/rnicholus/web-components-loader) make the process farily painless. After integrating the Webpack loader into your project, simply `import` the root HTML file of the web component in your React component and use the generated URL to import the Web component using an HTML import in your `render` method. For example: + +```jsx +import React, { Component } from 'react' + +import reactify from 'skatejs-react-integration' + +const importWcUrl = require('my-web-component/component.html') + +class MyWebComponentWrapper extends Component { + render() { + const MyComponent = reactify('my-web-component') + + return ( + + + + + ) + } +} + +export default MyWebComponentWrapper +``` + +### Web component attributes +If the underlying web component you intend to Reactify requires some properties to be set directly on the element as attributes, include an `attr-` prefix on the property name. For example: + +```jsx + +``` + +The above code will set a `data-foo` attribute on the underlying custom element, instead of setting a corresponding property on the element object. An example of such a web component is [the hugely popular `` element](https://github.com/geelen/x-gif), which requires the GIF `src` to be set as an element and _not_ a property. + +### Type-extension elements +Custom elements that extend an existing native element are also supported. Take [the ajax-form element](https://github.com/rnicholus/ajax-form) as an example. Ajax-form extends the native `
` element to provide additional features. This is an example of a type-extension element. In order to use any type-extension element, such as ajax-form, your render method might look something like this: + +```jsx +render() { + const Form = reactify('form') + + return ( + + + + + + + +
+ ) +} +``` + +Notice that the above example also makes use of attribute and HTML imported elemenent support, both of which are discussed earlier in the documentation. + ### Children If your web component renders content to itself, make sure you're using Shadow DOM and that you render it to the shadow root. If you do this `children` and props get passed down as normal and React won't see your content in the shadow root. From 8d71411839070a41749ccb9a4d4c092e023b202e Mon Sep 17 00:00:00 2001 From: Ray Nicholus Date: Sat, 4 Feb 2017 23:47:16 -0600 Subject: [PATCH 09/14] Revert "support for CE tag name instead of constructor" This reverts commit 76390e485756f37a1d356d739641743d76f6d5e3. --- src/index.js | 56 +++++++++++++++++++++------------------------------- 1 file changed, 23 insertions(+), 33 deletions(-) diff --git a/src/index.js b/src/index.js index 63b5549..4450e27 100644 --- a/src/index.js +++ b/src/index.js @@ -26,20 +26,12 @@ function syncEvent(node, eventName, newEventHandler) { } } -export default function (CustomElementOrTagName, opts) { +export default function (CustomElement, opts) { opts = assign({}, defaults, opts); - if (typeof CustomElementOrTagName !== 'function' && typeof CustomElementOrTagName !== 'string') { - throw new Error('Given element is not a valid constructor or tag name'); + if (typeof CustomElement !== 'function') { + throw new Error('Given element is not a valid constructor'); } - - let CustomElementConstructor = CustomElementOrTagName - if (typeof CustomElementOrTagName === 'string') { - CustomElementConstructor = document.createElement(CustomElementOrTagName).constructor - } - - const tagName = typeof CustomElementOrTagName === 'string' - ? CustomElementOrTagName - : (new CustomElementOrTagName()).tagName; + const tagName = (new CustomElement()).tagName; const displayName = pascalCase(tagName); const { React, ReactDOM } = opts; @@ -55,24 +47,24 @@ export default function (CustomElementOrTagName, opts) { this.componentWillReceiveProps(this.props); } componentWillReceiveProps(props) { - const node = ReactDOM.findDOMNode(this); - Object.keys(props).forEach(name => { - if (name === 'children' || name === 'style') { - return; - } + const node = ReactDOM.findDOMNode(this); + Object.keys(props).forEach(name => { + if (name === 'children' || name === 'style') { + return; + } - if (name.indexOf('on') === 0 && name[2] === name[2].toUpperCase()) { - syncEvent(node, name.substring(2), props[name]); - } else if (name.indexOf('attr-') === 0) { - let attrValue = props[name]; - if (typeof attrValue === 'object') { - attrValue = JSON.stringify(attrValue) - } - node.setAttribute(name.substring(5), attrValue) - } else { - node[name] = props[name]; - } - }); + if (name.indexOf('on') === 0 && name[2] === name[2].toUpperCase()) { + syncEvent(node, name.substring(2), props[name]); + } else if (name.indexOf('attr-') === 0) { + let attrValue = props[name]; + if (typeof attrValue === 'object') { + attrValue = JSON.stringify(attrValue) + } + node.setAttribute(name.substring(5), attrValue) + } else { + node[name] = props[name]; + } + }); } render() { return React.createElement( @@ -86,12 +78,10 @@ export default function (CustomElementOrTagName, opts) { } } - const proto = typeof CustomElementOrTagName === 'string' - ? document.createElement(tagName).constructor.prototype - : CustomElementOrTagName.prototype; + const proto = CustomElement.prototype; Object.getOwnPropertyNames(proto).forEach(prop => { Object.defineProperty(ReactComponent.prototype, prop, Object.getOwnPropertyDescriptor(proto, prop)); -}); + }); return ReactComponent; } From 63f290ef54418393885e809787d22ee6fa8eb62f Mon Sep 17 00:00:00 2001 From: Ray Nicholus Date: Sat, 4 Feb 2017 23:47:25 -0600 Subject: [PATCH 10/14] Revert "typescript: first param for exported function is now a union type (CE constructor or a string/tag-name)" This reverts commit 9e2d77067dc1b56da72eee2c5f88aab8ba061745. --- src/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.d.ts b/src/index.d.ts index d62cf1b..49ab457 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -3,4 +3,4 @@ import * as React from 'react'; type CustomElementCtor = { new(...args: any[]): HTMLElement }; type ReactComponentCtor = { new(...args: any[]): React.Component }; type Options = { React?: any, ReactDOM?: any }; -export default function (CustomElementOrTagName: CustomElementCtor | string, opts: Options = {}): ReactComponentCtor; +export default function (CustomElement: CustomElementCtor, opts: Options = {}): ReactComponentCtor; From 5392b27594501e0a7c2627d8110390d936bf844f Mon Sep 17 00:00:00 2001 From: Ray Nicholus Date: Sat, 4 Feb 2017 23:48:12 -0600 Subject: [PATCH 11/14] Revert "more unit tests" This reverts commit 82f6734a8bfe439f89618a19bf576c6773fd5555. --- test/unit.js | 1 - test/unit/attributes.js | 38 -------------- test/unit/children.js | 30 ++--------- test/unit/display-name.js | 10 ---- test/unit/webcomponent-proto-funcs.js | 75 ++++++++------------------- 5 files changed, 25 insertions(+), 129 deletions(-) delete mode 100644 test/unit/attributes.js diff --git a/test/unit.js b/test/unit.js index 1135fe4..da475f0 100644 --- a/test/unit.js +++ b/test/unit.js @@ -1,4 +1,3 @@ -import './unit/attributes'; import './unit/children'; import './unit/events'; import './unit/display-name'; diff --git a/test/unit/attributes.js b/test/unit/attributes.js deleted file mode 100644 index 7835b5e..0000000 --- a/test/unit/attributes.js +++ /dev/null @@ -1,38 +0,0 @@ -import reactify from '../../src/index'; -import React from 'react'; -import ReactDOM from 'react-dom'; - -let x = 0; - -function createComponent() { - const tagName = `x-attributes-${x++}`; - - return { - constructor: document.registerElement(tagName, { - prototype: Object.create(HTMLElement.prototype), - }), - tagName: tagName - }; -} - -function getReactifiedComponentByConstructor() { - return reactify(createComponent().constructor); -} - -function getReactifiedComponentByTagName() { - return reactify(createComponent().tagName); -} - -describe('attributes', () => { - it('should pass on properties that start with "attr-" as attributes', () => { - const Comp = getReactifiedComponentByConstructor(); - const comp = ReactDOM.render(, window.fixture); - expect(ReactDOM.findDOMNode(comp).getAttribute('data-test')).to.equal('test-data'); - }); - - it('should pass on properties that start with "attr-" as attributes using tagName based reactification', () => { - const Comp = getReactifiedComponentByTagName(); - const comp = ReactDOM.render(, window.fixture); - expect(ReactDOM.findDOMNode(comp).getAttribute('data-test')).to.equal('test-data'); - }); -}); diff --git a/test/unit/children.js b/test/unit/children.js index 332b2fc..4b7b61e 100644 --- a/test/unit/children.js +++ b/test/unit/children.js @@ -3,38 +3,16 @@ import React from 'react'; import ReactDOM from 'react-dom'; let x = 0; - function createComponent() { - const tagName = `x-children-${x++}`; - - return { - constructor: document.registerElement(tagName, { - prototype: Object.create(HTMLElement.prototype), - }), - tagName: tagName - }; -} - -function getReactifiedComponentByConstructor() { - return reactify(createComponent().constructor, { React, ReactDOM }); -} - -function getReactifiedComponentByTagName() { - return reactify(createComponent().tagName, { React, ReactDOM }); + return reactify(document.registerElement(`x-children-${x++}`, { + prototype: Object.create(HTMLElement.prototype), + }), { React, ReactDOM }); } describe('children', () => { it('should pass on children', () => { - const Comp = getReactifiedComponentByConstructor(); - const comp = ReactDOM.render(, window.fixture); - expect(ReactDOM.findDOMNode(comp).tagName).to.match(/^X-CHILDREN/); - expect(ReactDOM.findDOMNode(comp).firstChild.tagName).to.equal('CHILD'); - }); - - it('should pass on children using tagName based reactification', () => { - const Comp = getReactifiedComponentByTagName(); + const Comp = createComponent(); const comp = ReactDOM.render(, window.fixture); - expect(ReactDOM.findDOMNode(comp).tagName).to.match(/^X-CHILDREN/); expect(ReactDOM.findDOMNode(comp).firstChild.tagName).to.equal('CHILD'); }); }); diff --git a/test/unit/display-name.js b/test/unit/display-name.js index fee99d7..eca1540 100644 --- a/test/unit/display-name.js +++ b/test/unit/display-name.js @@ -10,14 +10,4 @@ describe('display-name', () => { ReactDOM.render(React.createElement(Comp), window.fixture); expect(Comp.displayName).to.equal('XDisplayName_1'); }); - - it('should be a PasalCased version of the tagName when reactifying using CE tagName', () => { - document.registerElement('x-display-name-2', { - prototype: Object.create(HTMLElement.prototype), - }); - - const Comp = reactify('x-display-name-2', { React, ReactDOM }); - ReactDOM.render(React.createElement(Comp), window.fixture); - expect(Comp.displayName).to.equal('XDisplayName_2'); - }); }); diff --git a/test/unit/webcomponent-proto-funcs.js b/test/unit/webcomponent-proto-funcs.js index 272014b..532bf29 100644 --- a/test/unit/webcomponent-proto-funcs.js +++ b/test/unit/webcomponent-proto-funcs.js @@ -2,77 +2,44 @@ import reactify from '../../src/index'; let x = 0; function createComponent() { - const tagName = `x-webcomponent-proto-funcs-${x++}`; - return { - constructor: document.registerElement(tagName, { - prototype: Object.create(HTMLElement.prototype, { - prop: { - value: 'prop', + return reactify(document.registerElement(`x-webcomponent-proto-funcs-${x++}`, { + prototype: Object.create(HTMLElement.prototype, { + prop: { + value: 'prop', + }, + foo: { + value() { + return 'bar'; }, - foo: { - value() { - return 'bar'; - }, + }, + getProp: { + value() { + return this.prop; }, - getProp: { - value() { - return this.prop; - }, + }, + getter: { + get() { + throw new Error('should not throw when reactifying'); }, - getter: { - get() { - throw new Error('should not throw when reactifying'); - }, - }, - }), + }, }), - tagName: tagName - }; -} - -function getReactifiedComponentByConstructor() { - return reactify(createComponent().constructor); + })); } -function getReactifiedComponentByTagName() { - return reactify(createComponent().tagName); -} - - describe('Webcomponent prototype functions', () => { it('should be callable on React component', () => { - const Comp = getReactifiedComponentByConstructor(); - expect(Comp.prototype.foo()).to.equal('bar'); - }); - - it('should return prop', () => { - const Comp = getReactifiedComponentByConstructor(); - expect(Comp.prototype.getProp()).to.equal('prop'); - }); - - it('should not invoke getters', () => { - // If this functionality fails, calling createComponent() should cause the error to be thrown. - const Comp = getReactifiedComponentByConstructor(); - - // We expect it to throw here to make sure we've written our test correctly. - expect(() => Comp.prototype.getter).to.throw(); - }); -}); - -describe('Webcomponent prototype functions - reactified with tagName', () => { - it('should be callable on React component', () => { - const Comp = getReactifiedComponentByTagName(); + const Comp = createComponent(); expect(Comp.prototype.foo()).to.equal('bar'); }); it('should return prop', () => { - const Comp = getReactifiedComponentByTagName(); + const Comp = createComponent(); expect(Comp.prototype.getProp()).to.equal('prop'); }); it('should not invoke getters', () => { // If this functionality fails, calling createComponent() should cause the error to be thrown. - const Comp = getReactifiedComponentByTagName(); + const Comp = createComponent(); // We expect it to throw here to make sure we've written our test correctly. expect(() => Comp.prototype.getter).to.throw(); From ef5f7d30fb0e2a605beeaf7ec04fb723444d8bef Mon Sep 17 00:00:00 2001 From: Ray Nicholus Date: Sat, 4 Feb 2017 23:48:15 -0600 Subject: [PATCH 12/14] Revert "document HTML import, attribute, and type-extension support" This reverts commit 270e646b9a7b2bba1ef6e5a802630d363440d1f8. --- README.md | 65 ------------------------------------------------------- 1 file changed, 65 deletions(-) diff --git a/README.md b/README.md index 282b2ed..3c9fc4f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ Converts web components into React components so that you can use them as first - Listen for custom events triggered from web components declaratively using the standard `on*` syntax. - Passes React `props` to web components as properties instead of attributes. - Works with any web component library that uses a standard native custom element constructor, not just Skate or native web components. -- Also supports custom elements that have been loaded using HTML imports, as well as type-extension elements. ## Usage @@ -23,9 +22,6 @@ const CustomElement = window.customElements.define('my-component', MyComponent); // Reactify it! export default reactify(CustomElement); - -// You can also Reactify it using the tag name. -export default reactify('my-component'); ``` Usage with [SkateJS](https://github.com/skatejs/skatejs) is pretty much the same, Skate just makes defining your custom element easier: @@ -72,67 +68,6 @@ When you pass down props to the web component, instead of setting attributes lik ``` -### Reactifying web components loaded using HTML import -Custom elements that depend on HTML imports (which are described in another part of the Web Components specification) were previously a bit trickier to integrate into a React project. But this `react-integration` library combined with [the `web-components` Webpack loader](https://github.com/rnicholus/web-components-loader) make the process farily painless. After integrating the Webpack loader into your project, simply `import` the root HTML file of the web component in your React component and use the generated URL to import the Web component using an HTML import in your `render` method. For example: - -```jsx -import React, { Component } from 'react' - -import reactify from 'skatejs-react-integration' - -const importWcUrl = require('my-web-component/component.html') - -class MyWebComponentWrapper extends Component { - render() { - const MyComponent = reactify('my-web-component') - - return ( - - - - - ) - } -} - -export default MyWebComponentWrapper -``` - -### Web component attributes -If the underlying web component you intend to Reactify requires some properties to be set directly on the element as attributes, include an `attr-` prefix on the property name. For example: - -```jsx - -``` - -The above code will set a `data-foo` attribute on the underlying custom element, instead of setting a corresponding property on the element object. An example of such a web component is [the hugely popular `` element](https://github.com/geelen/x-gif), which requires the GIF `src` to be set as an element and _not_ a property. - -### Type-extension elements -Custom elements that extend an existing native element are also supported. Take [the ajax-form element](https://github.com/rnicholus/ajax-form) as an example. Ajax-form extends the native `
` element to provide additional features. This is an example of a type-extension element. In order to use any type-extension element, such as ajax-form, your render method might look something like this: - -```jsx -render() { - const Form = reactify('form') - - return ( - - - - - - - -
- ) -} -``` - -Notice that the above example also makes use of attribute and HTML imported elemenent support, both of which are discussed earlier in the documentation. - ### Children If your web component renders content to itself, make sure you're using Shadow DOM and that you render it to the shadow root. If you do this `children` and props get passed down as normal and React won't see your content in the shadow root. From d57c7db1fbbdb78cb776805fd309a174c6819070 Mon Sep 17 00:00:00 2001 From: Ray Nicholus Date: Sat, 4 Feb 2017 23:53:40 -0600 Subject: [PATCH 13/14] attr- & tagName support should be part of 2.1.0 --- test/unit.js | 1 + test/unit/attrs.js | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 test/unit/attrs.js diff --git a/test/unit.js b/test/unit.js index da475f0..5da14c7 100644 --- a/test/unit.js +++ b/test/unit.js @@ -1,3 +1,4 @@ +import './unit/attrs'; import './unit/children'; import './unit/events'; import './unit/display-name'; diff --git a/test/unit/attrs.js b/test/unit/attrs.js new file mode 100644 index 0000000..30f968a --- /dev/null +++ b/test/unit/attrs.js @@ -0,0 +1,21 @@ +import reactify from '../../src/index'; +import React from 'react'; +import ReactDOM from 'react-dom'; + +let x = 0; + +function createComponent() { + const tagName = `x-attributes-${x++}`; + + return document.registerElement(tagName, { + prototype: Object.create(HTMLElement.prototype), + }) +} + +describe('attrs', () => { + it('should pass on properties that start with "attr-" as attributes', () => { + const Comp = reactify(createComponent()); + const comp = ReactDOM.render(, window.fixture); + expect(ReactDOM.findDOMNode(comp).getAttribute('data-test')).to.equal('test-data'); + }); +}); From a58b7ece9298353436bda295969ee24afdd369ab Mon Sep 17 00:00:00 2001 From: Ray Nicholus Date: Sun, 5 Feb 2017 00:12:50 -0600 Subject: [PATCH 14/14] use `attrs` prop to specify all attributes --- src/index.js | 16 +++++++++------- test/unit/attrs.js | 5 ++++- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/index.js b/src/index.js index 4450e27..56c74e2 100644 --- a/src/index.js +++ b/src/index.js @@ -53,14 +53,16 @@ export default function (CustomElement, opts) { return; } + const value = props[name]; + if (name.indexOf('on') === 0 && name[2] === name[2].toUpperCase()) { - syncEvent(node, name.substring(2), props[name]); - } else if (name.indexOf('attr-') === 0) { - let attrValue = props[name]; - if (typeof attrValue === 'object') { - attrValue = JSON.stringify(attrValue) - } - node.setAttribute(name.substring(5), attrValue) + syncEvent(node, name.substring(2), value); + } else if (name.indexOf('attrs') === 0 && value && typeof value === 'object') { + Object.keys(value).forEach(attrName => { + const attrValue = value[attrName]; + + node.setAttribute(attrName, attrValue); + }) } else { node[name] = props[name]; } diff --git a/test/unit/attrs.js b/test/unit/attrs.js index 30f968a..470438a 100644 --- a/test/unit/attrs.js +++ b/test/unit/attrs.js @@ -15,7 +15,10 @@ function createComponent() { describe('attrs', () => { it('should pass on properties that start with "attr-" as attributes', () => { const Comp = reactify(createComponent()); - const comp = ReactDOM.render(, window.fixture); + const attrs = { + 'data-test': 'test-data' + }; + const comp = ReactDOM.render(, window.fixture); expect(ReactDOM.findDOMNode(comp).getAttribute('data-test')).to.equal('test-data'); }); });