Display the helper text when TextField focused in Material UI - javascript

I want the error message isn't shown until a user clicks on TextField. Here is my code:
import React, { useState, useEffect } from 'react';
import { TextField, Grid, Button } from '#material-ui/core';
const ReplyToComment = () => {
const [replyContent, setReplyContent] = useState();
const [errorMessage, setErrorMessage] = useState([]);
useEffect(() => {
if(replyContent) {
if(replyContent.length > 7){
setErrorMessage([]);
} else {
setErrorMessage(["Your answer is too short"])
}
} else {
setErrorMessage(["answer field can not empty"])
}
}, [replyContent]);
const handleChange = (event) => {
setReplyContent(event.target.value)
};
const handleSubmit = async () => {
console.log("************")
};
return (
<Grid container spacing={4} alignItems="center" justify="center" >
<Grid item xs={12}>
<TextField
value={replyContent}
name="reply"
fullWidth
required
error={Boolean(errorMessage.length)}
multiline
label="answer"
helperText={errorMessage[0]}
onChange={handleChange}
/>
</Grid>
<Button onClick={handleSubmit} variant="contained" color="primary" disabled={Boolean(errorMessage.length)}>add</Button>
</Grid>
);
}
export default ReplyToComment;
This way, the error message is shown before a user any action, but I want the error message isn't shown until a user clicks on TextField.
Try it on Codesandbox

Easiest solution would be to use a flag which holds the information about the clicked (touched) state for the TextField.
Example:
const ReplyToComment = () => {
const [replyContent, setReplyContent] = useState();
const [errorMessage, setErrorMessage] = useState([]);
const [touched, setTouched] = useState(false);
const handleTouch = () => {
setTouched(true);
};
// ...
return (
<Grid container spacing={4} alignItems="center" justify="center" >
<Grid item xs={12}>
<TextField
value={replyContent}
name="reply"
fullWidth
required
onFocus={handleTouch}
error={touched && Boolean(errorMessage.length)}
multiline
label="answer"
helperText={touched && errorMessage[0]}
onChange={handleChange}
/>
</Grid>
<Button onClick={handleSubmit} variant="contained" color="primary" disabled={Boolean(errorMessage.length)}>add</Button>
</Grid>
);
}
I have set the value on onFocus but it is totally up to you when you would want the TextField to be classified as touched (onBlur, onClick)

Related

How to set initial state with most up to date data returned from API?

I'm making a simple form to edit an app, the initial state of the name & description of the app is set using the data returned from the API.
Currently, when submitting the form the initial data seems to be logging as undefined, the name & description is being set as undefined which occurs in the first render (I have commented in the code where the logs are)
How can I make sure the initial state of name & description has the most up to date information?
Is the excessive renders the problem?
Thanks for taking a look, any help would be appreciated.
import React, { useState, useContext, useEffect } from "react";
import Typography from "#material-ui/core/Typography";
import Button from "#material-ui/core/Button";
import Container from "#material-ui/core/Container";
import SaveIcon from "#mui/icons-material/Save";
import CloseIcon from "#mui/icons-material/Close";
import { makeStyles } from "#material-ui/styles";
import TextField from "#material-ui/core/TextField";
import { Grid } from "#mui/material";
import { useDispatch } from "react-redux";
import { updateApp, updateSelectedApp } from "../../services/reducers/apps";
import { EndpointContext } from "../../baxios/EndpointProvider";
import { useParams } from "react-router-dom";
export default function EditApp() {
const { appid } = useParams();
const classes = useStyles();
const dispatch = useDispatch();
const endpoints = useContext(EndpointContext);
const [selectedApp, setSelectedApp] = useState({});
const [isLoaded, setIsLoaded] = useState(false); // <--- Is there anyway I can also remove this useState? without this the default values in the forms dont populate
useEffect(() => {
async function fetchApp() {
await endpoints.appEndpoints.get(appid).then((response) => {
if (response.status === 200) {
setSelectedApp(response.data);
setIsLoaded(true);
}
});
}
fetchApp();
}, []);
useEffect(() => {
console.log(selectedApp);
}, [selectedApp]);
const [name, setName] = useState(selectedApp.name);
const [description, setDescription] = useState(selectedApp.description);
console.log("---", name, selectedApp.name); // <--- The page renders 3 times, each render log looks like this
// 1st render - --- undefined, undefined
// 2nd render - --- undefined, Appname
// 3rd render - --- undefined, Appname
const handleSubmit = (e) => {
e.preventDefault();
console.log("triggered", name, description); // <--- This logs (triggered, undefined, undefined)
if (name && description) {
const body = { name: name, description: description };
endpoints.appEndpoints.put(selectedApp.id, body).then((response) => {
if (response.status === 200) {
dispatch(updateApp(response.data));
setSelectedApp(response.data);
setName(selectedApp.name);
setDescription(selectedApp.description);
}
});
}
};
return (
<div style={{ margin: 100, marginLeft: 350 }}>
{isLoaded ? (
<Container size="sm" style={{ marginTop: 40 }}>
<Typography
variant="h6"
color="textSecondary"
component="h2"
gutterBottom
>
Edit App
</Typography>
<form noValidate autoComplete="off" onSubmit={handleSubmit}>
<TextField
className={classes.field}
onChange={(e) => setName(e.target.value)}
label="App Name"
variant="outlined"
color="secondary"
fullWidth
required
size="small"
defaultValue={selectedApp.name}
error={nameError}
/>
<TextField
className={classes.field}
onChange={(e) => setDescription(e.target.value)}
label="Description"
variant="outlined"
color="secondary"
rows={4}
fullWidth
required
size="small"
defaultValue={selectedApp.description}
error={descriptionError}
/>
<Grid container spacing={2}>
<Grid item>
<Button
// onClick={handleSubmit}
type="submit"
color="primary"
variant="contained"
endIcon={<SaveIcon />}
>
Save
</Button>
</Grid>
</Grid>
</form>
</Container>
) : (
<></>
)}
</div>
);
}
When using const [name, setName] = useState(defaultName), if the defaultName is updated in a future render, then the name value will not be updated to the this latest value.
So in your case you can make the following changes :
const [name, setName] = useState();
const [description, setDescription] = useState();
useEffect(() => {
setName(selectedApp.name)
setDescription(selectedApp.description)
}, [selectedApp])
)
Name and Description are undefined
Your selectedApp is initialized as an empty object. Your useEffect fires a request off to retrieve that data, but the page renders once before it gets the response. There are a couple of ways to handle this. You could do anything from displaying a loading icon on the field, to having a default value for the field until your useEffect with [selectedApp] is called. Once that information is retrieved and sent back, your information will be up to date in state, but if you need to store it for later, you'll need to build out a function to save that data.
Default value:
const [name, setName] = useState(selectedApp.name ?? "Your default value here");
const [description, setDescription] = useState(selectedApp.description ?? "Your default value here");
Loading icon:
{selectedApp ? (
<form noValidate autoComplete="off" onSubmit={handleSubmit}>
<TextField
className={classes.field}
onChange={(e) => setName(e.target.value)}
label="App Name"
variant="outlined"
color="secondary"
fullWidth
required
size="small"
defaultValue={selectedApp.name}
error={nameError}
/>
<TextField
className={classes.field}
onChange={(e) => setDescription(e.target.value)}
label="Description"
variant="outlined"
color="secondary"
rows={4}
fullWidth
required
size="small"
defaultValue={selectedApp.description}
error={descriptionError}
/>
<Grid container spacing={2}>
<Grid item>
<Button
// onClick={handleSubmit}
type="submit"
color="primary"
variant="contained"
endIcon={<SaveIcon />}
>
Save
</Button>
</Grid>
</Grid>
</form>
) : <LoadingIconComponent/>}

Cant bind form to nested array properly in react js

I'm trying to bind to a nested array to text inputs from a form. There are 6 blocks and each block contains 3 values. I have initially populated the array for mapping by using:
new Array(6).fill(['','',''])
and rendering the values using 2 loops. One for the 6 blocks and the other 3 values inside each block. The 3 values are the ones binding to the form. I refer to each of the inputs using the second parameter of the map function which is the index.
Here is the code in its entirety.
import {
Card,
CardContent,
CardHeader,
CardMedia,
TextField,
Grid,
Typography,
Button,
} from "#material-ui/core";
import axios from "axios";
import { useState } from "react";
import { useHistory } from "react-router";
export default function CreateEditQuestion() {
const answerPack = new Array(6).fill(["", "", ""]);
const [question, setQuestion] = useState("");
const [answers, setAnswer] = useState(answerPack);
const [TimeAllowed, setTimeAllowed] = useState(30);
const [Score, setScore] = useState(1);
const [Date, setDate] = useState("");
const history = useHistory();
const handleAnswerSet = (value, answerIndex, answerTextIndex) => {
var updatedAnswer = answers;
updatedAnswer[answerIndex][answerTextIndex] = value;
setAnswer(updatedAnswer);
};
const handleSubmit = () => {
let data = {
question,
answer_set: answers,
time_allowed: TimeAllowed,
score_value: Score,
date: Date,
};
for (const [key, value] of Object.entries({ question, Date })) {
if (value.trim().length == 0) {
alert(`${key} has not been filled in`);
return false;
}
}
axios
.post(
"https://localhost:8000/question",
data
)
.then((resp) => {
alert("Succesfully added Question");
history.push("/question");
})
.catch((err) => {
console.log(err);
});
};
return (
<>
<h1>Create Question</h1>
<Card elevation={1}>
<CardHeader></CardHeader>
<CardContent>
<div>
<TextField
fullWidth
label="Question"
variant="outlined"
onChange={(e) => {
setQuestion(e.target.value);
}}
></TextField>
</div>
<Grid container direction={"row"} spacing={4}>
{answers.map((answerTexts, i) => {
return (
<Grid key={i} item md={4} width={50}>
{answerTexts.map((text, j) => {
return (
<div style={{ width: "70%", marginTop: "30px" }}>
<TextField
fullWidth
label={`Answer ${i + 1} - ${j + 1}`}
onChange={(ev) => {
handleAnswerSet(ev.target.value, i, j);
}}
variant="outlined"
/>
<br />
</div>
);
})}
</Grid>
);
})}
</Grid>
<Grid container direction={"row"} spacing={4}>
<Grid item md={5} width={80}>
<Typography variant="h6" gutterBottom gutterTop>
Extra Options
</Typography>
<TextField
label={"Time Allowed : "}
variant="outlined"
defaultValue={TimeAllowed}
onChange={(ev) => {
setTimeAllowed(ev.target.value);
}}
/>{" "}
<TextField
label="Score"
variant="outlined"
defaultValue={Score}
onChange={(ev) => {
setScore(ev.target.value);
}}
/>
</Grid>
<Grid item md={5} width={100}>
<Typography variant="h6" gutterBottom gutterTop>
Question Date
</Typography>
<TextField
type="date"
onChange={(ev) => {
setDate(ev.target.value);
}}
/>
</Grid>
</Grid>
<div align="center">
<Button
onClick={() => {
handleSubmit();
}}
variant="contained"
color="primary"
>
Submit Question
</Button>
</div>
</CardContent>
</Card>
</>
);
}
Problem:
On each block, changing any value also changes all the other corresponding inputs in other blocks, so if the first input on the first block is changed, then all other first inputs in the other blocks also get changed,e.g(changing answer 1-1 also changes 2-1,3-1,4-1, etc). I could not trace why. Only the corresponding values should be changed
This is the function responsible for setting the values.
const handleAnswerSet = (value, answerIndex, answerTextIndex) => {
var updatedAnswer = answers;
updatedAnswer[answerIndex][answerTextIndex] = value;
setAnswer(updatedAnswer);
};

Control the material-ui slider with a play and stop buttons in React JS

I am using the continuous slider from metrial-ui in ReactJS project. I want to control the slider such that when click on the play button the slider start moving and stops when clicking stop.below is the code of the slider.
const useStyles = makeStyles({
root: {
width: 200,
},
});
<div className={classes.root}>
<Typography id="continuous-slider" gutterBottom>
Volume
</Typography>
<Grid container spacing={2}>
<Grid item>
<VolumeDown />
</Grid>
<Grid item xs>
<Slider value={value} onChange={handleChange} aria-labelledby="continuous-slider" />
</Grid>
<Grid item>
<VolumeUp />
</Grid>
</Grid>
Thanks in advance
This can be done by calling setState to update the Slider value after an interval. You'll need to use useEffect to create and clean up the interval callback when the isRunning state changes:
export default function ContinuousSlider() {
const [value, setValue] = useState<number>(30);
const [isRunning, setIsRunning] = useState(false);
const directionRef = useRef<"back" | "forward">("back");
const intervalIdRef = useRef(0);
const handleChange = (event: any, newValue: number | number[]) => {
setValue(newValue as number);
};
const handleBack = () => {
directionRef.current = "back";
if (!isRunning) {
setIsRunning(true);
}
};
const handleNext = () => {
directionRef.current = "forward";
if (!isRunning) {
setIsRunning(true);
}
};
const handleStop = () => {
setIsRunning((r) => !r);
};
useEffect(() => {
if (isRunning) {
intervalIdRef.current = setInterval(() => {
if (directionRef.current === "forward") {
setValue((v) => ++v);
}
if (directionRef.current === "back") {
setValue((v) => --v);
}
}, 16);
}
return () => clearInterval(intervalIdRef.current);
}, [isRunning]);
return (
<>
<IconButton onClick={handleStop}>
<StopIcon />
</IconButton>
<IconButton onClick={handleBack}>
<ArrowBackIcon />
</IconButton>
<IconButton onClick={handleNext}>
<ArrowForwardIcon />
</IconButton>
<Slider value={value} onChange={handleChange} />
</>
);
}
Live Demo

Dynamic input fields in React using functional component

I am trying to create dynamic input fields in my react application(using Material UI) on the click of a button. Whenever the add button is clicked, it creates a new input field inside a dialog box but I can't seem to enter any values in the text-field. Following is my app.js file -
import React from "react";
import "./styles.css";
import Button from "#material-ui/core/Button";
import TextField from "#material-ui/core/TextField";
import Dialog from "#material-ui/core/Dialog";
import DialogActions from "#material-ui/core/DialogActions";
import DialogContent from "#material-ui/core/DialogContent";
import DialogContentText from "#material-ui/core/DialogContentText";
import DialogTitle from "#material-ui/core/DialogTitle";
import IconButton from "#material-ui/core/IconButton";
import DeleteIcon from "#material-ui/icons/Delete";
import { Box, Grid } from "#material-ui/core";
export default function App() {
const [open, setOpen] = React.useState(false);
const [values, setValues] = React.useState([]);
const [text, setText] = React.useState("");
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
setValues([]);
};
const handleChangeText = (e) => {
setText(e.target.value);
};
const addValue = () => {
setValues([...values, ""]);
};
const handleValueChange = (index, e) => {
values[index] = e.target.value;
console.log(values);
setValues(values);
};
const deleteValue = (jump) => {
setValues(values.filter((j) => j !== jump));
};
return (
<div>
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
Create
</Button>
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="form-dialog-title"
>
<DialogTitle id="form-dialog-title">New Dialog</DialogTitle>
<DialogContent>
<DialogContentText>Sample Text.</DialogContentText>
<TextField
autoFocus
margin="dense"
value={text}
onChange={handleChangeText}
label="Text"
fullWidth
/>
{values.map((jump, index) => (
<Box key={"jump" + index}>
<Grid container spacing={1} alignItems="flex-end">
<Grid item xs={10}>
<TextField
autoFocus
margin="dense"
label="Value"
value={jump || ""}
onChange={(e) => handleValueChange(index, e)}
fullWidth
/>
</Grid>
<Grid item xs={2}>
<div
className="font-icon-wrapper"
onClick={() => deleteValue(jump)}
>
<IconButton aria-label="delete">
<DeleteIcon />
</IconButton>
</div>
</Grid>
</Grid>
</Box>
))}
</DialogContent>
<Button onClick={addValue} color="primary">
Add
</Button>
<DialogActions>
<Button onClick={handleClose} variant="contained" color="secondary">
Cancel
</Button>
<Button onClick={handleClose} variant="contained" color="primary">
Create
</Button>
</DialogActions>
</Dialog>
</div>
);
}
Here is the code sandbox link.
I have a simple text field inside the dialog box also that works perfectly. The problem is with the dynamic fields. I don't know what mistake I am making in this code. Someone please help me out. Thanks in advance
You a have problem in the function handleValueChanges, don't use mutation use immutable data.
the correct code:
const handleValueChange = (index, e) => {
const updatedValues = values.map((value, i) => {
if(i === index) {
return e.target.value
}else {
return value
}
})
setValues(updatedValues);
};
You can test from here: https://codesandbox.io/s/dynamic-textfield-forked-gck25?file=/src/App.js

Styling a card causes TextField to lose focus in Material UI + React

I'm trying to style my card so that it's not so big but the problem is every time I do that, my TextField loses its' functionality and I have to keep on clicking on the TextField because it keeps on losing focus. I need to make my Card component smaller without losing the functionality of my TextField.
https://codesandbox.io/s/mutable-monad-dsvf8?file=/src/index.js
import React, { useState } from "react";
import ReactDOM from "react-dom";
import Grid from "#material-ui/core/Grid";
import Button from "#material-ui/core/Button";
import Card from "#material-ui/core/Card";
import TextField from "#material-ui/core/TextField";
import CreateIcon from "#material-ui/icons/Create";
import Box from "#material-ui/core/Box";
import CardMedia from "#material-ui/core/CardMedia";
import MuiAlert from "#material-ui/lab/Alert";
import Snackbar from "#material-ui/core/Snackbar";
import { withStyles } from "#material-ui/core/styles";
const PetitionCard = () => {
const StyledCard = withStyles({
root: { height: 450, width: 350 }
})(Card);
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [open, setOpen] = useState(false);
const [petition, newPetition] = useState(false);
const handleTitleChange = event => {
setTitle(event.target.value);
};
const handleDescriptionChange = event => setDescription(event.target.value);
const handleClose = (event, reason) => {
if (reason === "clickaway") {
return;
}
};
const Alert = props => <MuiAlert elevation={6} variant="filled" {...props} />;
const Message = (message, severity) => {
return (
<Snackbar open={!open} autoHideDuration={3000} onClose={handleClose}>
<Alert severity={severity}>{message}</Alert>
</Snackbar>
);
};
const clearField = event => {
setOpen(true);
if (title.length > 0 && description.length > 0) {
setTitle("");
setDescription("");
return (
<Message
open={open}
message={"You have successfully created a petition!"}
severity={"success"}
/>
);
} else {
return (
<Message
message={"You have one more more fields missing"}
severity={"error"}
/>
);
}
};
return (
<StyledCard>
<Box mt={1}>
<Grid container justify="center">
<TextField
id="outlined-multiline-static"
multiline
rows={1}
variant="outlined"
placeholder="Title"
value={title}
onChange={handleTitleChange}
/>
</Grid>
</Box>
<CardMedia title="Petition" style={{ height: 0, paddingTop: "40.25%" }} />
<Box mt={1} justify="center">
<Grid container justify="center">
<TextField
size="small"
inputProps={{
style: { fontSize: 15 }
}}
id="outlined-multiline-static"
multiline
rows={5}
placeholder="Description"
variant="outlined"
value={description}
onChange={handleDescriptionChange}
/>
</Grid>
</Box>
<Box mt={1}>
<Grid container justify="center">
<Button onClick={clearField}>
<CreateIcon />
Create Petition!
</Button>
</Grid>
</Box>
</StyledCard>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.StrictMode>
<PetitionCard />
</React.StrictMode>,
rootElement
);
The problem is that you are re-creating StyledCard whenever PetitionCard is rendered:
const PetitionCard = () => {
const StyledCard = withStyles({
root: { height: 450, width: 350 }
})(Card);
[...]
}
Therefore, a new TextField is created on every render since its container changes. TextField is by default not focused and doesn't know whether the TextField from the previous render was focused.
The solution is to create StyledCard outside PetitionCard:
const StyledCard = withStyles({
root: { height: 450, width: 350 }
})(Card);
const PetitionCard = () => {
[...]
}

Categories