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];
Related
I want to make a generic filter function. Currently I have a function that looks like this:
const filterRows = () => {
items.filter((item) => {
if(selectedDrinks.length > 0 && selectIds.length > 0) {
return selectedDrinks.includes(item.description) && selectedIds.includes(item.props.id)
}else if(selectedDrinks.length > 0) {
return selectedDrinks.includes(item.description)
}else if(selectedIds.length > 0) {
return selectedIds.includes(item.props.id)
}
}
}
The number of if checks I need to do will grow exponentially if I add one more thing to filter by.
I've made a pathetic try below. One issue I encountered is if I have a nested structure and want to access ["props/id"] as I don't know the syntax for it. Also tried ["props:id"] etc. And if I add multiple strings in the query it does not work either. And even if I could add multiple strings properly it would only work as an OR.
And for me it would be selectedDrinks && selectedId as both need to match for it to filter, not selectedDrinks || selectedIds
I want to include everything in both selectedDrinks and selectedIds as a query, and they should filter only if both are included in "assets" as description and props:id. I should also be able to add e.g "selectedNames" as a third "query parameter".
const selectedDrinks: string[] = [
"cola",
"fanta",
]
const selectedIds : string[] = [
"5",
"4",
]
interface s {
description: string;
name: string;
props: {
id: string
}
}
const items: s[] = [
{
description: "cola",
name: "computer",
props: {
id: "4"
}
},
{
description: "fanta",
name: "laptop",
props: {
id: "5"
}
},
{
description: "sprite",
name: "phone",
props: {
id: "6"
}
}
]
export function genericFilter<T>(
object: T,
filters: Array<keyof T>,
query: string[]
):boolean {
if(query.length === 0)
return true
return filters.some(filter => {
const value = object[filter]
console.log(value)
if(typeof value === "string") {
return value.toLowerCase().includes(query.map(q => q.toLowerCase()).join(""))
}
if(typeof value === "number") {
return value.toString().includes(query.map(q => q.toLowerCase()).join(""))
}
return false
})
}
const myFilterResult = items.filter((asset) => genericFilter(item, ["props", "name"], ["5"]))
console.log(myFilterResult)
If anyone is interested, here is how I solved it.
/**
*
* #returns A new list of filtered objects
* #param objects The objects that we want to filter
* #param properties The properties we want to apply on the object and compare with the query
* #param queries The queries we want to filter by
*/
export function genericFilter<T>(
objects: T[],
properties: Array<keyof T>,
queries: Array<string>[] | Array<number>[]
):T[] {
return objects.filter((object) => {
var count = 0;
properties.some((props) => {
const objectValue = object[props]
if(typeof objectValue === "string" || typeof objectValue === "number") {
queries.forEach((query) => {
query.forEach((queryValue) => {
if(queryValue === objectValue) {
count+=1;
}
})
})
}
})
return count === properties.length;
})
}
export default genericFilter;
How you call the function, can include X amount of filters and strings to search for.
const result = genericFilter(assets, ["description", "id", "name"], [selectedAssetTypes, selectedIds, selectedNames])
We have a lot of arrays that store objects of key/value pairs for dropdowns and other items in our app.
We also use next-i18next for our translations and I'm trying to create a helper function that can receive 1...n of these, transform the values to use the translation function and destructure them in a single line, as opposed to calling the hook multiple times
type OptionType = { text: string, value: string };
const untransformedMap1: OptionType[] = [
{ text: 'settings:preferences', value: 'preferences' },
{ ... }
];
// eg:
const [map1, map2, ...] = useAddTranslationsToOpts(untransformedMap1, untransformedMap2, ...);
// instead of
const map1 = useAddTranslationsToOpts(untransformedMap1);
const map2 = useAddTranslationsToOpts(untransformedMap2);
// ...
so far what I have:
import { useTranslation } from 'next-i18next';
function useAddTranslationsToOptions<T extends OptionType[]>(...opts: T[]): [...T] {
const { t } = useTranslation();
const arr: T[] = [];
opts.forEach((map: T) => {
const transformed = map.reduce((acc: T, item) => [...acc, { text: t(item.text), value: item.value }], []);
arr.push(transformed)
});
return arr;
}
this isn't working but I think should give an idea of what I'm after here? Haven't got my head around rest spread tuple types yet but I would imagine they would solve this?
I'm not sure what you're after exactly, but maybe this helps:
function useAddTranslationsToOptions<T extends OptionType>(...opts: T[][]): T[][] {
const { t } = useTranslation();
const arr: T[][] = [];
opts.forEach((map: T[]) => {
const transformed = map.reduce<T[]>((acc, item) => [...acc, { ...item, text: t(item.text) }], []);
arr.push(transformed)
});
return arr;
}
One of the problems was that { text: t(item.text), value: item.value } is not compatible with T (although it is with OptionType).
This can be solved by using the spread operator which will add all keys and values of item to make it compatible with T:
{ ...item, text: t(item.text) }
I have a tree object which is an irregular tree which children's names and key values can change everytime I run my code. For example:
{
addressRouter: 192.168.0.1,
addresses:
{
address1: 'A',
},
{
address2: 'B',
},
{
ports: [
{
portA: 'C',
portB: null
},
}
route: 'D',
}
so the names: 'addressRouter', 'addresses', 'address1', etc and their keys are unpredictable but I need to convert the tree object in arrays with the following format:
addressRouter
addresses/address1
addresses/address2
addresses/ports/portA
addresses/ports/portB
route
and then have their keys next to them.
I have this function to construct the tree, which is correct:
const iterate = (obj, obj2) => {
Object.keys(obj).forEach(key => {
obj2[key] = obj[key];
if (typeof obj[key] === 'object') {
iterate(obj[key], obj2)
}
})
}
but after debugging, I realized it doesn't get all branches.
We can use a recursive function to traverse the tree and get the keys in the required format.
I am assuming addresses in the given tree object is an array of objects
function processTree(obj, rootKey) {
const arr = [];
obj && Object.keys(obj).forEach(key => {
const val = obj[key];
if (val && val instanceof Array) {
val.forEach(item => arr.push(...processTree(item, key)))
}else if (val && typeof(val) == "object") {
arr.push(...processTree(val, key));
}else {
arr.push(key);
}
});
return rootKey ? arr.map(item => rootKey + "/" + item) : arr;
}
console.log(processTree(tree, null));
Result : ["addressRouter", "addresses/address1", "addresses/address2", "addresses/ports/portA", "addresses/ports/portB", "route"]
I just write the code below, see if it's what you want.
const tree = {
addressRouter: '192.168.0.1',
addresses: [
{
address1: 'A',
},
{
address2: 'B',
},
{
ports: [
{
portA: 'C',
portB: null,
},
],
},
],
route: 'D',
};
const traverse = (input) => {
const resultList = [];
const isEndPoint = (obj) => {
return typeof obj !== 'object' || obj === null;
};
const buildPath = (currentPath, key) =>
currentPath === '' ? key : `${currentPath}/${key}`;
const innerTraverse = (tree, currentPath = '') => {
if (tree !== null && typeof tree === 'object') {
Object.entries(tree).forEach(([key, value]) => {
if (isEndPoint(value)) {
resultList.push(buildPath(currentPath, key));
return;
}
let path = currentPath;
if (!Array.isArray(tree)) {
path = buildPath(currentPath, key);
}
innerTraverse(value, path);
});
}
};
innerTraverse(input);
return resultList;
};
console.log(traverse(tree));
/**
* [
* 'addressRouter',
* 'addresses/address1',
* 'addresses/address2',
* 'addresses/ports/portA',
* 'addresses/ports/portB',
* 'route'
* ]
*/
I've just found the loop I needed it here, which is:
function* traverse(o,path=[]) {
for (var i of Object.keys(o)) {
const itemPath = path.concat(i);
yield [i,o[i],itemPath];
if (o[i] !== null && typeof(o[i])=="object") {
//going one step down in the object tree!!
yield* traverse(o[i],itemPath);
}
}
}
Then, if the tree object (first gray box in my question) were name, for instance, "params", I would do this:
if (params != null && params != undefined) {
for(var [key, value, path] of traverse(params)) {
// do something here with each key and value
if (typeof value == 'string'){
var tempName = '';
for (name in path) {
//console.log(path[name])
p= tempName += path[name] + "/"
}
console.log(p, value)
}
}
}
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))
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.