I'm currently using Netlify CMS to build a website for a client. It's a completely static site, but it uses React as a template engine. I also use the React bundle to render a preview inside Netlify CMS.
The other important detail is that this website is styled with styled-components, which works by injecting css rules into document.getElementsByTagName('head')[0].
Netlify CMS transplants the preview component inside an iframe, while the <style> elements generated by styled-components remain outside the iframe. This leaves me with an unstyled preview, which is most unappealing. ✨
I haven't tested, but I expect this to affect other CSS-in-JS libraries like Glamorous, jsxstyle, or JSS as well as any other React library that injects extra elements, like react-helmet or react-portal.
It's not always a given that a site's CSS will automatically apply to the preview pane - doing so often requires porting the styles in via registerPreviewStyle. Does styled-components provide any way to output a CSS file?
Hmm looks like styled components adds a data attribute to its style elements - I'll bet the others do the same. The registerPreviewStyle registry accepts file paths only, but it could also accept a CSS selector for this use case, which we could run with querySelectorAll and copy matching elements into the preview pane. We should also accept raw styles while we're on the subject.
That said, we need to give some higher level consideration to the proper abstraction for these API changes. What do you think?
I had the same problem with CSS-Modules on GatsbyJS, I hope this helps:
according to the documentation style-loader is able to inject the inline-CSS into an iframe.
But in the end I was unable to set up this functionality with netlify-cms and used the Extract-Text-Plugin
This extracts every CSS from all components into a new stylesheet which i include with
CMS.registerPreviewStyle('stylesCMS.css')
The relevant parts from my webpack.config look like this:
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const extractLess = new ExtractTextPlugin({
filename: "stylesCMS.css",
// path: path.resolve(__dirname, '../static/admin/')
});
....
module: {
rules: [
{
test: /\.css$/,
use: extractLess.extract({
use: [
{
loader: "css-loader?modules&importLoaders=1"
}],
})
Nice trick @zionis137.
I'm still hoping for some more official support, but this is a nice workaround!
@erquhart Your proposal for finding CSS by selector sounds reasonable.
Another possibility would be to allow loading the preview iframe via a URL rather than trying to inject a react tree inside. I think it might be a more surefire solution than trying to identify the misplaced CSS and teleport it somewhere else.
@whmountains we need some pretty tight, realtime control over that preview pane, so far it seems injecting the React tree is a requirement - the preview isn't served separately. How would you propose doing this with a URL?
Hey,
i'm wondering if there is any news on this, as i ran into the exact same issue as @whmountains. In addition i feel that using extract-text-webpack-plugin is not going to work, as in my understanding this won't pick up the styled-components definitions. This is discussed here, here and here in a little more detail.
Maybe rendering the preview into a React portal instead of an iFrame would solve the issue?
That's an interesting idea. I haven't looked into portals at all, but feel free to check it out and see if it's possible.
@erquhart To answer your question I would create a subscriber interface, which a HoC within the preview pane can access. Similar to how react-redux works with connect wrapping the store.subscribe api. In fact, I would copy the react-redux api as much as possible since it's performant and everyone knows how to use it.
You could also have another HoC which would wrap the entire preview pane and implement scroll sync by listening to scroll events.
AFAIK netlify-cms uses redux under the hood. Could you just expose the store inside the iframe?
Everything from EditorPreviewPane on down would be incorporated into the custom build running inside the iframe.
Just throwing out ideas. I'm not very familiar with the codebase or all the caveats a system like this would introduce. It just seems that netlify-cms's preview interface is breaking core assumptions about how web pages are rendered and it would be nice to fix that somehow so everything "just works".
For anyone using emotion, I solved this issue by using SSR on the fly to extract the css and then inject it into the nearest document (iframe) head. Very hacky but it works.
import { renderToString } from "react-dom/server"
import { renderStylesToString } from "emotion-server"
class CSSInjector extends React.Component {
render() {
return (
<div
ref={ref => {
if (ref && !this.css) {
this.css = renderStylesToString(renderToString(this.props.children))
ref.ownerDocument.head.innerHTML += this.css
}
}}>
{React.Children.only(this.props.children)}
</div>
)
}
}
It works by wrapping your preview template:
```js
CMS.registerPreviewTemplate("blog", props => (
))
That will be much less hacky once #1162 lands. Any library that can export strings will be covered at that point.
Anyone up for talking @mxstbr into exporting strings from styled components?
On second thought that PR won't help for the emotion case at all. I'm also thinking that what you've done really isn't hacky, especially considering how you moved it into a component. This might even find it's way into the docs! 😂
styled-components has a way to target an iframe
as it's injection point:import { StyleSheetManager } from 'styled-components'
<StyleSheetManager target={iframeHeadElem}>
<App />
</StyleSheetManager>
Any styled component within App will now inject it's style tags into the target elem! Maybe that's helpful?
I was just looking at StyleSheetManager recently and wondering if it might work for this - looks like it should!
@whmountains care to give it a go and let us know?
Note that the target feature was only introduced in v3.2.0 and isn't documented just yet: https://www.styled-components.com/releases#v3.2.0_stylesheetmanager-target-prop
I just tried this out and it works great! Just simply:
const iframe = document.querySelector(".nc-previewPane-frame")
const iframeHeadElem = iframe.contentDocument.head;
return (
<StyleSheetManager target={iframeHeadElem}>
{/* styled elements */}
</StyleSheetManager>
)
Leaving this open as I'd still like to discuss how our API might improve so that this isn't so manual. Perhaps we need some kind of preview plugin API that would allow a styled-components plugin to handle this behind the scenes.
We got this working. It will bring in all styles from the front end URL.
if (
window.location.hostname === 'localhost' &&
window.localStorage.getItem('netlifySiteURL')
) {
CMS.registerPreviewStyle(
window.localStorage.getItem('netlifySiteURL') + '/styles.css'
)
} else {
CMS.registerPreviewStyle('/styles.css')
}
CMS.registerPreviewTemplate('home-page', ({ entry }) => (
<HomePageTemplate {...entry.toJS().data} />
))
@Firthir Can you elaborate what is the idea behind the localStorage Item?
Hey @pungggi you'll find this useful if you're using localhost and logging into the CMS. This gets the preview to display correctly locally and live using the if else. I use the .env.development var for a few things in my dev.
Try this, then we can use this with the example above.
NETLIFY_SITE_URL=https://yoursstiehere.netlify.comOR in this example just hard code it into your custom JS file.
if (
window.location.hostname === 'localhost'
) {
CMS.registerPreviewStyle( 'https://yoursstiehere.netlify.com/styles.css')
} else {
CMS.registerPreviewStyle('/styles.css')
}
Include Custom JS into gatsby-plugin-netlify-cms in the gatsby-config.js file.
// all your other gatsby-config stuff above here.....
// then set up gatsby-plugin-netlify-cms
{
resolve: 'gatsby-plugin-netlify-cms',
options: {
modulePath: `${__dirname}/src/cms/cms.js`,
stylesPath: `${__dirname}/src/cms/admin.css`,
enableIdentityWidget: true,
},
},
'gatsby-plugin-netlify', // make sure to keep it last in the array
],
}
Hope that helps you or someone else.
@Firthir this issue is about CSS in JS solutions like Emotion, Styled Components, etc.
@erquhart
I just tried this out and it works great! Just simply:
const iframe = document.querySelector(".nc-previewPane-frame") const iframeHeadElem = iframe.contentDocument.head; return ( <StyleSheetManager target={iframeHeadElem}> {/* styled elements */} </StyleSheetManager> )
Seems not to work with v2
https://github.com/netlify/netlify-cms/issues/1408#issuecomment-424965185
@pungggi Replace the first line with
const iframe = document.getElementsByTagName('iframe')[0]
For anyone using emotion, I solved this issue by using SSR on the fly to extract the css and then inject it into the nearest document (iframe) head. Very hacky but it works.
This worked great! Just make sure to import react as well.
import React from "react" import { renderToString } from "react-dom/server" import { renderStylesToString } from "emotion-server" class CSSInjector extends React.Component { render() { return ( <div ref={ref => { if (ref && !this.css) { this.css = renderStylesToString(renderToString(this.props.children)) ref.ownerDocument.head.innerHTML += this.css } }}> {React.Children.only(this.props.children)} </div> ) } }It works by wrapping your preview template:
CMS.registerPreviewTemplate("blog", props => ( <CSSInjector> <BlogPreviewTemplate {...props} /> </CSSInjector> ))
@robertgonzales Hey, now with emotion 10 we have to do something like this:
import React from 'react'
import createCache from '@emotion/cache'
import { CacheProvider } from '@emotion/core'
class CSSInjector extends React.Component {
constructor() {
super()
const iframe = document.getElementsByTagName('iframe')[0]
const iframeHead = iframe.contentDocument.head
this.cache = createCache({ container: iframeHead })
}
render() {
return (
<CacheProvider value={this.cache}>
{this.props.children}
</CacheProvider>
)
}
}
document.getElementsByTagName('iframe')[0] didn't work for me. In my case for styled-components I had to do:
import React from "react";
import { StyleSheetManager } from "styled-components";
export default function StyledSheets({ children }) {
const iframe = document.querySelector("#nc-root iframe");
const iframeHeadElem = iframe && iframe.contentDocument.head;
if (!iframeHeadElem) {
return null;
}
return (
<StyleSheetManager target={iframeHeadElem}>{children}</StyleSheetManager>
);
}
Hi there,
Has anyone been successful with Material-UI? Following similar approaches described above I got to this:
import React from 'react';
import { install } from '@material-ui/styles';
import { StylesProvider } from '@material-ui/styles';
import { jssPreset } from '@material-ui/core/styles';
import { create } from 'jss';
import camelCase from 'jss-plugin-camel-case';
install();
export default function MaterialUiSheets({ children }) {
const iframe = document.querySelector('#nc-root iframe');
const iframeHeadElem = iframe && iframe.contentDocument.head;
if (!iframeHeadElem) {
return null;
}
const jss = create({
plugins: [...jssPreset().plugins, camelCase()],
insertionPoint: iframeHeadElem.firstChild,
});
return <StylesProvider jss={jss}>{children}</StylesProvider>;
}
This only works partially, meaning not all the styles are injected into the Iframe.
This is probably a Material-UI question, which I'm not familiar with, but anyway someone might be able to help.
Hi All!
I struggled with this a bit. For some reason setting this up (with styled-components ^4.2.0, netlify-cms-app 2.9.1, and netlify-cms-core 2.11.0) as a functional component wouldn't work, so I tried using the effect hook, which didn't work either. But perhaps my grasp of the effect hook isn't what it should be. Ended up setting it up as a class component which worked mostly, except for my theme.
So if you are using a theme with styled-components, you should be able to get this to work simply by wrapping your StyleSheetManager component with the ThemeProvider component. Perhaps there is a better way to do this, but I have spent enough time on this for now, and am moving on!
import React, { Component } from 'react';
import { StyleSheetManager, ThemeProvider } from 'styled-components';
import theme from '../styles/theme/theme';
class StylesheetInjector extends Component {
constructor(props) {
super(props);
this.state = {
iframeRef: '',
};
}
componentDidMount() {
const iframe = document.querySelector('#nc-root iframe');
const iframeHeadElem = iframe && iframe.contentDocument.head;
this.setState({ iframeRef: iframeHeadElem });
}
render() {
return (
<>
{this.state.iframeRef && (
<ThemeProvider theme={theme}>
<StyleSheetManager target={this.state.iframeRef}>
{this.props.children}
</StyleSheetManager>
</ThemeProvider>
)}
</>
);
}
}
export default StylesheetInjector;
Refactored using hooks:
import React, { useState, useEffect } from 'react';
import { StyleSheetManager, ThemeProvider } from 'styled-components';
import theme from '../styles/theme/theme';
const StylesheetInjector = ({ children }) => {
const [iframeRef, setIframeRef] = useState(undefined);
useEffect(() => {
const iframe = document.querySelector('#nc-root iframe');
const iframeHeadElem = iframe && iframe.contentDocument.head;
setIframeRef(iframeHeadElem);
});
return (
<>
{iframeRef && (
<ThemeProvider theme={theme}>
<StyleSheetManager target={iframeRef}>{children}</StyleSheetManager>
</ThemeProvider>
)}
</>
);
};
export default StylesheetInjector;
Did that do the trick?
Indeed, it did. I am also using createGlobalStyle and found that my global style component would work with this if I placed it inside the StyleSheetManager component, adjacent to { children }.
@homearanya I was able to get material-ui styles to appear in the iFrame by following this approach: https://github.com/mui-org/material-ui/issues/13625#issuecomment-493608458
I ended up with the following, as an example:
import React from 'react'
import PropTypes from 'prop-types'
import CssBaseline from '@material-ui/core/CssBaseline';
import { ThemeProvider } from '@material-ui/styles';
import { create } from "jss";
import { jssPreset, StylesProvider } from "@material-ui/styles";
import theme from '../../theme';
import { AboutPageTemplate } from '../../templates/about-page'
class AboutPagePreview extends React.Component {
state = {
ready: false
};
handleRef = ref => {
const ownerDocument = ref ? ref.ownerDocument : null;
this.setState({
ready: true,
jss: create({
...jssPreset(),
insertionPoint: ownerDocument ? ownerDocument.querySelector("#demo-frame-jss") : null
}),
sheetsManager: new Map()
});
};
render() {
const { entry, widgetFor } = this.props;
const data = entry.getIn(['data']).toJS()
if (data) {
return (
<React.Fragment>
<div id="demo-frame-jss" ref={this.handleRef} />
{this.state.ready ? (
<StylesProvider
jss={this.state.jss}
sheetsManager={this.state.sheetsManager}
>
<ThemeProvider theme={theme}>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
<AboutPageTemplate
title={entry.getIn(['data', 'title'])}
content={widgetFor('body')}
/>
</ThemeProvider>
</StylesProvider>
) : null}
</React.Fragment>
)
} else {
return <div>Loading...</div>
}
}
}
AboutPagePreview.propTypes = {
entry: PropTypes.shape({
getIn: PropTypes.func,
}),
widgetFor: PropTypes.func,
}
export default AboutPagePreview
@d3sandoval this is close to how I'm handling the previews for material-ui also, but you might have a better implementation on the ref. Nice job.
Thanks @d3sandoval, that looks like a nice approach. I like the way the callback ref is used.
I ended up using the approach that @talves show me on Gitter, which worked for me. Tony, I hope you don't mind that I share it here:
Basically, I render the full page style context into a string and inject that into a component for the preview. I also do the same for the Preview component.
PreviewContainer.js
import React from 'react'
export default props => (
<React.Fragment>
<style
type="text/css"
id={props.id}
dangerouslySetInnerHTML={{ __html: props.innerStyles }}
/>
<div
dangerouslySetInnerHTML={{ __html: props.innerHTML }}
/>
</React.Fragment>
)
withJss.js
function withJss(Component) {
class WithRoot extends React.Component {
constructor(props) {
super(props);
this.muiPageContext = getPageContext();
}
render() {
return (
<JssProvider registry={this.muiPageContext.sheetsRegistry}>
{/* MuiThemeProvider makes the theme available down the React
tree thanks to React context. */}
<MuiThemeProvider
theme={this.muiPageContext.theme}
// sheetsManager={this.muiPageContext.sheetsManager}
>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
<Component {...this.props} />
</MuiThemeProvider>
</JssProvider>
);
}
}
return WithRoot;
}
export default withJss;
ServicesPreview .js
const styles = theme => ({
root: {
flexGrow: 1,
marginTop: 5,
},
wrapper: {
padding: 20,
},
});
const StyledPreview = ({ classes, data }) => (
<div className={classes.root}>
<div className={classes.wrapper}>
<PageHeader data={{title: data.services.title, description: data.services.introduction}} />
</div>
<div className={classes.wrapper}>
<IntroSection data={data} />
<Services data={data} />
</div>
</div>
)
const Preview = withJss(withStyles(styles)(StyledPreview))
export default class ServicesPreview extends React.Component {
constructor(props) {
super(props);
this.muiPageContext = getPageContext()
}
render () {
// const data = this.props.entry.toJS().data // This or next line
const data = this.props.entry.getIn(['data']).toJS()
const previewHTML = renderToString(<Preview data={{services: { ...data }}}/>)
return (
<PreviewContainer
innerStyles={this.muiPageContext.sheetsRegistry.toString()}
innerHTML={previewHTML}
/>
)
}
}
JssProviderin this case is thereact-jss/lib/JssProvidercomponent in the jss monorepo.
that example preview is pretty complicated, but wanted you to see what it is rendering. I have multiple components being rendered into one preview. As you can see, the
muiPageContext.sheetsRegistryobject is created byimport { SheetsRegistry } from 'jss'
I must say @d3sandoval approach looks simpler
@homearanya The simpler solution will work fine too, but I have some edge cases that the pre-render is required using the method above. A combination of these both would cover it.
I would start with the simpler approach and move from there (captain obvious 😜).
I wonder if the cms should be forwarding the ifame ref to the preview rather than a query.
thanks @devolasvegas that solved the issue for me!
created a short gist for my PreviewLayout component using the more appropriate (IMO) useRef & useLayoutEffect based on the example above -
https://gist.github.com/yoavniran/0953eff3d88f385431b9decc0c3a6be5
showing how to enable styled-components with netlify-cms
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
Closing as the issue seems resolved. Please re-open if necessary
This may merit staying open. What we have so far are instructions for individual libraries, most of which are on the tedious side. At a minimum we should document this stuff, at a maximum we should have a simpler support model.
Sent with GitHawk
I re-opened an added a pinned label so it won't get marked as stale.
I have a suggestion though, we can close this one and open a new issue with the title "Document how to use with CSS in JS libs" and link to this issue for reference.
I think once we start writing the documentation it will make it easier to decide if that is enough or we should make some code changes.
What do you think?
Thanks for the previous comments, which inspired me a lot!
In my opinion, the most natural solution is to create a higher-order component that adds support to a specific CSS-IN-JS library.
For example, I would add support for styled-components and emotion with the following snippet today:
// src/cms/with-styled.js, define the higher-order function to support styled
import React from "react";
import { StyleSheetManager } from "styled-components";
export default Component => props => {
const iframe = document.querySelector("#nc-root iframe");
const iframeHeadElem = iframe && iframe.contentDocument.head;
if (!iframeHeadElem) {
return null;
}
return (
<StyleSheetManager target={iframeHeadElem}>
<Component {...props} />
</StyleSheetManager>
);
};
// src/cms/with-emotion.js, define the higher-order function to support emotion
import React from "react";
import { CacheProvider } from "@emotion/core";
import createCache from "@emotion/cache";
import weakMemoize from "@emotion/weak-memoize";
const memoizedCreateCacheWithContainer = weakMemoize(container => {
let newCache = createCache({ container });
return newCache;
});
export default Component => props => {
const iframe = document.querySelector("#nc-root iframe");
const iframeHeadElem = iframe && iframe.contentDocument.head;
if (!iframeHeadElem) {
return null;
}
return (
<CacheProvider value={memoizedCreateCacheWithContainer(iframeHeadElem)}>
<Component {...props} />
</CacheProvider>
);
};
// src/cms/cms.js, use higher-order functions defined above
import CMS from "netlify-cms-app";
import withStyled from "./with-styled";
import withEmotion from "./with-emotion";
import UserPreview from "./preview-templates/UserPreview";
import OrderPreview from "./preview-templates/OrderPreview";
CMS.registerPreviewTemplate("user", withStyled(UserPreview));
CMS.registerPreviewTemplate("order", withEmotion(OrderPreview));
But for the long term, in order to achieve best possible user experience, it would be best to hide these dirty details and try to detect whether the project is using styled-component or css-modules and add support for these CSS-IN-JS libraries automagically, within the lower level CMS.registerPreviewTemplate function:
function smartRegisterPreviewTemplate(name, component) {
// check styled-components
try {
require("styled-components");
return registerPreviewTemplate(name, withStyled(component));
} catch (styledNotFound) {
// do nothing
}
// check emotion
try {
require("@emotion/core");
return registerPreviewTemplate(name, withEmotion(component));
} catch (emotionNotFound) {
// do nothing
}
// not using any css-in-js library
return registerPreviewTemplate(name, component);
}
Should this work for nextjs implementations as well? I'm using css modules, and have failed to get any of the examples to work. Nextjs is embeded the styles into the admin page
as
Most helpful comment
I just tried this out and it works great! Just simply: