-
Notifications
You must be signed in to change notification settings - Fork 31
Support for attributes, type-extension CEs, and those loaded by HTML import #60
Changes from 8 commits
76390e4
9519f44
194096d
42aa5ab
9e2d770
82f6734
bd5e3c7
270e646
8d71411
63f290e
5392b27
ef5f7d3
d57c7db
a58b7ec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | |
<MyComponent items={elem.items} callback={elem.callback} /> | ||
``` | ||
|
||
### 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 ( | ||
<span> | ||
<link rel='import' href={ importWcUrl } /> | ||
<MyComponent /> | ||
</span> | ||
) | ||
} | ||
} | ||
|
||
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 | ||
<MyComponent attr-data-fo='bar' /> | ||
``` | ||
|
||
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 `<x-gif>` 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 `<form>` 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') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My first thought would be to do There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will that work? That will result in the library rendering an element named ajax-form, but this isn't a valid tag name as far as I can tell. It's a form element. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I commented in the particular parts of the code that could handle this. Essentially, you'd try and look up the component with I'm unsure how this will work, though. The v1 CE polyfill doesn't support customised built-ins, I don't think. I also don't think React will call There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I'll take some time to look over those comments ASAP.
Most of this PR is driven to better support CEs loaded using HTML imports. During development, I spent quite a bit of time integrating my own CEs as well as a few other CEs that are loaded via HTML import (some of these are mentioned the docs I contributed). When HTML imports are involved, there is no guarantee that the CE constructor will be available by the time
My testing seemed to suggest that, as long as the
That very well may be true. The v1 CE polyfill is fairly new to me. I have only recently started using it in new WC projects. But if this is missing, it seems this support will need to be added, since the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. +1 for automatically adding the Once skatejs-react-integration has support for customized builtins (when this pr is merged), myself and the company I work for are interested in using this. Love the work you're doing here! |
||
|
||
return ( | ||
<span> | ||
<link rel='import' href={ ajaxFormImportUrl } /> | ||
<Form attr-action='/user' | ||
is='ajax-form' | ||
attr-method='POST' | ||
onSubmit={ this.props.onSubmit } | ||
> | ||
<input name='name' value={ this.props.username } /> | ||
<input name='address' value={ this.props.address } /> | ||
<button>Submit</button> | ||
</Form> | ||
</span> | ||
) | ||
} | ||
``` | ||
|
||
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. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -50,5 +50,8 @@ | |
"dist", | ||
"src" | ||
], | ||
"version": "2.0.2" | ||
"version": "2.1.0", | ||
"engines": { | ||
"node": ">=6.0.0" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will return |
||
} | ||
|
||
const tagName = typeof CustomElementOrTagName === 'string' | ||
? CustomElementOrTagName | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You wouldn't need this part if looking up via |
||
: (new CustomElementOrTagName()).tagName; | ||
const displayName = pascalCase(tagName); | ||
const { React, ReactDOM } = opts; | ||
|
||
|
@@ -50,25 +58,40 @@ 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 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(tagName, { style: this.props.style }, this.props.children); | ||
return React.createElement( | ||
tagName, | ||
{ | ||
is: this.props.is, | ||
style: this.props.style | ||
}, | ||
this.props.children | ||
); | ||
} | ||
} | ||
|
||
const proto = CustomElement.prototype; | ||
const proto = typeof CustomElementOrTagName === 'string' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since there's already |
||
? document.createElement(tagName).constructor.prototype | ||
: CustomElementOrTagName.prototype; | ||
Object.getOwnPropertyNames(proto).forEach(prop => { | ||
Object.defineProperty(ReactComponent.prototype, prop, Object.getOwnPropertyDescriptor(proto, prop)); | ||
}); | ||
}); | ||
|
||
return ReactComponent; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
import './unit/attributes'; | ||
import './unit/children'; | ||
import './unit/events'; | ||
import './unit/display-name'; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(<Comp attr-data-test='test-data'/>, 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(<Comp attr-data-test='test-data'/>, window.fixture); | ||
expect(ReactDOM.findDOMNode(comp).getAttribute('data-test')).to.equal('test-data'); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if it used something like
attributes={{ foo: 'bar' }}
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like a reasonable alternative.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've also been playing around with a similar pattern in a separate lib but added support for more special props https://github.com/skatejs/dom-diff#attributes.