Attaching my code - built using create-react-app.
Working on a small component for searching inputs. Highlight word if match and continue to display other text if not matched.
Current solution displays mark on all text once called.
import React, { useState } from "react";
const app = (props) => {
const [value, setValue] = useState("");
const [searchValue, setSearchValue] = useState("");
const [checked, setChecked] = useState(false);
const [sensitive, setSensitive] = useState("i");
let highlight = null;
const handleChange = (event) => {
setValue(event.target.value);
};
console.log(value);
const handleSearchChange = (event) => {
setSearchValue(event.target.value);
};
console.log(searchValue);
const getHighlightedText = (value, searchValue) => {
let regex = new RegExp(`(${searchValue})`, `g${sensitive}`);
console.log(regex);
const parts = value.split(regex);
//console.log(parts);
highlight = <span> { parts.map((part, i) =>
<span key={i} style={part === searchValue ? { backgroundColor: 'Yellow' } : {} }>
{ part }
</span>)
} </span>;
}
const checkedTest = () => {
if(checked === true) {
setSensitive(" ") // makes it case sensitive
console.log(sensitive);
setChecked(true);
} else {
setSensitive("i")
console.log(sensitive);
setChecked(false);
}
}
return (
<>
<form className="text-search-form">
<textarea className="source-text" value={value} onChange={handleChange}/>
<input className="search-term" value={searchValue} onChange={handleSearchChange} onKeyPress={getHighlightedText(value, searchValue)} />
<label htmlFor="caseSensitive">case sensitive?
<input
type="checkbox"
className="case-sensitive"
name="caseSensitive"
defaultChecked={checked}
onClick={getHighlightedText(value, searchValue)}
onChange={checkedTest}
/>
</label>
</form>
<div className="result">{highlight}</div>
</>
);
};
export default Highlighter;
Adjusted the code to display my proper component name.
This should work as expected
Removed all manual invocation of getHighlightedText and instead moved it to JSX and removed its parameters (May not be a good idea). The reason is that the values getHighlightedText depends on are all in the function state and change of any would trigger a rerender which would automatically call this function.
Secondly, fixed your checkedTest, this is more cleaner.
Thirdly, the comparison you were doing in the iteration of parts wasn't case insensitive, so even if your regex was case insensitive, === of string isn't so, moved it to a seperate function and handled both cases. The insensitive case was handled by converting both part and searchvalue to a common case, in this case, lowercase.
As always, this can be improved so much more, but I think it solves your issues.
Demo:
codesandbox
import React, { useState } from "react";
const App = props => {
const [value, setValue] = useState("");
const [searchValue, setSearchValue] = useState("");
const [checked, setChecked] = useState(false);
const [sensitive, setSensitive] = useState("i");
const handleChange = event => {
setValue(event.target.value);
};
const handleSearchChange = event => {
setSearchValue(event.target.value);
};
const getHighlightColor = part => {
let isEqual = false;
if (checked) {
isEqual = part === searchValue;
} else {
isEqual = part.toLowerCase() === searchValue.toLowerCase();
}
return isEqual ? "Yellow" : "transperant";
};
const getHighlightedText = () => {
let regex = new RegExp(`(${searchValue})`, `g${sensitive}`);
const parts = value.split(regex);
if (searchValue)
return (
<span>
{parts.map((part, i) => (
<span key={i} style={{ backgroundColor: getHighlightColor(part) }}>
{part}
</span>
))}
</span>
);
};
const checkedTest = event => {
setChecked(event.target.checked);
setSensitive(event.target.checked ? "" : "i");
};
return (
<>
<form className="text-search-form">
<textarea
className="source-text"
value={value}
onChange={handleChange}
/>
<input
className="search-term"
value={searchValue}
onChange={handleSearchChange}
/>
<label htmlFor="caseSensitive">
case sensitive?
<input
type="checkbox"
className="case-sensitive"
name="caseSensitive"
defaultChecked={checked}
onChange={checkedTest}
/>
</label>
</form>
<div className="result">{getHighlightedText()}</div>
</>
);
};
export default App;
If you change following component and callback, it should work.
1) onChange callback, get the user selected option (true/false)
2) In the callback, call the search function.
Hope this helps.
<input
type="checkbox"
className="case-sensitive"
name="caseSensitive"
defaultChecked={checked}
onChange={event => checkedTest(event.target.checked)}
/>;
const checkedTest = isChecked => {
setChecked(isChecked);
setSensitive(isChecked ? " " : "i");
// call the getHighlightedText
getHighlightedText(value, searchValue);
};
Related
I am trying to validate an onchange but can't seem to get it working.
Essentially, I want to test that if the input matches the regex then we present a message under the input.
I am unsure where to put the validator, I wondered if anyone could point me in the right direction
Here is a sandbox.
https://codesandbox.io/s/blazing-hooks-gni5jy?file=/src/components/Dashboard/Dashboard.js
const Dashboard = () => {
const [number, setNumber] = useState(null);
// const [isValid, setIsValid] = useState(false);
// const validator = (value) => {
// if (!value) return false;
// const re = /\b\d{5}\b/g;
// return re.test(value.trim());
// };
const onChangeHandler = (event) => {
const value = event.target.value;
setNumber(value);
};
return (
<div>
<input value={number || ""} onChange={onChangeHandler} />
{/* {isValid ? <p>is valid</p> : null} */}
</div>
);
};
export default Dashboard;
Set the state inside the validator
const [isValid, setIsValid] = useState(true);
const validator = (value) => {
if (!value) return false;
const re = /\b\d{5}\b/g;
const valid = re.test(value);
setIsValid(valid);
};
const onChangeHandler = (event) => {
const value = event.target.value;
setNumber(value);
validator(value);
};
Demo
You will have to call the validator for it to work.
import React, { useState } from "react";
const Dashboard = () => {
const [number, setNumber] = useState(null);
const [isValid, setIsValid] = useState(false);
const validator = (value) => {
if (!value) return false;
const re = /\b\d{5}\b/g;
return re.test(value.trim());
};
const onChangeHandler = (event) => {
const value = event.target.value;
setNumber(value);
setIsValid(validator(value)); // Validating the input
};
return (
<div>
<input value={number || ""} onChange={onChangeHandler} />
{isValid ? <p>is valid</p> : null}
</div>
);
};
export default Dashboard;
I'm making it so that every component is one element (button, the whole list, a single element...) I'm having trouble figuring out how to make my list print below the form. Tasks are shown in console.log() but I can't seem to get the right data transferred.
Thanks in advance for any help
This is items.jsx code
import React, { useState} from 'react'
import './todo.css'
import List from './list'
import Button from './button';
function Items () {
const [tasks, setTasks] = useState([]);
const [value, setvalue] = useState("");
/* const onChange = (e) => {
setvalue(e.target.value)
// console.log('type')
} */
const onAddTask = (e) =>{
e.preventDefault();
console.log('submit')
const obj = {
name: value ,
id: Date.now(),
};
if (value !== "") {
setTasks(tasks.concat(obj));
setvalue("")
console.log(obj)
}
};
return(
<div className="form">
<header>Your todo list</header>
<input
placeholder="type your task"
value={value}
onChange={(e) => setvalue(e.target.value)}/>
<input type="date" placeholder='Set your date!'/>
<button onClick={onAddTask}>Submit task</button>
<List data = {List}/>
</div>
)
}
export default Items
This is list.jsx code
import React , { useState } from "react";
import "./Items"
import Button from "./button"
const List = (tasks) => {
return(
<div>
{tasks.map}
</div>
)
console.log(task.map)
}
export default List
step 1
Here's a fully functioning demo to get you started -
function Todo() {
const [items, setItems] = React.useState([])
const [value, setValue] = React.useState("")
const addItem = event =>
setItems([...items, { id: Date.now(), value, done: false }])
return <div>
<List items={items} />
<input value={value} onChange={e => setValue(e.target.value)} />
<button type="button" onClick={addItem}>Add</button>
</div>
}
function List({ items = [] }) {
return <ul>
{items.map(item =>
<ListItem key={item.id} item={item} />
)}
</ul>
}
function ListItem({ item = {} }) {
return <li>{item.value}</li>
}
ReactDOM.render(<Todo />, document.body)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
start with good state
Note using an Array to store the items is somewhat inefficient for the kinds of operations you will need to perform. Consider using a Map instead. Run the demo again and click on a list item to toggle its state -
const update = (m, key, func) =>
new Map(m).set(key, func(m.get(key)))
function Todo() {
const [items, setItems] = React.useState(new Map)
const [value, setValue] = React.useState("")
const addItem = event => {
const id = Date.now()
setItems(update(items, id, _ => ({ id, value, done: false })))
}
const toggleItem = id => event =>
setItems(update(items, id, item => ({ ...item, done: !item.done })))
return <div>
<List items={items} onClick={toggleItem} />
<input value={value} onChange={e => setValue(e.target.value)} />
<button type="button" onClick={addItem}>Add</button>
</div>
}
function List({ items = new Map, onClick }) {
return <ul>
{Array.from(items.values(), item =>
<ListItem key={item.id} item={item} onClick={onClick(item.id)} />
)}
</ul>
}
function ListItem({ item = {}, onClick }) {
return <li onClick={onClick}>
{ item.done
? <s>{item.value}</s>
: item.value
}
</li>
}
ReactDOM.render(<Todo />, document.body)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.14.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.14.0/umd/react-dom.production.min.js"></script>
do more with less
Functional programming goes a long way in React. Using a curried update function we can take advantage of React's functional updates -
const update = (key, func) => m => // <-
new Map(m).set(key, func(m.get(key)))
function Todo() {
// ...
const addItem = event => {
const id = Date.now()
setItems(update(id, _ => ({ id, value, done: false }))) // <-
}
const toggleItem = id => event =>
setItems(update(id, item => ({ ...item, done: !item.done }))) // <-
// ...
}
but don't stop there
Avoid creating the todo item data by hand { id: ..., value: ..., done: ... }. Instead let's make an immutable TodoItem class to represent our data. A class also gives us an appropriate container for functions that would operate on our new data type -
class TodoItem {
constructor(id = 0, value = "", done = false) {
this.id = id
this.value = value
this.done = done
}
toggle() {
return new TodoItem(id, value, !this.done) // <- *new* data
}
}
Now our Todo component is unmistakable with its intentions -
function Todo() {
// ...
const [items, setItems] = useState(new Map)
const addItem = event => {
const id = Date.now()
setItems(update(id, _ => new TodoItem(id, value))) // <- new TodoItem
}
const toggleItem = id => event =>
setItems(update(id, item => item.toggle())) // <- item.toggle
// ...
}
In my code, I replace these values
const [items, setItem] = useState<string[]>([]);
const [value, setValue] = useState('')
const [error, setValue]= useState('')
to this
type Props = {
items?: string[],
value?: string,
error?: string
}
and then change the following setItem, setValue, setValue which causes the following error
import React, { useRef, useState } from "react";
import ReactDOM from "react-dom";
import Chip from "#material-ui/core/Chip";
import TextField from "#material-ui/core/TextField";
type Props = {
items?: string[],
value?: string,
error?: string
}
export const TagActions = (props:Props) => {
const { items, value, error } = props;
// const [items, setItem] = useState<string[]>([]);
// const [value, setValue] = useState('')
// const [error, setError]= useState('')
const divRef = useRef<HTMLDivElement>(null)
const handleDelete = (item:any) => {
console.log("handleDelete", item)
const result = items?.filter(i => i !== item)
setItem(result)
};
const handleItemEdit = (item:any) =>{
console.log("handleItemEdit", item)
const result = items?.filter(i => i !== item)
items = result // setItem(result)
value = item // setValue(item)
console.log("value", value)
};
const handleKeyDown = (evt:any) => {
if (["Enter", "Tab", ","].includes(evt.key)) {
evt.preventDefault();
var test = value?.trim();
if (test && isValid(test)) {
items?.push(test)
setValue("")
}
}
};
const isValid = (email:any)=> {
let error = null;
if (isInList(email)) {
error = `${email} has already been added.`;
}
if (!isEmail(email)) {
error = `${email} is not a valid email address.`;
}
if (error) {
setError(error);
return false;
}
return true;
}
const isInList = (email:any)=> {
return items?.includes(email);
}
const isEmail = (email:any)=> {
return /[\w\d\.-]+#[\w\d\.-]+\.[\w\d\.-]+/.test(email);
}
const handleChange = (evt:any) => {
setValue(evt.target.value)
// setError("")
};
const handlePaste = (evt:any) => {
evt.preventDefault();
var paste = evt.clipboardData.getData("text");
var emails = paste.match(/[\w\d\.-]+#[\w\d\.-]+\.[\w\d\.-]+/g);
if (emails) {
var toBeAdded = emails.filter((email:any) => !isInList(email));
setItem(toBeAdded)
}
};
return (
<>
<div>
<TextField id="outlined-basic" variant="outlined"
InputProps={{
startAdornment: items?.map(item => (
<Chip
key={item}
tabIndex={-1}
label={item}
onDelete={() => handleDelete(item)}
onClick={() => handleItemEdit(item)}
/>
)),
}}
ref={divRef}
value={value}
placeholder="Type or paste email addresses and press `Enter`..."
onKeyDown={(e) => handleKeyDown(e)}
onChange={(e) => handleChange(e)}
onPaste={(e) => handlePaste(e)}
/>
</div>
{error && <p className="error">{error}</p>}
</>
);
}
I am a beginner in react typescript, so I don't know how to fix this, Please give me a solution to fix this problem
While changing const to let may fix immediate errors in the console, I doubt this will give you the behaviour that you desire.
The main issue here is that you are mutating the value of props, which in general you should never do. Props are used to pass stateful data down from a parent to a child component. If you wish to update the state of this data from the child component, you should pass an update function using props as well. Below gives an example of what I mean by this by implementing the delete item function (no typescript, but hopefully it gets the idea across):
const ParentComponent = () => {
const [items, setItems] = useState(["item1", "item2", "item3"])
const deleteItem = (itemToDelete) => {
//here we use the functional update form of setState which is good practise when the new state depends on the old state
setItems((items) => items.filter((item) => item!==itemToDelete))
}
return <ChildComponent items={items}, onDeleteItem={deleteItem} />
}
const ChildComponent = ({items, onDeleteItem}) => {
//e.g. to delete item2 call
onDeleteItem("item2")
}
This is one of the more confusing patterns in React, but it is very important to get your head around. Only the component where state is declared should actually be updating that state - as it is the only place where you have access to the setState function.
I have this Component:
export const DashboardPage = () => {
const [mounted, setMounted] = useState(false);
const [currentNumber, setCurrentNumber] = useState(null);
const [insertedNumber, setInsertedNumber] = useState(null);
const [result, setResult] = useState(null);
const [arrayNumbers, setArrayNumbers] = useState([]);
useEffect(() => {
const rnd = randomNumber();
if (!mounted) {
setCurrentNumber(rnd);
setMounted(true);
}
}, [mounted]);
const handleChange = (e) => {
setInsertedNumber(e.target.value);
}
const handleClick = (e) => {
e.preventDefault();
arrayNumbers.push(insertedNumber);
setArrayNumbers(arrayNumbers);
setResult("Oops, ritenta! era " + currentNumber)
if (parseInt(insertedNumber) === parseInt(currentNumber)) {
setResult("EVVIVA hai vinto!");
}
}
const handleOnSubmit = (e) => {
e.preventDefault();
}
return (
<>
<h2>Storico tentativi: {arrayNumbers.map(e => e)}</h2>
<h2>La soluzione è: {currentNumber}</h2>
<Form onSubmit={handleOnSubmit}>
<Form.Group controlId="formGTN">
<Form.Label>Guess the number:</Form.Label>
<Form.Control type="text" onChange={handleChange} />
</Form.Group>
<Button variant="primary" type="submit" onClick={handleClick} label="Is it?" />
</Form>
<h1>{result}</h1>
</>
)
}
export default DashboardPage;
It is a simple "Guess The Number" play.
I have the issue that:
On very first loading of page, I can get into arrayNumbers the only one number I inserted.
From second and other attempts, when I CHANGE the value inside the input, so when I delete previous number, I get state update and I have the previous (and on) numbers. Like if "state" is "back" of one try.
I can see this behavious also in Chrome's Redux plugin. State is update ONLY a step other.
I could move the push into array on onChange, but... If I insert a number of 2 digits I get 2 insert on array (44 will be "4" and "4").
THank you!
You should never mutate the state and/or update the state without setState. In JavaScript, the Array.push() method mutates the original array. So, in your example, you mutate arrayNumbers but you should add a new element to the array without updating the original one.
Also, listening to the onSubmit event is enough, you don't have to add the onClick event to your submit button. You can move everything from handleClick to handleOnSubmit.
export const DashboardPage = () => {
const [mounted, setMounted] = useState(false);
const [currentNumber, setCurrentNumber] = useState(null);
const [insertedNumber, setInsertedNumber] = useState(null);
const [result, setResult] = useState(null);
const [arrayNumbers, setArrayNumbers] = useState([]);
useEffect(() => {
const rnd = randomNumber();
if (!mounted) {
setCurrentNumber(rnd);
setMounted(true);
}
}, [mounted]);
const handleChange = (e) => {
setInsertedNumber(e.target.value);
};
const handleOnSubmit = (e) => {
e.preventDefault();
setArrayNumbers([...arrayNumbers, insertedNumber]);
if (parseInt(insertedNumber, 10) === parseInt(currentNumber, 10)) {
setResult('EVVIVA hai vinto!');
} else {
setResult('Oops, ritenta! era ' + currentNumber);
}
};
return (
<>
<h2>Storico tentativi: {arrayNumbers.map((e) => e)}</h2>
<h2>La soluzione è: {currentNumber}</h2>
<Form onSubmit={handleOnSubmit}>
<Form.Group controlId="formGTN">
<Form.Label>Guess the number:</Form.Label>
<Form.Control type="text" onChange={handleChange} />
</Form.Group>
<Button variant="primary" type="submit" label="Is it?" />
</Form>
<h1>{result}</h1>
</>
);
};
For the above component, you might try these changes in the handleClick function:
const handleClick = (e) => {
e.preventDefault();
let newArr = [...arrayNumbers];
newArr.push(insertedNumber);
setArrayNumbers(newArr);
setResult('Oops, ritenta! era ' + currentNumber);
if (parseInt(insertedNumber) === parseInt(currentNumber)) {
setResult('EVVIVA hai vinto!');
}
};
I'm stuck trying to understand why my state won't update until I change the value in the text input twice (calling the handleChange function). What am I doing wrong here?
import React, {useEffect, useState} from "react";
export default function Typeahead(props){
const {list} = props;
const [colorList] = useState(list.map(element => element.toLowerCase()));
const [color,setColor] = useState();
const [showResults, setShowResults]= useState(false);
const [results,setResults]= useState();
let handleChange = (e) =>{
setShowResults(true);
setColor(e.target.value.toLowerCase());
const match = (colorList) =>{
return colorList.startsWith(color,0);
};
const matches = colorList.filter(match);
setResults((matches));
console.log(results);
console.log(showResults);
};
useEffect(() => {
//setResults(list.map(elements => elements.toLowerCase()));
}, [results]);
return(
<div>
<input type= "text" onChange={handleChange}/>
{showResults ?
<div>
{results.map((options) => {
return (
<option key={options} value={options}> {options}</option>
)
})}
</div>
: null }
</div>
);
}