Skip to content
ghiscoding edited this page Dec 12, 2022 · 90 revisions

index

Description

Multiple Select (dropdown) filter is useful when we want to filter the grid 1 or more search term value.

Note

For this filter to work you will need to add Multiple-Select.js to your angular-cli.json file, however this is a customized version of the original (thought all the original lib options are available so you can still consult the original site for all options). See Multiple-Select.js Options down below for more info.

Demo

Demo Page / Demo Component

Demo with Localization

Demo Page / Demo Component

UI Sample

Scroll down below to see the UI Print Screens

Types

There are 3 types of select filter

  1. Filters.singleSelect which will filter the dataset with 1 value (uses EQ internally) with checkbox icons.
  2. Filters.multipleSelect which will do a search with 1 or more values (uses IN internally) with radio icons.
Less recommended
  1. Filters.select which will filter the dataset with 1 value (uses EQ internally), same as singleSelect but uses styling from the browser setup.
  • this one is less recommended, it is a simple and plain select dropdown. There are no styling applied and will be different in every browser. If you want a more consistent visual UI, it's suggested to use the other 2 filters (multipleSelect or singleSelect)

SASS Styling

You can change the multipleSelect and singleSelect styling with SASS variables for styling. For more info on how to use SASS in your project, read the Wiki - Styling

How to use Select Filter

Simply set the flag filterable to True and and enable the filters in the Grid Options. Here is an example with a full column definition:

// define you columns, in this demo Effort Driven will use a Select Filter
this.columnDefinitions = [      
  { id: 'title', name: 'Title', field: 'title' },
  { id: 'description', name: 'Description', field: 'description', filterable: true },
  { id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', 
    type: FieldType.boolean,
    filterable: true,
    filter: {
       collection: [ { value: '', label: '' }, { value: true, label: 'true' }, { value: false, label: 'false' } ],
       model: Filters.multipleSelect,

       // you can add "multiple-select" plugin options like styling the first row
       filterOptions: {
          offsetLeft: 14,
          width: 100
       } as MultipleSelectOption,

       // you can also add an optional placeholder
       placeholder: 'choose an option'
   }
];

// you also need to enable the filters in the Grid Options
this.gridOptions = {
   enableFiltering: true
};

Default Search Term(s)

If you want to load the grid with certain default filter(s), you can use the following optional property:

  • searchTerms (array of values)

Note

Even though the option of searchTerms it is much better to use the more powerful presets grid options, please refer to the Grid State & Presets for more info.

NOTE If you also have presets in the grid options, then your searchTerms will be ignored completely (even if it's a different column) since presets have higher priority over searchTerms. See Grid State & Grid Presets from more info.

Sample

// define you columns, in this demo Effort Driven will use a Select Filter
this.columnDefinitions = [
  { id: 'title', name: 'Title', field: 'title' },
  { id: 'description', name: 'Description', field: 'description', filterable: true },
  { id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven',
    type: FieldType.boolean,
    filterable: true,
    filter: {
       collection: [ { value: '', label: '' }, { value: true, label: 'true' }, { value: false, label: 'false' } ],
       model: Filters.multipleSelect,
       searchTerms: [true],
   }
];

How to add Translation?

LabelKey

For the Select (dropdown) filter, you can fill in the "labelKey" property, if found it will translate it right away. If no labelKey is provided nothing will be translated (unless you have enableTranslateLabel set to true), else it will use "label"

// define you columns, in this demo Effort Driven will use a Select Filter
this.columnDefinitions = [      
  { id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', 
    formatter: Formatters.checkmark, 
    type: FieldType.boolean,
    filterable: true,
    filter: {
       collection: [ { value: '', label: '' }, { value: true, labelKey: 'TRUE' }, { value: false, label: 'FALSE' } ],
       model: Filters.singleSelect,
   }
];

enableTranslateLabel

You could also use the enableTranslateLabel which will translate regardless of the label key name (so it could be used with label, labelKey or even a customStructure label).

// define you columns, in this demo Effort Driven will use a Select Filter
this.columnDefinitions = [      
  { id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', 
    formatter: Formatters.checkmark, 
    type: FieldType.boolean,
    filterable: true,
    filter: {
       collection: [ { value: '', label: '' }, { value: true, label: 'true' }, { value: false, label: 'false' } ],
       model: Filters.singleSelect,
       enableTranslateLabel: true
   }
];

Custom Structure (key/label pair)

What if your select options (collection) have totally different value/label pair? In this case, you can use the customStructure to change the property name(s) to use. You can change the label and/or the value, they can be passed independently.

// define you columns, in this demo Effort Driven will use a Select Filter
this.columnDefinitions = [      
  { id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', 
    formatter: Formatters.checkmark, 
    type: FieldType.boolean,
    filterable: true,
    filter: {
       collection: [
         { customValue: '', customLabel: '' }, 
         { customValue: true, customLabel: 'true' }, 
         { customValue: false, customLabel: 'false' } 
       ],
       customStructure: {
         label: 'customLabel',
         value: 'customValue'
       },
       model: Filters.multipleSelect,
   }
];

LabelPrefix / LabelSuffix

labelPrefix and labelSuffix were recently added, they are also supported by the customStructure and can also be overridden. See Collection Label Prefix/Suffix

Custom Structure with Translation

What if you want to use customStructure and translate the labels? Simply pass the flag enableTranslateLabel: true

// define you columns, in this demo Effort Driven will use a Select Filter
this.columnDefinitions = [      
  { id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', 
    formatter: Formatters.checkmark, 
    type: FieldType.boolean,
    filterable: true,
    filter: {
       collection: [
         { customValue: '', customLabel: '' }, 
         { customValue: true, customLabel: 'TRUE' }, 
         { customValue: false, customLabel: 'FALSE' } 
       ],
       customStructure: {
         label: 'customLabel',
         value: 'customValue'
       },
       enableTranslateLabel: true,
       model: Filters.multipleSelect,
   }
];

How to filter empty values?

By default you cannot filter empty dataset values (unless you use a multipleSelect Filter). You might be wondering, why though? By default an empty value in a singleSelect Filter is equal to returning all values. You could however use this option emptySearchTermReturnAllValues set to false to add the ability to really search only empty values.

Note: the defaults for single & multiple select filters are different

  • single select filter default is emptySearchTermReturnAllValues: true
  • multiple select filter default is emptySearchTermReturnAllValues: false
// define you columns, in this demo Effort Driven will use a Select Filter
this.columnDefinitions = [
  { id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven',
    formatter: Formatters.checkmark,
    type: FieldType.boolean,
    filterable: true,
    filter: {
       collection: [ { value: '', label: '' }, { value: true, labelKey: 'TRUE' }, { value: false, label: 'FALSE' } ],
       model: Filters.singleSelect,
       emptySearchTermReturnAllValues: false, // False when we really want to filter empty values
   }
];

Collection FilterBy/SortBy

You can also pre-sort or pre-filter the collection given to the multipleSelect/singleSelect Filters. Also note that if the enableTranslateLabel flag is set to True, it will use the translated value to filter or sort the collection. For example:

// define you columns, in this demo Effort Driven will use a Select Filter
this.columnDefinitions = [      
  { id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', 
    formatter: Formatters.checkmark, 
    type: FieldType.boolean,
    filterable: true,
    filter: {
       collection: [ 
         { value: '', label: '' }, 
         { value: true, label: 'true' }, 
         { value: false, label: 'false' },
         { value: undefined, label: 'undefined' }
       ],
       collectionFilterBy: {
          property: 'effortDriven',
          operator: OperatorType.equal, // defaults to equal when not provided
          value: undefined
       },
       collectionSortBy: {
          property: 'effortDriven',    // will sort by translated value since "enableTranslateLabel" is true
          sortDesc: false,             // defaults to "false" when not provided
          fieldType: FieldType.boolean // defaults to FieldType.string when not provided
       }, 
       model: Filters.multipleSelect
   }
];

Multiple FilterBy/SortBy

You can also pass multiple collectionFilterBy or collectionSortBy simply by changing these object to array of objects.

// prepare a multiple-select array to filter with
const multiSelectFilterArray = [];
for (let i = 0; i < 365; i++) {
  multiSelectFilterArray.push({ value: i, label: i, labelSuffix: ' days' });
}

this.columnDefinitions = [      
  { id: 'duration', name: 'Duration', field: 'duration', 
    formatter: Formatters.checkmark, 
    type: FieldType.boolean,
    filterable: true,
    filter: {
       collection: multiSelectFilterArray,
       collectionFilterBy: [{
          property: 'value',
          operator: OperatorType.notEqual, // remove day 1
          value: 1
       }, {
          property: 'value',
          operator: OperatorType.notEqual, // remove day 365
          value: 365
       }],
       model: Filters.multipleSelect
     }
   }
];

However please note that by default the collectionFilterBy will not merge the result after each pass, it will instead chain them and use the new returned collection after each pass (which means that if original collection is 100 items and 20 items are returned after 1st pass, then the 2nd pass will filter out of these 20 items and so on).

What if you wanted to merge the results instead? Then in this case, you can change the filterResultAfterEachPass flag defined in `collectionOptions

this.columnDefinitions = [      
  { id: 'duration', name: 'Duration', field: 'duration', 
    filter: {
      collection: [yourCollection],
      collectionFilterBy: [
        // ...
      ],
      collectionOptions: {
        filterResultAfterEachPass: 'chain' // options are "merge" or "chain" (defaults to "chain")
      },
      model: Filters.multipleSelect
    }
  }
];

Collection Override

In some cases you might want to provide a custom collection based on the current item data context, you can do that via the collection override. Also note that this override is processed after collectionFilterBy and collectionSortBy but before the customStructure (if you have any), in other words make sure that the collection returned by the override does have the properties defined in the "customStructure".

Let take this example, let say that we want to allow collection values lower than or greater than 50 depending on its item Id, we could do the following

this.columnDefinitions = [
  {
    id: 'prerequisites', name: 'Prerequisites', field: 'prerequisites',
    type: FieldType.string,
    editor: {
      model: Editors.multipleSelect,
      collection: Array.from(Array(12).keys()).map(k => ({ value: `Task ${k}`, label: `Task ${k}` })),
      collectionOverride: (updatedCollection, args) => {
        console.log(args);
        return updatedCollection.filter((col) => args.dataContext.id % 2 ? col.value < 50 : col.value > 50);
      },
    }
  }
];

Collection Label Prefix/Suffix

You can use labelPrefix and/or labelSuffix which will concatenate the multiple properties together (labelPrefix + label + labelSuffix) which will used by each Select Filter option label. You can also use the property separatorBetweenTextLabels to define a separator between prefix, label & suffix.

Note If enableTranslateLabel flag is set to True, it will also try to translate the Prefix / Suffix / OptionLabel texts.

For example, say you have this collection

const currencies = [ 
  { symbol: '$', currency: 'USD', country: 'USA' }, 
  { symbol: '$', currency: 'CAD', country: 'Canada' }
];

You can display all of these properties inside your dropdown labels, say you want to show (symbol with abbreviation and country name). Now you can.

So you can create the multipleSelect Filter with a customStructure by using the symbol as prefix, and country as suffix. That would make up something like this:

  • $ USD USA
  • $ CAD Canada

with a customStructure defined as

filter: {
  collection: this.currencies,
  customStructure: {
    value: 'currency',
    label: 'currency',
    labelPrefix: 'symbol',
    labelSuffix: 'country',
  },
  collectionOptions: {
    separatorBetweenTextLabels: ' ' // add white space between each text
  },
  model: Filters.multipleSelect
}

Collection Label Render HTML

By default HTML is not rendered and the label will simply show HTML as text. But in some cases you might want to render it, you can do so by enabling the enableRenderHtml flag.

NOTE: this is currently only used by the Filters that have a collection which are the MultipleSelect & SingleSelect Filters.

this.columnDefinitions = [
  { 
    id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven',
    formatter: Formatters.checkmark,
    type: FieldType.boolean,
    filterable: true,
    filter: {
      // display checkmark icon when True
      enableRenderHtml: true,
      collection: [{ value: '', label: '' }, { value: true, label: 'True', labelPrefix: `<i class="fa fa-check"></i> ` }, { value: false, label: 'False' }],
      model: Filters.singleSelect
    }
  }
];

Collection Add Blank Entry

In some cases a blank entry at the beginning of the collection could be useful, the most common example for this is to use the first option as a blank entry to tell our Filter to show everything. So for that we can use the addBlankEntry flag in `collectionOptions

this.columnDefinitions = [      
  { id: 'duration', name: 'Duration', field: 'duration', 
    filter: {
      collection: [yourCollection],
      collectionOptions: {
        addBlankEntry: true
      },
      model: Filters.multipleSelect
    }
  }
];

Collection Add Custom Entry at the Beginning/End of the Collection

We can optionally add a custom entry at the beginning of the collection, the most common example for this is to use the first option as a blank entry to tell our Filter to show everything. So for that we can use the addCustomFirstEntry or addCustomLastEntry flag in `collectionOptions

this.columnDefinitions = [      
  { id: 'duration', name: 'Duration', field: 'duration', 
    filter: {
      collection: [yourCollection],
      collectionOptions: {
        addCustomFirstEntry: { value: '', label: '--n/a--' }

        // or at the end 
        addCustomLastEntry: { value: 'end', label: 'end' }
      },
      model: Filters.multipleSelect
    }
  }
];

Collection Async Load

You can also load the collection asynchronously, but for that you will have to use the collectionAsync property, which expect an Observable (HttpClient) to be passed.

Load the collection through an Http call

this.columnDefinitions = [
    {
    id: 'prerequisites', name: 'Prerequisites', field: 'prerequisites',
    filterable: true,
    filter: {
      collectionAsync: this.http.get<{ value: string; label: string; }[]>('api/data/pre-requisites'),
      model: Filters.multipleSelect,
    }
  }
];

Using Async Load when Collection is inside an Object Property

What if my collection is nested under the response object? For that you can use collectionInsideObjectProperty to let the filter know how to get the collection.

this.columnDefinitions = [
    {
    id: 'prerequisites', name: 'Prerequisites', field: 'prerequisites',
    filterable: true,
    filter: {
      model: Filters.multipleSelect,
      
      // this async call will return the collection inside the response object in this format
      // { data: { myCollection: [ /*...*/ ] } }
      collectionAsync: this.http.get<{ value: string; label: string; }[]>('api/data/pre-requisites'),
      collectionOptions: {
        collectionInsideObjectProperty: 'data.myCollection' // with/without dot notation
      }
    }
  }
];

Modifying the collection afterward

If you want to modify the collection afterward (can be collection or even collectionAsync), you will need to advise Angular-Slickgrid about the change via a .next(newCollection) call on a Subject. We need to do this because Filter(s) are shown at all time and we need to refresh them if the collection changes.

For conveniences, we will use the collectionAsync property for that behavior (every Filter that support collection automatically have this Subject assigned as collectionAsync). So you will need to find the associated column which has the collectionAsync attached to it.

For a live demo, visit Example 3 and click on "Add Item" button, then open the "Prerequisites" Filter and you will see the collection includes the newly added Task.

For example

  addItem() {
    const lastRowIndex = this.dataset.length;
    const newRows = this.mockData(1, lastRowIndex);

    // wrap into a timer to simulate a backend async call
    setTimeout(() => {
      const requisiteColumnDef = this.columnDefinitions.find((column: Column) => column.id === 'prerequisites');
      if (requisiteColumnDef) {
        const collectionFilterAsync = requisiteColumnDef.filter.collectionAsync;

        if (Array.isArray(collectionFilterAsync)) {
          // add the new row to the grid
          this.angularGrid.gridService.addItemToDatagrid(newRows[0]);

          // then refresh the Filter "collection", we have 2 ways of doing it

          // 1- Push to the Filter "collection"
          collection.push({ value: lastRowIndex, label: lastRowIndex, prefix: 'Task' });

          // or 2- replace the entire "collection"
          // requisiteColumnDef.filter.collection = [...collection, ...[{ value: lastRowIndex, label: lastRowIndex }]];

          // finally we have a trigger the RxJS/Subject change with the new collection passed as argument
          if (collectionFilterAsync instanceof Subject) {
            collectionFilterAsync.next(collection);
          }
        }
      }
    }, 250);
  }

Note: to my knowledge, Angular 4+ does not seem to support collection observers, unless someone knows how to do it without a Subject please reach out.

Filter Options (MultipleSelectOption interface)

All the available options that can be provided as filterOptions to your column definitions can be found under this multipleSelectOption interface and you should cast your filterOptions to that interface to make sure that you use only valid options of the multiple-select.js library.

filter: {
  model: Filters.singleSelect,
    maxHeight: 400
  } as MultipleSelectOption
}

Collection Watch

Sometime you wish that whenever you change your filter collection, you'd like the filter to be updated, it won't do that by default but you could use enableCollectionWatch for that purpose to add collection observers and re-render the Filter DOM element whenever the collection changes. Also note that using collectionAsync will automatically watch for changes, so there's no need to enable this flag for that particular use case.

this.columnDefinitions = [
  {
    id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven',
    formatter: Formatters.checkmark,
    type: FieldType.boolean,
    editor: {
      // watch for any changes in the collection and re-render when that happens
      enableCollectionWatch: true,
      collection: [{ value: '', label: '' }, { value: true, label: 'True' }, { value: false, label: 'False' }],
      model: Editors.singleSelect
    }
  }
];

Multiple-select.js Options

You can use any options from Multiple-Select.js and add them to your filterOptions property. However please note that this is a customized version of the original (all original lib options are available so you can still consult the original site for all options).

Couple of small options were added to suit Angular-SlickGrid needs, which is why it points to angular-slickgrid/lib folder (which is our customized version of the original). This lib is required if you plan to use multipleSelect or singleSelect Filters. What was customized to (compare to the original) is the following:

  • okButton option was added to add an OK button for simpler closing of the dropdown after selecting multiple options.
    • okButtonText was also added for locale (i18n)
  • offsetLeft option was added to make it possible to offset the dropdown. By default it is set to 0 and is aligned to the left of the select element. This option is particularly helpful when used as the last right column, not to fall off the screen.
  • autoDropWidth option was added to automatically resize the dropdown with the same width as the select filter element.
  • autoAdjustDropHeight (defaults to true), when set will automatically adjust the drop (up or down) height
  • autoAdjustDropPosition (defaults to true), when set will automatically calculate the area with the most available space and use best possible choise for the drop to show (up or down)
  • autoAdjustDropWidthByTextSize (defaults to true), when set will automatically adjust the drop (up or down) width by the text size (it will use largest text width)
  • to extend the previous 3 autoAdjustX flags, the following options can be helpful
    • minWidth (defaults to null, to use when autoAdjustDropWidthByTextSize is enabled)
    • maxWidth (defaults to 500, to use when autoAdjustDropWidthByTextSize is enabled)
    • adjustHeightPadding (defaults to 10, to use when autoAdjustDropHeight is enabled), when using autoAdjustDropHeight we might want to add a bottom (or top) padding instead of taking the entire available space
    • maxHeight (defaults to 275, to use when autoAdjustDropHeight is enabled)
  • useSelectOptionLabel` to show different selected label text (on the input select element itself)
    • useSelectOptionLabelToHtml is also available if you wish to render label text as HTML for these to work, you have define the optionLabel in the customStructure
Code
this.columnDefinitions = [
  {
    id: 'isActive', name: 'Is Active', field: 'isActive', 
    filterable: true,
    filter: {
      collection: [{ value: '', label: '' }, { value: true, label: 'true' }, { value: false, label: 'false' }],
      model: Filters.singleSelect, 
      filterOptions: {
        // add any multiple-select.js options (from original or custom version)
        autoAdjustDropPosition: false, // by default set to True, but you can disable it
        position: 'top'
      } as MultipleSelectOption
    }
  }
];

Display shorter selected label text

If we find that our text shown as selected text is too wide, we can choose change that by using optionLabel in Custom Structure.

this.columnDefinitions = [
  {
    id: 'isActive', name: 'Is Active', field: 'isActive', 
    filterable: true,
    filter: {
      collection: [
        { value: 1, label: '1', suffix: 'day' }, 
        { value: 2, label: '2', suffix: 'days' },
        { value: 3, label: '3', suffix: 'days' },
        // ...
      ],
      model: Filters.multipleSelect, 
      customStructure: {
        label: 'label',
        labelSuffix: 'suffix',
        value: 'value',
        optionLabel: 'value', // use value instead to show "1, 2" instead of "1 day, 2 days"
      },
      filterOptions: {
        // use different label to show as selected text
        // please note the Custom Structure with optionLabel defined 
        // or use "useSelectOptionLabelToHtml" to render HTML
        useSelectOptionLabel: true
      } as MultipleSelectOption
    }
  }
];

Query against another field property

What if your grid is displaying a certain field but you wish to query against another field? Well you could do that with 1 of the following 3 options:

  • queryField (query on a specific field for both the Filter and Sort)
  • queryFieldFilter (query on a specific field for only the Filter)
  • queryFieldSorter (query on a specific field for only the Sort)
Example
this.columnDefinitions = [
  {
    id: 'salesRepName', 
    field: 'salesRepName',          // display in Grid the sales rep name with "field"
    queryFieldFilter: 'salesRepId', // but query against a different field for our multi-select to work
    filterable: true,
    filter: {
      collection: this.salesRepList, // an array of Sales Rep
      model: Filters.multipleSelect,
      customStructure: {
        label: 'salesRepName',
        value: 'salesRepId'
      }
    }
  }
];

UI Sample

Filters.multipleSelect

Multiple-Select

Filters.singleSelect

Single-Select

Filters.select (regular dropdown select)

This one is less recommended, it is a plain and simple select dropdown. Depending on your browser, the styling is expected to be different. If you want a more consistent styling, then it's suggested to use the other 2 filters (multipleSelect or singleSelect)

Select

Contents

Clone this wiki locally