By default JSS adds <style> to <head>. We can specify a place by using insertionPoint but this does not work if we want to add <style> inside of a same origin iframe of course. As far as I understand it is possible to do so either by loading <script> inside iframe or by using sheet.toString() and manually adding the contents inside iframe <style> element. Both are not great but the latter looks better.
It would be nice if we could pass a DOM element to attach() function or had a special function for it like attachTo(). Then all <style> tags would be appended to that element.
Maybe there is already a better solution for my problem. I would appreciate your help.
What is your usecase, why do you want to run your code in the main document but insert styles into an iframe?
I make a 3rd party widget system. My main script creates an iframe that loads assets to avoid conflicts with the site. Then this asset loader iframe creates iframes containing widgets. So I need to render widgets with their styles from asset loader iframe with React. Now I load scripts inside widget iframes to make styles work properly. But adding scripts and parsing them takes time. Instead I can load them only once in asset loader iframe and add widgets with a function.
boah, I read this twice, and could't quite get it, can you please try again, maybe reduce information and just focus on why you want to render the styles from the main window into the iframe)))
So you have some widget in an iframe and you want to generate styles which override the existing styles inside of that iframe?
No) I will try better.
Actually I want to render the styles from one iframe into others. Not to override. Maybe to make myself clear I should explain my app architecture.
Customers load 1 main.js file on their sites. To avoid conflicts with libs already installed on the sites, main.js creates an iframe and loads all assets there. Then it sends a request to our server for information about what widgets should be installed and where. After that the code from asset loader iframe creates widgets and places them on the site. Again to avoid conflicts with customer code and CSS, widgets are created inside iframes. After that I have 2 options:
<head> inside the iframe. Everything is fine except for the fact that I load the same code in each widget iframe. And even with long term caching it takes time for the browser to add scripts and parse them.<head> of document where they were loaded, that is the asset loader iframe.Ok I get it.
The problem with passing document like sheet.attach(document) is that you would loose the sheets order management feature of jss.
There are 2 options jss provides: insertionPoint and index. Both of them allow you to specify the order of the sheets.
An option would be to pass document to the setup:
import {create} from 'jss'
const jss = create({document: iframeDoc})
// or
import jss from 'jss'
jss.setup({document: iframeDoc})
Yes, that would solve my problem.
I think if you need it asap, then you should write a wrapper function like this:
const style = iframeDoc.head.appendChild(iframeDoc.createElement('style'))
style.textContent = jss.createStyleSheet(styles).toString()
Yeah, writing something like this right now) Thank you.
In case someone else need this before the feature is ready, this is how I solved it.
// style_manager.js
import { create } from 'jss';
import preset from 'jss-preset-default';
export default class StyleManager {
constructor({ sheet, local }) {
this.sheet = sheet;
this.local = local;
this.components = [];
}
addStyleSheet(component) {
if (this.components.includes(component)) return;
if (this.styleElId) {
this.components.push(component);
return;
}
const styleEl = this.local.document.createElement('style');
const styleElId = `styles-${Date.now()}`;
styleEl.id = styleElId;
this.styleElId = styleElId;
styleEl.textContent = this.sheet.toString();
this.local.document.head.appendChild(styleEl);
}
removeStyleSheet(component) {
this.components = this.components.filter((item) => item !== component);
if (this.components.length) return;
const styleEl = this.local.document.getElementById(this.styleElId);
if (!styleEl) return;
styleEl.parentNode.removeChild(styleEl);
delete this.styleElId;
}
}
let jss;
export function getJss() {
if (jss) return jss;
jss = create(preset());
return jss;
}
// Usage:
// button.js
import React, { PureComponent, PropTypes } from 'react';
import StyleManager, { getJss } from './style_manager';
import stylesSrc from './styles';
const styles = getJss().createStyleSheet(stylesSrc);
let styleManager;
export default class Button extends PureComponent {
static propTypes = {
text: PropTypes.string.isRequired,
};
static contextTypes = {
// Getting iframe window as local variable from React context.
// We pass it down in a container so that any component can access it.
local: PropTypes.object.isRequired,
};
componentWillMount() {
const { local } = this.context;
if (!styleManager) {
styleManager = new StyleManager({ sheet: styles, local });
}
styleManager.addStyleSheet(this);
}
componentWillUnmount() {
styleManager.removeStyleSheet(this);
}
render() {
return (
<button className={styles.classes.root}>
{this.props.text}
</button>
);
}
}
StyleManager adds <style> only once per component class and removes it if all instances of the class are unmounted.
I think with the new option type for insertionPoint which can be an element now, you can do that now, it is in the next branch right now and contains breaking changes, so plugins won't work.
Adding tests to make sure it works.
Released with JSS 8
I'm trying to inject styles into the iframe's head element but without the success. It's still inserting styles into parent document. Here is sample code. I couldn't find any example. I've tried many different ways of defining insertionPoint but none works. What am I doing wrong?
import React, { Component } from "react";
import ReactDOM from "react-dom";
import { create } from "jss";
import { StylesProvider, jssPreset } from "@material-ui/styles";
import Paper from "@material-ui/core/Paper";
class Frame extends Component {
componentWillUnmount() {
ReactDOM.unmountComponentAtNode(
ReactDOM.findDOMNode(this).contentDocument.getElementById("root")
);
}
renderFrame = () => {
const frame = ReactDOM.findDOMNode(this);
const head = frame.contentDocument.head;
const comment = document.createComment("jss");
head.appendChild(comment);
let root = frame.contentDocument.getElementById("root");
if (!root) {
root = document.createElement("div");
root.setAttribute("id", "root");
frame.contentDocument.body.appendChild(root);
}
const jss = create({ ...jssPreset(), insertionPoint: comment });
ReactDOM.render(
<StylesProvider jss={jss}>
<Paper>ASDASDASDASDASDAS</Paper>
</StylesProvider>,
root
);
};
render() {
const { children, ...props } = this.props;
return <iframe title="iframe" {...props} onLoad={this.renderFrame} />;
}
}
This is an issue with material-ui because this works for me with our own integration: https://codesandbox.io/s/j21r5pv42w
I think the problem is that the Paper component doesn't use the new @material-ui/styles yet (not sure about that though). You will need to create a ticket there.
@HenriBeck yes I've noticed that it works with pure react-jss. Thanks for information. I will create a ticket there
hey @lukejagodzinski, I can't find any related ticket there, how did you solve this problem?
edit: nvm, I just found out. I had to use StylesProviderfrom material-ui instead of JssProvider from react-jss
Most helpful comment
Adding tests to make sure it works.