react-dnd not detecting hover and drop events - reactjs

I'm having trouble getting react-dnd to to work. Specifically, while I can confirm dragging is being detected properly, my droppable targets are not detecting hover or drop events. I've been following the example at http://survivejs.com/react/implementing-kanban/drag-and-drop/ to create a draggable item. I've tried to use a combination of the same examples and the official examples from the official repo to create a DropTarget to accept the draggable. However, my DropTarget is giving no indication that it is detecting the draggable. My code below has multiple debugger statements to indicate if code is being reached, but none of them ever are.
I suspect that the compose call at the end might be the problem, but I'm following Dan Abramov's example here. Just to add to the problem, the React inspector in Chrome dev tools lets me see the itemType state variable change as I drag an item. However, both the canDrop and isOver state variables remain false. I'd appreciate any help to get this to work.
import { findDOMNode } from 'react-dom';
import React, { Component } from 'react';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import Paper from 'material-ui/Paper';
import FaDelete from 'react-icons/lib/fa/trash-o';
import RaisedButton from 'material-ui/RaisedButton';
import FaEdit from 'react-icons/lib/fa/star';
import actions from '../actions/actions';
import TextField from 'material-ui/TextField';
import { connect } from 'react-redux';
//import EmojiPickerPopup from './EmojiPickerPopup';
import RenderIf from 'render-if';
import globals from '../globals';
import { DropTarget } from 'react-dnd';
import { compose } from 'redux';
const locationItemContainer = {
display: 'flex',
flexDirection: 'column',
backgroundColor: 'lightgoldenrodyellow',
border: '1px solid Moccasin',
width: "33%",
maxHeight: "15em"
}
const controlsContainer = {
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-around',
width: "100%"
}
const paperStyle = {
padding: '8px 4px',
display: 'flex',
flexDirection: "column",
alignItems: 'center',
justifyContent: 'center',
width: "100%",
height: "100%"
};
class LocationItemComponent extends Component {
constructor(props, context) {
super(props, context);
this.state = {
locationMarkers: []
}
}
componentWillReceiveProps(nextProps) {
if (!this.props.isOver && nextProps.isOver) {
// You can use this as enter handler
debugger
}
if (this.props.isOver && !nextProps.isOver) {
// You can use this as leave handler
debugger
}
if (this.props.isOverCurrent && !nextProps.isOverCurrent) {
// You can be more specific and track enter/leave
// shallowly, not including nested targets
debugger
}
}
nameChanged = (id, event, value) => {
this.props.dispatch(actions.storyMapActions.updateMarkerName(value, id));
}
deleteMarker = (id) => {
this.props.dispatch(actions.storyMapActions.deleteMarker(id));
}
showEmojiPicker = (id, event) => {
this.props.dispatch(actions.modalsActions.showEmojiPicker(id, event.currentTarget))
}
render() {
const { isOver, canDrop, connectDropTarget } = this.props;
if (isOver) {
console.log("is over");
}
return connectDropTarget(
<div style={locationItemContainer}>
<MuiThemeProvider>
<Paper zDepth={5}
style={paperStyle}
rounded={false}>
<TextField
id="markerName"
hintText="marker Name"
onChange={this.nameChanged.bind(this, this.props.marker.id)}
value={this.props.marker.name}
underlineFocusStyle={{ color: globals.textUnderlineColor }}
/>
<div style={controlsContainer}>
<RaisedButton
icon={<FaEdit />}
primary={true}
onClick={this.showEmojiPicker.bind(this, this.props.marker.id)} />
<RaisedButton
icon={<FaDelete />}
secondary={true}
onClick={this.deleteMarker.bind(this, this.props.marker.id)} />
</div>
</Paper>
</MuiThemeProvider>
</div>
);
}
}
const mapStateToProps = (state) => {
return Object.assign({}, { state: state });
}
const locationTarget = {
canDrop(props, monitor) {
debugger;
// You can disallow drop based on props or item
const item = monitor.getItem();
return true;
},
hover(props, monitor, component) {
debugger;
// This is fired very often and lets you perform side effects
// in response to the hover. You can't handle enter and leave
// here—if you need them, put monitor.isOver() into collect() so you
// can just use componentWillReceiveProps() to handle enter/leave.
// You can access the coordinates if you need them
const clientOffset = monitor.getClientOffset();
const componentRect = findDOMNode(component).getBoundingClientRect();
// You can check whether we're over a nested drop target
const isJustOverThisOne = monitor.isOver({ shallow: true });
// You will receive hover() even for items for which canDrop() is false
const canDrop = monitor.canDrop();
},
drop(props, monitor, component) {
debugger;
if (monitor.didDrop()) {
// If you want, you can check whether some nested
// target already handled drop
debugger
return;
}
// Obtain the dragged item
const item = monitor.getItem();
// You can do something with it
//ChessActions.movePiece(item.fromPosition, props.position);
// You can also do nothing and return a drop result,
// which will be available as monitor.getDropResult()
// in the drag source's endDrag() method
return { moved: true };
}
};
const collect = (connect, monitor) => {
return {
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
itemType: monitor.getItemType()
};
}
export default compose(
connect(mapStateToProps),
DropTarget(globals.itemTypes.LOCATION_ITEM, locationTarget, collect)
)(LocationItemComponent);

Related

How can I change my data fetching strategy to avoid stale data on initial dynamic route renders?

I'm building a Next.js app which allows users to create and view multiple Kanban boards. There are a couple of different ways that a user can view their different boards:
On the Home page of the app, users see a list of boards that they can click on.
The main navigation menu has a list of boards users can click on.
Both use Next.js Link components.
Clicking the links loads the following dynamic page: src/pages/board/[boardId].js The [boardId].js page fetches the board data using getServerSideProps(). An effect fires on route changes, which updates the redux store. Finally, the Board component uses a useSelector() hook to pull the data out of Redux and render it.
The problem I'm experiencing is that if I click back and forth between different boards, I see a brief flash of the previous board's data before the current board data loads. I am hoping someone can suggest a change I could make to my approach to alleviate this issue.
Source code:
// src/pages/board/[boardId].js
import React, { useEffect } from 'react'
import { useDispatch } from 'react-redux'
import Board from 'Components/Board/Board'
import { useRouter } from 'next/router'
import { hydrateTasks } from 'Redux/Reducers/TaskSlice'
import { unstable_getServerSession } from 'next-auth/next'
import { authOptions } from 'pages/api/auth/[...nextauth]'
import prisma from 'Utilities/PrismaClient'
const BoardPage = ({ board, tasks }) => {
const router = useRouter()
const dispatch = useDispatch()
useEffect(() => {
dispatch(hydrateTasks({ board, tasks }))
}, [router])
return (
<Board />
)
}
export async function getServerSideProps ({ query, req, res }) {
const session = await unstable_getServerSession(req, res, authOptions)
if (!session) {
return {
redirect: {
destination: '/signin',
permanent: false,
},
}
}
const { boardId } = query
const boardQuery = prisma.board.findUnique({
where: {
id: boardId
},
select: {
name: true,
description: true,
id: true,
TO_DO: true,
IN_PROGRESS: true,
DONE: true
}
})
const taskQuery = prisma.task.findMany({
where: {
board: boardId
},
select: {
id: true,
title: true,
description: true,
status: true,
board: true
}
})
try {
const [board, tasks] = await prisma.$transaction([boardQuery, taskQuery])
return { props: { board, tasks } }
} catch (error) {
console.log(error)
return { props: { board: {}, tasks: [] } }
}
}
export default BoardPage
// src/Components/Board/Board.js
import { useEffect } from 'react'
import { useStyletron } from 'baseui'
import Column from 'Components/Column/Column'
import ErrorBoundary from 'Components/ErrorBoundary/ErrorBoundary'
import useExpiringBoolean from 'Hooks/useExpiringBoolean'
import { DragDropContext } from 'react-beautiful-dnd'
import Confetti from 'react-confetti'
import { useDispatch, useSelector } from 'react-redux'
import useWindowSize from 'react-use/lib/useWindowSize'
import { moveTask } from 'Redux/Reducers/TaskSlice'
import { handleDragEnd } from './BoardUtilities'
import { StyledBoardMain } from './style'
const Board = () => {
const [css, theme] = useStyletron()
const dispatch = useDispatch()
useEffect(() => {
document.querySelector('body').style.background = theme.colors.backgroundPrimary
}, [theme])
// get data from Redux
const { boardDescription, boardName, columnOrder, columns, tasks } = useSelector(state => state?.task)
// set up a boolean and a trigger to control "done"" animation
const { boolean: showDone, useTrigger: doneUseTrigger } = useExpiringBoolean({ defaultState: false })
const doneTrigger = doneUseTrigger({ duration: 4000 })
// get width and height for confetti animation
const { width, height } = useWindowSize()
// curry the drag end handler for the drag and drop UI
const curriedDragEnd = handleDragEnd({ dispatch, action: moveTask, handleOnDone: doneTrigger })
return (
<ErrorBoundary>
<DragDropContext onDragEnd={curriedDragEnd}>
<div className={css({
marginLeft: '46px',
marginTop: '16px',
fontFamily: 'Roboto',
display: 'flex',
alignItems: 'baseline'
})}>
<h1 className={css({ fontSize: '22px', color: theme.colors.primary })}>{boardName}</h1>
{boardDescription &&
<p className={css({ marginLeft: '10px', color: theme.colors.primary })}>{boardDescription}</p>
}
</div>
<StyledBoardMain>
{columnOrder.map(columnKey => {
const column = columns[columnKey]
const tasksArray = column.taskIds.map(taskId => tasks[taskId])
return (
<Column
column={columnKey}
key={`COLUMN_${columnKey}`}
tasks={tasksArray}
title={column.title}
status={column.status}
/>
)
})}
</StyledBoardMain>
</DragDropContext>
{showDone && <Confetti
width={width}
height={height}
/>}
</ErrorBoundary>
)
}
export default Board
// src/pages/index.tsx
import React, {PropsWithChildren} from 'react'
import {useSelector} from "react-redux";
import {authOptions} from 'pages/api/auth/[...nextauth]'
import {unstable_getServerSession} from "next-auth/next"
import CreateBoardModal from 'Components/Modals/CreateBoard/CreateBoard'
import Link from 'next/link'
import {useStyletron} from "baseui";
const Index: React.FC = (props: PropsWithChildren<any>) => {
const {board: boards} = useSelector(state => state)
const [css, theme] = useStyletron()
return boards ? (
<>
<div style={{marginLeft: '46px', fontFamily: 'Roboto', width: '600px'}}>
<h1 className={css({fontSize: '22px'})}>Boards</h1>
{boards.map(({name, description, id}) => (
<Link href="/board/[boardId]" as={`/board/${id}`} key={id}>
<div className={css({
padding: '20px',
marginBottom: '20px',
borderRadius: '6px',
background: theme.colors.postItYellow,
cursor: 'pointer'
})}>
<h2 className={css({fontSize: '20px'})}>
<a className={css({color: theme.colors.primary, width: '100%', display: 'block'})}>
{name}
</a>
</h2>
<p>{description}</p>
</div>
</Link>
))}
</div>
</>
) : (
<>
<h1>Let's get started</h1>
<button>Create a board</button>
</>
)
}
export async function getServerSideProps(context) {
const session = await unstable_getServerSession(context.req, context.res, authOptions)
if (!session) {
return {
redirect: {
destination: '/signin',
permanent: false,
},
}
}
return {props: {session}}
}
export default Index
It looks like there's only ever one board in the redux. You could instead use a namespace so that you don't have to keep swapping different data in and out of the store.
type StoreSlice = {
[boardId: string]: Board;
}
Then the "brief flash" that you will see will either be the previous data for the correct board, or nothing if it has not yet been fetched.

How to test React component that uses React Hooks useHistory hook with Enzyme?

I am trying to test a React component using Enzyme. Tests worked fine until we converted the component to hooks. Now I am getting the error, "Error: Uncaught [TypeError: Cannot read property 'history' of undefined]"
I have already read through the following similar issues and wasn't able to solve it:
React Jest/Enzyme Testing: useHistory Hook Breaks Test
testing react component with enzyme
Jest & Hooks : TypeError: Cannot read property 'Symbol(Symbol.iterator)' of undefined
Also this article:
* https://medium.com/7shifts-engineering-blog/testing-usecontext-react-hook-with-enzyme-shallow-da062140fc83
Full component, AccessBarWithRouter.jsx:
/**
* #description Accessibility bar component to allow user to jump focus to different components on screen.
* One dropdown will focus to elements on screen.
* The other will route you to routes in your navigation bar.
*
*/
import React, { useState, useEffect, useRef } from 'react';
import Dropdown from 'react-dropdown-aria';
import { useHistory } from 'react-router-dom';
const AccessBarWithRouter = () => {
const pathname = useHistory().location.pathname;
const [sectionInfo, setSectionInfo] = useState(null);
const [navInfo, setNavInfo] = useState(null);
const [isHidden, setIsHidden] = useState(true);
// creating the refs to change focus
const sectionRef = useRef(null);
const accessBarRef = useRef(null);
// sets focus on the current page from the 1st dropdown
const setFocus = e => {
const currentLabel = sectionInfo[e];
const currentElement = document.querySelector(`[aria-labelledBy='${currentLabel}']`);
currentElement.tabIndex = -1;
sectionRef.current = currentElement;
// can put a .click() after focus to focus with the enter button
// works, but gives error
sectionRef.current.focus();
};
// Changes the page when selecting a link from the 2nd dropdown
const changeView = e => {
const currentPath = navInfo[e];
const accessLinks = document.querySelectorAll('.accessNavLink');
accessLinks.forEach(el => {
if (el.pathname === currentPath) {
el.click();
};
});
};
// event handler to toggle visibility of AccessBar and set focus to it
const accessBarHandlerKeyDown = e => {
if (e.altKey && e.keyCode === 191) {
if (isHidden) {
setIsHidden(false)
accessBarRef.current.focus();
} else setIsHidden(true);
}
}
/**
*
* useEffect hook to add and remove the event handler when 'alt' + '/' are pressed
* prior to this, multiple event handlers were being added on each button press
* */
useEffect(() => {
document.addEventListener('keydown', accessBarHandlerKeyDown);
const navNodes = document.querySelectorAll('.accessNavLink');
const navValues = {};
navNodes.forEach(el => {
navValues[el.text] = el.pathname;
});
setNavInfo(navValues);
return () => document.removeEventListener('keydown', accessBarHandlerKeyDown);
}, [isHidden]);
/**
* #todo figure out how to change the dropdown current value after click
*/
useEffect(() => {
// selects all nodes with the aria attribute aria-labelledby
setTimeout(() => {
const ariaNodes = document.querySelectorAll('[aria-labelledby]');
let sectionValues = {};
ariaNodes.forEach(node => {
sectionValues[node.getAttribute('aria-labelledby')] = node.getAttribute('aria-labelledby');
});
setSectionInfo(sectionValues);
}, 500);
}, [pathname]);
// render hidden h1 based on isHidden
if (isHidden) return <h1 id='hiddenH1' style={hiddenH1Styles}>To enter navigation assistant, press alt + /.</h1>;
// function to create dropDownKeys and navKeys
const createDropDownValues = dropDownObj => {
const dropdownKeys = Object.keys(dropDownObj);
const options = [];
for (let i = 0; i < dropdownKeys.length; i++) {
options.push({ value: dropdownKeys[i]});
}
return options;
};
const sectionDropDown = createDropDownValues(sectionInfo);
const navInfoDropDown = createDropDownValues(navInfo);
return (
<div className ='ally-nav-area' style={ barStyle }>
<div className = 'dropdown' style={ dropDownStyle }>
<label htmlFor='component-dropdown' tabIndex='-1' ref={accessBarRef} > Jump to section: </label>
<div id='component-dropdown' >
<Dropdown
options={ sectionDropDown }
style={ activeComponentDDStyle }
placeholder='Sections of this page'
ariaLabel='Navigation Assistant'
setSelected={setFocus}
/>
</div>
</div>
<div className = 'dropdown' style={ dropDownStyle }>
<label htmlFor='page-dropdown'> Jump to page: </label>
<div id='page-dropdown' >
<Dropdown
options={ navInfoDropDown }
style={ activeComponentDDStyle }
placeholder='Other pages on this site'
ariaLabel='Navigation Assistant'
setSelected={ changeView }
/>
</div>
</div>
</div>
);
};
/** Style for entire AccessBar */
const barStyle = {
display: 'flex',
paddingTop: '.1em',
paddingBottom: '.1em',
paddingLeft: '5em',
alignItems: 'center',
justifyContent: 'flex-start',
zIndex: '100',
position: 'sticky',
fontSize: '.8em',
backgroundColor: 'gray',
fontFamily: 'Roboto',
color: 'white'
};
const dropDownStyle = {
display: 'flex',
alignItems: 'center',
marginLeft: '1em',
};
/** Style for Dropdown component **/
const activeComponentDDStyle = {
DropdownButton: base => ({
...base,
margin: '5px',
border: '1px solid',
fontSize: '.5em',
}),
OptionContainer: base => ({
...base,
margin: '5px',
fontSize: '.5em',
}),
};
/** Style for hiddenH1 */
const hiddenH1Styles = {
display: 'block',
overflow: 'hidden',
textIndent: '100%',
whiteSpace: 'nowrap',
fontSize: '0.01px',
};
export default AccessBarWithRouter;
Here is my test, AccessBarWithRouter.unit.test.js:
import React from 'react';
import Enzyme, { mount } from 'enzyme';
import AccessBarWithRouter from '../src/AccessBarWithRouter.jsx';
import Adapter from 'enzyme-adapter-react-16';
Enzyme.configure({ adapter: new Adapter() });
describe('AccessBarWithRouter component', () => {
it('renders hidden h1 upon initial page load (this.state.isHidden = true)', () => {
const location = { pathname: '/' };
const wrapper = mount(
<AccessBarWithRouter location={location}/>
);
// if AccessBarWithRouter is hidden it should only render our invisible h1
expect(wrapper.exists('#hiddenH1')).toEqual(true);
})
it('renders full AccessBarWithRouter when this.state.isHidden is false', () => {
// set dummy location within test to avoid location.pathname is undefined error
const location = { pathname: '/' };
const wrapper = mount(
<AccessBarWithRouter location={location} />
);
wrapper.setState({ isHidden: false }, () => {
// If AccessBar is not hidden the outermost div of the visible bar should be there
// Test within setState waits for state change before running test
expect(wrapper.exists('.ally-nav-area')).toEqual(true);
});
});
});
I am new to React Hooks so trying to wrap my mind around it. My understanding is that I have to provide some sort of mock history value for my test. I tried creating a separate useContext file as so and wrapping it around my component in the test, but that didn't work:
import React, { useContext } from 'react';
export const useAccessBarWithRouterContext = () => useContext(AccessBarWithRouterContext);
const defaultValues = { history: '/' };
const AccessBarWithRouterContext = React.createContext(defaultValues);
export default useAccessBarWithRouterContext;
My current versions of my devDependencies:
"#babel/cli": "^7.8.4",
"#babel/core": "^7.8.6",
"#babel/polyfill": "^7.0.0-beta.51",
"#babel/preset-env": "^7.8.6",
"#babel/preset-react": "^7.8.3",
"babel-core": "^7.0.0-bridge.0",
"babel-jest": "^25.1.0",
"enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1",
"jest": "^25.1.0",
"react": "^16.13.0",
"react-dom": "^16.13.0"
I'm not finding much documentation for testing a component utilizing the useHistory hook in general. It seems Enzyme only started working with React Hooks a year ago, and only for mock, not for shallow rendering.
Anyone have any idea how I can go about this?
The problem here comes from inside of useHistory hook as you can imagine. The hook is designed to be used in consumers of a router provider. If you know the structure of Providers and Consumers, it'll make perfect sense to you that, here the consumer (useHistory) is trying to access some information from provider, which doesn't exist in your text case.
There are two possible solutions:
Wrap your test case with a router
it('renders hidden h1 upon initial page load (this.state.isHidden = true)', () => {
const location = { pathname: '/' };
const wrapper = mount(
<Router>
<AccessBarWithRouter location={location}/>
</Router>
)
});
Mock useHistory hook with a fake history data
jest.mock('react-router-dom', () => {
const actual = require.requireActual('react-router-dom')
return {
...actual,
useHistory: () => ({ methods }),
}
})
I personally prefer the 2nd one as you can place it in setupTests file and forget about it.
If you need to mock it or spy on it, you can overwrite the mock from setupTests file in your specific unit test file.

How do you access the Material UI theme inside event handlers?

I have event handlers for things like onClick or onFocus, and I can't figure out how to use the theme inside of the handler code. I want to change the color of an iconButton and I don't want to hard-code the color because we want components that can be general use, and eventually work with themes using completely different colors.
Tried using withTheme in addition to withStyles, so I can get the theme inside of the render(), but I can't get to it from a handler called from that rendering. Tried passing it, calling as a prop, declaring constants based upon theme values in the class (both inside and outside of render), nothing.
I don't know if this is possible, or not built in, or what. I'm hoping that I'm just missing something.
Environment: CodeSandBox, so CreateReactApp. Material-UI plus React-Select, withStyles and withTheme (useTheme help here?).
handleInfoClick = (e) => {
if (this.instructionsContent.current.style.display !== "block") {
this.instructionsContent.current.style.display = "block";
this.instructionsButton.current.style.color = "#f9be00"; //works
} else {
this.instructionsContent.current.style.display = "none";
this.instructionsButton.current.style.color = this.theme.palette.text.disabled; // doesn't work
also tried this:
handleSelectFocus = () => {
if (this.state.visited === false) {
this.instructionsContent.current.style.display = "block";
this.instructionsButton.current.style.color = this.activeButtonColor;
this.setState({ visited: true });
}
};
...
render() {
const { theme } = this.props;
...
const activeButtonColor = theme.palette.secondary.main;
Finally, also tried to use the classes I can use within render(), but it doesn't recognize those either:
const styles = theme => ({
...
infoButton: {
position: "absolute",
bottom: 0,
left: 0,
marginBottom: 20,
width: 48,
color: theme.palette.text.disabled,
"&:active": {
color: theme.palette.secondary.main
}
},
infoButtonActive: {
position: "absolute",
bottom: 0,
left: 0,
marginBottom: 20,
width: 48,
color: theme.palette.secondary.main
},
....
Hoping one of these approaches would give me a color for my <IconButton> - from my theme:
<div className={classes.infoButtonDiv}>
<IconButton
aria-label="Instructions"
className={classes.infoButton}
buttonRef={this.instructionsButton}
onClick={this.handleInfoClick}
>
<HelpOutline />
</IconButton>
</div>
(in a different theme.js file applied to the root element:
const theme = createMuiTheme({
typography: {
fontFamily: ["Roboto", '"Helvetica Neue"', "Arial", "sans-serif"].join(",")
},
palette: {
primary: {
main: "#00665e"
},
secondary: {
main: "#f9be00"
}
},
overrides: {
LeftNav: {
drawerDiv: {
backgroundColor: "#00665e",
width: 300
}
}
},
direction: "ltr",
typography: {
useNextVariants: true
}
});
Triggering a state change onClick will update the color, but only if you pass one of the supported values for the IconButton color prop ("primary" or "secondary").
import React, { Component } from "react";
import IconButton from "#material-ui/core/IconButton";
import DeleteIcon from "#material-ui/icons/Delete";
class ButtonStyle extends Component {
constructor(props) {
super(props);
this.state = {
buttonColor: "primary"
};
}
handleClick = e => {
this.setState({
buttonColor: "secondary"
});
};
render() {
const buttonColor = this.state.buttonColor;
return (
<div>
<IconButton
aria-label="Delete"
color={buttonColor}
onClick={this.handleClick}
>
<DeleteIcon />
</IconButton>
</div>
);
}
}
export default ButtonStyle;

How to use hls.js with react

I need some help trying to figure how to use hls.js in react.
Let me explain the situation I have to fetch the m3u8 from an api I'm able to make it work from a basic html with the <script> tag but when i try to implement it on react it doesn't work any help is appreciated. This is what I've got so far:
import React, { Component } from "react";
import PropTypes from "prop-types";
import { withStyles } from "#material-ui/core/styles";
import Paper from "#material-ui/core/Paper";
import Typography from "#material-ui/core/Typography";
import Grid from "#material-ui/core/Grid";
import ButtonBase from "#material-ui/core/ButtonBase";
import CircularProgress from "#material-ui/core/CircularProgress";
import Hls from "hls.js";
const styles = theme => ({
root: {
flexGrow: 1,
paddingTop: theme.spacing.unit * 2,
paddingBottom: theme.spacing.unit * 2,
marginBottom: 24,
marginLeft: 24,
marginRight: 60
},
image: {
marginLeft: 24,
width: 200,
height: 200
},
img: {
display: "block",
width: 200,
height: 200,
maxWidth: "100%",
maxHeight: "100%"
},
detail: {
marginLeft: 16
},
progress: {
display: "flex",
justifyContent: "center",
alignItems: "center"
}
});
class Video extends Component {
constructor(props) {
super(props);
}
componentWillReceiveProps(props) {
if (props.episode && this.player) {
var hlsUrl = props.episode.assets.hls;
var video = this.player;
if (video.canPlayType("application/vnd.apple.mpegurl")) {
// If HLS is natively supported, let the browser do the work!
video.src = "hlsUrl";
video.addEventListener("loadedmetadata", function() {
video.play();
});
} else if (Hls.isSupported()) {
// If the browser supports MSE, use hls.js to play the video
var hls = new Hls({
// This configuration is required to insure that only the
// viewer can access the content by sending a session cookie
// to api.video service
xhrSetup: function(xhr, url) {
xhr.withCredentials = true;
}
});
hls.loadSource(hlsUrl);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, function() {
video.play();
});
} else {
alert("Please use a modern browser to play the video");
}
}
}
handleSerieClick = () => {
this.props.history.push("/" + this.props.serie.apiName);
};
_onTouchInsidePlayer() {
if (this.player.paused) {
this.player.play();
} else {
this.player.pause();
}
}
render() {
const { classes, theme } = this.props;
if (this.props.episode) {
const { assets, title, description, videoId } = this.props.episode;
return (
<Grid className={classes.root} item xs={12}>
<video
controls
onClick={this._onTouchInsidePlayer}
ref={player => (this.player = player)}
autoPlay={true}
/>
</Grid>
);
} else {
return (
<Grid className={classes.progress} item xs={12}>
<CircularProgress size={100} />
</Grid>
);
}
}
}
export default withStyles(styles, { withTheme: true })(Video);
This is the code that works with <script> tag
<script src="https://cdn.jsdelivr.net/npm/hls.js#latest"></script>
<video id="video"></video>
<script>
var hlsUrl = 'https://cdn.libcast.net/stream/3de8ff01-18f7-4262-a1f2-abeeb9bb962b/hls/manifest.hls';
var video = document.getElementById('video');
if (video.canPlayType('application/vnd.apple.mpegurl')) {
// If HLS is natively supported, let the browser do the work!
video.src = 'hlsUrl';
video.addEventListener('loadedmetadata',function() {
video.play();
});
} else if (Hls.isSupported()) {
// If the browser supports MSE, use hls.js to play the video
var hls = new Hls({
// This configuration is required to insure that only the
// viewer can access the content by sending a session cookie
// to api.video service
xhrSetup: function(xhr, url) { xhr.withCredentials = true; }
});
hls.loadSource(hlsUrl);
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED,function() {
video.play();
});
} else {
alert('Please use a modern browser to play the video');
}
</script>
I pass the hls source from a parent component in the props and in componentWillRecieveProps i try to use the source to run the player
EDIT
THe problem seems to be that <video> tag is undefined when I try to apply the source.
Initing hls in componentWillReceiveProps is probably "too early". Refs are created during the render execution so your this.video is probably null at that time.
Try moving your logic into componentDidMount (if you pass proper props from the beginnig) or at least componentDidUpdate.

Passing props to MUI styles

Given the Card code as in here. How can I update the card style or any material UI style as from:
const styles = theme => ({
card: {
minWidth: 275,
},
To such follows:
const styles = theme => ({
card: {
minWidth: 275, backgroundColor: props.color
},
when I tried the latest one, I got
Line 15: 'props' is not defined no-undef
when I updated code to be :
const styles = theme => (props) => ({
card: {
minWidth: 275, backgroundColor: props.color
},
also
const styles = (theme ,props) => ({
card: {
minWidth: 275, backgroundColor: props.color
},
Instead of
const styles = theme => ({
card: {
minWidth: 275, backgroundColor: props.color
},
I got the component card style at the web page messy.
By the way, I pass props as follows:
<SimpleCard backgroundColor="#f5f2ff" />
please help!
Deleted the old answer, because it's no reason for existence.
Here's what you want:
import React from 'react';
import { makeStyles } from '#material-ui/core';
const useStyles = makeStyles({
firstStyle: {
backgroundColor: props => props.backgroundColor,
},
secondStyle: {
color: props => props.color,
},
});
const MyComponent = ({children, ...props}) =>{
const { firstStyle, secondStyle } = useStyles(props);
return(
<div className={`${firstStyle} ${secondStyle}`}>
{children}
</div>
)
}
export default MyComponent;
Now you can use it like:
<MyComponent color="yellow" backgroundColor="purple">
Well done
</MyComponent>
Official Documentation
Solution for how to use both props and theme in material ui :
const useStyles = props => makeStyles( theme => ({
div: {
width: theme.spacing(props.units || 0)
}
}));
export default function ComponentExample({ children, ...props }){
const { div } = useStyles(props)();
return (
<div className={div}>{children}</div>
);
}
Here the Typescript solution:
import React from 'react';
import { makeStyles } from '#material-ui/core/styles';
import Button from '#material-ui/core/Button';
import {Theme} from '#material-ui/core';
export interface StyleProps {
height: number;
}
const useStyles = makeStyles<Theme, StyleProps>(theme => ({
root: {
background: 'green',
height: ({height}) => height,
},
}));
export default function Hook() {
const props = {
height: 48
}
const classes = useStyles(props);
return <Button className={classes.root}>Styled with Hook API</Button>;
}
If you want to play with it, try it in this CodeSandbox
In MUI v5, this is how you access the props when creating the style object using styled():
import { styled } from "#mui/material";
const StyledBox = styled(Box)(({ theme, myColor }) => ({
backgroundColor: myColor,
width: 30,
height: 30
}));
For folks who use typescript, you also need to add the prop type to the CreateStyledComponent:
type DivProps = {
myColor: string;
};
const Div = styled(Box)<DivProps>(({ theme, myColor }) => ({
backgroundColor: myColor,
width: 30,
height: 30
}));
<StyledBox myColor="pink" />
If you want to use system props in your custom component like Box and Typography, you can use extendSxProp like the example below:
import { unstable_extendSxProp as extendSxProp } from "#mui/system";
const StyledDiv = styled("div")({});
function DivWithSystemProps(inProps) {
const { sx } = extendSxProp(inProps);
return <StyledDiv sx={sx} />;
}
<DivWithSystemProps
bgcolor="green"
width={30}
height={30}
border="solid 1px red"
/>
Explanation
styled("div")(): Add the sx props to your custom component
extendSxProp(props): Gather the top level system props and put it inside the sx property:
const props = { notSystemProps: true, color: 'green', bgcolor: 'red' };
const finalProps = extendSxProp(props);
// finalProps = {
// notSystemProps: true,
// sx: { color: 'green', bgcolor: 'red' }
// }
To use with typescript, you need to add the type for all system properties:
type DivSystemProps = SystemProps<Theme> & {
sx?: SxProps<Theme>;
};
function DivWithSystemProps(inProps: DivSystemProps) {
const { sx, ...other } = extendSxProp(inProps);
return <StyledDiv sx={sx} {...other} />;
}
Here's the official Material-UI demo.
And here's a very simple example. It uses syntax similar to Styled Components:
import React from "react";
import { makeStyles, Button } from "#material-ui/core";
const useStyles = makeStyles({
root: {
background: props => props.color,
"&:hover": {
background: props => props.hover
}
},
label: { fontFamily: props => props.font }
});
export function MyButton(props) {
const classes = useStyles(props);
return <Button className={classes.root} classes={{ label: classes.label }}>My Button</Button>;
}
// and the JSX...
<MyButton color="red" hover="blue" font="Comic Sans MS" />
This demo uses makeStyles, but this feature is also available in styled and withStyles.
This was first introduced in #material-ui/styles on Nov 3, 2018 and was included in #material-ui/core starting with version 4.
This answer was written prior to version 4.0 severely out of date!
Seriously, if you're styling a function component, use makeStyles.
The answer from James Tan is the best answer for version 4.x
Anything below here is ancient:
When you're using withStyles, you have access to the theme, but not props.
Please note that there is an open issue on Github requesting this feature and some of the comments may point you to an alternative solution that may interest you.
One way to change the background color of a card using props would be to set this property using inline styles. I've forked your original codesandbox with a few changes, you can view the modified version to see this in action.
Here's what I did:
Render the component with a backgroundColor prop:
// in index.js
if (rootElement) {
render(<Demo backgroundColor="#f00" />, rootElement);
}
Use this prop to apply an inline style to the card:
function SimpleCard(props) {
// in demo.js
const { classes, backgroundColor } = props;
const bull = <span className={classes.bullet}>•</span>;
return (
<div>
<Card className={classes.card} style={{ backgroundColor }}>
<CardContent>
// etc
Now the rendered Card component has a red (#F00) background
Take a look at the Overrides section of the documentation for other options.
#mui v5
You can use styled() utility (Make sure that you're importing the correct one) and shouldForwardProp option.
In the following example SomeProps passed to a div component
import { styled } from '#mui/material'
interface SomeProps {
backgroundColor: 'red'|'blue',
width: number
}
const CustomDiv = styled('div', { shouldForwardProp: (prop) => prop !== 'someProps' })<{
someProps: SomeProps;
}>(({ theme, someProps }) => {
return ({
backgroundColor: someProps.backgroundColor,
width: `${someProps.width}em`,
margin:theme.spacing(1)
})
})
import React from "react";
import { makeStyles } from "#material-ui/styles";
import Button from "#material-ui/core/Button";
const useStyles = makeStyles({
root: {
background: props => props.color,
"&:hover": {
background: props => props.hover
}
}
});
export function MyButton(props) {
const classes = useStyles({color: 'red', hover: 'green'});
return <Button className={classes.root}>My Button</Button>;
}
Was missing from this thread a props use within withStyles (and lead to think it wasn't supported)
But this worked for me (say for styling a MenuItem):
const StyledMenuItem = withStyles((theme) => ({
root: {
'&:focus': {
backgroundColor: props => props.focusBackground,
'& .MuiListItemIcon-root, & .MuiListItemText-primary': {
color: props => props.focusColor,
},
},
},
}))(MenuItem);
And then use it as so:
<StyledMenuItem focusColor={'red'} focusBackground={'green'}... >...</StyledMenuItem>
I spent a couple of hours trying to get withStyles to work with passing properties in Typescript. None of the solutions I found online worked with what I was trying to do, so I ended up knitting my own solution together, with snippets from here and there.
This should work if you have external components from, lets say Material UI, that you want to give a default style, but you also want to reuse it by passing different styling options to the component:
import * as React from 'react';
import { Theme, createStyles, makeStyles } from '#material-ui/core/styles';
import { TableCell, TableCellProps } from '#material-ui/core';
type Props = {
backgroundColor?: string
}
const useStyles = makeStyles<Theme, Props>(theme =>
createStyles({
head: {
backgroundColor: ({ backgroundColor }) => backgroundColor || theme.palette.common.black,
color: theme.palette.common.white,
fontSize: 13
},
body: {
fontSize: 12,
},
})
);
export function StyledTableCell(props: Props & Omit<TableCellProps, keyof Props>) {
const classes = useStyles(props);
return <TableCell classes={classes} {...props} />;
}
It may not be the perfect solution, but it seems to work. It's a real bugger that they haven't just amended withStyles to accept properties. It would make things a lot easier.
Solution for TypeScript with Class Component:
type PropsBeforeStyle = {
propA: string;
propB: number;
}
const styles = (theme: Theme) => createStyles({
root: {
color: (props: PropsBeforeStyle) => {}
}
});
type Props = PropsBeforeStyle & WithStyles<typeof styles>;
class MyClassComponent extends Component<Props> {...}
export default withStyles(styles)(MyClassComponent);
export const renderButton = (tag, method, color) => {
const OkButton = withStyles({
root: {
"color": `${color}`,
"filter": "opacity(0.5)",
"textShadow": "0 0 3px #24fda39a",
"backgroundColor": "none",
"borderRadius": "2px solid #24fda3c9",
"outline": "none",
"border": "2px solid #24fda3c9",
"&:hover": {
color: "#24fda3c9",
border: "2px solid #24fda3c9",
filter: "opacity(1)",
},
"&:active": {
outline: "none",
},
"&:focus": {
outline: "none",
},
},
})(Button);
return (
<OkButton tag={tag} color={color} fullWidth onClick={method}>
{tag}
</OkButton>
);
};
renderButton('Submit', toggleAlert, 'red')
Here's another way of dynamically passing props to hook's API in MUI v5
import React from "react";
import { makeStyles } from "#mui/styles";
import { Theme } from "#mui/material";
interface StyleProps {
height: number;
backgroundColor: string;
}
const useStyles = makeStyles<Theme>((theme) => ({
root: ({ height, backgroundColor }: StyleProps) => ({
background: backgroundColor,
height: height
})
}));
export default function Hook() {
const props = {
height: 58,
backgroundColor: "red"
};
const classes = useStyles(props);
return (
<button className={classes.root}>
another way of passing props to useStyle hooks
</button>
);
}
here's the codesandbox https://codesandbox.io/s/styles-with-props-forked-gx3bf?file=/demo.tsx:0-607
Here's 2 full working examples of how to pass props to MUI v5 styles. Either using css or javascript object syntax.
With css syntax:
import { styled } from '#mui/system'
interface Props {
myColor: string
}
const MyComponent = ({ myColor }: Props) => {
const MyStyledComponent = styled('div')`
background-color: ${myColor};
.my-paragraph {
color: white;
}
`
return (
<MyStyledComponent >
<p className="my-paragraph">Hello there</p>
</MyStyledComponent >
)
}
export default MyComponent
Note that we define MyStyledComponent within MyComponent, making the scoped props available to use in the template string of the styled() function.
Same thing with javascript object syntax:
import { styled } from '#mui/system'
const MyComponent = ({ className }: any) => {
return (
<div className={className}>
<p className="my-paragraph">Hello there</p>
</div>
)
}
interface Props {
myColor: string
}
const MyStyledComponent = styled(MyComponent)((props: Props) => ({
backgroundColor: props.myColor,
'.my-paragraph': { color: 'white' },
}))
export default MyStyledComponent
For this second example, please note how we pass the className to the component we want to apply the styles to. The styled() function will pass a className prop with the styles you define. You typically want to apply that to your root element. In this case the div.
Result:
I'm sure there's other variations of how to do this, but these two are easy to implement and understand.
You might need to memoize the calculated styles, and maybe not use this approach if your props changes a lot. I don't think it's very performant.
Using styled-components, sometimes you only want to apply styles if the prop is passed, in such cases you can do something like this (remove : {foo: boolean} if not using TypeScript):
const SAnchor = styled("a", {
shouldForwardProp: prop => prop !== "foo",
})(({foo = false}: {foo: boolean}) => ({
...(foo && {
color: "inherit",
textDecoration: "none",
}),
}));
Object spread source
shouldForwardProp docs
#mui v5
I use theme and prop from JSON object
const Answerdiv = styled((props) => {
const { item_style, ...other } = props;
return <div {...other}></div>;
})(({ theme, item_style }) => {
for(var i of Object.keys(item_style)){
item_style[i] =Object.byString(theme,item_style[i])|| item_style[i];
}
return (item_style)
});
component use
<Answerdiv item_style={(item_style ? JSON.parse(item_style) : {})}>
for Object.byString
Object.byString = function(o, s) {
s = s.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
s = s.replace(/^\./, ''); // strip a leading dot
var a = s.split('.');
for (var i = 0, n = a.length; i < n; ++i) {
var k = a[i];
if (k in o) {
o = o[k];
} else {
return;
}
}
return o;
}

Resources