DEV Community

Cover image for How to Generate Product Variant Permutations in Vanilla JavaScript
Alexander Pechkarev
Alexander Pechkarev

Posted on

How to Generate Product Variant Permutations in Vanilla JavaScript

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>
Enter fullscreen mode Exit fullscreen mode

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 },
    ],
  },
];
Enter fullscreen mode Exit fullscreen mode

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

  // ...
}
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

Let's trace it:

  1. Initial Call: acc is [], current is ['Small', 'Medium']. The function returns [['Small'], ['Medium']].
  2. Second Call: acc is [['Small'], ['Medium']], current is ['Blue']. The loops run and produce [['Small', 'Blue'], ['Medium', 'Blue']].
  3. 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 ...
    });
  });
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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)