Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hard to use with CSS-in-JS libs. #793

Open
whmountains opened this issue Nov 9, 2017 · 59 comments
Open

Hard to use with CSS-in-JS libs. #793

whmountains opened this issue Nov 9, 2017 · 59 comments

Comments

@whmountains
Copy link

whmountains commented Nov 9, 2017

Background

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].

The Problem

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.

@erquhart
Copy link
Contributor

erquhart commented Nov 28, 2017

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?

@erquhart
Copy link
Contributor

erquhart commented Dec 9, 2017

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?

@ghost
Copy link

ghost commented Dec 27, 2017

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"
           }],
         })

@whmountains
Copy link
Author

Nice trick @zionis137.

I'm still hoping for some more official support, but this is a nice workaround!

@whmountains
Copy link
Author

whmountains commented Jan 2, 2018

@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.

@erquhart
Copy link
Contributor

erquhart commented Jan 3, 2018

@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?

@tizzle
Copy link

tizzle commented Feb 25, 2018

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?

@erquhart
Copy link
Contributor

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.

@whmountains
Copy link
Author

whmountains commented Mar 8, 2018

@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.

@whmountains
Copy link
Author

whmountains commented Mar 8, 2018

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".

@robertgonzales
Copy link

robertgonzales commented Mar 10, 2018

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:

CMS.registerPreviewTemplate("blog", props => (
  <CSSInjector>
    <BlogPreviewTemplate {...props} />
  </CSSInjector>
))

@erquhart
Copy link
Contributor

erquhart commented Mar 10, 2018

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?

@erquhart
Copy link
Contributor

erquhart commented Mar 10, 2018

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! 😂

@mxstbr
Copy link
Contributor

mxstbr commented Mar 10, 2018

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?

@erquhart
Copy link
Contributor

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?

@mxstbr
Copy link
Contributor

mxstbr commented Mar 10, 2018

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

@markacola
Copy link

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>
  )

@erquhart
Copy link
Contributor

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.

@Frithir

This comment has been minimized.

@pungggi
Copy link

pungggi commented Aug 8, 2018

@firthir Can you elaborate what is the idea behind the localStorage Item?

@Frithir

This comment has been minimized.

@erquhart
Copy link
Contributor

@firthir this issue is about CSS in JS solutions like Emotion, Styled Components, etc.

@erquhart
Copy link
Contributor

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

@erezrokah
Copy link
Contributor

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?

@erezrokah erezrokah reopened this Oct 30, 2019
@bencao
Copy link

bencao commented Nov 5, 2019

Thanks for the previous comments, which inspired me a lot!

Short Term Solution

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));

Long Term Solution

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);
}

@emileswain
Copy link

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 <style> blocks.

I would expect to see the iframe head element to have shows the styles in a similar way, but i'm not. I imagine its because nextjs works in a different way, but i'm not entirely sure.

///////////
netlify-cms-app 2.13.3
netlify-cms-app.js:143 netlify-cms-core 2.34.0
netlify-cms-app.js:43 Looking for Netlify CMS Proxy Server at 'http://localhost:8081/api/v1'
netlify-cms-app.js:43 Detected Netlify CMS Proxy Server at 'http://localhost:8081/api/v1' with repo: 'emile.info'
netlify-cms-app.js:43 'editorial_workflow' is not supported by 'local_fs' backend, switching to 'simple'

@emileswain
Copy link

Ok, So i think this is particular to my issue with NextJS, running in dev with local cms, and very likely i'm just not doing things the right way (i'm not that clued into css.modules and styled components).

But i simply copied the styles in my header into the iframe head element. I imagine that i'm using withEmotion wrong in some way.

import React from "react";
import { CacheProvider } from "@emotion/core";
import createCache from "@emotion/cache";
import weakMemoize from "@emotion/weak-memoize";

const memoizedCreateCacheWithContainer = weakMemoize(container => {
    // @ts-ignore
    let newCache = createCache({ container });
    return newCache;
});

const withEmotion =  Component => props => {
    const iframe:any = document.querySelector("#nc-root iframe");
    const iframeHeadElem = iframe && iframe.contentDocument.head;

    if (!iframeHeadElem) {
        return null;
    }

    const styles:any = document.querySelectorAll("html>head>style");
    let i;
    for (i = 0; i < styles.length; ++i) {
        const style = styles[i];
        iframeHeadElem.appendChild(style.cloneNode(true));
    }

    return (
        <CacheProvider value={memoizedCreateCacheWithContainer(iframeHeadElem)}>
            <Component {...props} />
        </CacheProvider>
    );
};

export default withEmotion;

@miklschmidt
Copy link

miklschmidt commented Jan 28, 2021

None of these solutions seem to work particularly well for me. As soon as i register emotion preview templates, the UI goes to shit. This is enough to break it:

CMS.registerPreviewTemplate(
	'pages',
	withEmotion(() => <div></div>),
);

using the code from #793 (comment)

Screenshot from 2021-01-28 16-24-37

Further more, in editing mode, the cms.css gets inserted into the iframe, instead of the main document, so the widgets go to shit as well. If i move that back out, the widgets look fine. I can't seem to figure out what to do to make the main UI work. I'm not entirely sure what's happening.

EDIT: So actually this is enough to break it:

import CMS from 'netlify-cms-app';

CMS.registerPreviewTemplate('custom-pages', () => <div></div>);

So it probably has nothing to do with emotion. I just need to register an arbitrary preview template. If i remove the last line, CMS looks fine.

@wittenbrock
Copy link

I'm in the boat as @miklschmidt - none of the above solutions worked. The CMS's styles look fine until I add this line:

CMS.registerPreviewTemplate('blog', () => <div>Test</div>);

Then they have the janky formatting mirrored in his screenshot.

gatsby: 2.32.2
emotion: 11.0.0
gatsby-plugin-emotion: 6.0.0
netlify-cms-app: 2.14.21

@erezrokah
Copy link
Contributor

erezrokah commented Mar 4, 2021

For those who are experiencing the issue in #793 (comment), can you try downgrading to emotion@10 and gatsby-plugin-emotion@4? The CMS uses emotion@10, so the two different versions might be conflicting.

@javialon26
Copy link

I have this working with Emotion 10 but if I try with Emotion 11 the entire preview panel breaks with a React error. Any plan in the near future to update to Emotion 11? Thanks.

@erezrokah
Copy link
Contributor

Hi @javialon26, yes there are plans to upgrade but no timeline yet. This will need to be behind a major version bump as it is a breaking change for emotion 10 users. I'm not sure if we should couple this to #5111 (also a breaking change).

@mosesoak
Copy link

mosesoak commented Oct 9, 2021

Hi, we're using the (still beta) variable type lists and yaml aliases to pull off a page-builder model for a project.

But we're using twin.macro which extends emotion with tailwind functionality.

Is there any normal way to get these styles working in preview templates yet?

Thanks

@erezrokah
Copy link
Contributor

Hi @mosesoak, if you could share an example repo with that setup it would help to figure out a solution.

TLDR: you'd either need to extract the styles as files, or as raw CSS

@mosesoak
Copy link

mosesoak commented Jan 4, 2022

@erezrokah I can put something together if it helps but I'm seeing that Netlify CMS currently doesn't support version 11 of Emotion (the way we use Twin it's basically just emotion with some extra support for tailwind, but I'm not sure we can downgrade to Emotion 10).

Do you know if there's any timeline for getting Netlify CMS updated so it can work with the latest version of Emotion? We are actively using the CMS with client projects now so not having preview is becoming a serious problem for us... thanks!

@erezrokah
Copy link
Contributor

Hi @mosesoak, there isn't a timeline for upgrading emotion, however we'd be happy for a contribution.

The upgrade will need to be behind a major version bump as it is a breaking change, see #5652.

@mosesoak
Copy link

mosesoak commented Jan 5, 2022

Thanks @erezrokah -- I ended up taking a different approach since extracting styles from Emotion 11 wasn't working, and also because we are using Gatsby, so even with styling working our components that include special Gatsby functionality like useStaticQuery fail to run.

I realized that since we're using a page-builder model where all components for a single page are listed in a variable-type list, that the ideal way to do this would be to show the actual rendered web page, and then overlay the new data changes onto it.

I was able to do it that way! I realize that it doesn't cover all use cases of this CMS but will post the details here in case it's helpful for anyone else.

"I heard you liked iframes, so I put an iframe in your iframe" 😏

The page is loaded into a second iframe inside the preview, like so:

image

At first you're seeing the original page with unaltered content, but as soon as you type into a CMS field you see the content change without any glitches. Since it's precompiled, all the jss styles are included in the page already so no need to use CMS styling. It was cool to find that changing images in the CMS swaps the Gatsby images without any trouble, that's due to a special image component we wrote that will replace the GatsbyImage with a regular img tag if a string src is passed to it.

Data is sent into the secondary iframe using the secure postMessage javascript API to send a same-origin MessageEvent. The assumption is that this isn't easily spoofable, but our site is static and doesn't have any user session or sensitive data in it so we're happy trusting the browser vendors with this API, and would be happy to hear from anyone here if that's a bad idea.

Because we're doing a page-builder model, it actually works to just pass the entire entry.toJS().data blob and use that as a direct replacement for the pageContext blob that Gatsby provides during static generation, which makes this a really lightweight implementation.

const PagePreview: FC<PreviewTemplateComponentProps> = ({ entry }) => {
  const win = useRef<HTMLIFrameElement | null>(null);
  
  // home page is a special case without a slug
  let src = `${window.location.origin}/`;
  if (props.slug !== 'home') {
    src += props.slug;
  }
  
  // not in hook - refresh page data on render
  win.current?.contentWindow?.postMessage(props, location.origin);
  
  return (
    <iframe
      ref={win}
      onLoad={handleLoad}
      title={props.slug}
      style={{ width: '100%', height: '100vh' }}
      src={src}
    />
  );
};

CMS.registerPreviewTemplate('pages', PagePreview);

Then in the main page template,

  // this is subbed in for pageContext when set
  const [cmsPreviewData, setCmsPreviewData] = useState<typeof pageContext>();

  useEffect(() => {
    if (typeof window !== 'undefined') {
      const handleDataChange = (event: MessageEvent) => {
        // Verify same-origin. Browsers won't encode active scripts in postMessage
        // Also lightly validate data structure but keep it minimal to avoid false flags
        if (
          event instanceof MessageEvent &&
          event.origin === window.origin &&
          event.data?.slug === pageContext.slug &&
          Array.isArray(event.data?.['components'])
        ) {
          setCmsPreviewData(event.data);
        } else {
          console.error('invalid message received');
        }
      };
      window.addEventListener('message', handleDataChange);
      return () => window.removeEventListener('message', handleDataChange);
    }
  }, [pageContext]);

I added a few other niceties, including a loading message and a height state that sets the inner iframe height to the page height on load, which avoids a double scrollbar and lets 'scroll sync' work.

I would hesitate to recommend that anyone use this on a site with live user sessions, but it seems like a great solution for a static site. We haven't made this live yet so please reply with feedback if you have any! Thanks

@vbrzezina
Copy link

Hello,
Have anyone managed to make it work with MUI 5 / emotion 11 🙏 ? Also.. does anyone have at least a slight idea where to start if I wanted to try solving the issue in the core repo?

@vbrzezina
Copy link

vbrzezina commented Jan 4, 2023

Hi @mosesoak, there isn't a timeline for upgrading emotion, however we'd be happy for a contribution.

The upgrade will need to be behind a major version bump as it is a breaking change, see #5652.

I'll give it a shot 🙂 When using MUI 5 npm has a lot of issues with dependency conflicts and that's bugging me dearly

Edit: WIP, one test suite failing I don't understand why, I'll try to solve it or create the PR anyway

@vbrzezina
Copy link

vbrzezina commented Jan 6, 2023

Hello, Have anyone managed to make it work with MUI 5 / emotion 11 🙏 ? Also.. does anyone have at least a slight idea where to start if I wanted to try solving the issue in the core repo?

Okay so I tried out a couple of solutions proposed here and I can verify that emotion's cache provider with iframe as a container element solves the issue. One problem tho, I notices that in preview, the styles are being duplicated (with every mount or render, i'm not sure okay, seems to be with every occurence of that component and seems to be related to this thread emotion-js/emotion#2436), any advice appriciated

Screenshot 2023-01-07 at 0 00 05


import { PreviewTemplateComponentProps } from 'netlify-cms-core';

import { CssBaseline, ThemeProvider } from '@mui/material';

import createCache from '@emotion/cache';
import { CacheProvider } from '@emotion/react';

import { BlogPostTemplate } from '../../templates/blog-post';
import theme from '../../theme';

const BlogPostPreview = ({ entry, widgetFor }: PreviewTemplateComponentProps) => {
  const cache = createCache({
    key: 'preview',
    container: document.getElementById('preview-pane').contentWindow.document.head,
  });

  return (
    <CacheProvider value={cache}>
      <ThemeProvider theme={theme}>
        <CssBaseline />
        <BlogPostTemplate
          content={widgetFor('body')}
          description={entry.getIn(['data', 'description'])}
          tags={entry.getIn(['data', 'tags'])?.toJS()}
          title={entry.getIn(['data', 'title'])}
          date={entry.getIn(['data', 'date'])}
          featuredpost={entry.getIn(['data', 'featuredpost'])}
        />
      </ThemeProvider>
    </CacheProvider>
  );
};

export default BlogPostPreview;

@vbrzezina
Copy link

vbrzezina commented Jan 6, 2023

Turns out memoizing the cache helps Sorry about the any-ies


import { PreviewTemplateComponentProps } from 'netlify-cms-core';

import { CssBaseline, ThemeProvider } from '@mui/material';

import createCache from '@emotion/cache';
import { CacheProvider } from '@emotion/react';
import weakMemoize from '@emotion/weak-memoize';

import { BlogPostTemplate } from '../../templates/blog-post';
import theme from '../../theme';

const memoizedCreateCacheWithContainer = weakMemoize((container: any) => {
  return createCache({
    key: 'preview',
    container,
  });
});

const BlogPostPreview = ({ entry, widgetFor }: PreviewTemplateComponentProps) => {
  return (
    <CacheProvider
      value={memoizedCreateCacheWithContainer(
        (document.getElementById('preview-pane') as any).contentWindow.document.head
      )}
    >
      <ThemeProvider theme={theme}>
        <CssBaseline />
        <BlogPostTemplate
          content={widgetFor('body')}
          description={entry.getIn(['data', 'description'])}
          tags={entry.getIn(['data', 'tags'])?.toJS()}
          title={entry.getIn(['data', 'title'])}
          date={entry.getIn(['data', 'date'])}
          featuredpost={entry.getIn(['data', 'featuredpost'])}
        />
      </ThemeProvider>
    </CacheProvider>
  );
};

export default BlogPostPreview;

@vbrzezina
Copy link

Hi @mosesoak, there isn't a timeline for upgrading emotion, however we'd be happy for a contribution.
The upgrade will need to be behind a major version bump as it is a breaking change, see #5652.

I'll give it a shot 🙂 When using MUI 5 npm has a lot of issues with dependency conflicts and that's bugging me dearly

Edit: WIP, one test suite failing I don't understand why, I'll try to solve it or create the PR anyway

#6645 One test suite failing I don't understand why

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

No branches or pull requests