Trying to get Material ui dialog into contextualised modal - reactjs

I have a modal component which I have put into context.
If I return html , some divs etc, then no problem, but
ideally I want to do this with the material-ui dialog.
But if I put in materail-ui dialog, as below,
then nothing is displayed.
The component:
import React, { useCallback, useEffect, useState } from 'react'
import {Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "#material-ui/core";
import {useTranslation} from "react-i18next";
import {errorStyles} from "../errorStyle";
// Create a React context
const ModalContext = React.createContext(null);
//Declare the modal component
const Modal = ({ modal, unSetModal, errorClasses = errorStyles(),
title, subtitle, children, }) => {
useEffect(() => {
const bind = e => {
if (e.keyCode !== 27) {
return
}
if (document.activeElement && ['INPUT', 'SELECT'].includes(document.activeElement.tagName)) {
return
}
unSetModal()
}
document.addEventListener('keyup', bind)
return () => document.removeEventListener('keyup', bind)
}, [modal, unSetModal])
const { t } = useTranslation()
return (
<>
<Dialog
className={errorClasses.modal}
fullWidth
maxWidth='md'
aria-labelledby='max-width-dialog-title'
>
<DialogTitle id='max-width-dialog-title'
className={errorClasses.header}>{title}</DialogTitle>
<DialogContent>
<DialogContentText>{subtitle}</DialogContentText>
</DialogContent>
<DialogActions>
<button className={errorClasses.cancelButton}
onClick={unSetModal}>{t("Close")}</button>
</DialogActions>
</Dialog>
</>
)
}
const ModalProvider = props => {
const [modal, setModal] = useState()
const unSetModal = useCallback(() => {
setModal('')
}, [setModal])
return (
<ModalContext.Provider value={{ unSetModal, setModal }} {...props} >
{props.children}
{modal && <Modal modal={modal} unSetModal={unSetModal} />}
</ModalContext.Provider>
)
}
// useModal: shows and hides the Modal
const useModal = () => {
const context = React.useContext(ModalContext)
if (context === undefined) {
throw new Error('useModal must be used within a UserProvider')
}
return context
}
export { ModalProvider, useModal }
And in another component (an edit form)
import { useModal, ModalProvider } from '../context/modal-context'
.....
export function DeviceModal (props) {
const { setModal } = useModal()
...
some other markup
<div style={{ position: 'sticky', bottom:'0', width:'100%', height:'40px', paddingTop:'5px'}}>
<Grid container direction="row-reverse" item xs={12} alignItems="right">
<button className={classes.cancelButton}
onClick={() => { setModal(<></>)}} >
{'Cancel'}
</button>
</Grid>
</div>
Hope someone can help
Thanks

Answer:
By adding the following to the dialogprops, it is resolved.
open={true}
backdropComponent={Backdrop}
and also, getting it to display values from the parent.
const ModalProvider = props => {
const [modal, setModal] = useState();
const [title, setTitle] = useState();
const [errMsg, setErrMsg] = useState();
const unSetModal = useCallback(() => {
setModal('')
}, [setModal])
// Provide the Modals functions and state variables to the consuming parent component
return (
<ModalContext.Provider value={{ unSetModal, setModal, setTitle, setErrMsg }} {...props} >
{props.children}
{modal && <Modal modal={modal} unSetModal={unSetModal} title={title} errMsg={errMsg} />}
</ModalContext.Provider>
)}
In the parent use setTitle("yada yada") setErrMsg('more yerra yada");

Related

How to extend Context or Provider in ReactJS to display a Loading Spinner

I would like to add a loading spinner during transitions. For example, when the app is fetching if the user is authenticated. How can this additional variable be added to the Context(Provider)?
My idea was to extend my AuthProvider (see below) with an additional variable, but its unclear to me if this is a good practice, and also if it can be actually consumed properly. The below example resulted that the variable loading is always false in the Consumer.js:
See [PartX] linked to my comment above.
App.js
import { AuthProvider } from "./auth/firebase/AuthProvider";
function App() {
return (
<AuthProvider>
<p>
Hello <code>world</code>
</p>
</AuthProvider>
);
}
AuthProvider.js
import React, { useEffect, useState } from "react";
import auth from "./configAuth";
export const AuthContext = React.createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true); // <--- [PartX]
useEffect(() => {
auth.onAuthStateChanged(setUser, setLoading(false)); // <--- [PartX]
}, []);
return (
<AuthContext.Provider value={{ user }}>{children}</AuthContext.Provider>
);
};
Consumer.js
import React, { useState, useContext } from "react";
import Button from "#mui/material/Button";
import Menu from "#mui/material/Menu";
import MenuItem from "#mui/material/MenuItem";
import { SignInButton, SignOutButton } from "../sign-in/SignInModal";
import { AuthContext } from "../firebase/AuthProvider";
export default function ProfileBox() {
/* General Anchor Clicks */
const [anchorEl, setAnchorEl] = useState(null);
const open = Boolean(anchorEl);
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
/* Auth state */
const { user, loading } = useContext(AuthContext);
// [PartX] ----> Does not work, always False
console.log(loading)
if (!user) {
return (
<SignInButton />
);
} else {
return (
<div>
<Button
color="inherit"
id="basic-button"
aria-controls={open ? 'basic-menu' : undefined}
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
onClick={handleClick}
>
{user.displayName}
</Button>
<Menu
disableScrollLock={true}
id="basic-menu"
anchorEl={anchorEl}
open={open}
onClose={handleClose}
MenuListProps={{
'aria-labelledby': 'basic-button',
}}
>
<MenuItem onClick={handleClose}> <SignOutButton /></MenuItem>
</Menu>
</div>
);
}
}

react populate modal from graphql

I have a list of products in a react component:
I am trying to display a modal when I click on the specific product that displays the product information through a graphql query. The query works fine. I'm just having problems displaying the modal.
Nothing happens when I click except my console.log statement. DisplayProduct never really gets loaded except when the page is loaded. How do I trigger a reload of DisplayProduct on click.
I'm pretty new to react, hence the issue...
ProductList.jsx
/* eslint-disable no-unused-vars */
import { useState, useCallback } from "react";
import { ResourceList, TextStyle, Stack, Thumbnail } from "#shopify/polaris";
import { Layout } from "#shopify/polaris";
import { DisplayProduct } from "./DisplayProduct";
export function ProductsList({ data }) {
const [open, setOpen] = useState(false);
const [clickedItem, setClickedItem] = useState(false);
const [active, setActive] = useState(false);
return (
<Layout>
<ResourceList // Defines your resource list component
showHeader
resourceName={{ singular: `Product`, plural: `Products` }}
items={data.nodes}
renderItem={(item) => {
const media = (
<Thumbnail
source={
item.images.edges[0] ? item.images.edges[0].node.originalSrc : ``
}
alt={item.images.edges[0] ? item.images.edges[0].node.altText : ``}
/>
);
const price = item.variants.edges[0].node.price;
return (
<ResourceList.Item
id={item.id}
media={media}
accessibilityLabel={`View details for ${item.title}`}
onClick={() => {
setClickedItem(item.id);
setOpen(true);
console.log(`itemId`, clickedItem);
}}
onCancel={() => setOpen(false)}
>
<Stack>
<Stack.Item fill>
<h3>
<TextStyle variation="strong">{item.title}</TextStyle>
</h3>
</Stack.Item>
<Stack.Item>
<p>${price}</p>
</Stack.Item>
</Stack>
</ResourceList.Item>
);
}}
/>
<Layout.Section>
<div style={{ height: `500px` }}>
<Modal
onClose={() => {
setActive(false);
setOpen(false);
console.log(`clicked close button: `, { active });
}}
title="Display Product"
open={active}
loading={false}
limitHeight={false}
sectioned={true}
show={active}
>
<Modal.Section>
<TextContainer>
<DisplayProduct productId={clickedItem} />
</TextContainer>
</Modal.Section>
</Modal>
</div>
</Layout.Section>
</Layout>
);
}
DisplayProduct.jsx
/* eslint-disable no-unused-vars */
import { useState, useCallback } from "react";
import { Banner, Button, Modal, TextContainer } from "#shopify/polaris";
import { gql, useQuery } from "#apollo/client";
import { Loading } from "#shopify/app-bridge-react";
const GET_PRODUCT_BY_ID = gql`
query getProductsById($id: ID!) {
product(id: $id) {
createdAt
defaultCursor
description
descriptionHtml
vendor
}
}
`;
export function DisplayProduct({ productId }) {
const [active, setActive] = useState(true);
const { loading, error, data, refetch } = useQuery(GET_PRODUCT_BY_ID, {
variables: { id: productId },
});
if (loading || typeof data === `undefined`) {
console.log(`loading`);
return <Loading />;
}
if (error) {
console.warn(error);
return (
<Banner status="critical">There was an issue loading products.</Banner>
);
}
console.log(`productId: `, productId);
console.log(`data: `, data);
return (
<p>
display our product here {data.id} and {productId}
</p>
);
}
New to react is right!
I needed a conditional display for the modal. If open is true, display the modal. When the modal is closed, the dom element is removed and then reloaded when open is set to true. I'm not sure if I can refine that down even further so it doesn't have to rebuild the modal each time a product is clicked
Here is the complete .jsx:
import { useState, useCallback } from "react";
import {
ResourceList,
TextStyle,
Stack,
Thumbnail,
Button,
Modal,
TextContainer,
Layout,
} from "#shopify/polaris";
import { DisplayProduct } from "./DisplayProduct";
export function ProductsList({ data }) {
const [open, setOpen] = useState(false);
const [clickedItem, setClickedItem] = useState(false);
const { prodData, setProdData } = useState(false);
return (
<Layout>
<ResourceList // Defines your resource list component
showHeader
resourceName={{ singular: `Product`, plural: `Products` }}
items={data.nodes}
renderItem={(item) => {
const media = (
<Thumbnail
source={
item.images.edges[0] ? item.images.edges[0].node.originalSrc : ``
}
alt={item.images.edges[0] ? item.images.edges[0].node.altText : ``}
/>
);
const price = item.variants.edges[0].node.price;
return (
<ResourceList.Item
id={item.id}
media={media}
accessibilityLabel={`View details for ${item.title}`}
onClick={() => {
setClickedItem(item.id);
setOpen(true);
}}
onCancel={() => setOpen(false)}
>
<Stack>
<Stack.Item fill>
<h3>
<TextStyle variation="strong">{item.title}</TextStyle>
</h3>
</Stack.Item>
<Stack.Item>
<p>${price}</p>
</Stack.Item>
</Stack>
</ResourceList.Item>
);
}}
/>
<Layout.Section> /* here is the important part. Conditionally load the modal and therefore, DisplayProduct. */
{open ? (
<div style={{ height: `500px` }}>
<Modal
onClose={() => {
setOpen(false);
}}
title="Display Product"
open={open}
loading={false}
limitHeight={false}
sectioned={true}
>
<Modal.Section>
<TextContainer>
<DisplayProduct productId={clickedItem} />
</TextContainer>
</Modal.Section>
</Modal>
</div>
) : (
``
)}
</Layout.Section>
</Layout>
);
}

Materail UI x React: Autocomplete closes when an item tries to show Popover

I'm trying to have a customized Autocomplete and each item has a more icon when you hover a mouse pointer.
When you click the icon, it supposes to bring Popover up, but instead it closes the Autocomplete list.
The image above, I'm hovering a pointer on the icon, and when I clicked it, it closed the list.
App.js
import React, { useState } from "react";
import { TextField, Paper } from "#material-ui/core";
import { Autocomplete, useAutocomplete } from "#material-ui/lab";
import Item from "./Item";
const App = () => {
const [isOpen, setIsOpen] = useState(false);
const [anchorEl, setAnchorEl] = useState(null);
const countries = [
{ code: "US", label: "USA", phone: "1" },
{ code: "HU", label: "Hungary ", phone: "2" },
{ code: "IT", label: "Italy ", phone: "3" }
];
const openMultipleOptionSearch = (event) => {
setIsOpen(true);
setAnchorEl(event.currentTarget);
};
const {
getRootProps,
getInputLabelProps,
getInputProps,
getListboxProps,
getOptionProps,
groupedOptions
} = useAutocomplete({
id: "use-autocomplete-demo",
options: countries,
getOptionLabel: (option) => option.label,
disableCloseOnSelect: true
});
return (
<div {...getRootProps()}>
<input {...getInputProps()} />
<Paper>
{groupedOptions.length > 0 &&
groupedOptions.map((option, index) => (
<Item key={index} text={option.label} onClose={() => {}} />
))}
{groupedOptions.length === 0 && <p>No match.</p>}
</Paper>
</div>
);
};
export default App;
Item.js
import React, { useState } from "react";
import MoreHoriz from "#material-ui/icons/MoreHoriz";
import { Box, Typography, Button } from "#material-ui/core";
import Popup from "./Popup";
const Item = (prop) => {
const [onMouseOver, setOnMouseOver] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [anchorEl, setAnchorEl] = useState(null);
const openDeleteItem = (event) => {
setAnchorEl(event.currentTarget);
setIsOpen(true);
};
const closeDeleteItem = () => {
setAnchorEl(null);
setIsOpen(false);
setOnMouseOver(false);
prop.onClose();
};
return (
<Box
onMouseOver={() => setOnMouseOver(true)}
onMouseOut={() => isOpen || setOnMouseOver(false)}
>
<div className="item-container">
<Typography variant="body1">{prop.text}</Typography>
</div>
<Button
style={{ display: onMouseOver ? "" : "none" }}
onClick={openDeleteItem}
>
<MoreHoriz />
</Button>
<Popup
isOpen={isOpen}
anchorEl={anchorEl}
onClose={() => closeDeleteItem()}
/>
</Box>
);
};
export default Item;
Popup.js
import React from 'react';
import { Popover } from '#material-ui/core';
const Popup = (prop) => {
return (
<Popover
id={prop.isOpen ? 'simple-popover' : undefined}
open={prop.isOpen}
anchorEl={prop.anchorEl}
onClose={() => prop.onClose()}
>
Popup content
</Popover>
);
};
export default Popup;
Does anyone know how to fix this?
I think your autocomplete popup is getting blurred when you click on more button, so it looses focus and closes the dropdown itself.
this might help you - OnBlur closing react component if clicked inside

Adding a dependency to useEffect() causes inifnite loop. But removing dependency causes component to not rerender when database updates

The following code caused useEffect() to be called infinitely. Why is that the case? Removing drawingboarditems from the useEffect dependencies array solves the infinite loop, but as a result DrawingBoard will not automatically rerender whenenver a user adds an item to the database.
DrawingBoard.jsx
export default function DrawingBoard() {
const [drawingboarditems, setdrawingboarditems] = useState([]);
const currentUser = useContext(CurrentUserContext);
const [loading, setLoading] = useState(true);
const classes = useStyles();
useEffect(() => {
if (currentUser) {
const items = [];
//retrieving data from database
db.collection("drawingboarditems")
.where("userID", "==", currentUser.id)
.get()
.then((query) => {
query.forEach((doc) => {
items.push({
id: doc.id,
...doc.data(),
});
});
setdrawingboarditems(items);
setLoading(false);
});
}
}, [currentUser, drawingboarditems]);
return (
<>
{loading == false ? (
<Container>
<Masonry
breakpointCols={breakpoints}
className="my-masonry-grid"
columnClassName="my-masonry-grid_column"
>
{drawingboarditems.map((item) => (
<div>
<Note item={item} />
</div>
))}
<Note form />
</Masonry>
</Container>
) : (
<div className={classes.root}>
<CircularProgress />
</div>
)}
</>
);
Note.jsx
import React from "react";
import { Card, CardHeader, IconButton, makeStyles } from "#material-ui/core";
import MoreVertIcon from "#material-ui/icons/MoreVert";
import Form from "./Form";
import CardWindow from "./CardWindow";
const useStyles = makeStyles((theme) => ({
card: {
backgroundColor: theme.palette.secondary.main,
margin: theme.spacing(1, 0),
},
}));
export default function Note({ item, form }) {
const classes = useStyles();
return (
<Card className={classes.card}>
{form ? (
<Form />
) : (
<CardHeader
action={
<CardWindow/>
}
title={item.title}
/>
)}
</Card>
);
}
Form.jsx
import React, { useContext } from "react";
import TextField from "#material-ui/core/TextField";
import { makeStyles } from "#material-ui/core/styles";
import AddCircleOutlineIcon from "#material-ui/icons/AddCircleOutline";
import IconButton from "#material-ui/core/IconButton";
import { db } from "../FireStore";
import { CurrentUserContext } from "../utils/Context";
const useStyles = makeStyles((theme) => ({
form: {
"& .MuiTextField-root": {
margin: theme.spacing(1),
width: "70%", // 70% of card in drawing board
},
},
}));
export default function Form() {
const classes = useStyles();
const [value, setValue] = React.useState("");
const currentUser = useContext(CurrentUserContext);
const handleChange = (event) => {
setValue(event.target.value);
};
const handleSubmit = (event) => {
if (value) {
event.preventDefault();
db.collection("drawingboarditems").add({
title: value,
userID: currentUser.id,
});
setValue("");
}
};
return (
<form className={classes.form} noValidate autoComplete="off">
<div>
<TextField
id="standard-textarea"
placeholder="Add item"
multiline
onChange={handleChange}
value={value}
/>
<IconButton aria-label="add" onClick={handleSubmit}>
<AddCircleOutlineIcon fontSize="large" />
</IconButton>
</div>
</form>
);
}
In your case I would move the logic to the DrawingBoard component, and would pass props to the children, so when a children adds an item, the main component would know to refresh the list of items.
Example (not tested):
Extract the logic to work with FireBase to functions. In that way they would be more re-usable, and would not add clutter to your code.
const drawingboarditemsCollection = 'drawingboarditems';
function getAllNotes(userID) {
return db.collection(drawingboarditemsCollection)
.where("userID", "==", userID)
.get()
.then((query) => {
return query.map(doc => {
items.push({
id: doc.id,
...doc.data(),
});
});
});
}
function addNote(userID, title) {
return db.collection(drawingboarditemsCollection).add({
title,
userID,
});
}
The DrawingBoard component should handle the connection with the server, and should pass functions as props to children:
export default function DrawingBoard() {
const [drawingboarditems, setdrawingboarditems] = useState([]);
const currentUser = useContext(CurrentUserContext);
const [loading, setLoading] = useState(true);
const classes = useStyles();
// add the logic to get notes by
const getNotes = useCallback(() => {
setLoading(true);
getAllNotes(currentUser.id)
.then(items => {
setdrawingboarditems(items);
})
.finally(=> {
setLoading(false);
});
}, [currentUser]);
// create the submit handler
const handleSubmit = value => {
addNote(currentUser.id, value)
.then(getNotes); // after adding a note update the items
}
// initial get notes, or when currentUser changes
useEffect(() => {
getNotes();
}, [getNotes]);
return (
<>
{loading == false ? (
<Container>
<Masonry
breakpointCols={breakpoints}
className="my-masonry-grid"
columnClassName="my-masonry-grid_column"
>
{drawingboarditems.map((item) => (
<div>
<Note item={item} />
</div>
))}
<Note form onSubmit={handleSubmit} />
</Masonry>
</Container>
) : (
<div className={classes.root}>
<CircularProgress />
</div>
)}
</>
);
}
Pass the onSubmit function to Form:
export default function Note({ item, form, onSubmit }) {
const classes = useStyles();
return (
<Card className={classes.card}>
{form ? (
<Form onSubmit={onSubmit} />
) : (
<CardHeader
action={
<CardWindow/>
}
title={item.title}
/>
)}
</Card>
);
}
Use onSubmit to handle the submission:
export default function Form({ onSubmit }) {
const classes = useStyles();
const [value, setValue] = React.useState("");
const handleChange = (event) => {
setValue(event.target.value);
};
const handleSubmit = (event) => {
if (value) {
event.preventDefault();
onSubmit(value); // let the parent handle the actual update
setValue("");
}
};
return ( ... );
}

unmute react-player when fullscreen is toggled from outer component

I have a VideoItem- and a Player-component
In VideoList a button is clicked and is going to fullscreen mode (is working as expected)
I will unmute player when fullscreen is clicked.
How can I pass down a "mute" change from VideoList to Player? In my Player I also have a "Unmute" button (which is also working as expected:
This is what I have so far
VideoItem.jsx
import React, { useRef, useState, useEffect } from "react"
import { findDOMNode } from "react-dom"
import screenfull from "screenfull"
import VideoPlayer from "./VideoPlayer"
const VideoList = (videos) => {
const ref = useRef()
const toggleFullScreen = () => {
screenfull.request(findDOMNode(ref.current))
}
const unMute = () => {
console.log("Should pass Mute state to player", muted)
}
return (
<>
<VideoPlayer
ref={ref}
mute={muted}
videoURL={videoUrl}
/>
<a
href="#"
onClick={e => {
e.preventDefault()
unMute()
toggleFullScreen()
}}
>
Show Fullscreen
</a>
)
}
Player.jsx
import React, { forwardRef, useState, useEffect } from "react"
import ReactPlayer from "react-player"
const VideoPlayer = forwardRef((props, ref, mute) => {
let [muteState, setMuteState] = useState(true)
return (
<>
<i className={`fal fa-volume-${muteState ? "up" : "mute"}`}
onClick={() => {
setMuteState(!muteState)
}}
/>
<ReactPlayer
ref={ref}
muted={muteState}
loop={true}
playing={true}
url={props.videoURL}
/>
</>
)
}
Thank you!
When you attempt to set the state from the parent this usually is an indicator that you should move the state up and make the child controlled by the parent:
const VideoList = (videos) => {
const player = useRef();
const [muted, setMuted] = useState(true);
const [fullscreen, setFullscreen] = useState(false);
const handleToggleMute = () => setMuted(current => !current);
const handleFullscreen = event => {
event.preventDefault();
setMuted(false);
setFullscreen(true);
};
return (
<>
<VideoPlayer
ref={ref}
muted={muted}
fullscreen={fullscreen}
videoURL={videoUrl}
onToggleMute={handleToggleMute}
/>
<a href="#" onClick={handleFullscreen}>Show Fullscreen</a>
)
}
Also I would use useEffect together with another state fullscreen to avoid having to forward a ref of the video player.
const VideoPlayer = ({videoURL, muted, fullscreen, onToggleMute}) => {
const playerRef = useRef();
useEffect(() => {
if (fullscreen) {
const videoElem = playerRef.current.getInternalPlayer();
screenfull.request(videoElem);
}
}, [fullscreen]);
return (
<>
<i
className={`fal fa-volume-${muted ? "up" : "mute"}`}
onClick={onToggleMute}
/>
<ReactPlayer
ref={playerRef}
muted={muted}
loop={true}
playing={true}
url={videoURL}
/>
</>
)
}

Resources