I am using CKEditor5 with React.
Functionality: User creates multiple questions with answer options.
Simplified version of code:
function QuestionComponent() {
const [questions, setQuestions] = useState([{ question: null, choices: [] }]);
const [index, setIndex] = useState(0);
function previous(e) {
e.preventDefault();
setIndex(index - 1);
}
function next(e) {
e.preventDefault();
// Adding new empty question
if (questions.length <= index + 1) {
setQuestions([...questions, { question: null, choices: [] }]);
}
setIndex(index + 1);
}
function handleQuestionInput(value) {
setQuestions(
questions.map((el, i) => {
if (i === index) {
el.question = value;
}
return el;
})
);
}
function handleChoiceInput(value, choiceIndex) {
setQuestions(
questions.map((el, i) => {
if (i === index) {
el.choices[choiceIndex].text = value;
}
return el;
})
);
}
return (
<>
<CKEditor
editor={Editor}
config={EditorConfig}
data={questions[index].question || ""}
onChange={(event, editor) => {
const data = editor?.getData();
handleQuestionInput(data);}}
/>
<div className="choices-box">
{questions[index].choices.map((el, i) => (
<CKEditor
editor={Editor}
config={EditorConfig}
data={el.text || ""}
onChange={(event, editor) => {
const data = editor?.getData();
handleChoiceInput(data, i);
}}
/>))
}
</div>
<button
type="submit"
label="Previous"
onClick={previous}
/>
<button
type="submit"
label="Next"
onClick={next}
/>
}
Problem: Whenever Next or Previous is clicked current question (questions[index].question) value is cleared and set to "" (or default value from data={questions[index].question || ""}) . However, choices keep their state and work well.
I tested that this code works well if I change CKEditor with simple input
I am also wondering why it is working with choices, but not question
Thanks in advance
Related
import React, {useState} from 'react'
import Result from './Result'
import './Exam.css'
function Exam() {
const data = JSON.parse(localStorage.getItem('Que'))
const selectedAns = JSON.parse(localStorage.getItem('Ans'))
const [current, setCurrent] = useState(0)
const [ans, setAns] = useState({selectedAns:""})
const [correct, setCorrect] = useState(0)
const [exit, setExit] = useState(false)
const input = (e) => {
//setAns((ans) => ans, e.target.value)
setAns({...ans,
[e.target.name]: e.target.value})
}
console.log(ans);
const handlePrevious = (e) => {
setCurrent(current - 1)
let allRadio = document.querySelectorAll('.radioButton')
console.log(selectedAns[(current)].selectedAns);
allRadio.forEach(value => {
if(selectedAns[current].selectedAns === value.value){
value.checked = true
}
})
}
const handlerSave = () => {
if (data[current].answer === ans) {
setCorrect(correct + 1)
}
if ((current + 1) === data.length){
setExit(true)
}
else {
setCurrent(current + 1)
}
let allRadio = document.querySelectorAll('.radioButton')
allRadio.forEach(value => value.checked = false)
const answers = JSON.parse(localStorage.getItem("Ans") || "[]")
answers.push(ans)
localStorage.setItem('Ans', JSON.stringify(answers))
}
return(
<div>
{ exit === false ?
<div className='mcq'>
<div>
Question :<span className='span'>{current + 1}</span>
{data[current].question}?
</div><br/>
<div>
<div className='option'><input type='radio' value='A' name='selectedAns' onChange={input} className='radioButton'/> {data[current].A} </div>
<div className='option'><input type='radio' value='B' name='selectedAns' onChange={input} className='radioButton'/> {data[current].B} </div>
<div className='option'><input type='radio' value='C' name='selectedAns' onChange={input} className='radioButton'/> {data[current].C} </div>
<div className='option'><input type='radio' value='D' name='selectedAns' onChange={input} className='radioButton'/> {data[current].D} </div>
</div><br/>
<div>
{ current !== 0 && <button onClick={handlePrevious}>Previous</button>}
<button onClick={handlerSave}>Save & Next</button>
</div>
</div> :
<Result props={{correct}}/>
}
</div>
)
}
export default Exam;
I am making a quiz app in react js. I am facing an issue which is that I have hundreds of questions with four options, and also next and previous buttons in that. For options, I am using radio buttons. I want that after the user selects an option for the first question, goes to the next question and selects an option for the second question, he will be able to go to the previous question, and there the radio button will be checked with the option the user has been selected earlier.
i have tried this function.
but it gives me diffrent button checked
const handlePrevious = (e) => { setCurrent(current - 1) let allRadio = document.querySelectorAll('.radioButton') console.log(ans); allRadio.forEach(value => { if(ans === value.value){ value.checked = true } }) }
You can create a hook with the help of useState and add default type as object.
At question -1 index you can store your value. OnSelectRadio function will accept event and index which will be called when you click on any of the radio button.
const[ans, setAns] = useState({}):
onSelectRadio(event, idx){
let temp = [..ans];
temp[idx+1] = event.target.value;
setAns(temp);
}
I have made another function to check status of my radio button...
if have any other query please check my full code first and then my solution.
const handlePrevious = (e) => {
setCurrent(current - 1)
Previous()
}
const Previous = () => {
let allRadio = document.querySelectorAll('.radioButton')
allRadio.forEach(value => {
if(selectedAns[current-1].index === value.value){
value.checked = true
}
})
}
const Component = ()=>{
const [list, setList] = useState(getLocalStorage());
const [isEditing, setIsEditing] = useState(false);
const [itemToEdit, setItemToEdit] = useState();
const refContainer = useRef(null);
const putLocalStorage = () => {
localStorage.setItem("list", JSON.stringify(list));
};
const editItem = (id) => {
refContainer.current.focus();
setItemToEdit(() => {
return list.find((item) => item.id === id);
});
setIsEditing(true);
};
const handleSubmit = (e)=>{
e.preventDefault();
let nameValue = refContainer.current.value;
if (isEditing){
setList(list.map((item)=>{
if (item.id === itemToEdit.id){
return {...item, name: nameValue};
}
else {
return item;
}
);
}
else {
let newItem = {
id: new Date().getItem().toString(),
name: nameValue,
}
setList([...list, newItem])
}
nameValue="";
setIsEditing(false);
}
useEffect(() => {
putLocalStorage();
}, [list]);
return (
<div>
<form onSubmit={handleSubmit}>
<input type="text" ref={refContainer} defaultValue={isEditing ? itemToEdit.name : ""}/>
<button type="submit">submit</button>
</form>
<div>
{list.map((item) => {
const { id, name } = item;
return (
<div>
<h2>{name}</h2>
<button onClick={() => editItem(id)}>edit</button>
<button onClick={() => deleteItem(id)}>
delete
</button>
</div>
);
})}
</div>
</div>
)
}
So this part:
<input type="text" ref={refContainer} defaultValue={isEditing ? itemToEdit.name : ""} />
I want to show to users what they are editing by displaying the itemToEdit on the input.
It works on the first time when the user clicks edit button
But after that, the defaultValue does not change to itemToEdit
Do you guys have any idea for the solution?
(i could use controlled input instead, but i want to try it with useRef only)
Otherwise, placeholder will be the only solution...
The defaultValue property only works for inicial rendering, that is the reason that your desired behavior works one time and then stops. See a similar question here: React input defaultValue doesn't update with state
One possible solution still using refs is to set the itemToEdit name directly into the input value using ref.current.value.
const editItem = (id) => {
refContainer.current.focus();
setItemToEdit(() => {
const item = list.find((item) => item.id === id);
refContainer.current.value = item.name;
return item;
});
setIsEditing(true);
};
We have a pretty basic form that has a dynamic number of inputs. The number of inputs is being passed in as a prop to our SerialsForm component. We would like to set focus on the first input when this form renders.
const SerialsForm = ({qty}) => {
const { handleSubmit, register, setFocus, control } = useForm();
const { fields, append, prepend, remove } = useFieldArray({ name: 'serials', control });
// Handle qty changes
useEffect(() => {
const oldVal = fields.length;
if (qty > oldVal) {
for (let i = oldVal; i < qty; i++) {
append({ value: '' });
}
} else {
for (let i = oldVal; i > qty; i--) {
remove(i - 1);
}
}
}, [qty, fields])
const handleFormSubmit = (data) => {
console.log(data)
}
return (
<Form onSubmit={handleSubmit(handleFormSubmit)}>
<div>
{fields.map((item, i) => (
<FormGroup key={item.id}>
<FormControl
placeholder={`Serial #${i + 1}`}
{...register(`serials.${i}.value`)}
/>
</FormGroup>
))}
<Button type='submit'>
<CheckCircle size={20} />
</Button>
</div>
</Form>
);
}
What We Have Tried:
adding { shouldFocus: true } and { shouldFocus: true, focusIndex: 0} as well as { shouldFocus: true, focusName: serials.0.value } as the second argument to append --> Nothing focuses
adding setFocus('serials.0.value'); at the end of useEffect. --> Error
adding autoFocus={i === 0} to the bootstrap FormControl (input) element. --> Nothing focuses
We have tried the same with prepend (we will likely end up using prepend because we want the first input focused)
Any idea of how to accomplish this would be greatly appreciated as we are not seeing a way to set focus outside of using a second argument to append, and that does not seem to be working.
What ended up working was adding a setTimeout to the end of the useEffect call.
useEffect(() => {
// other useEffect code
setTimeout(() => {
if (qty) {
setFocus('serials.0.value');
}
}, 0);
}, [qty, setFocus, ...yourOtherDeps])
I've created a form and am saving the data to a json file locally. I can save all the data except for the questions with multiple selections and multiple checkboxes. It only saves the last one selected. I am trying to write a switch statement within a React Hook that is working to help save the submitted form. I keep getting an error "cannot identify type of undefined." I'm new to react and don't know what to do from here.
This is in my hooks folder:
export const useInputChange = (customValue, callback) => {
const [value, setValue] = useState(customValue ? customValue : "" || []);
const handleChange = (event) => {
var newValue;
switch (customValue.type) {
case "multipleSelection":
newValue = $("multipleSelection").find("option:checked");
break;
case "checkboxChoice":
newValue = $("checkboxChoice").find("input:checked");
break;
default:
newValue = event.target.value;
}
setValue(newValue);
if (callback) {
callback(event.target.name, newValue);
}
};
return {
value: value,
handleChange: handleChange
};
};
This is my callback in my components folder:
const callback = (name, value) => {
console.log("callback", name, value);
inlineData[name] = value;
setInlineData(inlineData);
console.log(inlineData);
};
The jquery works in the console to pull up the correct arrays.
This is the component:
export const Survey = (props) => {
const [page, setPage] = useState(1);
const [isFinalPage, setIsFinalPage] = useState(false);
const [surveyValues, setSurveyValues] = useState({});
const [loadedInputs, setLoadedInputs] = useState({});
const [question, setQuestion] = useState({});
const [inlineData, setInlineData] = useState({});
const { surveyId } = props;
const triggerBackendUpdate = () => {
console.log(question);
console.log(surveyValues);
setPage(1);
setSurveyValues({});
setQuestion({});
};
useEffect(() => {
if (surveyId) {
const inputDataFile = import(`./data_${surveyId}.json`);
inputDataFile.then((response) => {
setLoadedInputs(response.default);
});
}
});
const handleSubmit = (event) => {
event.preventDefault();
event.persist();
for (let formInput of event.target.elements) {
const isText = isTextInput(formInput.type);
console.log(formInput);
if (isText) {
surveyValues[formInput.name] = formInput.value;
question[formInput.question] = formInput.question;
}
if (formInput.type === "selectMultiple") {
let selected = [].filter.call(
formInput.options,
(option) => option.selected
);
console.log(formInput);
console.log(selected);
console.log(formInput.options.selected);
const values = selected.map((option) => option.value);
surveyValues[formInput.name] = values;
question[formInput.name] = formInput.question;
}
if (formInput.type === "checkbox") {
surveyValues[formInput.name] = formInput.value;
question[formInput.name] = formInput.question;
}
}
setQuestion(question);
setSurveyValues(surveyValues);
const nextPage = page + 1;
const inputs = props.inputs
? props.inputs.filter((inputOption) => inputOption.page ===
nextPage): [];
if (isFinalPage) {
triggerBackendUpdate();
} else {
if (inputs.length === 0) {
setIsFinalPage(true);
} else {
setPage(nextPage);
}
}
};
const callback = (name, value) => {
console.log("callback", name, value);
inlineData[name] = value;
setInlineData(inlineData);
console.log(inlineData);
};
const saveSurvey = async () => {
await fetch("/api/survey", {
method: "POST",
body: JSON.stringify(inlineData),
headers: {
"Content-Type": "application/json",
},
}).catch((error) => {
console.error(error);
});
};
const inputs = props.inputs
? props.inputs.filter((inputOption) => inputOption.page === page)
: [];
return (
<form onSubmit={handleSubmit}>
{isFinalPage !== true &&
inputs.map((obj, index) => {
let inputKey = `input-${index}-${page}`;
return obj.type === "radio" || obj.type === "checkbox" ? (
<SurveyRadioInput
object={obj}
type={obj.type}
required={props.required}
triggerCallback={callback}
question={obj.question}
defaultValue={obj.defaultValue}
name={obj.name}
key={inputKey}
/>
) : obj.type === "checkbox" ? (
<SurveyCheckboxInput
object={obj}
type={obj.type}
required={props.required}
triggerCallback={callback}
question={obj.question}
defaultValue={obj.defaultValue}
name={obj.name}
key={inputKey}
/>
) : obj.type === "select" ? (
<SurveySelectInput
className="form-control mb-3 mt-3"
object={obj}
type={obj.type}
question={obj.question}
required={props.required}
triggerCallback={callback}
defaultValue={obj.defaultValue}
name={obj.name}
key={inputKey}
/>
) : obj.type === "selectMultiple" ? (
<SurveySelectMultipleInput
className="form-control mb-3 mt-3"
object={obj}
type={obj.type}
question={obj.question}
required={props.required}
triggerCallback={callback}
defaultValue={obj.defaultValue}
name={obj.name}
key={inputKey}
/>
) : (
<SurveyTextInput
className="mb-3 mt-3 form-control"
object={obj}
type={obj.type}
question={props.question}
required={props.required}
triggerCallback={callback}
placeholder={obj.placeholder}
defaultValue={obj.defaultValue}
name={obj.name}
key={inputKey}
/>
);
})}
{isFinalPage !== true ? (
<button name="continue-btn" className="btn btn-primary my-5 mx-5">
Continue
</button>
) : (
<Link to="/thankyou">
<button
onClick={saveSurvey}
type="button"
className="btn btn-primary my-5 mx-5"
>
Submit Survey
</button>
</Link>
)}
</form>
);
};
This is in my inputs folder:
export const SurveySelectMultipleInput = (props) => {
const { object } = props;
const { value, handleChange } = useInputChange(
props.defaultValue,
props.triggerCallback
);
const inputType = isTextInput(props.type) ? props.type : "";
const inputProps = {
className: props.className ? props.className : "form-control",
onChange: handleChange,
value: value,
required: props.required,
question: props.question,
type: inputType,
name: props.name ? props.name : `${inputType}_${props.key}`,
};
console.log(value);
return (
<>
<div id={object.name}>
<h5>{props.question}</h5>
<select
{...inputProps}
name={object.name}
className={props.className}
multiple={object.multiple}
>
<option hidden value>
Select one
</option>
{object.options.map((data, index) => {
return (
<option
value={data.value}
id={`${object.name}-${index}`}
key={`${object.type}-${index}`}
className={`form-check ${props.optionClassName}`}
>
{data.label}
</option>
);
})}
</select>
</div>
</>
);
};
It's hard to tell exactly how your components and hooks behave without having an example showing their behavior and properties. Regardless, I made some assumptions and tried to answer:
First of all, what is the expected type of customValue in useInputChange? Are you expecting a string or an array? Then what is the type attribute on it that you're checking in your switch statement?
As for the jquery selector, what is multipleSelection? Is it the class name you're using for your select elements? Then your selector must start with a dot a/nd then you can get the value by calling .val method on the selected element:
newValue = $(".multipleSelection").val();
Here's a working example for multiple select elements, using your code: https://codepen.io/kaveh/pen/QWNNQMV
Note that I had to assign an arbitrary type attribute to VALUE to get it working with your switch statement.
All that being said, as I mentioned in my comment, it's recommended to use ref to access elements created by React and not other query selectors such as those you get from jquery.
https://reactjs.org/docs/refs-and-the-dom.html
https://reactjs.org/docs/hooks-reference.html#useref
Here is the original example of group checkbox of antd that I need and its fine:
const plainOptions = ['Apple', 'Pear', 'Orange'];
const defaultCheckedList = ['Apple', 'Orange'];
class App extends React.Component {
state = {
checkedList: defaultCheckedList,
indeterminate: true,
checkAll: false,
};
onChange = checkedList => {
this.setState({
checkedList,
indeterminate: !!checkedList.length && checkedList.length < plainOptions.length,
checkAll: checkedList.length === plainOptions.length,
});
};
onCheckAllChange = e => {
this.setState({
checkedList: e.target.checked ? plainOptions : [],
indeterminate: false,
checkAll: e.target.checked,
});
};
render() {
return (
<div>
<div style={{ borderBottom: '1px solid #E9E9E9' }}>
<Checkbox
indeterminate={this.state.indeterminate}
onChange={this.onCheckAllChange}
checked={this.state.checkAll}
>
Check all
</Checkbox>
</div>
<br />
<CheckboxGroup
options={plainOptions}
value={this.state.checkedList}
onChange={this.onChange}
/>
</div>
);
}
}
My question is how can I replace the plainOptions and defaultCheckedList by object array instead of simple array and using attribute name for this check boxes?
For example this object:
const plainOptions = [
{name:'alex', id:1},
{name:'milo', id:2},
{name:'saimon', id:3}
];
const defaultCheckedList = [
{name:'alex', id:1},
{name:'milo', id:2}
];
I want to use attribute name as the key in this example.
Problem solved. I should use "Use with grid" type of group checkbox. It accepts object array. The only think I could do was creating a function that inject "label" and "value" to my object. It makes some duplicates but no problem.
function groupeCheckboxify(obj, labelFrom) {
for (var i = 0; i < obj.length; i++) {
if (obj[i][labelFrom]) {
obj[i]['label'] = obj[i][labelFrom];
obj[i]['value'] = obj[i][labelFrom];
}
if (i == obj.length - 1) {
return obj;
}
}
}
// for calling it:
groupeCheckboxify( myObject , 'name');
I'd this same problem and couldn't find any answer on the entire web. But I tried to find a good way to handle it manually.
You can use this code:
import { Checkbox, Dropdown } from 'antd';
const CheckboxGroup = Checkbox.Group;
function CheckboxSelect({
title,
items,
initSelectedItems,
hasCheckAllAction,
}) {
const [checkedList, setCheckedList] = useState(initSelectedItems || []);
const [indeterminate, setIndeterminate] = useState(true);
const [checkAll, setCheckAll] = useState(false);
const onCheckAllChange = (e) => {
setCheckedList(e.target.checked ? items : []);
setIndeterminate(false);
setCheckAll(e.target.checked);
};
const onChangeGroup = (list) => {
if (hasCheckAllAction) {
setIndeterminate(!!list.length && list.length < items.length);
setCheckAll(list.length === items.length);
}
};
const updateItems = (el) => {
let newList = [];
if (el.target.checked) {
newList = [...checkedList, el.target.value];
} else {
newList = checkedList.filter(
(listItem) => listItem.id !== el.target.value.id,
);
}
setCheckedList(newList);
};
useEffect(() => {
setCheckedList(initSelectedItems);
}, []);
const renderItems = () => {
return (
<div classname="items-wrapper">
{hasCheckAllAction ? (
<Checkbox
indeterminate={indeterminate}
onChange={onCheckAllChange}
checked={checkAll}
>
All
</Checkbox>
) : null}
<CheckboxGroup onChange={onChangeGroup} value={checkedList}>
<>
{items.map((item) => (
<Checkbox
key={item.id}
value={item}
onChange={($event) => updateItems($event)}
>
{item.name}
</Checkbox>
))}
</>
</CheckboxGroup>
</div>
);
};
return (
<Dropdown overlay={renderItems()} trigger={['click']}>
<div>
<span className="icon icon-arrow-down" />
<span className="title">{title}</span>
</div>
</Dropdown>
);
}
It looks like the only difference you are talking about making is using an array of objects instead of strings? If that's the case, when looping through the array to create the checkboxes, you access the object attributes using dot notation. It should look something like this if I understand the problem correctly.
From CheckboxGroup component:
this.props.options.forEach(el => {
let name = el.name;
let id = el.id;
//rest of code to create checkboxes
or to show an example in creating components
let checkboxMarkup = [];
checkboxMarkup.push(
<input type="checkbox" id={el.id} name={el.name} key={`${el.id} - ${el.name}`}/>
);
}
'el' in this case refers to each individual object when looping through the array. It's not necessary to assign it to a variable, I just used that to show an example of how to access the properties.