Skip to content

Commit 1a5c275

Browse files
committed
Update: adding this.shadow for easy style placement
1 parent 2cac97b commit 1a5c275

File tree

2 files changed

+33
-14
lines changed

2 files changed

+33
-14
lines changed

README.md

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ function Incrementer(): React.ReactElement {
3131
<button
3232
id="iterate-button"
3333
type="button"
34-
onClick={(): void => setIncrement(prevIncrement => prevIncrement + 1)}
34+
onClick={(): void => setIncrement((prevIncrement) => prevIncrement + 1)}
3535
>
3636
Increment
3737
</button>
@@ -60,6 +60,7 @@ customElements.define('incrementer', ReactTestComponent);
6060
The key pieces of code are `... extends ReactHTMLElement` and `this.mountPoint`.
6161

6262
> ### Polyfills
63+
>
6364
> One thing to remember is that you will need to load [the webcomponentsjs polyfills](https://www.webcomponents.org/polyfills) for `ReactHTMLElement` to work in all browsers. Be sure to include [the ES5 adapter](https://github.com/webcomponents/polyfills/tree/master/packages/webcomponentsjs#custom-elements-es5-adapterjs), as we currently transpile `ReactHTMLElement` down to ES5. The polyfills should be in the `<head>`, and should look something like this:
6465
>
6566
> ```html
@@ -94,9 +95,7 @@ This will allow us to utilize our Web Component as an element in any HTML:
9495
<script src="./path/to/incrementer.js"></script>
9596
</head>
9697
<body>
97-
<h1>
98-
Behold: An Incrementer
99-
</h1>
98+
<h1>Behold: An Incrementer</h1>
10099
<!-- put your web component in your html -->
101100
<incrementer></incrementer>
102101
</body>
@@ -134,21 +133,27 @@ customElements.define('incrementer', ReactTestComponent);
134133

135134
Using styled-components with ReactHTMLElement seems tricky, but there's actually a very simple way to implement it: the [`StyleSheetManager`](https://styled-components.com/docs/api#stylesheetmanager). An app rendered with `StyleSheetManager` might look like this:
136135

137-
```react
136+
```jsx
138137
class ReactWebComponent extends ReactHTMLElement {
139138
connectedCallback() {
140-
ReactDOM.render((
141-
<StyleSheetManager target={this.mountPoint.parentNode}>
139+
ReactDOM.render(
140+
<StyleSheetManager target={this.shadow}>
142141
<App />
143-
</StyleSheetManager>
144-
), this.mountPoint);
142+
</StyleSheetManager>,
143+
this.mountPoint
144+
);
145145
}
146146
}
147147
```
148148

149-
We use `this.mountPoint.parentNode` for the styles instead of simply using `this.mountPoint` for the case of unmounting. If stylesheets are a child of `this.mountPoint`, ReactDOM will throw an error when you try to unmount. (`unmountComponentAtNode(): The node you're attempting to unmount was rendered by another copy of React.`) This error is a little cryptic, but the bottom line is that ReactDOM expects that everything inside the mounted node was generated by React itself. When we use the same node to place our styles, it breaks that expectation. Using the `parentNode` will cause the styles to be placed within the Shadow DOM, but not inside the same component where our app is mounted.
149+
`this.shadow` is a getter that will initialize your Web Component, attaching a Shadow
150+
Root with `{mode: 'open'}`, and setting the Shadow Root's innerHTML to your
151+
template or `<div></div>`. If this initialization has already occurred, it will
152+
simply return the previously created Shadow Root.
153+
154+
We use `this.shadow` for the styles instead of simply using `this.mountPoint` because of unmounting. If stylesheets are a child of `this.mountPoint`, ReactDOM will throw an error when you try to unmount. (`unmountComponentAtNode(): The node you're attempting to unmount was rendered by another copy of React.`) This error is a little cryptic, but the bottom line is that ReactDOM expects that everything inside the mounted node was generated by React itself. When we use the same node to place our styles, it breaks that expectation. Using the `this.shadow` will cause the styles to be placed as a first-child of the Shadow DOM, but not inside the same component where our app is mounted.
150155

151-
If you're using a [custom template](#thismountpoint-and-using-custom-templates), you can find a node for your `StyleSheetManager` target by searching through the `shadowRoot` in the same way you might search through `document.body`. Often, simply using `this.mountPoint.parentNode` will still work as expected, even with custom templates.
156+
If you're using a [custom template](#thismountpoint-and-using-custom-templates), you may need to set the target for your `StyleSheetManager` differently. Often, simply using `this.mountPoint.parentNode` will work as expected, even with custom templates, but this will depend on your template. (You may run into competing styles or have a very unusual use-case where placing a `style` tag as a sibling of your application causes some other issue.)
152157

153158
# Contributing
154159

src/ReactHTMLElement.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,32 @@
11
import ReactDOM from 'react-dom';
22

33
class ReactHTMLElement extends HTMLElement {
4+
private _initialized?: boolean;
5+
46
private _mountPoint?: Element;
57

8+
private getShadowRoot(): ShadowRoot {
9+
return this.shadowRoot || this.attachShadow({ mode: 'open' });
10+
}
11+
612
private template: string;
713

814
private mountSelector: string;
915

16+
get shadow(): ShadowRoot {
17+
if (this._initialized) return this.getShadowRoot();
18+
19+
const shadow = this.getShadowRoot();
20+
shadow.innerHTML = this.template;
21+
this._initialized = true;
22+
23+
return shadow;
24+
}
25+
1026
get mountPoint(): Element {
1127
if (this._mountPoint) return this._mountPoint;
1228

13-
const shadow = this.attachShadow({ mode: 'open' });
14-
shadow.innerHTML = this.template;
15-
this._mountPoint = shadow.querySelector(this.mountSelector) as Element;
29+
this._mountPoint = this.shadow.querySelector(this.mountSelector) as Element;
1630

1731
return this._mountPoint;
1832
}

0 commit comments

Comments
 (0)