I was creating a product page for one of my e-commerce projects. And then I came into an issue of building a color and size selection component. I searched a lot on developer's best friend "Google" but could not find any optimal solution. After few exploration and testing I came up with this code.
Basic Setup
1.React
2.Tailwind
Preparing the data
Let's say we have an array of sizes for a particular product fetched from the db. We need to convert it to an object containing an id which will be used to map input to label and the value.
const productSize = ["S", "M", "X", "XXL"];
//map
const sizeData = productSize?.map((item) => ({
id: `input_${item}`,
value: item,
}));
Defining States
I defined a state for tracking the state of radio values. If you are using multiple radio buttons , you should create multiple states
let [sizeValue, setSizeValue] = useState("");
Rendering the component
Map through the sizeData array and render each size. I am using tailwind css for styling which might make the code a bit messy.
Radio input is set to invisible so that the label is the clickable element. input id is mapped to label for.
<div className="main max-w-lg mx-auto">
<div className="grid grid-cols-4 gap-8 gap-y-4">
{sizeData?.map(({ id, value }) => (
<div className="-mt-2" key={id}>
<input
id={id}
className="invisible radio_custom"
type="radio"
value={value}
checked={sizeValue === { sizeValue }}
onChange={(e) => setSizeValue(e.target.value)}
/>
<label htmlFor={id} className="radio_custom_label">
<div
className={`border border-gray-300 py-3 text-center cursor-pointer "
}`}
>
{value}
</div>
</label>
</div>
))}
</div>
</div>
Issues
Now, I have few issues with the above code. When a button is clicked , the other clicked buttons stays active. This doesn't give the toggle functionality that we require. The state is changing but the border on "onClick" persists. So even if the the state is changing, border stays on the element.
What we need is to remove the border from all element and add border to the "target" element.
onChange={(e) => {
const nodes = e.target.parentElement.parentElement.childNodes;
for (let i = 0; i < nodes.length; i++) {
nodes[i].lastChild.firstChild.classList.remove("show_border");
}
e.target.nextSibling.firstChild.classList.toggle("show_border");
return setSizeValue(e.target.value);
}}
Final Code
I had to refactored the code like three times๐. Here is the final code.
import React, { useState } from "react";
const Main = () => {
const productSize = ["S", "M", "X", "XXL"];
//map
const sizeData = productSize?.map((item) => ({
id: `input_${item}`,
value: item,
}));
let [sizeValue, setSizeValue] = useState("");
console.log(sizeValue);
return (
<div className="main max-w-lg mx-auto">
<div className="grid grid-cols-4 gap-8 gap-y-4">
{sizeData?.map(({ id, value }) => (
<div className="-mt-2" key={id}>
<input
id={id}
className="invisible radio_custom"
type="radio"
value={value}
checked={sizeValue === { sizeValue }}
onChange={(e) => {
const nodes = e.target.parentElement.parentElement.childNodes;
for (let i = 0; i < nodes.length; i++) {
nodes[i].lastChild.firstChild.classList.remove("show_border");
}
e.target.nextSibling.firstChild.classList.toggle("show_border");
return setSizeValue(e.target.value);
}}
/>
<label htmlFor={id} className="radio_custom_label">
<div
className={`border border-gray-300 py-3 text-center cursor-pointer "
}`}
>
{value}
</div>
</label>
</div>
))}
</div>
</div>
);
};
export default Main;
I used tailwind so no need to add css code. Only extra one css class is required
.show_border {
border: 1px solid #2b2b2b !important;
}
Final Result
Final Notes
There are multiple ways of doing this. If you have any suggestion or code improvement please do share. Feel free to connect. It's lively to make new friends.๐
Top comments (0)