A Search Interface for Your Angular Application

Some users utilize the search textbox as a final resort if they’ve failed at everything to find what they’re looking for. For some it is the first thing to look for to quickly find the information they need. Whatever the user behavior, the search textbox is an essential feature to the user experience. In this article, we will build a prototype that simulates such functionality most commonly seen in modern applications today.

See a live demo of the application here.

Project Setup

For this project you will need to install Angular CLI 1.6.5. You will also need to have Node 6.9.0 or higher.

npm i -g @angular/cli

We will also be utilizing Lodash 4.6.1, a JavaScript utility library that will help us with a few simple tasks.

npm i lodash -S

Here’s overview of the application’s file tree:

app
│   app.component.css
│   app.component.html
│   app.component.spec.ts
│   app.component.ts
│   app.module.ts
│   product.ts
│
├───pipes
│       search.pipe.spec.ts
│       search.pipe.ts
│
├───search
│       search.component.css
│       search.component.html
│       search.component.scss
│       search.component.spec.ts
│       search.component.ts
│
└───services
        product.service.spec.ts
        product.service.ts

The Products

We create a constant array PRODUCTS from the Product class. This will serve as our application’s data source.

products.ts

export class Product {

    constructor(

        public id: number,
        public name: string,
        public type: string,
        public status: string,
        public price: number,
        public description: string,
        public is_valid: boolean
    ) {}    
}

Define an array of Products using the Product class.

export const PRODUCTS: Product[] = [
    { id: 1, name: 'Medavac Survival Kit', type: 'Camping Gear', status: 'In stock', price: 14.99, description: 'First Aid Kit', is_valid: true },
    { id: 2, name: 'WazSUP Stand Up Paddle Board', type: 'Outdoor Sports Equipment', status: 'In stock', price: 975.00, description: 'Wave Rider', is_valid: true },
    { id: 3, name: 'Pole Benda Fishing Rod', type: 'Fishing Gear', status: 'Out of stock', price: 129.99, description: 'Made of graphite composite', is_valid: false },
    { id: 4, name: 'The Great Outdoors Dome Tent', type: 'Camping Equipment', status: 'In stock', price: 300.00, description: 'Camping shelter', is_valid: true },
    { id: 5, name: 'KalusTum Conventional Reel', type: 'Fishing Gear', status: 'In stock', price: 47.98, description: 'For Big game fish', is_valid: true },
    { id: 6, name: 'ClearBlue Dive Mask', type: 'Diving Gear', status: 'On Order', price: 96.00, description: 'Fog resistant', is_valid: true }
];

The Service

Create a service so that the component can retrieve the products from the data source. Remember that a service is injected into a Component’s constructor method, a system known as dependency injection. This approach allows a service to be shared with other components.

Command to generate a service:

ng g service services/product

product.service.ts

import { Injectable } from '@angular/core';
import { PRODUCTS } from '../product';

@Injectable()
export class ProductService {

  constructor() {}

      getProducts() {

          return PRODUCTS;
      }
}

The Custom Pipe

For this prototype, we create a custom pipe productSearch that takes a parameter productSearchInput. The pipe works in conjunction with the component’s ngFor directive.

// FROM search.component.html
<div *ngFor="let product of (products | productSearch : productSearchInput)" class="product-line-item">

The parameter’s value gets passed when the user clicks the Enter key or clicks outside of the search textbox after typing in some text.

// FROM search.component.html
<input #box placeholder="Search Products" (keyup.enter)="update(box.value)" (blur)="update(box.value)">

The pipe’s transform() method then takes this parameter value and passes it into a regular expression (“regex”), which does a number of tasks. For one, if the user happened to initially type blank spaces or special characters into the search textbox, which are flagged as invalid, the regex replaces them with an empty string. If you take a look at the following code, the regex finds a match for anything that is NOT in the brackets.

strTrimPreWhitespace = strUserInput.replace(/(^\s*$|[^\w\d\s.\/,\s\w]|_)/gi, '');

Think of the brackets as a whitelist. Anything inside of the brackets will be flagged as valid. If invalid characters were found, the pipe immediately returns an empty array to the component and a “No results found” message is shown to the user. If no invalid characters were detected, the pipe loops through the Product model and finds matches of any value in all products’ properties against the regex value. Finally, the pipe returns an array of products to the component for displaying the search results, otherwise, the pipe returns an empty array signaling to the component to show the “No results found.” message.

Command to generate a pipe:

ng g pipe pipes/search

search.pipe.ts

import { Pipe, PipeTransform } from '@angular/core';
import { Product } from '../product';
import * as _ from 'lodash';

@Pipe({

    name: 'productSearch'
})

export class SearchPipe implements PipeTransform {

    transform(allProducts: Product[], strUserInput: any): any {

        let arrKey: any[] = Object.keys(allProducts),
        objProduct: any = Object,
        arrProps: any[] = [],
        arrProductProps: any[] = [],
        regExResult: any[] = [],
        strProductProps: string = "",
        strTrimPreWhitespace: string = "",
        prop: any,
        regEx: RegExp;

        strTrimPreWhitespace = strUserInput.replace(/(^\s*$|[^\w\d\s.\/,\s\w]|_)/gi, '');

        // CONSTRUCTOR
        regEx = new RegExp(strTrimPreWhitespace, "gi");

        // LOOP THROUGH PRODUCTS
        arrKey.forEach((key: any) => {

            objProduct = allProducts[key];
            arrProductProps = _.map(objProduct);

            // LOOP THROUGH PRODUCT PROPERTIES AND LOOK FOR STRING MATCHES WITH USER INPUT
            for(prop = 1; prop <= arrProductProps.length; prop++){

                strProductProps = _.toString(arrProductProps[prop]);
                regExResult = strProductProps.match(regEx);

                if(_.toString(regExResult) === strUserInput || _.lowerCase(_.toString(regExResult)) ===
                _.lowerCase(strUserInput)) {
                    
                    arrProps.push(objProduct);
                }
            }
        });

        // RETURN EMPTY ARRAY IF SPACES WERE INITIALLY ENTERED
        if(strTrimPreWhitespace === '') {

            return [-1];
        }

        // RETURN PRODUCTS
        if(arrProps.length > 0) {

            return arrProps;
        }
        
        // NO MATCH
        if(arrProps.length == 0) {
            
            return [-1];
        }
    }
}

The Styles

If you want to create the styles for your application from scratch, you can easily set your preference in
the angular-cli.json file. Look for the “defaults” object and set it’s styleExt property to either scss or css. I personally develop styles with scss and rarely write vanilla css. For this prototype, scss is the choice. Then, in your component, add the scss file and reference the file in the syleUrls property.

.angular-cli.json

...
"defaults": {
    "styleExt": "scss",
    "component": {}
  }
...

Then in your component specify:

styleUrls: ['./search.component.scss']

To set the style generation method from css to scss on an existing project using the command line:

ng set defaults.styleExt scss

You could also choose your style generation preference when you create a new project with this command:

ng new search --style=sass

The Component

The component’s job is to initiate a product search and present the user with the search results. When the pipe returns the array of products to the component, the ngFor directive loops the array and displays the results to the user. Each search result presented will also be clickable to reveal more details about the item.

Command to generate a component:

ng g component search/search

search.component.ts

import { Component } from '@angular/core';
import { ProductService } from '../services/product.service';
import { Product } from '../product';

@Component({
  selector: 'app-search',
  templateUrl: './search.component.html',
  styleUrls: ['./search.component.scss'],
  providers: [ProductService]
})

export class SearchComponent {

    products: any[] = [];
    theSelectedProduct: Product;
    showProductDetails: boolean = false;
    search: boolean = true;
    title: string = "";
    filteredProductType: string = "";
    filteredProductStatus: string = "";
    filteredProductCustomer: string = "";
    productSearchInput: string = "";
    activeProduct: string = "";

    constructor(ProductService: ProductService) {

        this.products = ProductService.getProducts();    
    }
    
    update(value:string) {

        this.productSearchInput = value;
        this.title = value;
    }

    onSelect(product: Product) {

        this.showProductDetails = true;
        this.theSelectedProduct = product;
        this.search = false;
        this.activeProduct = this.theSelectedProduct.is_valid ? "Yes" : "No";
    }

    searchProductType() {

        this.showProductDetails = false;
        this.search = true;
        this.productSearchInput = "";
    }
}

search.component.html

<div class="search-component">
    <div class="textbox-wrap" *ngIf="search">
        <input #box placeholder="Search Products" (keyup.enter)="update(box.value)" (blur)="update(box.value)">
    </div>
    <div class="search-results" *ngIf="productSearchInput">
        <h2>You searched for: {{title}}</h2>
        <div class="product-line-item" *ngFor="let product of (products | productSearch : productSearchInput)">
            <p *ngIf="product === -1">No results found.</p>
            <ul class="item-master" *ngIf="product !== -1" (click)="onSelect(product)">
                <li>{{product.name}}</li>
                <li>${{product.price}}</li>
            </ul>
        </div>
    </div>
    <div class="item-detail" *ngIf="showProductDetails">
        <button (click)="searchProductType()">New Search</button>
        <h3>Details for {{theSelectedProduct.name}}</h3>
        <ul>
            <li>Product ID: {{theSelectedProduct.id}}</li>
            <li>Name: {{theSelectedProduct.name}}</li>
            <li>Type: {{theSelectedProduct.type}}</li>
            <li>Status: {{theSelectedProduct.status}}</li>
            <li>Price: ${{theSelectedProduct.price}}</li>
            <li>Product Description: {{theSelectedProduct.description}}</li>
            <li>Active Product: {{activeProduct}}</li>
        </ul>
    </div>
</div>

The root Module

Define the component, custom pipe, and service for the application.

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { SearchComponent } from './search/search.component';
import { SearchPipe } from './pipes/search.pipe';
import { ProductService } from './services/product.service';

@NgModule({
  declarations: [
    AppComponent,
    SearchComponent,
    SearchPipe
  ],
  imports: [
    BrowserModule,
    FormsModule
  ],
  providers: [ProductService],
  bootstrap: [AppComponent]
})
export class AppModule { }

Finally, add the app-search selector in the main component’s template to load the app into:
app.component.html

<app-search></app-search>

Conclusion

This article is just a simple demonstration on how you can build a search feature for your application. From here, you can apply additional functionality to the search feature such as loading data from a remote source by a ReST API call or add complex search filtering so your users can fine-tune searches on your data based on a set of specific criteria.

Checkout the source code for this application on GitHub.

Featured photo by  

unsplash-logoAlex Franzelin