Skip to main content
Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.

Using A Single, Pre-Compiled Keyword Search Target For Filtering In Angular 10.1.5

By Ben Nadel on

The other day, I picked up a cool Angular trick from fellow InVision engineer, Josh Siok: when performing a keyword-based search on a given collection, he will pre-compile a "keywords" String for each item. Then, when he goes to perform the keyword-based filtering, he only has to inspect the one pre-compiled value. At work, we do a lot of in-page filtering; as such, I wanted to explore this approach in Angular 10.1.5.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

To explore this idea, I'm going to take a collection of Friends and wrap it in another Array for safer mutations. This way, I can augment the wrapper-Array without worrying about corrupting the underlying data, which may be referenced by other parts of my application. In this case, the underlying friends collection uses this TypeScript interface:

interface Friend {
	id: number;
	name: string;
	isBFF: boolean;
	hobbies: string[];
}

And, the wrapper-Array that will render the search results uses this TypeScript interface:

interface SearchResult {
	friend: Friend;
	sort: string;
	keywords: string;
}

As you can see, the wrapper-Array is going to contain two search-based properties:

  • sort
  • keywords

Both of these values represent a pre-compiled data-point that aggregates values from within the embedded friend payload. The former determines how the results will be sorted, bubbling BFF (Best-Friends Forever) to the top; and, the latter determines which results will match a given search query, incorporating the Name, BFF, and Hobby values.

Functionally speaking, we're going to have this:

  • Sort = stringify( isBFF + Name )

  • Keywords = stringify( Name + isBFF + Hobbies )

Here's what this looks like in my App component - the wrapper-Array is computed in setAllSearchResults() method and the filtering is then applied in the setFilteredSearchResults() method:

// Import the core angular services.
import { Component } from "@angular/core";

// Import the application components and services.
import { Friend } from "./friends";
import { friends } from "./friends";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

interface SearchResult {
	friend: Friend;
	sort: string;
	keywords: string;
}

@Component({
	selector: "app-root",
	styleUrls: [ "./app.component.less" ],
	templateUrl: "./app.component.html"
})
export class AppComponent {

	public allSearchResults!: SearchResult[];
	public filteredSearchResults!: SearchResult[];
	public searchFilter: string;

	private friends: Friend[] = friends;

	// I initialize the app component.
	constructor() {

		this.searchFilter = "";
		this.setAllSearchResults();
		this.setFilteredSearchResults();

	}

	// ---
	// PUBLIC METHODS.
	// ---

	// I update the filtered search results to use the given filter.
	public applySearchFilter( searchFilter: string ) : void {

		this.searchFilter = searchFilter.trim();
		this.setFilteredSearchResults();

	}

	// ---
	// PRIVATE METHODS.
	// ---

	// I setup the all-results collection based on the current friends.
	private setAllSearchResults() : void {

		this.allSearchResults = this.friends.map(
			( friend ) => {

				// When we sort the results, we want to bubble the BFFs to the top. In
				// order to simplify this operation - treating it as an alpha-numeric
				// sort - we're going to prefix the calculated sort value with a string
				// that separates out the two cohorts.
				var sortPrefix = ( friend.isBFF )
					? "a|"
					: "z|"
				;

				// Now, the sortable target will be implicitly sorted by BFF first and
				// then Name second.
				var sort = ( sortPrefix + friend.name ).toLowerCase();

				// When the user searches the list, we want them to be able to search
				// across a variety of data-points. In order to simplify this operation,
				// we're going to pre-compile a "keywords" payload that aggregates all of
				// the targeted data-points.
				var keywords = [ friend.name ]
					.concat( friend.hobbies )
					.concat( friend.isBFF ? "bff" : "" )
					.join( "\n" )
					.toLowerCase()
				;

				return({
					friend: friend,
					sort: sort,
					keywords: keywords
				});

			}
		);

		// Note that our in-place sort just uses the pre-compiled "sort" property - it
		// doesn't need to inspect the "friend" object at this point.
		this.allSearchResults.sort(
			( a, b ) => {

				return( a.sort.localeCompare( b.sort ) );

			}
		);

	}


	// I setup the filtered-results collection based on the current all-results.
	private setFilteredSearchResults() : void {

		var normalizedFilter = this.searchFilter.toLowerCase();

		// If there is no search-filter, then we can just reset the filtered-results to
		// be the all-results collection.
		if ( ! normalizedFilter ) {

			this.filteredSearchResults = this.allSearchResults;
			return;

		}

		// Note that when we apply the filter against the search result, we only have to
		// examine the pre-compiled "keywords" value - we don't have to start searching
		// across a number of embedded properties - that work has already been done.
		this.filteredSearchResults = this.allSearchResults.filter(
			( result ) => {

				return( result.keywords.includes( normalizedFilter ) );

			}
		);

	}

}

As you can see, by pre-compiling the sort and keywords payload, our subsequent .sort() and .filter() operations become dead simple, respectively:

  • return( a.sort.localeCompare( b.sort ) );

  • return( result.keywords.includes( normalizedFilter ) );

No messing around with different properties, no digging into embedded objects - we just use the single, pre-compiled String values. Easy peasy!

Here's the HTML view template for this component:

<p>
	<input
		#searchFilterRef
		type="search"
		placeholder="Search friends..."
		autofocus
		autocomplete="off"
		class="filter"
		(input)="applySearchFilter( searchFilterRef.value )"
	/>
</p>

<!--
	When we output the list, we're outputting the FILTERED search results, not the ALL
	search results.
-->
<ul class="results">
	<li
		*ngFor="let result of filteredSearchResults"
		class="results__result"
		[class.results__result--highlight]="result.friend.isBFF">

		<div class="results__name">
			{{ result.friend.name }}
		</div>

		<div *ngIf="result.friend.hobbies.length" class="hobbies">
			<span class="hobbies__label">
				Hobbies:
			</span>
			<span
				*ngFor="let hobby of result.friend.hobbies"
				class="hobbies__hobby">
				{{ hobby }}
			</span>
		</div>

	</li>
</ul>

<div
	*ngIf="( ! filteredSearchResults.length )"
	class="no-results">

	None of your {{ allSearchResults.length }} friends match
	your current search query.

</div>

Now, if we run this Angular 10 application and we try to search for the following keywords:

  • brooke - friend.name based search.
  • bff - friend.isBFF based search.
  • golf - friend.hobbies based search.

... we get the following browser output:

Filtering a collection of friends using a keyword search in Angular 10.

As you can see, we were able to successfully locate matching friend results using the keyword-based search. Also note that the BFF results always bubbled to the top in the sort.

Obviously, keyword-based searching provides "fuzzy" matches. As such, you may want other techniques in place to provide more exact matching on specific properties. But, for a simple, open-ended search in Angular 10.1.5, this seems like a really easy and elegant solution.



Reader Comments

@All,

A quick follow-up post to this, once you have two different arrays - one for the "all" results and one for the "filtered" results - it means that you can start to add a progressive-search optimization with next-to-no effort:

www.bennadel.com/blog/3910-using-a-progressive-search-optimization-when-filtering-arrays-in-angular-10-1-6.htm

This approach uses the filtered search results as the target for each subsequent search operation as long as the user is "typing forward".

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Blog
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.