Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to filter complex structured Json data in Angular 6

I have a complex structured json data that needs to be apply an advanced filtering in my Angular 6 App.

JSON Data:

[{
    "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"
        }
    ]
 }]

Filter Options:

I would like to have 3 drop-down list in HTML page to filter by:

  1. Sex
  2. ProgramCategory
  3. ProgramStatus

HTML View:

The UI View will look like the below: enter image description here

Wanted Result:

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: enter image description here 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.

My ts code:

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"
              }
          ]
       }
  ]
    `);
  }

}

My HTML Code:

<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>

Help:

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!

like image 744
Lester Avatar asked Jul 12 '18 20:07

Lester


4 Answers

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.

like image 178
ZackDeRose Avatar answered Oct 11 '22 13:10

ZackDeRose


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);
})
like image 36
VtoCorleone Avatar answered Oct 11 '22 14:10

VtoCorleone


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

like image 30
user184994 Avatar answered Oct 11 '22 15:10

user184994


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>
like image 31
bodorgergely Avatar answered Oct 11 '22 13:10

bodorgergely