Skip to content

Instantly share code, notes, and snippets.

@Taymindis
Last active May 31, 2020 06:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Taymindis/f1b0c3ec8ee9c277986a7357ebd85d5f to your computer and use it in GitHub Desktop.
Save Taymindis/f1b0c3ec8ee9c277986a7357ebd85d5f to your computer and use it in GitHub Desktop.
It's react-select library with tree options

[Demo] (https://codesandbox.io/s/react-codesandboxer-example-5sh93?file=/example.js)

import React, { useState, useEffect } from "react";
import Select, { components } from "react-select";

const menuHeaderStyle = {
  padding: "8px 12px",
  background: "#333",
  color: "white"
};

/**
 *
 * @param { minTreeLevel: number default 1,
 *          treeLevel: number required,
 *          onChange: value, actionMeta => void } props
 */

const ReactTreeSelect = props => {
  const { defaultValue, minTreeLevel, options, onChange, ...rest } = props;

  const [valTree, setValueTree] = useState(defaultValue || []);
  const [targetOption, setTargetOption] = useState([]);
  const [menuIsOpen, toggleMenu] = useState(false);
  const [isForceOpen, forceToggle] = useState(false);

  const SingleValue = ({ children, ...props }) => {
    return (
      <components.SingleValue {...props}>
        {valTree.reduce((b, v) => b + v.prevText + v.label, "")}
      </components.SingleValue>
    );
  };

  const MenuList = props => {
    const headerTxt =
      targetOption && targetOption.length && targetOption[0].prevText;
    return headerTxt ? (
      <components.MenuList {...props}>
        <div style={menuHeaderStyle}>{headerTxt}</div>
        {props.children}
      </components.MenuList>
    ) : (
      <components.MenuList {...props} />
    );
  };

  useEffect(() => {
    const defaultValue = [];
    const currentOption = valTree.reduce((base, v, i) => {
      const found = base.find(o => o.value === v.value);
      if (found) {
        defaultValue.push(found);
        return found.subOptions && found.subOptions(); // will stop reducing if null
      }
      return base;
    }, options);
    // console.log(defaultValue);
    // console.log(currentOption);
    if (defaultValue.length === 0) {
      setValueTree([]);
      setTargetOption(options);
    } else {
      setTargetOption(currentOption);
    }

    return () => {};
  }, []);

  return (
    <Select
      onMenuOpen={() => {
        toggleMenu(true);
        if (!targetOption || targetOption.length === 0) {
          forceToggle(true);
        }
      }}
      onMenuClose={() => {
        toggleMenu(false);
        forceToggle(false);
      }}
      menuIsOpen={
        (!!targetOption && targetOption.length > 0 && menuIsOpen) || isForceOpen
      }
      closeMenuOnSelect={!menuIsOpen}
      onBlur={() => {
        if (valTree.length < minTreeLevel) {
          setValueTree([]);
          setTargetOption(options);
          onChange([], { action: "incomplete-clear" });
        }
      }}
      isClearable
      onChange={(o, actionmeta) => {
        if (actionmeta.action === "select-option") {
          const newTree = [
            ...valTree,
            {
              value: o.value,
              label: o.label,
              prevText: o.prevText || " "
            }
          ];
          setValueTree(newTree);
          setTargetOption(o.subOptions && o.subOptions());
          onChange(newTree, actionmeta);
        } else if (actionmeta.action === "clear") {
          setValueTree([]);
          setTargetOption(options);
          onChange([], actionmeta);
        }
        // console.log(actionmeta);
      }}
      components={{ SingleValue, MenuList }}
      styles={{
        option: base => ({
          ...base,
          height: "100%"
        })
      }}
      defaultValue={valTree}
      options={targetOption}
      {...rest}
    />
  );
};

ReactTreeSelect.defaultProps = {
  minTreeLevel: 1,
  treeLevel: 0,
  onChange: () => {}
};

export default ReactTreeSelect;

Example to Run

import React, { useState, useEffect, useRef } from "react";
// import Select, { components } from "react-select";
import { alwaysBasedOnOpts } from "./docs/data";

import ReactSelectTree from "./ReactSelectTree";

<ReactSelectTree
    onChange={val => {
      console.log(val);
    }}
    placeholder="Always based on"
    minTreeLevel={2}
    options={alwaysBasedOnOpts} />

Example Options

const ymdhms = ["Year", "Month", "Day", "Hour", "Minute"];

const counterMetrics = [...Array(101).keys()];

export const alwaysBasedOnOpts = counterMetrics.map(w => {
  return {
    prevText: "Always based on Last ",
    value: w,
    label: w,
    subOptions: () =>
      ymdhms.map(x => ({
        value: x,
        label: x,
        subOptions: () =>
          counterMetrics.map(y => ({
            prevText: " till advance ",
            value: y,
            label: y,
            subOptions: () =>
              ymdhms.map(z => ({
                value: z,
                label: z
              }))
          }))
      }))
  };
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment