snowleopard
snowleopard

Reputation: 833

How to create insertion point to mount styles in shadow dom for MUI material v5 in React custom element

Using @material-ui/core V4(4.12.3 to be exact) I HAD a custom element created successfully using webpack and babel. I used to be styling it using the @material-ui/core makeStyles. Now I am upgrading to @mui/material v5 and want to use the built-in components from @mui/material but they do not display styled within the custom element. Please note that I need this to be a custom element as it will be integrated within another hosting app.

index.tsx BEFORE in v4

import AppComponent from './App';
import { render } from 'react-dom';
import { StylesProvider, jssPreset } from '@material-ui/core/styles';
import { create } from 'jss';

class MyWebComponent extends HTMLElement {
    connectedCallback() {
        const shadowRoot = this.attachShadow({ mode: 'open' });
        const mountPoint = document.createElement('custom-jss-insertion-point');
        const reactRoot = shadowRoot.appendChild(mountPoint);
        const jss = create({
            ...jssPreset(),
            insertionPoint: reactRoot,
        });

        render(
            <StylesProvider jss={jss}>
                <AppComponent />
            </StylesProvider>,
            mountPoint
        );
    }
}
customElements.define('my-element', MyWebComponent);

Upgrading to @mui/material v5(v5.0.4 to be exact), first I tried with StyledEngineProvider in order to mount styles. Then I tried with @mui/styles jssPreset. Either way doesn't work. What I mean by doesn't work is that DataContainer which is referenced by AppComponent has @mui/material components and they are all loading without any styles (such as Grid, Button, InputLabel, Select, any many more).

First try with StyledEngineProvider

import AppComponent from './App';
import { ThemeProvider, createTheme, StyledEngineProvider } from '@mui/material/styles';
import { render } from 'react-dom';
    

const theme = createTheme();

class MyWebComponent extends HTMLElement {
    connectedCallback() {
        // can't use jss in mui v5
        const shadowRoot = this.attachShadow({ mode: 'open' });
        const mountPoint = document.createElement('custom-insertion-point');
        const reactRoot = shadowRoot.appendChild(mountPoint);

        render(
            <StyledEngineProvider injectFirst>
            <ThemeProvider theme={theme}>
                <AppComponent />
            </ThemeProvider>
            </StyledEngineProvider>,
            mountPoint //I have also used reactRoot here instead and got same result
        );
    }
}
customElements.define('my-element', MyWebComponent);

Second try with @mui/styles jssPreset

import AppComponent from './App';
import { render } from 'react-dom';
import { StylesProvider, jssPreset } from '@mui/styles';
import { create } from 'jss';

class MyWebComponent extends HTMLElement {
    connectedCallback() {
        const shadowRoot = this.attachShadow({ mode: 'open' });
        const mountPoint = document.getElementById('jss-insertion-point');
        const reactRoot = shadowRoot.appendChild(mountPoint);
        const jss = create({
            ...jssPreset(),
            insertionPoint: reactRoot,
        });

        render(
            <StylesProvider jss={jss}>
                <AppComponent />
            </StylesProvider>,
            mountPoint
        );       
    }
}
customElements.define('my-element', MyWebComponent);

AppComponent

import React from 'react';
import { Suspense } from 'react';
import DataContainer from './components/DataContainer';

class AppComponent extends React.Component<any> {
    
    render() {
        return (
            <Suspense fallback='Loading...'>
                <div className='AppComponent'>
                    <DataContainer />
                </div>
            </Suspense>
        );
    }
}
export default AppComponent;

DataContainer

import { styled } from '@mui/material/styles';
import Box from '@mui/material/Box';
import Paper from '@mui/material/Paper';
import Grid from '@mui/material/Grid';
import Button from '@mui/material/Button';
import Select from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem';
import FormControl from '@mui/material/FormControl';

const Item = styled(Paper)(({ theme }) => ({
  ...theme.typography.body2,
  padding: theme.spacing(1),
  textAlign: 'center',
  color: theme.palette.text.secondary,
}));

export default function FullWidthGrid() {
  return (
    <Box sx={{ flexGrow: 1 }}>
      <Grid container spacing={2}>
        <Grid item xs={6} md={8}>
          <Button variant="contained">xs=6 md=8</Button>
        </Grid>
        <Grid item xs={6} md={4}>
          <Item>xs=6 md=4</Item>
        </Grid>
        <Grid item xs={6} md={4}>
          <Item>xs=6 md=4</Item>
        </Grid>
        <Grid item xs={6} md={8}>
          <Item>xs=6 md=8</Item>
        </Grid>
      </Grid>
      <div>
        <FormControl sx={{ m: 1, minWidth: 180 }}>
          <Select autoWidth>
            <MenuItem value="">
              <em>None</em>
            </MenuItem>
            <MenuItem value={10}>Twenty</MenuItem>
            <MenuItem value={21}>Twenty one</MenuItem>
            <MenuItem value={22}>Twenty one and a half</MenuItem>
          </Select>
        </FormControl>
      </div>
    </Box>
  );
}

index.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <link rel="icon" href="favicon.ico" />
        <link rel="apple-touch-icon" href="logo192.png" />
        <link rel="manifest" href="manifest.json" />
        <title>React Custom Element</title>
    </head>
    <body>
        <my-element id="elem"> </my-element>
        </body>
</html>

This is what I see:

enter image description here

This is what I'm supposed to see as shown in this stackblitz. (Note that I was unable to create a stackblitz with a custom element unfortunately) https://stackblitz.com/edit/react-d8xtdu?file=index.js

enter image description here

Upvotes: 5

Views: 4084

Answers (1)

jmk
jmk

Reputation: 1610

Here is how I would do it:

You need to create style tag. This will be entry point to emotion (material ui 5 styling solution) to insert scoped shadow DOM styles.

Next step is to configure jss and emotion cache

const jss = create({
    ...jssPreset(),
    insertionPoint: reactRoot,
});

const cache = createCache({
    key: 'css',
    prepend: true,
    container: emotionRoot,
 });

Last thing to do is to wrap our tree in providers

render(
   <StylesProvider jss={jss}>
      <CacheProvider value={cache}>
         <ThemeProvider theme={theme}>
            <Demo />
         </ThemeProvider>
      </CacheProvider>
   </StylesProvider>,
   mountPoint
);

Full example:

    import React from 'react';
    import Demo from './demo';
    import { ThemeProvider, createTheme } from '@mui/material/styles';
    import { StylesProvider, jssPreset } from '@mui/styles';
    import { CacheProvider } from '@emotion/react';
    import createCache from '@emotion/cache';
    import { create } from 'jss';
    import { render } from 'react-dom';
    
    const theme = createTheme();
    
    class MyWebComponent extends HTMLElement {
      connectedCallback() {
        const shadowRoot = this.attachShadow({ mode: 'open' });
        const emotionRoot = document.createElement('style');
        const mountPoint = document.createElement('div');
        shadowRoot.appendChild(emotionRoot);
        const reactRoot = shadowRoot.appendChild(mountPoint);
    
        const jss = create({
          ...jssPreset(),
          insertionPoint: reactRoot,
        });
    
        const cache = createCache({
          key: 'css',
          prepend: true,
          container: emotionRoot,
        });
    
        render(
          <StylesProvider jss={jss}>
            <CacheProvider value={cache}>
              <ThemeProvider theme={theme}>
                <Demo />
              </ThemeProvider>
            </CacheProvider>
          </StylesProvider>,
          mountPoint
        );
      }
    }
    if (!customElements.get('my-element')) {
      customElements.define('my-element', MyWebComponent);
    }

Upvotes: 5

Related Questions