J'ai des données json structurées complexes qui doivent être appliquées à un filtrage avancé dans mon 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"
}
]
}]
Je voudrais avoir 3 liste déroulante dans la page HTML pour filtrer par:
La vue de l'interface utilisateur ressemblera à celle ci-dessous:
Lorsque je sélectionne ProgramCategory = 'HR'
et ProgramStatus = 'Part Time'
, il n'y aura que 2 étudiants (student1
, student2
) retournés. Je passe des jours à essayer d’obtenir le résultat que je veux, mais toujours pas à résoudre. J'utilise cet article comme référence et j'apporte des améliorations en fonction de mes données, mais les données renvoyées sont incorrectes, voir l'image ci-dessous: Je n'ai donc besoin que des lignes marquées (row#:1,2
) pour être renvoyées.
La rangée #: 5 est marquée par erreur dans l'image ci-dessus.
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>
Quelqu'un pourrait-il m'aider à atteindre mon objectif?
Vous pouvez changer mon code ts actuel ou avoir une nouvelle solution, les deux sont les bienvenus!
Merci beaucoup!
J'ai une solution pour vous ici en utilisant des formes réactives et des objets BehaviorSubjects de rxjs:
https://stackblitz.com/edit/how-to-filter-complex-json-data-new-chind-array-object-xtlbxy
Ce lien a votre solution complète, mais voici l'essentiel du problème du filtrage, je pense que vous aviez:
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('');
}
Filtrer à la fois pour ProgramCategory et ProgramStatus (où les deux doivent correspondre pour le même programme) est un filtre fondamentalement différent du filtrage séparé.
Comme vous voulez ce que vous voulez avec vos deux filtres de programme est essentiellement de "montrer uniquement aux étudiants qui ont au moins un programme qui correspond à tous les filtres existants", vous pouvez voir dans mon blitz de pile que je regroupe les contrôles pertinents dans une FormGroup
et écrit des filtres qui reflètent ce comportement prévu.
Si vous le souhaitez, je vous recommande d’ajuster votre tableau à l’utilisation de @angular/cdk/table
, je travaille actuellement sur un article sur ce sujet avec le type de Angular Firebase (comme dans le lien que vous avez posté). Je pense que cela en vaudrait la peine, surtout si vous aimez cette approche plus centrée sur les risques que j'ai utilisée dans cette solution.
Définissez vos filtres, puis appelez la méthode suivante avec les valeurs appropriées.
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);
})
Étant donné que la clé de this.filters [propriété] est toujours Programmes, vous écrasez toujours la sélection précédente. Pour cette raison, seul le dernier des 2 sous-filtres est appliqué.
Au lieu de cela, vous devriez vérifier si un filtre est déjà défini pour this.filters[property]
. Si c'est le cas, assurez-vous qu'il est également coché.
Vous pouvez modifier votre filterMatchSub
comme ceci:
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();
}
Voici mon point de vue complet sur la façon dont cela devrait être géré. Exemple de travail complet sur 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 { }
Composant:
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>