Search code examples
reactjsmaterial-uicustom-elementreact-typescript

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


Solution

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