HiveBrain v1.2.0
Get Started
← Back to all entries
snippetjavascriptMinor

Handling combinations of optional parameters for an Angular filter pipe

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
handlingcombinationsangularpipeoptionalfilterforparameters

Problem

This is a simple implementation of an ng2 filter pipe, which can currently take 2 optional facets: 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 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 th

Code 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.