Skip to content

Commit

Permalink
[angular] improve filter component
Browse files Browse the repository at this point in the history
  • Loading branch information
mshima committed Aug 16, 2022
1 parent 80c7b87 commit e0415ad
Show file tree
Hide file tree
Showing 5 changed files with 184 additions and 119 deletions.
Original file line number Diff line number Diff line change
@@ -1,23 +1,18 @@
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { IFilterableComponent, IFilterOptions } from './filter.model';
import { Component, Input } from '@angular/core';
import { IFilterOptions } from './filter.model';

@Component({
selector: '<%= jhiPrefixDashed %>-filter',
templateUrl: './filter.component.html',
})
export class FilterComponent implements IFilterableComponent {
export class FilterComponent {
@Input() filters!: IFilterOptions;

@Output() filterChange = new EventEmitter<IFilterOptions>();

clearAllFilters(): void {
this.filters.clear();
this.filterChange.emit();
}

clearFilter(filterName: string, value: string): void {
if (this.filters.getFilterOptionByName(filterName)?.removeValue(value)) {
this.filterChange.emit();
}
this.filters.removeFilter(filterName, value);
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { convertToParamMap, ParamMap, Params } from '@angular/router';
import { FilterOptions, IFilterOptions, IFilterOption, FilterOption } from './filter.model';
import { FilterOptions, FilterOption } from './filter.model';

describe('FilterModel Tests', () => {
describe('FilterOption', () => {
let filterOption: IFilterOption;
let filterOption: FilterOption;

beforeEach(() => {
filterOption = new FilterOption('foo', ['bar', 'bar2']);
Expand Down Expand Up @@ -75,48 +75,94 @@ describe('FilterModel Tests', () => {
});

describe('clear', () => {
it('removes filters', () => {
it("removes empty filters and dosn't emit next element", () => {
const filters = new FilterOptions([new FilterOption('foo'), new FilterOption('bar')]);
jest.spyOn(filters.filterChanges, 'next');

filters.clear();

expect(filters.filterChanges.next).not.toBeCalled();
expect(filters.filterOptions).toMatchObject([]);
});
});
it('removes empty filters and emits next element', () => {
const filters = new FilterOptions([new FilterOption('foo', ['existingFoo1']), new FilterOption('bar')]);
jest.spyOn(filters.filterChanges, 'next');

describe('clone', () => {
it('returns an identical FilterOptions', () => {
const filters = new FilterOptions([new FilterOption('foo', ['aValue', 'anotherValue']), new FilterOption('bar')]);
expect(filters.clone()).toMatchObject(filters);
filters.clear();

expect(filters.filterChanges.next).toHaveBeenCalledTimes(1);
expect(filters.filterOptions).toMatchObject([]);
});
});

describe('equals', () => {
it('returns true for identical FilterOptions', () => {
const filters = new FilterOptions([new FilterOption('foo', ['aValue', 'anotherValue']), new FilterOption('bar')]);
const otherFilters = new FilterOptions([new FilterOption('foo', ['aValue', 'anotherValue']), new FilterOption('bar')]);
expect(filters.equals(otherFilters)).toBe(true);
describe('addFilter', () => {
it('adds a non existing FilterOption, returns true and emit next element', () => {
const filters = new FilterOptions([new FilterOption('foo', ['existingFoo1', 'existingFoo2']), new FilterOption('bar')]);
jest.spyOn(filters.filterChanges, 'next');

const result = filters.addFilter('addedFilter', 'addedValue');

expect(result).toBe(true);
expect(filters.filterChanges.next).toHaveBeenCalledTimes(1);
expect(filters.filterOptions).toMatchObject([
{ name: 'foo', values: ['existingFoo1', 'existingFoo2'] },
{ name: 'addedFilter', values: ['addedValue'] },
]);
});
it('adds a non existing value to FilterOption, returns true and emit next element', () => {
const filters = new FilterOptions([new FilterOption('foo', ['existingFoo1', 'existingFoo2']), new FilterOption('bar')]);
jest.spyOn(filters.filterChanges, 'next');

const result = filters.addFilter('foo', 'addedValue1', 'addedValue2');

expect(result).toBe(true);
expect(filters.filterChanges.next).toHaveBeenCalledTimes(1);
expect(filters.filterOptions).toMatchObject([
{ name: 'foo', values: ['existingFoo1', 'existingFoo2', 'addedValue1', 'addedValue2'] },
]);
});
it('returns false for different FilterOptions', () => {
const filters = new FilterOptions([new FilterOption('foo', ['aValue', 'anotherValue']), new FilterOption('bar')]);
const otherFilters = new FilterOptions([new FilterOption('foo', ['aValue', 'anotherValue']), new FilterOption('bar', ['bar'])]);
expect(filters.equals(otherFilters)).toBe(false);
it("doesn't add FilterOption values already added, returns false and doesn't emit next element", () => {
const filters = new FilterOptions([new FilterOption('foo', ['existingFoo1', 'existingFoo2']), new FilterOption('bar')]);
jest.spyOn(filters.filterChanges, 'next');

const result = filters.addFilter('foo', 'existingFoo1', 'existingFoo2');

expect(result).toBe(false);
expect(filters.filterChanges.next).not.toBeCalled();
expect(filters.filterOptions).toMatchObject([{ name: 'foo', values: ['existingFoo1', 'existingFoo2'] }]);
});
});

describe('getFilterOptionByName', () => {
it('finds the option if exists', () => {
const fooOption = new FilterOption('foo');
const filters = new FilterOptions([fooOption, new FilterOption('bar')]);
expect(filters.getFilterOptionByName('foo')).toStrictEqual(fooOption);
describe('removeFilter', () => {
it('removes an existing FilterOptions and returns true', () => {
const filters = new FilterOptions([new FilterOption('foo', ['existingFoo1', 'existingFoo2']), new FilterOption('bar')]);
jest.spyOn(filters.filterChanges, 'next');

const result = filters.removeFilter('foo', 'existingFoo1');

expect(result).toBe(true);
expect(filters.filterChanges.next).toHaveBeenCalledTimes(1);
expect(filters.filterOptions).toMatchObject([{ name: 'foo', values: ['existingFoo2'] }]);
});
it("doesn't remove a non existing FilterOptions values returns false", () => {
const filters = new FilterOptions([new FilterOption('foo', ['existingFoo1', 'existingFoo2']), new FilterOption('bar')]);
jest.spyOn(filters.filterChanges, 'next');

it("doesn't finds the option if doesn't exists", () => {
const filters = new FilterOptions();
expect(filters.getFilterOptionByName('bar')).toBe(null);
const result = filters.removeFilter('foo', 'nonExisting1');

expect(result).toBe(false);
expect(filters.filterChanges.next).not.toBeCalled();
expect(filters.filterOptions).toMatchObject([{ name: 'foo', values: ['existingFoo1', 'existingFoo2'] }]);
});
it("doesn't remove a non existing FilterOptions returns false", () => {
const filters = new FilterOptions([new FilterOption('foo', ['existingFoo1', 'existingFoo2']), new FilterOption('bar')]);
jest.spyOn(filters.filterChanges, 'next');

it('passing true to add, creates the option', () => {
const filters = new FilterOptions();
expect(filters.getFilterOptionByName('bar', true)).toMatchObject({ name: 'bar' });
const result = filters.removeFilter('nonExisting', 'nonExisting1');

expect(result).toBe(false);
expect(filters.filterChanges.next).not.toBeCalled();
expect(filters.filterOptions).toMatchObject([{ name: 'foo', values: ['existingFoo1', 'existingFoo2'] }]);
});
});

Expand All @@ -143,43 +189,49 @@ describe('FilterModel Tests', () => {
'filter[hello.notIn]': ['world3', 'world4'],
};

it('should parse from Params if there are any', () => {
const filters: IFilterOptions = new FilterOptions();

it('should parse from Params if there are any and not emit next element', () => {
const filters: FilterOptions = new FilterOptions([new FilterOption('foo', ['bar'])]);
jest.spyOn(filters.filterChanges, 'next');
const paramMap: ParamMap = convertToParamMap(oneValidParam);

filters.initializeFromParams(paramMap);

expect(filters.filterChanges.next).not.toHaveBeenCalled();
expect(filters.filterOptions).toMatchObject([{ name: 'hello.in', values: ['world'] }]);
});

it('should parse from Params and have none if there are none', () => {
const filters: IFilterOptions = new FilterOptions();

const filters: FilterOptions = new FilterOptions();
const paramMap: ParamMap = convertToParamMap(noValidParam);
jest.spyOn(filters.filterChanges, 'next');

filters.initializeFromParams(paramMap);

expect(filters.filterChanges.next).not.toHaveBeenCalled();
expect(filters.filterOptions).toMatchObject([]);
});

it('should parse from Params and have a parameter with 2 values', () => {
const filters: IFilterOptions = new FilterOptions();
it('should parse from Params and have a parameter with 2 values and one aditional value', () => {
const filters: FilterOptions = new FilterOptions([new FilterOption('hello.in', ['world'])]);
jest.spyOn(filters.filterChanges, 'next');

const paramMap: ParamMap = convertToParamMap(paramWithTwoValues);

filters.initializeFromParams(paramMap);

expect(filters.filterChanges.next).not.toHaveBeenCalled();
expect(filters.filterOptions).toMatchObject([{ name: 'hello.in', values: ['world', 'world2'] }]);
});

it('should parse from Params and have a parameter with 2 keys', () => {
const filters: IFilterOptions = new FilterOptions();
const filters: FilterOptions = new FilterOptions();
jest.spyOn(filters.filterChanges, 'next');

const paramMap: ParamMap = convertToParamMap(paramWithTwoKeys);

filters.initializeFromParams(paramMap);

expect(filters.filterChanges.next).not.toHaveBeenCalled();
expect(filters.filterOptions).toMatchObject([
{ name: 'hello.in', values: ['world', 'world2'] },
{ name: 'hello.notIn', values: ['world3', 'world4'] },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,20 @@
import { ParamMap } from '@angular/router';
import { Subject } from 'rxjs';

export interface IFilterOptions {
filterOptions: IFilterOption[];
readonly filterChanges: Subject<FilterOption[]>;
get filterOptions(): IFilterOption[];
hasAnyFilterSet(): boolean;
clear(): void;
getFilterOptionByName(name: string, add: true): IFilterOption;
getFilterOptionByName(name: string, add: false): IFilterOption | null;
getFilterOptionByName(name: string): IFilterOption | null;
clear(): boolean;
initializeFromParams(params: ParamMap): boolean;
equals(other: IFilterOptions): boolean;
clone(): IFilterOptions;
addFilter(name: string, ...values: string[]): boolean;
removeFilter(name: string, value: string): boolean;
}

export interface IFilterOption {
name: string;
values: string[];
isSet(): boolean;
nameAsQueryParam(): string;
addValue(...values: string[]): boolean;
removeValue(value: string): boolean;
equals(other: IFilterOption): boolean;
}

export interface IFilterableComponent {
clearFilter(filterName: string, value?: string): void;
clearAllFilters(): void;
}

export class FilterOption implements IFilterOption {
Expand Down Expand Up @@ -59,6 +49,10 @@ export class FilterOption implements IFilterOption {
return true;
}

clone(): FilterOption {
return new FilterOption(this.name, this.values.concat());
}

equals(other: IFilterOption): boolean {
return (
this.name === other.name &&
Expand All @@ -70,58 +64,34 @@ export class FilterOption implements IFilterOption {
}

export class FilterOptions implements IFilterOptions {
filterOptions: IFilterOption[];

constructor(filterOptions: IFilterOption[] = []) {
this.filterOptions = filterOptions;
}
readonly filterChanges: Subject<FilterOption[]> = new Subject();
private _filterOptions: FilterOption[];

hasAnyFilterSet(): boolean {
return this.filterOptions.length > 0 && this.filterOptions.some(e => e.isSet());
constructor(filterOptions: FilterOption[] = []) {
this._filterOptions = filterOptions;
}

clear(): void {
this.filterOptions = [];
get filterOptions(): FilterOption[] {
return this._filterOptions.filter(option => option.isSet());
}

getFilterOptionByName(name: string, add: true): IFilterOption;
getFilterOptionByName(name: string, add: false): IFilterOption | null;
getFilterOptionByName(name: string): IFilterOption | null;
getFilterOptionByName(name: string, add = false): IFilterOption | null {
const addOption = (option: IFilterOption): IFilterOption => {
this.filterOptions.push(option);
return option;
};

return this.filterOptions.find(thisOption => thisOption.name === name) ?? (add ? addOption(new FilterOption(name)) : null);
hasAnyFilterSet(): boolean {
return this._filterOptions.some(e => e.isSet());
}

equals(other: IFilterOptions): boolean {
const thisFilters = this.filterOptions.filter(option => option.isSet());
const otherFilters = other.filterOptions.filter(option => option.isSet());
if (thisFilters.length !== otherFilters.length) {
return false;
clear(): boolean {
const hasFields = this.hasAnyFilterSet();
this._filterOptions = [];
if (hasFields) {
this.changed();
}
return (
thisFilters.every(option => other.getFilterOptionByName(option.name)?.equals(option)) &&
otherFilters.every(option => this.getFilterOptionByName(option.name)?.equals(option))
);
}

clone(): IFilterOptions {
const newObject: FilterOptions = new FilterOptions();

this.filterOptions.forEach(option => {
newObject.filterOptions.push(new FilterOption(option.name, option.values.concat()));
});

return newObject;
return hasFields;
}

initializeFromParams(params: ParamMap): boolean {
const oldFilters: IFilterOptions = this.clone();
const oldFilters: FilterOptions = this.clone();

this.clear();
this._filterOptions = [];

const filterRegex = /filter\[(.+)\]/;
params.keys
Expand All @@ -133,6 +103,54 @@ export class FilterOptions implements IFilterOptions {
}
});

return !oldFilters.equals(this);
if (oldFilters.equals(this)) {
return false;
}
return true;
}

addFilter(name: string, ...values: string[]): boolean {
if (this.getFilterOptionByName(name, true).addValue(...values)) {
this.changed();
return true;
}
return false;
}

removeFilter(name: string, value: string): boolean {
if (this.getFilterOptionByName(name)?.removeValue(value)) {
this.changed();
return true;
}
return false;
}

protected changed(): void {
this.filterChanges.next(this.filterOptions.map(option => option.clone()));
}

protected equals(other: FilterOptions): boolean {
const thisFilters = this.filterOptions;
const otherFilters = other.filterOptions;
if (thisFilters.length !== otherFilters.length) {
return false;
}
return thisFilters.every(option => other.getFilterOptionByName(option.name)?.equals(option));
}

protected clone(): FilterOptions {
return new FilterOptions(this.filterOptions.map(option => new FilterOption(option.name, option.values.concat())));
}

protected getFilterOptionByName(name: string, add: true): FilterOption;
protected getFilterOptionByName(name: string, add: false): FilterOption | null;
protected getFilterOptionByName(name: string): FilterOption | null;
protected getFilterOptionByName(name: string, add = false): FilterOption | null {
const addOption = (option: FilterOption): FilterOption => {
this._filterOptions.push(option);
return option;
};

return this._filterOptions.find(thisOption => thisOption.name === name) ?? (add ? addOption(new FilterOption(name)) : null);
}
}
Loading

0 comments on commit e0415ad

Please sign in to comment.