aphrodite
aphrodite copied to clipboard
Injecting style into a shadowDOM node?
I have a chrome extension that injects a content script into a shadow DOM node:
window.addEventListener('load', () => {
let injectDiv = document.createElement('div')
const shadowRoot = injectDiv.attachShadow({ mode: 'open' })
shadowRoot.innerHTML =
`<style>${require('raw-loader!app/styles/extension-material')}</style>
<style data-aphrodite/>
<div id='shadowReactRoot' />`
document.body.appendChild(injectDiv)
ReactDOM.render(
<App />,
shadowRoot.querySelector('#shadowReactRoot')
)
})
})
The first style
tag is for react-mdl
components. This nicely containerizes all the requisite style in the Extension content script w/o leaking style into the parent page.
Aphrodite doesn't seem to like this, though. I followed the readme & tagged a style
tag with data-aphrodite
, but got this error:
invariant.js:44 Uncaught (in promise) Error: _registerComponent(...):
Target container is not a DOM element.
(…)invariant @ invariant.js:44
_renderNewRootComponent @ ReactMount.js:311
_renderSubtreeIntoContainer @ ReactMount.js:401
render @ ReactMount.js:422
(anonymous function) @ index.js:88
I assume the problem is this snippet from https://github.com/Khan/aphrodite/blob/master/src/inject.js
const injectStyleTag = (cssContents /* : string */) => {
if (styleTag == null) {
// Try to find a style tag with the `data-aphrodite` attribute first.
styleTag = document.querySelector("style[data-aphrodite]");
// If that doesn't work, generate a new style tag.
if (styleTag == null) {
// Taken from
// http://stackoverflow.com/questions/524696/how-to-create-a-style-tag-with-javascript
const head = document.head || document.getElementsByTagName('head')[0];
styleTag = document.createElement('style');
styleTag.type = 'text/css';
styleTag.setAttribute("data-aphrodite", "");
head.appendChild(styleTag);
}
}
if (styleTag.styleSheet) {
// $FlowFixMe: legacy Internet Explorer compatibility
styleTag.styleSheet.cssText += cssContents;
} else {
styleTag.appendChild(document.createTextNode(cssContents));
}
};
Is there a way using the existing Aphrodite API to inject into the correct style tag, nested within the shadow DOM node?
ETA: things I've tried:
-
When I remove the
<style data-aphrodite />
tag, the content script renders fine, and theaphrodite
styles are injected into the parent page'shead
-
Adding my own
<head />
to my shadow DOM node doesn't work--the shadow's<head />
doesn't even render -
^^ same is true when i try to structure shadow DOM more (
head
,html
,body
, etc.)
i've worked up a solution but not sure it's solid. basically: i start a MutationObserver
that listens for data-aphrodite
style to be injected in the head
, then I take the textContent
of data-aphrodite
and inject it into my ShadowDOM
Does Aphrodite change the content of the style tag during run time? i.e., shoudl I also add a mutation observer for changes to the data-aphrodite
tag?
// observer listens for nodes added to <head>
// (this code isn't tested but it works at first blush)
const findAphrodite = new MutationObserver((mutations, obs) => {
mutations.forEach(mutation => {
Array.from(mutation.addedNodes).forEach(n => {
if (n.attributes.getNamedItem('data-aphrodite')) {
// find shadow DOM style tag and inject
document.querySelector('#extension')
.shadowRoot.querySelector('#aphroditeStyle').textContent = n.textContent
obs.disconnect()
}
})
})
})
then modified the original injection script to:
window.addEventListener('load', () => {
let injectDiv = document.createElement('div')
injectDiv.id = 'extension'
const shadowRoot = injectDiv.attachShadow({ mode: 'open' })
shadowRoot.innerHTML =
`<style>${require('raw-loader!app/styles/extension-material')}</style>
<div id='shadowReactRoot' />`
document.body.appendChild(injectDiv)
findAphrodite.observe(document.head, { childList: true })
ReactDOM.render(
<App />,
shadowRoot.querySelector('#shadowReactRoot')
)
})
})
The original error you were seeing,
Target container is not a DOM element.
(…)invariant @ invariant.js:44
_renderNewRootComponent @ ReactMount.js:311
_renderSubtreeIntoContainer @ ReactMount.js:401
render @ ReactMount.js:422
(anonymous function) @ index.js:88
is because you forgot to close your <style>
tag, so your <div id='shadowReactRoot' />
wasn't being added, and so React was complaining that you were trying to render to undefined. Closing your <style>
tag at least makes the error go away, but doesn't fix your overall problem.
There's another issue that's sorta similar to this one, #130. I wonder if the suggested StyleSheet.renderInFrame(frame, () => { ... })
suggestion might work here as well? Then your code would turn out like...
StyleSheet.renderInDocument(shadowRoot, () => {
ReactDOM.render(
<App />,
shadowRoot.querySelector('#shadowReactRoot')
)
});
Maybe we could do something fancy to detect if we're in a shadow DOM or an iframe, so that we don't try to use the <head>
tag.
@xymostech ah, i've spent too much time in jsx
i think. i didn't forget to close, i just thought style
could be self-closing (ie, <style data-aphrodite/>
)
the mutation observer is doing the trick for now, but i'll give a look @ the renderInDocument
approach if it starts to give me trouble.
Okay! That API doesn't exist yet, it was just something I was thinking about a while back and never got to implementing. :P If I get around to it I'll mention it here, maybe you could try it out.