Customize withStyles Material UI React styled components - reactjs

I have a library of material-ui styled React components.
Styled components look like this:
import React from "react";
import withStyles from "#material-ui/core/styles/withStyles";
import headerStyle from "material-kit-pro-react/assets/jss/material-kit-pro-react/components/headerStyle.jsx";
class Header extends React.Component {
// ...
}
export default withStyles(headerStyle)(Header);
I want to customize the style of the component, but I want to keep the library untouched to avoid conflicts with future updates.
So I decided to create my own component:
import React from "react";
import withStyles from "#material-ui/core/styles/withStyles";
import Header from "material-kit-pro-react/components/Header/Header.jsx";
export default withStyles(theme => ({
primary: {
color: "#000"
}
}))(Header);
But with this approach the JSS engine creates two classes for the same component (Header-primary-25 WithStyles-Header--primary-12). That's not exactly a problem, but in my case there's a small conflict with a method in the component that expects only ONE class:
headerColorChange() {
const { classes, color, changeColorOnScroll } = this.props;
// ...
document.body
.getElementsByTagName("header")[0]
.classList.add(classes[color]) // classes[color] is a string with spaces (two classes)
;
// ...
}
Ok. It's not a big deal. I can modify that method and fix the problem.
But when I extend the class:
import React from "react";
import Header from "material-kit-pro-react/components/Header/Header.jsx";
export default class extends Header {
constructor(props) {
super(props);
this.headerColorChange = this.headerColorChange.bind(this);
}
headerColorChange() {
const { classes, color, changeColorOnScroll } = this.props
console.log(classes[color])
}
}
I get this error related to withStyles:
withStyles.js:125 Uncaught TypeError: Cannot read property '64a55d578f856d258dc345b094a2a2b3' of undefined
at ProxyComponent.WithStyles (withStyles.js:125)
at new _default (Header.jsx:11)
at new WithStyles(Header) (eval at ./node_modules/react-hot-loader/dist/react-hot-loader.development.js (http://localhost:8080/bundle.js:137520:54), <anonymous>:5:7)
And now I'm lost.
What's the best way to customize an already styled component?
How can I extend and override the constructor of a HoC (withStyles)?
Edit to provide a better and minimal example:
I have a component I can't modifiy because it's provided by a library:
./Component.jsx:
import React from "react";
import withStyles from "#material-ui/core/styles/withStyles";
class Component extends React.Component {
constructor(props) {
super(props);
this.headerColorChange = this.headerColorChange.bind(this);
}
componentDidMount() {
window.addEventListener("scroll", this.headerColorChange);
}
headerColorChange() {
const { classes } = this.props;
// THIS FAILS IF classes.primary IS NOT A VALID CLASS NAME
document.body.classList.add(classes.primary);
}
render() {
const { classes } = this.props;
return <div className={classes.primary}>Component</div>
}
}
export default withStyles(theme => ({
primary: {
color: "#000"
}
}))(Component);
I want to customize the primary color of the component. The straight way:
./CustomizedComponent.jsx:
import React from "react";
import withStyles from "#material-ui/core/styles/withStyles";
import Component from "./Component";
export default withStyles({
primary: {
color: "#00f"
}
})(Component)
Ok. The color of the has changed to #0ff. But then the component fails in document.body.classList.add(classes.primary) because classes.primary contains two class names concatenated (Component-primary-13 WithStyles-Component--primary-12). If I were allowed to modify the original component there would be no problem. But I can't, so I decided extend it and override the headerColorChange:
./CustomizedComponent.jsx:
import React from "react";
import withStyles from "#material-ui/core/styles/withStyles";
import Component from "./Component";
class MyComponent extends Component {
constructor(props) {
super(props)
this.headerColorChange = this.headerColorChange.bind(this);
}
headerColorChange() {
}
}
export default withStyles({
primary: {
color: "#00f"
}
})(MyComponent)
But then I get the error:
withStyles.js:125 Uncaught TypeError: Cannot read property '64a55d578f856d258dc345b094a2a2b3' of undefined
at ProxyComponent.WithStyles (withStyles.js:125)
at new MyComponent (Header.jsx:7)
at new WithStyles(Component) (eval at ./node_modules/react-hot-loader/dist/react-hot-loader.development.js (http://0.0.0.0:8080/bundle.js:133579:54), <anonymous>:5:7)
at constructClassInstance (react-dom.development.js:12484)
at updateClassComponent (react-dom.development.js:14255)
Questions are:
Is it possible to fully override the styles of the component so I get only one class name?
How can I extend a component returned by withStyles function as a higher order component?

Related

React JSS and TypeScript

I've been using React for a while, and now I want to switch to using React with TypeScript. However, I've grown used to JSS styles (via the react-jss package), and I can't understand how I'm supposed to use them with TypeScript. I also use the classnames package, to assign multiple class names conditionally, and I get TypeSCript errors for that.
Here is my React component template:
import React, { Component } from 'react';
import withStyles from 'react-jss';
import classNames from 'classnames';
const styles = theme => ({
});
class MyClass extends Component {
render() {
const { classes, className } = this.props;
return (
<div className={classNames({ [classes.root]: true, [className]: className})}>
</div>
);
}
};
export default withStyles(styles)(MyClass);
I'm just learning TypeScript, so I'm not sure I even understand the errors I get. How would I write something like the above in TypeScript?
UPDATE
Here is how I finally converted my template:
import React from 'react';
import withStyles, { WithStylesProps } from 'react-jss';
import classNames from 'classnames';
const styles = (theme: any) => ({
root: {
},
});
interface Props extends WithStylesProps<typeof styles> {
className?: string,
}
interface State {
}
class Header extends React.Component<Props, State> {
render() {
const { classes, className } = this.props;
return (
<div className={classNames({ [classes.root as string]: true, [className as string]: className})}>
</div>
);
}
};
export default withStyles(styles)(Header);
Things to keep in mind:
when defining the styles object, any member of classes that is referenced in the render method has to be defined. Without TypeScript, you could get away with "using" lots of classes and not defining them, like a placeholder; with TypeScript, they all have got to be there;
in a call to the classnames function, all the keys must be typed. If they come from a variable that could be null or undefined, you have to add as string, or to convert them to string otherwise. Other than this, the className property works the same as without TypeScript.
With TypeScript, you'll need to define your props as shown in here. It is also recommended to use function component if your React component only need render method
For your case, the code should look like this:
import React from 'react';
import withStyles, { WithStyles } from 'react-jss';
import classNames from 'classnames';
const styles = theme => ({
root: {
}
});
interface IMyClassProps extends WithStyles<typeof styles> {
className: string;
}
const MyClass: React.FunctionComponent<IMyClassProps> = (props) => {
const { classes, className } = props;
return (
<div className={classNames({ [classes.root]: true, [className]: className})}>
</div>
);
};
export default withStyles(styles)(MyClass);

How to avoid repeating interface in Typescript React

It's my first app I try to build using Typescript. I want to keep styles and components in separate files to make the code more descriptive and clear. Project will consist of dozens of components and I'll use props to call the classes. Each component will look more or less like this:
import * as React from 'react'
import withStyles from "#material-ui/core/styles/withStyles"
import { LandingPageStyles } from "./landing-page-styles"
interface LandingPageProps {
classes: any
}
class LandingPage extends React.Component<LandingPageProps> {
get classes() {
return this.props.classes;
}
render() {
return(
<div className={this.classes.mainPage}>
Hello Typescript
</div>
)
}
}
export default withStyles(LandingPageStyles)(LandingPage)
And simplified styles module :
import { createStyles } from "#material-ui/core";
export const LandingPageStyles = () => createStyles({
mainPage: {
textAlign: "center",
minHeight: "100vh",
}
})
In every component I want to have the classes props with type of any. Is there a way to avoid declaring interface for each component? It works now but I don't like my current solution beacuse of repetition the same code in every single component.
The proper way to do it is as bellow. Material-ui expose WithStyles interface that you can inherit to include classes props. The main advantage is that you IDE will handle autocompletion for the defined jss class. But anyway Typescript is more verbose than Javacript. With React you often have to repeat obvious things.
import * as React from 'react'
import {withStyles, WithStyles} from "#material-ui/core"
import { LandingPageStyles } from "./landing-page-styles"
interface LandingPageProps extends WithStyles<typeof LandingPageStyles> {
}
class LandingPage extends React.Component<LandingPageProps> {
get classes() {
return this.props.classes;
}
render() {
return(
<div className={this.classes.mainPage}>
Hello Typescript
</div>
)
}
}
export default withStyles(LandingPageStyles)(LandingPage)
The best solution is to declare interface which extends WithStyles . So in component there is need to declare:
import * as React from 'react'
import withStyles, { WithStyles } from "#material-ui/core/styles/withStyles"
import { LandingPageStyles } from "./landing-page-styles"
interface LandingPageProps extends WithStyles<typeof LandingPageStyles>{
}
class LandingPage extends React.Component<LandingPageProps> {
get classes() {
return this.props.classes;
}
render() {
return(
<div className={this.classes.mainPage}>
Hello Typescript
</div>
)
}
}
export default withStyles(LandingPageStyles)(LandingPage)

Referencing a React-Redux TypeScript component in another TypeScript component causes a "no export member" error

I have the following TypeScript (v3.01) React component named TopBar to which I'm now adding React-Redux. The TopBar component is then referenced in a parent component named Layout.
import * as React from 'react';
import { connect } from "react-redux";
import { decrementZoomLevel, incrementZoomLevel, setCenterPoint, setZoomLevel } from '../store/actions';
class TopBar extends React.Component<any, any>{
...
};
/*
* Redux-React setup
*/
const mapStateToProps = (state: any): any => {
return {
centerPoint: state.centerPoint,
zoomLevel: state.zoomLevel
}
}
export default connect(mapStateToProps)(TopBar);
However, when I reference this component in another Layout TypeScript component
import * as React from 'react';
import { TopBar } from './TopBar';
export class Layout extends React.Component<any, any> {
...
};
I get the following TypeScript error
(TS) Module: "C:/....../TopBar" has no exported member 'TopBar'.
Prior to adding the React-Redux code the class definition was
export default class TopBar extends React.Component<any, any>{
...
};
and I was able to reference the TopBar component in Layout w/ no errors.
Now that I'm adding the React-Redux connect() statement how do I properly reference TopBar in another component?
It is a default export, you have to import this way.
import * as React from 'react';
import TopBar from './TopBar';
export class Layout extends React.Component<any, any> {
...
};

material-ui-next - Dynamically set palette color

I am using "material-ui": "^1.0.0-beta.33" for my project.
What I want to do is set primary palette color dynamically inside a react component (color will be fetched from some api).
Basically I want to override below:
const theme = createMuiTheme({
palette: {
primary: "some color from api"
},
})
Is there a way to set this in componentDidMount function of any component?
Reference: https://material-ui-next.com/
I created a component that uses MuiThemeProvider and wrap my entire app around that component. Below is the structure of the component.
import React, {Component} from "react";
import {connect} from "react-redux";
import {withStyles} from 'material-ui/styles';
import * as colors from 'material-ui/colors';
import { MuiThemeProvider, createMuiTheme } from 'material-ui/styles';
import { withRouter } from 'react-router-dom';
export class ThemeWrapperComponent extends Component {
constructor(props){
super(props);
}
render(){
return (
<MuiThemeProvider theme={createMuiTheme(
{
palette: {
primary: { main: **colorFromApi** },
}
)}>
<div>
{ this.props.children }
</div>
</MuiThemeProvider>
)
}
}
export const ThemeWrapper = withRouter(connect(mapStateToProps)(ThemeWrapperComponent));
Below is how I wrapped my app around this component:
<ThemeWrapper>
<div>
<Routes/>
</div>
</ThemeWrapper>
Now, whatever colour you are sending from the api gets applied to the whole theme. More customisation can be done based on requirement.
I'm doing exactly this. Even got it working with WebMIDI using MIDI controller sliders and knobs just for fun.
The basic strategy is to use createMuiTheme and ThemeProvider and to store the theme in your application store (context, state, redux), etc.
class ThemeManager extends React.Component {
getThemeJson = () => this.props.context.themeJson || defaultThemeJson
componentDidMount () {
const themeJson = this.getThemeJson()
const theme = createMuiTheme(themeJson)
this.props.setContext({ theme, themeJson })
}
render () {
const { children, context } = this.props
const theme = context.theme
return theme
? <ThemeProvider theme={theme}>{children}</ThemeProvider>
: children
}
}
https://github.com/platform9/pf9-ui-plugin/blob/master/src/app/ThemeManager.js
and then you simply update your application's state.
handleImport = themeStr => {
const themeJson = JSON.parse(themeStr)
const theme = createMuiTheme(themeJson)
this.props.setContext({ theme, themeJson })
}
https://github.com/platform9/pf9-ui-plugin/blob/master/src/app/plugins/theme/components/ImportExportPanel.js#L17

autocomplete is undefined with material-ui

I have the following code in a jsx file and I get error:
Uncaught ReferenceError: AutoComplete is not defined
From what I see it should be working ok, Code:
import React, {Component} from 'react';
import { Autocomplete } from 'material-ui';
class MaterialUIAutocomplete extends Component {
constructor(props) {
super(props);
this.onUpdateInput = this.onUpdateInput.bind(this);
this.state = {
dataSource : [],
inputValue : ''
}
}
onUpdateInput(inputValue) {
}
render() {
return <AutoComplete
dataSource = {this.state.dataSource}
onUpdateInput = {this.onUpdateInput} />
}
}
export default MaterialUIAutocomplete;
It's a typo, you are importing Autocomplete and using AutoComplete.
Use these ways to import AutoComplete:
import { AutoComplete } from 'material-ui';
Or
import AutoComplete from 'material-ui/AutoComplete';
Update:
To render material-ui component we need to add the default theme and styling, include these lines in your component, like this:
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import getMuiTheme from 'material-ui/styles/getMuiTheme';
const muiTheme = getMuiTheme({});
Then render the AutoComplete inside MuiThemeProvider:
render() {
return <MuiThemeProvider muiTheme={muiTheme}>
<AutoComplete
dataSource = {this.state.dataSource}
onUpdateInput = {this.onUpdateInput} />
</MuiThemeProvider>
}
Use this:
import React, {Component} from 'react';
import AutoComplete from 'material-ui/AutoComplete';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import getMuiTheme from 'material-ui/styles/getMuiTheme';
const muiTheme = getMuiTheme({});
class MaterialUIAutocomplete extends Component {
constructor(props) {
super(props);
this.state = {
dataSource : [],
inputValue : ''
}
this.onUpdateInput = this.onUpdateInput.bind(this);
}
onUpdateInput(inputValue) {
}
render() {
return <MuiThemeProvider muiTheme={muiTheme}>
<AutoComplete
dataSource = {this.state.dataSource}
onUpdateInput = {this.onUpdateInput} />
</MuiThemeProvider>
}
}
export default MaterialUIAutocomplete;
Note: MuiThemeProvider is not required to include inside each component, you can use this in main page and then you can use any material-ui component inside any component.
this looks like a migration issue to me - you want to use the material-ui 0.x AutoComplete, but you have installed the new material-ui v1.x.
as such, you need to follow the Migration steps and in order to use any v0.x component, put this wherever you create/declare your themes:
<MuiThemeProvider theme={theme}>
<V0MuiThemeProvider muiTheme={themeV0}>
{/*Components*/}
</V0MuiThemeProvider>
Because the new 1.5 theme is available via props, the old one through context, you need to include both for AutoComplete to have reference to the old theme. I wouldn't do this unless you really need something from the old library, such as the AutoComplete widget.
https://material-ui.com/guides/migration-v0x/

Resources