Sometimes, you need to display a list and provide users the ability to filter your list. Entity-store give the ability to filter, a sorting, by given options to selectAll() function. But each time, you want to change this filter, you need to recall the function.
Filters plugins give you the possibility to managed multiples filters, add, remove and update filters, and each time filters have been updated, new filtered value has been emitted.
This could be useful, to display :
- products list, (with categories filter, search, price filter, etc...)
- Store locator (with filter by region, location, etc...)
- portfolio, image gallery
- Any list of elements that need multiple filters
- Config filter that could be applied in multiple pages
- ...
-
Add multiple filters (dynamically add, remove and update filters) without changing data.
-
The filters and the entity query could be separated. You could have one component, that displays only your element. Is just make a select, to observe and display all information. And have another component to manage all filters.
-
You could also display a list of filters, and permit to delete one.
-
You could set the order to apply all filters
Install the module
npm install --save akita-filters-plugin
yarn add akita-filters-plugin
npm page : https://www.npmjs.com/package/akita-filters-plugin
You need to instantiate the filters Plugins :
myFilter = new AkitaFiltersPlugin<MyEntitiesState>(this.myEntitiesQuery);
-> Give just the entytiesQuery class to the plugins.
You could define it in the constructor of your service, and add it to the property of your service.
constructor(private productsStore: ProductsFiltersStore, private productsQuery: ProductsFiltersQuery, private productsDataService: ProductsFiltersDataService) {
this.filtersProduct = new AkitaFiltersPlugin<ProductPlantState>(this.productsQuery);
}
You could also extend your class with last AkitaFiltersPlugin, and call the super method. All methods from AkitaFiltersPlugin will be available
class CustomService extends AkitaFiltersPlugin<ProductPlantState> {
constructor() {
super(wishQuery, {filtersStoreName: 'CustomFilters'});
}
}
To get elements you need to call the function selectAllByFilters() form your filters plugins instance, instead of using the selectAll() function from your Query Class.
myFilter.selectAllByFilters();
Then add filter
myFilter.setFilter({
id: 'category',
value: 'garden',
predicate: (value: ProductPlant, index, array) => value.category === category
});
By adding these filters, the data will be filtered, and the new data will be emitted.
Example with Angular, you could add a filter for example with a component like this.
@Component({
selector: 'app-category-filter',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="col m2 s6">
<label>Filter Category</label>
<div class="input-field col s12">
<select class="browser-default" [formControl]="categoryControl">
<option value="" disabled selected>Choose your catgory</option>
<option value="Interior">Interior</option>
<option value="Garden">Garden</option>
<option value="Balcony">Balcony</option>
<option value="Flowers">Flowers</option>
<option value="Tree">Tree</option>
<option value="Roses">Roses</option>
</select>
</div>
</div>
`
})
export class CategoryFiltersComponent {
categoryControl: new FormControl();
constructor(private productsService: ProductsFiltersService) {}
ngOnInit() {
this.getallFiltersValues();
// On each change, set filters
this.categoryControl.valueChanges.pipe(untilDestroyed(this)).subscribe(category => {
this.productsService.setFilter({
id: 'category',
value: category,
predicate: (value: ProductPlant, index, array) => value.category === category
});
});
this.filterForm.controls.sortControl.setValue(this.productsService.getFilterValue('category'), { emitEvent: false }); // emit event false, to not emit value in the above value change subscribe
}
}
An Akita filter is an object with the corresponding format :
type AkitaFilter: AkitaFilter<EntityState> = {
id: ID;
/** A corresponding name for display the filter, by default, it will be ${id): ${value} */
name?: string;
/** set the order for filter, by default, it is 10 */
order?: number;
/** The filter value, this will be used to compute name, or getting the current value, to initiate your form */
value?: any;
/** If you want to have filter that is not displayed on the list */
hide?: boolean;
/** If you have enabled server filter, specify witch filters will be call to server, default to false. */
server?: boolean;
/** The function to apply filters, by default use defaultFilter helpers, that will search the value in the object */
predicate: ( entity: getEntityType<S>, index: number, array: getEntityType<S>[] | HashMap<getEntityType<S>>, filter: AkitaFilterBase<S> | AkitaFilterLocal<S> | AkitaFilterServer<S> ) => boolean;
/** add any other data you want to add **/
[key: string]: any;
};
-
Id and function were mandatory. (By default, Id will guid(), and default function, will be defaultFilter helpers).
-
But you can set a name, that will be useful to display the filter in the ui. (by default, it will be calculated with ID and value).
-
You can set the value, that could be used in your filter function, or retrieve the value for a filter (in example, to init the form filter)
-
Or it could be useful, to execute a filter at the begin or the end. (Could be useful to execute simple filter at the beginning, and complex filter like full search at the end)
-
hide: true, it will be applied and not displayed in the ui.
This type is deprecated in favor of AkitaFilterLocal or AkitaFilterServer, to specify if a filter is local (that must have a predicate) or server that must have a value, and don't have predicate.
export interface AkitaFilterLocal<S extends EntityState, E = getEntityType<S>> extends AkitaFilterBase<S, E> {
/** The function to apply filters, by default use defaultFilter helpers, that will search the value in the object */
predicate: (entity: E, index: number, array: E[] | HashMap<E>, filter: AkitaFilterBase<S> | AkitaFilterLocal<S> | AkitaFilterServer<S>) => boolean;
}
export interface AkitaFilterServer<S extends EntityState, E = getEntityType<S>> extends AkitaFilterBase<S, E> {
/** If you have enabled server filter, specify witch filters will be call to server, default to false. */
server: true;
/** The filter value will be sent to withServer function */
value: any;
}
selectAllByFilters(options?: SelectAllOptions*): Observable<getEntityType[] | HashMap<getEntityType>>
The main function to subscribe to filtered data. Select All Entity with an apply filter to it, and updated with any change (entity or filter)
You can pass the same options than selectAll Function in EntityQuery.
The snapshot function to get snapshot filtered data. Get All current Entity with apply filter to it.
You can pass the same options than getAll Function in EntityQuery.
Select all the filters
Note: filters with hide=true, will not be displayed. If you want it, call directly the filterQuery :
this.filterQuery.selectAll()
Get all the current snapshot filters Note: filters with hide=true, will not be displayed. If you want it, call directly the filterQuery :
this.filterQuery.getAll()
Create or update a filter (give a Partial AkitaFilter object, all mandatory properties missing will be calculated)
filterPlugin.setFilter({
id: 'fastDelivery',
name: 'Only fast Delivery',
value: true,
order: 1,
predicate: (value: ProductPlant, index, array) => value.rapidDelivery
});
Create or update multiples filters at once (will trigger only one change)
Remove a specified filter by their id name
Remove multiples filters at once by their id name (will trigger only one change)
Remove all filters.
Get filter value or return null, if value not available.
Usefull to set init a form value, ex:
this.filterForm.controls.searchControl.setValue( this.productsService.getFilterValue('search') );
@Component({
selector: 'app-list-filter',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div *ngIf="(filters$ | async).length">
Filter :
<div class="chip" *ngFor="let filter of (filters$ | async)" (click)="removeFilter(filter.id)">
{{filter.name}}
<i class="close material-icons">close</i>
</div>
<a class="waves-effect waves-teal btn-flat" (click)="removeFilterAll()">remove all</a>
</div>
`
})
export class ListFiltersComponent {
private filters$: Observable<AkitaFilter[]>;
constructor(private productsService: ProductsFiltersService) {}
ngOnInit() {
this.filters$ = this.productsService.selectFilters();
}
removeFilter(id: any) {
this.productsService.removeFilter(id);
}
removeFilterAll() {
this.productsService.clearFilters();
}
}
You could also set sorting, that will be applied after the filter. Change sorting will also re-emit newly sorted data.
set the sort value
Retrieve the defined sort value,
All filter is managed by an EntityStore, if you need to do more you could access it and use all standard API
get the filter store, It's an Entity store. Be getting the instance, you could do everything than EntityStore could be done.
get the Filter Query. To query the list of your filters. Use the API of EntityFilters.
If you want to use a different filterStore name, you can set it setting params : filtersStoreName when create plugins:
myFilter = new AkitaFiltersPlugin<MyEntitiesState>(this.myEntitiesQuery, {filtersStoreName: 'newFiltersName'});
By default, the name will, your 'EntityStoreName' concat with 'Filter'
If you want to create an akitaFiltersPlugin by giving the AkitaFiltersStore or AkitaFiltersQuery, You can specify in constructor params, an existing AkitaFiltersStore or AkitaFiltersQuery. Useful to create another plugins that use the same Filters store, for exemple to separate your query. @see issue : #10
const myFiltersStore = new AkitaFiltersStore<S>("MyFilters");
const myFiltersQuery = new AkitaFiltersQuery<S>(filtersStore);
myFilter = new AkitaFiltersPlugin<MyEntitiesState>(this.myEntitiesQuery,
{filtersStore: myFiltersStore, filtersQuery: myFiltersQuery});
In filter-utils.ts file, there is a helper function, to do some search filters.
Helper function to do a default filter, that will do a search if the value is the object, or equals otherwise. (only if the filter value is defined)
this.filterForm.controls.search.valueChanges.pipe(untilDestroyed(this)).subscribe((search: string) => {
if (search) {
this.productsService.setFilter({
id: 'search',
value: search,
order: 20,
name: `" ${search} "`,
predicate: defaultFilter);
} else {
this.productsService.removeFilter('search');
}
});
Helper function to do a search on all string element
this.filterForm.controls.search.valueChanges.pipe(untilDestroyed(this)).subscribe((search: string) => {
if (search) {
this.productsService.setFilter({
id: 'search',
value: search,
order: 20,
name: `" ${search} "`,
predicate: (value: ProductPlant, index, array) => {
return searchFilter(search, value);
}
});
} else {
this.productsService.removeFilter('search');
}
});
Helper function to do a search in one string key of an object
this.filterForm.controls.search.valueChanges.pipe(untilDestroyed(this)).subscribe((search: string) => {
if (search) {
this.productsService.setFilter({
id: 'search',
value: search,
order: 20,
name: `" ${search} "`,
predicate: (value: ProductPlant, index, array) => {
return searchFilterIn(search, value, 'name');
}
});
} else {
this.productsService.removeFilter('search');
}
});
Previous options apply all filters locally. But sometimes, you want to call filters by making a request to the server.
To do this, you need to set WithServer options to your AkitaFiltersPlugin. You will provide a callback function, that will be called every time a filter changed.
You will make your call, and return this observable. AkitaFiltersPlugin will subscribe to this observable, and add your new data in the store.
An example, is available in playground page "photos"
myFilter = new AkitaFiltersPlugin<MyEntitiesState>(this.myEntitiesQuery)
.withServer((filtersNormalized: string | HashMap<any>) =>
{
return this.httpClient.get({params: filtersNormalized as HashMap<any>}); // make here your pull request to server and return the observable
});
The data will be filtersNormalized that will be returned by getNormalizedFilters(). This function will return all your server filters in a normalized format :
By default, return a key value object, with all server filters :
{"filter1": "value 1", "filter2": "value 2"}
Or if you specify the NormalizedFilterOptions options in withServer(Callbackfunc, {asQueryParams: true})
You will have directly a serialized version for your query params:
filter1=value%201&filter2=value%202
You can also give some others options :
ìnterface NormalizedFilterOptions {
asQueryParams?: boolean; // display as query params : filter1=value%201&filter2=value%202
withSort?: boolean; // To include sort : { filter1: 'value 1',
// filter2: 'value 2',
// sortBy: 'id',
// sortByOrder: 'desc' }
// or as query params : filter1=value%201&filter2=value%202&sortBy=id&sortByOrder=desc
sortByKey?: string; // to specify the sortBy key used in previous example, default 'sortBy'
sortByOrderKey?: string; // to specify the sortBy key used in previous example, 'sortByOrder'
// will result for example : filter1=value%201&filter2=value%202&_sort=id&_order=desc
}
This will return only all filters that was set as server = true
.
In your components, you will need to only subscribe to your selectAll() function from your store, to get all new data.
Or you can also combine it with locals filters or sort (if you add some filters with server: false
). In this case subscribe to selectAllByFilters().
Trigger a refresh of the data. This will force the library to replay all filters. Very useful for the withServer feature, because it allows to call back the with server callback function with the same parameters, to make a new call to the api.
This specific package is only for Angular Material data table. But Akita filters plugins could be used without angular material. And maybe without angular. This was released since version 4.x as a sub package to avoid error, when not using Angular Material.
npm install --save akita-filters-mat-datasource
yarn add akita-filters-mat-datasource
or to install both package use this :
npm install --save akita-filters-plugin akita-filters-mat-datasource
yarn add akita-filters-plugin akita-filters-mat-datasource
For dealing with angular material table, you need to provide a Data Connector. This connector, help you by just giving the Entity Store. Data Connector, will deal with Akita Filter for you.
Define your data source here :
this.dataSource = new AkitaMatDataSource<EntityState>(EntityQuery);
this.dataSource.setDefaultSort('colomnName', 'asc');
Import it with package :
import { AkitaMatDataSource } from 'akita-filters-mat-datasource';
Then use it in Mat Data Table like other data source.
<table
mat-table
#table
[dataSource]="dataSource"
[trackBy]="trackByName"
matSort
>...</table>
A demo page is available in the playground "angular-material-demo" .
Need to have installed in your project these APIs : "@angular/material": "latest", "@angular/cdk": "latest"
If needed you can specify, an already existing AkitaFiltersPlugins in the constructor. Useful to share it with another page/components. @see demo : angular-material-demo. Else, it will create an internal AkitaFiltersPlugins.
this.dataSource = new AkitaMatDataSource<EntityState>(this.productsQuery, this.productsService.filtersProduct);
Then you have some function that you can use to manage filters
By setting search properties, you set a filter like search.
this.dataSource.search = "Search";
Both use filter and filter, but prefer using search, as filter is so confusing with setFilters functions. ("filter" is here to be the same as the MatDataSource) You can also customize the filter ID, if different in your server API
this.dataSource = new AkitaMatDataSource<PhotosState>(this.photosQuery, this.photosService, {
searchFilterId: 'search', // you can set the id of this filter, usefull if the params is different for you (default : "search")
});
By setting Mat Sort for sorting you can set the sort. Used by Mat Table when changing filter
@ViewChild(MatSort, { static: true }) sort: MatSort;
ngAfterViewInit(): void {
this.dataSource.sort = this.sort;
}
By setting MatPaginator to enable pagination with datasource. Used by Mat Paginator to define pagination
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
ngAfterViewInit(): void {
this.dataSource.paginator = this.paginator;
}
And add the MatPaginator directly like this in the template. No need to give the size, as it will be settled by the data source.
<mat-paginator #paginator
[pageIndex]="0"
[pageSize]="25"
[pageSizeOptions]="[10, 25, 50, 100, 250]">
</mat-paginator>
By setting MatPaginator (cf before), it will use a local pagination. You can specify the option : "serverPagination" to use server side pagination. (Need also to set a MatPaginator). It will subscribe to MatPaginator change page, and create a server filter that send the page Number, and page size. We will be then notified in withServer callback function.
See example in "photos" page.
this.dataSource = new AkitaMatDataSource<PhotosState>(this.photosQuery, this.photosService, {
serverPagination: true, // set to true, to use server side pagination.
pageIndexId: '_page', // you can set the id of this filter, usefull if the params is different for you (default : "page")
pageIndexDisplay: true, // Set to true to display page filter (default: false)
pageIndexName: 'Page', // Set the Name, to you filter, usefull if you want to display the filter : "Name: (value)" (default: "Page")
pageSizeId: '_limit', // you can set the id of this filter, usefull if the params is different for you (default : "size")
pageSizeDisplay: true, // Set to true to display size filter (default: false)
pageSizeName: 'Size', // Set the Name, to you filter, usefull if you want to display the filter : "Name: (value)" (default: "Size")
debounceTimeBetweenTwoChanges: 60, // Debounce time number between two changes, to avoid closest multiples changes events
resetPageIndexOnFiltersChange: true, // If true, will resets page Index after each filters changes (default: true)
});
You can also set options after constructor with function :
this.dataSource = new AkitaMatDataSource<PhotosState>(this.photosQuery, this.photosService).withOptions({
serverPagination: true, // set to true, to use server side pagination.
});
But you will need to set the total by your one, depending on how you get this information.
this.dataSource.total = 100; // set the total to Mat Paginator
console.log('total number', this.dataSource.total); // or get the setted total, was the length setted in Mat Paginator
subscribe to be noticed when a filter has changed (and with server pagination, will exclude pagination filters).
this.dataSource.onFiltersChanges$.subscribe(() => {
// have some actions here
});
Access to the AkitaFilters plugins instance, and use all function from AkitaFilters plugins.
this.dataSource.akitaFiltersPlugin.setFilter({
id: 'category',
value: 'garden',
predicate: (value: ProductPlant, index, array) => value.category === category
});
Set the default sort.
this.dataSource.setDefaultSort('colomnName', 'asc');
Some proxy function, just to call AkitaFilters Plugins.
setFilter(filter: Partial<AkitaFilterBase< S >>): void; // Create or update a filter. Any mandatory properties, will be added
setFilters(filter: Partial<AkitaFilterBase< S >>[]): void; // Create or update multiples filters in one time with emit changes onlu once
removeFilter(id: ID): void; // Remove a filter by their name
removeFilters(id: ID[]): void; // Remove multiples filters with emit changes onlu once
clearFilters(): void; // Remove all filters
getFilterValue< S >(id: string): E | null; // Get a filters values
Compatibility has been added with all methods and properties present in MatDatasource. (except the method "sortData" because the sorting in Akita works differently). Which implements the MatTableDataSourceInterface which takes all the methods and properties of the following MatTableDataSource:
export interface MatTableDataSourceInterface<T> {
data: T[];
filter: string;
paginator: MatPaginator;
sort: MatSort;
filterPredicate: ((data: T, filter: string) => boolean);
filteredData: T[];
// Not Implemented : sortData: ((data: T[], sort: MatSort) => T[]);
sortingDataAccessor: ((data: T, sortHeaderId: string) => string | number);
}
- data : getter and setter for the data that will be managed in the datasource
- filter: string, to perform a full text search in the data.
- paginator: getter and setter to use pagination with MatPaginator
- sort: getter and setter to use sorting with MatSort
- filteredData : to retrieve synchronously the filtered data in the datasource
Is the function that is called by default, for filtering the data during the simple search. This method can be overridden to use a different function as it is the case for MatDataSource.
/**
* Checks if a data object matches the data source's filter string. By default, each data object
* is converted to a string of its properties and returns true if the filter has
* at least one occurrence in that string. By default, the filter string has its whitespace
* trimmed and the match is case-insensitive. May be overridden for a custom implementation of
* filter matching.
* @param data Data object used to check against the filter.
* @param filter Filter string that has been set on the data source.
* @returns Whether the filter matches against the data
*/
filterPredicate: ((data: E, filter: string) => boolean) = (data: E, searchFilter: string) => {
return defaultFilter(data, null, null, {value: searchFilter});
}
Throw an error, because this function cannot be used in akita sorting.
You can use instead "sortFunction" which will be the function used with Akita, to perform the comparison.
The override method should have this signature :
((a: E, b: E, sort: MatSort) => 0 | 1 | -1) = (a, b, sort): 0 | 1 | -1
As in MatTableDataSource, you can define a custom sortingDataAccessor, that need to have this signature :
((data: T, sortHeaderId: string) => string | number)
/**
* Data accessor function that is used for accessing data properties for sorting through
* the default sortFunction.
* This default function assumes that the sort header IDs (which defaults to the column name)
* matches the data's properties (e.g. column Xyz represents data['Xyz']).
* May be set to a custom function for different behavior.
* @param data Data object that is being accessed.
* @param sortHeaderId The name of the column that represents the data.
*/
sortingDataAccessor: ((data: T, sortHeaderId: string) => string | number) =
(data: T, sortHeaderId: string): string|number => {
const value = (data as {[key: string]: any})[sortHeaderId];
if (_isNumberValue(value)) {
const numberValue = Number(value);
// Numbers beyond `MAX_SAFE_INTEGER` can't be compared reliably so we
// leave them as strings. For more info: https://goo.gl/y5vbSg
return numberValue < MAX_SAFE_INTEGER ? numberValue : value;
}
return value;
}
You can also use AkitaFilters plugins withServer in Akita-Mat-DataSource by using AkitaFilters Plugins function. Or there is also a function similar in Akita-Mat-DataSource.
Akita-mat-data-source is now a subpackage to avoid error with akita-filters-plugin, if you don't use Angular Material.
Changes this
import { AkitaMatDataSource } from 'akita-filters-plugin';
to
import { AkitaMatDataSource } from 'akita-filters-mat-datasource';
To correspond with Akita, you need now to specify only the entityState. The entity element is calculated as in akita with getEntityType
Changes this
new AkitaFiltersPlugin<MyEntitiesState, MyEntity>()
to
new AkitaFiltersPlugin<MyEntitiesState>()
Changes this
AkitaFilter<MyEntitiesState, MyEntity>[]
to
AkitaFilterBase<MyEntitiesState>[]
Changes this
new AkitaMatDataSource<MyEntity, MyEntitiesState>()
to
new AkitaMatDataSource<MyEntitiesState>()
Only update all libraries : Use last version of Angular, and last version of Akita
Deprecation of AkitaFilter type. Use AkitaFilterLocal when you use local filters, and AkitaFilterServer, when you use server feature. All type extends AkitaFilterBase, this type is used in the libraries. So you can also use it.
update to angular 10. So this libs should not be fully compatible with previous Angular version.
- update to angular 16-17 and Build in Ivy modes. So this lib should not be fully compatible with previous Angular version.
- Akita filters Mat Datasource is no longer published in the same package o Akita-filter-plugins.
- You need to install it from a separate package
- Imports it from different package
- minimal required version was angular 16 and Akita 8
- Updated all repository
Install the new module akita-filters-mat-datasource
npm install --save akita-filters-mat-datasource
yarn add akita-filters-mat-datasource
Import it with package :
import { AkitaMatDataSource } from 'akita-filters-mat-datasource';
npm page : https://www.npmjs.com/package/akita-filters-mat-datasource