Modifying select for product variation in React Js

1.5k Views Asked by At

In a small ecommerce project (sourcing data from WooCommerce) in ReactJs i have a variable product Besides all the product data i got two array object representing the product variations, one contains all the available attribute and options:

nodes: Array(3)
 0:
  id: "d2VpZ2h0OjMxOldlaWdodA=="
  name: "Weight"
  options: Array(2)
   0: "250gr"
   1: "500gr"
  position: 0
  variation: true
  visible: true
1:
 id: "cm9hc3Q6MzE6Um9hc3Q="
 name: "Roast"
  options: Array(3)
   0: "Light"
   1: "Medium"
   2: "Dark"
 position: 1
 variation: true
 visible: true
2:
 id: "cGFja2FnaW5nOjMxOlBhY2thZ2luZw=="
  name: "Packaging"
  options: Array(2)
   0: "Card"
   1: "Tin"
  position: 2
  variation: true
  visible: true

I save this one in a state and use it to build the select (3 select in this case) then i have an array of object which contans all the available combinations, with price, id and so on:

nodes: Array(5)
 0:
  attributes:
  nodes: Array(3)
   0: {id: "NDB8fHdlaWdodHx8MjUwZ3I=", name: "weight", value: "250gr"}
   1: {id: "NDB8fHJvYXN0fHxNZWRpdW0=", name: "roast", value: "Medium"}
   2: {id: "NDB8fHBhY2thZ2luZ3x8Q2FyZA==", name: "packaging", value: "Card"}
  id: "cHJvZHVjdF92YXJpYXRpb246NDA="
  price: "€7,00"
  variationId: 40
1:
 attributes:
 nodes: Array(3)
  0: {id: "Mzh8fHdlaWdodHx8NTAwZ3I=", name: "weight", value: "500gr"}
  1: {id: "Mzh8fHJvYXN0fHxEYXJr", name: "roast", value: "Dark"}
  2: {id: "Mzh8fHBhY2thZ2luZ3x8VGlu", name: "packaging", value: "Tin"}
 length: 3
 id: "cHJvZHVjdF92YXJpYXRpb246Mzg="
 price: "€12,00"
 variationId: 38
2:
 attributes:
 nodes: Array(3)
  0: {id: "Mzd8fHdlaWdodHx8NTAwZ3I=", name: "weight", value: "500gr"}
  1: {id: "Mzd8fHJvYXN0fHxNZWRpdW0=", name: "roast", value: "Medium"}
  2: {id: "Mzd8fHBhY2thZ2luZ3x8VGlu", name: "packaging", value: "Tin"}
  length: 3
  id: "cHJvZHVjdF92YXJpYXRpb246Mzc="
  price: "€12,00"
  variationId: 37
3:
 attributes:
 nodes: Array(3)
  0: {id: "MzZ8fHdlaWdodHx8NTAwZ3I=", name: "weight", value: "500gr"}
  1: {id: "MzZ8fHJvYXN0fHxMaWdodA==", name: "roast", value: "Light"}
  2: {id: "MzZ8fHBhY2thZ2luZ3x8VGlu", name: "packaging", value: "Tin"}
 id: "cHJvZHVjdF92YXJpYXRpb246MzY="
 price: "€12,00"
 variationId: 36  
4:
attributes:
 nodes: Array(3)
  0: {id: "MzR8fHdlaWdodHx8MjUwZ3I=", name: "weight", value: "250gr"}
  1: {id: "MzR8fHJvYXN0fHxMaWdodA==", name: "roast", value: "Light"}
  2: {id: "MzR8fHBhY2thZ2luZ3x8Q2FyZA==", name: "packaging", value: "Card"}
 length: 3
 id: "cHJvZHVjdF92YXJpYXRpb246MzQ="
 price: "€7,00"
 variationId: 34

Basically, when i select something on any select, the other select shoould filter based on the avaialble variations: if i select 250gr as weight, the Dark Roast and the Tin Packaging sould be removed from their select since they arent combinable with 250gr; if i switch back to 500gr (or to "no option selected") they should reappear in the same place as they were before.

on the basic WooCommerce product page it already works this way (you can check its behaviour here: https://shop.popland.it/prodotto/coffee-bean/) but replicating it in ReactJs its harder than i was thinking. At the moment im stuck on loop that generate the select with an empty onChange handler:

const [attr, setAttr] = useState(product.attributes);
<div>
        {attr.nodes.map((attribute, l) => {
          return (
            <div key={l}>
              <span>{attribute.name}</span>
              <select
                id={attribute.name}
                onChange={handleChange}
                data-attribute_name={`attribute_${attribute.name}`}
              >
                <option>Select option</option>
                {attribute.options.map((option, o) => {
                  return (
                    <option key={o} value={option}>
                      {option}
                    </option>
                  );
                })}
              </select>
            </div>
          );
        })}
      </div>

Any suggetion/help on how to go on from here?

1

There are 1 best solutions below

1
On

here is my solution.

/src/hooks.js

import { useState } from "react";
import { filterVariations, formatVariations } from "./helpers";

export function useVariations(product) {
  const [state, setState] = useState({});
  const filteredVariations = filterVariations(
    product?.variations?.nodes,
    state,
  );
  const formatedVariations = formatVariations(
    filteredVariations,
    product?.variations?.nodes,
  );
  return [formatedVariations, state, setState];
}

/src/helpers.js

export function filterVariations(variations, state) {
  if (Object.keys(state).length === 0) {
    return variations;
  }

  return Object.keys(state)?.reduce((accStateVars, currStateVar) => {
    return accStateVars.reduce((accVars, currVar) => {
      const filteredAttrsByName = currVar?.attributes?.nodes?.filter(
        (attr) => currStateVar === attr?.name,
      );

      const withSelected = currVar?.attributes?.nodes?.findIndex(
        (attr) => attr?.attributeId === state?.[currStateVar]?.value,
      );

      return [
        ...accVars,
        {
          attributes: {
            nodes:
              withSelected >= 0
                ? currVar?.attributes?.nodes
                : filteredAttrsByName,
          },
        },
      ];
    }, []);
  }, variations);
}

export function formatVariations(filteredVariations, variations) {
  const defaultSelects = variations?.reduce(
    (accVars, currVar) => ({
      ...accVars,
      ...currVar?.attributes?.nodes?.reduce(
        (accAttrs, currAttr) => ({ ...accAttrs, [currAttr.name]: {} }),
        {},
      ),
    }),
    {},
  );

  return filteredVariations.reduce((accVars, currVar) => {
    const filteredAttrs = currVar?.attributes?.nodes?.reduce(
      (accAttrs, currAttr) => {
        const exists =
          0 <=
          accVars[currAttr.name]?.options?.findIndex(
            (option) => option.value === currAttr.attributeId,
          );
        return {
          ...accAttrs,
          [currAttr.name]: {
            placeholder: currAttr.label,
            options: exists
              ? accVars[currAttr.name]?.options || []
              : [
                  ...(accVars[currAttr.name]?.options || []),
                  { label: currAttr.value, value: currAttr.attributeId },
                ],
          },
        };
      },
      {},
    );

    return { ...accVars, ...filteredAttrs };
  }, defaultSelects);
}

/src/App.js

import "./styles.css";
import Select from "react-select";
import { useVariations } from "./hooks";

const product = {
  variations: {
    nodes: [
      {
        attributes: {
          nodes: [
            {
              name: "pa_size",
              attributeId: 254,
              id: "NTY5MHx8cGFfc2l6ZXx8MTItc3BlYWtlcg==",
              label: "Size",
              value: '12" Speaker',
            },
            {
              name: "pa_color",
              attributeId: 304,
              id: "NTY5MHx8cGFfY29sb3J8fGdyYXBoaXRl",
              label: "Color",
              value: "Graphite",
            },
            {
              name: "pa_flavor",
              attributeId: 320,
              id: "NTY5MHx8cGFfZmxhdm9yfHxnb2xk",
              label: "Flavor",
              value: "Gold",
            },
            {
              name: "pa_pallet",
              attributeId: 336,
              id: "NTY5MHx8cGFfcGFsbGV0fHxncmVlbg==",
              label: "Pallet",
              value: "Green",
            },
          ],
        },
      },
      {
        attributes: {
          nodes: [
            {
              name: "pa_size",
              attributeId: 255,
              id: "NTY4Nnx8cGFfc2l6ZXx8MTAtc3BlYWtlcg==",
              label: "Size",
              value: '10" Speaker',
            },
            {
              name: "pa_color",
              attributeId: 67,
              id: "NTY4Nnx8cGFfY29sb3J8fGJlaWdl",
              label: "Color",
              value: "Beige",
            },
            {
              name: "pa_flavor",
              attributeId: 320,
              id: "NTY4Nnx8cGFfZmxhdm9yfHxnb2xk",
              label: "Flavor",
              value: "Gold",
            },
            {
              name: "pa_pallet",
              attributeId: 337,
              id: "NTY4Nnx8cGFfcGFsbGV0fHxwZWFjaA==",
              label: "Pallet",
              value: "Peach",
            },
          ],
        },
      },
      {
        attributes: {
          nodes: [
            {
              name: "pa_size",
              attributeId: 255,
              id: "NTY4N3x8cGFfc2l6ZXx8MTAtc3BlYWtlcg==",
              label: "Size",
              value: '10" Speaker',
            },
            {
              name: "pa_color",
              attributeId: 439,
              id: "NTY4N3x8cGFfY29sb3J8fGZhdGFsLWFwcGxl",
              label: "Color",
              value: "Fatal Apple",
            },
            {
              name: "pa_flavor",
              attributeId: 319,
              id: "NTY4N3x8cGFfZmxhdm9yfHx2YW5pbGxh",
              label: "Flavor",
              value: "Vanilla",
            },
            {
              name: "pa_pallet",
              attributeId: 336,
              id: "NTY4N3x8cGFfcGFsbGV0fHxncmVlbg==",
              label: "Pallet",
              value: "Green",
            },
          ],
        },
      },
      {
        attributes: {
          nodes: [
            {
              name: "pa_size",
              attributeId: 254,
              id: "NTY4OHx8cGFfc2l6ZXx8MTItc3BlYWtlcg==",
              label: "Size",
              value: '12" Speaker',
            },
            {
              name: "pa_color",
              attributeId: 67,
              id: "NTY4OHx8cGFfY29sb3J8fGJlaWdl",
              label: "Color",
              value: "Beige",
            },
            {
              name: "pa_flavor",
              attributeId: 320,
              id: "NTY4OHx8cGFfZmxhdm9yfHxnb2xk",
              label: "Flavor",
              value: "Gold",
            },
            {
              name: "pa_pallet",
              attributeId: 337,
              id: "NTY4OHx8cGFfcGFsbGV0fHxwZWFjaA==",
              label: "Pallet",
              value: "Peach",
            },
          ],
        },
      },
      {
        attributes: {
          nodes: [
            {
              name: "pa_size",
              attributeId: 254,
              id: "NTY4OXx8cGFfc2l6ZXx8MTItc3BlYWtlcg==",
              label: "Size",
              value: '12" Speaker',
            },
            {
              name: "pa_color",
              attributeId: 436,
              id: "NTY4OXx8cGFfY29sb3J8fGJlbGdyYXZlcw==",
              label: "Color",
              value: "Belgraves",
            },
            {
              name: "pa_flavor",
              attributeId: 319,
              id: "NTY4OXx8cGFfZmxhdm9yfHx2YW5pbGxh",
              label: "Flavor",
              value: "Vanilla",
            },
            {
              name: "pa_pallet",
              attributeId: 336,
              id: "NTY4OXx8cGFfcGFsbGV0fHxncmVlbg==",
              label: "Pallet",
              value: "Green",
            },
          ],
        },
      },
    ],
  },
};

export default function App() {
  const [selects, state, setState] = useVariations(product);

  function onChange(select, value) {
    const newState = {
      ...state,
      [select]: { ...value, name: select },
    };

    if (!value) {
      delete newState[select];
    }

    setState(newState);
  }

  return (
    <div className="App">
      {Object.keys(selects).map((select) => (
        <Select
          isClearable
          key={selects[select].placeholder}
          placeholder={selects[select].placeholder}
          // value={state[select]}
          onChange={(o) => onChange(select, o)}
          options={selects[select].options}
        />
      ))}
    </div>
  );
}

Playground