Fawzan
Fawzan

Reputation: 4849

Recursive way of removing an item from a Javascript Object

I have something similar to a tree structure in my javascript program and I want to remove a child from it, I used a recursive approach to traverse the tree but I am quite stuck in deleting a node from the tree.

In contrast, I will get an ID as a parameter and I should check it against the child.metadata.id and delete the child if it maches with the ID.

Note : I have certain type of child nodes in my graph where it divides it path based on its type 'true' or 'false' that is if the type of the child node in the metadata is if/empty.

Here is the sample tree structure.

 var graph =
{
    "metadata": {"id": "sms_in", "type": "api", "optionsDivId": "div_sms_in", "options": {}},
    "data": {
        "true": {
            "isEmpty1": {
                "metadata": {
                    "id": "isEmpty1",
                    "type": "empty",
                    "optionsDivId": "div_isEmpty1",
                    "options": {}
                },
                "data": {
                    "true": {
                        "sms1": {
                            "metadata": {
                                "id": "sms1",
                                "type": "api",
                                "optionsDivId": "div_sms1",
                                "options": {}
                            }, "data": {"true": {}, "false": false}
                        }
                    },
                    "false": {
                        "dbInsert1": {
                            "metadata": {
                                "id": "dbInsert1",
                                "type": "dbInsert",
                                "optionsDivId": "div_dbInsert1",
                                "options": {}
                            },
                            "data": {
                                "true": {
                                    "sms2": {
                                        "metadata": {
                                            "id": "sms2",
                                            "type": "api",
                                            "optionsDivId": "div_sms2",
                                            "options": {}
                                        }, "data": {"true": {}, "false": false}
                                    }
                                }, "false": false
                            }
                        }
                    }
                }
            }
        }, "false": false
    }
};

and this is my traverse function

var traverse = function (current) {



    if( current == 'undefined' ) return ;

    var currentChildId = current['metadata']['id'];
    var currentChildType = current['metadata']['type'];



    console.log('visiting : ', currentChildId);


    if (currentChildType == 'if' || currentChildType == 'empty') {

        for(var childKeyType in current['data']){

            for( var childKey in current['data'][childKeyType]){

                var child = current['data'][childKeyType][childKey];

                traverse(child);

            }

        }


    } else {

        for (var childKey in current['data']['true']) {

            var child = current['data']['true'][childKey];

            traverse(child);

        }

    }


};

Can someone help me to complete the delete function ?

function popChild(current, childId){
    if( current == 'undefined' ) return ;

    var currentChildId = current['metadata']['id'];
    var currentChildType = current['metadata']['type'];

    if(currentChildId == childId){
        delete current;
        return;
    }


    console.log('visiting : ', currentChildId);


    if (currentChildType == 'if' || currentChildType == 'empty') {

        for(var childKeyType in current['data']){

            for( var childKey in current['data'][childKeyType]){

                var child = current['data'][childKeyType][childKey];

                popChild(child, childId, );

            }

        }


    } else {

        for (var childKey in current['data']['true']) {

            var child = current['data']['true'][childKey];

            popChild(child, childId);

        }

    }
}

Upvotes: 1

Views: 2935

Answers (3)

vincent
vincent

Reputation: 2181

I'd try not to reinvent the wheel here. We use object-scan for most of our data processing like this. It's powerful once you wrap your head around it and makes the code a lot more maintainable. Here is how you could solve your problem

// const objectScan = require('object-scan');

const rm = (obj, id) => objectScan(['++.metadata.id'], {
  abort: true,
  rtn: 'bool',
  filterFn: ({ value, parents, key }) => {
    if (value === id) {
      delete parents[2][key[key.length - 3]];
      return true;
    }
    return false;
  }
})(obj);

const graph = { metadata: { id: 'sms_in', type: 'api', optionsDivId: 'div_sms_in', options: {} }, data: { true: { isEmpty1: { metadata: { id: 'isEmpty1', type: 'empty', optionsDivId: 'div_isEmpty1', options: {} }, data: { true: { sms1: { metadata: { id: 'sms1', type: 'api', optionsDivId: 'div_sms1', options: {} }, data: { true: {}, false: false } } }, false: { dbInsert1: { metadata: { id: 'dbInsert1', type: 'dbInsert', optionsDivId: 'div_dbInsert1', options: {} }, data: { true: { sms2: { metadata: { id: 'sms2', type: 'api', optionsDivId: 'div_sms2', options: {} }, data: { true: {}, false: false } } }, false: false } } } } } }, false: false } };

console.log(rm(graph, 'isEmpty1'));
// => true

console.log(graph);
/* =>
  { metadata:
    { id: 'sms_in',
      type: 'api',
      optionsDivId: 'div_sms_in',
      options: {} },
   data: { true: {}, false: false } }
*/
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/[email protected]"></script>

Disclaimer: I'm the author of object-scan

Note that since the code mutates the existing structure, the delete can not work for the root node.

Upvotes: 0

Scott Sauyet
Scott Sauyet

Reputation: 50797

Since this has been resurrected...

Vincent points out that battle-tested libraries are often the best way to solve such problems. While I certainly must agree (and am in fact one of the principal authors of Ramda), there is another approach of maintaining one's own collection of snippets to reuse across projects.

Although I've never really considered including it in Ramda, I have handy a recursive filter function that makes writing this pretty simple:

const filterDeep = (pred) => (obj) => 
  Object (obj) === obj
    ? Object .fromEntries (
        Object .entries (obj) 
          .flatMap (([k, v]) => pred (v) ? [[k, filterDeep (pred) (v)]] : [])
      )
    : obj

const removeItem = (targetId, graph) => 
  filterDeep (({metadata: {id} = {}}) => id !== targetId) (graph)

const graph = {metadata: {id: "sms_in", type: "api", optionsDivId: "div_sms_in", options: {}}, data: {true: {isEmpty1: {metadata: {id: "isEmpty1", type: "empty", optionsDivId: "div_isEmpty1", options: {}}, data: {true: {sms1: {metadata: {id: "sms1", type: "api", optionsDivId: "div_sms1", options: {}}, data: {true: {}, false: !1}}}, false: {dbInsert1: {metadata: {id: "dbInsert1", type: "dbInsert", optionsDivId: "div_dbInsert1", options: {}}, data: {true: {sms2: {metadata: {id: "sms2", type: "api", optionsDivId: "div_sms2", options: {}}, data: {true: {}, false: !1}}}, false: !1}}}}}}, false: !1}}

console .log (removeItem ('dbInsert1', graph))
console .log (removeItem ('sms2', graph))
.as-console-wrapper {max-height: 100% !important; top: 0}

Here, we write a very simple removeItem function, depending on our already tested filterDeep. This is pretty powerful. Functions like this are often not as hardened as those in popular libraries. But they are written directly for our own needs, and can be designed precisely how we want them to be.

For instance, there is something odd about using filter with a predicate that says the opposite of what we want, though. It might make sense to write a rejectDeep that would let our predicate test for a match rather than a mismatch. It turns out to be trivial:

const rejectDeep = (pred) => 
  filterDeep (x => ! (pred (x)))

const removeItem = (target, obj) => 
  rejectDeep (({metadata: {id} = {}}) => id === target) (obj)

Now we can then add rejectDeep to our personal library.


There is one part of filterDeep that might be confusing. We use flatMap here as a one-pass filter and map combination. It would be perfectly reasonable, but (possibly) slightly less efficient, to separate it into two steps:

const filterDeep = (pred) => (obj) => 
  Object (obj) === obj
    ? Object .fromEntries (
        Object .entries (obj) 
          .filter (([k, v]) => pred (v)) 
          .map (([k, v]) => [k, filterDeep (pred) (v)])
      )
    : obj

Upvotes: 0

user663031
user663031

Reputation:

Consider using JSON.stringify as a way to iterate through your object, using the replacer argument:

function remove_ids(object, id) {
  return JSON.parse(JSON.stringify(object, function(key, value) {
    if (typeof value !== 'object' || typeof value.metadata !== 'object' ||
        value.metadata.id !== id) return value;
  }));
}

Upvotes: 4

Related Questions