Skip to content

Commit

Permalink
Support filter constraints being arrays or single values
Browse files Browse the repository at this point in the history
  • Loading branch information
latonv committed Nov 19, 2022
1 parent 907911e commit e0422f1
Show file tree
Hide file tree
Showing 4 changed files with 145 additions and 31 deletions.
24 changes: 15 additions & 9 deletions demo/app-root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,23 +262,28 @@ export class AppRoot extends LitElement {

private removeFilterClicked(e: Event) {
const target = e.target as HTMLButtonElement;
const { field, value } = target.dataset;
const { field, value, constraint } = target.dataset;

if (field && value) {
this.filterMap = { ...this.filterMap };
delete this.filterMap[field][value];

if (Object.keys(this.filterMap[field]).length === 0) {
delete this.filterMap[field];
}
if (field && value && constraint) {
this.filterMap = new FilterMapBuilder()
.setFilterMap(this.filterMap)
.removeSingleFilter(field, value, constraint as FilterConstraint)
.build();
}
}

private get appliedFiltersTemplate() {
const filtersArray: SingleFilter[] = [];
for (const [field, filters] of Object.entries(this.filterMap)) {
for (const [value, constraint] of Object.entries(filters)) {
filtersArray.push({ field, value, constraint });
// The constraint may be either a single item or an array
if (Array.isArray(constraint)) {
for (const subConstraint of constraint) {
filtersArray.push({ field, value, constraint: subConstraint });
}
} else {
filtersArray.push({ field, value, constraint });
}
}
}

Expand All @@ -305,6 +310,7 @@ export class AppRoot extends LitElement {
class="remove-filter"
data-field=${field}
data-value=${value}
data-constraint=${constraint}
@click=${this.removeFilterClicked}
>
x
Expand Down
80 changes: 69 additions & 11 deletions src/filter-map-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ export class FilterMapBuilder {

/**
* Adds a filter to the FilterMap under construction.
* The new filter may overwrite existing filters already added to the builder.
* If existing constraint(s) already exist for this field and value, then the old and new constraints
* will be joined into a single array.
* @param field The field to filter on (e.g., 'subject', 'year', ...)
* @param value The value of the field to filter on (e.g., 'Cicero', '1920', ...)
* @param constraint The constraint to apply to the `field`, with respect to the given `value`.
Expand All @@ -19,24 +20,74 @@ export class FilterMapBuilder {
this.filterMap[field] = {};
}

// Overwrite any existing filter for this value
this.filterMap[field][value] = constraint;
// If there are already constraints for this value, concat them into an array
if (this.filterMap[field][value]) {
const mergedConstraints = ([] as FilterConstraint[]).concat(
this.filterMap[field][value],
constraint
);

// Ensure there are no duplicate constraints in the array
this.filterMap[field][value] = Array.from(new Set(mergedConstraints));
} else {
// Otherwise just use the provided value
this.filterMap[field][value] = constraint;
}

return this;
}

/**
* Removes any filter currently associated with the given field and value.
* Removes a single filter currently associated with the given field, value, and constraint type.
* @param field The field to remove a filter for
* @param value The value to remove the filter for
* @param constraint The constraint type to remove for this field and value
*/
removeFilter(field: string, value: string): this {
if (this.filterMap[field]) {
removeSingleFilter(
field: string,
value: string,
constraint: FilterConstraint
): this {
if (!this.filterMap[field]?.[value]) return this;

const constraints = ([] as FilterConstraint[]).concat(
this.filterMap[field][value]
);
const constraintIndex = constraints.indexOf(constraint);
if (constraintIndex >= 0) {
constraints.splice(constraintIndex, 1);
}

// 2 or more constraints -> leave as array
// 1 constraint -> pull out single constraint
// 0 constraints -> delete the value entirely
this.filterMap[field][value] =
constraints.length === 1 ? constraints[0] : constraints;
if (constraints.length === 0) {
delete this.filterMap[field][value];
}

// If there are no remaining filters for this field, delete the whole field object.
if (Object.keys(this.filterMap[field]).length === 0) {
delete this.filterMap[field];
}
// If there are no remaining filters for this field, delete the whole field object.
if (Object.keys(this.filterMap[field]).length === 0) {
delete this.filterMap[field];
}

return this;
}

/**
* Removes any filters currently associated with the given field and value.
* @param field The field to remove a filter for
* @param value The value to remove the filter for
*/
removeFilters(field: string, value: string): this {
if (!this.filterMap[field]) return this;

delete this.filterMap[field][value];

// If there are no remaining filters for this field, delete the whole field object.
if (Object.keys(this.filterMap[field]).length === 0) {
delete this.filterMap[field];
}

return this;
Expand All @@ -60,7 +111,14 @@ export class FilterMapBuilder {
mergeFilterMap(map: FilterMap): this {
for (const [field, filters] of Object.entries(map)) {
for (const [value, constraint] of Object.entries(filters)) {
this.addFilter(field, value, constraint);
// There may be either a single constraint or an array of them
if (Array.isArray(constraint)) {
for (const subConstraint of constraint) {
this.addFilter(field, value, subConstraint);
}
} else {
this.addFilter(field, value, constraint);
}
}
}
return this;
Expand Down
8 changes: 5 additions & 3 deletions src/search-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,15 @@ export enum FilterConstraint {
}

/**
* A filter mapping a field value to the type of constraint that it should impose on results.
* A filter mapping a field value to the type of constraint(s) that it should impose on results.
* Multiple constraints for the same value may be provided as an array.
*
* Some examples (where the values are members of `FilterConstraint`):
* Some examples (where the property values are members of `FilterConstraint`):
* - `{ 'puppies': INCLUDE }`
* - `{ '1950': GREATER_OR_EQUAL, '1970': LESS_OR_EQUAL }`
* - `{ '1950': [ GREATER_OR_EQUAL, EXCLUDE ] }`
*/
export type FieldFilter = Record<string, FilterConstraint>;
export type FieldFilter = Record<string, FilterConstraint | FilterConstraint[]>;

/**
* A map of fields (e.g., 'year', 'subject', ...) to the filters that should be
Expand Down
64 changes: 56 additions & 8 deletions test/filter-map-builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,17 @@ describe('filter map builder', () => {
});
});

it('can add multiple constraints for one value', () => {
const builder = new FilterMapBuilder();
builder.addFilter('foo', 'bar', FilterConstraint.INCLUDE);
expect(builder.build()).to.deep.equal({ foo: { bar: 'inc' } });

builder.addFilter('foo', 'bar', FilterConstraint.GREATER_OR_EQUAL);
expect(builder.build()).to.deep.equal({
foo: { bar: ['inc', 'gte'] },
});
});

it('can remove filters', () => {
const builder = new FilterMapBuilder();
builder.addFilter('foo', 'bar', FilterConstraint.INCLUDE);
Expand All @@ -35,19 +46,53 @@ describe('filter map builder', () => {
baz: { boop: 'exc' },
});

builder.removeFilter('foo', 'bar');
builder.removeFilters('foo', 'bar');
expect(builder.build()).to.deep.equal({
foo: { beep: 'gt' },
baz: { boop: 'exc' },
});

builder.removeFilter('foo', 'beep');
builder.removeFilters('foo', 'beep');
expect(builder.build()).to.deep.equal({ baz: { boop: 'exc' } });

builder.removeFilter('not', 'exist');
builder.removeFilters('not', 'exist');
expect(builder.build()).to.deep.equal({ baz: { boop: 'exc' } });

builder.removeFilter('baz', 'boop');
builder.removeFilters('baz', 'boop');
expect(builder.build()).to.deep.equal({});
});

it('can remove single filters by constraint type', () => {
const builder = new FilterMapBuilder();
builder.addFilter('foo', 'bar', FilterConstraint.INCLUDE);
builder.addFilter('foo', 'bar', FilterConstraint.GREATER_OR_EQUAL);
builder.addFilter('baz', 'boop', FilterConstraint.EXCLUDE);
expect(builder.build()).to.deep.equal({
foo: { bar: ['inc', 'gte'] },
baz: { boop: 'exc' },
});

builder.removeSingleFilter('foo', 'bar', FilterConstraint.GREATER_OR_EQUAL);
expect(builder.build()).to.deep.equal({
foo: { bar: 'inc' },
baz: { boop: 'exc' },
});

builder.removeSingleFilter('foo', 'bar', FilterConstraint.EXCLUDE);
expect(builder.build()).to.deep.equal({
foo: { bar: 'inc' },
baz: { boop: 'exc' },
});

builder.removeSingleFilter('foo', 'bar', FilterConstraint.INCLUDE);
expect(builder.build()).to.deep.equal({
baz: { boop: 'exc' },
});

builder.removeSingleFilter('baz', 'boop', FilterConstraint.EXCLUDE);
expect(builder.build()).to.deep.equal({});

builder.removeSingleFilter('not', 'exist', FilterConstraint.INCLUDE);
expect(builder.build()).to.deep.equal({});
});

Expand Down Expand Up @@ -76,17 +121,20 @@ describe('filter map builder', () => {
bar: FilterConstraint.INCLUDE,
},
baz: {
boop: FilterConstraint.EXCLUDE,
boop: [FilterConstraint.EXCLUDE, FilterConstraint.LESS_OR_EQUAL],
},
};

builder.addFilter('foo', 'bar', FilterConstraint.GREATER_OR_EQUAL);
builder.addFilter('foo', 'beep', FilterConstraint.LESS_OR_EQUAL);
expect(builder.build()).to.deep.equal({ foo: { beep: 'lte' } });
expect(builder.build()).to.deep.equal({
foo: { bar: 'gte', beep: 'lte' },
});

builder.mergeFilterMap(filterMap);
expect(builder.build()).to.deep.equal({
foo: { bar: 'inc', beep: 'lte' },
baz: { boop: 'exc' },
foo: { bar: ['gte', 'inc'], beep: 'lte' },
baz: { boop: ['exc', 'lte'] },
});
});
});

0 comments on commit e0422f1

Please sign in to comment.