0

I have created a component which adds an additional selection box dropdown whenever a key inside an object is another object.

For example, consider the following object:

{
  a1: {
    x1: 1,
    x2: 2,
    x3: 3,
    x4: {
      z1: "z1",
      z2: "z2"
    },
    x5: [
      {
        x5a: {
          z5a1: 1,
          z5a2: 2
        }
      },
      {
        x5b: {
          z5b1: 1,
          z5b2: 2
        }
      }
    ]
  },
  a2: {
    x1: 1,
    x2: 2,
    x3: 3
  },
  a3: "some values"
};

What I want to achieve is (when I select a value from the dropdown menu):

  • if subTree[value] is an object ({}) or an array ([]), display its keys or indices in a new selection box drop down, directly bellow the current
  • else stop

Initial display

enter image description here

Selecting a value in the dropdown

After I select a value, the next selection will show empty, and so on and so forth...

enter image description here

enter image description here

The problem

When I update a value in a selection box, my code doesn't update/clear the selections bellow it properly.

enter image description here

The source code of my project is available at: https://codesandbox.io/s/frosty-grass-9jdue

6
  • If you try to look at the JSON structure, you will see that they keys are displayed in the selection on the first level, if you select a specific key and its corresponding value is an object, it will show another selection just below, and this behavior will continue until the selected key is not an object or array of an object. i posted link also on codesandbox so someone might see the behavior themselves. Commented Feb 24, 2021 at 20:25
  • Do you want to do this as an exercise or are you trying to implement some sort of tree menu? Commented Feb 24, 2021 at 21:07
  • yeah it is similar to tree menu, but this is the requirement. the succeeding selections wont show until the user havent selected a key on the first level. in a sense the user is restricted to 1 key at a time on the navigational perspective. The problem i got is when the key is updated somewhere between, and the selection down below should not shown. This only happens when the new key and its corresponding object has the same number of keys or more inside that object vs the previous one. i know i get complicated, But you may get the idea on 4th image. Commented Feb 24, 2021 at 21:29
  • I totally get what you are trying to do. The issue is probably in your process function as I don't quite get why you are looking at item[1]. I'll see if I can fix this and write an answer. Commented Feb 24, 2021 at 23:25
  • Ok I see that what you are doing is looking at the next element after the current and that's why it's item[1] Personally I would manage the path as an array and render the individual selects based on that. When we update the path value at index 2 for example, we need to clear everything after that in the path. Is it cool if I write that method as an answer or do you want me to figure out what's going wrong in the current approach? Commented Feb 24, 2021 at 23:34

2 Answers 2

1

When changing a value that is not the last in the path, you need to clear all subsequent selections because they were based on a different path. I'm not quite sure how we do that in your setup because I haven't quite wrapped my head around it.

What makes sense to me is to store the pieces of the path as an array. That way we can use slice to remove the tail. I am going to use lodash's get method as a helper to access the value at a path. I am expecting the prop data to be the object itself rather than Object.entries like you were doing before.

import React, { useState, useEffect } from "react";
import { MenuItem, TextField } from "@material-ui/core";
import _get from "lodash/get";

const InfiniteSelection = ({ data, onCaseCadeSelection }) => {
  // an array of segments like ['a1', 'x4', 'z1']
  const [path, setPath] = useState([]);

  // joins to a string like `a1.x4.z1`
  const cascade = path.join(".");

  // call callback whenever the cascade changes
  useEffect(() => {
    if (onCaseCadeSelection) {
      onCaseCadeSelection(cascade);
    }
  }, [cascade]);

  // need to know the index in the paths array where the change occurred
  const handleChange = (index) => (event) => {
    // set this value and delete everything after it
    setPath([...path.slice(0, index), event.target.value]);
  };

  // options for the NEXT value from a given path
  const optionsForPath = (path) => {
    // lodash get handles this except when path is empty array []
    const value = path.length > 0 ? _get(data, path) : data;

    // get the options from this path, or null if it is terminal
    return typeof value === "object" ? Object.keys(value) : null;
  };

  // either the current path is to a terminal value, or there should be one more level of selects
  const currentOptions = optionsForPath(path);

  // helper function can be used as a callback to path.map
  // will also be called one extra time for the next values if not on a terminal value
  const renderSelect = (value, index) => {
    return (
      <SelectControlled
        className="text form_text"
        variant="outlined"
        list={optionsForPath(path.slice(0, index)) ?? []}
        onChange={handleChange(index)}
        value={value ?? ""}
      />
    );
  };

  // render selects for each element in the path and maybe a next select
  return (
    <div className="vertically_spaced">
      {path.map(renderSelect)}
      {currentOptions === null || renderSelect("", path.length)}
    </div>
  );
};

Code Sandbox Link

Sign up to request clarification or add additional context in comments.

1 Comment

This is exactly I need.
1

From @LindaPaiste's answer:

When changing a value that is not the last in the path, you need to clear all subsequent selections because they were based on a different path.

That's the key to solving your problem! You have to somehow blow away and forget everything bellow the selection box whose value you are currently changing.

React was designed around the "blow away and forget" principle. Note also that The Data Flows Down. With that in mind, your task should be fairly easy to complete and while Linda's solution seems to work, it is perhaps not as simple as it could be.


What if we could have a special component that (1) accepts a sub-tree of your data, (2) renders its 1st level children as a selection box dropdown and then (3) repeats the process recursively? Something like this:

<RecursiveComponent subTree={DATA_SAMPLE} {/*maybe some other props*/}/>

When we think of recursion, we have to think of terminal conditions. In our case, this happens when the sub-tree is a primitive type (i.e. not an object ({}) or an array ([])).

Every RecursiveComponent has to:

  • render the selection menu dropdown, containing all the 1st level children of the sub-tree
  • render the nested RecursiveComponent, based on props.subTree[selection]
  • handle user interaction

Something like this:

import { MenuItem, Select } from "@material-ui/core";
import { useState } from "react";

function RecursiveComponent(props) {
  const [selection, setSelection] = useState(props.currentSelection);
  const handleChange = (event) => {
    setSelection(event.target.value);
  };
  return (
    <>
      <Select variant="outlined" value={selection} onChange={handleChange}>
        {Object.keys(props.subTree).map((key) => (
          <MenuItem value={key}>{key}</MenuItem>
        ))}
      </Select>
      <div /> {/* forces a line break between selection boxes */}
      {props.subTree[selection] !== Object(props.subTree[selection]) ? (
        <></>
      ) : (
        <RecursiveComponent
          subTree={props.subTree[selection]}
          currentSelection=""
        />
      )}
    </>
  );
}
export default RecursiveComponent;

This is how you can use RecursiveComponent in your project by editing index.js:

import { StrictMode } from "react";
import ReactDOM from "react-dom";

import { DATA_SAMPLE } from "./DataSample";
import RecursiveComponent from "./RecursiveComponent";

const rootElement = document.getElementById("root");

ReactDOM.render(
  <StrictMode>
    <RecursiveComponent subTree={DATA_SAMPLE} currentSelection="" />
  </StrictMode>,
  rootElement
);

1 Comment

By looking at it, its very simple and much easier to follow. Will give a try on this.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.