Last active
February 6, 2023 13:35
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * 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