material-ui-next - Dynamically set palette color - reactjs

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

Related

Published styled-components UI library does not have access to extended theme types on the consumer side

I am creating UI library using styled-components. I am extending the DefaultTheme type to support custom themes that are needed for our use case. Both theme and theme definition is coming from a different internal package that I use. When working on the UI library components, it works correctly. The issue begins when we tried to use the theming on the consumer side.
The problem is that, it seems that types are not really extended to the consumer correctly, so without adding styled.d.ts file on the client side, it doesn't seem to understand the types of the theme. Theme get a type anyand it should be read asDefaultTheme type.
I wonder if there is any way to expose the extended types to consumer so they don't have to add this additional file on their side?
Is there anyone out there who had similar problem? Maybe you could share your findings?
Here is my setup:
Design System project:
// theme.ts
import tokens from 'my-external-package/variables.json';
import type { ThemeProps } from 'styled-components';
import type { MyThemeType } from 'my-external-package//theme';
const { light, dark } = tokens.color.theme;
export const lightTheme = {
color: light,
};
export const darkTheme = {
color: dark,
};
export const defaultTheme = lightTheme;
// styled.d.ts
import {} from 'styled-components';
import type { MyThemeType } from 'my-external-package//theme';
// extend theme
declare module 'styled-components' {
// eslint-disable-next-line #typescript-eslint/no-empty-interface
export interface DefaultTheme extends MyThemeType {}
}
// CustomThemeProvider.tsx
import React, { createContext, useState, ReactNode, useContext } from 'react';
import { ThemeProvider } from 'styled-components';
import { lightTheme, darkTheme } from './theme';
const themes = {
light: lightTheme,
dark: darkTheme,
};
type CustomThemeProviderProps = {
children: ReactNode;
defaultTheme?: keyof typeof themes;
};
const themeContext = createContext({ toggleTheme: () => {} });
const { Provider } = themeContext;
export const CustomThemeProvider = ({
children,
defaultTheme = 'light',
}: CustomThemeProviderProps) => {
const [currentTheme, setCurrentTheme] = useState(defaultTheme);
return (
<Provider
value={{
toggleTheme: () =>
setCurrentTheme((current) => current === 'light' ? 'dark' : 'light'),
}}
>
<ThemeProvider theme={themes[currentTheme]}>{children}</ThemeProvider>
</Provider>
);
};
// I also export hook over here so I can use it on the client side
export const useToggleTheme = () => {
const { toggleTheme } = useContext(themeContext);
return toggleTheme;
};
App consumer NextJs
//_app.tsx
import type { AppProps } from 'next/app';
import { CustomThemeProvider } from 'my-library-package/theming';
function MyApp({ Component, pageProps }: AppProps) {
return (
<CustomThemeProvider defaultTheme='light'>
<Component {...pageProps} />
</CustomThemeProvider>
);
}
export default MyApp;
// consumer_page.tsx
import type { NextPage } from 'next';
import { useCallback, useState } from 'react';
import styled from 'styled-components';
import tokens from 'my-external-package/variables.json';
import { useToggleTheme, Switch } from 'my-library-package';
const CustomComponent = styled.p`
color: ${({ theme }) => theme.color.feedback.success.foreground};
`;
const MyPage: NextPage = () => {
const toggleTheme = useToggleTheme();
return (
<>
<Switch onChange={toggleTheme}/>
<CustomComponent>This component have access to theme</CustomComponent>
</>
)
}
export default MyPage;
We are considering re-export utilities from styled-components with the right DefaultTheme and instruct consumers not to install styled-components
Instruct design-system consumers to create a styled.d.ts file to get the theme correctly populated.
Both of those seems rather painful. :(

How to change a SASS variable value using React Js?

Before making it duplicate question please make sure you read my question
I am asking this question in 2019, Where React Js documentation specify we can use SASS in our react project here's link
I want to switch between light theme and dark theme using dynamic variable which is control by user click
My React Code
import React from 'react';
import './App.scss';
class App extends React.Component {
render() {
return (
<div className="Home">
Iā€™m slow and smooth
<button onClick={() => console.log()}>Change theme</button>
</div>
);
}
}
export default App;
My SASS code:
$theme: light; // I want to control this variable
$bgcolor: #222222;
#if($theme== light) {
$bgcolor: white;
}
#else {
$bgcolor: black;
}
.Home {
background-color: $bgcolor;
height: 100vh;
}
in case you are still interested, you can kind of change a sass variable by instead using css variables
:root {
--app-primaryColor: #f49ad1;
--app-secondaryColor: #211f1e;
}
Then use these variables in your scss files
.button {
background-color: var(--app-primaryColor);
color: var(--app-secondaryColor);
}
and update them using React
document.documentElement.style.setProperty('--app-primaryColor', '#ffae00')
Here is a (almost) full example using react and redux. A setTheme action is used to update colors from the document root element. This way you can also configure your theme directly from your react root tag props. These props will be set as the initial state.
// index.html
<div
id="app"
primaryColor="red"
secondaryColor="#f2f2f2"
/>
// css-variables.scss
:root {
--app-primaryColor: #f49ad1;
--app-secondaryColor: #211f1e;
}
// app.module.scss
.button {
background-color: var(--app-primaryColor);
color: var(--app-secondaryColor);
}
// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import './css-variables'
import App from './app'
import configureStore from './configureStore'
const rootElement = document.getElementById('app')
//Here you could extract your theme variables from your rootElement props and set it as an initial redux state
const initialProps = {}
const store = configureStore(initialProps)
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
rootElement)
// app.js
import React from 'react'
import { connect } from 'react-redux'
import { setTheme } from './actions'
import styles from './app.module'
class App extends React.Component {
componentDidMount() {
//In case you have an initial state set from your rootElement
const { theme, setTheme } = this.props
setTheme(theme)
}
generateTheme() {
const primaryColors = ['#ffae00', '#f49ad1', '#d0666b', '#7c6cd0', '#6cd09d', '#d0ba6c']
const secondaryColors = ['#4c4c4e', '#2f2f2f', '#dcdcdc', '#fff']
return {
primaryColor: primaryColors[Math.floor(Math.random() * primaryColors.length)]
secondaryColor: secondaryColors[Math.floor(Math.random() * secondaryColors.length)]
}
}
onButtonClick() {
const theme = this.generateTheme()
this.props.setTheme(theme)
}
render() {
return (
<div className="{styles.button}" onClick={this.onButtonClick.bind(this)}>
Change theme
</div>
)
}
}
const mapStateToProps = (state) => ({
theme: state.theme,
})
export default connect(mapStateToProps, { setTheme })(App)
// actions.js
export const setTheme = theme => dispatch => {
//You change your theme vars directly from the root element
Object.keys(theme)
.filter(prop => typeof theme[prop] !== 'undefined' && theme[prop] !== null)
.forEach(prop => document.documentElement.style.setProperty(`--app-${prop}`, theme[prop]))
dispatch({
type: 'THEME/SET',
payload: theme
})
}

How should the new context api work with React Native navigator?

I created a multiscreen app using React Navigator following this example:
import {
createStackNavigator,
} from 'react-navigation';
const App = createStackNavigator({
Home: { screen: HomeScreen },
Profile: { screen: ProfileScreen },
});
export default App;
Now I'd like to add a global configuration state using the new builtin context api, so I can have some common data which can be manipulated and displayed from multiple screens.
The problem is context apparently requires components having a common parent component, so that context can be passed down to child components.
How can I implement this using screens which do not share a common parent as far as I know, because they are managed by react navigator?
You can make it like this.
Create new file: GlobalContext.js
import React from 'react';
const GlobalContext = React.createContext({});
export class GlobalContextProvider extends React.Component {
state = {
isOnline: true
}
switchToOnline = () => {
this.setState({ isOnline: true });
}
switchToOffline = () => {
this.setState({ isOnline: false });
}
render () {
return (
<GlobalContext.Provider
value={{
...this.state,
switchToOnline: this.switchToOnline,
switchToOffline: this.switchToOffline
}}
>
{this.props.children}
</GlobalContext.Provider>
)
}
}
// create the consumer as higher order component
export const withGlobalContext = ChildComponent => props => (
<GlobalContext.Consumer>
{
context => <ChildComponent {...props} global={context} />
}
</GlobalContext.Consumer>
);
On index.js wrap your root component with context provider component.
<GlobalContextProvider>
<App />
</GlobalContextProvider>
Then on your screen HomeScreen.js use the consumer component like this.
import React from 'react';
import { View, Text } from 'react-native';
import { withGlobalContext } from './GlobalContext';
class HomeScreen extends React.Component {
render () {
return (
<View>
<Text>Is online: {this.props.global.isOnline}</Text>
</View>
)
}
}
export default withGlobalContext(HomeScreen);
You can also create multiple context provider to separate your concerns, and use the HOC consumer on the screen you want.
This answer takes in consideration react-navigation package.
You have to wrap your App component with the ContextProvider in order to have access to your context on both screens.
import { createAppContainer } from 'react-navigation'
import { createStackNavigator } from 'react-navigation-stack'
import ProfileContextProvider from '../some/path/ProfileContextProvider'
const RootStack = createStackNavigator({
Home: { screen: HomeScreen },
Profile: { screen: ProfileScreen },
});
const AppContainer = createAppContainer(RootStack)
const App = () => {
return (
<ProfileContextProvider>
<AppContainer />
</ProfileContextProvider>);
}
https://wix.github.io/react-native-navigation/docs/third-party-react-context/
As RNN screens are not part of the same component tree, updating the values in the shared context does not trigger a re-render across all screens. However you can still use the React.Context per RNN screen component tree.
If you need to trigger a re-render across all screens, there are many popular third party libraries such as MobX or Redux.

material-ui : Extract color from theme

I want to use a color from my material-ui theme inside a component like that :
const MyComponent = props => (
<UsersIcon color={currentTheme.primary1Color} />
)
So, my need is to extract a value from the current provided theme.
I found a working solution to solve this case, using context to retrieve the current theme :
const MyComponent = (props, {muiTheme}) => (
<UsersIcon color={muiTheme.palette.primary1Color} />
)
contextTypes = {
muiTheme: PropTypes.object.isRequired,
}
The React context is used "under the hood" by material-ui, so my solution is not future proof ā€“ the implementation of MUI can change ā€“, is there any way to solve this in a proper (or recommended) way ?
You can access the theme variables with react hook or with higher-order component.
Example with hook:
//...
import { useTheme } from '#material-ui/core/styles';
const MyComponent = () => {
const theme = useTheme();
return <UsersIcon color={theme.palette.primary.main} />
}
Example with HOC:
//...
import { withTheme } from '#material-ui/core/styles';
const MyComponent = ({theme, ...other}) => {
return <UsersIcon color={theme.palette.primary.main} />
}
export default withTheme(MyComponent)
Don't forget to wrap root application component with ThemeProvider
Another method to mention is makeStyles for CSS-in-JS styling:
//...
import { makeStyles } from '#material-ui/core/styles'
const useStyles = makeStyles(theme => ({
icon: {
color: theme.palette.primary.main
}
}))
const MyComponent = () => {
const classes = useStyles()
return <UsersIcon className={classes.icon} />
}
Yes you have! using muiThemeable..
import muiThemeable from 'material-ui/styles/muiThemeable';
const MyComponent = props => (
<UsersIcon color={props.muiTheme.palette.primary1Color} />
)
export default muiThemeable()(MyComponent )
from material-ui docs
If your colors don't change at runtime, you can store these constants in a global object that gets used to initialize the theme as well as used in your custom components. This would allow you to not depend on context while keeping your code dry.

Changing material-ui theme on the fly --> Doesn't affect children

I'm working on a react/redux-application where I'm using material-ui.
I am setting the theme in my CoreLayout-component (my top layer component) using context (in accordance to the documentation). This works as expected on initial load.
I want to be able to switch themes during runtime. When I select a new theme, my redux store gets updated and therefore triggers my components to update. The problem is that the children of my CoreLayout-component doesn't get affected - the first time! If I repeatedly change my theme (using a select-list that sends out a redux-action onChange) the children are updated. If a child component is located 2 layers down in my project hierarchy, it is updated after 2 action calls - so there is some issue with how the context is passed down.
My CoreLayout.js component
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import ThemeManager from 'material-ui/lib/styles/theme-manager';
const mapStateToProps = (state) => ({
uiStatus: state.uiStatus
});
export class CoreLayout extends React.Component {
getChildContext() {
return {
muiTheme: ThemeManager.getMuiTheme(this.props.uiStatus.get("applicationTheme").toJS())
};
}
render () {
return (
<div className='page-container'>
{ this.props.children }
</div>
);
}
}
CoreLayout.propTypes = {
children: PropTypes.element
};
CoreLayout.childContextTypes = {
muiTheme: PropTypes.object
};
export default connect(mapStateToProps)(CoreLayout);
One of my child components (LeftNavigation.js)
import React from "react";
import { connect } from 'react-redux';
import { List, ListItem } from 'material-ui';
const mapStateToProps = (state) => ({
uiStatus: state.uiStatus
});
export class LeftNavigation extends React.Component {
render () {
return (
<div className="left-pane-navigation">
<List subheader="My Subheader" >
<ListItem primaryText="Search" />
<ListItem primaryText="Performance Load" />
</List>
</div>
);
}
}
LeftNavigation.contextTypes = {
muiTheme: React.PropTypes.object
};
export default connect(mapStateToProps)(LeftNavigation);
I can access the theme located in context by this.context.muiTheme.
I can get the component to update the theme by using another instance of getChildContext() inside each child component, but I will have such a large number of components that I would very much like to avoid having to do that.
My CoreLayout component's getChildContext-method is called when I change theme and all my child components gets re-rendered as expected.
Any ideas?
Update: It works as expected on mobile devices (at least iOS)
You can use muiThemeProvider to avoid having to add getChildContext to any child component, the provider does this for you.
...
import getMuiTheme from 'material-ui/styles/getMuiTheme';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import MyAwesomeReactComponent from './MyAwesomeReactComponent';
const App = () => (
<MuiThemeProvider muiTheme={getMuiTheme()}>
<MyAwesomeReactComponent />
</MuiThemeProvider>
)
...
More info in documentation.

Resources