Skip to content

Instantly share code, notes, and snippets.

@georgiarnaudov
Last active February 6, 2023 13:35
Show Gist options
  • Select an option

  • Save georgiarnaudov/7f3173154821a3d84ce2f8094f3d7f4d to your computer and use it in GitHub Desktop.

Select an option

Save georgiarnaudov/7f3173154821a3d84ce2f8094f3d7f4d to your computer and use it in GitHub Desktop.
Product variations combination script. Works with a lot of options. In the current example there are only few available.
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Variant Selector</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
<script src="//unpkg.com/alpinejs" defer></script>
</head>
<body>
<div x-data="variantSelector()" class="p-10">
<template x-for="attribute in attributes" :key="attribute.id">
<div class="flex flex-col space-y-4">
<template x-if="attribute.component === 'radio'">
<div>
<label x-text="attribute.label" class="font-bold text-lg"></label>
<!-- options -->
<div class="mt-3">
<template x-for="option in attribute.options">
<label
class="cursor-pointer group"
:class="{'opacity-50 pointer-events-none': isDisabled(option.id)}"
>
<input
type="radio"
@click="onChoiceChange(attribute.id, $event.target.value)"
:value="option.id"
class="sr-only"
/>
<span
class="group-hover:bg-gray-300 group-hover:text-gray-800 w-auto px-4 py-2 rounded-lg text-lg border border-gray-500"
:class="{'bg-orange-300 text-orange-800': isActive(attribute.id, option.id)}"
x-text="option.label"
></span>
</label>
</template>
</div>
</div>
</template>
<template x-if="attribute.component === 'color_picker'">
<div>
<label x-text="attribute.label" class="font-bold text-lg"></label>
<!-- options -->
<div class="mt-3 space-x-1">
<template x-for="option in attribute.options">
<label
class="cursor-pointer group h-full w-full"
:class="{'opacity-50 pointer-events-none': isDisabled(option.id)}"
>
<input
type="radio"
@click="onChoiceChange(attribute.id, $event.target.value)"
:value="option.id"
class="sr-only"
/>
<div
class="inline-block h-10 w-10 rounded-lg"
:class="{ 'ring ring-orange-500': isActive(attribute.id, option.id) }"
:style="{backgroundColor: option.value}"
></div>
<!-- <span x-text="option.id"></span> -->
</label>
</template>
</div>
</div>
</template>
</div>
</template>
<!-- <pre wrap class="mt-10" x-text="JSON.stringify(choices)"></pre> -->
<p class="mt-10" x-text="choices.length !== 0 && selectedCombination()?.label"></p>
</div>
<script src="./variants.js"></script>
</body>
</html>
/**
* Available combinations of attributes
*
* XS Blue Leather [1, 5, 7]
* XS Green Leather [1, 6, 7]
* S Red Leather [2, 4, 7]
* S Green Leather [2, 6, 7]
* M Red Metal [3, 4, 8]
* M Blue Metal [3, 5, 8]
*/
/**
* Missing combinations
* XS Red Leather [1, 4, 7]
* S Blue Leather [2, 5, 7]
* M Green Metal [3, 6, 8]
*/
const availableCombinations = [
{
id: 1,
label: "XS Blue Leather",
optionIds: [1, 5, 7]
},
{
id: 2,
label: "XS Green Leather",
optionIds: [1, 6, 7]
},
{
id: 3,
label: "S Red Leather",
optionIds: [2, 4, 7]
},
{
id: 4,
label: "S Green Leather",
optionIds: [2, 6, 7]
},
{
id: 5,
label: "M Red Metal",
optionIds: [3, 4, 8]
},
{
id: 6,
label: "M Blue Metal",
optionIds: [3, 5, 8]
}
];
const attributes = [
{
id: 1,
label: "Size",
component: "radio",
options: [
{
id: 1,
label: "XS",
value: "xs"
},
{
id: 2,
label: "S",
value: "s"
},
{
id: 3,
label: "M",
value: "m"
}
]
},
{
id: 2,
label: "Color",
component: "color_picker",
options: [
{
id: 4,
label: "Red",
value: "#FF0000"
},
{
id: 5,
label: "Blue",
value: "#0000FF"
},
{
id: 6,
label: "Green",
value: "#00FF00"
}
]
},
{
id: 3,
label: "Material",
component: "radio",
options: [
{
id: 7,
label: "Leather"
},
{
id: 8,
label: "Metal"
}
]
}
];
function variantSelector() {
return {
init() {
this.variants = this.availableCombinations.map((c) => c.optionIds);
this.selectInitialCombination();
},
choices: {},
variants: [],
attributes,
availableCombinations,
selectInitialCombination() {
const combination = this.availableCombinations[0];
const firstOption = combination.optionIds[0];
const attribute = this.attributes.find((a) =>
a.options.some((o) => o.id === firstOption)
);
this.choices[attribute.id] = String(firstOption);
// Set combination with all option ids selected
// const combination = this.availableCombinations[0];
// combination.optionIds.forEach((id) => {
// const attribute = this.attributes.find((a) =>
// a.options.some((o) => o.id === id)
// );
// this.choices[attribute.id] = String(id);
// });
},
selectedCombination() {
const currentCombination = Object.values(this.choices).map((id) => +id);
return this.availableCombinations.find((c) => {
return c.optionIds.every((id) => currentCombination.includes(id));
});
},
isActive(attributeId, optionId) {
return +this.choices[attributeId] === optionId;
},
isDisabled(optionId) {
// handle empty selection
if (this.currentCombination.length === 0) return false;
// handle single attribute variants
if (this.attributes.length === 1) return false;
const filteredVariants = this.variants.filter((variant) => {
return this.currentCombination.every((_optionId) => variant.includes(_optionId));
});
const restOfOptions = _.uniq(filteredVariants.flat());
return !restOfOptions.includes(optionId);
},
get currentCombination() {
return Object.values(this.choices)
.sort()
.map((id) => +id);
},
onChoiceChange(attributeId, optionId) {
if (this.choices[attributeId] === optionId) {
delete this.choices[attributeId];
return;
}
this.choices[attributeId] = optionId;
}
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment