Home > Software engineering >  How to create insertion point to mount styles in shadow dom for MUI material v5 in React custom elem
How to create insertion point to mount styles in shadow dom for MUI material v5 in React custom elem

Time:11-03

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) enter image description here

CodePudding user response:

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