I am wondering if there is an elegant and/or recommended solution to this problem.
If I have an input which I'm using for numbers, and I'd like to enter a negative number, I first have to enter the - symbol. When I enter this, parseInt will return NaN, which is understandable. However, if the value of the input is bound to the result of the parseInt, then I can never finish entering the number, as it will fail as soon as it attempts to parse the - as an int.
const { useState } = React;
const App = () => {
const [count, setCount] = useState(0);
const [inputValue, setInputValue] = useState('')
const update = ({ target }) => {
const { value } = target;
const attemptedParse = parseInt(value);
if (!attemptedParse) {
setInputValue(value);
setCount(0);
} else {
setInputValue(attemptedParse);
setCount(attemptedParse);
}
}
return (
<div>
<h1>{count}</h1>
<input value={inputValue} onChange={update} />
</div>
)
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
The above solution uses two separate states, one for the input, and another for the actual value. But this seems a little messy, and I wonder if anyone has any solutions which involve a little less code.
There should be two states, one for input value, another one for parsed integer.
Input value shouldn't necessarily re-set to parsed integer, this may make input more troublesome, like in the case of negative number:
const update = ({ target }) => {
const { value } = target;
const attemptedParse = parseInt(value);
if (!Object.is(NaN, attemptedParse)) {
setCount(attemptedParse);
}
}
You can use regular expression
const { useState } = React;
const App = () => {
const [count, setCount] = useState(0);
const [inputValue, setInputValue] = useState('')
const update = ({ target }) => {
var { value } = target;
// Replace all non-numeric characters to ''
value = value.replace(/[^0-9-]+/g, '');
// The real pattern you are looking for
var pattern = /([-])?([0-9]+)/g;
var matches = value.match(pattern);
if(matches){
value = matches[0];
}
setInputValue(value);
setCount(value);
}
return (
<div>
<h1>{count}</h1>
<input value={inputValue} onChange={update} />
</div>
)
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Related
everyone!
I have a component that displays an input field. This component has some patterns it has to meet before doing anything with its data. The regex is: /^(\d*)\s*(\([+-]\d+\))?$/. Basically, it accepts a number, optionally followed by another number between parenthesis and a signal (+ or -), like: 4 (+2); 5 (-1); 75 (-20); 10; 2.
I couldn't find a proper way to build my component that meets this pattern while displaying the proper text to the user. In this iteration, I created three states: one for the text field, one for the first value, one for the value between parenthesis.
The problem with this approach below is that there are two sources of truth: field and modifier|value, which is not ideal and lead to unnecessary code trying to sync them.
const CharacterTraitField: React.FC<CharacterTraitFieldProps> = (props) => {
const [field, setField] = useState(`${props.value} (+${props.modifier})` /*string*/);
const [value, setValue] = useState(props.value /*number*/);
const [modifier, setModifier] = useState(props.modifier /*number*/);
const onValueBlur: React.FocusEventHandler<HTMLInputElement> = (ev) => {
const match = field.match(FIELD_REGEX);
if (!match) {
setField('0');
setValue(0);
setModifier(0);
return console.warn(value, 'input is not valid.');
}
const val = parseInt(match[1]) || 0;
const mod = parseInt(match[2].replace(/\(|\)/g, '')) || 0;
setValue(val);
setModifier(mod);
};
return (
<>
<div>
<FormControl
// pattern='^(\d*)\s*(\([+-]\d+\))?$'
className='text-center'
size='sm'
value={field}
onChange={(ev) => setField(ev.target.value)}
onBlur={onValueBlur}
/>
</div>
</>
);
};
I have tried using only modifier|value but this leads to poor text manipulation when handling onChange event:
const CharacterTraitField: React.FC<CharacterTraitFieldProps> = (props) => {
const [value, setValue] = useState(props.value /*number*/);
const [modifier, setModifier] = useState(props.modifier /*number*/);
return (
<>
<div>
<FormControl
pattern='^(\d*)\s*(\([+-]\d+\))?$'
className='text-center'
size='sm'
value={`${value} (+${modifier})`}
onChange={(ev) => {
if (ev.target.validity.patternMismatch) return;
setField(ev.target.value);
}}
/>
</div>
</>
);
};
What is the best approach, given these props, that is both efficient and user-friendly?
I'm making a blackjack hand simulation and I've encountered an issue with my code.
The game goes like this: users gets two random cards and a total of the points, clicks 'hit' to get another random card from the deck. Now that's all working but there's one more rule: if that card is an Ace, user chooses if they want to get 1 or 10 points. I implemented it before when I only had one card at a time with useEffect, however now I refactored my code and the total isn't kept in useState + the array has two cards that need to evaluated, not the most recent one.
I've tried putting my loop and if statement in a useEffect and conditionally render the Popup to let the user decide (with and without dependencies), but when I put the useState() to trigger the condition, it throws an error that there have been too many renders and I'm not sure why that is.
Here's my Home component:
import {useState, useEffect} from 'react'
import Card from '../components/Card';
import Total from '../components/Total';
import Popup from '../components/Popup'
import {shuffle} from '../hooks/shuffleCards'
import {deckArray} from '../utils/data'
export default function Home(){
const startHandSize = 2
const [starterDeck, setStarterDeck] = useState(shuffle(deckArray))
const [howManyDealt, setHowManyDealt] = useState(startHandSize)
const [triggerPopup, setButtonPopup] = useState(false)
const deal = () => {
setHowManyDealt(startHandSize)
setStarterDeck(shuffle(deckArray))
}
const hit = () => !bust && setHowManyDealt(prev => prev + 1)
const usersCards = starterDeck.slice(-howManyDealt)
var total = 0
usersCards.forEach(function (arrayItem) {
if(arrayItem.card === "A"){
alert("you have an ace")
}
else{
total += arrayItem.value
}
});
const bust = total > 21;
return(
<div>
<button onClick={deal}>DEAL</button>
<button disabled={bust} onClick={hit}>HIT</button>
<button disabled={bust}>STAND</button>
<Total total={total} usersCards={usersCards}/>
<Card usersCards={usersCards}/>
{triggerPopup && <Popup total={total} setButtonPopup={setButtonPopup}/>}
</div>
)
}
and my Popup:
export default function Popup({total, setButtonPopup}){
const handleClick = (points) => {
total += points
setButtonPopup(false)
}
return(
<div className="popup">
<div className="popup-inner">
<h4>You've got an Ace. Would you like to collect 1 or 10 points?</h4>
<button className=".btn-popup" onClick={() => handleClick(1)}>1 POINT</button>
<button className=".btn-popup" onClick={() => handleClick(10)}>10 POINTS</button>
</div>
</div>
)
}
Any help much appreciated!
Good attempt. However, there seems to be a general misunderstanding about state. Consider this code:
const handleClick = (points) => {
total += points
setButtonPopup(false)
}
total is a purely local variable to Popup, so this += pretty much does nothing. To change state in the caller, you'd normally pass a callback that can trigger a setState and move the new value for total into state.
Remember: any data change must happen immutably, and if you want to trigger a re-render, you have to set state. Of course, there are ways to circumvent this flow using refs and so forth, but these are escape hatches you shouldn't use if you don't have to.
However, a design with total kept in state strikes me as redundant. We already know the total based on the cards in play. A better strategy seems to be having ace values individually settable via the popup modal, assuming you don't want to auto-compute these ace values to be as high as possible without busting or use a toggle switch instead of a modal.
I kept going with my code from your previous question and added the modal. I'm treating high aces as 11 per the rules of Blackjack, but you can easily make that 10 if you want.
As before, I'm hoping you can apply the techniques here to your code. The keys are the handleAceSet callback and the new piece of state aceToSet, which is a ace the user has picked, or null if the user hasn't chosen an ace. aceToSet is like your setButtonPopup, but tracks an object or null rather than a boolean. When aceToSet isn't null, the user has selected an ace and we show the modal to let them pick a value for it.
handleAceSet may seem a bit complex, but it has to be due to immutability. It finds the index of the ace the user wants to set in the deck array, then creates a new object at this index with the new value and glues the subarray slices before and after the index back together.
// utility library "import"
const cards = (() => {
const shuffle = a => {
a = a.slice();
for (let i = a.length - 1; i > 0; i--) {
const j = ~~(Math.random() * (i + 1));
const x = a[i];
a[i] = a[j];
a[j] = x;
}
return a;
};
const frz = (...args) => Object.freeze(...args);
const suits = frz([..."HCSD"]);
const faces = frz([..."AJQK"]);
const pips = frz([...Array(9)].map((_, i) => i + 2));
const ranks = frz([...pips, ...faces]);
const cards = frz(
suits.flatMap(s =>
ranks.map(r =>
frz({
rank: r,
suit: s,
str: r + s,
value: isNaN(r) ? (r === "A" ? 1 : 10) : r,
})
)
)
);
const shuffled = () => shuffle(cards);
return {shuffled};
})();
const {Fragment, useState} = React;
const AceSetterModal = ({handleSetLow, handleSetHigh}) => (
<div>
<button onClick={handleSetLow}>Set ace low</button>
<button onClick={handleSetHigh}>Set ace high</button>
</div>
);
const Card = ({card, handleAceSet}) => (
<div>
{card.str}
{card.rank === "A" && (
<Fragment>
{" "}
<button onClick={handleAceSet}>
Set ({card.value})
</button>
</Fragment>
)}
</div>
);
const Game = () => {
const startHandSize = 2;
const goal = 21;
const lowAce = 1;
const highAce = 11;
const [deck, setDeck] = useState(cards.shuffled());
const [cardsDealt, setCardsDealt] = useState(startHandSize);
const [aceToSet, setAceToSet] = useState(null);
const handleAceSet = value => {
setDeck(deck => {
const i = deck.findIndex(e => e.str === aceToSet.str);
return [
...deck.slice(0, i),
{...aceToSet, value},
...deck.slice(i + 1),
];
});
setAceToSet(null);
};
const deal = () => {
setCardsDealt(startHandSize);
setDeck(cards.shuffled());
};
const hit = () => !bust && setCardsDealt(prev => prev + 1);
const cardsInPlay = deck.slice(-cardsDealt);
const total = cardsInPlay.reduce((a, e) => a + e.value, 0);
const bust = total > goal;
return (
<div>
{aceToSet ? (
<AceSetterModal
handleSetLow={() => handleAceSet(lowAce)}
handleSetHigh={() => handleAceSet(highAce)}
/>
) : (
<Fragment>
<button onClick={deal}>Deal</button>
<button disabled={bust} onClick={hit}>
Hit
</button>
<div>
{cardsInPlay.map(e => (
<Card
key={e.str}
handleAceSet={() => setAceToSet(e)}
card={e}
/>
))}
</div>
<div>Total: {total}</div>
<div>{bust && "Bust!"}</div>
</Fragment>
)}
</div>
);
};
ReactDOM.createRoot(document.querySelector("#app"))
.render(<Game />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
I want to create an input box that allows to type only a distinct alphabet letter in the input box
( No duplicate alphabet value, ONLY ONE)
I looked up all the attributes for input box but I couldn't find one and there were no example.
Do I have to handle it within JavaScript functions?
(I am using React)
<input
className="App-Contain-Input"
name="containedLetter"
type={"text"}
onChange={containedChange}
/>
Here is how this can be done, note that we have to use onkeydown which fires when a key is being pressed, but not before it is being released (this way we can intercept and prevent the key stroke from being taken into account):
function MyInput() {
const containedChange = (event) => {
if (event.key.length === 1 && event.code.startsWith('Key') && event.code.length === 4 && event.target.value.indexOf(event.key) !== -1) {
event.preventDefault()
return false;
}
return true
}
return (
<input
id="input"
className="App-Contain-Input"
name="containedLetter"
type="text"
onkeydown={containedChange}
/>
}
A way to do it with a cheeky two-liner is to get rid of any non-letters with regex, then let Set do the de-duping by converting to Array => Set => Array => String:
As a side note, many of the other solutions posted here make one or both of two assumptions:
The user is only ever going to edit the last character... but what if they edit from the beginning or middle? Any last character solution will then fail.
The user will never copy/paste a complete value... again, if they do, most of these solutions will fail.
In general, it's best to be completely agnostic as to how the value is going to arrive to the input, and simply deal with the value after it has arrived.
import { useState } from 'react';
export default function Home() {
const [value, setValue] = useState('');
return (
<div>
<input
type='text'
value={value}
onChange={(e) => {
const onlyLetters = e.target.value.replace(/[^a-zA-Z]/g, '');
setValue(Array.from(new Set(onlyLetters.split(''))).join(''));
}}
/>
</div>
);
}
Your containedChange function can be like this:
const containedChange = (e) => {
const value = e.target.value;
const lastChar = value[value.length - 1];
if (value.slice(value.length - 1, value.length).indexOf(lastChar) != -1) {
// show error message
}
The function checks whether the last entered character already exists in the previous value or not.
An isogram is a word with no repeating letters. You can check this using regex
function isIsogram (str) {
return !/(.).*\1/.test(str);
}
Or using array.some
function isIsogram(str) {
var strArray = str.split("");
return !strArray.some(function(el,idx,arr){
return arr.lastIndexOf(el)!=idx;
});
}
In react - you need to use state to reject inputs which contain repeated letters (not an Isogram). Complete example on codesandbox.
import { useState } from 'react';
export default function App() {
const [ text, setText ] = useState('');
function isIsogram(str) {
var strArray = str.split("");
return !strArray.some(function(el,idx,arr){
return arr.lastIndexOf(el)!==idx;
});
}
function handleChange(e) {
const { value } = e.target;
console.log(isIsogram(value));
if (value.length === 1 || isIsogram(value)) {
setText(value);
}
}
return (
<input value={text} onChange={handleChange} />
);
}
Use a state to maintain the current value of the input. Then when the value changes get the last letter of the input, check that it's a letter, and if it is a letter and it isn't already in the state, update the state with the new input value.
const { useState } = React;
function Example() {
const [ input, setInput ] = useState('');
function handleChange(e) {
const { value } = e.target;
const lastChar = value.slice(-1);
const isLetter = /[a-zA-Z]/.test(lastChar);
if (isLetter && !input.includes(lastChar)) {
setInput(value);
}
}
function handleKeyDown(e) {
if (e.code === 'Backspace') {
setInput(input.slice(0, input.length - 1));
}
}
return (
<input
value={input}
onChange={handleChange}
onKeyDown={handleKeyDown}
/>
);
}
ReactDOM.render(
<Example />,
document.getElementById('react')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>
One pure js solution would be to handle the input value in onkeyup event where we have access to the latest key as well as updated input target value.
If the latest key pressed is duplicate then we will found it on 2 location in the target value, first on some where middle of the target value and second at the end of the target value. So we can remove the value at the end (which is the duplicate one).
The below code snippet shows the implementation of above in React
const Input = () => {
const handleKeyUp = (e) => {
const { key, target: { value }} = e;
const len = value.length;
if (value.indexOf(key) < len - 2) {
e.target.value = value.slice(0, len - 1);
}
}
return (
<div>
<label>Unique Keywords</label>
<input type="text" placeholder="my-input" onKeyUp={handleKeyUp} />
</div>
);
}
I am currently trying to teach myself react. I ran into this weird behavior and couldn't explain what was going on so I was hoping to get some answers.
In the below code snippet. I have a variable index that's initially set to 0. When I click the button, I expect handleClick() to update the value of index. However, it does not do it as I would expect.
const { useState } = React;
const Home = () => {
let names = ["Hello", "World"];
let index = 0;
const [name, setName] = useState(names[index]);
const handleClick = () => {
console.log(`Output: ${name}`);
console.log(`Index = ${index}`);
index = (index+1)%2;
setName(names[index]);
}
return (
<div className="Home">
<button onClick={handleClick}>Click</button>
</div>
);
}
ReactDOM.render(<Home />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
I expect the console.log output to be as follows:
Output: Hello
Index = 0
Output: World
Index = 1
Output: Hello
Index = 0
Output: World
Index = 1
...
Instead, what I get is:
Output: Hello
Index = 0
Output: World
Index = 0
Output: World
Index = 1
Output: Hello
Index = 0
...
Can someone explain what is going on here?
When a state variable is updated using setState method, the component re-renders. In your code the index variable is not a state variable, so whenever the Home component re-renders the index variable will be inialized to 0.
You can change index to state variable or you can also use ref, if you what to persist its state accross re-renders.
You should consider moving the index to the state and moving names to props. The former will correct the behaviour of the component, as you're looking for, and the latter will make the component more re-usable.
const { useState } = React;
const Home = ({names}) => {
const [index, setIndex] = useState(0);
const name = names[index];
const handleClick = () => {
console.log(`Output: ${name}`);
console.log(`Index = ${index}`);
setIndex((index+1)%2);
}
return (
<div className="Home">
<button onClick={handleClick}>Click</button>
</div>
);
}
ReactDOM.render(<Home names={["Hello", "World"]} />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
instead of just let index = 0
which is re-initialized to 0 every rerender when setName is run
you need something which is not re-initialized, as below
//on your imports
import {useRef} from 'react'
//inside you component
const index = useRef(0);
//on updating it
index.current = (index.current+1)%2
this is my number input filed commponnet, and i want to make it on the UI as number with commas when use type for example: 100000 I want it show it like that => 100,000. im using react-bootstrap. someone can help me in how i can done it right?
this is an image on the its need to be on the UI
https://i.stack.imgur.com/lfyls.png
import { Form } from 'react-bootstrap';
export default function (props: any) {
return (
<Form.Control type="number" />
)
}
Use this function:
const commaSeparators = num => {
let numParts = num.toString().split('.');
numParts[0] = numParts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return numParts.join('.');
};
Edit: Short way:
const commaSeparators = num => num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");