I have a complex structured json data that needs to be apply an advanced filtering in my Angular 6 App.
[{
"StudentId": 1,
"StudentName": "Student1",
"Sex":"M",
"Programs": [
{
"StudentId": 1,
"ProgramName": "Java",
"ProgramCategory": "Engineering",
"ProgramStatus": "Full Time"
},
{
"StudentId": 1,
"ProgramName": "HR Management 2",
"ProgramCategory": "HR",
"ProgramStatus": "Part Time"
},
{
"StudentId": 1,
"ProgramName": "Accounting 1",
"ProgramCategory": "Finance",
"ProgramStatus": "Full Time"
}
]
},
{
"StudentId": 2,
"StudentName": "Student2",
"Sex":"F",
"Programs": [
{
"StudentId": 2,
"ProgramName": "HR Management 1",
"ProgramCategory": "HR",
"ProgramStatus": "Part Time"
},
{
"StudentId": 2,
"ProgramName": "Accounting 3",
"ProgramCategory": "Finance",
"ProgramStatus": "Full Time"
}
]
},
{
"StudentId": 3,
"StudentName": "Student3",
"Sex":"F",
"Programs": [
{
"StudentId": 3,
"ProgramName": "Java 3",
"ProgramCategory": "Engineering",
"ProgramStatus": "Full Time"
}
]
},
{
"StudentId": 4,
"StudentName": "Student4",
"Sex":"M",
"Programs": [
{
"StudentId": 4,
"ProgramName": "Java 2",
"ProgramCategory": "Engineering",
"ProgramStatus": "Full Time"
},
{
"StudentId": 4,
"ProgramName": "Accounting 2",
"ProgramCategory": "Finance",
"ProgramStatus": "Part Time"
}
]
},
{
"StudentId": 5,
"StudentName": "Student5",
"Sex":"M",
"Programs": [
{
"StudentId": 5,
"ProgramName": "JavaScript",
"ProgramCategory": "Engineering",
"ProgramStatus": "Part Time"
},
{
"StudentId": 5,
"ProgramName": "HR Management 5",
"ProgramCategory": "HR",
"ProgramStatus": "Full Time"
}
]
}]
I would like to have 3 drop-down list in HTML page to filter by:
The UI View will look like the below:
When I select ProgramCategory = 'HR'
and ProgramStatus = 'Part Time'
, there will be only 2 students (student1
,student2
) returned. I spend days try to get the result that I want, but still not solve. I use this article as ref and made some improvement based on my data, but the returned data is incorrect, see the image below:
So, I only need the marked rows (row#:1,2
) to be returned.
The the marked row#:5 is marked by mistake in the image above.
import { Component, OnInit } from '@angular/core';
import * as _ from 'lodash';
@Component({
selector: 'app-hfo',
templateUrl: './hfo.component.html',
styleUrls: ['./hfo.component.css']
})
export class HfoComponent implements OnInit {
students: any;
filteredStudents: any;
// basic info
Sex: string;
// child info
ProgramCategory: string;
ProgramStatus: string;
// filter by value
filters = { };
constructor() { }
ngOnInit() {
/// get all students
this.students = this.getStudents();
this.setFilters();
}
private setFilters() {
this.filteredStudents = _.filter(this.students, _.conforms(this.filters) );
}
filterMatch(property: string, value: any) {
this.filters[property] = i => i === value;
this.setFilters();
}
filterMatchSub(property: string, childProperty: string, value: any) {
this.filters[property] = val => val.find( child => child[childProperty] === value);
this.setFilters();
}
/// removes filter
removeFilter(property: string) {
delete this.filters[property];
this[property] = null;
this.ProgramCategory = null;
this.ProgramStatus = null;
this.setFilters();
}
private getStudents() {
return JSON.parse(`
[
{
"StudentId": 1,
"StudentName": "Student1",
"Sex":"M",
"Programs": [
{
"StudentId": 1,
"ProgramName": "Java",
"ProgramCategory": "Engineering",
"ProgramStatus": "Full Time"
},
{
"StudentId": 1,
"ProgramName": "HR Management 2",
"ProgramCategory": "HR",
"ProgramStatus": "Part Time"
},
{
"StudentId": 1,
"ProgramName": "Accounting 1",
"ProgramCategory": "Finance",
"ProgramStatus": "Full Time"
}
]
},
{
"StudentId": 2,
"StudentName": "Student2",
"Sex":"F",
"Programs": [
{
"StudentId": 2,
"ProgramName": "HR Management 1",
"ProgramCategory": "HR",
"ProgramStatus": "Part Time"
},
{
"StudentId": 2,
"ProgramName": "Accounting 3",
"ProgramCategory": "Finance",
"ProgramStatus": "Full Time"
}
]
},
{
"StudentId": 3,
"StudentName": "Student3",
"Sex":"F",
"Programs": [
{
"StudentId": 3,
"ProgramName": "Java 3",
"ProgramCategory": "Engineering",
"ProgramStatus": "Full Time"
}
]
},
{
"StudentId": 4,
"StudentName": "Student4",
"Sex":"M",
"Programs": [
{
"StudentId": 4,
"ProgramName": "Java 2",
"ProgramCategory": "Engineering",
"ProgramStatus": "Full Time"
},
{
"StudentId": 4,
"ProgramName": "Accounting 2",
"ProgramCategory": "Finance",
"ProgramStatus": "Part Time"
}
]
},
{
"StudentId": 5,
"StudentName": "Student5",
"Sex":"M",
"Programs": [
{
"StudentId": 5,
"ProgramName": "JavaScript",
"ProgramCategory": "Engineering",
"ProgramStatus": "Part Time"
},
{
"StudentId": 5,
"ProgramName": "HR Management 5",
"ProgramCategory": "HR",
"ProgramStatus": "Full Time"
}
]
}
]
`);
}
}
<div class="row">
<div class="col-sm-12">
<div class="panel panel-sm ">
<div class="panel-body">
<h5>Basic Info</h5>
<div class="hs-lead">
<div class="row">
<div class="col-sm-3">
<div class="form-group">
<label for="exampleSelect1">Sex</label>
<div class="row">
<div class="col-sm-9">
<select class="form-control" [(ngModel)]="Sex" (change)="filterMatch('Sex', Sex)">
<option value="M">M</option>
<option value="F">F</option>
</select>
</div>
<div class="col-sm-3">
<button class="btn btn-primary" *ngIf="Sex" (click)="removeFilter('Sex')">
Clear
</button>
</div>
</div>
</div>
</div>
<div class="col-sm-3">
<div class="form-group">
<label for="exampleSelect1">ProgramCategory</label>
<div class="row">
<div class="col-sm-9">
<select class="form-control" [(ngModel)]="ProgramCategory" (change)="filterMatchSub('Programs', 'ProgramCategory', ProgramCategory)">
<option value="Engineering">Engineering</option>
<option value="HR">HR</option>
<option value="Finance">Finance</option>
</select>
</div>
<div class="col-sm-3">
<button class="btn btn-primary" *ngIf="ProgramCategory" (click)="removeFilter('Programs')">
Clear
</button>
</div>
</div>
</div>
</div>
<div class="col-sm-3">
<div class="form-group">
<label for="exampleSelect1">ProgramStatus</label>
<div class="row">
<div class="col-sm-9">
<select class="form-control" [(ngModel)]="ProgramStatus" (change)="filterMatchSub('Programs', 'ProgramStatus', ProgramStatus)">
<option value="Full Time">Full Time</option>
<option value="Part Time">Part Time</option>
</select>
</div>
<div class="col-sm-3">
<button class="btn btn-primary" *ngIf="ProgramStatus" (click)="removeFilter('Programs')">
Clear
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div class="panel panel-xl">
<div class="panel-body">
<h5>Result
<span class="badge badge-info badge-pill pull-right">{{ filteredStudents.length }}</span>
</h5>
<div class="hs-lead">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>#</th>
<th>Name</th>
<th>Sex</th>
<th>Programs</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of filteredStudents ">
<td>{{item.StudentId }}</td>
<td>{{item.StudentName }}</td>
<td>{{item.Sex}}</td>
<td>
{{item.Programs.length}}
<ol *ngFor="let obj of item.Programs">
<li>{{obj.ProgramCategory}} / {{obj.ProgramStatus}}</li>
</ol>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
Could anyone help me to achieve my goal?
You can change my current ts code or have a new solution, both are welcomed!
Thanks a lot!
I've got a solution for you here using reactive forms and rxjs BehaviorSubjects:
https://stackblitz.com/edit/how-to-filter-complex-json-data-new-chind-array-object-xtlbxy
That link has your full solution, but here's the core of the filtering issue I think you had:
private setFilters() {
this.filteredStudents$.next(this.students$.value);
combineLatest(
this.students$,
this.sexFilterControl.valueChanges,
this.programControls.valueChanges,
this.courseControls.valueChanges
)
.subscribe(([students, sexFilter, programFilters, courseFilters]) => {
let filteredStudents = [ ... students ];
if (sexFilter) {
filteredStudents = filteredStudents.filter(student => student.Sex === sexFilter);
}
// programs
filteredStudents = filteredStudents.filter(student => {
return student.Programs.reduce((programsPrev, program) => {
return programsPrev || Object.entries(programFilters).reduce((filterPrev, [filterName, filterValue]) => {
if (!filterValue) {
return filterPrev;
}
return filterPrev && program[filterName] === filterValue;
}, true);
}, false)
});
// courses
filteredStudents = filteredStudents.filter(student => {
return student.Courses.reduce((coursesPrev, course) => {
return coursesPrev || Object.entries(courseFilters).reduce((filterPrev, [filterName, filterValue]) => {
if (!filterValue) {
return filterPrev;
}
return filterPrev && course[filterName] === filterValue;
}, true);
}, false)
});
this.filteredStudents$.next(filteredStudents);
});
this.sexFilterControl.setValue('');
this.programCategoryFilterControl.setValue('');
this.programStatusFilterControl.setValue('');
this.courseCategoryFilterControl.setValue('');
this.courseStatusFilterControl.setValue('');
}
Filtering for both ProgramCategory and ProgramStatus (where both have to match for the same Program) is a fundamentally different filter than filtering for either separately.
Since you what you want with your two program filters is essentially "only show students who have at least 1 Program that matches all existing filters", you can see in my stack blitz that I group the relevant controls into a FormGroup
and write filters that reflect this intended behavior.
If you're up for it, I'd recommend adjusting your table to using @angular/cdk/table
, I'm actually working on an article on that topic now with the guy from Angular Firebase (like in the link you posted). I think that'd be well-worth the effort, especially if you like this more rxjs-centric approach I used in this solution.
Set your filters and then call the following method with the appropriate values.
const people = [{
"StudentId": 1,
"StudentName": "Student1",
"Sex": "M",
"Programs": [
{
"StudentId": 1,
"ProgramName": "Java",
"ProgramCategory": "Engineering",
"ProgramStatus": "Full Time"
},
{
"StudentId": 1,
"ProgramName": "HR Management 2",
"ProgramCategory": "HR",
"ProgramStatus": "Part Time"
},
{
"StudentId": 1,
"ProgramName": "Accounting 1",
"ProgramCategory": "Finance",
"ProgramStatus": "Full Time"
}
]
},
{
"StudentId": 2,
"StudentName": "Student2",
"Sex": "F",
"Programs": [
{
"StudentId": 2,
"ProgramName": "HR Management 1",
"ProgramCategory": "HR",
"ProgramStatus": "Part Time"
},
{
"StudentId": 2,
"ProgramName": "Accounting 3",
"ProgramCategory": "Finance",
"ProgramStatus": "Full Time"
}
]
},
{
"StudentId": 3,
"StudentName": "Student3",
"Sex": "F",
"Programs": [
{
"StudentId": 3,
"ProgramName": "Java 3",
"ProgramCategory": "Engineering",
"ProgramStatus": "Full Time"
}
]
},
{
"StudentId": 4,
"StudentName": "Student4",
"Sex": "M",
"Programs": [
{
"StudentId": 4,
"ProgramName": "Java 2",
"ProgramCategory": "Engineering",
"ProgramStatus": "Full Time"
},
{
"StudentId": 4,
"ProgramName": "Accounting 2",
"ProgramCategory": "Finance",
"ProgramStatus": "Part Time"
}
]
},
{
"StudentId": 5,
"StudentName": "Student5",
"Sex": "M",
"Programs": [
{
"StudentId": 5,
"ProgramName": "JavaScript",
"ProgramCategory": "Engineering",
"ProgramStatus": "Part Time"
},
{
"StudentId": 5,
"ProgramName": "HR Management 5",
"ProgramCategory": "HR",
"ProgramStatus": "Full Time"
}
]
}];
const findFilteredStudents = (students, sex, category, status) => {
const foundStudents = students.filter(student => {
// if sex is set as a filter, compare students to it
if (sex && student.sex !== sex) {
return false;
}
// if category is a filter, return false if a student
// does not have the category
if (category) {
const hasCategory = student.Programs.find(Program => Program.ProgramCategory === category);
if (!hasCategory) {
return false;
}
}
// if status is a filter, return false if a student
// does not have the status
if (status) {
const hasStatus = student.Programs.find(Program => Program.ProgramStatus === status);
if (!hasStatus) {
return false;
}
}
return true;
});
return foundStudents;
};
const students = findFilteredStudents(people, null, 'HR', 'Part Time');
students.forEach(student => {
console.log(student);
})
Because the key of this.filters[property] is always Programs, you're always overwriting the previous selection. For that reason it will only ever apply the latest of the 2 sub-filters.
Instead, you should check if a filter is already defined for this.filters[property]
. If it is, make sure it is also checked.
You can modify your filterMatchSub
like so:
filterMatchSub(property: string, childProperty: string, value: any) {
let existing = (val) => true; // Define a function that always returns true
// If a filter is already defined, hold a reference to it in existing
if (this.filters[property]) {
existing = this.filters[property];
}
// Call the existing function as well
this.filters[property] = val => val.find( child => child[childProperty] === value) && existing(val);
this.setFilters();
}
Here is a Stackblitz demo
Here is my full take on how this should be handled. Full working exmaple on stackblitz.
Module:
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
@NgModule({
imports: [BrowserModule, FormsModule, ReactiveFormsModule],
declarations: [AppComponent],
bootstrap: [AppComponent]
})
export class AppModule { }
Component:
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
import { STUDENTS } from './students';
interface FilterFormValue {
sex: string;
category: string;
status: string;
}
interface Program {
studentId: number;
programName: string;
programCategory: string;
programStatus: string;
}
export interface Student {
studentId: number;
studentName: string;
sex: string;
programs: Array<Program>;
}
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
students: Array<Student> = [];
filteredStudents: Array<Student> = [];
sexOptions: Array<string> = [];
programCategoryOptions: Array<string> = [];
programStatusOptions: Array<string> = [];
filterForm: FormGroup;
constructor(private formBuilder: FormBuilder) { }
ngOnInit() {
this.getStudents();
}
private getStudents() {
// you would get students from an API in a real word scenario, now we just simply initialize it here
// I put the data in a different file for convinience
this.students = STUDENTS;
// also setting filtered students to all of the students to display all of them at the start
this.filteredStudents = this.students;
// again, normally you would get these options from the backend but here we simply reduce our array of students
this.getSexOptions();
this.getProgramCategoryOptions();
this.getProgramStatusOptions();
// when we get all our data initialize the filter form
this.initFilterForm();
}
private getSexOptions() {
// get all unique values from array of students
this.sexOptions = Array.from(new Set(this.students.map((student: Student) => student.sex)));
}
private getProgramCategoryOptions() {
// this is a little bit trickier and normally you get these from the backend
// but suffice it to say that at the end we get all unique values for program categories
const categoryGroups = this.students.map((student: Student) => {
return student.programs.map((program: Program) => program.programCategory);
});
this.programCategoryOptions = Array.from(new Set(categoryGroups.reduce((a, b) => a.concat(b))));
}
private getProgramStatusOptions() {
// same as categories, we get all unique values for program statuses
const statusGroups = this.students.map((student: Student) => {
return student.programs.map((program: Program) => program.programStatus);
});
this.programStatusOptions = Array.from(new Set(statusGroups.reduce((a, b) => a.concat(b))));
}
private initFilterForm() {
// initialize the form with empty strings, in html the 'All' option will be selected
this.filterForm = this.formBuilder.group({
sex: [''],
category: [''],
status: ['']
});
// init watch for any form changes
this.watchFormChanges();
}
private watchFormChanges() {
// this will fire on any filter changes and call the filtering method with the value of the form
this.filterForm.valueChanges.subscribe((value: FilterFormValue) => this.filterStudents(value));
}
private filterStudents(value: FilterFormValue) {
// again, this operation would be executed on the backend, but here you go
// initialize a new array of all the students
let filteredStudents: Array<Student> = this.students;
if (value.sex) {
// if filter for sex is set, simply filter for any student that has the same value for sex
filteredStudents = filteredStudents.filter((student: Student) => student.sex === value.sex);
}
if (value.category && !value.status) {
// when category is set but status is not, filter for any student that has the category in any of its programs
filteredStudents = filteredStudents.filter((student: Student) => {
return student.programs
.map((program: Program) => program.programCategory)
.includes(value.category);
});
}
if (!value.category && value.status) {
// when status is set but category is not, filter for any student that has the status in any of its programs
filteredStudents = filteredStudents.filter((student: Student) => {
return student.programs
.map((program: Program) => program.programStatus)
.includes(value.status);
});
}
if (value.category && value.status) {
// when category and status is both set, filter for any student that has the status AND category in any of its programs
filteredStudents = filteredStudents.filter((student: Student) => {
return student.programs
.filter((program: Program) => program.programCategory === value.category)
.map((program: Program) => program.programStatus)
.includes(value.status);
});
}
// set the filtered students to display
this.filteredStudents = filteredStudents;
}
}
HTML:
<div class="row">
<div class="col-sm-12">
<div class="panel panel-sm ">
<div class="panel-body">
<h5>Basic Info</h5>
<div class="hs-lead">
<form [formGroup]="filterForm">
<div class="row">
<div class="col-sm-4">
<div class="form-group">
<label for="exampleSelect1">Sex</label>
<div class="row">
<div class="col-sm-9">
<select class="form-control" formControlName="sex">
<option value="">All</option>
<option *ngFor="let option of sexOptions" [value]="option">{{ option }}</option>
</select>
</div>
<div class="col-sm-3">
<button class="btn btn-primary" *ngIf="filterForm && !!filterForm.get('sex').value" (click)="filterForm.get('sex').setValue('')">Clear</button>
</div>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="form-group">
<label for="exampleSelect1">ProgramCategory</label>
<div class="row">
<div class="col-sm-9">
<select class="form-control" formControlName="category">
<option value="">All</option>
<option *ngFor="let option of programCategoryOptions" [value]="option">{{ option }}</option>
</select>
</div>
<div class="col-sm-3">
<button class="btn btn-primary" *ngIf="filterForm && !!filterForm.get('category').value" (click)="filterForm.get('category').setValue('')">Clear</button>
</div>
</div>
</div>
</div>
<div class="col-sm-4">
<div class="form-group">
<label for="exampleSelect1">ProgramStatus</label>
<div class="row">
<div class="col-sm-9">
<select class="form-control" formControlName="status">
<option value="">All</option>
<option *ngFor="let option of programStatusOptions" [value]="option">{{ option }}</option>
</select>
</div>
<div class="col-sm-3">
<button class="btn btn-primary" *ngIf="filterForm && !!filterForm.get('status').value" (click)="filterForm.get('status').setValue('')">Clear</button>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12">
<div class="panel panel-xl">
<div class="panel-body">
<h5>Result
<span class="badge badge-info badge-pill pull-right">{{ filteredStudents.length }}</span>
</h5>
<div class="hs-lead">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>#</th>
<th>Name</th>
<th>Sex</th>
<th>Programs</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let student of filteredStudents">
<td>{{ student.studentId }}</td>
<td>{{ student.studentName }}</td>
<td>{{ student.sex }}</td>
<td>
{{ student.programs.length }}
<ol *ngFor="let program of student.programs">
<li>{{ program.programCategory }} / {{ program.programStatus }}</li>
</ol>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With