Material-ui: [withStyles] Add the ability to get the component's props from within the style object

Created on 2 Aug 2017  路  39Comments  路  Source: mui-org/material-ui

Description

When using createStyleSheet, there is no way (at least that i know of) to access the component's props. I think this is important, as sometimes it is wanted to pass sizes, url images, and other stylings as props to the component.

Today the only solution to this is to separate these stuff into inline-styles, but as far as i know, material-ui v1 uses jss and react-jss, and those two already gives you the possibility of access the props via functions that receive the props and then return the desired style, like that:

const styles = {
  button: {
    background: props => props.color
  },
  root: {
    backgroundImage: props => `url(${props.backgroundImage})`
  }
}

// Reference: https://github.com/cssinjs/react-jss#example

What do you think of implementing something like that on material-ui too?

enhancement important

Most helpful comment

I'm adding this issue in the v1.x milestone. A lot of people are requesting it. We will soon get to it.

capture d ecran 2018-07-29 a 13 12 03

All 39 comments

react-jss implement that feature. You can always use it over our styling solution. The real motivation for implementing that feature here should be around simpler/better internal implementation of the components.

react-jss injectStyles() does this too, but it seems to be that it would be better to add props to the StyleRuleCallback.

const styles = (props, theme) => ({})
That way you are not limited to only values depending on props.

@Shamplol feedback makes me think that we could be taking advantage of this new feature to allow users providing a custom color to our components, instead of the predefined one. The CSS size impact is unclear but it would make the component color override even simpler. This is something to investigate. Then, once css variables browser support is high enough (2 years from now?), we can rely on it.

@oliviertassinari wouldn't the css size actually decrease in some cases?
As i get it, we currently define all classes for ...Primary and ...Accent - wouldn't this change mean, that we would only have to maintain classes for ...Colorized? Or are you concerned about the generated css?

Either way, I think this would hugely improve dx as we have to basically reimplement complex classes like https://github.com/callemall/material-ui/blob/v1-beta/src/Button/Button.js#L11 when we want to use non palette colors.

wouldn't the css size actually decrease in some cases?

@sakulstra Hard to tell. It will depend on the implementation. Maybe :).

From a TypeScript typing perspective, it would be nice if _both_ props and theme could be accessed this way within a styles specification:

const styles = {
  button: {
    background: ({ theme }) => theme.palette.primary[200]
  },
  root: {
    backgroundImage: ({ props }) => `url(${props.backgroundImage})`
  }
};

The reason is that TypeScript frequently fails to infer the right type for withStyles when it is passed a function object, so you have to provide extra type annotations to make it work, e.g.

withStyles<'button' | 'root'>(theme => ...)

If props are passed in too, this will become

withStyles<'button' | 'root', Props>((theme, props) => ...)

What's the current status of this? It would be really nice to have that feature

@lucasmafra I have added this feature to the post v1 release milestone. The sooner we can release v1, the better.

This functionality is key to being able to write expressive style rules that combine props, media queries, and interactive states. You can't replace that functionality with inline styles. Unfortunately, withStyles is unusable in any of my projects until this gets added.

You can always setup a custom theme, and use this convention: We really like how styled components gives you access to the props.theme out of the box by nesting ThemeProvider inside MuiThemeProvider when both called theme={theme}. It extends the default theme that mui exposes

//inline with HOC Method
 h1 style= {{ 'color: this.props.theme.palette.primary[500]' }}

//styled components ( no method necessary )
const Heading = styled.h1`
  color: ${p => p.theme.palette.primary['500']};
`

I just needed to use function values, so I used react-jss' injectSheet() function. I am used to use Mui's withStyles() function, so is there any drawback when using injectSheet instead of withStyles?

@damien-monni You don't get access to your MUI theme.

Hello all, just wanted to share my version of the Material-UI styled-components-like API. It's not "heavily" tested but it seems to work well and provides:

  • Hoisted props and theme to style function.
  • Passing custom props to wrapped component
  • Forwarding refs via React.forwardRef
    (EDIT: Refs are only currently working for html tags, not most base Material-UI components, this requires internal support. Might work on a PR when I have time, would require accepting function or object prop-types, and doing checks on stateless components whether to pass through to child )

Related issues: #10825, #7633

styled.js

import React, { Component } from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import { withStyles } from 'material-ui/styles';

function styled(WrappedComponent, customProps) {
  return (style, options) => {
    class StyledComponent extends Component {
      render() {
        let {
          forwardRef,
          classes,
          className,
          ...passThroughProps
        } = this.props;
        return (
          <WrappedComponent
            ref={forwardRef}
            className={classNames(classes.root, className)}
            {...passThroughProps}
            {...customProps}
          />
        );
      }
    }

    StyledComponent.propTypes = {
      classes: PropTypes.object.isRequired,
      className: PropTypes.string,
    };

    let hoistedProps = {};

    const styles =
      typeof style === 'function'
        ? theme => {
            return { root: style({ ...theme, theme, props: hoistedProps }) };
          }
        : { root: style };

    const WithRef = withStyles(styles, options)(StyledComponent);

    return React.forwardRef((props, ref) => {
      hoistedProps = props;
      return <WithRef {...props} forwardRef={ref} />;
    });
  };
}

export default styled;

Example use

const MobileNav = styled('nav')(({ theme, props }) => ({
  '&.pos-fixed': {
    top: 0,
    position: props.fixed ? 'fixed' : 'absolute',
    zIndex: 99,
    animation: 'fadeInDown 0.3s ease-out forwards',
    backgroundColor: theme.palette.common.white,
  },
}));

With custom props

const StyledButton = styled(Button, { component: Link })(
  ({ theme, props }) => ({
    color: props.disabled ? 'gray' : 'blue',
    [theme.breakpoints.down('sm')]: {
      color: 'red',
    },
  })
);

@oliviertassinari To solve this problem, I'm using the solution below:

    componentWillMount() {
        const {color} = this.props;
        jss.setup(jssPreset());
        const stylesheet = jss.createStyleSheet({
            underline: {
              "&:after": {
                backgroundColor: `${color} !important`
              }
            }
        }).attach();
        this.underlineClass = stylesheet.classes.underline;
    }

It works great but are there some potential issues I'm not seeing? I don't like that I have to call jss.setup twice for instance 馃槄. I'm not sure I understand jss lifecycle. I was surprised that I needed to invoke setup() here.

It works great but are there some potential issues I'm not seeing?

@wcandillon Some potential issue I can see: 1. You will inject new class names everytime an instance of the component is mounted. 2. You won't be able to server-side render your component. 3. You won't get the correct CSS override injection order.

I developped this library to have props with style:

https://github.com/JacquesBonet/jss-material-ui

Tested successfully on a project.

At first, I use danielmahon solution, but get style inheritance problem.

@oliviertassinari Do we have an alternative approach for creating dynamic css / animations at the moment? Thanks :)

@HunderlineK Some alternatives: https://material-ui.com/customization/overrides/#2-dynamic-variation-for-a-one-time-situation.

@danielmahon your approach is just what I'm looking for right now to solve my problem, though the "Styled Component" does not re render when it receive new props. Have you tried anything else?

I'll think of something different, and if I come up with something I'll let you know

馃挵 Just attached a $50 bounty to that one :)

https://www.bountysource.com/issues/47838203-withstyles-add-the-ability-to-get-the-component-s-props-from-within-the-style-object

Like @lewisdiamond said, const styles = (props, theme) => ({}) would be really neat.

Or const styles = (theme, props) => ({}) to be non-breaking maybe.

I'm adding this issue in the v1.x milestone. A lot of people are requesting it. We will soon get to it.

capture d ecran 2018-07-29 a 13 12 03

@oliviertassinari this probably has significant implications for the typing of withStyles, let me know if I can help with that

@pelotom Thanks, I will let you know. I hope I can start looking at the issue this month.

Is there work in progress regarding this? It's a key feature IMO, maybe I could help with it.

EDIT: I started to work on it. Managed to pass props to the styles function that withStyles accepts, only problem is styles are not updated when props change. Will create a PR when that is solved.

Hi I just came across a use case where i needed this to customize the colors of the avatar component and there is no other way to control the style for all the variants of the component other than this.

const styles = theme => ({
  chip:{
  },
  separator: {
    marginRight: theme.spacing.unit,
  },
  fright: {
    float: 'right',
  },
  fleft: {
    float: 'left',
  },
  outlinedPrimary:{
    color: props =>  stringToColor( props.username),
    border: props => `1px solid ${fade(stringToColor(props.username), 0.5)}`,
    '$clickable&:hover, $clickable&:focus, $deletable&:focus': props => ({
      backgroundColor: fade(stringToColor(props.username), theme.palette.action.hoverOpacity),
      border: `1px solid ${stringToColor(props.username)}`,
      }),
  },
  outlined: {
    backgroundColor: 'transparent',
    border: props => `1px solid ${
      theme.palette.type === 'light' ? stringToColor(props.username) : fade(stringToColor(props.username))
    }`,
    '$clickable&:hover, $clickable&:focus, $deletable&:focus': props => ({
      backgroundColor: fade(stringToColor(props.username), theme.palette.action.hoverOpacity),
      }),
    },
});

You can check my solution Japrogramer: https://github.com/JacquesBonet/jss-material-ui

thanks, I will take a look at it.

I needed this feature earlier today so i wrote a HOC. withStyles does some caching on its own so i can't really tell how much this affect that but i will look at the caching implementation of withStyles in my spare time, for now anyone looking for a quick way to get props and theme to play nice there you go

Warning

This component will do a full remount if props change something to do with the index number of the stylesheet class changing or somthing in the withStyles HOC

import React from 'react';
import { withStyles } from '@material-ui/core/styles';

const { createElement, forwardRef } = React

const withPropsStyles = ( style ) => {

    const withPropsStyles = ( component ) => {

        return forwardRef( (props, ref) => {

            const proxy = (theme) => style(props, theme)

            const hoc = withStyles(proxy)(component)

            return props.children ? 
                createElement(hoc, { ...props, ref}, props.children) :
                createElement(hoc, {...props, ref}) 
        }) 
    }

    return withPropsStyles
}

export default withPropsStyles

Example use

const styles = (props, theme) => ({
    root:{
        backgroundColor: props.light ? theme.palette.primary.light : theme.palette.primary.main 
    },
})

const SomeComponent = ({classes}) => <div className={classes.root}/>

export default withPropsStyles(styles)(SomeComponent)

conclusion

Simple and works(but rem the full remount cost too)

With this change can we remove inline style usage from the library in favor of 100% JSS? My app does not work with inline styles and when I went to replace the ripple effect with JSS I realized I needed this feature. Maybe a small performance hit, but seems cleaner.

@koshea ditch the inline style. I also really don't like inline styles too, it should work just fine as a drop in replacement for withStyles as a decorator or as in the example.

I also wanted to mention, that using inline styles doesn't allow to enable strong Content Security Policy.
It requires adding unsafe-inline flag to styles directives, which is not secure.

Dynamic styles with props support should resolve that problem

Hi guys, sorry to jump in to the discussion "just like this". I recently started using Mui (with Typescript) and whilst I find it an extremely powerful library, it certainly has its complexities.

I noticed in some comments above that there's a bit of a discussion for whether this feature should be (props, theme) => {} or (theme, props) => {}. I'd like to reinforce what @pelotom said about making both props and theme named properties in his comment. Making it that way will probably make it easier for us to refactor the style definitions once this change lands (which I'm really looking forward to). Cheers 馃檪

Thank you everyone for the patience! This issue is being taken care of in #13503. It's a requirement for the component helpers we want to implement. We have also started experimenting with the hook API: https://twitter.com/olivtassinari/status/1058805751404261376.

Did this make it into 4.0? It looks like the makeStyles callback does not have a props parameter.

@city41 const classes = useStyles(props);

I see. So it looks like it is

const useStyles = makeStyles(theme => {
    return {
        foo: {
            color: theme.props.danger ? '#ff0000' : '#00ff00'
        }
    };
});

function MyComponent(props) {
    const classes = useStyles(props);

    return <div className={classes.foo}>...</div>;
}

I don't see this documented in the styles API section on the website. Let me see if I can send a PR.

@city41 There is a start of a documentation in https://material-ui.com/styles/basics/#adapting-based-on-props.

cool, glad to see the docs are getting updated. for anyone else coming to this issue, here is how I combined theme and props to style a component

import React from 'react';
import { Button, Theme, makeStyles } from '@material-ui/core';

interface ButtonProps {
    destructive: boolean;
}

const useButtonStyles = makeStyles<Theme, ButtonProps>(theme => {
    return {
        root: props => ({
            backgroundColor: props.destructive ? theme.palette.error.main : theme.palette.primary.main
        })
    };
});

export const PrimaryButton: React.FunctionComponent<ButtonProps> = props => {
    const classes = useButtonStyles(props);

    return <Button {...props} className={classes.root} variant="contained" />;
};

How can i use prop in an external styles json file ?

for example this is external file

const typographyStyle = {
  title2: {
    fontFamily:"Montserrat",
    fontStyle:"normal",
    fontWeight:"800",
    fontSize:"72px",
    lineHeight:"88px",
    letterSpacing:"-0.02em",
    // color:"#304C82"
    color : props => {
      console.log(props,'c1');
      return props.color
    }

  }
};

export default typographyStyle;

i import this file and spread the object

import typographyStyle from "../assets/jss/material-kit-pro-react/views/componentsSections/typographyStyle";


const styles = theme =>  ({
    ...typographyStyle,
    homeSearch:{
        width: '100%',
        '& div':{
            '& input':{
                "color":"#304C82",
                height: 65,
                fontFamily: 'Open Sans',
                fontStyle: 'normal',
                fontWeight: 800,
                fontSize: '48px',
                lineHeight: '65px',
                letterSpacing: '-0.03em',
                '&::placeholder':{
                    fontFamily: 'Open Sans',
                    fontStyle: 'normal',
                    fontWeight: 800,
                    fontSize: '48px',
                    lineHeight: '65px',
                    letterSpacing: '-0.03em',
                    color: '#EAEAEA'
                }
            }
        }
    },

  });

now on color function i get props = {} .

can someone help me in this regard ?

UPDATE:

seems like i am doing something wrong cause i am getting empty object even in my main styles.js file

    homeSearch: props => {
        console.log(props);
        return {
        width: '100%',
        border:  `1px solid ${props.color}`
        ,
        '& div':{
            '& input':{
                "color":"#304C82",
                height: 65,
                fontFamily: 'Open Sans',
                fontStyle: 'normal',
                fontWeight: 800,
                fontSize: '48px',
                lineHeight: '65px',
                letterSpacing: '-0.03em',
                '&::placeholder':{
                    fontFamily: 'Open Sans',
                    fontStyle: 'normal',
                    fontWeight: 800,
                    fontSize: '48px',
                    lineHeight: '65px',
                    letterSpacing: '-0.03em',
                    color: '#EAEAEA'
                }
            }
        }
    }
},
Was this page helpful?
0 / 5 - 0 ratings