React.js Image Slider Back Button - javascript

I am fairly new to JavaScript and React, so I was hoping someone would be able to help me with something that seems fairly easy, I just don't fully grasp the concepts. I do not want to use ReactRouter, just manage it within the file.
I am creating a simple image slider using React and Material UI. However, I am having some difficulty with the functionality of the back button, preventing the app from crashing when going past the first image (into the negatives), and displaying the image number (ie: 3/5).
Here is my code:
import React, { useState } from "react";
import Button from "#material-ui/core/Button";
import MobileStepper from "#material-ui/core/MobileStepper";
import Paper from "#material-ui/core/Paper";
import KeyboardArrowRight from "#material-ui/icons/KeyboardArrowRight";
import Typography from "#material-ui/core/Typography";
import { useTheme } from "#material-ui/core/styles";
import KeyboardArrowLeft from "#material-ui/icons/KeyboardArrowLeft";
const myCollection = [
{
label: "First Picture",
imgPath:
"https://randompicturegenerator.com/img/dog-generator/g0ae914387a19ba58fc07fffe7f6952176159f445a3cd128c43ad59ae8b9baed35d8784cb3cdf4f4c16897571568d60c5_640.jpg",
},
{
label: "Second Picture",
imgPath:
"https://randompicturegenerator.com/img/dog-generator/g913a2b6f81253654df3b9e66abc189e6b966daf7a7a37b814b2336ab4459a832db90c8b923a5a8e28e63bb2fdd4496e1_640.jpg",
},
{
label: "Third Picture",
imgPath:
"https://randompicturegenerator.com/img/dog-generator/g5d33df79829d9cfbba39903471d1cd1bc2d6cdd243e222607e337d5575cb76ed10582deda8ed10bb0d1c3c43a6494f5a_640.jpg",
},
{
label: "Fourth Picture",
imgPath:
"https://randompicturegenerator.com/img/dog-generator/g446bd15f34f6fcb35272fa878493a90e9600b73e57f1f1dbca68ac22b9a1f780cb58c8a940d5727f930aa78d7b537b82_640.jpg",
},
{
label: "Fifth Picture",
imgPath:
"https://randompicturegenerator.com/img/dog-generator/g2efe8ff4951d16cc2d042d3714796204a975eb2c32fc5ee60d08956a729b7d5114b32cb0db6a5ba2e6d2948b8a6a0320_640.jpg",
},
];
const App = () => {
const CollectionSize = myCollection.length;
const theme = useTheme();
const [index, setIndex] = useState(0);
const goToNextPicture = () => {
setIndex((prevIndex) => prevIndex + 1);
};
const goToPrevPicture = () => {
setIndex((prevIndex) => prevIndex - 1);
};
return (
<>
<div style={{ marginLeft: "40%" }}>
<h2>Mans Best-Friend</h2>
<div style={{ maxWidth: 400, flexGrow: 1 }}>
<Paper
square
elevation={0}
style={{
height: 50,
display: "flex",
paddingLeft: theme.spacing(4),
backgroundColor: theme.palette.background.default,
alignItems: "center",
}}
>
<Typography>{myCollection[index].label}</Typography>
</Paper>
<img
src={myCollection[index].imgPath}
style={{
height: 255,
width: "100%",
maxWidth: 400,
display: "block",
overflow: "hidden",
}}
alt={myCollection[index].label}
/>
<MobileStepper
variant="text"
position="static"
index={index}
steps={CollectionSize}
nextButton={
<Button
size="small"
onClick={goToNextPicture}
disabled={index === CollectionSize - 1}
>
Next
{theme.direction !== "rtl" ? (
<KeyboardArrowRight />
) : (
<KeyboardArrowLeft />
)}
</Button>
}
/>
<MobileStepper
variant="text"
position="static"
index={index}
steps={CollectionSize}
backButton={
<Button
size="small"
onClick={goToPrevPicture}
disabled={index === CollectionSize + 1}
>
Back
{theme.direction !== "ltl" ? (
<KeyboardArrowRight />
) : (
<KeyboardArrowLeft />
)}
</Button>
}
/>
</div>
</div>
</>
);
};
export default App;
Error I am getting in the console:

const goToNextPicture = () => {
index<{your_array}.length-1?
setIndex((prevIndex) => prevIndex + 1):setIndex(0);
};
const goToPrevPicture = () => {
index>0
setIndex((prevIndex) => prevIndex - 1):setIndex(arr.length-1);
};
enter code here

Related

REACT : Error: Rendered fewer hooks than expected. This may be caused by an

i have a problem i have the famous react error : Uncaught (in promise) Error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement.
But i'm looking for the issue but i didnt find because i think my hooks order are well but its not the case.... Do you have an idea where is the mistake?
Thanks
import { useNavigate, Link, useParams } from 'react-router-dom';
import { useEffect, useState, useCallback, useMemo } from 'react';
import { Formik } from 'formik';
import { useIntl, FormattedMessage } from 'react-intl';
import { useQuery, gql, useMutation } from '#apollo/client';
import { sub } from 'date-fns';
import {
Text,
Button,
TextField,
DateField,
Banner,
Loader,
Icon,
} from '#customer-portal/components';
import { StandardField } from '../../components/FormikFields';
import { useError } from '../../hooks/useError';
import { toISODate, toStartOfDay } from '../../utils/date';
import { useFilteredContracts } from '../../components/ContractsFilter';
export function MeterReadingToEnterForm() {
const [date, setDate] = useState();
const [forceResult, setForceResult] = useState(false);
const [successBanner, setSuccessBanner] = useState(false);
let navigate = useNavigate();
let { serial } = useParams();
const intl = useIntl();
const contract = useFilteredContracts();
const initialValues = {};
const getError = useError(error);
useEffect(() => {
return () => {
navigate('/meter-reading');
};
}, [contract.id]);
const {
loading,
data: queryData,
error,
refetch,
} = useQuery(QUERY, {
notifyOnNetworkStatusChange: true,
fetchPolicy: 'network-only',
variables: {
date: toStartOfDay(Date.now()),
contractId: contract.id,
},
});
const [enterMeterReading, { loading: enterLoading, error: enterError }] =
useMutation(MUTATION_QUERY, {
onCompleted: () => setSuccessBanner(true),
});
useEffect(() => {
refetch({ variables: { date: toStartOfDay(date) } });
}, [date]);
if (error) {
return (
<Banner data-test="BannerNoContracts" type="error" iconName="Alert">
<Text>{getError()}</Text>
</Banner>
);
}
if (enterError) {
return (
<Banner type="error" iconName="Alert">
<Text>{getError()}</Text>
</Banner>
);
}
const formattedValues = (valeur) => {
let array = Object.keys(valeur).map((key) => ({
id: key,
result: valeur[key],
}));
return array;
};
const handle = useCallback((values) => {
mutationQuery({
variables: {
data: {
serial: serial,
results: formattedValues(values),
},
Id: id,
},
refetchQueries: ['newQuery'],
});
});
return (
<div sx={{ display: 'flex', flexDirection: 'column', width: '100%', p: 2 }}>
<div sx={{ display: 'flex', pt: 6, width: '100%' }}>
<Icon
sx={{ display: 'flex', alignItems: 'center', m: 3 }}
color="primary"
name="Counter"
size="large"
/>
<Text weight="bold" sx={{ display: 'flex', alignItems: 'center' }}>
<FormattedMessage
defaultMessage="N° {number}"
values={{
number: `${serial}`,
}}
/>
</Text>
</div>
{successBanner && (
<div sx={{ mb: 4 }}>
<Banner
iconName="SuccessOutline"
type="success"
>
<Text>
<FormattedMessage
defaultMessage="Done"
/>
</Text>
</Banner>
</div>
)}
<div sx={{ display: 'flex', justifyContent: 'flex-start', py: 2 }}>
<DateField
name="datefoield"
value={date || Date.now()}
onChange={(date) => setDate(date)}
required={true}
label={intl.formatMessage({
defaultMessage: 'Date du relevé',
})}
defaultSelected={date}
disabledDays={[
{
before: sub(new Date(toISODate(Date.now())), {
days: 60,
}),
},
{
after: new Date(Date.now()),
},
]}
/>
</div>
{enterLoading && <Loader type="radiance" overlay={true} />}
{loading ? (
<Loader sx={{ mx: [0, 11] }} />
) : (
<Formik
onSubmit={handleEnterMeterReading}
initialValues={initialValues}
>
{({ values, handleSubmit }) => {
const query = useMemo(() => {
return queryDatafind(
(serialNumber) => serialNumber === serial
);
}, [queryData]);
return (
<form
noValidate={true}
onSubmit={handleSubmit}
>
<div
sx={{
display: 'flex',
flexWrap: 'wrap',
width: '100%',
py: 2,
}}
>
{query.map((value) => (
<div sx={{ mr: 4 }}>
<StandardField
as={TextField}
id={id}
label={label}
required
name={id}
size="standard"
type="number"
variant="standard"
/>
</div>
))}
</div>
<div sx={{ display: 'flex', py: 6 }}>
<Link to="/">
<Button
sx={{ my: 4, mr: 4 }}
startIcon="ErrorOutline"
size="standard"
variant="outlined"
>
{intl.formatMessage({
defaultMessage: 'Annuler',
})}
</Button>
</Link>
<Button
sx={{ whiteSpace: 'nowrap', my: 4 }}
startIcon="SuccessOutline"
size="standard"
type="submit"
disabled={loading}
>
{loading
? intl.formatMessage({
defaultMessage: 'loading',
})
: intl.formatMessage({
defaultMessage: 'Validate',
})}
</Button>
</div>
</form>
);
}}
</Formik>
)}
</div>
);
}
-- GRAPHQL REQUEST---
The issue is that you call useCallback after some if conditionals that may return prematurely. You could either remove the useCallback call (just set handle to a new function closure each rendering), or move the useCallback call to above the if ... returns.

React.js, Material Ui Stepper direction

I have a weird situation right here where I try to build a stepper component within my app.
everything goes well so far but I try to make this stepper to go from Right to left.
things I have tried:
I tried to instal i18next (translation library) and it didn't work out.
I tried to go from 3 down to 2, and it also is not a satisfying result
here is the result:
and here is my code:
import {
makeStyles,
Theme,
createStyles,
withStyles,
} from '#material-ui/core/styles';
import clsx from 'clsx';
import Stepper from '#material-ui/core/Stepper';
import Step from '#material-ui/core/Step';
import StepLabel from '#material-ui/core/StepLabel';
import SettingsIcon from '#material-ui/icons/Settings';
import GroupAddIcon from '#material-ui/icons/GroupAdd';
import VideoLabelIcon from '#material-ui/icons/VideoLabel';
import StepConnector from '#material-ui/core/StepConnector';
import Button from '#material-ui/core/Button';
import Typography from '#material-ui/core/Typography';
import { StepIconProps } from '#material-ui/core/StepIcon';
const ColorlibConnector = withStyles({
alternativeLabel: {
top: 22,
},
active: {
'& $line': {
backgroundImage:
'linear-gradient( 95deg,rgb(242,113,33) 0%,rgb(233,64,87) 50%,rgb(138,35,135) 100%)',
},
},
completed: {
'& $line': {
backgroundImage:
'linear-gradient( 95deg,rgb(242,113,33) 0%,rgb(233,64,87) 50%,rgb(138,35,135) 100%)',
},
},
line: {
height: 3,
border: 0,
backgroundColor: '#eaeaf0',
borderRadius: 1,
},
})(StepConnector);
const useColorlibStepIconStyles = makeStyles({
root: {
backgroundColor: '#ccc',
zIndex: 1,
color: '#fff',
width: 50,
height: 50,
display: 'flex',
borderRadius: '50%',
justifyContent: 'center',
alignItems: 'center',
},
active: {
backgroundImage:
'linear-gradient( 136deg, rgb(242,113,33) 0%, rgb(233,64,87) 50%, rgb(138,35,135) 100%)',
boxShadow: '0 4px 10px 0 rgba(0,0,0,.25)',
},
completed: {
backgroundImage:
'linear-gradient( 136deg, rgb(242,113,33) 0%, rgb(233,64,87) 50%, rgb(138,35,135) 100%)',
},
});
function ColorlibStepIcon(props: StepIconProps) {
const classes = useColorlibStepIconStyles();
const { active, completed } = props;
const icons: { [index: string]: React.ReactElement } = {
1: <SettingsIcon />,
2: <GroupAddIcon />,
3: <VideoLabelIcon />,
};
return (
<div
className={clsx(classes.root, {
[classes.active]: active,
[classes.completed]: completed,
})}
>
{icons[String(props.icon)]}
</div>
);
}
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
width: '100%',
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
},
button: {
marginRight: theme.spacing(1),
},
instructions: {
marginTop: theme.spacing(1),
marginBottom: theme.spacing(1),
},
stepperContainer: {
width: '100%',
},
contentContainer: {
width: '50%',
display: 'flex',
justifyContent: 'center',
},
})
);
function getSteps() {
return ['Select campaign settings', 'Create an ad group', 'Create an ad'];
}
function getStepContent(step: number) {
switch (step) {
case 0:
return 'Select campaign settings...';
case 1:
return 'What is an ad group anyways?';
case 2:
return 'This is the bit I really care about!';
default:
return 'Unknown step';
}
}
const SignUp: React.FC = () => {
const classes = useStyles();
const [activeStep, setActiveStep] = React.useState(0);
const steps = getSteps();
const handleNext = () => {
setActiveStep((prevActiveStep) => prevActiveStep + 1);
};
const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1);
};
const handleReset = () => {
setActiveStep(0);
};
return (
<div className={classes.root}>
<div className={classes.stepperContainer}>
<Stepper
dir="rtl"
alternativeLabel
activeStep={activeStep}
connector={<ColorlibConnector />}
>
{steps.map((label) => (
<Step key={label}>
<StepLabel StepIconComponent={ColorlibStepIcon}>
{label}
</StepLabel>
</Step>
))}
</Stepper>
</div>
<div className={classes.contentContainer}>
<div>
{activeStep === steps.length ? (
<div>
<Typography className={classes.instructions}>
All steps completed - you&apos;re finished
</Typography>
<Button onClick={handleReset} className={classes.button}>
Reset
</Button>
</div>
) : (
<div>
<Typography className={classes.instructions}>
{getStepContent(activeStep)}
</Typography>
<div>
<Button
disabled={activeStep === 0}
onClick={handleBack}
className={classes.button}
>
Back
</Button>
<Button
variant="contained"
color="primary"
onClick={handleNext}
className={classes.button}
>
{activeStep === steps.length - 1 ? 'Finish' : 'Next'}
</Button>
</div>
</div>
)}
</div>
</div>
</div>
);
};
export default SignUp;
Hi I was fiddling around with the material UI sandbox,
here's the sandbox with working left to right
things I've change, were:
line 43 - initate step to 2
line 62 - handleNext, prevActiveStep - 1 (from +1)
line 67 - handleBack, prevActiveStep + 1 (from -1)
line 86 - setActiveStep(2) (from setActiveStep(0))
the Stepper component:
<Stepper activeStep={activeStep}>
{steps.map((label, index) => {
const stepProps: { completed?: boolean } = {
completed: activeStep < index // <<< this setting
};
const labelProps: { optional?: React.ReactNode } = {};
if (isStepOptional(index)) {
labelProps.optional = (
<Typography variant="caption">Optional</Typography>
);
}
if (isStepSkipped(index)) {
stepProps.completed = false;
}
return (
<Step key={label} {...stepProps}>
<StepLabel {...labelProps}>{label}</StepLabel>
</Step>
);
})}
</Stepper>
hope that helps

How to add search functionality to the SearchAppBar in Material UI?

So I have a basic react app that allows the user to search for and display movies from the Movie Database API.
Heres the problem: I want to link my custom React Function SearchMovies() to a material ui component, specifically the Material Ui SearchAppBar. However, I am having trouble adding this function to the Material Ui SearchAppBar.
First, my custom code that features the SearchMovies() function. The code is below:
import React, { useState } from "react";
import MovieCard from "./movieCard";
export default function SearchMovies() {
//states = input query, movies
const [query, setQuery] = useState("");
const [movies, setMovies] = useState([]);
const searchMovies = async (e) => {
e.preventDefault();
const url = `https://api.themoviedb.org/3/search/movie?api_key=a2657ca16cc801deb9a65e9f7f9e3d4f&language=en-US&query=${query}&page=1&include_adult=false`;
try {
const res = await fetch(url);
const data = await res.json();
//console.log(data.results);
setMovies(data.results);
} catch (err) {
console.error(err);
}
};
return (
<>
<form className="form" onSubmit={searchMovies}>
<label className="label" htmlFor="query">
Movie Name
</label>
<input
type="text"
className="input"
name="query"
placeholder="i.e. Star Wars"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<button className="button" type="Submit">
Search
</button>
</form>
<div className="card-list">
{movies
.filter((movie) => movie.poster_path)
.map((movie) => (
<MovieCard movie={movie} key={movie.id} />
))}
</div>
</>
);
}
Second, here is the code for the Material UI SearchAppBar:
import React from "react";
import AppBar from "#material-ui/core/AppBar";
import Toolbar from "#material-ui/core/Toolbar";
import IconButton from "#material-ui/core/IconButton";
import Typography from "#material-ui/core/Typography";
import InputBase from "#material-ui/core/InputBase";
import { fade, makeStyles } from "#material-ui/core/styles";
import MenuIcon from "#material-ui/icons/Menu";
import SearchIcon from "#material-ui/icons/Search";
import searchMovies from "./searchMovies";
const useStyles = makeStyles((theme) => ({
root: {
flexGrow: 1,
},
menuButton: {
marginRight: theme.spacing(2),
},
title: {
flexGrow: 1,
display: "none",
[theme.breakpoints.up("sm")]: {
display: "block",
},
},
search: {
position: "relative",
borderRadius: theme.shape.borderRadius,
backgroundColor: fade(theme.palette.common.white, 0.15),
"&:hover": {
backgroundColor: fade(theme.palette.common.white, 0.25),
},
marginLeft: 0,
width: "100%",
[theme.breakpoints.up("sm")]: {
marginLeft: theme.spacing(1),
width: "auto",
},
},
searchIcon: {
padding: theme.spacing(0, 2),
height: "100%",
position: "absolute",
pointerEvents: "none",
display: "flex",
alignItems: "center",
justifyContent: "center",
},
inputRoot: {
color: "inherit",
},
inputInput: {
padding: theme.spacing(1, 1, 1, 0),
// vertical padding + font size from searchIcon
paddingLeft: `calc(1em + ${theme.spacing(4)}px)`,
transition: theme.transitions.create("width"),
width: "100%",
[theme.breakpoints.up("sm")]: {
width: "12ch",
"&:focus": {
width: "20ch",
},
},
},
}));
export default function SearchAppBar() {
const classes = useStyles();
return (
<div className={classes.root}>
<AppBar position="static">
<Toolbar>
<IconButton
edge="start"
className={classes.menuButton}
color="inherit"
aria-label="open drawer"
>
<MenuIcon />
</IconButton>
<Typography className={classes.title} variant="h5" noWrap>
MovieZone
</Typography>
<div className={classes.search}>
<div className={classes.searchIcon}>
<SearchIcon />
</div>
<InputBase
//value={query}
//onChange={(e) => setQuery(e.target.value)}
onSubmit={searchMovies}
placeholder="Search…"
classes={{
root: classes.inputRoot,
input: classes.inputInput,
}}
inputProps={{ "aria-label": "search" }}
/>
</div>
</Toolbar>
</AppBar>
</div>
);
}
So how can I inject the SearchMovies() function into the Material UI SearchAppBar, so that the user can use the Material UI component to call the SearchMovies() function and display the movie results?
You can take the reference from an angular material table example but you have mat cards its just the matter of filtering the dataSource what you give and recall that method or just change the arrayList by constructing filter arraylist or something which you provide again to the UI component
Here is the link
https://stackblitz.com/angular/kgboooaejyn?embed=1&file=src/app/table-filtering-example.ts
you need to focus on this function
applyFilter(event: Event) {
const filterValue = (event.target as HTMLInputElement).value;
this.dataSource.filter = filterValue.trim().toLowerCase();
}
You won't have a Datasource here but you can filter contents from ArrayList
Hope this idea works...!!

Switch function in react component not working as logic would suggest

So my react code does not execute as logic would suggest:
I am trying to create a page that resembles an email app.
I have components in React which I import into my message(main) component (e.g. Inbox component, send mail component etc). I have a useState, which is set to 'inbox' as default and a switch statement which checks which value is in the state and returns the component matching that case. I also have a onclick function which changes the state value based on which email tab you want to view (e.g. Inbox or trash etc). When the state changes the component should run the switch statement in my page render and display the correct component in the main component, which is does most of the time.
My problem comes when I keep switching between the tabs which have components it at some point goes to the default of the switch statement which does not make sense because either of the values should have passed.
Please help
import React, {useState} from 'react';
// Components to be displayed
import Inbox from './Inbox';
import SendMessage from './SendMessage';
// Other components on the page which you can ignore
import Navbar from '../Layout/Navbar';
import Copyright from '../Layout/Copyright';
import SendMail from './SendMail';
//Material ui for styling
import AppBar from '#material-ui/core/AppBar';
import CssBaseline from '#material-ui/core/CssBaseline';
import Divider from '#material-ui/core/Divider';
import Drawer from '#material-ui/core/Drawer';
import Hidden from '#material-ui/core/Hidden';
import IconButton from '#material-ui/core/IconButton';
import InboxIcon from '#material-ui/icons/MoveToInbox';
import List from '#material-ui/core/List';
import ListItem from '#material-ui/core/ListItem';
import ListItemIcon from '#material-ui/core/ListItemIcon';
import ListItemText from '#material-ui/core/ListItemText';
import MailIcon from '#material-ui/icons/Mail';
import { makeStyles, useTheme } from '#material-ui/core/styles';
import Link from '#material-ui/core/Link';
import ControlPointIcon from '#material-ui/icons/ControlPoint';
const drawerWidth = 240;
const list = [
{id: 1, title: 'Singing lessons', message: 'We are practicing at 5:00'},
{id: 2, title: 'Meeting', message: 'Hi Guys, we can meet during lunch at Nandos'},
{id: 3, title: 'New Product release', message: 'Hi guys, we are encouraging everyone to buy in on the new product'}
]
const useStyles = makeStyles((theme) => ({
root: {
display: 'flex'
},
drawer: {
[theme.breakpoints.up('sm')]: {
width: drawerWidth,
flexShrink: 0,
zIndex: '0'
}
},
appBar: {
[theme.breakpoints.up('sm')]: {
width: `calc(100% - ${drawerWidth}px)`,
marginLeft: drawerWidth,
},
},
menuButton: {
marginRight: theme.spacing(2),
[theme.breakpoints.up('sm')]: {
display: 'none',
},
},
// necessary for content to be below app bar
toolbar: theme.mixins.toolbar,
drawerPaper: {
width: drawerWidth,
},
content: {
flexGrow: 1,
padding: theme.spacing(3)
},
listItemStyle: {
marginTop: '50px'
},
newMessage: {
fontSize: '40px',
backgroundColor: '#64b5f6',
borderRadius: '50px',
color: '#fff',
position: 'absolute',
bottom: '50px',
right: '50px',
'&:hover': {
backgroundColor: '#1976d2'
}
}
}));
// Main component
const Message = (props) => {
const { window } = props;
const classes = useStyles();
const theme = useTheme();
const [mobileOpen, setMobileOpen] = useState(false);
const [tabTracker, setTabTracker] = useState('Inbox');
const renderSwitch = (param) => {
switch(param) {
case 'Inbox':
return <Inbox />;
case 'Send email':
return <SendMail />;
case 'Drafts':
return 'Draft';
case 'Trash':
return 'Trash';
case 'Spam':
return 'Spam';
case 'newMessage':
return <SendMessage />;
default:
return 'foo';
}
}
const tabControl = (e) => {
setTabTracker(e.target.firstChild.data);
//renderSwitch(tabTracker);
}
const newMsg = (e) => {
setTabTracker(e.target.attributes[5].value);
//renderSwitch(tabTracker);
}
const handleDrawerToggle = () => {
setMobileOpen(!mobileOpen);
};
const drawer = (
<div>
<div className={classes.toolbar} />
<Divider />
<List className={classes.listItemStyle}>
{['Inbox', 'Send email', 'Drafts'].map((text, index) => (
<ListItem button key={text} onClick={tabControl}>
<ListItemIcon>{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}</ListItemIcon>
<ListItemText primary={text} />
</ListItem>
))}
</List>
<Divider />
<List>
{['Trash', 'Spam'].map((text, index) => (
<ListItem button key={text} onClick={tabControl}>
<ListItemIcon>{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}</ListItemIcon>
<ListItemText primary={text} />
</ListItem>
))}
</List>
</div>
);
const container = window !== undefined ? () => window().document.body : undefined;
return (
<React.Fragment>
<span>
<Navbar />
</span>
<div className={classes.root}>
<CssBaseline />
<nav className={classes.drawer} aria-label="mailbox folders">
{/* The implementation can be swapped with js to avoid SEO duplication of links. */}
<Hidden smUp implementation="css">
<Drawer
container={container}
variant="temporary"
anchor={theme.direction === 'rtl' ? 'right' : 'left'}
open={mobileOpen}
onClose={handleDrawerToggle}
classes={{
paper: classes.drawerPaper,
}}
ModalProps={{
keepMounted: true, // Better open performance on mobile.
}}
>
{drawer}
</Drawer>
</Hidden>
<Hidden xsDown implementation="css">
<Drawer
classes={{
paper: classes.drawerPaper,
}}
variant="permanent"
open
>
{drawer}
</Drawer>
</Hidden>
</nav>
<main className={classes.content}>
<div className={classes.toolbar} />
{renderSwitch(tabTracker)}
<Link href="#">
<ControlPointIcon value="newMessage" primary="newMessage" className= {classes.newMessage} onClick={newMsg} />
</Link>
</main>
</div>
<Copyright />
</React.Fragment>
);
}
// ResponsiveDrawer.propTypes = {
// /**
// * Injected by the documentation to work in an iframe.
// * You won't need it on your project.
// */
// window: PropTypes.func,
// };
export default Message;
e.target gives you the element on which click was actually triggered, so when you have multiple elements as children within ListItem, its possible that you would have clicked on ListItemIcon and hence e.target is listItemIcon which was not your intention as it will give you an incorrect value for e.target.firstChild.data
So you could either use e.currentTarget which will give you the element on which the onClick listener is attached or you could simply pass on the information to tabControl by using an inline arrow function
const tabControl = (value) => {
setTabTracker(value);
}
const drawer = (
<div>
<div className={classes.toolbar} />
<Divider />
<List className={classes.listItemStyle}>
{['Inbox', 'Send email', 'Drafts'].map((text, index) => (
<ListItem button key={text} onClick={() => tabControl(text)}>
<ListItemIcon>{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}</ListItemIcon>
<ListItemText primary={text} />
</ListItem>
))}
</List>
<Divider />
<List>
{['Trash', 'Spam'].map((text, index) => (
<ListItem button key={text} onClick={() => tabControl(text)}>
<ListItemIcon>{index % 2 === 0 ? <InboxIcon /> : <MailIcon />}</ListItemIcon>
<ListItemText primary={text} />
</ListItem>
))}
</List>
</div>
);

React Beginner Question: Textfield Losing Focus On Update

I wrote a component that is supposed to list out a bunch of checkboxes with corresponding textfields. When you click on the checkboxes, or type in the fields it's meant to update state.
The textbox is working ok, but when I type in the fields, it updates state ok, but I lose focus whenever I tap the keyboard.
I realized this is probably due to not having keys set, so I added keys to everything but it still is losing focus. At one point I tried adding in stopPropegation on my events because I thought maybe that was causing an issue?? I'm not sure.. still learning...didn't seem to work so I removed that part too.
Still can't seem to figure out what is causing it to lose focus... does anyone have any advice/solves for this issue?
I consolidated my code and cut out the unnecessary bits to make it easier to read. There are three relevant JS files.. please see below:
I'm still a beginner/learning so if you have useful advice related to any part of this code, feel free to offer. Thanks!
App.js
import React, { Component } from 'react';
import Form from './Form'
class App extends Component {
constructor() {
super();
this.state = {
mediaDeliverables: [
{label: 'badf', checked: false, quantity:''},
{label: 'adfadf', checked: false, quantity:''},
{label: 'adadf', checked: false, quantity:''},
{label: 'addadf', checked: false, quantity:''},
{label: 'adfdes', checked: false, quantity:''},
{label: 'hghdgs', checked: false, quantity:''},
{label: 'srtnf', checked: false, quantity:''},
{label: 'xfthd', checked: false, quantity:''},
{label: 'sbnhrr', checked: false, quantity:''},
{label: 'sfghhh', checked: false, quantity:''},
{label: 'sssddrr', checked: false, quantity:''}
]
}
}
setMediaDeliverable = (value, index) => {
let currentState = this.getStateCopy();
currentState.mediaDeliverables[index] = value;
this.setState(currentState);
}
getStateCopy = () => Object.assign({}, this.state);
render() {
return (
<div className="App">
<Form
key="mainForm"
mediaDeliverablesOptions={this.state.mediaDeliverables}
setMediaDeliverable={this.setMediaDeliverable}
/>
</div>
);
}
}
export default App;
Form.js
import React from 'react';
import { makeStyles, useTheme } from '#material-ui/core/styles';
import FormControl from '#material-ui/core/FormControl';
import FormLabel from '#material-ui/core/FormLabel';
import FormGroup from '#material-ui/core/FormGroup';
import FormControlLabel from '#material-ui/core/FormControlLabel';
import Checkbox from '#material-ui/core/Checkbox';
import MediaDeliverablesCheckBox from './MediaDeliverablesCheckBox';
const useStyles = makeStyles(theme => ({
container: {
display: 'inline-block',
flexWrap: 'wrap',
},
root: {
display: 'inline-block',
flexWrap: 'wrap',
maxWidth: 600,
textAlign: 'left',
},
extendedIcon: {
marginRight: theme.spacing(1),
},
formControl: {
margin: theme.spacing(1),
minWidth: 120,
maxWidth: 300,
},
textField: {
marginLeft: theme.spacing(1),
marginRight: theme.spacing(1),
width: 370,
},
dense: {
marginTop: 19,
},
chips: {
display: 'flex',
flexWrap: 'wrap',
},
chip: {
margin: 2,
},
noLabel: {
marginTop: theme.spacing(3),
},
}));
const ITEM_HEIGHT = 48;
const ITEM_PADDING_TOP = 8;
const MenuProps = {
PaperProps: {
style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
width: 250,
},
},
};
function getStyles(name, accountName, theme) {
// console.log('>> [form.js] (getStyles) ',accountName)
return {
fontWeight:
accountName.indexOf(name) === -1
? theme.typography.fontWeightRegular
: theme.typography.fontWeightMedium,
};
}
export default function Form(props) {
const mediaDeliverablesOptions = props.mediaDeliverablesOptions;
const classes = useStyles();
const theme = useTheme();
const CheckboxGroup = ({ values, label, onChange }) => (
<FormControl component="fieldset">
<FormLabel component="legend">{label}</FormLabel>
<FormGroup>
{values.map((value, index) => (
<FormControlLabel
key={index}
control={
<Checkbox
checked={value.checked}
onChange={onChange(index)}
/>
}
label={value.label}
/>
))}
</FormGroup>
</FormControl>
);
const MediaDeliverableCheckBoxList = ({values, label}) => (
<FormControl component="fieldset">
<FormLabel component="legend">{label}</FormLabel>
<FormGroup>
{values.map((value, index) => (
<MediaDeliverablesCheckBox
key={index}
mediaDeliverablesOptions={value}
onMediaDeliverableChange={onMediaDeliverableChange(index)}
/>
))}
</FormGroup>
</FormControl>
);
const onCheckBoxChange = index => ({ target: { checked } }) => {
const newValues = [...values];
const value = values[index];
newValues[index] = { ...value, checked };
props.setDesignOrDigital(newValues);
};
const onMediaDeliverableChange = index => (deliverableData, e) => {
props.setMediaDeliverable(deliverableData, index);
}
return (
<div className={classes.root}>
<MediaDeliverableCheckBoxList
label="Please Choose Deliverables:"
values={mediaDeliverablesOptions}
key="media-deliverable-checkbox-list"
/>
</div>
);
}
MediaDeliverablesCheckbox.js
import React from 'react';
import Checkbox from '#material-ui/core/Checkbox';
import { makeStyles, useTheme } from '#material-ui/core/styles';
import FormControl from '#material-ui/core/FormControl';
import FormLabel from '#material-ui/core/FormLabel';
import FormGroup from '#material-ui/core/FormGroup';
import FormControlLabel from '#material-ui/core/FormControlLabel';
import TextField from '#material-ui/core/TextField';
export default function MediaDeliverablesCheckBox(props) {
let deliverableData = Object.assign({}, props.mediaDeliverablesOptions);
const onCheckBoxChange = (e) => {
deliverableData.checked = e.target.checked;
props.onMediaDeliverableChange(deliverableData, e);
}
const onQuantityChange = (e) => {
deliverableData.quantity = e.target.value;
props.onMediaDeliverableChange(deliverableData, e);
}
const CheckboxGroup = ({ value, label }) => (
<FormControl component="fieldset">
<FormGroup>
<FormControlLabel
control={
<Checkbox
key={props.index}
checked={value.checked}
onChange={onCheckBoxChange}
/>
}
label={label}
/>
</FormGroup>
</FormControl>
);
return(
<div className="MediaDeliverablesCheckBox">
<CheckboxGroup
key={props.index}
label={props.mediaDeliverablesOptions.label}
value={props.mediaDeliverablesOptions}
/>
<TextField
key={'tf'+props.index}
id={'quantity-'+props.index}
label="Quantity"
placeholder="How many do you need?"
multiline
variant="outlined"
value={props.mediaDeliverablesOptions.quantity}
onChange={onQuantityChange}
fullWidth
/>
</div>
);
}
Updated Form.js based on recommended edits by Ryan C.
import React from 'react';
import { makeStyles, useTheme } from '#material-ui/core/styles';
import FormControl from '#material-ui/core/FormControl';
import FormLabel from '#material-ui/core/FormLabel';
import FormGroup from '#material-ui/core/FormGroup';
import FormControlLabel from '#material-ui/core/FormControlLabel';
import Checkbox from '#material-ui/core/Checkbox';
import MediaDeliverablesCheckBox from './MediaDeliverablesCheckBox';
const useStyles = makeStyles(theme => ({
container: {
display: 'inline-block',
flexWrap: 'wrap',
},
root: {
display: 'inline-block',
flexWrap: 'wrap',
maxWidth: 600,
textAlign: 'left',
},
extendedIcon: {
marginRight: theme.spacing(1),
},
formControl: {
margin: theme.spacing(1),
minWidth: 120,
maxWidth: 300,
},
textField: {
marginLeft: theme.spacing(1),
marginRight: theme.spacing(1),
width: 370,
},
dense: {
marginTop: 19,
},
chips: {
display: 'flex',
flexWrap: 'wrap',
},
chip: {
margin: 2,
},
noLabel: {
marginTop: theme.spacing(3),
},
}));
const ITEM_HEIGHT = 48;
const ITEM_PADDING_TOP = 8;
const MenuProps = {
PaperProps: {
style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
width: 250,
},
},
};
function getStyles(name, accountName, theme) {
return {
fontWeight:
accountName.indexOf(name) === -1
? theme.typography.fontWeightRegular
: theme.typography.fontWeightMedium,
};
}
// Failed to compile
// ./src/Form.js
// Line 86: Parsing error: Unexpected token, expected ","
// 84 |
// 85 | const MediaDeliverableCheckBoxList = ({values, label, onMediaDeliverableChange}) => (
// > 86 | {values.map((value, index) => (
// | ^
// 87 | <MediaDeliverablesCheckBox
// 88 | key={index}
// 89 | index={index}
// This error occurred during the build time and cannot be dismissed.
const MediaDeliverableCheckBoxList = ({values, label, onMediaDeliverableChange}) => (
{values.map((value, index) => (
<MediaDeliverablesCheckBox
key={index}
index={index}
mediaDeliverablesOptions={value}
onMediaDeliverableChange={onMediaDeliverableChange(index)}
/>
))}
);
export default function Form(props) {
const mediaDeliverablesOptions = props.mediaDeliverablesOptions;
const classes = useStyles();
const theme = useTheme();
const CheckboxGroup = ({ values, label, onChange }) => (
<FormControl component="fieldset">
<FormLabel component="legend">{label}</FormLabel>
<FormGroup>
{values.map((value, index) => (
<FormControlLabel
key={index}
control={
<Checkbox
checked={value.checked}
onChange={onChange(index)}
/>
}
label={value.label}
/>
))}
</FormGroup>
</FormControl>
);
const onCheckBoxChange = index => ({ target: { checked } }) => {
const newValues = [...values];
const value = values[index];
newValues[index] = { ...value, checked };
props.setDesignOrDigital(newValues);
};
const onMediaDeliverableChange = index => (deliverableData, e) => {
props.setMediaDeliverable(deliverableData, index);
}
return (
<div className={classes.root}>
<MediaDeliverableCheckBoxList
onMediaDeliverableChange={onMediaDeliverableChange}
/>
</div>
);
}
I see two main issues:
How you are defining your different components (nesting component types)
Not passing the index prop through to components that are expecting it
You have the following structure (leaving out details that are not directly related to my point):
export default function Form(props) {
const onMediaDeliverableChange = index => (deliverableData, e) => {
props.setMediaDeliverable(deliverableData, index);
}
const MediaDeliverableCheckBoxList = ({values, label}) => (
<FormGroup>
{values.map((value, index) => (
<MediaDeliverablesCheckBox key={index} onMediaDeliverableChange={onMediaDeliverableChange(index)}/>
))}
</FormGroup>
);
return (
<MediaDeliverableCheckBoxList/>
);
}
The function MediaDeliverableCheckBoxList represents the component type used to render the <MediaDeliverableCheckBoxList/> element. Whenever Form is re-rendered due to props or state changing, React will re-render its children. If the component type of a particular child is the same (plus some other criteria such as key being the same if specified), then it will update the existing DOM node(s). If the component type of a particular child is different, then the corresponding DOM nodes will be removed and new ones added to the DOM.
By defining the MediaDeliverableCheckBoxList component type within the Form function, you are causing that component type to be different on every render. This will cause all of the DOM nodes to be replaced rather than just updated and this will cause the focus to go away when the DOM node that previously had focus gets removed. It will also cause performance to be considerably worse.
You can fix this by moving this component type outside of the Form function and then adding any additional props that are needed (e.g. onMediaDeliverableChange) to convey the context known inside of Form. You also need to pass index as a prop to MediaDeliverablesCheckBox since it is using it.
const MediaDeliverableCheckBoxList = ({values, label, onMediaDeliverableChange}) => (
<FormGroup>
{values.map((value, index) => (
<MediaDeliverablesCheckBox key={index} index={index} onMediaDeliverableChange={onMediaDeliverableChange(index)}/>
))}
</FormGroup>
);
export default function Form(props) {
const onMediaDeliverableChange = index => (deliverableData, e) => {
props.setMediaDeliverable(deliverableData, index);
}
return (
<MediaDeliverableCheckBoxList onMediaDeliverableChange={onMediaDeliverableChange}/>
);
}
You have this same issue with CheckboxGroup and possibly other components as well.
This issue is solely because of your key in the TextField. You must ensure that the key remains the same on every update. Otherwise you would the face the current issue.

Categories

Resources