Skip to content

Instantly share code, notes, and snippets.

@magnusjt
Last active February 28, 2019 13:07
Show Gist options
  • Select an option

  • Save magnusjt/32250b89bbc3a529c413f1fc8cc256f8 to your computer and use it in GitHub Desktop.

Select an option

Save magnusjt/32250b89bbc3a529c413f1fc8cc256f8 to your computer and use it in GitHub Desktop.
Create react app inside webcomponent (v1) with shadow dom fallback
const ReactDOM = require('react-dom')
const retargetEvents = require('react-shadow-dom-retarget-events')
function getStyleElementsFromReactWebComponentStyleLoader(){
try{
return require('react-web-component-style-loader/exports').styleElements
}catch(e){
return []
}
}
module.exports = function createWebComponent(reactApp, tag, observedAttributes = []){
customElements.define(tag, class extends HTMLElement{
constructor(){
super()
if(this.attachShadow){
this._constructWithShadowDOM()
}else{
this._constructWithoutShadowDOM()
}
}
_constructWithShadowDOM(){
const shadowRoot = this.attachShadow({mode: 'open'})
const mountPoint = document.createElement('div')
const styles = getStyleElementsFromReactWebComponentStyleLoader()
for(let style of styles){
shadowRoot.appendChild(style.cloneNode(true))
}
shadowRoot.appendChild(mountPoint)
this.appInstance = null
let that = this
ReactDOM.render(reactApp, mountPoint, function(){
that.appInstance = this
that._callLifeCycleHook('webComponentConstructed', that)
})
retargetEvents(shadowRoot)
}
_constructWithoutShadowDOM(){
const mountPoint = document.createElement('div')
const styles = getStyleElementsFromReactWebComponentStyleLoader()
for(let style of styles){
this.appendChild(this._getTagScopedStyle(style))
}
this.appendChild(mountPoint)
this.appInstance = null
let that = this
ReactDOM.render(reactApp, mountPoint, function(){
that.appInstance = this
that._callLifeCycleHook('webComponentConstructed', that)
})
}
_getTagScopedStyle(style){
let replacedStyle = document.createElement('style')
replacedStyle.innerHTML = style.innerHTML.replace(/:host/g, tag)
return replacedStyle
}
// Determines what attributes we will get change-callback for
static get observedAttributes(){
return observedAttributes
}
_callLifeCycleHook(name, ...args){
if(this.appInstance[name]){
this.appInstance[name](...args)
}
}
connectedCallback(){
this._callLifeCycleHook('webComponentConnected')
}
disconnectedCallback(){
this._callLifeCycleHook('webComponentDisconnected')
}
attributeChangedCallback(attrName, oldVal, newVal){
this._callLifeCycleHook('webComponentAttributeChanged', attrName, oldVal, newVal)
}
adoptedCallback(){
this._callLifeCycleHook('webComponentAdopted')
}
})
}
Basically the same as react-web-component (https://github.com/WeltN24/react-web-component), except it works with webcomponents v1.
NB: There is no need to include a heavy-handed-barely-working shadow dom polyfill.
### Scoped styles
If shadow dom is not available, it falls back on a normal div.
Styles must be scoped by adding the following postcss plugin to your build:
require('postcss-scopify')(':host')
This will scope all styles to the custom element (actually, it prefixes all your styles with ":host").
If shadow dom is supported, this makes no difference.
If shadow dom is not supported, we replace :host with the tag of the webcomponent (ex: my-element).
This means that you can use the :host tag in your css even when using a browser without shadow dom!
NB: Remember that the styles you include in your react app are scoped, but the global styles on the page
will still leak inside. It's a good idea to include a css reset of some kind to mitigate this (e.g. bootstrap).
If you were to use a shadow dom polyfill with shadycss, you would still need to do this.
### What polyfills do I need?
Polyfill for custom elements (and html imports):
<script src="https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/1.1.0/webcomponents-hi-ce.js"></script>
Polyfill for es5 definition of web component in v1 style:
<script src="https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/1.1.0/custom-elements-es5-adapter.js"></script>
### Observed attributes?
In web components v1, you need to explicitly list the attributes you want change callback on.
Use the observedAttributes array for this.
@magnusjt
Copy link
Copy Markdown
Author

Lately I've decided to skip shadow dom entirely when using react inside a web component. The reason is that several react libraries depend on beeing able to access events from inside the custom element (various datepickers and dialogues). These libraries could be updated to fix the problem, but for now it's easier to just not use shadow dom.

@magnusjt
Copy link
Copy Markdown
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment