I have simple react input component that accepts only numbers, each keyboard input i check inputs and return validated entries only, after 3rd character I add - character to the input. everything works fine but when i edit / remove one char from first third character my caret jumps to last position its annoying, how can i keep caret at the same position.
Link to working example
My input component
class App extends Component {
constructor(props){
super(props)
this.state = {
value: ''
}
}
render() {
const {value} = this.state
return (
<div className="App">
<input type="text" value={value} onChange={this._onChange}/>
</div>
)
}
_onChange = (e) => {
this.setState({value: this._validate(e.target.value)})
}
_validate(string){
return new Input(string)
.filterNumbersOnly()
.addSeparator()
.getString()
}
}
export default App;
Here is my string validation class
export default class Input{
constructor(string){
this.string = string
}
filterNumbersOnly(){
const regex = /(\d)/g
,strArray = this.string.match(regex)
this.string = strArray ? strArray.reduce((a,b) => { return `${a}${b}` }, '') : ''
return this
}
addSeparator(){
const charArray = this.string.split('')
this.string = charArray.reduce((a,b,index) => {
if(index > 2 && index < 4) {
return `${a}-${b}`
}
return `${a}${b}`
}, '')
return this
}
getString(){
return this.string
}
}
There is way if you could preserve the selection range and element target and then apply it after the set state i.e on set state callback like this:
_onChange = (e) => {
var r =[e.target.selectionStart,e.target.selectionEnd];
var el = e.target;
this.setState({value: this._validate(e.target.value)},()=>{
//just to make sure you extend cursor in case of - addition
if (r[1] === this.state.value.indexOf("-")+1)
r[1]++;
el.setSelectionRange(r[0], r[1]);
})
}
Sample url https://codesandbox.io/s/628q1rvnl3
It resolve all my problem, init selection in callback setState
let el = ev.target
this.setState({
sum: value
}, () => {
el.setSelectionRange(1, 1)
})
Related
people, I have a question. I have two javascript functions that do the same thing. Note that it only changes the "let selected" variable. I don't think it's the best way to use functions in js, how can I reuse them?
First function:
onChange(id) {
let selected = this.state.selectedDevice
let find = selected.indexOf(id)
if(find > -1) {
selected.splice(find, 1)
} else {
selected.push(id)
}
this.setState({ selected })
}
Second function:
onChangeSec(id) {
let selected = this.state.selectedSection
let find = selected.indexOf(id)
if(find > -1) {
selected.splice(find, 1)
} else {
selected.push(id)
}
this.setState({ selected })
}
thanks !!!
You could just pass key as another function parameter
onChange(id, key) {
let selected = this.state[key]
let find = selected.indexOf(id)
if(find > -1) {
selected.splice(find, 1)
} else {
selected.push(id)
}
this.setState({ selected })
}
and in your case key would be 'selectedDevice' or 'selectedSection' passed as a string.
It seems like this.state.selectedDevice and this.state.selectedSection are both arrays, and since you're using index-based retrieval from the array, there's no way from within the function to determine which should be used. Instead you should pass that information into the function. For example:
onChange(id, data) {
const position = data.indexOf(id)
this.setState({
selected: position > -1
? [...data.slice(0, position), ...data.slice(position + 1)]
: [...data, 1]
})
}
Note: this also works around your mutation of an object in state, which will cause bugs.
You might also want to consider using a Set instead:
constructor() {
this.state = {
selected: new Set()
}
}
onChange(value) {
const newSelected = new Set(this.state.selected)
if (newSelected.has(value)) {
newSelected.delete(value)
} else {
newSelected.add(value)
}
this.setState({ selected: newSelected })
}
This is safer than an array where you can have multiple entries of the same value.
I currently have a React tsx page with some input boxes; for example:
<textarea value={this.state.myData!.valueOne}
onChange={(e) => this.handleValueOneChange(e)}/>
<textarea value={this.state.myData!.valueTwo}
onChange={(e) => this.handleValueTwoChange(e)}/>
handleValueOneChange and handleValueTwoChange look almost identical:
handleValueOneChange(event: React.FormEvent<HTMLTextAreaElement>) {
let newState = { ...this.state };
newState.myData!.valueOne = event.currentTarget.value;
this.setState(newState);
}
handleValueTwoChange(event: React.FormEvent<HTMLTextAreaElement>) {
let newState = { ...this.state };
newState.myData!.valueTwo = event.currentTarget.value;
this.setState(newState);
}
Is it possible to have a single function for both events; for example:
handleDataChange(event: React.FormEvent<HTMLTextAreaElement>, valueByRef) {
let newState = { ...this.state };
valueByRef = event.currentTarget.value;
this.setState(newState);
}
I'm a little unclear on what this might look like in TypeScript: is what I'm trying to do possible and, if so, how?
EDIT:
To further complicate matters myData contains a mix of types (which appears to be an issue for TS)
Try something like this
HTML becomes
<textarea value={this.state.myData!.valueOne}
onChange={(e) => this.handleValueChange(e, 'valueOne')}/>
<textarea value={this.state.myData!.valueTwo}
onChange={(e) => this.handleValueChange(e, 'valueTwo')}/>
and logic (I suppose your state has type MyState)
handleDataChange<K extends keyof MyDataType>(event: React.FormEvent<HTMLTextAreaElement>, ref: K) {
let newState = { ...this.state };
newState.myData![ref] = event.currentTarget.value;
this.setState(newState);
}
EDIT:
I fixed the answer according to the comments
EDIT:
If myData contains strings, numbers and booleans you can use type guards and casts
handleDataChange<K extends keyof MyDataType>(event: React.FormEvent<HTMLTextAreaElement>, ref: K) {
let newState = { ...this.state };
let old = newState.myData![ref];
if (typeof old === "number") newState.myData![ref] = parseInt(event.currentTarget.value); /* or parseFloat, this depends on your app */
else if (typeof old === "boolean") newState.myData![ref] = (event.currentTarget.value === "1"); /* use any string -> boolean conversion function */
else newState.myData![ref] = event.currentTarget.value; /* string case */
this.setState(newState);
}
Of course, pass a string back, for example:
<Input onChange={(event)=>this.handleChange(event, 'type1')} />
If you give your text area's a name prop, you can use this name to add the value to state.
handleChange(event: React.FormEvent<HTMLTextAreaElement) {
this.setState({
...this.state.myData,
[event.currentTarget.name]: event.currentTarget.value,
})
}
<textarea value={this.state.myData.one} name="one"
onChange={this.handleChange} />
<textarea value={this.state.myData.two} name="two"
onChange={this.handleChange} />
onChange prop looks cleaner in this case too. No use of an inline arrow function.
Working Code sandbox https://codesandbox.io/s/04rm89oq8w
I'm a beginner to React; I understand that setState is asynchronous, but I don't understand why in the example pen below the box below the refresh is not done immediately, and only updates after the second character is input.
Codepen: (updated to link to correct pen)
https://codepen.io/anon/pen/odZrjm?editors=0010
Portion:
// Returns only names matching user's input
filterList = (term) => {
let finalList = []
for (let x = 0; x < this.state.names.length; x++) {
if (this.state.names[x].toLowerCase().includes(term)) {
finalList.push(this.state.names[x])
}
}
finalList.sort()
return finalList
}
// Returns the name list as string
listNames = () => {
let filteredNames = [], filter = this.state.filter.toLowerCase(), names = ""
if (filter === "") return "Enter filter chars"
// Generate filtered array with only names matching filter
filteredNames = this.filterList(filter)
// Build and return comma-separated list of filtered names
names = filteredNames.join(', ')
if (names.length === 0) return 'None found'
return names
}
handleChange = (event) => {
let result, alert = 'alert-primary'
result = this.listNames()
this.setState({
filter: event.target.value,
namesList: result
})
}
If I replace the line "result = this.listNames()" in handleChange with just "result = 'test'" then it updates immediately. Can you please explain why it does not when I call the functions?
It occurs because you are calling the listNames method before this.state.filter is set, thus reaching:
if (filter === "") return "Enter filter chars"
Here's a working pen: https://codepen.io/anon/pen/gzmVaR?editors=0010
This is because listNames() is being called before setState
when you call handleChange method, it can't find 'this' scope.
so 'this' is undefined and can't call this.listNames().
solution:
use onChange={this.handleChange.bind(this)}
instead of onChange={this.handleChange}
I am very new to react and am trying to create a checkbox and also display the selected checkbox names list in a textbox. I am using selected[] to store the values for the selected box and checked[] to store whether that box is checked or not.If it is checked I update the value of selected accordingly.
The code as of now works fine but I want to avoid use of forceupdate() and use setState(). When I use I am unable to update the selected[] value using it. Can somebody tell me of how to update the particular array index value using setstate so thatit gets render itself and I do not have to use forceupdate() ?
thank you.
var history = React.createClass({
getInitialState : function(){
return {
checked : [],
selected: []
};
},
componentWillMount : function(){
},
handleChangechk: function (e){
const target = e.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
if(value===true)
{
this.state.checked[name]= true;
this.state.selected[name] = name;
this.forceUpdate();
}
else
{
this.state.checked[name]= false;
this.state.selected[name] = '';
this.forceUpdate();
}
},
render : function() {
var historyList = [];
var selectedList = [];
for (var i = 0; i < 10; i++) {
historyList.push(<span key={i}><input type="checkbox" name = {i} checked={!!this.state.checked[i]} onChange ={(e)=> this.handleChangechk(e)}/><span ></span><label >checkbox {i}</label></span>);
if(this.state.selected[i])
{
selectedList.push(this.state.selected[i]);
}
};
return( /* display selected checkbox (selectedList ); */}});
Never mutate the state variable directly by this.state.a.push() or this.state.a = '', always use setState to update the value.
From Doc:
Never mutate this.state directly, as calling setState() afterwards may
replace the mutation you made. Treat this.state as if it were
immutable.
forceUpdate() is not required if you update the state values properly, it will automatically do the re-rendering, Check this:
class App extends React.Component{
constructor(){
super();
this.state = {value: [], selected: []}
}
change(event) {
let target, value, selected;
target = event.target;
value = this.state.value.slice();
selected = this.state.selected.slice();
value[target.name] = target.checked;
if(target.checked){
selected.push(target.name);
}else{
let index = selected.indexOf(target.name);
selected.splice(index, 1);
}
this.setState({
value, selected
});
}
render(){
return(
<div>
{[1,2,3,4,5].map((el,i)=>{
return <div key={i}>
<input checked={this.state.value[i]} type='checkbox' onChange={this.change.bind(this)} name={i}/>
Item- {el}
</div>
})}
Selected Values: {JSON.stringify(this.state.selected)}
</div>
)
}
}
ReactDOM.render(<App/>, document.getElementById('app'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id='app'/>
You could get a copy of the checked and selected arrays, change the relevant index and then update the state:
handleChangechk: function (e) {
const target = e.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
let checked = this.state.checked.slice(),
selected = this.state.selected.slice();
if(value === true) {
checked[name]= true;
selected[name] = name;
} else {
checked[name]= false;
selected[name] = '';
}
this.setState({ items, selected });
}
You should almost never assign values to your state directly and/or use forceUpdate:
this.state.checked[name]= true;
this.state.selected[name] = name;
this.forceUpdate();
Avoiding the use of forceUpdate is as simple as using setState to assign the values. setState will automatically trigger a re-render.
var newChecked = this.state.checked.slice();
newChecked[name] = true;
var newSelected = this.state.selected.slice();
newSelected [name] = name;
this.setState({
checked: newChecked,
selected: newSelected,
});
Have filter, it's filtering ok, but when clear input. I see filtered result:
filterList(event) {
var updatedList = this.state.items;
if (event.target.value !== '') {
updatedList = updatedList.filter(function(item){
return item.name.toLowerCase().indexOf(event.target.value.toLowerCase()) !== -1;
});
}
this.setState({items: updatedList}); // now this.state.items has a value
}
My condition not working.
https://plnkr.co/edit/dPZM9BZVa4uzEMwdNpZ8?p=catalogue full component in script.js
There's a better approach for this. You usually don't want to use props to set your state directly. What you want is to only use the state for filtered item sets, since your props will always contain the full set of items. So first, remove your componentWillReceiveProps function. Then change the following things:
// in your constructor
this.state = {
filteredItems: false
}
filterList(e) {
var value = e.target.value;
this.setState({
filteredItems: !value
? false
: this.props.items.filter(function(item) {
return item.name.toLowerCase().indexOf(value.toLowerCase()) > -1
})
})
}
// inside render
var elems = this.state.filteredItems || this.props.items