I have the following utility method: it removes all the empty keys of a payload object.
Here is the code:
const removeEmptyKeysUtil = (payload: any): any => {
Object.keys(payload).map(
(key): any => {
if (payload && payload[key] === '') {
delete payload[key];
}
return false;
}
);
return payload;
};
export default removeEmptyKeysUtil;
But I get the following eslint error:
Assignment to property of function parameter 'payload'.eslint(no-param-reassign)
It was suggested to me that I use either object destructuring or Object.assign. But I am a little confused on how to do that.
For example, destructuring:
if (payload && payload[key] === '') {
const {delete payload[key], ...actualPayload} = payload;
}
return false;
But I get this error:
Block-scoped variable 'payload' used before its declaration.
I know, I can disable the rule, but I do not want to do that. I want to properly code that branch
Can you help me a little bit? I don't think I understand those 2 concepts at all. Thank you.
Lint is warning you to fulfill one of the properties called "immutability".
So when you receive a parameter to use in this function (which is an object) indicates that what you return from that function is a new object with the modifications you want but that this is a new object.
PD: In addition, if you use Typescript and you know what that payload is made of, it would be best if you created an interface with its data rather than assigning any because it can help you select internal properties and avoid errors, as well as the response it will return.
One solution could be this:
const removeEmptyKeysUtil = (payload: any): any =>
Object.keys(payload)
.filter(key => payload[key] !== "")
.reduce((result, key) => ({ ...result, [key]: payload[key] }), {});
export default removeEmptyKeysUtil;
I know this answer is probably not exactly what you were looking for. Since your code will perform badly on complicated object I created a quick solution which will give the result you wanted. I hope it helps.
function isEmptyObject(obj) {
if (!obj || typeof obj !== 'object') return false;
if (obj.constructor === Array) return obj.length === 0;
return Object.keys(obj).length === 0 && obj.constructor === Object;
}
function removeEmptyKeysUtil(obj) {
if (!obj) return {};
Object.keys(obj).map(key => {
// Add additional check here for null, undefined, etc..
if (obj[key] === '') delete obj[key];
if (obj.constructor === Object) {
obj[key] = removeEmptyKeysUtil(obj[key])
}
if (obj.constructor === Array) {
for (let i = obj.length; i >= 0; i--) {
obj[i] = removeEmptyKeysUtil(obj[i])
if (isEmptyObject(obj[i])) {
obj.splice(i, 1);
}
}
}
if (isEmptyObject(obj[key])) {
delete obj[key];
}
})
return obj;
}
const obj = {
test: '11',
test1: '1',
test2: {
test: '',
test1: ''
},
test3: [
{
test: ''
},
{
test: ''
},
{
test: '3'
},
{
test33: {
test: '1',
test1: ''
}
}
]
};
console.log(removeEmptyKeysUtil(obj))
Related
I have here a map function for an array of object
and also added some condition
userList.map((item) => {
const newFilter = dailyData.filter((value) => {
return value.author == item.MSM;
});
let obj_idx = userList.findIndex(
(obj) => obj.MSM == newFilter[0]?.author
);
const newArr = userList?.map((obj, idx) => {
if (idx == obj_idx) {
return {
...obj,
storeTimeIn: newFilter[0]?.store,
timeIn: newFilter[0]?.date_posted,
storeTimeOut: newFilter[newFilter.length - 1]?.store,
timeOut: newFilter[newFilter.length - 1]?.date_posted,
};
} else {
return obj;
}
});
console.log(newArr);
setAttendanceData(newArr);
});
that just check if the Item exist in the array before updating it.
and this condition here works fine
if (idx == obj_idx) {
return {
...obj,
storeTimeIn: newFilter[0]?.store,
timeIn: newFilter[0]?.date_posted,
storeTimeOut: newFilter[newFilter.length - 1]?.store,
timeOut: newFilter[newFilter.length - 1]?.date_posted,
};
}
as seen in this picture
but when my condition becomes false the whole array of object becomes empty again
my hunch is I'm setting the state wrongly . which appear in the setAttendanceData(newArr)
this state is just an empty array state const [attendanceData, setAttendanceData] = useState([]);. is there a way to not update the whole array of object when the condition gets false like how can I use spread operator in this situation. TIA
I feel you need to add a little more description including the current react component and little bit about your inputs/outputs for a more refined answer.
userList.map((item, index, self) => {
const newFilter = dailyData.find((value) => {
return value.author === item.MSM;
});
// this is only needed if there can be multiple
// occurrence of same obj.MSM in 'userList' array
let obj_idx = self.findIndex((obj) => obj.MSM === newFilter[0] ? .author);
// why are you doing this
const newArr = self ? .map((obj, idx) => {
if (idx == obj_idx) {
return {
...obj,
storeTimeIn: newFilter[0] ? .store,
timeIn: newFilter[0] ? .date_posted,
storeTimeOut: newFilter[newFilter.length - 1] ? .store,
timeOut: newFilter[newFilter.length - 1] ? .date_posted,
};
} else {
return obj;
}
});
console.log(newArr);
// why are you doing this , updating state on every iteration
// after user.map attendanceData will only reflect for the last index in userList
setAttendanceData(newArr);
});
But if my assumptions are correct, this following code snippet may help you in some way.
setAttendanceData(
// make sure to run setAttendanceData only once
userList.map((item, index, self) => {
const newFilter = dailyData.find((value) => {
return value.author === item.MSM;
});
const obj_idx = self.findIndex((obj) => obj.MSM === newFilter[0] ? .author);
// there can be refinement around this but i feel this is what you need
if (index === obj_idx) {
return {
...obj,
storeTimeIn: newFilter[0] ? .store,
timeIn: newFilter[0] ? .date_posted,
storeTimeOut: newFilter[newFilter.length - 1] ? .store,
timeOut: newFilter[newFilter.length - 1] ? .date_posted,
};
} else {
return obj;
}
})
);
First, I would re-examine this logic. You're calling map inside of map on the same array, which is confusing. The top level map is used more like a forEach than a map because nothing is done with the output. You're also updating state on each iteration of the loop. I'm not sure how React handles this situation.
Try to only set state once the data is in the shape you want. Ideally it would only require one pass through the array. Here is an example:
const [attendanceData, setAttendanceData] = useState([]);
const [userList, setUserList] = useState([
{ MSM: "Edmond" },
{ MSM: "Dantes" },
{ MSM: "Conan" },
]);
const [dailyData, setDailyData] = useState([
{ author: "No one", store: "store", date_posted: "a date" },
{ author: "Edmond", store: "store", date_posted: "a date" },
{ author: "Conan", store: "store", date_posted: "a date" },
]);
useEffect(() => {
const mappedUserList = userList.map((user) => {
// Find the daily data author that matches the current user.
const day = dailyData.find((value) => {
return value.author == user.MSM;
});
if (!day) {
// If no author matched in daily data, return the user unchanged.
return user;
} else {
return {
...user,
storeTimeIn: day.store,
timeIn: day.date_posted,
// I'll leave these lines for you to figure out
// since I don't understand why you're getting the last element of the filtered data.
// storeTimeOut: filteredDailyData[filteredDailyData.length - 1]?.store,
// timeOut: filteredDailyData[filteredDailyData.length - 1]?.date_posted,
};
}
});
setAttendanceData(mappedUserList);
}, [userList, dailyData]);
console.log(attendanceData);
I have a small React + Typescript application. I am running a filtering function which is executed when some buttons are clicked or a value is entered into an input.
I have interfaces everywhere. I also passed the interface to the state, or the fact that I will have an empty array. But I keep getting an error with the following text:
"TS2345: Argument of type 'TodoItemInterface[] | undefined' is not assignable to parameter of type 'TodoItemInterface[]'. Type 'undefined' is not assignable to type 'TodoItemInterface[]'."
I can’t find how and where to pass that I definitely won’t have undefined or fix this error in a different way. Please help. Below I send the code of the function in useEffect and functions with a state that are related to it.
error is thrown here in filterItems.
useEffect(() => {
const newArray = searchInputFilter(filterItems(todoListStateArray, infoSearchObject.filter), textForSearch);
setVisibleTodoListItems(newArray);
}, [infoSearchObject, todoListStateArray, textForSearch])
Another code
const filterItems = (arr: TodoItemInterface[], filter: string) => {
if (filter === 'all') {
return arr;
} else if (filter === 'active') {
return arr.filter((item) => (!item.done));
} else if (filter === 'done') {
return arr.filter((item) => item.done);
}
};
const searchInputFilter = (arr: TodoItemInterface[], search: string) => {
if (search.length === 0) {
return arr;
}
return arr.filter((item) => {
return item.subtitle.toLowerCase().includes(search.toLowerCase());
});
}
const [infoSearchObject, setInfoSearchObject] = useState({inWork: '', done: '', filter: 'all'});
const [todoListStateArray, setTodoListStateArray] = useState<TodoItemInterface[] | []>(todoListDefault);
const [visibleTodoListItems, setVisibleTodoListItems] = useState<TodoItemInterface[] | []>(todoListDefault);
const [textForSearch, setTextForSearch] = useState('');
That is actually one of the benefits in using typescript; It’s actually telling you there is the possibility that your filterItems can possibly return undefined, and that happens when neither of your conditions are true.
As a solution, you should add a return value when none of the conditions are met:
const filterItems = (arr: TodoItemInterface[], filter: string): TodoItemInterface[] => {
if (filter === 'all') {
return arr;
} else if (filter === 'active') {
return arr.filter((item) => (!item.done));
} else if (filter === 'done') {
return arr.filter((item) => item.done);
}
return arr;
};
To note that in the example I also added a return type to your function; That is pretty important to ensure consistency in what get actually returned from it.
I am trying to update a state value w/ a multidimensional array but I can't seem to figure how to update one of the arrays object key values without effecting the previous state value which I use later in the process after the dispatch call.
I the code below the payload carries an array of ids (nodes) that I loop through and change the only those objects within the state object. Rather straight forward, but updating a multidimensional array of objects and not effecting the state has me confused.
UPDATE_RESTRICTION: (curState, payload) => {
const updatedNodes = {...curState.layout}
const accessProfile = BpUAE.accessProfileID
payload.nodes.forEach((node, index) => {
if (typeof (updatedNodes[node].settings.bp_uae_restrictions) === 'undefined') {
updatedNodes[node].settings.bp_uae_restrictions = {};
}
if (typeof (updatedNodes[node].settings.bp_uae_restrictions[accessProfile]) === 'undefined') {
updatedNodes[node].settings.bp_uae_restrictions[accessProfile] = {};
}
updatedNodes[node].settings.bp_uae_restrictions[accessProfile].is_node_restricted = JSON.parse(payload.isRestricted);
})
return {layout: updatedNodes}
}
Please let me know if you need more information and thanks for any help you can provide.
You are not applying immutable update pattern correctly, you are mutating the nested references pointing into the current state object. You need to create new object references and shallow copy all state that is being updated.
UPDATE_RESTRICTION: (curState, payload) => {
const updatedNodes = { ...curState.layout }
const accessProfile = BpUAE.accessProfileID;
payload.nodes.forEach((node, index) => {
if (typeof (updatedNodes[node].settings.bp_uae_restrictions) === 'undefined') {
updatedNodes[node] = {
...updatedNodes[node],
settings: {
...updatedNodes[node].settings,
bp_uae_restrictions: {},
},
};
}
if (typeof (updatedNodes[node].settings.bp_uae_restrictions[accessProfile]) === 'undefined') {
updatedNodes[node] = {
...updatedNodes[node],
settings: {
...updatedNodes[node].settings,
bp_uae_restrictions: {
...updatedNodes[node].settings.bp_uae_restrictions,
[accessProfile]: {},
},
},
};
}
// now all the new references have been created and previous
// state shallow copied, you can update the deeply nested
// `is_node_restricted` property.
updatedNodes[node].settings.bp_uae_restrictions[accessProfile].is_node_restricted = JSON.parse(payload.isRestricted);
});
return {
...curState,
layout: updatedNodes,
};
}
UPDATE: Added the last immutable pattern
const updatedNodes = { ...curState.layout }
const accessProfile = BpUAE.accessProfileID;
payload.nodes.forEach((node, index) => {
if (typeof (updatedNodes[node].settings.bp_uae_restrictions) === 'undefined') {
updatedNodes[node] = {
...updatedNodes[node],
settings: {
...updatedNodes[node].settings,
bp_uae_restrictions: {},
},
};
}
if (typeof (updatedNodes[node].settings.bp_uae_restrictions[accessProfile]) === 'undefined') {
updatedNodes[node] = {
...updatedNodes[node],
settings: {
...updatedNodes[node].settings,
bp_uae_restrictions: {
...updatedNodes[node].settings.bp_uae_restrictions,
[accessProfile]: {},
},
},
};
}
// now all the new references have been created and previous
// state shallow copied, you can update the deeply nested
// `is_node_restricted` property.
updatedNodes[node] = {
...updatedNodes[node],
settings: {
...updatedNodes[node].settings,
bp_uae_restrictions: {
...updatedNodes[node].settings.bp_uae_restrictions,
[accessProfile]: {
...updatedNodes[node].settings.bp_uae_restrictions[accessProfile],
is_node_restricted: JSON.parse(payload.isRestricted)
},
},
},
};
});
return {
...curState,
layout: updatedNodes,
};
}```
Check your return statement, you have to include your previous state and override layout like shown below.
return {...curState, layout: updatedNodes}
I am trying to filter an array by string, but when I do it modifies the original array.
My code is like this:
private copy: MenuItem[];
#Input() links: MenuItem[];
ngOnInit(): void {
this.copy = this.links;
}
public filter(target: MenuItem): void {
console.log(target);
let link = this.links.find((link: MenuItem) => link.path === target.path);
if (!link) return;
console.log(link);
this.copy.forEach((item: MenuItem) => {
if (item.path !== link.path) return;
let children = [...item.children];
link.childrenCount = children.length;
console.log(children.length);
console.log(item.children.length);
link.children = children.filter((child: MenuItem) => child.label.indexOf(item.filterChildren) > -1);
});
}
As you can see, I try to clone the children of the copied array, but when I try to filter, both item.children and children are modified. I don't want the items in the copy array to ever change, just the one in links.
I have tried a few ways. The way I thought would work was this:
public filter(target: MenuItem): void {
let link = this.links.find((link: MenuItem) => link.path === target.path);
if (!link) return;
this.copy.forEach((item: MenuItem) => {
if (item.path !== link.path) return;
link.childrenCount = item.children.length;
link.children = [...item.children.map((o) => ({ ...o }))].filter(
(child: MenuItem) => child.label.indexOf(item.filterChildren) > -1,
);
console.log(item.children.length);
console.log(link.children.length);
});
}
But it doesn't. Both item.children.length and link.children.length return the length of the filtered array, not the original size.
Can anyone help?
PS: This is the MenuItem model:
export class MenuItem {
label: string;
path: string;
filterChildren?: string;
open?: boolean;
children?: MenuItem[];
childrenCount?: number;
}
That's because objects (arrays as well, since they're also objects in a specific way) in JS are cloned by reference not values. The best way to do so:
const clone = JSON.parse(JSON.stringify(original));
that wont mutate the original object if changes (even deep ones) occur in the clone.
In your case :
this.copy = JSON.parse(JSON.stringify(this.links));
You need to perform a total copy of your data in order to separate them.
There are multiple stackoverflow post about how to clone an array of value.
First, look at the following snippet where I reproduced the error.
Now with the fix : snippet
Code :
interface MenuItem {
label: string;
path: string;
filterChildren?: string;
open?: boolean;
children?: MenuItem[];
childrenCount?: number;
}
function cloneSomething<T>(something: T): T {
//
// Handle null, undefined and simple values
if (something === null || typeof something === 'undefined') return something;
//
// Handle Array object and every values in the array
if (something instanceof Array) {
return something.map(x => cloneSomething(x)) as unknown as T;
}
//
// Handle the copy of all the types of Date
if (something instanceof Date) {
return new Date(something.valueOf()) as unknown as T;
}
//
// Handle all types of object
if (typeof (something as any).toJSON === 'function') {
return (something as any).toJSON();
}
if (typeof something === 'object') {
return Object.keys(something as any)
.reduce((tmp, x) => ({
...tmp,
[x]: cloneSomething((something as any)[x]),
}), {}) as unknown as T;
}
// No effect to simple types
return something;
}
class Foo {
public copy: MenuItem[] = [];
public links: MenuItem[] = [
{
label: 'A',
path: 'A',
},
{
label: 'B',
path: 'B',
filterChildren: 'alex',
children: [
{
label: 'catherine',
path: 'B',
},
{
label: 'alexis',
path: 'D',
},
],
childrenCount: 2,
},
];
constructor() {
this.copy = cloneSomething(this.links);
}
public filter(target: MenuItem): void {
let link = this.links.find((link: MenuItem) => link.path === target.path);
if (!link) {
return;
}
this.copy.forEach((item: MenuItem) => {
if (item.path !== link?.path) {
return;
}
const children = [
...(item.children ?? []),
];
link.childrenCount = children.length;
link.children = children.filter((child: MenuItem) => child.label.indexOf(item?.filterChildren ?? '') > -1);
});
}
}
const foo = new Foo();
foo.filter((foo as any).links[1]);
console.log('COPY', foo.copy);
console.log('LINKS', foo.links);
When you say this.copy = this.links, you are creating a copy of the reference to an existing array. This mean any changes on either of the variables will reflect on the other (it is the same reference).
If you want to change only links and not copy, then copy must be a deep copy of links, not just a link to the same reference.
this.copy = [];
this.links.forEach(item => {
let copiedItem = {...item};
copiedItem.children = {...item.children};
this.copy.push(copiedItem);
})
You can also include filter here already:
this.copy = [];
this.links.filter(item => item.path === target.path).forEach(item => {
let copiedItem = {...item};
copiedItem.children = {...item.children};
this.copy.push(copiedItem);
})
Instead of
this.copy = this.links;
You can use
Object.assign(this.coppy,this.links);
or
this.coppy = [...this.links];
I have a unit test that is producing something I didn't expect:
Background: I'm making a simple todo list with Angular/test driven development.
Problem: When I call editTask on an item in the array, it changes the item's value. But, I don't see how it's changed in the original array because the original array is never accessed in the method I'm testing. Please help me connect HOW the original array is being changed? It seems Object.assign is doing this, but why?
describe('editTask', () => {
it('should update the task by id', () => {
const dummyTask1 = { id: 1, name: 'test', status: false };
service.tasks.push(dummyTask1); //refers to TestBed.get(TaskService)
const id = 1;
const values = { name: 'cat', status: false };
service.editTask(id, values);
console.log(service.tasks); // why does this log this object? [Object{id: 1, name: 'cat', status: false}]
expect(service.tasks[0].name).toEqual(values.name); // Test passes
});
});
Here is the method I'm testing:
editTask(id, values) {
const task = this.getTask(id);
if (!task) {
return;
}
Object.assign(task, values); //How does this line change the array?
return task;
}
getTask(id: number) {
return this.tasks.filter(task => task.id === id).pop(); //is this altering the original array somehow?
}
If needed, here's the full Angular service:
export class TaskService {
tasks: any = [];
lastId = 0;
constructor() { }
addTask(task) {
if (!task.id) {
task.id = this.lastId + 1;
}
this.tasks.push(task);
}
editTask(id, values) {
const task = this.getTask(id);
if (!task) {
return;
}
Object.assign(task, values);
return task;
}
deleteTask(id: number) {
this.tasks = this.tasks.filter(task => task.id !== id);
}
toggleStatus(task) {
const updatedTask = this.editTask(task.id, { status: !task.status});
return updatedTask;
}
getTasks() {
return of(this.tasks);
}
getTask(id: number) {
return this.tasks.filter(task => task.id === id).pop();
}
}
Here is the github repo: https://github.com/capozzic1/todo-tdd
The getTask() method is getting a reference to the item in the array using the array filter() method.
It then uses Object.assign() to change the properties of the item. The Object.assign() method is used to copy the values of all enumerable own properties from one or more source objects to a target object. It will return the target object.
So now the values of the reference in memory of the item is changed. Because it is a reference in memory you will see the original item being changed.