0.3.0 • Published 6 months ago

@r-ko/ngx-category-search v0.3.0

Weekly downloads
-
License
MIT
Repository
github
Last release
6 months ago

ngx-category-search

NPM Version License: MIT Build Status

Version: 0.3.0

ngx-category-search is a powerful and highly configurable Angular component designed to create intuitive, category-driven search experiences. Inspired by the Azure Portal's search paradigm, it provides a flexible UI layer that decouples data fetching, allowing developers to integrate sophisticated search functionalities with ease.


Table of Contents

  1. Core Features
  2. Design Philosophy
  3. Installation
  4. Basic Usage
  5. API Reference
  6. Customization
  7. Development & Contribution
  8. License

Core Features

  • Decoupled Data Fetching: Emits debounced search terms (searchRequested), allowing the host application to manage data retrieval. Results are fed back via the searchResults input.
  • Flexible Initial Data: Accepts initial category context either as an array of data items (T[]) or a direct Record<string, number> of category counts via the data input.
  • Dynamic Categorization: Intelligently displays filter chiclets based on categories derived from initial data and live search results.
  • Structured Result Presentation: Organizes search results visually under collapsible category headers within the dropdown.
  • Stateful Recent Searches: Enhances user workflow with optional localStorage-backed persistence of recent search terms.
  • Controlled Result Visibility: Manages large category result sets gracefully with "Show more" links for progressive disclosure.
  • Seamless Navigation Hooks: Provides dedicated output events (navigateToAll, navigateToCategory) for integrating with application routing.
  • "Search for {term}" Line: Offers a configurable line to trigger a global search for the entered term, with support for basic HTML formatting in the display string.
  • Enter Key Action: Emits an enterKeyPressed event, allowing custom actions when the user presses Enter in the search input.
  • Extensive UI Customization: Offers ng-template outlets for nearly every UI element (results, chiclets, chiclet content, headers, header content, input adornments, loading/empty states).
  • Fine-Grained Configuration: A rich set of @Input properties allows precise tuning of behavior (debouncing, batching, initial state, feature toggles like chiclet container border) and localization (UI labels).
  • Advanced Highlighting: Includes HighlightPipe for single-term highlighting and MultiWordHighlightPipe for highlighting multiple words from the search query.
  • Themeable Foundation: Leverages CSS Custom Properties for straightforward styling and adaptation to various design systems.
  • Modern Angular: Built as a standalone component, promoting modularity and aligning with current Angular best practices.
  • Type Safety: Utilizes generics (<T extends SearchDataItem>) for enhanced type checking in consuming applications.

Design Philosophy

  • Separation of Concerns: The component deliberately focuses on the presentation and interaction aspects of search, leaving data fetching and state management to the consuming application. This promotes flexibility and testability.
  • Configurability over Opinion: Provides numerous inputs and template hooks to adapt to diverse requirements rather than imposing rigid structures.
  • Developer Experience: Aims for a clear API surface and straightforward integration, leveraging standard Angular patterns.

Installation

npm install @r-ko/ngx-category-search@^0.3.0

Or using yarn:

yarn add @r-ko/ngx-category-search@^0.3.0

Peer Dependencies:

Ensure your project includes compatible versions of these packages:

  • @angular/common: ^17.1.0 || ^18.0.0 || ^19.0.0
  • @angular/core: ^17.1.0 || ^18.0.0 || ^19.0.0
  • @angular/forms: ^17.1.0 || ^18.0.0 || ^19.0.0
  • @angular/platform-browser: ^17.1.0 || ^18.0.0 || ^19.0.0 (Required for DomSanitizer used by MultiWordHighlightPipe and the "Search for {term}" feature)
  • rxjs: ~7.8.0

Basic Usage

  1. Import CategorySearchComponent (and optionally MultiWordHighlightPipe) into your Angular component or module:

    // In your component.ts
    import { Component } from '@angular/core';
    import { CommonModule } from '@angular/common';
    import { CategorySearchComponent, MultiWordHighlightPipe, SearchDataItem } from '@r-ko/ngx-category-search';
    import { YourSearchService, YourDataType } from './your-search.service'; // Your application's service
    import { Observable, of } from 'rxjs';
    
    @Component({
      selector: 'app-my-search-feature',
      standalone: true,
      imports: [
        CommonModule,
        CategorySearchComponent,
        MultiWordHighlightPipe // If using custom templates with multi-word highlighting
      ],
      providers: [YourSearchService],
      templateUrl: './my-search-feature.component.html',
      // styleUrls: ['./my-search-feature.component.css']
    })
    export class MySearchFeatureComponent {
      initialCategoryData$: Observable<Record<string, number> | null>;
      searchResults$: Observable<YourDataType[] | null> = of(null);
    
      // Define required fields for your data type
      idField: keyof YourDataType = 'id'; // Replace 'id' with your actual ID field
      nameField: keyof YourDataType = 'name'; // Replace 'name' with your actual name field
      categoryField: keyof YourDataType = 'category'; // Replace 'category' with your actual category field
    
      constructor(private searchService: YourSearchService) {
        this.initialCategoryData$ = this.searchService.getInitialCategoryCounts();
      }
    
      onSearchRequested(term: string): void {
        if (term) {
          this.searchResults$ = this.searchService.fetchResults(term);
        } else {
          this.searchResults$ = of(null);
        }
      }
    
      onItemSelected(item: YourDataType): void {
        console.log('Item selected:', item);
        // Navigate or perform action
      }
    
      onEnterPressed(term: string): void {
        console.log('Enter pressed with term:', term);
        // Optionally trigger a global search or navigation
      }
       // ... other event handlers
    }
  2. Add the component to your template:

    <!-- my-search-feature.component.html -->
    <div style="max-width: 700px; margin: 20px auto;">
      <ncs-category-search
        [data]="initialCategoryData$ | async"
        [searchResults]="searchResults$ | async"
        [trackByIdField]="idField"
        [nameField]="nameField"
        [categoryField]="categoryField"
        placeholder="Search for products, articles, users..."
        [enableRecentSearches]="true"
        [showSearchForTermLine]="true"
        [searchForTermFormat]="'Search all items for <b>{term}</b>'"
        (searchRequested)="onSearchRequested($event)"
        (itemSelected)="onItemSelected($event)"
        (enterKeyPressed)="onEnterPressed($event)"
        (navigateToAll)="handleNavigateToAll($event)"
        (navigateToCategory)="handleNavigateToCategory($event)">
        <!-- Optional: Add custom templates here -->
      </ncs-category-search>
    </div>

    Your YourSearchService would handle API calls and data transformation. YourDataType should extend SearchDataItem (which is Record<string, any>) or be compatible.

API Reference

Component Selector

<ncs-category-search>

Inputs

InputTypeRequiredDefault ValueDescription
dataT[] \| Record<CategoryKey, number> \| nullfalsenullOptional: Data for initial category chiclets. Can be an array of items (component calculates counts) or a direct map of category counts.
searchResultsT[] \| nullfalsenullCore Input: Results provided by the parent based on searchRequested. Should be an array of items.
trackByIdFieldkeyof TtrueundefinedProperty name of the unique identifier for each item (used for trackBy).
nameFieldkeyof TtrueundefinedProperty name of the primary display text for each item.
categoryFieldkeyof TtrueundefinedProperty name used for grouping results into categories.
friendlyIdFieldkeyof T \| nullfalsenullOptional: Property name of a secondary identifier for display (e.g., a human-readable ID).
placeholderstringfalse'Search...'Placeholder text for the input field.
debounceMsnumberfalse300Delay (in milliseconds) before searchRequested is emitted after the user stops typing.
resultsBatchSizenumberfalse5Initial number of items shown per category in the results list before a "Show more" link appears.
minSearchLengthnumberfalse1Minimum number of characters required in the search term to trigger the searchRequested event.
initialDropdownState'closed' \| 'openOnFocus'false'openOnFocus'Controls whether the dropdown opens automatically when the input field receives focus and is empty.
closeOnItemSelectbooleanfalsetrueIf true, the dropdown closes after an item is selected.
closeOnNavigatebooleanfalsetrueIf true, the dropdown closes after a navigation link (e.g., "Show all", "Search for {term}") is clicked.
enableRecentSearchesbooleanfalsetrueToggles the display and localStorage persistence of recent search terms.
maxRecentSearchesnumberfalse5Maximum number of recent searches to store and display.
recentSearchesKeystringfalse'ngx_category_search_recent'The localStorage key used for storing recent searches.
showSearchForTermLinebooleanfalsefalseIf true, displays a line below chiclets (when a search term is active) to trigger a global search for the term.
searchForTermFormatstringfalse'Search for {term}'Format string for the "Search for {term}" line. The library replaces {term} with the current search term. Can include <b>{term}</b>.
allCategoryLabelstringfalse'All'Label for the "All" category chiclet.
recentSearchesLabelstringfalse'Recent Searches'Section header text for recent searches.
noRecentSearchesLabelstringfalse'No recent searches.'Text displayed when no recent searches are available.
noResultsLabelstringfalse'No results found for'Prefix for the message shown when no search results match the term. The term is appended.
showAllResultsLabelstringfalse'Show all'Prefix for the button at the bottom of the dropdown to navigate to all results. Counts and term are appended.
showMoreLabelstringfalse'Show more'Text for the "Show more" link within individual category result groups.
showAllCategoryLabelstringfalse'Show all'Prefix for the link in category headers to navigate to all results for that specific category. Count is appended.
showBottomShowAllButtonbooleanfalsetrueToggles visibility of the main "Show all X results for 'term'" button at the bottom of the dropdown.
showCategoryShowAllLinkbooleanfalsetrueToggles visibility of the "Show all count" link in category headers.
showCategoryShowMoreLinkbooleanfalsetrueToggles visibility of the "Show more" link in category headers.
showInitialCategoriesbooleanfalsetrueToggles visibility of initial category chiclets (requires data input to be provided).
showResultCategoriesbooleanfalsetrueToggles visibility of category chiclets when search results are displayed.
hideAllChicletInitialbooleanfalsetrueIf true, hides the "All" chiclet in the initial view (when no search term is active).
showChicletContainerBorderbooleanfalsetrueToggles visibility of the border below the chiclet container.
inputPrefixTemplateTemplateRef<any> \| nullfalsenullCustom template to render content before the input field within the input group.
inputSuffixTemplateTemplateRef<any> \| nullfalsenullCustom template to render content after the input field (e.g., a custom clear button or search icon).
resultItemTemplateTemplateRef<{$implicit: T, term: string}> \| nullfalsenullCustom template for rendering each individual result item in the list.
recentItemTemplateTemplateRef<{$implicit: string}> \| nullfalsenullCustom template for rendering each recent search term in the list.
chicletTemplateTemplateRef<{$implicit: {category: string, count: number, isActive: boolean, type: 'initial' \| 'results'}}> \| nullfalsenullCustom template for the entire chiclet button. If provided, you are responsible for its appearance and click handling (via selectCategory).
chicletContentTemplateTemplateRef<{$implicit: {category: string, count: number, isActive: boolean, type: 'initial' \| 'results'}}> \| nullfalsenullCustom template for the content inside the default chiclet button structure.
categoryHeaderTemplateTemplateRef<{$implicit: {category: string, count: number}, actions?: TemplateRef<any>}> \| nullfalsenullCustom template for the entire category header row, including its title and action links area.
categoryHeaderContentTemplateTemplateRef<{$implicit: {category: string, count: number}}> \| nullfalsenullCustom template for the content inside the default category header (specifically the name and count part).
noResultsTemplateTemplateRef<{$implicit: string, isTermTooShort?: boolean, minSearchLengthVal?: number}> \| nullfalsenullCustom template displayed when no results are found or if the term is too short. Context includes isTermTooShort and minSearchLengthVal.
loadingTemplateTemplateRef<any> \| nullfalsenullCustom template displayed while search results are being loaded (when isLoading is true).

(T represents the generic type of your data items, which should be compatible with Record<string, any>. CategoryKey is an alias for string.)

Outputs

OutputPayload TypeDescription
searchRequestedstringCore Output: Emitted after debounceMs when the search term changes; signals the parent application to fetch data for the given term.
itemSelectedTEmitted when a search result item is selected from the list.
recentSearchSelectedstringEmitted when a recent search term is selected. This also populates the input and triggers searchRequested.
categorySelectedstringEmitted when a category chiclet is selected (only in the results view, not the initial view). The library handles filtering by this category.
showMoreClickedstring (CategoryKey)Emitted when the "Show more" link is clicked within a category group, indicating the category for which to show more items.
navigateToCategory{ term: string; category: string }Emitted when the "Show all count" link in a category header is clicked. Useful for navigating to a dedicated page for that category's results.
navigateToAll{ term: string }Emitted when the bottom "Show all results" button is clicked. Useful for navigating to a global search results page.
searchClearedvoidEmitted when the search input is cleared (e.g., via the clear button or by clicking outside if configured).
searchTermChangedstringEmitted immediately whenever the search input value changes (no debounce).
dropdownVisibilityChangedbooleanEmitted when the dropdown's visibility state changes (true for open, false for closed).
searchForTermClickedstringEmitted when the "Search for {term}" line is clicked (if showSearchForTermLine is true). Provides the current search term.
enterKeyPressedstringEmitted when the Enter key is pressed while the search input is focused. Provides the current search term.

Customization

Custom Templates

You can extensively customize the look and feel of the component using ng-template. Pass your TemplateRef to the corresponding @Input.

Available Template Contexts:

  • resultItemTemplate:
    • $implicit: T (The data item for the current result)
    • term: string (The current search term)
  • recentItemTemplate:
    • $implicit: string (The recent search term string)
  • chicletTemplate: (For the entire chiclet button)
    • $implicit: { category: string, count: number, isActive: boolean, type: 'initial' | 'results' }
  • chicletContentTemplate: (For content inside the default chiclet button)
    • $implicit: { category: string, count: number, isActive: boolean, type: 'initial' | 'results' }
  • categoryHeaderTemplate: (For the entire category header row)
    • $implicit: { category: string, count: number }
    • actions?: TemplateRef<any> (A template containing the default "Show more" / "Show all" links for that header, which you can choose to render)
  • categoryHeaderContentTemplate: (For content inside the default category header's title area)
    • $implicit: { category: string, count: number }
  • noResultsTemplate:
    • $implicit: string (The current search term)
    • isTermTooShort?: boolean (True if the current term is shorter than minSearchLength)
    • minSearchLengthVal?: number (The value of the minSearchLength input)
  • inputPrefixTemplate / inputSuffixTemplate / loadingTemplate:
    • No specific context variables are passed by default.

Example: Custom Result Item

<ng-template #customResult let-item let-term="term">
  <div class="my-custom-item">
    <img [src]="item.imageUrl" alt="" class="my-item-icon">
    <div>
      <strong [innerHTML]="item.productName | multiWordHighlight: term"></strong>
      <small>{{ item.sku }}</small>
    </div>
  </div>
</ng-template>

<ncs-category-search [resultItemTemplate]="customResult" ...></ncs-category-search>

Included Pipes

The library exports two pipes for text highlighting, which you can use in your custom templates:

  • HighlightPipe:
    • Name: highlight
    • Usage: [innerHTML]="textToHighlight | highlight: searchTerm"
    • Description: Highlights a single search term within a string by wrapping matches in <b> tags. Returns a string.
  • MultiWordHighlightPipe:

    • Name: multiWordHighlight
    • Usage: [innerHTML]="textToHighlight | multiWordHighlight: searchTerm"
    • Description: Highlights multiple words from the search term (space-separated) within a string by wrapping matches in <b> tags. Returns SafeHtml (requires DomSanitizer).

    To use these, ensure MultiWordHighlightPipe (if used) and CategorySearchComponent are in the imports array of your standalone component or module.

Styling & Theming

The component is styled with CSS Custom Properties (variables) for easy theming. Override these in your global stylesheet (e.g., styles.css) or a more specific parent selector.

Key CSS Custom Properties:

  • --ncs-primary-color: Primary theme color (e.g., for focus, active states).
  • --ncs-focus-glow-color: Color for the focus glow effect on the input.
  • --ncs-border-color: Default border color for input, dropdown.
  • --ncs-dropdown-bg: Background color of the dropdown.
  • --ncs-hover-bg: Background color for hover states on items.
  • --ncs-chiclet-initial-bg: Background for initial/disabled chiclets.
  • --ncs-chiclet-search-bg: Background for non-active chiclets in search results.
  • --ncs-chiclet-active-bg: Background for active/selected chiclets.
  • --ncs-chiclet-search-hover-bg: Hover background for non-active search chiclets.
  • --ncs-chiclet-active-hover-bg: Hover background for active chiclets.
  • --ncs-chiclet-text-color: Default text color for chiclets.
  • --ncs-chiclet-search-text-color: Text color for non-active search chiclets.
  • --ncs-chiclet-active-text-color: Text color for active chiclets.
  • --ncs-text-muted: Color for secondary or less important text.
  • --ncs-match-text-color: Text color for highlighted matches (used by pipes).
  • --ncs-separator-color: Color for separator lines within the dropdown.
  • --ncs-error-color: Color for error states or messages (not used by default).

Example: Customizing Borders To remove the border below the chiclet container:

<ncs-category-search [showChicletContainerBorder]="false" ...></ncs-category-search>

To remove other borders (e.g., around the main input, dropdown):

/* In your consuming app's styles.css */
:root { /* Or a more specific selector wrapping your search component */
  --ncs-border-color: transparent; /* Affects main input and dropdown borders */
  --ncs-separator-color: transparent; /* Affects internal separator lines */
}

/* To remove borders specifically from the "Search for {term}" line */
.ncs-search-for-term-line { /* Target the specific class from library CSS */
  border-top: none !important;
  border-bottom: none !important;
}

Development & Contribution

We welcome contributions! To get started:

  1. Fork & Clone: Fork the repository and clone it locally.
  2. Setup: Navigate to the workspace root (ngx-category-search-workspace) and run npm install.
  3. Build Library: npm run build:lib or ng build ngx-category-search. This builds the library into the dist/ngx-category-search folder.
  4. Local Linking (for testing in another local project):
    • Navigate into the built library: cd dist/ngx-category-search
    • Link it globally: npm link
    • Navigate to your test application: cd path/to/your/test-app
    • Link the library: npm link @r-ko/ngx-category-search
  5. Serve Demo App (if available in workspace): If a demo application exists within the workspace (e.g., under projects/demo-app), you can serve it using ng serve demo-app to test changes interactively after rebuilding the library.
  6. Develop: Make your changes in the projects/ngx-category-search/src/lib directory. Rebuild the library (npm run build:lib) to see changes reflected in linked projects or the demo app.
  7. Test: Ensure any relevant unit tests pass or are updated/added.
  8. Pull Request: Submit a PR detailing your changes, adhering to the Angular Style Guide.

License

MIT © 2025 Ram Kotagiri (rnrkotagiri@gmail.com)