Skip to content

Instantly share code, notes, and snippets.

@uhop
Created June 12, 2017 22:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save uhop/4fe6a37fc6daf717d9ddd08888f36a27 to your computer and use it in GitHub Desktop.
Save uhop/4fe6a37fc6daf717d9ddd08888f36a27 to your computer and use it in GitHub Desktop.
React to Web Components bridge

Legend:

  • Wrapper.js wraps a web component, allows to attach callbacks to handle custom events.
  • Handler.js is just like Wrapper.js, but:
    • It is a container for other elements.
    • All it does, is handles bubbled custom events from web component children.
  • Component.js is a sample web component registered as <test-component>, which tests following features:
    • Handles "text" attribute dynamically.
    • Can produce regular events ("click").
    • Produces custom events ("start-transition" and "finish-transition").
  • Sample.js shows how to use web components with React:
    • It hosts <Handler>.
    • It hosts two instances of <test-component>: as is, and the wrapped one (with <Wrapper>).
    • It can dynamically change attributes on web components.
    • It handles regular events, which bubbled from web components ("click").
    • It handles custom events at different levels (<Wrapper> and <Handler>).
class Component extends HTMLElement {
constructor () {
super();
this.addEventListener('click', this.changeBackground.bind(this));
this.addEventListener('transitionend', this.revertBackground.bind(this));
}
static get observedAttributes () {
return ['text'];
}
// life cycle methods
connectedCallback () {
this.setText(this.getAttribute('text'));
}
disconnectedCallback () {
while (this.lastChild) {
this.removeChild(this.lastChild);
}
}
attributeChangedCallback (attrName, oldVal, newVal) {
if (attrName === 'text') {
this.setText(newVal);
}
}
// class methods
setText (text, defaultText='Click me!') {
if (this.firstChild) {
this.firstChild.nodeValue = text || defaultText;
} else {
this.appendChild(document.createTextNode(text || defaultText));
}
}
changeBackground (event) {
if (this.getAttribute('allow-clicks') === null) {
event.preventDefault();
event.stopPropagation();
}
if (!this.classList.contains('red')) {
this.classList.add('red');
this.dispatchEvent(new Event('start-transition', {bubbles: true}));
}
};
revertBackground () {
if (this.classList.contains('red')) {
this.classList.remove('red');
} else {
this.dispatchEvent(new Event('finish-transition', {bubbles: true}));
}
};
}
// Component.observedAttributes = ['text'];
window.customElements.define('test-component', Component);
import React from 'react';
import PropTypes from 'prop-types';
const eventHandler = /^on(?:[A-Z][a-z0-9]*)+$/;
const eventSplitter = /(?=[A-Z])/;
class Handler extends React.Component {
static propTypes = {
tag: PropTypes.string
};
constructor (props) {
super(props);
this.customEventListeners = {};
}
// life cycle methods
componentDidMount () {
Object.keys(this.props).
filter(name => eventHandler.test(name)).
forEach(name => { this.setProp(name, this.props[name]); });
}
componentWillUnmount () {
Object.keys(this.customEventListeners).
filter(name => this.customEventListeners.hasOwnProperty(name)).
forEach(name => { this.parent.removeEventListener(name, this.customEventListeners[name]); });
this.customEventListeners = {};
}
componentWillReceiveProps (nextProps) {
// set changed attributes
Object.keys(nextProps).
filter(name => eventHandler.test(name)).
forEach(name => {
const value = nextProps[name];
if (!(name in this.props) || value !== this.props[name]) { this.setProp(name, value); }
});
// remove unused attributes
Object.keys(this.props).
filter(name => eventHandler.test(name) && !(name in nextProps)).
forEach(name => { this.setProp(name, null); });
}
// own methods
setProp (name, value) {
let eventName = name.split(eventSplitter);
eventName.shift();
eventName = eventName.map(part => part.toLowerCase()).join('-');
if (this.customEventListeners.hasOwnProperty(eventName)) {
this.parent.removeEventListener(eventName, this.customEventListeners[eventName]);
}
if (value) {
this.customEventListeners[eventName] = value;
this.parent.addEventListener(eventName, value);
} else {
delete this.customEventListeners[eventName];
}
}
// render component
render () {
return <div ref={elem => { this.parent = elem; }}>{this.props.children}</div>;
}
}
export default Handler;
import React from 'react';
import Wrapper from './webcomponents/Wrapper';
import Handler from './webcomponents/Handler';
// import './webcomponents/Component';
import './webcomponents/component.css';
class Sample extends React.Component {
constructor (props) {
super(props);
this.state = {
textWrapped: 'Wrapped',
textUnwrapped: 'Unwrapped',
clicked: false,
wrapperTransition: false,
handlerTransition: false
};
}
changeText = () => {
this.setState(prevState => ({
textWrapped: prevState.textWrapped === 'Wrapped' ? 'Controlled' : 'Wrapped',
textUnwrapped: prevState.textUnwrapped === 'Unwrapped' ? 'Controlled' : 'Unwrapped'
}));
};
clicked = () => {
this.setState(prevState => ({
clicked: !prevState.clicked
}));
};
startWrapperTransition = () => { this.setState({wrapperTransition: true}); };
finishWrapperTransition = () => { this.setState({wrapperTransition: false, clicked: false}); };
startHandlerTransition = () => { this.setState({handlerTransition: true}); };
finishHandlerTransition = () => { this.setState({handlerTransition: false, clicked: false}); };
render () {
return (
<Handler
onStartTransition={this.startHandlerTransition}
onFinishTransition={this.finishHandlerTransition}
>
<h1>Welcome to Web Components</h1>
<p>
<span>{this.state.handlerTransition && 'handler' || ''}</span>&nbsp;
<button onClick={this.changeText}>Toggle text</button>&nbsp;
<span>{this.state.clicked && 'clicked' || ''}</span>&nbsp;
<span>{this.state.wrapperTransition && 'wrapper' || ''}</span>
</p>
<div onClick={this.clicked}>
<div><test-component text={`${this.state.textUnwrapped}, click me!`} allow-clicks="yes" /></div>
<Wrapper
tag="test-component"
text={`${this.state.textWrapped}, click me!`}
onStartTransition={this.startWrapperTransition}
onFinishTransition={this.finishWrapperTransition}
/>
</div>
</Handler>
);
}
}
export default Sample;
import React from 'react';
import PropTypes from 'prop-types';
const eventHandler = /^on(?:[A-Z][a-z0-9]*)+$/;
const eventSplitter = /(?=[A-Z])/;
class Wrapper extends React.Component {
static propTypes = {
tag: PropTypes.string
};
constructor (props) {
super(props);
this.customEventListeners = {};
}
// life cycle methods
componentDidMount () {
let elem = document.createElement(this.props.tag);
Object.keys(this.props).
forEach(name => { this.setProp(elem, name, this.props[name]); });
this.parent.appendChild(elem);
}
componentWillUnmount () {
Object.keys(this.customEventListeners).
filter(name => this.customEventListeners.hasOwnProperty(name)).
forEach(name => { this.parent.removeEventListener(name, this.customEventListeners[name]); });
this.customEventListeners = {};
}
componentWillReceiveProps (nextProps) {
let elem = this.parent.firstChild;
if (elem) {
// set changed attributes
Object.keys(nextProps).
forEach(name => {
const value = nextProps[name];
if (!(name in this.props) || value !== this.props[name]) {
this.setProp(elem, name, value);
}
});
// remove unused attributes
Object.keys(this.props).
filter(name => !(name in nextProps)).
forEach(name => {
this.setProp(elem, name, null);
});
}
}
// own methods
setProp (elem, name, value) {
if (value === null) {
if (eventHandler.test(name)) {
let eventName = name.split(eventSplitter);
eventName.shift();
eventName = eventName.map(part => part.toLowerCase()).join('-');
if (this.customEventListeners.hasOwnProperty(eventName)) {
this.parent.removeEventListener(eventName, this.customEventListeners[eventName]);
delete this.customEventListeners[eventName];
return;
}
}
elem.removeAttribute(name);
return;
}
if (typeof value == 'string') {
elem.setAttribute(name, value);
return;
}
if (typeof value == 'function' && eventHandler.test(name)) {
let eventName = name.split(eventSplitter);
eventName.shift();
eventName = eventName.map(part => part.toLowerCase()).join('-');
if (this.customEventListeners.hasOwnProperty(eventName)) {
this.parent.removeEventListener(eventName, this.customEventListeners[eventName]);
}
this.customEventListeners[eventName] = value;
this.parent.addEventListener(eventName, value);
return;
}
elem[name] = value;
}
// render component
render () {
return <div ref={elem => { this.parent = elem; }}></div>;
}
}
export default Wrapper;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment