snippetjavascriptMinor
Handling combinations of optional parameters for an Angular filter pipe
Viewed 0 times
handlingcombinationsangularpipeoptionalfilterforparameters
Problem
This is a simple implementation of an ng2 filter pipe, which can currently take 2 optional facets:
The filter receives a list of objects (
Currently, I have implemented only one object type, a "maintenance event", which can be filtered by:
The filter is implemented in the view like so:
where
The rest parameter (
This is my complete pipe module:
```
import { Pipe, PipeTransform } from "@angular/core";
@Pipe({ name: 'filter' })
export class FilterPipe implements PipeTransform {
transform(values: any, type: string, ...filters: Object[]): any {
switch (type) {
case 'MAINTENANCE_EVENT':
const ALL = "Show all";
const BLANK = "";
const DESCRIPTION_FILTER = new RegExp(filters[1], 'gi');
if (filters.length !== 2) {
throw new SyntaxError('MAINTENANCE_EVENT requires exactly 2 filter parameters: status and description')
}
if (filters[0] === ALL && filters[1] === BLANK) {
return values;
}
else {
return values.filter(value => {
if (value.hasOwnProperty('status') && value.hasOwnProperty('part') && value.part.hasOwnProperty('description')) {
if (filters[0] === ALL && filters[1] !== BLANK) {
return DESCRIPTION_FILTER.test(
status and description. A "filter pipe" is an angular2 view decorator.The filter receives a list of objects (
values) which I want to filter by some criteria specific to the type of data object passed to it. The types of criteria are known in advance for each type of object.Currently, I have implemented only one object type, a "maintenance event", which can be filtered by:
- a status field, which is a fixed enum of strings, and
- a free-text description
The filter is implemented in the view like so:
where
statusFilter and wordFilter are populated by a form.The rest parameter (
...filters) was used to allow any future arbitrary number of filters to be passed to the common filter module. So, in future, I could implement a filter like this:This is my complete pipe module:
```
import { Pipe, PipeTransform } from "@angular/core";
@Pipe({ name: 'filter' })
export class FilterPipe implements PipeTransform {
transform(values: any, type: string, ...filters: Object[]): any {
switch (type) {
case 'MAINTENANCE_EVENT':
const ALL = "Show all";
const BLANK = "";
const DESCRIPTION_FILTER = new RegExp(filters[1], 'gi');
if (filters.length !== 2) {
throw new SyntaxError('MAINTENANCE_EVENT requires exactly 2 filter parameters: status and description')
}
if (filters[0] === ALL && filters[1] === BLANK) {
return values;
}
else {
return values.filter(value => {
if (value.hasOwnProperty('status') && value.hasOwnProperty('part') && value.part.hasOwnProperty('description')) {
if (filters[0] === ALL && filters[1] !== BLANK) {
return DESCRIPTION_FILTER.test(
Solution
Direction
My approach would be based on the Strategy design pattern. Each of the original
A) It defines whether it may or may not be applied to the given filter input;
B) It defines the application function (logic) itself.
In order to select a proper rule dynamically in run time, we need a registry of the rules which is implemented as a simple array.
Benefits
Implementation of a new "rule" becomes almost trivial: we'll need to create a rule object itself, and add it to a proper place in the list of registry entries, so that it's picked up.
Code
Consuming code (that uses the Rule registry)
Now your filter can be implemented like this:
Disclaimer
P.S.
Notice that the same approach may be applied to
My approach would be based on the Strategy design pattern. Each of the original
if-elseif-elseif-... sections is now becoming an IRule object that represents the rule. This object does two things:A) It defines whether it may or may not be applied to the given filter input;
B) It defines the application function (logic) itself.
In order to select a proper rule dynamically in run time, we need a registry of the rules which is implemented as a simple array.
Benefits
Implementation of a new "rule" becomes almost trivial: we'll need to create a rule object itself, and add it to a proper place in the list of registry entries, so that it's picked up.
Code
import { Pipe, PipeTransform } from "@angular/core";
const ALL = "Show all";
const BLANK = "";
/**
* Represents a contract of a single rule.
*/
interface IRule {
/**
* A predicate that sayw whether the rule may or may not be applied to a value based on the filter.
*/
isApplicableTo(filters: Object[]): boolean;
/**
* Implementation of rule applied to a value based on filter parameters.
*/
applyTo(filters: Object[], value: any): boolean;
}
/**
* The complete list of rules that your system supports.
* NOTICE that the order of the rule is important.
* If more than one rule is applicable to a filters object,
* the very first applicable rule is applied.
*/
const RULES_REGISTRY: IRule[] = [
new AllMatchingRegexRule(),
new MatchingStatusRule(),
new SomeMatchingRegexRule()
];
/**
* Three sample rules based on your original code.
*/
class AllMatchingRegexRule implements IRule {
isApplicableTo(filters: Object[]): boolean {
const safeFilters = getSafeFilters(filters);
return isAllFilter(safeFilters) && isNotBlankFilter(safeFilters);
}
applyTo(filters: Object[], value: any): boolean {
return descriptionRegexMatched(filters[1], value);
}
}
class MatchingStatusRule implements IRule {
isApplicableTo(filters: Object[]): boolean {
const safeFilters = getSafeFilters(filters);
return isNotAllFilter(safeFilters) && isBlankFilter(safeFilters);
}
applyTo(filters: Object[], value: any): boolean {
return statusMatches(filters, value);
}
}
class SomeMatchingRegexRule implements IRule {
isApplicableTo(filters: Object[]): boolean {
const safeFilters = getSafeFilters(filters);
return isNotAllFilter(safeFilters) && isNotBlankFilter(safeFilters);
}
applyTo(filters: Object[], value: any): boolean {
return value.status === filters[0] && descriptionRegexMatched(filters[1], value);;
}
}
// Reusable working horse lambdas (use in Rules)
const getSafeFilters = (filters: Object[]) => filters && filters.length >= 2 ? filters : new Object[2];
const descriptionRegexMatched = (pattern: any, value: any) => new RegExp(pattern, 'gi').test(value['part']['description']);
const statusMatches = (filters: Object[], value: any) => value.status === filters[0];
const isAllFilter = (filters: Object[]) => filters[0] === ALL;
const isNotAllFilter = (filters: Object[]) => !isAllFilter(filters);
const isBlankFilter = (filters: Object[]) => filters[1] === BLANK;
const isNotBlankFilter = (filters: Object[]) => !isBlankFilter(filters);Consuming code (that uses the Rule registry)
Now your filter can be implemented like this:
@Pipe({ name: 'filter' })
export class FilterPipe implements PipeTransform {
transform(values: any, type: string, ...filters: Object[]): any {
switch (type) {
case 'MAINTENANCE_EVENT':
if (filters.length !== 2) {
throw new SyntaxError('MAINTENANCE_EVENT requires exactly 2 filter parameters: status and description')
}
if (isAllFilter(filters) && isNotBlankFilter(filters)) {
return values;
} else {
return values.filter(value => {
const ruleToApply = RULES_REGISTRY.find(rule => rule.isApplicableTo(filters));
if (!ruleToApply) {
throw new Error("Attempting to filter by properties not present in available s");
}
return ruleToApply.applyTo(filters, value);
});
}
default:
return values;
}
}
}Disclaimer
- I am very new to TypeScript so I may be using wrong idioms. The key in my answer is the design change, rather than implementation details.
P.S.
Notice that the same approach may be applied to
type == 'MAINTENANCE_EVENT' 'OTHER_EVENT' .... And, speaking more generally, Strategy design pattern is almost always a working alternative to an if or switch. Of course, to get the benefits of the pattern, we need to write some boilerplate code (IRule + Rule registry), however it's a great trade of for the scenarios where thCode Snippets
import { Pipe, PipeTransform } from "@angular/core";
const ALL = "Show all";
const BLANK = "";
/**
* Represents a contract of a single rule.
*/
interface IRule {
/**
* A predicate that sayw whether the rule may or may not be applied to a value based on the filter.
*/
isApplicableTo(filters: Object[]): boolean;
/**
* Implementation of rule applied to a value based on filter parameters.
*/
applyTo(filters: Object[], value: any): boolean;
}
/**
* The complete list of rules that your system supports.
* NOTICE that the order of the rule is important.
* If more than one rule is applicable to a filters object,
* the very first applicable rule is applied.
*/
const RULES_REGISTRY: IRule[] = [
new AllMatchingRegexRule(),
new MatchingStatusRule(),
new SomeMatchingRegexRule()
];
/**
* Three sample rules based on your original code.
*/
class AllMatchingRegexRule implements IRule {
isApplicableTo(filters: Object[]): boolean {
const safeFilters = getSafeFilters(filters);
return isAllFilter(safeFilters) && isNotBlankFilter(safeFilters);
}
applyTo(filters: Object[], value: any): boolean {
return descriptionRegexMatched(filters[1], value);
}
}
class MatchingStatusRule implements IRule {
isApplicableTo(filters: Object[]): boolean {
const safeFilters = getSafeFilters(filters);
return isNotAllFilter(safeFilters) && isBlankFilter(safeFilters);
}
applyTo(filters: Object[], value: any): boolean {
return statusMatches(filters, value);
}
}
class SomeMatchingRegexRule implements IRule {
isApplicableTo(filters: Object[]): boolean {
const safeFilters = getSafeFilters(filters);
return isNotAllFilter(safeFilters) && isNotBlankFilter(safeFilters);
}
applyTo(filters: Object[], value: any): boolean {
return value.status === filters[0] && descriptionRegexMatched(filters[1], value);;
}
}
// Reusable working horse lambdas (use in Rules)
const getSafeFilters = (filters: Object[]) => filters && filters.length >= 2 ? filters : new Object[2];
const descriptionRegexMatched = (pattern: any, value: any) => new RegExp(pattern, 'gi').test(value['part']['description']);
const statusMatches = (filters: Object[], value: any) => value.status === filters[0];
const isAllFilter = (filters: Object[]) => filters[0] === ALL;
const isNotAllFilter = (filters: Object[]) => !isAllFilter(filters);
const isBlankFilter = (filters: Object[]) => filters[1] === BLANK;
const isNotBlankFilter = (filters: Object[]) => !isBlankFilter(filters);@Pipe({ name: 'filter' })
export class FilterPipe implements PipeTransform {
transform(values: any, type: string, ...filters: Object[]): any {
switch (type) {
case 'MAINTENANCE_EVENT':
if (filters.length !== 2) {
throw new SyntaxError('MAINTENANCE_EVENT requires exactly 2 filter parameters: <string>status and <string>description')
}
if (isAllFilter(filters) && isNotBlankFilter(filters)) {
return values;
} else {
return values.filter(value => {
const ruleToApply = RULES_REGISTRY.find(rule => rule.isApplicableTo(filters));
if (!ruleToApply) {
throw new Error("Attempting to filter by properties not present in available <MaintenanceEvent>s");
}
return ruleToApply.applyTo(filters, value);
});
}
default:
return values;
}
}
}Context
StackExchange Code Review Q#157659, answer score: 2
Revisions (0)
No revisions yet.