How to properly use input patterns with React - javascript

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?

Related

How can I evaluate each element in array and use useState() on one condition?

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>

How to type unique letters only in input text box in html or react?

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>
);
}

cant find the matching array of object search by id

i used a custom hook and i want to show a single item which is matched by particular id,i tried multiple method but didnt work.here is my code :
const SingleServiceDetail = () => {
const {idOfService} = useParams();
const [data] = useCustom();
const singleItem = data.find(element =>element._id === idOfService);
console.log(singleItem);
return (
<div>
<h2>You have Choosed :{idOfService}</h2>
{/* <p>{singleItem.name}</p> */}
</div>
);
};
Possibly idOfService from useParams is a string and element._id is a number.
Try with double equal it will type coerce and check the id.
const singleItem = data.find(element => element._id == idOfService);
else change the idOfService to number
const singleItem = data.find(element => element._id === +idOfService);

Why is this variable not considered to be 0 despite debugger saying otherwise?

I'm writing a React component that takes a total number of reviews as a prop. When the number of reviews is 0, I want to render an element stating
<div>No reviews yet.</div>
Otherwise, I render elements containing review data. Here is the component and its context:
const Stats = ({good, neutral, bad, totalReviews}) => {
if ({totalReviews} === 0)
return <div>No reviews yet.</div>
else {
return (
<div>
<Stat text="Total: " amount={totalReviews} />
</div>
);
}
}
const App = () => {
const [good, setGood] = useState(0);
const [neutral, setNeutral] = useState(0);
const [bad, setBad] = useState(0);
let totalReviews = good + neutral + bad;
return (
<div>
<Stats totalReviews={totalReviews} />
</div>
)
}
I have used the debugger command to check in Chrome's developer console the values of each variable. It shows that totalReviews = 0. The variables good, neutral, and bad all also = 0.
I've also used console.log(totalReviews).
0 is displayed by the console.log. How come my program enters the second conditional as if totalReviews isn't 0?
if (totalReviews === 0)
You only wrap js statements in curly braces inside jsx, but your if statement is just regular js.
Problem with your if condition.
it should be if (totalReviews === 0) or if (totalReviews == 0) to avoid strongly type conversation check.
You have added {} inside if condtion which is not a stadard way

Allow minus symbol (-) in value-bound react inputs

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>

Categories