I'm trying to build a form that is implemented with Email JS.
What I want to do?
After click the send button it should be reset. No checks in the checkbox, Price should reset to $0.00.
What's happening?
Checkbox are unchecked but Price won't reset to $0.00 after button clicked.
Tools I used
Tailwind
emailjs/browser
Vite
Here's the code that I've been working so far. This is what it will look like.
When I checked what's in e.target I got the Price (marked red) into this so I was assuming that e.target.reset() will work in this part too but it won't reset. I don't know why this won't reset when it's in e.target
import { useState } from 'react';
// EmailJS
import EmailJS from '#emailjs/browser';
const App = () => {
// EmailJS
const sendEmail = (e) => {
e.preventDefault();
EmailJS.sendForm(
'YOUR_SERVICE_ID', // Please put your service ID if you have Email JS.
'YOUR_TEMPLATE_ID', // Please put your template ID if you have Email JS.
e.target,
'YOUR_PUBLIC_KEY' // Please put your key if you have Email JS.
).then(
(result) => {
console.log(result.text);
},
(error) => {
console.log(error.text);
}
);
e.target.reset();
};
// Checkbox
const list = [
{
name: 'A',
price: 50,
},
{
name: 'B',
price: 50,
},
];
const [checked, setChecked] = useState(new Array(list.length).fill(false));
const [total, setTotal] = useState(0);
const toggle = (place) => {
const updatedChecked = checked.map((item, index) => {
if (index === place) {
return !item;
} else {
return item;
}
});
setChecked(updatedChecked);
const initialValue = 0;
const totalPrice = updatedChecked.reduce((acc, currentTorF, index) => {
if (currentTorF === true) {
return acc + list[index].price;
}
return acc;
}, initialValue);
setTotal(totalPrice);
};
const getTotalPrice = (price) => {
return `$${price.toFixed(2)} `;
};
// JSX
return (
<>
<div className='flex mx-36'>
<div className='flex-1'>
<form onSubmit={sendEmail} required>
<fieldset className='border border-solid rounded border-gray-300 p-3'>
<legend className='p-1'>Choice</legend>
<ul>
{list.map(({ name, price }, index) => {
return (
<li key={index} className='flex justify-between'>
<div className='p-1'>
<input
type='checkbox'
checked={checked[index]}
onChange={() => toggle(index)}
id={`linked-${index}`}
name='services'
value={name}
/>
<label htmlFor={`linked-${index}`} className='p-1'>
{name}
</label>
</div>
<div>{getTotalPrice(price)}</div>
</li>
);
})}
</ul>
</fieldset>
<fieldset className='flex justify-between p-8'>
<label htmlFor='total'>Price</label>
<input
type='text'
name='total'
value={getTotalPrice(total)}
className='w-16 border-none outline-none'
readOnly
/>
</fieldset>
{/* Send button */}
<button
className='table mx-auto border border-gray-300 rounded mt-4 px-6 py-1'
type='submit'
>
Send
</button>
</form>
</div>
</div>
</>
);
};
export default App;
The reset method won't work because the form is not in control over the values in the inputs, React is.
I would recommend that you change some things up. Move your list into a state and add the checked property to each list item object. This will keep your data grouped.
The total value is always calculated based on the checked items in the list. Since we've now grouped added checked property to each object, we can calculate the total by looping over the list. Use useMemo for the total. The reason for this is that you want to calculate total after every time that list has been updated. With useMemo you can store each calculation and reuse the stored total whenever the same list state occurs.
import { useState, useMemo } from 'react';
// EmailJS
import EmailJS from '#emailjs/browser';
const App = () => {
const [list, setList] = useState([
{
name: 'A',
price: 50,
checked: false
},
{
name: 'B',
price: 50,
checked: false
},
]);
const total = useMemo(() => {
const initialValue = 0;
return list.reduce((acc, cur) => {
if (cur.checked === true) {
acc += cur.price;
}
return acc;
}, initialValue);
}, [list]);
const resetForm = () => {
setList(prevState =>
prevState.map(listItem => ({
...listItem,
checked: false
}))
);
};
const sendEmail = (e) => {
e.preventDefault();
EmailJS.sendForm(
'YOUR_SERVICE_ID', // Please put your service ID if you have Email JS.
'YOUR_TEMPLATE_ID', // Please put your template ID if you have Email JS.
e.target,
'YOUR_PUBLIC_KEY' // Please put your key if you have Email JS.
).then(
(result) => {
console.log(result.text);
},
(error) => {
console.log(error.text);
}
);
resetForm();
};
const toggle = (toggleIndex) => {
setList(prevState =>
prevState.map((listItem, index) => {
if (toggleIndex !== index) {
return listItem;
}
return ({
...listItem,
checked: !listItem.checked
});
})
);
};
const getTotalPrice = (price) => {
return `$${price.toFixed(2)} `;
};
// JSX
return (
<>
<div className='flex mx-36'>
<div className='flex-1'>
<form onSubmit={sendEmail} required>
<fieldset className='border border-solid rounded border-gray-300 p-3'>
<legend className='p-1'>Choice</legend>
<ul>
{list.map(({ name, price, checked }, index) => {
return (
<li key={index} className='flex justify-between'>
<div className='p-1'>
<input
type='checkbox'
checked={checked}
onChange={() => toggle(index)}
id={`linked-${index}`}
name='services'
value={name}
/>
<label htmlFor={`linked-${index}`} className='p-1'>
{name}
</label>
</div>
<div>{getTotalPrice(price)}</div>
</li>
);
})}
</ul>
</fieldset>
<fieldset className='flex justify-between p-8'>
<label htmlFor='total'>Price</label>
<input
type='text'
name='total'
value={getTotalPrice(total)}
className='w-16 border-none outline-none'
readOnly
/>
</fieldset>
{/* Send button */}
<button
className='table mx-auto border border-gray-300 rounded mt-4 px-6 py-1'
type='submit'
>
Send
</button>
</form>
</div>
</div>
</>
);
};
export default App;
Related
Hello everyone and thank you for reading this! Here is my problem that i can't solve:
My application has the following functionality:
There are 2 inputs, then a button, when clicked, 2 more inputs appear and a button to send data from all inputs to the console, however, in the additional field, one input is required. This is where my problem arises: now, if I called additional inputs and filled in all the data, they are transferred to the console, if I didn’t fill in the required field, an error message goes to the console, BUT. I also need, in the event that I did NOT call additional inputs, the data of 2 basic inputs was transferred to the console. At the moment I can't figure it out.
import React, { useState } from "react";
import ReactDOM from "react-dom/client";
import produce from "immer";
const FunctionalBlock = ({
id,
idx,
isDeleted,
toggleBlockState,
additionalValue,
additionalTitle,
setNewBlock,
index,
}) => {
return (
<div
style={{
display: "flex",
maxWidth: "300px",
justifyContent: "space-between",
}}
>
{!isDeleted ? (
<React.Fragment>
<strong>{idx}</strong>
<input
type="text"
value={additionalTitle}
onChange={(e) => {
const additionalTitle = e.target.value;
setNewBlock((currentForm) =>
produce(currentForm, (v) => {
v[index].additionalTitle = additionalTitle;
})
);
}}
/>
<input
type="text"
value={additionalValue}
onChange={(e) => {
const additionalValue = e.target.value;
setNewBlock((currentForm) =>
produce(currentForm, (v) => {
v[index].additionalValue = additionalValue;
})
);
}}
/>
<button onClick={toggleBlockState}>now delete me</button>
</React.Fragment>
) : (
<button onClick={toggleBlockState}>REVIVE BLOCK</button>
)}
</div>
);
};
const Application = () => {
const [newBlock, setNewBlock] = useState([]);
const [firstInput, setFirstInput] = useState("");
const [secondInput, setSecondInput] = useState("");
const getNewBlock = (idx) => ({
id: Date.now(),
idx,
isDeleted: false,
additionalValue: "",
additionalTitle: "",
});
const toggleIsDeletedById = (id, block) => {
if (id !== block.id) return block;
return {
...block,
isDeleted: !block.isDeleted,
};
};
const createOnClick = () => {
const block = getNewBlock(newBlock.length + 1);
setNewBlock([...newBlock, block]);
};
const toggleBlockStateById = (id) => {
setNewBlock(newBlock.map((block) => toggleIsDeletedById(id, block)));
};
const showInputData = () => {
newBlock.map((item) => {
if (item.additionalTitle.length < 3) {
console.log("it is less than 3");
} else if (!item.additionalTitle && !item.additionalValue) {
console.log(firstInput, secondInput);
} else {
console.log(
firstInput,
secondInput,
item.additionalTitle,
item.additionalValue
);
}
});
};
return (
<div>
<div>
<input
type="text"
value={firstInput}
onChange={(e) => {
setFirstInput(e.target.value);
}}
/>
<input
type="text"
value={secondInput}
onChange={(e) => {
setSecondInput(e.target.value);
}}
/>
</div>
<div>
<button onClick={createOnClick}>ADD NEW INPUTS</button>
</div>
<div>
{newBlock.map((block, index) => (
<FunctionalBlock
key={index}
{...block}
toggleBlockState={() => toggleBlockStateById(block.id)}
setNewBlock={setNewBlock}
index={index}
/>
))}
</div>
<button onClick={showInputData}>send data</button>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Application />);
Here is this code on sandbox for those who decided to help me. Thank you!
https://codesandbox.io/s/vigilant-booth-xnef6t
I have 2 arrays that I used the map() function to add to an array, but the problem I'm having is they are being grouped by name and grouped by review but I want them to return name + review independently of each other when I click submit on my form. Here's an example of what's happening:
I want it so David's review ("great movie") is separate from Daniel's review ("my favorite").
I've tried all sorts of things to no avail. Here is my code:
import { Button, Form, Input } from "reactstrap";
import Stars from "./stars";
export default function ReviewForm() {
const [reviewinput, setReviewInput] = useState("");
const [reviewarray, setReviewArray] = useState([]);
const [nameinput, setNameInput] = useState("");
const [namearray, setNameArray] = useState([])
const [starinput, setStarInput] = useState();
const [stararr, setStarArr] = useState(0)
const onChange = (e) => {
setReviewInput(e.target.value);
};
const onChangeName = (e) => {
setNameInput(e.target.value);
};
const onSubmit = (e) => {
e.preventDefault();
console.log('submitted');
if (reviewinput) {
reviewarray.push(reviewinput);
setReviewArray(reviewarray);
}
if (nameinput) {
namearray.push(nameinput);
setNameArray(namearray);
}
if (starinput) {
stararr.push(starinput);
setStarArr(stararr);
}
setReviewInput('');
setNameInput('');
setStarInput(0)
};
console.log(reviewarray);
return (
<div className="form-container">
<Stars setStar={setStarArr} />
<Form onSubmit={onSubmit}>
<Input
className="form-control" type="text"
placeholder="Enter your name"
value={nameinput}
onChange={onChangeName}
/>
<Input
className="form-control"
type="textarea"
placeholder="Enter your review"
value={reviewinput}
onChange={onChange}
/>
<br></br>
<Button type="submit" className="btn btn-primary">
Submit
</Button>
<br></br>
<br></br>
<div className="card-header border boorder-dark">
<h5>Reviews</h5>
</div>
<div className="card-body border border-secondary">
{namearray.map((name, i) => <p key={i}>{name}</p>)}
<br></br>
{reviewarray.map((review, i) => <p key={i}>{review}</p>)}
<p>I rate it this movie {stararr} stars!</p>
</div>
</Form>
</div>
);
}
// STAR COMPONENT \\
import React, { useState } from "react";
import { FaStar} from 'react-icons/fa'
const Stars = ({ setStar }) => {
const [rating, setRating] = useState(0);
const [hover, setHover] = useState(null);
const handleClick = (ratingValue) => {
setRating(ratingValue);
setStar(ratingValue);
};
return (
<div>
{[...Array(5)].map((star, i) => {
const ratingValue = i + 1;
return (
<label key={i}>
<input
type="radio"
name="rating"
value={ratingValue}
onClick={() => handleClick(ratingValue)}
/>
<FaStar
className="star"
color={ratingValue <= (hover || rating) ? "gold" : "lightgray"}
size={20}
onMouseEnter={() => setHover(ratingValue)}
onMouseLeave={() => setHover(null)}
/>
</label>
);
})}
<p>I rate this movie {rating + " stars"}</p>
</div>
);
};
export default Stars;```
It would be simpler to do this using an object instead of combining two arrays.
Make each review an object that contains a review, name, and your stars like so:
{
name: 'a',
review: 'good',
stars: 5
}
This way you could just use one array and push that object instead.
The reason your stars wasn't updating to 0 is because in your ./stars file you made a new stars state when you could have just re-used the one from your main component. Other than that, your code was fine.
updated code:
main file
import React, { useState } from "react";
import { Button, Form, Input } from "reactstrap";
import Stars from "./stars";
export default function ReviewForm() {
const [reviewinput, setReviewInput] = useState("");
const [reviewarray, setReviewArray] = useState([]);
const [nameinput, setNameInput] = useState("");
const [stararr, setStarArr] = useState(0);
const onChange = (e) => {
setReviewInput(e.target.value);
};
const onChangeName = (e) => {
setNameInput(e.target.value);
};
const onSubmit = (e) => {
e.preventDefault();
console.log("submitted");
const review = {};
if (reviewinput) {
review.review = reviewinput;
}
if (nameinput) {
review.name = nameinput;
}
review.stars = stararr;
setReviewArray([...reviewarray, review]);
setReviewInput("");
setNameInput("");
setStarArr(0);
const form = e.target
form.reset() /* to reset radio buttons to initial */
};
console.log(reviewarray);
return (
<div className="form-container">
<Form onSubmit={onSubmit}>
<Stars setStar={setStarArr} stararr={stararr} />
<Input
className="form-control"
type="text"
placeholder="Enter your name"
value={nameinput}
onChange={onChangeName}
required
/>
<Input
className="form-control"
type="textarea"
placeholder="Enter your review"
value={reviewinput}
onChange={onChange}
required
/>
<br></br>
<Button type="submit" className="btn btn-primary">
Submit
</Button>
<br></br>
<br></br>
<div className="card-header border boorder-dark">
<h5>Reviews</h5>
</div>
<div className="card-body border border-secondary">
<br></br>
{reviewarray.map(({ review, name, stars }, i) => (
<div key={i}>
<p>name: {name}</p>
<p>review: {review}</p>
<p>stars: {stars}</p>
</div>
))}
<p>I rate it this movie {stararr} stars!</p>
</div>
</Form>
</div>
);
}
star component
import React, { useState } from "react";
import { FaStar } from "react-icons/fa";
const Stars = ({ setStar, stararr }) => {
const [hover, setHover] = useState(null);
const handleClick = (ratingValue) => {
setStar(ratingValue);
};
return (
<div>
{[...Array(5)].map((star, i) => {
const stararr = i + 1;
return (
<label key={i}>
<input
type="radio"
name="rating"
value={stararr}
onClick={() => handleClick(stararr)}
/>
<FaStar
className="star"
color={stararr <= (hover || stararr) ? "gold" : "lightgray"}
size={20}
onMouseEnter={() => setHover(stararr)}
onMouseLeave={() => setHover(null)}
/>
</label>
);
})}
<p>I rate this movie {stararr + " stars"}</p>
</div>
);
};
export default Stars;
Edit: To incorporate the stars input as well
For your stars component I just replaced wherever you had ratings to your original stars value.
Like Bas bas told you, I think it's better to combine the name and the review in the same item in the reviews array.
I also wrote it a bit shorter and understandable:
import { Button, Form, Input } from "reactstrap";
import Stars from "./stars";
export default function ReviewForm() {
const [nameInput, setNameInput] = useState("");
const [reviewInput, setReviewInput] = useState("");
const [starInput, setStarInput] = useState(0);
const [reviews, setReviews] = useState([]);
const onSubmit = (e) => {
e.preventDefault();
if (!nameInput || !nameInput.length || !reviewInput || !reviewInput.length || isNaN(starInput)) {
return;
}
setReviews((prev) => [
...prev,
{
name: nameInput,
review: reviewInput,
rating: starInput
}
]);
clearForm();
};
const clearForm = () => {
setNameInput('');
setReviewInput('');
setStarInput(0);
};
return (
<div className="form-container">
<Stars setStar={setStarInput} />
<Form onSubmit={onSubmit}>
<Input
className="form-control" type="text"
placeholder="Enter your name"
value={nameInput}
onChange={(e) => setNameInput(e.target.value)}
/>
<Input
className="form-control"
type="textarea"
placeholder="Enter your review"
value={reviewInput}
onChange={(e) => setReviewInput(e.target.value)}
/>
<br></br>
<Button type="submit" className="btn btn-primary">
Submit
</Button>
<br></br>
<br></br>
<div className="card-header border boorder-dark">
<h5>Reviews</h5>
</div>
<div className="card-body border border-secondary">
{reviews.map(({ name, review }, i) =>
<p key={i}>{name} - {review}</p>
)}
</div>
</Form>
</div>
);
}
There are many options to achieve this. What you want to do is to zip() (at least Python developers would use this term), so that means you want to group together the first, second, third, ... element of the two given arrays. Then you could map() over the resulting array and display the values as you please.
For your example you could just use something like the following using map():
const names = ["Thomas", "Peter", "Tom", "Mark", "John"];
const reviews = [
"Well, this was shit.",
"Love me some sharks",
"Sharknadoooo!",
"It's a terrible joy.",
"I've seen a peanut stand, I've heard a rubber band, I've seen a needle wink it's eye, but I ain't never seen a Shark fly",
];
const result = names.map((name, idx) => ({ name: name, review: reviews[idx] }));
console.log(result);
.as-console-wrapper { max-height: 100% !important; top: 0; }
In a more general sense with any number of arrays you could use a generator function although you could do it without one. But this is a quite convenient and simple way to achieve what you want which generates the values as you need them when using for ... of.
const names = ["Thomas", "Peter", "Tom", "Mark", "John"]
const reviews = ["Well, this was shit.", "Love me some sharks", "Sharknadoooo!", "It's a terrible joy.", "I've seen a peanut stand, I've heard a rubber band, I've seen a needle wink it's eye, but I ain't never seen a Shark fly"]
/**
* Zips any number of arrays. It will always zip() the largest array returning undefined for shorter arrays.
* #param {...Array<any>} arrays
*/
function* zip(...arrays){
const maxLength = arrays.reduce((max, curIterable) => curIterable.length > max ? curIterable.length: max, 0);
for (let i = 0; i < maxLength; i++) {
yield arrays.map(array => array[i]);
}
}
// put them in a array
const test = [...zip(names, reviews)]
console.log(test);
// or lazy generate the values
for (const [name, review] of zip(names, reviews)) {
console.log(`${name}: ${review}`);
}
.as-console-wrapper { max-height: 100% !important; top: 0; }
In this React code what I'm trying to do is getting all items from the list that match with what type in the text input that's in the setManName function (also there is one in setModeName function). It works, but when I delete the text input and start over, the items disappear and will not appear anymore, not showing on the screen unless I reload the page again and start over again. I am using inludes() method, which works fine, but once I delete a letter or whole word and start over again it doesn't work. What's the problem here? Should I be using a different approach? Like another useEffect or something?
import React, { useState, useEffect } from "react";
import "./style.css";
export default function App() {
const [items, setItems] = useState([])
const [openFilterCt, setOpenFilterCt] = useState(false)
const [term1, setTerm1] = useState()
const [term2, setTerm2] = useState()
useEffect(() => {
fetch("https://private-anon-af560a53c6-carsapi1.apiary-mock.com/cars")
.then(res => res.json())
.then(data => {
setItems(data)
})
}, [])
function setManName(e) {
setTerm1(e.target.value);
let u = items.filter(item => {
**return item.make.includes(e.target.value)**
})
setItems(u)
}
function setModName(e) {
setTerm2(e.target.value);
let u = items.filter(item => {
**return item.model.includes(e.target.value)**
})
setItems(u)
}
function hi() {
setOpenFilterCt(!openFilterCt)
}
return (
<div>
<h1>React Search & Filter</h1>
<div>
<h3 onClick={hi}>Filter</h3>
<div className={openFilterCt ? "show" : "hide"}>
<label>
Name of manufacturer: <input type="text" value={term1} onChange={setManName} />
</label>
<br />
<label>
Name of model: <input type="text" value={term2} onChange={setModName} />
</label>
</div>
</div>
{items.slice(0, 50).map((a, index) => {
return (
<div key={index} style={{border: "1px solid black", margin: "10px", padding: "5px"}}>
<p>Manufacturer: {a.make[0].toUpperCase() + a.make.slice(1)}</p>
<p>Model: {a.model}</p>
</div>
)
})}
</div>
);
}
You are overwriting the items object, so any items not in a search will not show up even after deleting characters. This solution will dynamically filter the items, rather than removing them from the array.
Additionally, you should provide a default value to the term1 and term2 states. Without a default value, the inputs are switching from uncontrolled to controlled inputs, a practice that is discouraged in React.
See this Codesandbox.
import React, { useState, useEffect } from "react";
import "./style.css";
export default function App() {
const [items, setItems] = useState([]);
const [openFilterCt, setOpenFilterCt] = useState(false);
const [term1, setTerm1] = useState("");
const [term2, setTerm2] = useState("");
useEffect(() => {
fetch("https://private-anon-af560a53c6-carsapi1.apiary-mock.com/cars")
.then((res) => res.json())
.then((data) => {
setItems(data);
});
}, []);
function setManName(e) {
setTerm1(e.target.value);
}
function setModName(e) {
setTerm2(e.target.value);
}
function filterItems(item) {
if (term1 && !item.make.includes(term1)) return false;
if (term2 && !item.model.includes(term2)) return false;
return true;
}
function hi() {
setOpenFilterCt(!openFilterCt);
}
return (
<div>
<h1>React Search & Filter</h1>
<div>
<h3 onClick={hi}>Filter</h3>
<div className={openFilterCt ? "show" : "hide"}>
<label>
Name of manufacturer:{" "}
<input type="text" value={term1} onInput={setManName} />
</label>
<br />
<label>
Name of model:{" "}
<input type="text" value={term2} onInput={setModName} />
</label>
</div>
</div>
{items
.filter(filterItems)
.slice(0, 50)
.map((a, index) => {
return (
<div
key={index}
style={{
border: "1px solid black",
margin: "10px",
padding: "5px"
}}
>
<p>Manufacturer: {a.make[0].toUpperCase() + a.make.slice(1)}</p>
<p>Model: {a.model}</p>
</div>
);
})}
</div>
);
}
I have just fixed it for your model search, you can do the same for the manufacturer search. There may be more optimal ways, but this is something I worked it out.
What you need to do is preserve your original list. .filter() actually changes the original list, and when the response is blank, the original data is gone. So I just preserved the old data
const [orItem, setOrItems] = useState([]);
const prevList = orItem;
function setModName(e) {
setTerm2(e.target.value);
let u = prevList.filter((item) => {
return item.model.includes(e.target.value);
});
setItems(u);
}
You can see you code in action here for model search:
https://codesandbox.io/s/elegant-mcclintock-46ncr?file=/src/App.js
First, you should not modify the original array items. You need to create another one(another state variable) filteredItems so you can reset to the original state, also I believe there is another error here item.model.includes(e.target.value), it will always return false if the text is empty.
function setManName(e) {
setTerm1(e.target.value);
if(e.target.value){
let u = items.filter(item => {
return item.make.includes(e.target.value)
})
setFilteredItems(u)
}else{
setItems(items)
}
}
Also useEffect hook should be something like this:
useEffect(() => {
fetch("https://private-anon-af560a53c6-carsapi1.apiary-mock.com/cars")
.then(res => res.json())
.then(data => {
setItems(data)
setFilteredItems(data)
})
}, [])
And make sure to map over filteredItems.
The problem here is that you are resetting the value that you receive from the given URL. You should be maintaining a separate list for visibility you could go by approach 1. given below. Its the best I could do without modifying lots of your code however this is typically an over/mis use of states. Remember React is called react because of its amazing capability to react when the state changes.
The Approach2 realizes just that, you can be smart with the filter and alter it as you need. your search to behave.
// Approach 1
import React, { useState, useEffect } from "react";
// import "./style.css";
export default function App() {
const [items, setItems] = useState([]);
const [visibleItems, setVisibleItems] = useState([]);
const [openFilterCt, setOpenFilterCt] = useState(false)
const [term1, setTerm1] = useState()
const [term2, setTerm2] = useState()
useEffect(() => {
fetch("https://private-anon-af560a53c6-carsapi1.apiary-mock.com/cars")
.then(res => res.json())
.then(data => {
setItems(data);
setVisibleItems(data);
})
}, [])
function setManName(e) {
// setTerm1(e.target.value);
let u = items.filter(item => {
if(e.target.value){
return item.make.includes(e.target.value)
}
return true;
})
setVisibleItems(u)
}
function setModName(e) {
// setTerm2(e.target.value);
let u = items.filter(item => {
return item.model.includes(e.target.value)
})
setVisibleItems(u)
}
function hi() {
setOpenFilterCt(!openFilterCt)
}
return (
<div>
<h1>React Search & Filter</h1>
<div>
<h3 onClick={hi}>Filter</h3>
<div className={openFilterCt ? "show" : "hide"}>
<label>
Name of manufacturer: <input type="text" value={term1} onChange={setManName} />
</label>
<br />
<label>
Name of model: <input type="text" value={term2} onChange={setModName} />
</label>
</div>
</div>
{visibleItems.slice(0, 50).map((a, index) => {
return (
<div key={index} style={{border: "1px solid black", margin: "10px", padding: "5px"}}>
<p>Manufacturer: {a.make[0].toUpperCase() + a.make.slice(1)}</p>
<p>Model: {a.model}</p>
</div>
)
})}
</div>
);
}
// Approach2
import React, { useState, useEffect } from "react";
// import "./style.css";
export default function App() {
const [items, setItems] = useState([]);
const [openFilterCt, setOpenFilterCt] = useState(false);
const [term1, setTerm1] = useState("");
const [term2, setTerm2] = useState("");
useEffect(() => {
fetch("https://private-anon-af560a53c6-carsapi1.apiary-mock.com/cars")
.then((res) => res.json())
.then((data) => {
setItems(data);
});
}, []);
function setManName(e) {
setTerm1(e.target.value);
}
function setModName(e) {
setTerm2(e.target.value);
}
function hi() {
setOpenFilterCt(!openFilterCt);
}
return (
<div>
<h1>React Search & Filter</h1>
<div>
<h3 onClick={hi}>Filter</h3>
<div className={openFilterCt ? "show" : "hide"}>
<label>
Name of manufacturer:{" "}
<input type="text" value={term1} onChange={setManName} />
</label>
<br />
<label>
Name of model:{" "}
<input type="text" value={term2} onChange={setModName} />
</label>
</div>
</div>
{items
.filter((item) => {
return item.make.includes(term1) && item.model.includes(term2);
})
.slice(0, 50)
.map((a, index) => {
return (
<div
key={index}
style={{
border: "1px solid black",
margin: "10px",
padding: "5px"
}}
>
<p>Manufacturer: {a.make[0].toUpperCase() + a.make.slice(1)}</p>
<p>Model: {a.model}</p>
</div>
);
})}
</div>
);
}
I have a list in React where I'm using .map to render a loop from an array of elements.
The radio inputs are working perfectly, everyone is independent of the others, but I can't do the same for Select.
The Select area is changing in every field, I want it to change to it specified field like I did for the options.
I tried to re-use the same handleChange that I used in the radios for it and instead I had this error.
Cannot set property 'animation' of undefined
In this line
newAnimations[indexurl].animation = name;
How do I manage to separate the selects ?
Note: This code is working fine, but it's updating the value on every field.
const onChange = (animations) => {
setAnimations(animations);
console.log(`Animation selected:`, animations);
};
Code :
https://codesandbox.io/s/dank-violet-jibzh?file=/src/App.js:1065-1194
import React, { useState } from "react";
import Select from "react-select";
export default () => {
const animationsList = [
{ value: "animation0", label: "Dance" },
{ value: "animation1", label: "Flip" },
{ value: "animation2", label: "Salsa" }
];
const reactList = [
{
id: "14444",
selected: "layout3",
animation: "animation1"
},
{
id: "24444",
selected: "layout3",
animation: "animation2"
},
{
id: "34444",
selected: "layout3",
animation: "animation1"
}
];
const [links, setLinks] = useState(reactList);
const [animations, setAnimations] = useState(animationsList[0]);
const handleChangeSelectedReact = (indexurl, layout) => {
const cloneLinks = [...links];
cloneLinks[indexurl].selected = layout;
setLinks(cloneLinks);
console.log(cloneLinks);
};
/* const onChange = (animations) => {
setAnimations(animations);
console.log(`Animation selected:`, animations);
};*/
const onChange = (indexurl, name) => {
const newAnimations = [...links];
newAnimations[indexurl].animation = name;
setAnimations(newAnimations);
console.log(newAnimations);
};
return (
<div>
<ul>
<div>
{links.map((url, indexurl) => (
<li key={url.id}>
<div>
<Select
options={animationsList}
onChange={onChange}
value={animations}
/>
<p />{" "}
<input
type="radio"
id={url.id}
name={url.id}
value="layout1"
checked={url.selected === "layout1"}
onChange={() =>
handleChangeSelectedReact(indexurl, "layout1")
}
/>
<label for="huey">Option 1</label>
</div>
<div>
<input
type="radio"
id={url.id}
name={url.id}
value="layout2"
checked={url.selected === "layout2"}
onChange={() =>
handleChangeSelectedReact(indexurl, "layout2")
}
/>
<label for="dewey">Option 2</label>
</div>
<div>
<input
type="radio"
id={url.id}
name={url.id}
value="layout3"
checked={url.selected === "layout3"}
onChange={() =>
handleChangeSelectedReact(indexurl, "layout3")
}
/>
<label for="louie">Option 3</label>
</div>
<br />
</li>
))}
</div>
</ul>
</div>
);
};
Multiple Selects share the same state, resulting in updates the value on every field. So we need to wrap Select into components and maintain their own state independently:
function SelectItem({ list }) {
const [value, setValue] = React.useState(list[0]);
const onChange = (newValue) => {
setValue(newValue);
};
return <Select options={list} onChange={onChange} value={value} />;
}
...
...
return (
<div>
{links.map((url, indexurl) => (
<SelectItem list={animationsList} />
))}
</div>
);
...
...
Update
Modify the links when select is selected:
function SelectItem(props) {
const [value, setValue] = React.useState(props.list[0]);
return (
<Select
options={props.list}
onChange={(newValue) => {
setValue(newValue);
props.onChange(newValue.value);
}}
value={value}
/>
);
}
...
...
const onChange = (index, animation) => {
const cloneLinks = [...links];
cloneLinks[index].animation = animation;
setLinks(cloneLinks);
console.log(cloneLinks);
};
return (
<div>
{links.map((url, indexurl) => (
<SelectItem
onChange={(animation) => onChange(indexurl, animation)}
list={animationsList}
/>
))}
</div>
);
...
...
Full code: https://codesandbox.io/s/happy-cloud-m6z99?file=/src/App.js:77-298
I have set up a checkbox filter. I am filtering by brand or type. With my current code I can filter either by brand or type but not brand and type.
I want to be able to filter by brand and type.
I thought I had figured this out but lack of sleep is killing me and I can't seem to wrap my head around this problem at the moment so any help would be much appreciated.
My current code:
guns.jsx
const Guns = (props) => {
const { guns } = props
if (!guns) {
return (
<Layout>
<div className="flex justify-center items-center min-h-vh">
<Image src="/loading.gif" width={500} height={300} />
</div>
</Layout>
)
}
const [checkedInputs, setCheckedInputs] = useState({})
const handleInputChange = (event) => {
setCheckedInputs({ ...checkedInputs, [event.target.value]: event.target.checked })
}
useEffect(() => {
console.log('Checked Inputs', checkedInputs)
}, [checkedInputs])
// Get categories for the Gun filter.
const getCategories = () => {
const findCategories = guns.map(gun => {
return gun.Type
})
const filterCategories = findCategories.filter((category, index) => findCategories.indexOf(category) === index).sort()
const categories = filterCategories.map((cat, index) => {
return {
categories: {
catID: index,
name: cat
}
}
})
return categories
}
//Get brands for the Gun filter.
const getBrands = () => {
const findBrands = guns.map(gun => {
return gun.Make
})
const filterBrands = findBrands.filter((brand, index) => findBrands.indexOf(brand) === index).sort()
const brands = filterBrands.map((brand, index) => {
return {
brands: {
brandID: index,
name: brand
}
}
})
return brands
}
return (
<Layout>
<div className="flex mx-96">
<div className="w-1/4">
<GunFilter categories={getCategories()} brands={getBrands()} handleInputChange={handleInputChange} checkedInputs={checkedInputs} />
</div>
<div className="w-3/4">
<div className="grid grid-cols-3 gap-2 lg:my-12 lg:justify-center">
{guns.map(gun => {
if (gun.ImageCount > 1) {
// If no options boxes selected
if (Object.keys(checkedInputs).length < 1 || Object.keys(checkedInputs).every(value => checkedInputs[value] === false)) {
return <GunProductCard gun={gun} />
}
for (const [key, value] of Object.entries(checkedInputs)) {
if (value === true) {
if (key === gun.Type || key === gun.Make) {
return (
<GunProductCard gun={gun} />
)
}
}
}
}
})}
</div>
</div>
</div>
</Layout >
)
}
export async function getStaticProps() {
const res = await fetch(process.env.GUNTRADER_API)
const data = await res.json()
const guns = data.Guns
return {
props: {
guns
},
revalidate: 600
}
}
GunFilter.jsx
const GunFilter = (props) => {
const { categories, brands, handleInputChange, checkedInputs } = props
return (
<div className="text-black my-24">
<h4 className="p-2 border-b-2 border-r-2 font-bold">Filter By</h4>
<div className="p-2 my-2 border-r-2 border-b-2">
<h5 className="mb-2 font-semibold">Category</h5>
{categories.map(cat => {
return (
<div>
<input type="checkbox" id={cat.categories.name} value={cat.categories.name} checked={checkedInputs[cat.categories.name]} onChange={handleInputChange} />
<label key={cat.categories.name} className="ml-2" htmlFor={cat.categories.name}>{cat.categories.name}</label>
</div>
)
})}
</div>
<div className="p-2 my-2 border-r-2 border-b-2">
<h5 className="mb-2 font-semibold">Brand</h5>
{brands.map(brand => {
return (
<div>
<input type="checkbox" id={brand.brands.name} value={brand.brands.name} checked={checkedInputs[brand.brands.name]} onChange={handleInputChange} />
<label key={brand.brands.name} className="ml-2" htmlFor={brand.brands.name}>{brand.brands.name}</label>
</div>
)
})}
</div>
</div>
)
}
export default GunFilter