React: Number input with no negative, decimal, or zero value values - javascript

Consider an input of type number, I would like this number input to only allow a user to enter one positive, non-zero, integer (no decimals) number. A simple implementation using min and step looks like this:
class PositiveIntegerInput extends React.Component {
render () {
return <input type='number' min='1' step='1'></input>
}
}
ReactDOM.render(
<PositiveIntegerInput />,
document.getElementById('container')
)
<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>
<p>
Try to input a decimal or negative number or zero:
</p>
<div id="container"></div>
The above code works fine if a user sticks to ONLY clicking the up/down arrows in the number input, but as soon a the user starts using the keyboard they will have no problem entering numbers like -42, 3.14 and 0
Ok, lets try adding some onKeyDown handling to disallow this loophole:
class PositiveIntegerInput extends React.Component {
constructor (props) {
super(props)
this.handleKeypress = this.handleKeypress.bind(this)
}
handleKeypress (e) {
const characterCode = e.key
if (characterCode === 'Backspace') return
const characterNumber = Number(characterCode)
if (characterNumber >= 0 && characterNumber <= 9) {
if (e.currentTarget.value && e.currentTarget.value.length) {
return
} else if (characterNumber === 0) {
e.preventDefault()
}
} else {
e.preventDefault()
}
}
render () {
return (
<input type='number' onKeyDown={this.handleKeypress} min='1' step='1'></input>
)
}
}
ReactDOM.render(
<PositiveIntegerInput />,
document.getElementById('container')
)
<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>
<p>
Try to input a decimal or negative number or zero:
</p>
<div id="container"></div>
Now everything almost appears to work as desired. However if a user highlights all the digits in the text input and then types over this selection with a 0 the input will allow 0 to be entered as a value.
To fix this issue I added an onBlur function that checks if the input value is 0 and if so changes it to a 1:
class PositiveIntegerInput extends React.Component {
constructor (props) {
super(props)
this.handleKeypress = this.handleKeypress.bind(this)
this.handleBlur = this.handleBlur.bind(this)
}
handleBlur (e) {
if (e.currentTarget.value === '0') e.currentTarget.value = '1'
}
handleKeypress (e) {
const characterCode = e.key
if (characterCode === 'Backspace') return
const characterNumber = Number(characterCode)
if (characterNumber >= 0 && characterNumber <= 9) {
if (e.currentTarget.value && e.currentTarget.value.length) {
return
} else if (characterNumber === 0) {
e.preventDefault()
}
} else {
e.preventDefault()
}
}
render () {
return (
<input
type='number'
onKeyDown={this.handleKeypress}
onBlur={this.handleBlur}
min='1'
step='1'
></input>
)
}
}
ReactDOM.render(
<PositiveIntegerInput />,
document.getElementById('container')
);
<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>
<p>
Try to input a decimal or negative number or zero:
</p>
<div id="container"></div>
Is there a better way to implement a number input with this type of criteria? It seems pretty crazy to write all this overhead for an input to allow only positive, non-zero integers... there must be a better way.

If you did it as a controlled input with the value in component state, you could prevent updating state onChange if it didn't meet your criteria. e.g.
class PositiveInput extends React.Component {
state = {
value: ''
}
onChange = e => {
//replace non-digits with blank
const value = e.target.value.replace(/[^\d]/,'');
if(parseInt(value) !== 0) {
this.setState({ value });
}
}
render() {
return (
<input
type="text"
value={this.state.value}
onChange={this.onChange}
/>
);
}
}

Here's a number spinner implantation in React Bootstrap. It only accepts positive integers and you can set min, max and default values.
class NumberSpinner extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
oldVal: 0,
value: 0,
maxVal: 0,
minVal: 0
};
this.handleIncrease = this.handleIncrease.bind(this);
this.handleDecrease = this.handleDecrease.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleBlur = this.handleBlur.bind(this);
}
componentDidMount() {
this.setState({
value: this.props.value,
minVal: this.props.min,
maxVal: this.props.max
});
}
handleBlur() {
const blurVal = parseInt(this.state.value, 10);
if (isNaN(blurVal) || blurVal > this.state.maxVal || blurVal < this.state.minVal) {
this.setState({
value: this.state.oldVal
});
this.props.changeVal(this.state.oldVal, this.props.field);
}
}
handleChange(e) {
const re = /^[0-9\b]+$/;
if (e.target.value === '' || re.test(e.target.value)) {
const blurVal = parseInt(this.state.value, 10);
if (blurVal <= this.state.maxVal && blurVal >= this.state.minVal) {
this.setState({
value: e.target.value,
oldVal: this.state.value
});
this.props.changeVal(e.target.value, this.props.field);
} else {
this.setState({
value: this.state.oldVal
});
}
}
}
handleIncrease() {
const newVal = parseInt(this.state.value, 10) + 1;
if (newVal <= this.state.maxVal) {
this.setState({
value: newVal,
oldVal: this.state.value
});
this.props.changeVal(newVal, this.props.field);
};
}
handleDecrease() {
const newVal = parseInt(this.state.value, 10) - 1;
if (newVal >= this.state.minVal) {
this.setState({
value: newVal,
oldVal: this.state.value
});
this.props.changeVal(newVal, this.props.field);
};
}
render() {
return ( <
ReactBootstrap.ButtonGroup size = "sm"
aria-label = "number spinner"
className = "number-spinner" >
<
ReactBootstrap.Button variant = "secondary"
onClick = {
this.handleDecrease
} > - < /ReactBootstrap.Button> <
input value = {
this.state.value
}
onChange = {
this.handleChange
}
onBlur = {
this.handleBlur
}
/> <
ReactBootstrap.Button variant = "secondary"
onClick = {
this.handleIncrease
} > + < /ReactBootstrap.Button> < /
ReactBootstrap.ButtonGroup >
);
}
}
class App extends React.Component {
constructor(props, context) {
super(props, context);
this.state = {
value1: 1,
value2: 12
};
this.handleChange = this.handleChange.bind(this);
}
handleChange(value, field) {
this.setState({ [field]: value });
}
render() {
return (
<div>
<div>Accept numbers from 1 to 10 only</div>
< NumberSpinner changeVal = {
() => this.handleChange
}
value = {
this.state.value1
}
min = {
1
}
max = {
10
}
field = 'value1'
/ >
<br /><br />
<div>Accept numbers from 10 to 20 only</div>
< NumberSpinner changeVal = {
() => this.handleChange
}
value = {
this.state.value2
}
min = {
10
}
max = {
20
}
field = 'value2'
/ >
<br /><br />
<div>If the number is out of range, the blur event will replace it with the last valid number</div>
</div>);
}
}
ReactDOM.render( < App / > ,
document.getElementById('root')
);
.number-spinner {
margin: 2px;
}
.number-spinner input {
width: 30px;
text-align: center;
}
<script src="https://unpkg.com/react#16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js" crossorigin></script>
<script src="https://unpkg.com/react-bootstrap#next/dist/react-bootstrap.min.js" crossorigin></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/latest/css/bootstrap.min.css" crossorigin="anonymous">
<div id="root" />

That's how number input works. To simplify the code you could try to use validity state (if your target browsers support it)
onChange(e) {
if (!e.target.validity.badInput) {
this.setState(Number(e.target.value))
}
}

I had a similar problem when I need to allow only positive number, fount solution on another question on StackOverflow(https://stackoverflow.com/a/34783480/5646315).
Example code that I implemented for react-final-form.
P.S: it is not the most elegant solution.
onKeyDown: (e: React.KeyboardEvent) => {
if (!((e.keyCode > 95 && e.keyCode < 106) || (e.keyCode > 47 && e.keyCode < 58) || e.keyCode === 8)) {
e.preventDefault()
}
},

class BasketItem extends React.Component {
constructor(props) {
super(props);
this.state = {
countBasketItem: props.qnt,
};
}
componentDidMount() {
const $ = window.$;
// using jquery-styler-form(bad practice)
$('input[type="number"]').styler();
// minus 1
$(`#basket_${this.props.id} .jq-number__spin.minus`).click(() => {
if (this.state.countBasketItem > 1) {
this.setState({ countBasketItem: +this.state.countBasketItem - 1 });
this.setCountProduct();
}
});
// plus 1
$(`#basket_${this.props.id} .jq-number__spin.plus`).click(() => {
this.setState({ countBasketItem: +this.state.countBasketItem + 1 });
this.setCountProduct();
});
}
onChangeCount = (e) => {
let countBasketItem = +e.target.value
countBasketItem = (countBasketItem === 0) ? '' : (countBasketItem > 999) ? 999 : countBasketItem;
this.setState({ countBasketItem })
};
onBlurCount() {
// number empty
if (+this.state.countBasketItem == 0 || isNaN(+this.state.countBasketItem)) {
this.setState({ countBasketItem: 1 });
}
this.setCountProduct();
}
setCountProduct = (colrKey = this.props.colr.key, idProduct = this.props.product.id, qnt) => {
qnt = +this.state.countBasketItem || 1; // if don't work setState
this.props.basket.editCountProduct(idProduct, colrKey, qnt); // request on server
};
render() {
return;
<input
type="number"
className="number"
min="1"
value={this.state.countBasketItem}
onChange={this.onChangeCount.bind(this)}
// onFocused
onBlur={this.onBlurCount.bind(this)}
// input only numbers
onKeyPress={(event) => {
if (!/[0-9]/.test(event.key)) {
event.preventDefault();
}
}}
/>;
}
}

This is not a react problem, but a html problem as you can see over here https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/number and I have made a stateless example you can see right here
https://codesandbox.io/s/l5k250m87

Related

React suggestions Input setting state

im prety new to React and im trying to use an autocomplete input. Im having problems getting the value from it and clearing the input values after submitting. Any help would be greatly appretiated.
import React, { Component, Fragment } from "react";
import PropTypes from "prop-types";
import "../AutoComplete/styles.css"
class Autocomplete extends Component {
static propTypes = {
suggestions: PropTypes.instanceOf(Array)
};
static defaultProps = {
suggestions: [],
};
constructor(props) {
super(props);
this.state = {
// The active selection's index
activeSuggestion: 0,
// The suggestions that match the user's input
filteredSuggestions: [],
// Whether or not the suggestion list is shown
showSuggestions: false,
// What the user has entered
userInput: this.props.value ? this.props.value : "",
};
}
//Order by 'code'
generateSortFn(prop, reverse) {
return function (a, b) {
if (a[prop] < b[prop]) return reverse ? -1 : 1;
if (a[prop] > b[prop]) return reverse ? 1 : -1;
return 0;
};
}
onChange = e => {
const { suggestions } = this.props;
const userInput = e.currentTarget.value;
// Filter our suggestions that don't contain the user's input
const filteredSuggestions = suggestions.sort(this.generateSortFn('code', true)).filter(
(suggestion, i) => {
let aux = suggestion.descrp+"- "+suggestion.code
return aux.toLowerCase().indexOf(userInput.toLowerCase()) > -1
}
);
this.setState({
activeSuggestion: 0,
filteredSuggestions,
showSuggestions: true,
userInput: e.currentTarget.value
});
};
onClick = e => {
this.setState({
activeSuggestion: 0,
filteredSuggestions: [],
showSuggestions: false,
userInput: e.currentTarget.innerText
});
};
onKeyDown = e => {
const { activeSuggestion, filteredSuggestions } = this.state;
// User pressed the enter key
if (e.keyCode === 13) {
this.setState({
activeSuggestion: 0,
showSuggestions: false,
userInput: filteredSuggestions[activeSuggestion].code+" - "+filteredSuggestions[activeSuggestion].descrp
});
}
// User pressed the up arrow
else if (e.keyCode === 38) {
if (activeSuggestion === 0) {
return;
}
this.setState({ activeSuggestion: activeSuggestion - 1 });
}
// User pressed the down arrow
else if (e.keyCode === 40) {
if (activeSuggestion - 1 === filteredSuggestions.length) {
return;
}
this.setState({ activeSuggestion: activeSuggestion + 1 });
}
};
render() {
const {
onChange,
onClick,
onKeyDown,
state: {
activeSuggestion,
filteredSuggestions,
showSuggestions,
userInput
}
} = this;
let suggestionsListComponent;
if (showSuggestions && userInput) {
if (filteredSuggestions.length) {
suggestionsListComponent = (
<ul className="suggestions">
{filteredSuggestions.map((suggestion, index) => {
let className="";
// Flag the active suggestion with a class
if (index === activeSuggestion) {
className = "suggestion-active";
}
return (
<li className={className} key={suggestion.code} onClick={onClick}>
{suggestion.code+" - "+suggestion.descrp}
</li>
);
})}
</ul>
);
} else {
suggestionsListComponent = (
<div className="no-suggestions">
<p>Sin sugerencias</p>
</div>
);
}
}
and the return (this is where i think im wrong)
return (
<Fragment>
<label htmlFor="autocomplete-input" className="autocompleteLabel">{this.props.label}</label>
<div className="centerInput">
<input
className="autocomplete-input"
type="text"
onChange={onChange}
onKeyDown={onKeyDown}
defaultValue={this.props.initState}
value= {/* this.props.value ? this.props.value : */ userInput}
placeholder={this.props.placeholder}
selection={this.setState(this.props.selection)}
/>
{suggestionsListComponent}
</div>
</Fragment>
);
}
}
export default Autocomplete;
What I want is to use this component in different pages, so im passing the "selection" prop and setting the state there.
The input is working correctly (searches, gets the value and shows/hide the helper perfectly). The problem is i cant reset this inputs clearing them, and i suspect the error is in here.
I get the following warning (even with it somewhat functioning)
Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.
This is the Component usage with useState:
<Autocomplete label='Out cost Center:' placeholder='Set the out cost center' suggestions={dataCostCenterHelper} selection={(text) => setOutCostCenter(text.userInput)} value={outCostCenter} />
and last this is how im tryin to clear the state that is set in "selection":
const clearData = async () => {
setOutCostCenter('-');
// other inputs with the same component
setOutVendor('-');
setOutRefNumber('-');
}
This gets called inside the function that handles the button submitting the form.
Thanks in advance!
Looking at the code you posted this line might be the problem:
selection={this.setState(this.props.selection)}
You are updating state directly inside the render method, this is not recommended.
Try using a selection prop or state field and update the prop inside a componenteDidMount life cycle
selection={this.state.selection}

Increment or decrement counter on left or right arrow key press on window in ReactJS

Hi I have written a block a code where the increment or decrement of a counter happens when I have selected the button element. This is the code that I have:
import React from "react";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
clicks: 0
};
}
increment = (e) => {
if (e.keyCode === 39) {
this.setState({
clicks: this.state.clicks + 1
})
}
}
decrement = (e) => {
if (e.keyCode === 37) {
this.setState({
clicks: this.state.clicks - 1
})
}
}
IncrementItem = () => {
this.setState({ clicks: this.state.clicks + 1 });
};
DecreaseItem = () => {
this.setState({ clicks: this.state.clicks - 1 });
};
render() {
return (
<div>
<button onClick={this.DecreaseItem} onKeyDown={this.decrement} tabIndex="0">
-1
</button>
<span>{this.state.clicks}</span>
<button onClick={this.IncrementItem} onKeyUp={this.increment} tabIndex="0">
+1
</button>
</div>
);
}
}
export default App;
What I want to happen is, when I press left arrow key, it should automatically decrement the value without the requirement of the button element to be selected/focused. Similarly for the right arrow key action as well.
Can someone help with this?
Thanks in advance.
In your case to listen for Keyboard events you have to attach event listener for document/window but not on button element.
Please refer below snippet for reference.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
clicks: 0
};
}
componentDidMount() {
document.addEventListener('keydown', this.keydown);
}
componentWillUnmount() {
document.removeEventListener('keydown', this.keydown);
}
keydown = (e) => {
if (e.keyCode === 39 || e.keyCode === 37) {
this.setState({
clicks: this.state.clicks + (e.keyCode === 39 ? 1 : -1)
})
}
}
IncrementItem = () => {
this.setState({ clicks: this.state.clicks + 1 });
};
DecreaseItem = () => {
this.setState({ clicks: this.state.clicks - 1 });
};
render() {
return (
<div>
<button onClick={this.DecreaseItem} tabIndex="0">
-1
</button>
<span>{this.state.clicks}</span>
<button onClick={this.IncrementItem} tabIndex="0">
+1
</button>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>

React: how to add a spinner after click, and change screen after the animation completes

I saw there are already answered questions on how to add spinners during fetch requests.
However what I need is to stop showing the animation when the animation completes. The animation completes after the timeout is reached.
Also I have a best practice question.
It's a good practice to empty the resources on componentWillUnmount and clear there the timeout. In the code below I clear the timeout in a if condition, because it has to stop as the height of the element reaches the right level.
Is that ok as I did it? If now, how should it look like to have the same functionality in the componentWillUnmount lifecycle phase?
Here is the animation Component:
class Thermometer extends Component {
state = {
termFill : 0
};
componentDidMount() {
const interval = setInterval(() => {
this.setState({
termFill: this.state.termFill + 10
});
if (this.state.termFill === 110) {
window.clearInterval(interval);
}
}, 200)
}
render() {
const styles = {
height: `${this.state.termFill}px`
};
if (this.state.termFill < 100) {
return (
<section>
<div id="therm-fill" style={styles} />
[MORE CODE - SHORTENED FOR EASIER READING]
)
}
};
And here is the Component that has to appear after the animation disappears.
The steps are like this:
A user enter and uses this tool
The user clicks "calculate"
The animation appears instead or on top of the tool
When the animation completes, the animation Component disappears and the tool
is once again visible with its updated state (results of the
calculation).
class DiagnoseTool extends Component {
state = {
[OTHER STATES REMOVED TO KEEP THE CODE SHORTER]
wasBtnClicked: false
};
[OTHER RADIO AND CHECKBOX HANDLERS REMOVED TO KEEP THE CODE SHORTER]
onButtonClick = e => {
e.preventDefault();
this.calculate();
this.setState({
wasBtnClicked: true
})
};
addResult = () => {
const resultColor = {
backgroundColor: "orange"
};
let theResult;
if (this..... [CODE REMOVED TO HAVE THE CODE SHORTER]
return theResult;
};
calculate = () => {
let counter = 0;
let radiocounter = 0;
Object.keys(this.state).filter(el => ['cough', 'nodes', 'temperature', 'tonsillarex'].includes(el)).forEach(key => {
// console.log(this.state[key]);
if (this.state[key] === true) {
counter += 1;
}
});
if (this.state.radioAge === "age14") {
radiocounter++
} else if (this.state.radioAge === "age45") {
radiocounter--
}
if (this.state.radioAge !== "") {
this.setState({
isDisabled: false
})
}
this.setState({
points: counter + radiocounter
});
};
render() {
const {cough, nodes, temperature, tonsillarex, radioAge, wasBtnClicked} = this.state;
return (
<Container>
<BackArrow />
[JSX REMOVED TO KEEP THE CODE SHORTER]
<div className="resultbox">
{
(wasBtnClicked) && this.addResult()
}
</div>
</div>
[HERE IS THE BUTTON]
<button
style={{height: "40px", width: "150px", cursor:"pointer"}}
type="submit"
className="calculateBtn"
onClick={this.onButtonClick}
disabled={!radioAge}
>CALCULATE</button>
</Container>
Add a boolean to your state and set it to false, when the user clicks the button set it to true, after doing the calculation set it to false.
calculate = () => {
let counter = 0;
let radiocounter = 0;
this.setState({
isLoading: true // set is loading to true and show the spinner
})
Object.keys(this.state)
.filter(el =>
["cough", "nodes", "temperature", "tonsillarex"].includes(el)
)
.forEach(key => {
// console.log(this.state[key]);
if (this.state[key] === true) {
counter += 1;
}
});
if (this.state.radioAge === "age14") {
radiocounter++;
} else if (this.state.radioAge === "age45") {
radiocounter--;
}
if (this.state.radioAge !== "") {
this.setState({
isDisabled: false
});
}
this.setState({
points: counter + radiocounter,
isLoading: false // set it to false and display the results of the calculation
});
};
Example
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.0/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.21.1/babel.min.js"></script>
<div id="root"></div>
<script type="text/babel">
class App extends React.Component {
timer = null;
constructor() {
super();
this.state = {
result: '',
isLoading: false
};
}
showContent = () => { this.setState({ isLoading: false, result: `7 + 5 = ${7 + 5}` })}
calculate = () => {
this.setState({
isLoading: true,
result: ''
});
this.timer = setTimeout(this.showContent, 5000);
}
componentWillUnmount = () => {
clearTimeout(this.timer);
}
render() {
return (
<div>
<p>7 + 5</p>
<p>{this.state.result}</p>
{ this.state.isLoading
? <p>Calculating...</p>
: <button onClick={this.calculate}>Calculate</button>
}
</div>
);
}
}
ReactDOM.render(
<App />,
document.getElementById('root')
);
</script>

Component only updates after two clicks React

I am building a React app that - among other things - generates a random number when a button is clicked and then filters an array of JSON objects to only the one at the index of that random number (i.e. JSON[random]). Normally the app is supposed to re-render after the array of JSON objects is filtered, but for some reason, on the first time the button is clicked and a random is picked, it requires two clicks to update. From then on it updates as expected, with a new random rendering each time the button is clicked.
I'm not sure if the problem is coming from App.js or somewhere lower down. On the first click, it generates a new random and supposedly saves this to state, but fails to re-render right away. On subsequent clicks, things seem to update based on the previously-generated random, while a new random is put in the queue. I would prefer the this all happens in one go: click, generate random, save to state, update to reflect the new random à la JSON[random].
This might have something to do with the way I have implemented lifecycle methods, as I'm admittedly not sure of all the nuances of each and have just tried to use whichever ones seemed to do what I wanted. If you have any suggestions there, please let me know...
Thanks!
Here are the relevant files:
App.js - where the random is generated and stored when a new click is registered in Header.state.randomClicks
class App extends Component {
constructor(props){
super(props)
this.state = {headerLink: "", searchValue: "", random: 0, randomClicks: 0}
this.generateRandom = this.generateRandom.bind(this);
}
getLinkFromHeader = (link) => {
if (this.state.headerLink !== link) {
this.setState({
headerLink: link,
})
}
}
getSearchValueFromHeader = (string) => {
this.setState({
searchValue: string,
});
}
getRandomMax = (max) => {
this.setState({
randomMax: max,
})
}
getRandomClicks = (value) => {
this.setState({
randomClicks: value,
})
}
generateRandom(number) {
let random = Math.floor(Math.random() * number) + 1;
console.log("generateRandom = ", random)
return random
}
shouldComponentUpdate(nextProps, nextState) {
return this.state.randomClicks !== nextState.randomClicks;
}
componentWillUpdate() {}
componentDidUpdate(prevState) {
let randomClicks = this.state.randomClicks;
console.log("this.state.randomClicks: ", this.state.randomClicks)
// console.log("prevState: ", prevState)
// console.log("prevState.randomClicks = ", prevState.randomClicks)
// ^^ is this a bug ? ^^
let random = this.generateRandom(this.state.randomMax);
if (this.state.random !== random) {
this.setState({random: random})
}
}
render() {
return (
<div className="App background">
<div className="content">
<Header getLinkFromHeader={this.getLinkFromHeader} getSearchValueFromHeader={this.getSearchValueFromHeader} randomClick={this.randomClick} getRandomClicks={this.getRandomClicks}/>
<TilesContainer link={this.state.headerLink} searchValue={this.state.searchValue} getRandomMax={this.getRandomMax} random={this.state.random} randomClicks={this.state.randomClicks}/>
</div>
</div>
);
}
}
export default App
Header.js* - where the randomClick count is incremented each time RandomButton is clicked
class Header extends Component {
constructor(props){
super(props);
this.state = { selectorLink: "", searchValue: "", randomClicks: 0 }
this.randomClick = this.randomClick.bind(this);
}
getLinkFromSelector = (link) => {
this.setState({
selectorLink: link,
})
}
getSearchValue = (string) => {
this.setState({
searchValue: string,
})
}
shouldComponentUpdate(nextProps, nextState) {
console.log("this.state !== nextState: ", this.state !== nextState)
return this.state !== nextState;
}
componentDidUpdate(previousState){
if(this.state.selectorLink !== previousState.selectorLink) {
this.props.getLinkFromHeader(this.state.selectorLink);
}
this.props.getSearchValueFromHeader(this.state.searchValue);
this.props.getRandomClicks(this.state.randomClicks);
console.log("Header Did Update")
}
randomClick(){
this.props.randomClick;
this.setState({
randomClicks: this.state.randomClicks += 1,
});
}
render(){
return(
<div id="header" className="header">
<div className="title-div">
<div className="h1-wrapper title-wrapper">
<h1>Pokédex Viewer App</h1>
</div>
</div>
<PokedexSelector getLinkFromSelector={this.getLinkFromSelector}/>
<SearchBar getSearchValue={this.getSearchValue}/>
<button type="button" id="random-button" onClick={this.randomClick}>Random Pokémon</button>
<button type="button" id="show-all-button" onClick={this.showAllClick}>Show All</button>
</div>
)
}
}
export default Header
TilesContainer.js - where the random number from App is sent and the tiles list is filtered/re-rendered
class TilesContainer extends Component {
constructor(props){
super(props);
this.state = {
pokemon: [],
filteredPokemon: [],
randomMax: 0,
showDetails: false,
};
this.getPokemon = this.getPokemon.bind(this);
this.tiles = this.tiles.bind(this);
this.getPokemon(this.props.link);
}
getPokemon(pokedexLink) {
let link = "";
(pokedexLink === "")
? link = "https://pokeapi.co/api/v2/pokedex/national/"
: link = this.props.link;
fetch(link)
.then(response => response.json())
.then(myJson => {
let list = myJson['pokemon_entries'];
this.setState({
pokemon: list,
randomMax: list.length,
})
this.props.getRandomMax; // send randomMax to App
})
}
filterPokemon(string) {
if (string !== "") {
console.log("string: ", string)
string = string.toString().toLowerCase()
let filteredPokemon = this.state.pokemon.filter(pokemon => {
const name = pokemon.pokemon_species.name;
const nameStr = name.slice(0,string.length);
const number = pokemon.entry_number;
const numberStr = number.toString().slice(0, string.length);
return (this.state.random !== 0) ? number.toString() === string : nameStr === string || numberStr === string;
})
if (this.props.randomClicks !== 0) { // i.e. using a random
this.setState({
filteredPokemon: filteredPokemon,
})
} else {
this.setState({
filteredPokemon: filteredPokemon,
randomMax: filteredPokemon.length,
})
}
} else {
this.setState({
filteredPokemon: [],
randomMax: this.state.pokemon.length,
})
}
}
componentDidUpdate(prevProps, prevState) {
if (this.props.link !== prevProps.link) {
this.getPokemon(this.props.link)
}
if (this.props.searchValue !== prevProps.searchValue) {
this.filterPokemon(this.props.searchValue)
}
if (this.state.randomMax !== prevState.randomMax){
this.props.getRandomMax(this.state.randomMax);
}
if (this.props.random !== prevProps.random) {
console.log("TilesContainer random: ", this.props.random)
this.filterPokemon(this.props.random)
}
}
tiles() {
console.log("tiles() filteredPokemon: ", this.state.filteredPokemon)
console.log("tiles() searchValue: ", this.props.searchValue)
console.log("tiles() random: ", this.props.random)
if (this.state.pokemon.length > 0) {
if (this.state.filteredPokemon.length == 0 && this.props.searchValue === ""){
return (
this.state.pokemon.map(pokemon => (
<Tile key={pokemon.entry_number} number={pokemon.entry_number} name={pokemon.pokemon_species.name} url={pokemon.pokemon_species.url}/>
))
)
} else if (this.state.filteredPokemon.length > 0){
return (
this.state.filteredPokemon.map(pokemon => (
<Tile key={pokemon.entry_number} number={pokemon.entry_number} name={pokemon.pokemon_species.name} url={pokemon.pokemon_species.url}/>
))
)
}
}
}
render(){
return (
<div id="tiles-container"
className="tiles-container">
{this.tiles()}
</div>
)
}
}
export default TilesContainer
You should not use current state in setState and should not modify state directly. And you do no actually call this.props.randomClick and it is undefined. Change
randomClick(){
this.props.randomClick;
this.setState({
randomClicks: this.state.randomClicks += 1,
});
}
to
randomClick(){
if (typeof(this.props.randomClick) === 'function') this.props.randomClick();
this.setState(olState => ({
randomClicks: olState.randomClicks + 1,
}));
}
Also check your shouldComponentUpdate methods. They might be buggy or redundant. Looks like you prevent updating App when state.random changes. So every time you click the button you store the new random value but use the previous one. So for the initial render and for the first click you use random: 0.
And I guess that getRandomClicks should be setRandomClicks.

react input validation checking against last entered value instead of current value

I'm trying to validate my input , if the number is between 100 and 200 is should display valid or invalid , the problem i'm having is that it seems to be checking against the last entered value, so for instance if the user enters 1222 this will display valid as I believe it is actually checking against the last entered number of 122 and also if I then delete 2 charachers so it displays 12 this will also display valid. I believe this is because of how the state is set but I am not sure how to correctly get this to work.
How can I change this so it will check against the correct value and validate correctly?
Textbox.js
class TextBox extends React.Component {
state = {
valid: '',
value: ''
}
onChange = (event) => {
this.props.onChange(event.target.value);
this.validation()
}
validation() {
(this.props.value > 100 && this.props.value < 200) ? this.setState({value: this.props.value}) : this.setState({value: this.props.value})
}
onChangeInput(e) {
const { name, value } = e.target;
this.setState({
[name]: value
}, () => console.log(this.state.mail === this.state.confMail));
}
render() {
return (
<Box
invalid={!this.props.isValid}
>
<Label
rollo={this.props.designation === 'rollo'}
pleating={this.props.designation === 'pleating'}
>{this.props.label}</Label>
<span>
<Input
type={this.props.type && this.props.type}
defaultValue={this.props.defaultValue}
onChange={this.onChange}
placeholder={this.props.placeholder}
value={this.props.value || ''}
/>
<Tooltip>{this.state.valid}</Tooltip>
</span>
</Box>
);
}
};
export default TextBox;
component.js
<TextBox
type="number"
label="Fenstertyp"
defaultValue={ this.props.height / 100 * 66 }
onChange={newValue => this.props.selectOperationChainLength(newValue)}
tooltip="invalid"
value={this.props.operationChainLength.value}
/>
actions.js
export function selectOperationChainLength(operationChainLength) {
return {
type: SELECT_OPERATION_CHAIN_LENGTH,
operationChainLength
}
}
You can shift the validation logic to onChange method on event.target.value, there is no need to create the separate method. It will then look like this.
class TextBox extends React.Component {
state = {
valid: false,
value: ''
}
onChange = (event) => {
const value = event.target.value;
(value > 100 && value < 200) ? this.setState({value, valid: true}) : this.setState({value, valid: false})
this.props.onChange(value);
}
onChangeInput(e) {
const { name, value } = e.target;
this.setState({
[name]: value
}, () => console.log(this.state.mail === this.state.confMail));
}
render() {
return (
<Box
invalid={!this.props.isValid}
>
<Label
rollo={this.props.designation === 'rollo'}
pleating={this.props.designation === 'pleating'}
>{this.props.label}</Label>
<span>
<Input
type={this.props.type && this.props.type}
defaultValue={this.props.defaultValue}
onChange={this.onChange}
placeholder={this.props.placeholder}
value={this.props.value || ''}
/>
<Tooltip>{this.state.valid}</Tooltip>
</span>
</Box>
);
}
};
export default TextBox;
Ok, so there are some things going wrong here.
import React, { Component } from "react";
import { render } from "react-dom";
class TextBox extends Component {
constructor(props){
super(props);
// 1.
// You only need to store the `isValid` property.
// The value is needed only for the validation, right?
this.state = {
isValid: false
}
}
onChange(e) {
const { target } = e;
const { value } = target;
// 2.
// If the value is in the right range : isValid = true
// else : isValid = false
if( value > 100 && value < 200 ) {
this.setState({ isValid: true });
} else {
this.setState({ isValid: false });
}
}
render() {
// 3.
// Always use destructuring. It's way easier to follow ;)
const { type } = this.props;
const { isValid } = this.state;
return (
<Fragment>
<input
type={type}
onChange={e => this.onChange(e)}
/>
{/* 4. */}
{/* Assign the right text to your tooltip */}
<p>{ isValid ? "valid" : "invalid" }</p>
</Fragment>
);
}
}
ReactDOM.render(
<TextBox type="number" />,
document.getElementById('root')
);
<div id="root"></div>
<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>
I simplified the example so it can be easier to follow.
Here is a working example
I think you need to set your valid as false to begin with
state = {
valid: false,
value: ''
}

Categories

Resources