I have a FiltersAccordion component to manage table filtering. This component is pretty complex due to the fact that the users must be able to group filters in AND & OR groups. My current file structure is:
FiltersAccordion
: manages the overall “save filters” logic.FiltersBlock
: displays a list of filters grouped by an AND.Filter
: each one of the filters, consists of one or more inputs/selects/checkbox.
The goal is that each one of those Filters
can be drag&dropped from a FilterBlock
to another one. The main problem right now is that the “new filter” added to the drop destination block retains all the correct data, but is not reflecting it in the form values in the DOM.
This is the code for the FilterBlock
component. Here filterGroups
is an array of FormGroups which is looped in the <ul>
:
// filters-block.component.html
<ul
class="filter__group__list"
(dragover)="handleDragOver($event)"
(drop)="handleDrop($event)"
>
<li
class="filter__group__item"
draggable="true"
*ngFor="let group of filterGroups; let idx = index"
>
<app-filter
(change)="handleUpdateFilter($event, idx)"
[columnsService]="columnsService"
[data]="data"
[defaultValues]="group"
(dragStart)="handleDragStart($event, idx)"
(dragEnd)="handleDragEnd($event, idx)"
(removeFilter)="handleRemoveFilter(idx)"
></app-filter>
</li>
</ul>
// filters-block.component.ts
// * Filters grouped by AND operator
export class FiltersBlockComponent implements OnInit {
@Output() dragStart = new EventEmitter<{ event: DragEvent; item: number }>();
@Output() dragEnd = new EventEmitter<{ event: DragEvent; item: number }>();
@Output() removeBlock = new EventEmitter<void>();
public filterGroups: FormGroup<FilterGroupTO>[];
constructor() {}
ngOnInit() {
this.filterGroups = [
new FormGroup<FilterFormGroupTO>({
checkBox: new FormControl<boolean | null>(false),
field: new FormControl<FilterableColumnsTO | null>(null),
relation: new FormControl<string | null>(''),
value: new FormControl<FilterableColumnsTO | null>(null),
}),
];
}
handleUpdateFilter(filter: FilterFormGroupTO, index: number) {
this.filterGroups[index].patchValue(filter as any);
}
handleRemoveFilter(index: number) {
this.filterGroups.splice(index, 1);
if (this.filterGroups.length === 0) {
this.removeBlock.emit();
}
}
handleDragStart(event: DragEvent, index: number) {
this.dragStart.emit({ event, item: index });
}
handleDragEnd(event: DragEvent, index: number) {
this.dragEnd.emit({ event, item: index });
}
handleDragOver(event: DragEvent) {
event.preventDefault();
if (event.dataTransfer) event.dataTransfer.dropEffect = 'move';
}
handleDrop(event: DragEvent) {
event.preventDefault();
if (event.dataTransfer) {
const filterData = event.dataTransfer.getData('filter');
const properFilter = JSON.parse(filterData);
const newGroup = new FormGroup<FilterFormGroupTO>({
checkBox: new FormControl<boolean | null>(properFilter.checkBox),
field: new FormControl<FilterableColumnsTO | null>(properFilter.field),
relation: new FormControl<string | null>(properFilter.relation),
value: new FormControl<FilterableColumnsTO | null>(properFilter.value),
});
this.filterGroups.push(newGroup);
}
}
}
The Filter component contains all the form logic and inputs:
<div
[formGroup]="filterFormGroup"
class="filter__group__item"
[draggable]="enableDrag"
(dragstart)="handleDragStart($event)"
(dragend)="handleDragEnd($event)"
>
<select
formControlName="field"
>
<option
*ngFor="let option of filtersColumns"
[displayValue]="option.fieldName"
[readValue]="option"
>{{ option.fieldName }}</option
>
</select>
<select
formControlName="relation"
>
<option
*ngFor="
let option of filterFormGroup.controls['field'].value?.operations;
let i = index
"
[readValue]="option"
>{{
"DIALOGS.FILTERS.RELATION." + option | translate
}}</option
>
</select>
<select
formControlName="value"
>
<option
*ngFor="let option of fieldDistincValues; let i = index"
[displayValue]="option"
[readValue]="option"
>{{ option }}</option
>
</select>
<input
formControlName="value"
type="text"
></input>
</div>
<app-toggle
formControlName="checkBox"
[label]="'DIALOGS.FILTERS.CHECKBOX' | translate"
></app-toggle>
</div>
// filter.component.ts
export interface FilterFormGroupTO {
field: FormControl<FilterableColumnsTO | null>;
relation: FormControl<string | null>;
value: FormControl<FilterableColumnsTO | null>;
checkBox: FormControl<boolean | null>;
}
export class FilterComponent implements OnInit {
@Output() change = new EventEmitter<FilterFormGroupTO>();
@Output() dragStart = new EventEmitter<DragEvent>();
@Output() dragEnd = new EventEmitter<DragEvent>();
@Output() removeFilter = new EventEmitter<void>();
@Input() defaultValues: FormGroup<FilterFormGroupTO>;
// filters
private selectedFilters: FilterTO[] = [];
public availableFilters: Columns[] = [];
// form
public filterFormGroup: FormGroup<FilterFormGroupTO>;
constructor(filtersService: FiltersService) {}
ngOnInit() {
// Initialize form. NOT WORKING
this.filterFormGroup = new FormGroup<FilterFormGroupTO>({
checkBox: new FormControl<boolean | null>(
this.defaultValues.value.checkBox as boolean | null
),
field: new FormControl<FilterableColumnsTO | null>(
this.defaultValues.value.field as FilterableColumnsTO | null
),
relation: new FormControl<string | null>(
this.defaultValues.value.relation as string | null
),
value: new FormControl<FilterableColumnsTO | null>(
this.defaultValues.value.value as FilterableColumnsTO | null
),
});
// Patch form values. NOT WORKING
this.filterFormGroup.patchValues({
checkBox: this.defaultValues.value.checkBox as boolean | null,
field: this.defaultValues.value.field as FilterableColumnsTO | null,
relation: this.defaultValues.value.relation as string | null,
value: this.defaultValues.value.value as FilterableColumnsTO | null,
});
// Get available filters
filtersService().subscribe((res) => {
this.availableFilters = res;
});
// Changes in form listener
this.filterFormGroup.valueChanges.subscribe((value) => {
this.change.emit(value as unknown as FilterFormGroupTO);
});
}
handleRemoveFilter() {
this.removeFilter.emit();
}
handleDragStart(event: DragEvent) {
const fieldValue = this.filterFormGroup.value['field'];
const checkboxValue = Boolean(this.filterFormGroup.value['checkBox']);
const relationValue = this.filterFormGroup.value['relation'];
const valueValue = this.filterFormGroup.value['value'];
const data = {
checkBox: checkboxValue,
field: fieldValue,
relation: relationValue,
value: valueValue,
};
if (this.enableDrag) {
event.dataTransfer?.setData('filter', JSON.stringify(data));
if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move';
this.dragStart.emit(event);
}
}
handleDragEnd(event: DragEvent) {
if (this.enableDrag) {
this.dragEnd.emit(event);
if (event.dataTransfer?.dropEffect === 'move') {
this.handleRemoveFilter();
}
}
}
}
As I pointed out, the ngOnInit
of the Filter
component does does carry the correct data when I log it. Even when I console.log(this.filterFormGroup)
I’m getting the correct values. Why are they not being rendered in the DOM?
Am I approaching this the wrong way? I’m new to Angular forms and this is the best I could manage. Thanks.