Currently I have a textarea like this:
<textarea
onChange={handleTextAreaChange}
ref={textAreaRef as MutableRefObject<HTMLTextAreaElement>}
id={id}
value={content}
></textarea>
I am implementing some buttons to add markdown to the textarea to make it easier for the user to update and have this function for bold:
const handleBoldClick = useCallback(() => {
const selectionStart = textAreaRef.current?.selectionStart;
const selectionEnd = textAreaRef.current?.selectionEnd;
if (selectionStart && selectionEnd) {
setContent(
prevContent =>
prevContent.substring(0, selectionStart) +
'**' +
prevContent.substring(selectionStart, selectionEnd) +
'**' +
prevContent.substring(selectionEnd, prevContent.length)
);
} else {
setContent(prevContent => prevContent + '****');
// Want this to run after textarea gets updated
textAreaRef.current?.focus();
textAreaRef.current?.setSelectionRange(
content.length - 3,
content.length - 3
);
}
const changeEvent = new Event('change', { bubbles: true });
// Want to run this after textarea is updated
textAreaRef.current?.dispatchEvent(changeEvent);
}, [content]);
setContent is the setter for content which is passed to the textarea. Is there a way to guarantee the parts I've marked with comments as wanting to only run once the DOM gets updated run when I want them to?
I finagled around with things and went with this approach (gonna post the entire component, which contains some stuff irrelevant to the question):
const MarkdownTextArea = ({
value,
onBlur = () => {},
onChange = () => {},
touched = false,
error,
id,
label
}: MarkdownTextAreaProps) => {
const [content, setContent] = useState(value ?? '');
const [numberOfRows, setNumberOfRows] = useState(5);
const [numberOfCols, setNumberOfCols] = useState(20);
const [isPreview, setIsPreview] = useState(false);
const [changed, setChanged] = useState<'bold' | null>();
const textAreaRef = useRef<HTMLTextAreaElement>();
useEffect(() => {
const setColsAndRows = () => {
const newColumnsNumber = Math.floor(
(textAreaRef.current?.offsetWidth ?? 100) /
(convertRemToPixels(1.2) / 1.85)
);
setNumberOfCols(newColumnsNumber);
setNumberOfRows(calculateNumberOfRows(content, newColumnsNumber));
};
setColsAndRows();
window.addEventListener('resize', setColsAndRows);
return () => {
window.removeEventListener('resize', setColsAndRows);
};
}, [content]);
const handleTextAreaChange: ChangeEventHandler<HTMLTextAreaElement> =
useCallback(
event => {
onChange(event);
setContent(event.target.value);
setNumberOfRows(
calculateNumberOfRows(
event.target.value,
textAreaRef.current?.cols ?? 20
)
);
},
[onChange]
);
const handleBoldClick = useCallback(() => {
const selectionStart = textAreaRef.current?.selectionStart;
const selectionEnd = textAreaRef.current?.selectionEnd;
if (selectionStart && selectionEnd) {
setContent(
prevContent =>
prevContent.substring(0, selectionStart) +
'**' +
prevContent.substring(selectionStart, selectionEnd) +
'**' +
prevContent.substring(selectionEnd, prevContent.length)
);
} else {
setContent(prevContent => prevContent + '****');
}
setChanged('bold');
}, []);
if (changed && textAreaRef.current?.value === content) {
const changeEvent = new Event('change', { bubbles: true });
textAreaRef.current?.dispatchEvent(changeEvent);
if (changed === 'bold' && textAreaRef.current) {
textAreaRef.current.focus();
textAreaRef.current.selectionStart = content.length - 2;
textAreaRef.current.selectionEnd = content.length - 2;
}
setChanged(null);
}
return (
<div className={classes.container} data-testid="markdown-text-area">
<div className={classes['header']}>
<label className={classes.label} htmlFor={id}>
{label}
</label>
<Button
positive
style={{ justifySelf: 'flex-end' }}
onClick={() => setIsPreview(prev => !prev)}
>
{isPreview ? 'Edit' : 'Preview'}
</Button>
</div>
<div className={classes['text-effect-buttons']}>
<button
className={classes['text-effect-button']}
onClick={handleBoldClick}
type="button"
style={{ fontWeight: 'bold' }}
>
B
</button>
</div>
{isPreview ? (
<div className={classes['markdown-container']} id={id}>
<MarkdownParser input={content} />
</div>
) : (
<textarea
onChange={handleTextAreaChange}
className={`${classes['text-input']}${
error && touched ? ` ${classes.error}` : ''
}`}
ref={textAreaRef as MutableRefObject<HTMLTextAreaElement>}
rows={numberOfRows}
cols={numberOfCols}
onBlur={onBlur}
id={id}
value={content}
></textarea>
)}
{error && touched && (
<div className={classes['error-message']}>{error}</div>
)}
</div>
);
};
The part of the following component most relevant to answering the question is the following:
if (changed && textAreaRef.current?.value === content) {
const changeEvent = new Event('change', { bubbles: true });
textAreaRef.current?.dispatchEvent(changeEvent);
if (changed === 'bold' && textAreaRef.current) {
textAreaRef.current.focus();
textAreaRef.current.selectionStart = content.length - 2;
textAreaRef.current.selectionEnd = content.length - 2;
}
setChanged(null);
}
Related
I'm using react in my symfony project. I need to save data through a form.
No problem for saving string data, but not date one.
Any idea of how to do it ?
const CvexperienceForm = React.memo(({post = null, onComment, cvexperience = null, onCancel = null}) => {
// Variables
const ref = useRef(null)
const refPost = useRef(null)
const refEntreprise = useRef(null)
const refDebut = useRef(null)
const onSuccess = useCallback(cvexperience => {
onComment(cvexperience)
refPost.current.value = ''
refEntreprise.current.value = ''
ref.current.value = ''
refDebut.current.value = ''
}, [ref, refEntreprise, refPost, refDebut, onComment])
// Hooks
const method = cvexperience ? 'PUT' : 'POST'
const url = cvexperience ? cvexperience['#id'] : '/api/cv_experiences'
const {load, loading, errors, clearError} = useFetch(url, method, onSuccess)
// Méthodes
const onSubmit = useCallback(e => {
e.preventDefault()
load({
entreprise: ref.current.value,
post: refPost.current.value,
description: refEntreprise.current.value,
debut: refDebut.current.value,
//fin: ref.current.value,
profile: "/api/cv_experiences/" + post
})
}, [load, ref, refEntreprise, refPost, refDebut, post])
// Affichage du form
const [show,setShow]=useState(false)
// Effets
useEffect(() => {
if (cvexperience && cvexperience.description && ref.current && refPost.current && refEntreprise.current && refDebut.current) {
ref.current.value = cvexperience.description
refPost.current.value = cvexperience.post
refEntreprise.current.value = cvexperience.entreprise
refDebut.current.value = cvexperience.debut
}
}, [cvexperience, ref, refPost, refEntreprise, refDebut])
return(
<div className="well">
{
show?
<form onSubmit={onSubmit}>
<button onClick={()=>setShow(false)} ><Icon icon="times"/> Replier</button>
{cvexperience === null && <fieldset>
</fieldset>}
<Field
name="description"
help="Veuillez renseigner tous les champs."
ref={ref}
refPost={refPost}
refEntreprise={refEntreprise}
refDebut={refDebut}
required
minLength={3}
namePost="post"
requiredPost="required"
childrenPost="Post"
nameEntreprise="entreprise"
requiredEntreprise="required"
childrenEntreprise="Entreprise"
nameDebut="debut"
requiredDebut="required"
childrenDebut="Date de début"
onChange={clearError.bind(this, 'description')}
error={errors['description']}
/>
<div className="form-group">
<button className="theme-btn btn-style-one small text-white" disabled={loading}>
{cvexperience === null ? 'Envoyer' : 'Editer'}
</button>
{onCancel && <button className="theme-btn btn-style-eight small" onClick={onCancel}>
Annuler
</button>}
</div>
</form>
:null
}
<button onClick={()=>setShow(true)} ><Icon icon="plus"/> Ajouter une expérience</button>
</div>
)
})
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
Why my interval is speeding up?
When I press any of my buttons NextImage() or PrevImage() my interval starts speeding up and the image starts glitching. Any advice or help?
Here's my code =>
//Image is displayed
const [image, setImage] = React.useState(1);
let imageShowed;
if (image === 1) {
imageShowed = image1;
} else if (image === 2) {
imageShowed = image2;
} else if (image === 3) {
imageShowed = image3;
} else {
imageShowed = image4;
}
// Auto change slide interval
let interval = setInterval(
() => (image === 4 ? setImage(1) : setImage(image + 1)),
5000
);
setTimeout(() => {
clearInterval(interval);
}, 5000);
// Change image functionality
const ChangeImage = (index) => {
setImage(index);
};
/ /Next image
const NextImage = () => {
image === 4 ? setImage(1) : setImage(image + 1);
};
// Previous image
const PrevImage = () => {
image === 1 ? setImage(4) : setImage(image - 1);
};
When you need to have some logic which is depend on changing a variable, it's better to keep those logic inside useEffect
const interval = useRef(null);
const timeout = useRef(null);
useEffect(() => {
interval.current = setInterval(
() => (image === 4 ? setImage(1) : setImage((i) => i + 1)),
5000
);
timeout.current = setTimeout(() => {
clearInterval(interval.current);
}, 5000);
return () => {
clearInterval(interval.current);
clearTimeout(timeout.current);
}
}, [image]);
one point to remember is that if you use a variable instead of using useRef it can increase the possibility of clearing the wrong instance of interval or timeout during the rerenders. useRef can keep the instance and avoid any unwanted bugs
Your approach causes so many problems and you should learn more about react (watch youtube tutorials about react), I did make a working example slider hope to help you and people in the future:
let interval;
const images = [
"https://picsum.photos/300/200?random=1",
"https://picsum.photos/300/200?random=2",
"https://picsum.photos/300/200?random=3",
"https://picsum.photos/300/200?random=4",
"https://picsum.photos/300/200?random=5",
];
const App = () => {
const [slide, setSlide] = React.useState(0);
React.useEffect(() => {
interval = setInterval(() => {
NextSlide();
clearInterval(interval);
}, 5000);
return () => {
clearInterval(interval);
};
}, [slide]);
const ChangeSlideDots = (index) => {
setSlide(index);
};
const NextSlide = () =>
setSlide((prev) => (slide === images.length - 1 ? 0 : prev + 1));
const PrevSlide = () =>
setSlide((prev) => (slide === 0 ? images.length - 1 : prev - 1));
return (
<div style={styles.root}>
<img style={styles.imageDiv} src={images[slide]} />
<button style={styles.buttons} onClick={PrevSlide}>
◁
</button>
<div style={styles.dotDiv}>
{images.map((_, i) => (
<div
key={i}
style={i === slide ? styles.redDot : styles.blackDot}
onClick={() => ChangeSlideDots(i)}
>
.
</div>
))}
</div>
<button style={styles.buttons} onClick={NextSlide}>
▷
</button>
</div>
);
}
const styles = {
root: {
display: "flex",
position: "relative",
width: 300,
height: 200,
},
buttons: {
backgroundColor: "rgb(255 255 255 / 37%)",
border: "none",
zIndex: 2,
flex: 1,
},
imageDiv: {
position: "absolute",
zIndex: 1,
width: 300,
height: 200,
},
dotDiv: {
flex: 10,
zIndex: 2,
fontSize: "30px",
display: "flex",
justifyContent: "center",
},
redDot: {
cursor: "pointer",
color: "red",
},
blackDot: {
cursor: "pointer",
color: "black",
},
};
ReactDOM.render(<App />, document.getElementById("react"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>
Anytime that you rerender your component, you will run the whole function once. So you will set an interval every time you use setImage(). In order to prevent this, you have to use side effect functions. here you should use useEffect() because you have a functional component. in order to make useEffect() only run once, you have to pass an empty array for dependecy array; So your useEffect will act like componentDidMount() in class components. try the code below:
let interval = null
useEffect(() => {
interval = setInterval(
() => (image === 4 ? setImage(1) : setImage(image + 1)),
5000
)
setTimeout(() => {
clearInterval(interval);
}, 5000)
}, [])
Thanks, everybody for your great answers appreciated a lot your time and help!
So, my final solution looks like this:
const images = [image1, image2, image3, image4];
const quotes = [
'Jobs fill your pockets, adventures fill your soul',
'Travel is the only thing you buy that makes you richer',
'Work, Travel, Save, Repeat',
'Once a year, go someplace you’ve never been before',
];
const App = () => {
//Image is displayed
const [image, setImage] = React.useState(0);
// Auto change slide interval
useEffect(() => {
return () => {
clearInterval(
setInterval((interval) => {
image === 3 ? setImage(1) : setImage(image + 1);
clearInterval(interval.current);
}, 5000)
);
};
}, [image]);
// Change image functionality
const ChangeImage = (index) => {
setImage(index);
};
//Next image
const NextImage = () => {
image === 3 ? setImage(1) : setImage(image + 1);
};
// Previous image
const PrevImage = () => {
image === 1 ? setImage(3) : setImage(image - 1);
};
return (
<Section>
<div className='slideshow-container'>
<div>
<img className='slider_image' src={images[image]} alt='slider' />
<h1 className='slider_title'>{quotes[image]}</h1>
</div>
<button className='slider_prev' onClick={PrevImage}>
❮
</button>
<button className='slider_next' onClick={NextImage}>
❯
</button>
</div>
<div>
<div>
{images.map((image, i) => (
<img
key={i}
alt={`slider${i}`}
src={image}
className='bottom_image'
onClick={() => ChangeImage(i)}
></img>
))}
</div>
</div>
</Section>
);
};
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
I found this component online which creates a review component but it's not working.
import { useState } from 'react';
const Star = ({filled, starId}) => (
<span star-id={starId} style={{ color: '#ff9933' }} role="button">
{filled ? '\u2605' : '\u2606'}
</span>
);
export const Rating = props => (
const [rating, setRating] = useState(typeof props.rating == 'number' ? props.rating : 0);
const [selection, setSelection] = useState(0);
const hoverOver = event => {
let val = 0;
if (event && event.target && event.target.getAttribute('star-id'))
val = event.target.getAttribute('star-id');
setSelection(val);
};
return (
<div
onMouseOut={() => hoverOver(null)}
onClick={event => setRating(event.target.getAttribute('star-id') || rating)}
onMouseOver={hoverOver}
>
{Array.from({ length: 5 }, (v, i) => (
<Star
starId={i + 1}
key={`star_${i + 1} `}
filled={selection ? selection >= i + 1 : rating >= i + 1}
/>
))}
</div>
);
It throws an error for this line:
const [rating, setRating] = useState(typeof props.rating == 'number' ? props.rating : 0);
What is wrong with it? And how can be it fixed?
I think the hook is fine, but you need to use {} around the function body:
export const Rating = props => {
const [rating, setRating] = useState((typeof props.rating === 'number') ? props.rating : 0);
const [selection, setSelection] = useState(0);
const hoverOver = event => {
let val = 0;
if (event && event.target && event.target.getAttribute('star-id'))
val = event.target.getAttribute('star-id');
setSelection(val);
};
return (
<div
onMouseOut={() => hoverOver(null)}
onClick={event => setRating(event.target.getAttribute('star-id') || rating)}
onMouseOver={hoverOver}
>
{Array.from({ length: 5 }, (v, i) => (
<Star
starId={i + 1}
key={`star_${i + 1} `}
filled={selection ? selection >= i + 1 : rating >= i + 1}
/>
))}
</div>
);
};
codesandbox
A second thing you should consider: You probably should use === instead of == for the typeof check.