Ever wondered how e-commerce sites manage all the possible combinations of a product? A t-shirt might come in 4 sizes, 5 colors, and 3 styles. Manually listing every single one would be a nightmare.
Today, we'll build a clean, interactive component in vanilla JavaScript that solves this exact problem. It will take a set of options and dynamically generate every possible combination, updating in real-time as you make selections.
Here's a sneak peek of what we'll be building:
Ready? Let's dive in.
1. The HTML: A Simple Foundation
First, we need a basic structure to hold our options and the results. The HTML is straightforward: two containers are all we need.
<!-- permutations.html -->
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- We'll use Tailwind CSS for styling -->
<link href="./index.css" rel="stylesheet">
<script type="module" src="./permutations.js"></script>
</head>
<body>
<div class="p-8 bg-gray-50 min-h-screen">
<div class="max-w-4xl mx-auto">
<header class="mb-8">
<h1 class="text-3xl font-bold">Product Variant Combinations</h1>
<p class="text-gray-600">Select options to see all possible combinations.</p>
</header>
<!-- Our JS will populate this container -->
<div id="options-container" class="space-y-6"></div>
<div class="mt-10 pt-6 border-t">
<h2 class="text-2xl font-semibold">Resulting Combinations</h2>
<!-- And the results will appear here -->
<div id="results-container" class="p-4 mt-4 bg-white rounded-lg shadow-sm"></div>
</div>
</div>
</div>
</body>
</html>
2. The Data: Structuring Our Options
Before we write any logic, we need a clean way to represent our product options. An array of objects is perfect for this. Each object represents a category (like "Size"), and contains its possible variants.
// permutations.js
const optionsData = [
{
name: "Size",
variants: [
{ label: "Small", value: "Small", checked: true },
{ label: "Medium", value: "Medium", checked: true },
{ label: "Large", value: "Large", checked: false },
],
},
{
name: "Colour",
variants: [
{ label: "Blue", value: "Blue", checked: true },
{ label: "White", value: "White", checked: true },
],
},
{
name: "Pattern",
variants: [
{ label: "Plain", value: "Plain", checked: true },
{ label: "Logo", value: "Logo", checked: false },
],
},
];
This structure is scalable and easy to manage. The checked
property lets us set a default state.
3. The Core Logic: The Permutation Engine
This is where the magic happens. Our goal is to take the selected options and generate all combinations. We'll do this with a single, powerful function.
The logic can be broken down into two steps:
Step 1: Get the selected variants.
We first need to transform our optionsData
into a simpler array of arrays, containing only the value
of the variants that are currently checked.
function calculatePermutations() {
const activeVariants = optionsData
.map((option) =>
option.variants.filter((v) => v.checked).map((v) => v.value)
)
.filter((arr) => arr.length > 0);
// ...
}
If "Small," "Medium," "Blue," and "Plain" are selected, activeVariants
will look like this:
[['Small', 'Medium'], ['Blue'], ['Plain']]
Step 2: Use reduce
to create the combinations.
Now we'll use the reduce
method to iterate through activeVariants
and build our final list. reduce
is perfect for taking an array and "reducing" it to a single value—in our case, a final array of all combinations.
// ... next part of calculatePermutations()
const combinations = activeVariants.reduce((acc, current) => {
// If the accumulator is empty, just return the first set of variants.
if (acc.length === 0) {
return current.map((item) => [item]);
}
// Otherwise, combine the existing accumulator with the current set.
const newCombinations = [];
acc.forEach((accItem) => {
current.forEach((currentItem) => {
newCombinations.push([...accItem, currentItem]);
});
});
return newCombinations;
}, []); // Start with an empty accumulator array.
Let's trace it:
- Initial Call:
acc
is[]
,current
is['Small', 'Medium']
. The function returns[['Small'], ['Medium']]
. - Second Call:
acc
is[['Small'], ['Medium']]
,current
is['Blue']
. The loops run and produce[['Small', 'Blue'], ['Medium', 'Blue']]
. - Third Call:
acc
is[['Small', 'Blue'], ['Medium', 'Blue']]
,current
is['Plain']
. The loops run again, giving us our final result:[['Small', 'Blue', 'Plain'], ['Medium', 'Blue', 'Plain']]
.
4. Bringing it to Life: The UI Functions
The logic is done, but we need to let users interact with it. We'll create functions to render the options and the results.
The renderOptions()
function loops through optionsData
and creates the HTML for our checkboxes. Crucially, it adds an event listener to each one.
function renderOptions() {
const optionsContainer = document.getElementById("options-container");
optionsContainer.innerHTML = ""; // Clear previous options
optionsData.forEach((option, index) => {
// ... create container divs and titles ...
option.variants.forEach((variant) => {
// ... create labels and checkboxes ...
checkbox.addEventListener("change", (e) => {
variant.checked = e.target.checked;
renderOptions(); // Re-render the UI to show the new selection state
calculatePermutations(); // Re-run the calculation!
});
// ... append elements ...
});
});
}
The change
event listener is the key. It updates the checked
state in our optionsData
and then re-runs our rendering and calculation functions. This is what makes the component interactive.
Finally, renderResults()
simply takes the array from calculatePermutations()
and displays it as a list.
function renderResults(combinations) {
const resultsContainer = document.getElementById("results-container");
resultsContainer.innerHTML = ""; // Clear old results
const list = document.createElement("ul");
list.className = "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3";
combinations.forEach((combo) => {
const listItem = document.createElement("li");
listItem.className = "bg-gray-100 p-2 rounded-md text-center";
listItem.textContent = combo.join(" / ");
list.appendChild(listItem);
});
resultsContainer.appendChild(list);
}
Conclusion
And there you have it! We've built a powerful, reusable, and interactive permutation generator with just a handful of vanilla JavaScript functions. By combining a clean data structure with the power of map
, filter
, and reduce
, we solved a complex real-world problem in an elegant way.
How would you use this in your projects? Let me know in the comments below!
Top comments (0)