update object recursively with es6 - javascript

I have many objects with children inside a dataTree and I want to update the toggle value to true recursively with an es6 syntax and get back the updated dataTree
the object looks like this
{
name: "Misc",
toggled: true,
children: [{
name: "Apple",
toggled:false
children: [{
name: "banana",
toggled:false
}]
}]
}
etc...
Any idea, Thanks

Make a module for your type of { name, toggled, children } - here we call ours Node
const Node =
{ make : (name = "", toggled = false, children = []) =>
({ name, toggled, children })
, toggle : (node) =>
Node.make (node.name, !node.toggled, node.children)
, toggleAll : (node) =>
Node.make (node.name, !node.toggled, node.children.map (Node.toggleAll))
}
Note that toggle and toggleAll do not mutate the original input - instead, a new Node is always created
const n =
Node.make ("foo", true)
console.log (n, Node.toggle (n), n)
// { name: 'foo', toggled: true, children: [] }
// { name: 'foo', toggled: false, children: [] }
// { name: 'foo', toggled: true, children: [] }
We can use this directly on your data to toggle all toggled fields
const data =
{ name : "Misc"
, toggled : true
, children :
[ { name : "Apple"
, toggled : false
, children :
[ { name : "banana"
, toggled : false
, children : []
}
]
}
]
}
console.log (Node.toggleAll (data))
// { name : "Misc"
// , toggled : false <--- toggled
// , children :
// [ { name : "Apple"
// , toggled : true <--- toggled
// , children :
// [ { name : "banana"
// , toggled : true <--- toggled
// , children : []
// }
// ]
// }
// ]
// }
But instead of writing your data with object literal syntax, you should use your module instead
const data =
Node.make ( "Misc"
, true
, [ Node.make ( "Apple"
, false
, [ Node.make ("banana", false) ]
)
]
)
console.log (Node.toggleAll (data))
// same output
I want to update the toggle value to true recursively ...
If you want to set all toggled to a specific value, you could write a specific function for it
const Node =
{ ...
, toggleAllOn : (node) =>
Node.make (node.name, true, node.children.map (Node.toggleAllOn))
}
Or, instead of making lots of specific functions, we could make our original Node.toggle and Node.toggleAll more flexible using a parameter
const TOGGLE =
Symbol ()
const Node =
{ make : (name = "", toggled = false, children = []) =>
({ name, toggled, children })
, toggle : (node, value = TOGGLE) =>
Node.make ( node.name
, value === TOGGLE ? !node.toggled : Boolean (value)
, node.children
)
, toggleAll : (node, value = TOGGLE) =>
Node.make ( node.name
, value === TOGGLE ? !node.toggled : Boolean (value)
, node.children.map (n => Node.toggleAll (n, value))
)
}
Now we can toggle a node n using Node.toggle (n) or set a specific toggle state using Node.toggle (n, true) or Node.toggle (n, false)
const n =
Node.make ("foo", true)
console.log (n, Node.toggle (n, true), Node.toggle (n), n)
// { name: 'foo', toggled: true, children: [] } <--- original
// { name: 'foo', toggled: true, children: [] } <--- already true; no change
// { name: 'foo', toggled: false, children: [] } <--- toggled
// { name: 'foo', toggled: true, children: [] } <--- immutable
Of course it works for Node.toggleAll (n, true) too
const allTrue =
Node.toggleAll (data, true)
console.log (allTrue)
// { name : "Misc"
// , toggled : true <--- same value
// , children :
// [ { name : "Apple"
// , toggled : true <--- set to true
// , children :
// [ { name : "banana"
// , toggled : true <--- set to true
// , children : []
// }
// ]
// }
// ]
// }
Program demonstration
const TOGGLE =
Symbol ()
const Node =
{ make : (name = "", toggled = false, children = []) =>
({ name, toggled, children })
, toggle : (node, value = TOGGLE) =>
Node.make ( node.name
, value === TOGGLE ? !node.toggled : value
, node.children
)
, toggleAll : (node, value = TOGGLE) =>
Node.make ( node.name
, value === TOGGLE ? !node.toggled : value
, node.children.map (n => Node.toggleAll (n, value))
)
}
const data =
Node.make ( "Misc"
, true
, [ Node.make ( "Apple"
, false
, [ Node.make ("banana", false) ]
)
]
)
// display original
console.log ('original', data)
// only toggle this node
console.log ('toggle', Node.toggle (data))
// toggle this node and all children
console.log ('toggleAll', Node.toggleAll (data))
// set this node and all children to true
console.log ('toggleAll true', Node.toggleAll (data, true))
// check original data is not mutated (OK!)
console.log ('original', data)

I'm not sure if that's what you want, but I tried.
var obj = {
name: "Misc",
toggled: true,
children: [{
name: "Apple",
toggled:false,
children: [{
name: "banana",
toggled:false
}]
}]
};
var toggleToTrue = function(obj){
if (!obj) { return false; }
obj.toggled = true;
if (!obj.children) { return false; }
var childs = obj.children;
for (var i in childs){
toggleToTrue(childs[i]);
}
};
toggleToTrue(obj);
console.log(obj);

You could use the following for a recursive solution that doesn't mutate the original object, doesn't add properties to objects that didn't have those properties (adding toggled if object doesn't have it) and can be configured by caller to change some behavior:
var obj = {
name: "Misc",
toggled: true,
children: [{
name: "Apple",
toggled:false,
children: [{
name: "banana",
toggled:false
}]
}]
};
const changeProp = (shouldSet,set,doRecursive,rec) => calcValue => (obj) => {
const toggle = o => {
if (shouldSet(o)) {
return set({ ...o },calcValue(o));
}
return o;
};
return (!doRecursive(obj))
? toggle(obj)
: rec(toggle(obj),changeProp(shouldSet,set,doRecursive,rec)(calcValue))
};
const hasChild = o=>(o.hasOwnProperty("children") && Array.isArray(o.children));
const setRecursive = (o,rec)=>({
...o,
children:o.children.map(rec)
});
//set toggled to true
console.log(
changeProp(
o=>(typeof o.toggled === "boolean"),//should set function
(o,value)=>{o.toggled=value;return o; },//set function
hasChild,//do recursive?
setRecursive//recursively set
)(()=>true)//calculate value based on object (value is always true)
(obj)
);
//upper case name
console.log(
changeProp(
o=>(typeof o.name === "string"),//should set function
(o,value)=>{o.name=value;return o; },//set function
hasChild,//do recursive?
setRecursive//recursively set
)((o)=>o.name.toUpperCase())//calculate value based on object (name to uppercase)
(obj)
);

Related

How to change multiple Boolean values()Parent Item, Child Item) in an array on click

I am working in a task where I need to change the Boolean values onclick from an array of items, I need to change the Boolean value from array and child array. I have changed the value from an array it's working as expected but inside that array I am having an another array of objects from that I need to change the boolean value. OnClick I need to make parent isProcessing and child isProcessing false. I have tried changing it but I am not getting the expected output , can anyone guide me how to achieve this thanks in advance.
Mock Data:
const mockItems = [
{
id: '1',
itemType: 'metal',
doneBy: {
id: '1',
display: 'Item Name',
},
catg: 'A',
createdDate: '01/01/2021',
updatedBy: {
id: '1',
type: 'M-A',
},
isProcessing: 'true',
subItems: [
{
id: '1',
doneBy: {
id: '1',
display: 'sub item name',
},
status: {
type: 'notapproved',
},
isProcessing: 'true',
},
],
},
];
Code for accessing parent : isProcessing //it's working
const [processingItem, setProcessingItem] = useState(mockItems);
const handleToggle = () => {
setProcessingItem((prevState) =>
prevState.map((prItem, i) =>
i === 0 ? { ...prItem, isProcessing: false } : prItem
)
);
};
//code to change child array Boolean not working
const handleToggle = () => {
setProcessingItem((prevState) => {
prevState.map((prItem, index) => {
if (index === 0) {
const obj = { ...prItem.subItems, isProcessing: false };
return { ...prItem, isProcessing: false, obj };
}
});
});
};
Try this
const handleToggle = () => {
setProcessingItem((prevState) => {
prevState.map((prItem, index) => {
if(index !=0)
return prItem;
const subItems = prItem.subItems.map((si,idx)=>{
return idx != 0 ? si : {...si,isProcessing: false}
});
return { ...prItem, isProcessing: false, subItems:subItems }
}
)
}
)
}

How to delete an item from my Reducer Object?

I have an object in my reducer like this :
clientData : {
1080 : [{ID : 1111,name : 'John'},{ID : 2222,name : 'Stan'},],
1090 : [{ID : 3333,name : 'Adam'},{ID : 4444,name : 'Alan'},]
}
And I want to be able to delete an item (for example Stan with 2222 ID's)
The problem that is I don't have the key of my property (1080), and I don't have any idea how to achieve it.
I've tried the Object.values(object1) but it convert my object to array and I lost all my architecture.
Have you an idea please ?
Removing from existing objects might not be good, you can use Object.entries and filter to remove id.
If you want to change existing pass data to reducer
const data = {
1080: [
{ ID: 1111, name: "John" },
{ ID: 2222, name: "Stan" },
],
1090: [
{ ID: 3333, name: "Adam" },
{ ID: 4444, name: "Alan" },
],
};
const remove = (id, data) => {
const entries = Object.entries(data);
return entries.reduce((acc, [key, value]) => {
const newValue = value.filter((item) => item.ID !== id);
acc[key] = newValue;
return acc;
// return { ...acc, [key]: newValue };
}, {}); // for updating existing data here
};
console.log(remove(2222, data))

Do not change the checked in the second and third levels in react native

Why in the following code, when sending the ID, the value of the checked object in the second and third levels does not change to true or false !!!
But it works well in the first round
const terms = [
{
"term_id":21,
"name":"Clothing",
"checked":false,
"children":[
{
"term_id":24,
"name":"Accessories",
"checked":false,
"children":[
{
"term_id":25,
"name":"Scarf",
"checked":false,
"children":[
]
}
]
}
]
}
]
const setChecked = (data, id) => {
let newTerms = [];
data.map((item, key) => {
if (item.children && item.children.length) {
newTerms.push({
term_id: item.term_id,
name: item.name,
checked: item.term_id == id ? !item.checked : item.checked,
children: item.children ? item.children : [],
});
setChecked(item.children, id);
} else {
//item.checked = !item.checked;
newTerms.push({
term_id: item.term_id,
name: item.name,
checked: item.term_id == id ? !item.checked : item.checked,
children: item.children ? item.children : [],
});
}
});
return newTerms;
};
Works well with first level ID ==========> setChecked(terms, 21);
But it does not work with second or third level ID or later =========> setChecked(terms, 25);
You have to use your "newTerms" variable as a global variable, because each iteration of your function creates a new emtpy array for your output.

Tree json "menu" with nested items : check the "branch"

I have this JSON tree view that represents a menu :
var menus = [
{
label : "1",
items: [
{
label : "1.1"
},
{
label : "1.2",
items : [
{
label : "1.2.1"
},
{
label : "1.2.2"
}
]
},
{
label : "1.3"
},
]
},
{
label : "2"
}
]
I want to mutate this JSON by adding for each item a selected property. This property, a boolean, will be set to true if the label is the right one or this is the tricky part if the descendant is the right one.
For instance, if I'm looking for label 1.2, all labels 1 and 1.2 will be selected. So I will get this JSON :
var menus = [
{
label : "1",
selected : true,
items: [
{
label : "1.1"
selected : false
},
{
label : "1.2",
selected : true,
items : [
{
label : "1.2.1"
selected : false
},
{
label : "1.2.2",
selected : false
}
]
},
{
label : "1.3",
selected : false
},
]
},
{
label : "2",
selected : false
}
]
the selected : false is not necessary.
Lodash is OK for me;)!
Any suggestions?
edit : where I am ! --> https://codepen.io/anon/pen/XGoXjM?editors=0010
edit 2 : finding elements must not be based on the way I wrote the labels. The labels can be any string... Sorry...
Thanks
This solution uses a for loop to iterate recursively the menu items and their children. If an item is selected, it adds selected: true to the item and it's parents:
const selectMenuItems = menus => selectedLabel => {
const internal = arr => {
let selected = false
for(let i = 0; i < arr.length; i++) {
const item = arr[i]
const childrenSelected = !!item.items && internal(item.items)
item.selected = childrenSelected || item.label === selectedLabel
selected = selected || item.selected
}
return selected
}
internal(menus)
return menus
}
const menus = [{"label":"1","items":[{"label":"1.1"},{"label":"1.2","items":[{"label":"1.2.1"},{"label":"1.2.2"}]},{"label":"1.3"}]},{"label":"2"}]
const menuSelector = selectMenuItems(menus)
const result = menuSelector('1.2')
console.log(result)
.as-console-wrapper { top: 0; max-height: 100% !important; }
I would simply check labels this way:
var menus = [{
label: "1",
items: [{
label: "1.1"
},
{
label: "1.2",
items: [{
label: "1.2.1"
},
{
label: "1.2.2"
}
]
},
{
label: "1.3"
},
]
},
{
label: "2"
}
];
var checkSelected = function(items, search) {
for (var key in items) {
items[key].selected = search.startsWith(items[key].label) && items[key].label.length<=search.length;
if (items[key].items) {
checkSelected(items[key].items, search);
};
};
};
var test = "1.2";
checkSelected(menus, test);
console.log(menus);
Also on JSFiddle.
The startsWith() method determines whether a string begins with the
characters of a specified string, returning true or false as
appropriate.
quoted from here
You can use some recursive approach to implement this.
let str = '1.2.1';
function checkItem(arr, strArr) {
// iterate over the array
arr.forEach((obj) => {
// set selected property based on matching every digit in label in same order
// if digits would be single then you can use startsWith and no need to split string
obj.selected = obj.label.split('.').every((it, i) => it === strArr[i]);
// if nested item is there then call recursively
obj.items && checkItem(obj.items, strArr);
});
return arr;
}
checkItem(menus, str.split('.'));
var menus = [{
label: "1",
items: [{
label: "1.1"
},
{
label: "1.2",
items: [{
label: "1.2.1"
},
{
label: "1.2.2"
}
]
},
{
label: "1.3"
},
]
},
{
label: "2"
}
];
let str = '1.2.1';
function checkItem(arr, strArr) {
arr.forEach((obj) => {
obj.selected = obj.label.split('.').every((it, i) => it === strArr[i]);
obj.items && checkItem(obj.items, strArr);
});
return arr;
}
checkItem(menus, str.split('.'));
console.log(menus);
UPADATE : Since you want to update selected property completely independent of label you can do something like follows. I assume you want to
update based on position in the array.
let str = '1.2.1';
function checkItem(arr, prefixArray, strArr) {
// iterate over the array
arr.forEach((obj, i) => {
// generate new prefix array for checking
let pa = [...prefixArray, i + 1];
// compare prefix array with the string array to check matches
obj.selected = pa.every((it, i) => it == strArr[i]);
// if items defined do it recursively
obj.items && checkItem(obj.items, pa, strArr);
});
return arr;
}
checkItem(menus,[], str.split('.'));
var menus = [{
label: "1",
items: [{
label: "1.1"
},
{
label: "1.2",
items: [{
label: "1.2.1"
},
{
label: "1.2.2"
}
]
},
{
label: "1.3"
},
]
},
{
label: "2"
}
];
let str = '1.2.1';
function checkItem(arr, prefixArray, strArr) {
arr.forEach((obj, i) => {
let pa = [...prefixArray, i + 1];
obj.selected = pa.every((it, i) => it == strArr[i]);
obj.items && checkItem(obj.items, pa, strArr);
});
return arr;
}
checkItem(menus,[], str.split('.'));
console.log(menus);

Javascript multiple condition array filter

I need help putting together an array search that is based on multiple conditions. Furthermore, all the conditions are conditional, meaning I may or may not need to filter on those conditions. What I have:
Array of objects to filter:
var data = [{
"_id" : ObjectId("583f6e6d14c8042dd7c979e6"),
"transid" : 1,
"acct" : "acct1",
"transdate" : ISODate("2012-01-31T05:00:00.000Z"),
"category" : "category1",
"amount" : 103
},
{
"_id" : ObjectId("583f6e6d14c8042dd7c2132t6"),
"transid" : 2,
"acct" : "acct2",
"transdate" : ISODate("2012-01-31T05:00:00.000Z"),
"category" : "category2",
"amount" : 103
},
{
"_id" : ObjectId("583f6e6d14c8042dd7c2132t6"),
"transid" : 3,
"acct" : "acct2",
"transdate" : ISODate("2016-07-31T05:00:00.000Z"),
"category" : "category1",
"amount" : 103
},
{
"_id" : ObjectId("583f6e6d14c8042dd7c2132t6"),
"transid" : 4,
"acct" : "acct2",
"transdate" : ISODate("2012-01-31T05:00:00.000Z"),
"category" : "category2",
"amount" : 103
},
{
"_id" : ObjectId("583f6e6d14c8042dd7c2132t6"),
"transid" : 5,
"acct" : "acct2",
"transdate" : ISODate("2012-01-31T05:00:00.000Z"),
"category" : "category3",
"amount" : 103
},
{
"_id" : ObjectId("583f6e6d14c8042dd7c152g2"),
"transid" : 6,
"acct" : "acct3",
"transdate" : ISODate("2016-10-31T05:00:00.000Z"),
"category" : "category3",
"amount" : 103
}]
I am filtering the above array of objects based on another array of mixed elements. The elements represent the following search fields:
"searchstring": to search on all fields in the data array for any
matched text sequence
object with key values reprsenting account type and a true or false
for value indicating if it should be used to filter
startdate to filter transdate on
enddate to filter transdate
category name to filter category on
The array that has the search conditions looks like this (but if some of the fields are not necessary they will be set to undefined or just an empty string or array):
var filtercondition = {
"p",
{acct1:true,acct2:false,acct3:true...}
"2016-06-01",
"2016-11-30",
"category3"
}
What is the best way to accomplish this? What I've devised is a separate search for each element in the filter array, but this seems non optimal and very tedious. I'm open to a redesign of my setup...
// You wrote that it's an array, so changed the braces
var filtercondition = ["p",
{acct1:true,acct2:false,acct3:true...}
"2016-06-01",
"2016-11-30",
"category3"
];
var filtered = data.filter(o => {
if(filtercondition[0] && !o.category.includes(filtercondition[o])) { // checking just the category, but you can check if any of more fields contains the conditions
return false;
}
if(filtercondition[1]) {
for(var key in filtercondition[1]) {
if(filtercondition[1][key] === true && o.acct != key) {
return false;
}
}
}
if(filtercondition[2] && o.transdate < filtercondition[2]) {
return false;
}
if(filtercondition[3] && o.transdate > filtercondition[3]) {
return false;
}
if(filtercondition[4] && o.category !== filtercondition[4]) {
return false;
}
return true;
});
Two notes:
- changed the braces of filtercondition so that it is an array, however I would suggest to use an object instead.
- this {acct1:true,acct2:false,acct3:true...} sample doesn't make sense for me, since it suggests that the acct field should be acct1 and acct3 at the same time.
Create an array of functions, each function representing a condition.
Here's some sample code which demonstrates the approach...
var conditions = [];
// Dynamically build the list of conditions
if(startDateFilter) {
conditions.push(function(item) {
return item.transdate >= startDateFilter.startDate;
});
};
if(categoryFilter) {
conditions.push(function(item) {
return item.cateogry === categoryFilter.category;
});
};
// etc etc
Once you have an array of conditions, you can use Array.prototype.every to run each condition on an item.
var itemsMatchingCondition = data.filter(function(d) {
return conditions.every(function(c) {
return c(d);
});
});
Or, using the more compact arrow functions:
const itemsMatchingCondition = data.filter(d => conditions.every(c => c(d));
First, you'll want to use brackets for your array not curly braces:
var filtercondition = [
"p",
{acct1:true,acct2:false,acct3:true...},
"2016-06-01",
"2016-11-30",
"category3"
];
Then again, I don't think that an array is the best data type for that. Try an object like this:
var filtercondition = {
query: "p",
accounts: {acct1:true,acct2:false,acct3:true...},
date1: "2016-06-01",
date2: "2016-11-30",
category: "category3"
};
Then, try using Array.prototype.filter:
var filtered = data.filter(function(obj) {
for (var key in filtercondition) {
// if condition not met return false
}
return true;
});
I'd go with a bunch of small granular functions and compose them.
//only some utilities, from the top of my mind
var identity = v => v;
//string-related
var string = v => v == null? "": String(v);
var startsWith = needle => haystack => string(haystack).startsWith(needle);
var endsWith = needle => haystack => string(haystack).endsWith(needle);
var contains = needle => haystack => string(haystack).contains(needle);
//do sth with an object
var prop = key => obj => obj != null && prop in obj? obj[prop]: undefined;
var someProp = fn => obj => obj != null && Object.keys(obj).some(k => fn(k) );
var someValue = fn => obj => obj != null && Object.keys(obj).some(k => fn(obj[k]) );
//logic
var eq = b => a => a === b;
var not = fn => function(){ return !fn.apply(this, arguments) };
var and = (...funcs) => funcs.reduce((a, b) => function(){
return a.apply(this, arguments) && b.apply(this, arguments);
});
var or = (...funcs) => funcs.reduce((a, b) => function(){
return a.apply(this, arguments) || b.apply(this, arguments);
});
//composition
var compose = (...funcs) => funcs.reduce((a, b) => v => return a(b(v)));
var chain = (...funcs) => funcs.reduceRight((a, b) => v => return a(b(v)));
//and whatever else you want/need
//but stay granular, don't put too much logic into a single function
and an example composition:
var filterFn = and(
//some value contains "p"
someValue(contains("p")),
//and
chain(
//property "foo"
prop("foo"),
or(
//either contains "asdf"
contains("asdf"),
//or startsWith "123"
startsWith("123")
)
),
)
since I don't know how you build your filterconditions, I cannot tell you exactly how to parse them into such a composition, but you could compose them like this:
//start with something basic, so we don't ever have to check wether filterFn is null
var filterFn = identity;
//and extend/compose it depending on some conditions
if(/*hasQuery*/){
filterFn = and(
// previous filterFn(obj) && some value on obj contains `query`
filterFn,
someValue(contains(query)))
)
}
if(/*condition*/){
//extend filterFn
filterFn = or(
// (obj.foo === null) || previous filterFn(obj)
chain(prop("foo"), eq(null)),
filterFn
);
}
and so on
First, some points:
Your data object is invalid if you're going to use it in the browser. Probably the data comes from MongoDB, right? Your backend (data source) should have a method to encode it properly and remove ObjectID and ISODate references.
Your filtercondition is not a valid JavaScript object/JSON. Check my example.
So, you can filter your data array with Array#filter method.
Something like that:
let data = [{
"_id" : "583f6e6d14c8042dd7c979e6",
"transid" : 1,
"acct" : "acct1",
"transdate" : "2012-01-31T05:00:00.000Z",
"category" : "category1",
"amount" : 103
},
{
"_id" : "583f6e6d14c8042dd7c2132t6",
"transid" : 2,
"acct" : "acct2",
"transdate" : "2012-01-31T05:00:00.000Z",
"category" : "category2",
"amount" : 103
},
{
"_id" : "583f6e6d14c8042dd7c2132t6",
"transid" : 5,
"acct" : "acct2",
"transdate" : "2012-01-31T05:00:00.000Z",
"category" : "category3",
"amount" : 103
}];
let filterToApply = {
acct: {
acct1: true,
acct2: false,
acct3: true
},
initialDate: "2016-06-01",
finalDate: "2016-11-30",
category: "category3"
}
let filterData = (array, filter) => {
return array.filter( (item) => {
/* here, you iterate each item and compare with your filter,
if the item pass, you must return true. Otherwise, false */
/* e.g.: category check (if present only) */
if (filter.category && filter.category !== item.category)
return false;
}
/* add other criterias check... */
return true;
});
}
let dataFiltered = filterData(data, filterToApply);
console.log(dataFiltered);
If you want to filter an array with multiple conditions and the conditions may be optional, then use the following method.
const data = [
{ name: 'John', age: 25, city: 'New York' },
{ name: 'John', age: 25, city: 'New' },
{ name: 'Jane', age: 32, city: 'Los Angeles' },
{ name: 'Bob', age: 45, city: 'New York' },
{ name: 'Alice', age: 38, city: 'Los Angeles' }
];
const filteredData = (n, c, a) => data.filter(item => {
if (n || c || a) {
return (n ? item.name === n : true) && (c ? item.city === c : true) && (a ? item.age === a : true); // keep adding conditons as much as u want
}
});
console.log(filteredData('John', null, 25));
console.log(filteredData(null, 'Los Angeles', 38));
console.log(filteredData(null, 'Los Angeles', null));
You can chain as many as conditions

Categories

Resources