Filtering an Array of Nested Objects Using Angular Pipes

Angular&NodeEnthusiast
The Startup
Published in
5 min readAug 15, 2020

If you would like to check the implementation of how to filter and sort an array of non-nested objects, you can check the below story:

Consider the below array of JSON objects exported from users.ts

//users.ts
export const users=[{
name”:
{“title”:”Monsieur”,”first”:”Niklas”,”last”:”Philippe”},
dob”:{“date”:”1973–02–17T18:35:33.898Z”,”age”:47},
gender”:”male”,
email”:”niklas.philippe@example.com”,
location”:{“street”:{“number”:2179,”name”:”Avenue Joliot Curie”},”city”:”Aulnay-sous-Bois”,”state”:”Hautes-Pyrénées”,”country”:”France”,”postcode”:37752}
},
{
“name”:{“title”:”Mrs”,”first”:”Nicoline”,”last”:”Jensen”},
“dob”:{“date”:”1959–05–30T12:20:56.272Z”,”age”:61},
“gender”:”female”,”email”:”nicoline.jensen@example.com”,
“location”:{“street”:{“number”:544,”name”:”Poplar Dr”},”city”:”Australian Capital Territory”,”state”:”Queensland”,”country”:”Australia”,”postcode”:2703}
},
{
“name”:{“title”:”Miss”,”first”:”Lilly”,”last”:”Smith”},
“dob”:{“date”:”1995–04–11T08:12:02.912Z”,”age”:25},
“gender”:”female”,”email”:”lily.smith@example.com”,
“location”:{“street”:{“number”:8927,”name”:”Washington Ave”},”city”:”Cairns”,”state”:”Australian Capital Territory”,”country”:”Australia”,”postcode”:4313}
},
{
“name”:{“title”:”Mr”,”first”:”Julio”,”last”:”Ibanez”},
“dob”:{“date”:”1946–10–18T09:54:57.564Z”,”age”:74},
“gender”:”male”,”email”:”julio.ibanez@example.com”,
“location”:{“street”:{“number”:6283,”name”:”Calle de Atocha”},”city”:”Oviedo”,”state”:”Canarias”,”country”:”Spain”,”postcode”:94457}
},
{
“name”:{“title”:”Monsieur”,”first”:”Horst”,”last”:”Bernard”},
“dob”:{“date”:”1969–07–29T22:21:47.381Z”,”age”:51},
“gender”:”male”,”email”:”horst.bernard@example.com”,
“location”:{“street”:{“number”:1217,”name”:”Rue de Cuire”},”city”:”Nîmes”,”state”:”Maine-et-Loire”,”country”:”France”,”postcode”:27584}
},
]

As you can see that name, dob and location are properties that in turn have objects as values.

We shall a create a table with the above JSON data and search for any item in the table using Pipes.

Template:

<input type=”text” [(ngModel)]=”searchTerm” name=”searchTerm” placeholder=”Search”><table class=”table-bordered”>
<tr>
<th>Title</th>
<th>First Name</th>
<th>Last Name</th>
<th>Date</th>
<th>Age</th>
<th>Email</th>
<th>Street</th>
<th>City</th>
<th>State</th>
<th>Country</th>
<th>Postcode</th>
</tr>
<tr *ngFor=”let x of users|filter:searchTerm”>
<td>{{x.name.title}}</td>
<td>{{x.name.first}}</td>
<td>{{x.name.last}}</td>
<td>{{x.dob.date}}</td>
<td>{{x.dob.age}}</td>
<td>{{x.email}}</td>
<td>{{x.location.street.name}} {{x.location.street.number}}</td>
<td>{{x.location.city}}</td>
<td>{{x.location.state}}</td>
<td>{{x.location.country}}</td>
<td>{{x.location.postcode}}</td>
</tr>
</table>

As you can see, we have a textbox to enter the searchTerm and a table constructed with the JSON data.

filter is the selector of the FilterPipe which we shall implement to achieve the search functionality. We are passing 2 arguments to the pipe: The users array and the searchTerm entered in the textbox.

Textbox with Table

Component Class:

import {users} from '../users';export class AppComponent {searchTerm:string=””;
users=[...users];
}

The above class is self-explanatory. We have declared and initialized the ngModel searchTerm and the users array.

Lets now check the Filtering functionality.

import { PipeTransform, Pipe} from ‘@angular/core’;
import { filter } from ‘rxjs/operators’;
import * as _ from ‘lodash/fp’;
@Pipe({name:’filter’})export class FilterPipe implements PipeTransform{transform(items,searchTerm){
if(searchTerm){
let newSearchTerm=!isNaN(searchTerm)? searchTerm.toString(): searchTerm.toString().toUpperCase();return items.filter(item=>{
return this.lookForNestedObject(item,newSearchTerm);
})
}
else{return items;}
}
lookForNestedObject(item,newSearchTerm){
let origItem={…item};
let that=this;
parseNestedObject(item);
function parseNestedObject(item){
for(let key in item){
if(_.isPlainObject(item[key])){
if(origItem[key]) { delete origItem[key]}
parseNestedObject(item[key]);
}
else{
if(origItem[key]) { delete origItem[key]}
origItem[key]=item[key];
}}
}
return that.search(item,origItem,newSearchTerm);
}
search(item,origItem,newSearchTerm){
let filteredList=[];
let prop=””;
for(let koy in origItem){
prop=isNaN(origItem[koy]) ? origItem[koy].toString().toUpperCase() : origItem[koy].toString();if(prop.indexOf(newSearchTerm) > -1){
filteredList.push(item);
return filteredList;
}}
}
}

To help us in this task, I have used the lodash package. You can install the package using:

npm install — save lodash

Import the package in the FilterPipe Class as:

import * as _ from ‘lodash/fp’;

The class has defined 3 methods.

The transform() needs to be defined because the class implements the PipeTransform Interface. It accepts the users array(named as items)and the searchTerm as arguments.

transform(items,searchTerm){
if(searchTerm){
let newSearchTerm=!isNaN(searchTerm)? searchTerm.toString(): searchTerm.toString().toUpperCase();return items.filter(item=>{
return this.lookForNestedObject(item,newSearchTerm);
})
}
else{return items;}
}
  1. Only if the searchTerm has a non-null value, we shall be performing a search, else we shall return the original users list as it is back to the component.
if(searchTerm){
......Search functionality......
}
else{
return items;
}

Now lets check when there is a non-null searchTerm. We have the below logic.

let newSearchTerm=!isNaN(searchTerm)? searchTerm.toString(): searchTerm.toString().toUpperCase();return items.filter(item=>{
return this.lookForNestedObject(item,newSearchTerm);
})

2. If the searchTerm is numeric, convert it into a string and if it is non-numeric,convert it into a string and into uppercase.We assign the modified value to a variable newSearchTerm.

3. Next we are applying the filter method to the users array. The argument item of the filter method will be an object of the array. Each item along with the newSearchTerm is passed into the next method lookForNestedObject().

This method could be slightly tricky, where the concept of closures has been used.

lookForNestedObject(item,newSearchTerm){
let origItem={…item};
let that=this;
parseNestedObject(item);
function parseNestedObject(item){
for(let key in item){
if(_.isPlainObject(item[key])){
if(origItem[key]) { delete origItem[key]}
parseNestedObject(item[key]);
}
else{
if(origItem[key]) { delete origItem[key]}
origItem[key]=item[key];
}}
}
return that.search(item,origItem,newSearchTerm);
}
  1. We are storing a copy of the item object into another block scope variable origItem, because we want to keep the item object intact. Any kind of manipulation will be done to origItem.
let origItem={…item};

2. Closures revolve around the concept of inner functions. We have defined another function named parseNestedObject() within the lookForNestedObject(). The purpose of this function is to identify all the nested objects and their properties in the parent object, delete the nested object from the parent object but add the nested object’s properties back to the parent object.

This gives us a single object with properties having only string or numeric or date values. There are no properties having objects as values.

parseNestedObject(item);function parseNestedObject(item){
for(let key in item){
if(_.isPlainObject(item[key])){
if(origItem[key]) { delete origItem[key]}
parseNestedObject(item[key]);
}
else{
if(origItem[key]) { delete origItem[key]}
origItem[key]=item[key];
}}
}

The function iterates over the item object and checks if any property contains another object as a value.

Lodash method _.isPlainObject() helps us identify if the property’s value is a plain object literal or not.

If yes, we perform 2 actions:

=>Delete the property from the origItem.

=>Call the parseNestedObject() again passing the property’s value i.e object literal as argument.

Keep repeating this until we reach a point where no property has object as value. Once that point is reached, control goes to the else condition and we again perform 2 actions:

=>Delete the property from the origItem.

=>Again add the property and its string/numeric/date value to the origItem.

The purpose is to reconstruct the entire object to help us in the search task.

3. Finally call the search() passing the intact item object, reconstructed origItem object and the newsearchTerm as argument.

search(item,origItem,newSearchTerm){
let filteredList=[];
let prop=””;
for(let koy in origItem){
prop=isNaN(origItem[koy]) ? origItem[koy].toString().toUpperCase() : origItem[koy].toString();if(prop.indexOf(newSearchTerm) > -1){
filteredList.push(item);
return filteredList;
}}
}

Here are are iterating through the reconstructed origItem to check if a property’s value is non-numeric. If yes, convert it into string and then uppercase. If not, just convert it into string. We assign the modified property’s value to a block variable prop.

We are checking if the prop contains a portion or the entire newsearchTerm using the indexOf operator.

If yes, we add the object item whose property value includes the newsearchTerm to an array filteredList and return it back.

Searching based on string “Mo”
Searching based on string “st”
Searching based on number 25

--

--

Angular&NodeEnthusiast
The Startup

Loves Angular and Node. I wish to target issues that front end developers struggle with the most.