How would I apply Material-UI managed styles to non-material-ui, non-react elements?
Asked Answered
D

2

11

I have an application where I'm using Material UI and its theme provider (using JSS).

I'm now incorporating fullcalendar-react, which isn't really a fully fledged React library - it's just a thin React component wrapper around the original fullcalendar code.

That is to say, that I don't have access to things like render props to control how it styles its elements.

It does however, give you access to the DOM elements directly, via a callback that is called when it renders them (eg. the eventRender method).

Here's a basic demo sandbox.

enter image description here

Now what I'm wanting to do is make Full Calendar components (eg, the buttons) share the same look and feel as the rest of my application.

One way to do this, is that I could manually override all of the styles by looking at the class names it's using and implementing the style accordingly.

Or - I could implement a Bootstrap theme - as suggested in their documentation.

But the problem with either of these solutions, is that that:

  1. It would be a lot of work
  2. I would have synchronisation problems, if I made changes to my MUI theme and forgot to update the calendar theme they would look different.

What I would like to do is either:

  • Magically convert the MUI theme to a Bootstrap theme.
  • Or create a mapping between MUI class names and the calendar class names, something like:
.fc-button = .MuiButtonBase-root.MuiButton-root.MuiButton-contained
.fc-button-primary= .MuiButton-containedPrimary

I wouldn't mind having to massage the selectors etc to make it work (ie. For example - MUI Buttons have two internal spans, whereas Full Calendar have just one). It's mostly about when I change the theme - don't want to have to change it in two places.

Using something like Sass with its @extend syntax would is what I have in mind. I could create the full-calendar CSS with Sass easily enough - but how would Sass get access to the MuiTheme?

Perhaps I could take the opposite approach - tell MUI 'Hey these class names here should be styled like these MUI classes'.

Any concrete suggestions on how I would solve this?

Drue answered 6/12, 2019 at 5:19 Comment(1)
don't use a crutch like MUI - donePetree
T
4

Here is my suggestion (obviously, it's not straight forward). Take the styles from the MUI theme and generate style tag based on it using react-helmet. To do it event nicely, I created a "wrapper" component that do the map. I implemented only the primary rule but it can be extended to all the others.

This way, any change you will do in the theme will affect the mapped selectors too.

import React from "react";
import { Helmet } from "react-helmet";

export function MuiAdapter({ theme }) {
  if (!theme.palette) {
    return <></>;
  }
  return (
    <Helmet>
      <style type="text/css">{`
          .fc-button-primary {
            background: ${theme.palette.primary.main}
          }
          /* more styles go here */
      `}</style>
    </Helmet>
  );
}

And the use of the adapter

<MuiAdapter theme={theme} />

Working demo: https://codesandbox.io/s/reverent-mccarthy-3o856

screen shot

Twig answered 10/12, 2019 at 10:7 Comment(3)
I like this thinking. The problem with this solution is that you would still basically be implementing all the styles yourself (ie. the hover color, the padding, the border radius etc). If for example, you decided the button text should be underlined, you would need to remember to add the text-decoration property to the helmet.Drue
I understand what you ask for. I tried to take a different approach and manipulate the MUI style tags and add them .fc- selectors according the map but they have conflicts in the base levels (such as display, padding etc.) so I don't see how it will work even if you would have the option to migrate it in scss. For example: codesandbox.io/s/charming-sound-3xmj9Twig
Haha - now this I like. I'll give it a better look later on.Drue
A
3

You could create a mapping between MUI class names and the calendar class names by going through ref's. It's possible that this is not what some would call "best practice"...but it's a solution :). Note that I updated your component from a functional component to a class component, but you could accomplish this with hooks in a functional component.

Add refs

Add a ref to the MUI element you want to set as a reference, in your case the Button.

<Button
  color="primary"
  variant="contained"
  ref={x => {
    this.primaryBtn = x;
  }}
>

And a ref to a wrapping div around the component you want to map to. You can't add it directly to the component since that wouldn't give us access to children.

<div
  ref={x => {
    this.fullCal = x;
  }}
>
  <FullCalendar
    ...
  />
</div>

Map classes

From componentDidMount() add whatever logic you need to target the correct DOM node (for your case, I added logic for type and matchingClass). Then run that logic on all FullCalendar DOM nodes and replace the classList on any that match.

componentDidMount() {
  this.updatePrimaryBtns();
}

updatePrimaryBtns = () => {
  const children = Array.from(this.fullCal.children);
  // Options
  const type = "BUTTON";
  const matchingClass = "fc-button-primary";

  this.mapClassToElem(children, type, matchingClass);
};

mapClassToElem = (arr, type, matchingClass) => {
  arr.forEach(elem => {
    const { tagName, classList } = elem;
    // Check for match
    if (tagName === type && Array.from(classList).includes(matchingClass)) {
      elem.classList = this.primaryBtn.classList.value;
    }

    // Run on any children
    const next = elem.children;
    if (next.length > 0) {
      this.mapClassToElem(Array.from(next), type, matchingClass);
    }
  });
};

This is maybe a little heavy handed, but it meets your future proof requirement for when you updated update Material UI. It would also allow you to alter the classList as you pass it to an element, which has obvious benefits.

Caveats

If the 'mapped-to' component (FullCalendar) updated classes on the elements you target (like if it added .is-selected to a current button) or adds new buttons after mounting then you'd have to figure out a way to track the relevant changes and rerun the logic.

I should also mention that (obviously) altering classes might have unintended consequences like a breaking UI and you'll have to figure out how to fix them.

Here's the working sandbox: https://codesandbox.io/s/determined-frog-3loyf

Aspirant answered 12/12, 2019 at 21:12 Comment(3)
This works. Note that you don't have to convert to a class component - you can use hooks to do this.Drue
Ya you're correct, this can be done with hooks in a functional component. I've updated the answer to reflect that.Aspirant
Nice, quite the pragmatic approach. But not really feasible as you would always need a ref.Young

© 2022 - 2024 — McMap. All rights reserved.