Formik instant feedback input box - javascript

I'm trying to make a input box component that has instant feedback using Formik. I want the input box to turn green when the user input matches a predefined string (the "answer"), gray if the input matches the prefix of the answer (including the empty string) and red otherwise. This string is stored as a property of the initial values, values.answer. The Formik validate function checks if the input equals values.answer and sets values.correct = true. I then created a css class corresponding to a green input box and set the className of the input conditional on the value of values.correct. The problem is it only seems to update (i.e turn green with a correct input) when I click out of focus of the input box (i.e onBlur). I would like it to work onChange. How would I do this?
Here is the relevant code sandbox: https://codesandbox.io/s/instant-feedback-box-lub0g?file=/src/Frame.js

Cool problem, but you've overcomplicated your code a little bit 😉 Some feedback:
touched is set to true during onBlur by default. You can override this by using setTouched(), but I found it simpler to just use values instead of touched in your form
try to keep values as minimal as possible, it's only meant to access input values so there's no need for hint and answer to be assigned to it
the purpose of the validation function is to return an errors object and not to set values, so remove assignments like values.correct = true
You don't need to store isDisabled in state, you can derive it from formik.submitCount and formik.isSubmitting
const Note = () => {
const [showFrame, setShowFrame] = useState({ 1: true });
const onCorrectSubmission = (frameId) => {
setShowFrame({ ...showFrame, [frameId]: true });
};
const text =
"What is the sum of the first three natural numbers? (give answer as a word, i.e one, two etc.)";
const hint = "The first three natural numbers are 1, 2, and 3";
const answer = "six";
return (
<div>
<h1>Induction</h1>
{showFrame[1] ? (
<Frame
id={1}
text={text}
hint={hint}
answer={answer}
onCorrectSubmission={onCorrectSubmission}
/>
) : null}
{showFrame[2] ? (
<Frame
id={2}
text={text}
hint={hint}
answer={answer}
onCorrectSubmission={onCorrectSubmission}
/>
) : null}
</div>
);
};
const Frame = ({
id,
text,
hint,
answer,
values,
onCorrectSubmission,
...props
}) => {
const validate = (values) => {
const errors = {};
if (!answer.startsWith(values.cloze)) {
errors.cloze = hint;
} else if (values.cloze !== answer) {
errors.cloze = true;
}
return errors;
};
const formik = useFormik({
initialValues: {
cloze: ""
},
validate,
onSubmit: (values) => {
onCorrectSubmission(id + 1);
}
});
const isFinished = formik.isSubmitting || formik.submitCount > 0;
return (
<form enablereinitialize={true} onSubmit={formik.handleSubmit}>
<p>{text}</p>
<input
id="cloze"
name="cloze"
type="text"
autoComplete="off"
{...formik.getFieldProps("cloze")}
disabled={isFinished}
className={`input
${!answer.startsWith(formik.values.cloze) ? "invalid-input" : ""}
${formik.values.cloze && !formik.errors.cloze ? "valid-input" : ""}
`}
/>
{formik.values.cloze && formik.errors.cloze ? (
<div>{formik.errors.cloze}</div>
) : null}
<button disabled={!!formik.errors.cloze || isFinished} type="submit">
Submit
</button>
</form>
);
};
export default Frame;
Live Demo

Related

how to validate user inputs in react using setState hook

I have a form. In the form I getting brandName, supplierName, and date of expiry from the user.
I am usinf TextField from mui library and submit button.
I want to disable submit button on empty form fields and enable it when user filled all the inputs
here is my code for declaring useStates
const[brandName, setBrandName] = useState("");
const[supplierName, setSupplierName] = useState("");
const[expiryDate, setExpiryDate] = useState(null);
const[brandNameError, setBrandNameError] = useState(false);
const[supplierNameError, setSupplierNameError] = useState(false);
const[expiryDateError, setExpiryDateError] = useState(false);
const[submitButton, setSubmitButton] = useState(true);
and here is all function which i used to validate my inputs
// checking brandName Error
const brandNameValidateOnBlur = ()=>{
if(brandName === ""){
setBrandNameError(true);
}
}
// checking supplier name error
const supplierNameValidateOnBlur = ()=>{
if(supplierName === ""){
setSupplierNameError(true);
}
}
// checking expiry date
const expiryDateValidateOnBlur = ()=>{
if(expiryDate === ""){
setExpiryDateError(true);
}
}
// now checking all inputs again if all inputs are good then
// button should be enabled
const checkAllInputs = ()=>{
if(brandName !== "" && supplierName !== "" && expiryDate !== ""){
setSubmitButton(false);
}else{
setSubmitButton(true);
}
}
and here is the rest of code
<TextField fullWidth id="productName" label="Product Name"
value={brandName.toLowerCase()} variant="outlined"
onChange={(data)=>{setBrandName(data.target.value.toUpperCase());checkAllInputs()}}
onBlur={brandNameValidateOnBlur}
onFocus={()=>setBrandNameError(false)}
error={brandNameError}
helperText = {brandNameError ? "Enter Brand Name" : ""}
/>
<TextField id="supplierName"
label="Supplier Name"
value={supplierName.toLowerCase()} variant="outlined"
onBlur={supplierNameValidateOnBlur}
onFocus={()=>setSupplierNameError(false)}
error={supplierNameError}
helperText={supplierNameError ? "Enter Supplier Name " : ""}
onChange={(data)=>{setSupplierName(data.target.value.toUpperCase());checkAllInputs()}} />
<LocalizationProvider dateAdapter={AdapterDayjs}>
<DesktopDatePicker
label="Date Expiry"
inputFormat="MM/DD/YYYY"
value={expiryDate}
onBlur={expiryDateValidateOnBlur}
onFocus={()=>setExpiryDateError(false)}
error={expiryDateError}
helperText={expiryDateError ? "Enter Expiry Date ": ""}
onChange={(selectedDate)=>
{setExpiryDate(selectedDate.format("MM/DD/YYYY"));checkAllInputs()}}
renderInput={(params) => <TextField {...params} />}
/>
</LocalizationProvider>
<Button type='button' variant='contained' id="submitButton" disabled={submitButton}
style={{backgroundColor:'orangered'}} onClick={addData}>Add Data</Button>
now problem is when i enter a single word my useState is updated but my checkAllInputs not working as i want to be like if i enter a word my checkAllInputs method runs before setting of state as i enter second entry then it works like I want to be so i did not know what i am doing wrong
I TRY useEffect hook like that
useEffect(()=>{
checkAllInputs();
},[brandName, supplierName, expiryDate]);
its working fine as i accepted but I read that it should be a expensive to use useEffect
and an other approach which i use
const checkAllInputsWithDom = ()=>{
let brandNameEntry = document.getElementById("brandName").value;
let supplierNameEntry = document.getElementById("supplierName").value;
let expiryDateEntry = document.getElementById("expiryDate").value;
if(brandNameEntry !=="" && supplierNameEntry !== "" && expiryDateEntry !== ""){
setSubmitButton(false);
}else{
setSubmitButton(true);
}
}
it is working out of box but I thing in react this is against react principles to direct manipulate
dome elements can I do it with out using useeffect hook like only with my metohd and one more thing
this did not set expiryDate error in date field
As long as you're keeping the input values in state, you don't need another variable storing the button state as well. You can calculate it at render time. (Read more about avoiding redundant state here.)
You could do something like this:
const Component = () => {
const[brandName, setBrandName] = useState("");
const[supplierName, setSupplierName] = useState("");
const[expiryDate, setExpiryDate] = useState("");
const disabled = brandName.length === 0 && supplierName.length === 0 && expiryDate.length === 0
return (
<>
<input value={brandName} onChange={(e) => setBrandName(e.target.value)}/>
<input value={supplierName} onChange={(e) => setSupplierName(e.target.value)}/>
<input value={expiryDate} onChange={(e) => setExpiryDate(e.target.value)}/>
<button disabled={disabled}>Button</button>
</>
)
}
here is the link for codesandbox I have a suggestion for you to refactor this 6 use States with just one useState as an object having all the values like the following:
const initialValues = {
brandName: "",
supplierName: "",
expiryDate: "",
brandNameError: "",
supplierNameError: "",
expiryDateError: ""
};
and then update it with a generic handleInputChange like below:
const handleInputChange = (e) => {
//const name = e.target.name
//const value = e.target.value
const { name, value } = e.target;
setValues({
...values,
[name]: value
});
let isEmpty = Object.values(values).some((x) => x === "");
console.log(isEmpty);
setIsDisabled(isEmpty);
};
FOR YOUR SOLUTION
You will have to just check all the values and just create a boolean with isDisabled so when all of those values are empty then it will only set to false.
I have created a code snippet for you, it needs some improvement but it will serve your purpose. Link already at the top and here as well
as suggested by Eduardo Motta de Moraes
I used this for making my button enabled after all inputs validate
const disabled= brandName.length === 0 || supplierName.length === 0 || expiryDate.length === 0;
this solve my problem
thanks for Eduardo Motta de Moraes for this

Cannot read the array property. Objects are not valid as a React child

I am following a tutorial exercise and I got the following error
Objects are not valid as a React child
I know this error is related to the object as I am trying to access the object but it needs an individual item of an object but not sure.
Why cannot the map loop over each item in the array?
Following is my code
var template = <h1>Indecision App</h1>;
var app = {
title: 'Indecision App',
subtitle: 'yo',
options: []
}
let count = 0;
function checkSubtitles (subtitle){
if(subtitle){
return <p>{subtitle}</p>
}else{
return undefined
}
}
function reset(){
count = 0;
reRenderApp();
}
function increaseCount(){
count++;
reRenderApp();
}
function onSubmitHandle(e){
e.preventDefault();
const options = e.target.elements.options;
app.options.push(options);
reRenderApp();
e.target.elements.options.value = ''
}
function removeAll(){
app.options = [];
reRenderApp();
}
function reRenderApp(){
var templateTwo = (
<div>
<h1>{app.title}</h1>
{checkSubtitles(app.subtitle)}
<p>Count: {count}</p>
<p>Array Length: {app.options.length > 0 ? app.options.length : '0 Items'}</p>
<ol>
{app.options.map((item)=>{
return <li key={item}>{item}</li>
})}
</ol>
<hr></hr>
<form onSubmit={onSubmitHandle}>
<input type="text" name="options" />
<input type="submit" value="Push to the Array" />
<input type="reset" value="Empty my list" onClick={removeAll} />
</form>
<button onClick={()=>{
increaseCount();
}}>Increase Count</button>
<button onClick={()=>{
reset();
}}>Reset Count</button>
</div>
)
ReactDOM.render(templateTwo, appRoot)
}
var appRoot = document.getElementById('app');
reRenderApp();
<body>
<div id="app"></div>
<script src="https://unpkg.com/react#16.0.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16.0.0/umd/react-dom.development.js"></script>
<script src="./app.js"></script>
</body>
</html>
The main problem is, as you mentioned: Objects are not valid as a React child
But, what is happening?
If we go into:
function onSubmitHandle(e){
e.preventDefault();
// Line 1
const options = e.target.elements.options;
app.options.push(options);
reRenderApp();
// Line 2
e.target.elements.options.value = ''
}
So in Line 1, you're pushing options into the options array.
But, then in Line 2, we can notice options has an attribute (so, it's an object)
So, if you change Line 1, from:
const options = e.target.elements.options;
To this:
const options = e.target.elements.options.value;
It'd work.
Also, to check what I'm saying you have 2 options:
option 1: console.log
function onSubmitHandle(e){
e.preventDefault();
const options = e.target.elements.options;
console.log({ options })
app.options.push(options);
reRenderApp();
e.target.elements.options.value = ''
}
option 2: make that option a valid child of react with JSON.stringify()
<ol>
{app.options.map((item, index)=>{
return <li key={index}>{JSON.stringify(item)}</li>
})}
</ol>
You can do
{app.options.length && app.options.map((item)=>{
return <li key={item}>{item}</li>
})}
But you must be sure that "item" here is not an object as you can't render an object
The reason for this is that your options array is going to be filled with elements as you're pushing the input element with the name of "option" into your array - this elements are objects in JS which you can't render out as list items.
Use React State to store anything that's going to change in the UI - in this case your list of options> So rather than doing
var app = {
title: 'Indecision App',
subtitle: 'yo',
options: []
}
let count = 0;
Do:
const [options, setOptions] = React.useState([]);
const [count, setCount] = React.useState(0);
Title and subtitle are probably not going to change, so just put them in h1 & h2 elements - if they are, then use the state pattern again.
Get rid of the two inputs with types of "submit" & "reset" just use normal button elements instead.
You'll also need an onchange event on your input where the text will go in and each time the onchange event is fired (i.e, when a user types) you'll need to save the input text
const [inputText, setInputText] = React.useState('');
const handleChange = (e) => {
const {value} = e.target;
setInputText(value)
}
<input type="text" value={inputText} onChange={handleChange}/>
Then in your onHandleSubmit function, just have
const onHandleSubmit = () => {
setOptions([...options, inputText]);
setInputText('')
}
This should work

Toggle correct answer message with radio button in React

Given a small quiz app, I'm trying to toggle a message under the questions to show the user that they have selected the correct answer or incorrect answer.
I have a small message that I'm gating with a boolean value which is set by the selection of the radio button (though the correct radio button isn't getting selected on initial click for some reason, only incorrect answers are), and need it to show a message of "You got it right" if the answer is true or "incorrect" if false. I can show true, but the logic isn't working to show false if the answer is incorrect and then clear the messaging when the "next question" button is clicked.
function App() {
let [points, setPoints] = useState(null);
let [counter, setCounter] = useState(null);
let [question, setQuestions] = useState();
let [isCorrect, setIsCorrect] = useState(false); <==Store correct answer selected
function Answer(props) {
return (
<li aria-labelledby="answers-list">
<label>
<input
type="radio"
name="answer_group"
className="answer"
value={props.answer}
onChange={checkAnswer}
/>
{props.answer}
</label>
</li>
);
}
function checkAnswer(e) {
let val = e.target.value;
let ans = question[counter].answers.filter((ans) => ans.value === val)[0];
displayCorrect(ans.correct);
//Toggles message to true if answer is correct
ans.correct === true ? setIsCorrect(true) : setIsCorrect(false);
}
function Quiz(props) {
return (
<div className="quiz">
<div className="quesiton" role="h2">
{props.question}
</div>
<ul className="answers">{props.children}</ul>
</div>
);
}
function displayCorrect(correct) {
let correct_msg = correct ? "correct" : "incorrect";
console.log("Answer was " + correct_msg);
}
function nextQuestion() {
setIsCorrect(false); <== Should hide message when user selects "next question" button
if (document.querySelector('input[name="answer_group"]:checked') == null) {
alert("Must select an answer before proceeding to the next question");
return;
}
let val = document.querySelector('input[name="answer_group"]:checked')
.value;
let answerObj = question[counter].answers.filter(
(ans) => ans.value === val
)[0];
let updated_points = answerObj.correct ? points + 1 : points;
setPoints(updated_points);
let nextQuestion = counter + 1;
if (counter < question.length - 1) {
setCounter(nextQuestion);
} else {
setCounter(0);
}
displayCorrect(answerObj.correct);
}
return (
<div className="Quiz slide-top">
{!is_started ? (
<div className="start-intro-wrapper">
<h1 className="name">Quiz App</h1>
<Starter start={start} />
</div>
) : (
<div className="quick-wrapper slide-in-bottom">
<Quiz question={question[counter].question}>
{question[counter].answers.map((answer, index, arr) => {
return (
<Answer
key={index}
index={index}
answer={answer.value}
correct={answer.correct}
/>
);
})}
</Quiz>
<div className="answer-controls">
{isCorrect === true ? <p>You got it right!</p> : null} <== Messaging should show if user has selected correct message if correct option is selected
<button onClick={(e) => nextQuestion()}>
Next question
</button>
</div>
</div>
)}
</div>
);
}
Codesandbox Link
It doesn't display anything because you put null in the other condition for the display.
If you replace it with {isCorrect === true ? <p>You got it right!</p> : <p>You got it wrong!</p>} the correct message is displayed.
You can also remove completly the function displayCorrect it's only writing in the console.
If you want to clear the message when the user click on "next question" you could do it with a new variable hasAnswered set at false by default.
You set it to true in your checkAnswer function, and to false in your nextQuestion function.
The code for the text at the bottom would then looks like:
{hasAnswered &&
(isCorrect === true ? <p>You got it right!</p> : <p>You got it wrong!</p>)
}
Regarding the issue with you needing to click twice to update the radio button selected, i think it's a re-render issue when isCorrect change.
To avoid the issue, you could store the value checked by the user and use this to display if the radio should be checked or no.
It works with the following changes
let [isChecked, setChecked] = useState(null);
[...]
function Answer(props) {
return (
<li aria-labelledby="answers-list">
<label>
<input
checked={isChecked===props.answer}
type="radio"
name="answer_group"
className="answer"
value={props.answer}
onChange={checkAnswer}
/>
{props.answer}
</label>
</li>
);
}
function checkAnswer(e) {
let val = e.target.value;
let ans = question[counter].answers.filter((ans) => ans.value === val)[0];
setIsCorrect(ans.correct);
setChecked(ans.value);
}
You also need setChecked(null) in your nextQuestion function to reset everything.

How to Handle Multiple Checkboxes in typescript

I am trying to handle multiple checkboxes as follows:
sendFileNameToBackEnd = (filename: string[]) => {
console.log(filename)
this.vizsualizaForFileName(filename)
}
render() {
const handleChange = (checked: boolean, event: React.FormEvent<HTMLInputElement>) => {
let listoffilename : Array<string> = []
const target = event.currentTarget;
const name = target.name;
if(checked && name !== '')
{
listoffilename.push(name)
this.setState({
listoffilename1: listoffilename
});
}
console.log(this.state.listoffilename1)
};
return (
<DataList aria-label="Checkbox and action data list example" isCompact >
{ this.state.fileListData ?
this.state.fileListData.map((fd) =>
<DataListItem aria-labelledby="check-action-item1">
<DataListItemRow>
<DataListCheck
id="controlled-check"
aria-labelledby="check-action-item"
isChecked={isChecked1}
onChange={handleChange}
name={fd}/>
<DataListItemCells
dataListCells={[
<DataListCell key="primary content">
<span id="check-action-item1">{fd ? fd : "Loading..."}</span>
</DataListCell>
]}
/>
</DataListItemRow>
</DataListItem>
):
<>
<EmptyState>
<Title headingLevel="h4" size="lg">Empty state</Title>
</EmptyState>
</>
}
</DataList>
<Button variant="primary" onClick={ () => {this.sendFileNameToBackEnd(this.state.listoffilename1)}}>Visualize</Button>
);
}
In the above code, when I select any checkbox, I can get its value, but when I choose multiple checkboxes I only getting one value in an array.
In 'this.state.fileListData' I have n number of data and I am displaying a checkbox using a map and one button called 'Visualize'. When we select some of the values from the checkbox and click on the 'Visualize' button, I want to send all selected values to the backend for another operation.
When I select multiple checkboxes then it only sends one value in it not all selected values.
Guide me if I am doing something wrong.

restrict special characters in reactNative TextInput

I'm trying to prevent my TextInput from getting values like $,%,^,&,(,) etc. Basically my TextInput should allow letters only. My approach is as follows. But still i'm able to input these other characters. How can i prevent special characters from the TextInput
restrict(event) {
const regex = new RegExp("^[a-zA-Z]+$");
const key = String.fromCharCode(!event.charCode ? event.which : event.charCode);
if (!regex.test(key)) {
event.preventDefault();
return false;
}
}
<TextInput
underlineColorAndroid='transparent'
allowFontScaling={false}
style={styles.questionText}
onKeyPress={e => this.restrict(e)}
value={firstNameState}
/>
the onKeyPress event on android does not work very well.
That is why I have chosen to use a method that eliminates these characters and then save it wherever you want, just as it might change the state of your field.
restrict = text => text.replace(/[`~0-9!##$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, '')
I have to block special characters by this line of code.
var format = /[!##$%^&*()_+-=[]{};':"\|,.<>/?]+/;
if(format.test(string)){ }
You may define your OnChange event handler using your regex, where you will check if the input string matches your regex with /^[^!-\/:-#\[-`{-~]+$/.test(text):
const {useState} = React;
const App = () => {
const [value, setValue] = useState("");
const onChange = e => {
const input = e.currentTarget.value;
if (/^[^!-\/:-#\[-`{-~]+$/.test(input) || input === "") {
setValue(input);
}
};
return (
<div className="App">
<input
value={value}
onChange={onChange}
underlineColorAndroid='transparent'
allowFontScaling={false}
/>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js"></script>

Categories