In this article we implemented an Angular Todo application.
And the backbone of it was the Firefly Semantics Slice Reactive State Manager.
Now we are going to add search as well.
Approach
First fork the Stackblitz Demo from this article.
Dependencies Update
Add lunr
and @types/lunr
to the dependencies.
Search Component
Add search.component.ts
to the app/components
folder.
import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';
import { StateService } from '../services/state.service';
@Component({
selector: 'app-search-todo',
template: `
<div class="input-field">
<i class="material-icons prefix">search</i>
<input placeholder="Search"
(keyup)="search($event)"
[value]="s.todoStore.query">
</div>`,
})
export class TodoSearchComponent {
constructor(public s: StateService) {}
searchControl: FormControl = new FormControl(this.s.todoStore.query);
search(event: any) {
const element = event.currentTarget as HTMLInputElement;
const value = element.value;
this.s.onSearch(value);
}
}
This component listens to keyup
events on the input
field and emits the query value via the StateService
‘s onSearch
function.
Also register the component in the declarations
section of app.module.ts
.
State Service Update
Within the services/state.service.ts
file import lunr
.
import lunr from 'lunr';
In the constructor
initialize the todoStore
query
.
//============================================
// Set the todoStore.query to something
// so the obervable fires at least once.
//============================================
this.todoStore.query = '';
Also updated the expression that creates the selectedTodos$
observable so that the expression includes the todoStore
query observable.
//============================================
// Observe the Selected Todo category
//============================================
this.selectedTodos$ = combineLatest([
this.activeFilter$,
this.completeTodos$,
this.incompleteTodos$,
this.todos$,
this.todoStore.observeQuery(),
]).pipe(
map((arr) => {
return this.applyFilter(arr[0], arr[1], arr[2], arr[3], arr[4]);
})
);
We also need to update the applyFilter
function to apply the query from the todoStore
if the user has entered a query
value into the input
field.
public applyFilter(
filter,
completeTodos,
incompleteTodos,
todos,
query
): Todo[] {
switch (filter) {
case VISIBILITY_FILTER.SHOW_COMPLETED:
if (query) {
return this.search(query, completeTodos);
}
return completeTodos;
case VISIBILITY_FILTER.SHOW_ACTIVE:
if (query) {
return this.search(query, incompleteTodos);
}
return incompleteTodos;
default:
if (query) {
return this.search(query, todos);
}
return todos;
}
}
In order or to be able to search through the entered Todo
instances we index them with lunr
:
initializeSearchIndex(todos: Todo[]): lunr.Index {
const idx: lunr.Index = lunr(function () {
this.ref('gid');
this.field('complete');
this.field('title');
todos.forEach((todo) => {
this.add(todo);
});
});
return idx;
}
Note that in the callback
function to lunr we call this.ref('gid')
to let lunr
know that each Todo
instance should be identified by the gid
value.
We also implement a search function to use the created index:
search(query: string, todos: Todo[]): Todo[] {
const idx = this.initializeSearchIndex(todos);
const result: lunr.Index.Result[] = idx.search(query);
return result.map((r) => {
return this.todoStore.findOne(r.ref);
});
}
Lastly add an onSearch
function that we will bind in our search.component.ts
to receive the search queries on keyup
events.
onSearch(query: string) {
this.todoStore.query = query;
}
Update the Application
Now that that’s all done we can update the application.component.html with <app-search-todo></app-search-todo>
.
It should look like this:
<div style="margin: 2rem;">
<app-add-todo></app-add-todo>
<app-search-todo></app-search-todo>
<app-todos-filter></app-todos-filter>
<app-todos></app-todos>
<div *ngIf="s.finito$ | async">Finito!</div>
</div>
And that’s it. We can now both filter
and search
within the filter of the app.