I have simple component
class ContentEditable extends React.Component {
constructor(props) {
super(props);
this.handleInput = this.handleInput.bind(this);
}
handleInput(event) {
let html = event.target.innerHTML;
if (this.props.onChange && html !== this.lastHtml) {
this.props.onChange({ target: { value: html, name: this.props.name } });
this.lastHtml = html;
}
}
render() {
return (
<span
contentEditable="true"
onInput={this.handleInput}
className={"auto " + this.props.className}
dangerouslySetInnerHTML={{ __html: this.props.value }}
/>
);
}
}
export default ContentEditable;
<ContentEditable
value={this.state.name}
onChange={e => {
this.setState({ name: e.target.value });
}}
/>;
The component works but the cursor position never changes, it is always on first position instead after the rendered text.
I tested examples form this forum but it doesn't work for me.
I use React 15.6.1 and test it on chrome (Os X).
Any hint how I can solve this problem?
The solution with useRef will be something look like below.
Here the useRef will keep the default value / initial value apart from the component rendering cycles, so it will retain the original value without being affected by other kinds of operations we do in the react component.
This component does two things
This will emit the user input to the parent component with an onChange method
Takes a default value from parent component as prop named value and renders the value in the custom input box (that was created using contentEditable)
I have added a code sandbox, link here, use this to see how this works!
The code sandbox example contains two components
one is ContentEditableWithRef which solves the problem with useRef , which is an uncontrolled component and
the other component is ContentEditable which uses useState to solve the same problem.
I also had same problem. Just fixed it with ref. Just assign textContent of event.target to ref.
const textareaEl = useRef<HTMLDivElement>(null);
const handleChange = (e: React.ChangeEvent<HTMLDivElement>) => {
textareaEl.current.textContent = e.target.textContent;
onChange(e); // If you have change event for form/state
};
/** If you're passing value from state,
you can mutate it each change for not losing cursor position.
*/
useEffect(() => {
if (value) {
textareaEl.current.textContent = value;
}
}, [value]);
return (
<div
id="textarea-element"
ref={textareaEl}
contentEditable={true}
suppressContentEditableWarning={true}
onChange={handleChange}
/>
)
Related
I'm relatively new to react and am having a little trouble understanding passing props/states from a child to parent component. I've managed to implement something that works but it is extremely laggy.
Overview: I am trying to create a form with multiple Material UI TextFields, with a submission button at the end to submit the form.
My approach: I am using state to update individual textfields on their inputs, and then dynamically updating another state in the parent FormSection file. Once the user clicks on the 'Submit' Button (not implemented yet), it will then take all the states from the parent class.
Existing Classes:
CompanyField.js
JobTitle.js
FormSection.js (Main File)
Implementation Appearance
CompanyField Class (will be same for Job Title, etc):
const Root = styled('div')(({ theme }) => ({
}));
class CompanyField extends React.Component {
state = { company: '' }
handleOnChange = (event) => {
event.preventDefault();
this.setState({ company: event.target.value.toLowerCase() });
this.props.onCompanyChange(this.state.company);
}
render() {
return (
<Root>
<Box
noValidate
autoComplete="off"
>
<TextField
id = "outlined-basic"
label = "Company"
variant = "outlined"
fullWidth
value = {this.state.company}
onChange = { this.handleOnChange }
/>
</Box>
</Root>
);
}
}
export default CompanyField;
Index Class
class FormSection extends React.Component {
state = { company: '', jobTitle: '' }
onCompanyUpdate = (value) => {
this.setState({company: value})
// console.log('Company:', this.state.company);
}
render() {
return (
<FormContainer>
<FormContentHeaderWrapper>
<FormContentHeader>
Company & Job Information
</FormContentHeader>
</FormContentHeaderWrapper>
<FormWrapperFull>
<CompanyField onCompanyChange={ this.onCompanyUpdate } />
<JobTitleField onJobTitleChange={ this.onJobTitleUpdate } />
</FormWrapperFull>
</FormContainer>
)
}
Could someone explain whether I am doing this the correct way? Else, would there be a better way to resolve this state passing method?
Thanks in advance!
When you update the parent component the whole tree re-renders on every keystroke. In your case your component very small it should not be a big impact. There are three approaches in my mind.
first of all, you have to use react developer tools for investigating further re-renders and finding the real problem.
first: You might use form validation libraries. For example; "react hook forms"
second: You might use React's "React.memo" function to memorize components.
third: You might use refs for input value management. You add values to ref and when you need them you iterate that ref object. You don't update the state. If there is no state update there will be no rerender.
for example:
In parent component:
const values = useRef({description: "", jobtitle: ""});
const onChange(name, value) {
values.current[name] = value;
}
In child component: (it must be an "uncontrolled component")
const handleCompanyChange = (evt) => {
const value = evt.target.value;
const name = "company";
props.onChange(name, value);
}
I want to send events down to my React child.
I feel like this is kind of an easy thing to do, so maybe i just have a mental block, and there is something obvious that is staring me in the face.
Anyway, I have a little Test app which illustrates the problem:
export class Test extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
let {buttonClicked, textFieldChanged} = this.state
return (
<div>
<button onClick={()=>this.handleClick()}>
Click
</button>
<input type={"text"} onChange={()=>this.handleTextChange()}/>
<Inner buttonClicked={buttonClicked} textFieldChanged={textFieldChanged}/>
</div>
);
}
handleClick(e) {
this.setState({ buttonClicked: true })
}
handleTextChange(e) {
this.setState({textFieldChanged:true})
}
}
class Inner extends React.Component {
render() {
let {buttonClicked, textFieldChanged} = this.props;
return (
<React.Fragment>
<div>Clicked : {buttonClicked ? "CLICKED!" : " "}</div>
<div>Text input : {textFieldChanged ? "TYPED!" : " "}</div>
</React.Fragment>
);
}
}
A button and a textfield live in the parent. Both these widgets can fire off events and change the child component.
This is simply achieved by passing a state value as a property down to the child. Very easy stuff.
However I would like an either/or situation. When I click the button this removes the text event, and vice versa. Ie. I do not want to see a situation like this :
Now there is a very obvious way to fix this by changing the state value to "false" of the other value.
handleClick(e) {
this.setState({ buttonClicked: true, textFieldChanged: false })
}
handleTextChange(e) {
this.setState({textFieldChanged:true, buttonClicked: false})
}
Is there any OTHER way of doing this?
The problem is that I have LOTS and LOTS of even handlers in my component and I don't want to negate the other state properties of the other values.
if i understood you correctly just one function will help - pass the attribute name into it
handleClick(propName) {
this.setState({
...this.state,
[propName]: !this.state[propName]
})
}
Create property lastEventType in parent component state , whenever you click or type - update it. And pass only this property to Inner component
I have two components: ParentComponent and ChildComponent.
The ChildComponent has an input[type="text"] element that when it changes its text that event is propagated to the ParentComponent through the event change and the onChange listener.
The code below is a simplification of a bigger problem, that's why you will see some requirements highlighted there.
My problem is that I need to trigger the change event inside the function: handleClick. I did some experiments with no luck.
Here you have the code sandbox you can experiment with (please provide a fork with your approach):
https://codesandbox.io/s/wqw49j5krw
Here you have the code:
ParentComponent.js
import React from "react";
import ChildComponent from "./ChildComponent";
export default class ParentComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
name: "Peter"
};
}
handleChange = event => {
let target = event.target;
let value = target.value;
this.setState({
name: value
});
};
render() {
return (
<div>
<ChildComponent value={this.state.name} onChange={this.handleChange} /><br />
<span>Hello</span>
<span>{this.state.name}!</span>
</div>
);
}
}
ChildComponent.js
import React from "react";
import ReactDOM from "react-dom";
export default class ChildComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
value: this.props.value
};
}
handleChange = event => {
const name = event.target.value;
this.setState({ value: name });
if (this.props.onChange !== undefined) {
this.props.onChange(event);
}
};
handleClick = name => {
const inputName = this.refs.name;
console.log('Name before being changed: ' + inputName.value); // this works
// PROBABLY HERE IS WHERE THE CODE NEEDS TO BE MODIFIED
this.setState({ value: name });
var event = new Event('input', { bubbles: true });
inputName.dispatchEvent(event); // this doesn't propagate the event to the parent
};
render() {
return (
<div>
{"Your name: "}
<input type="text"
value={this.state.value}
onChange={this.handleChange}
ref="name"
/>
{/* requirement: the following 2 lines cannot be modified */}
<button onClick={e => { this.handleClick("Steve"); }}>Steve</button>
<button onClick={e => { this.handleClick("Emily"); }}>Emily</button>
</div>
);
}
}
Any idea on how to get this working?
Thanks!
You are missing the track of the input change,
Because React tracks when you set the value property on an input to
keep track of the node's value. When you dispatch a change event, it
checks it's last value against the current value and if they're the
same it does not call any event handlers (as no change has taken place
as far as react is concerned). So we have to set the value in a way
that React's value setter function will not be called, which is where
the setNativeValue comes into play.
Here you are setting the state instead of changing the input's value directly so, it will not get the updated value when you are dispatching the event. and if you write value directly like input.value it can not track the changes of the input.
so, you should set the value and dispatch the event, this way you can have the updated value when event is dispatched.
Here is the link of the reference and another, there are other ways too, to fire the change event.
Here is the function you need to set the property so that react can track the changes,
handleClick = name => {
const inputName = this.refs.name;
console.log("Name before being changed: " + inputName.value); // this works
var event = new Event("input", { bubbles: true });
this.setNativeValue(inputName, name);
inputName.dispatchEvent(event);
};
setNativeValue = (element, value) => {
const valueSetter = Object.getOwnPropertyDescriptor(element, "value").set;
const prototype = Object.getPrototypeOf(element);
const prototypeValueSetter = Object.getOwnPropertyDescriptor(
prototype,
"value"
).set;
if (valueSetter && valueSetter !== prototypeValueSetter) {
prototypeValueSetter.call(element, value);
} else {
valueSetter.call(element, value);
}
};
Demo
You can include an onClick function to the parent component to change the state and pass it onto the child component. In the child component you can call the onClick method passed down via props and pass the name when button is clicked.
You can find the example code in the link provided below.
https://codesandbox.io/s/k0n1j2rrx7
I corrected your code like that:
in handleChange function of parentComponent I changed the parameter to value from event.
In childComponent I added the function call below to handleClick function:
this.props.onChange(name);
Why I did these changes because, you call your parentComponent's handleChange function from your childComponent' s onChange function.
When you click the button, it calls handleClick function. In handleClick function it calls property onChange function. The props.onChange function calls handleChange function of parent component.
handleClick -> this.props.onChange(name) -> this.handleChange(name)
I hope it helps.
If I understood correctly, you are trying to change the parent text like Hello Peter when you click the button from child component.
Check my codesandbox : codesandbox
Changes:
In child component, pass a props like this.props.onClick(name);
In parent, get it like
handleClick = name => {
this.setState({
name
});
};
Hope this is will help you.
I'm using a high order component that is not rendering the child on a render change. The code below has been chopped down for simplicity sake.
The HOC that looks like this:
const withMyComponent = (WrapperComponent) => {
class HOC extends Component {
state = {
value: ''
};
changedHandler = (event) => {
this.setState({ value: event.target.value });
};
render() {
return (<WrapperComponent {...this.props}
changed={this.changedHandler}
value={this.state.value} />);
}
};
return HOC;
};
export default withMyComponent;
Then I have a child component that uses this HOC:
class myChildComponent extends Component {
render() {
return (
<input type="text"
onChange={(event) => this.props.changed(event)}
value={this.props.value || ''} />
);
};
};
export default withMyComponent(myChildComponent);
The problem I am experiencing is that the input is not updating with the new value that is passed back from the HOC. In fact, the child render is not even firing after the initial mount and doesn't seem to fire on any changed event.
I have placed debugging and console.logs in the HOC render and the changed event and the render are firing with the proper data.
So, if I type in the textbox, it fires the change event in the HOC and updates the state in the HOC and fires the render event in the HOC. But it is not firing the wrapped components render event, and thus the textbox never updates with any values.
Anyone able to solve this issue or lead me in the right direction?
Thanks again.
You need to pass an object to this.setState
const withMyComponent = WrapperComponent => {
class HOC extends Component {
state = {
value: ""
};
changedHandler = event => {
// Instead of
// this.setState( value: event.target.value );
// Do This.
this.setState({ value: event.target.value });
};
render() { ... }
}
return HOC;
};
Working Demo
I'm creating a simple application with React, my knowledge of React is about a week old, so please excuse any oversights that I may have had, or any overall idiocy when it comes to the code I have written to try and get this to work.
I am using the React version of Nouislider:
https://github.com/algolia/react-nouislider
And referencing the original documentation for Nouislider here:
https://refreshless.com/nouislider/
I am able to create a slider, and fire a function using the onChange prop. At first I logged the output to the console, and verified that the value was correct. However the moment I try and update the state using this.setState, it snaps the slider back to 0, and the state gets an undefined value instead of the value of the slider.
Here is my current code:
The constructor:
constructor(props) {
super(props);
this.state = {
matchWinnerScore: "",
}
this.handleWinnerClick = this.handleWinnerClick.bind(this);
}
The slider, inside the render function:
<Nouislider
start={[0]}
connect={[true, false]}
step={1}
range={{ min: 0, max: 30 }}
onChange={this.handleWinnerScoreChange}
/>
The onChange function:
handleWinnerScoreChange(event) {
console.log(event[0]);
this.setState({
matchWinnerScore: event[0]
});
console.log(this.matchWinnerScore);
}
In the console, the output is:
14.00
undefined
Any help, thoughts, or advice is greatly welcomed. I've hit a brick wall and need a fresh set of eyes to guide me in the right direction.
Look forward to your replies!
I had the same exact issue with this library:
https://github.com/leongersen/noUiSlider
Unfortunately, I had to create a child component for my text (or whatever you are binding the slider value to) with a ref. It works but obviously not ideal to use ref on such a basic component.
Parent Component (only relevant shown)
onChangeSlide = (values, handle, unencoded, tap, positions) => {
let { challengeObject } = this.props;
this.refs['ProgressText' + challengeObject.id].updateProgress(parseInt(values[0]))
}
componentDidMount = () => {
let uiSlider = this.refs['NoUiSlider' + challengeObject.id].slider
uiSlider.on('update', this.onChangeSlide);
uiSlider.on('end', function () {
//save it to db
});
}
render() {
const { classes, challengeObject } = this.props;
return (
<div>
<Nouislider
ref={'NoUiSlider' + challengeObject.id}
start={[challengeObject.progress['startValue']]}
connect={[true, false]}
step={1}
range={{
min: 0,
max: challengeObject.progress['targetValue']
}}
/>
<ProgressText
ref={'ProgressText' + challengeObject.id}
startValue={challengeObject.progress['targetValue']}
endValue={challengeObject.progress['targetValue']}
/>
</div>
);
}
Child Component
import React from "react";
class ProgressText extends React.Component {
constructor(props) {
super(props)
this.state = {
startValue: this.props.startValue
}
}
updateProgress = (val) => {
this.setState({ startValue: val })
}
render() {
let { endValue } = this.props;
let { startValue } = this.state
return (
<span>
{startValue + '/' + endValue}
</span>
);
}
}
export default ProgressText;
I had the same issue being that NouiSlider could not directly update the state in any form of callback thus my solution was to have Nouislider indirectly update the state. My solution was I had the "onUpdate" event update the inner text of an element linked to a ref that displayed the value to the user. Then the "onChange" event (which is called after drag is over) updates the state of a random state that was linked to a useEffect that had that random state as a dependency. Then inside that useEffect I would update the state based on the current related ref's values. The code looked something like this:
const [form, setForm] = useState({...state})
const [random, setRandom] = useState(null);
const noUiSliderValue = useRef(null);
useEffect(()=>{ setForm({...form, value:noUiSlider.current.innerText})}, [random]}
Then for the NouiSlider element:
<Nouislider
start={[0]}
connect={[true, false]}
step={1}
range={{ min: 0, max: 30 }}
onChange={(range)=>setRandom(range)}
onUpdate={(range) => noUiSliderValue.current.innerText=range[0]}
/>
I have no idea why NouiSlider has such strange behaviors with state-related callbacks but this was just a janky fix to fix a janky issue.