javascriptalgorithmrecursiondata-structures

JS - recursive function on multidimensional array to filter out only "checked" items (and its parents if a child is selected)


I have this kind of array (i call it categories, these have childCats too)

var cats = [
  {
      "id": 1,
      "name": "A",
      "parentCategoryId": null,
      "parentCategory": null,
      "childCategories": [
          {
              "id": 2,
              "name": "B",
              "parentCategoryId": 1,
              "parentCategory": null,
              "childCategories": [
                  {
                      "id": 3,
                      "name": "C",
                      "parentCategoryId": 2,
                      "parentCategory": null,
                      "childCategories": []
                  },
                  {
                      "id": 4,
                      "name": "D",
                      "parentCategoryId": 2,
                      "parentCategory": null,
                      "childCategories": []
                  },
                  {
                      "id": 5,
                      "name": "E",
                      "parentCategoryId": 2,
                      "parentCategory": null,
                      "childCategories": []
                  }
              ]
          }
      ]
  },
  {
      "id": 66,
      "name": "BLA",
      "parentCategoryId": null,
      "parentCategory": null,
      "childCategories": []
  },  
  
];

What i now need is to get an array with checked items (and its parents)

This means, i checked only "C", i get:

[
  {
      "name": "A",
      "childCategories": [
          {
              "name": "B",
              "childCategories": [
                  {
                      "name": "C",
                      "childCategories": []
                  }
              ]
          }
      ]
  }
]

if i checked "C" and "D":

var cats = [
  {
      "name": "A",
      "childCategories": [
          {
              "name": "B",
              "childCategories": [
                  {
                      "name": "C",
                      "childCategories": []
                  },
                  {
                      "name": "D",
                      "childCategories": []
                  }
              ]
          }
      ]
  },    
];

and for "B" this:

var cats = [
  {
      "name": "A",
      "childCategories": [
          {
              "name": "B",
              "childCategories": []
          }
      ]
  },    
];

What i currently have (and it feels like a mess) is that: (result in console.log(..))

console.clear();
var checkedInterests = ['C'];
var checkedInterestsForDisplay = [];

var getInterestsForDisplay = function(arr) {
  for (var a of arr) {
    if (checkedInterests.includes(a.name)) {
      a.childCategories = [];
      checkedInterestsForDisplay.push(a);

      //also if a child is checked, the parent should be expanded
      var getParents = function(arrayToFindParent, parentIdToFind) {
        for (var aTFP of arrayToFindParent) {
          if (aTFP.id == parentIdToFind) {
            aTFP.childCategories = checkedInterestsForDisplay;
            checkedInterestsForDisplay = [aTFP];
            //again check if it has parents
            getParents(cats, aTFP.parentCategoryId);
          } else {
            getParents(aTFP.childCategories, parentIdToFind);
          }
        }
      }
      getParents(cats, a.parentCategoryId);
    }
    getInterestsForDisplay(a.childCategories);
  }
}

getInterestsForDisplay(cats);

console.log('checkedInterestsForDisplay', checkedInterestsForDisplay);
<script>
var cats = [{
    "id": 1,
    "name": "A",
    "parentCategoryId": null,
    "parentCategory": null,
    "childCategories": [{
      "id": 2,
      "name": "B",
      "parentCategoryId": 1,
      "parentCategory": null,
      "childCategories": [{
          "id": 3,
          "name": "C",
          "parentCategoryId": 2,
          "parentCategory": null,
          "childCategories": []
        },
        {
          "id": 4,
          "name": "D",
          "parentCategoryId": 2,
          "parentCategory": null,
          "childCategories": []
        }
      ]
    }]
  },
  {
    "id": 66,
    "name": "BLA",
    "parentCategoryId": null,
    "parentCategory": null,
    "childCategories": []
  },
];
</script>

this works oky, but if

var checkedInterests = ['C','D'];

i get

Maximum call stack size exceeded


Solution

  • Here's a simplified recursive approach. I assume the result should be the same if you check A and C as if you just checked C.

    console.clear();
    var checkedInterests = ['B','BLA'];
    
    const getInterestsForDisplay = (arr) => 
      arr.reduce((acc, el) => {
        // get a list of checked children (recursive call)
        const children = el.childCategories?.length 
          ? getInterestsForDisplay(el.childCategories)
          : [];
        
        // either this element is checked, or a child is checked, to be added to output.  
        if (checkedInterests.includes(el.name) || children.length) {
          acc.push({...el, childCategories: children});
        }
        
        return acc;
      }, []);
    
    const checkedInterestsForDisplay = getInterestsForDisplay(cats);
    console.log(checkedInterestsForDisplay);
    <script>
    var cats = [{
        "id": 1,
        "name": "A",
        "parentCategoryId": null,
        "parentCategory": null,
        "childCategories": [{
          "id": 2,
          "name": "B",
          "parentCategoryId": 1,
          "parentCategory": null,
          "childCategories": [{
              "id": 3,
              "name": "C",
              "parentCategoryId": 2,
              "parentCategory": null,
              "childCategories": []
            },
            {
              "id": 4,
              "name": "D",
              "parentCategoryId": 2,
              "parentCategory": null,
              "childCategories": []
            }
          ]
        }]
      },
      {
        "id": 66,
        "name": "BLA",
        "parentCategoryId": null,
        "parentCategory": null,
        "childCategories": []
      },
    ];
    </script>