npm package: @searchstax-inc/searchstudio-ux-angular
Description: Library to build searchstudio search page
Latest version: 4.1.72

# searchstudio-ux-angular

Library to build searchstudio search page

## changelog
changelog is documented at CHANGELOG.md

## Installation
npm install following package
`npm install --save @searchstax-inc/searchstudio-ux-angular`

## Usage

After importing SearchstaxWrapper component needs to wrap all other components.

Make sure your module imports SearchstudioUxAngularModule:

imports: [
    SearchstudioUxAngularModule
  ],

```

```

## Initialization
Initialization object needs to be of type: [ISearchstaxConfig](https://github.com/searchstax/searchstudio-ux-js/blob/main/src/interfaces/connector.interface.ts#L5)

Initialization example
```
Add following code to your ts file and fill config

 config = {
    searchURL: '',
    suggesterURL: '',
    trackApiKey: '',
    searchAuth: '',
    authType: 'basic',
    relatedSearchesAPIKey: '',
    relatedSearchesURL: '',
    analyticsBaseUrl: 'https://analytics-us-east.searchstax.com'
  }

  beforeSearch(props: ISearchObject) {
    // gets searchProps, if passed along further search will execute, if null then event gets canceled
    // props can be modified and passed along
    const propsCopy = { ...props }
    // propsCopy.term = propsCopy.term;
    return propsCopy
  }
  afterSearch(results: ISearchstaxParsedResult[]) {
    const copy = [...results]
    // copy.splice(0, 1);
    return copy
  }

  afterAutosuggest(result: ISearchstaxSuggestResponse) {
    const copy = { ...result }
    return copy
  }

  beforeAutosuggest(props: ISearchstaxSuggestProps) {
    // gets suggestProps, if passed along further autosuggest will execute, if null then event gets canceled
    // props can be modified and passed along
    const propsCopy = { ...props }
    // propsCopy.term = propsCopy.term + '222';
    return propsCopy
  }

  afterLinkClick(results: ISearchstaxParsedResult): ISearchstaxParsedResult {
    // gets result that was clicked, if passed along further functions will execute, if null then event gets canceled
    const copy = { ...results }

    return copy
  }

  initialized(searchstax: Searchstax) {
    console.log('searchstax', searchstax);
  }
```

## Initial layout
Our base theme is designed with this layout in mind but it is optional as all widgets have id parameters and can be attached to any element.
```
<div>
  <app-searchstax-wrapper
    [searchURL]="config.searchURL"
    [suggesterURL]="config.suggesterURL"
    [trackApiKey]="config.trackApiKey"
    [searchAuth]="config.searchAuth"
    [beforeSearch]="beforeSearch"
    [afterSearch]="afterSearch"
    (initialized)="initialized($event)"
  >
    <div class="searchstax-page-layout-container">
      <app-searchstax-input
        [afterAutosuggest]="afterAutosuggest"
        [beforeAutosuggest]="beforeAutosuggest"
        [suggestAfterMinChars]="3"
      ></app-searchstax-input>
      <div class="search-details-container">
        <app-searchstax-search-feedback></app-searchstax-search-feedback>
        <app-searchstax-search-sorting></app-searchstax-search-sorting>
      </div>
      <div class="searchstax-page-layout-facet-result-container">
        <div class="searchstax-page-layout-facet-container">
          <app-searchstax-search-facets
            [facetingType]="'showUnavailable'"
            [itemsPerPageDesktop]="2"
            [itemsPerPageMobile]="99"
          ></app-searchstax-search-facets>
        </div>
        <div class="searchstax-page-layout-result-container">
          <app-searchstax-external-promotions></app-searchstax-external-promotions>
          <app-searchstax-results
            [afterLinkClick]="afterLinkClick"
          ></app-searchstax-results>
          <app-searchstax-related-searches
            [relatedSearchesURL]="config.relatedSearchesURL"
            [relatedSearchesAPIKey]="config.relatedSearchesAPIKey"
          ></app-searchstax-related-searches>

          <app-searchstax-search-pagination></app-searchstax-search-pagination>
        </div>
      </div>
    </div>
  </app-searchstax-wrapper>
</div>

```

### widgets
Following widgets are available:

[Answer Widget](#answer-widget)

[Input Widget](#input-widget)

[Location Widget](#location-widget)

[Result Widget](#result-widget)

[Facets Widget](#facets-widget)

[Pagination Widget](#pagination-widget)

[SearchFeedback Widget](#searchfeedback-widget)

[RelatedSearches Widget](#relatedsearches-widget)

[ExternalPromotions Widget](#externalpromotions-widget)

[sorting Widget](#sorting-widget)

### Answer Widget ###
SearchStax Site Search solution offers Angular widgets to assist in building your custom search page.

The AppSearchstaxAnswer component for Angular provides an AI answer widget for your searches.

**Usage**

```
<app-searchstax-answer [showMoreAfterWordCount]="100"></app-searchstax-answer>
```

**Props**

- showMoreAfterWordCount - number(default 100) determining after how many words UI will show “Show More” view.
- feedbackWidget – an optional object that configures thumbs-up and thumbs-down feedback functionality.

Example of feedbackWidget config:
```
const feedbackConfig = {
    renderFeedbackWidget: true,
    emailOverride: searchstaxEmailOverride,
    thumbsUpValue: 10,
    thumbsDownValue: 0
  }
```


**searchAnswerTemplate**

The templates prop allows customizing the answer UI.

It receives the following props:

- answerData – Data object of type: ISearchstaxAnswerData
- showMore - Handler for exiting the show more view

**Example**

```
<app-searchstax-answer [showMoreAfterWordCount]="100" [templateOverride]="answerTemplate" [feedbackwidget]="feedbackConfig">
<ng-template
      #answerTemplate
      let-answerData="answerData"
      let-showMore="showMore"
      let-context="context"
    >
      <div class="searchstax-answer-wrap">
        <div class="searchstax-answer-icon"></div>
        <div>
          <div
            [class]="{
              'searchstax-answer-container': true,
              'searchstax-answer-show-more': answerData.showMoreButtonVisible
            }"
          >
            <div class="searchstax-answer-title">Smart Answers</div>
            <div
              class="searchstax-answer-error"
              *ngIf="answerData.shouldShowAnswerError"
              [innerHTML]="answerData.answerErrorMessage"
            ></div>
            <div
              class="searchstax-answer-description"
              [innerHTML]="answerData.fullAnswerFormatted"
            ></div>
            <div
              *ngIf="answerData.answerLoading"
              class="searchstax-answer-loading"
            ></div>
          </div>
          <div
            *ngIf="answerData.showMoreButtonVisible"
            class="searchstax-answer-load-more-button-container"
          >
            <button
              class="searchstax-answer-load-more-button"
              (click)="showMore($event, context)"
            >
              Show More
            </button>
          </div>
        </div>
        <div class="searchstax-answer-footer">
          <div id="feedbackWidgetContainer"></div>
          <div class="searchstax-lightweight-widget-separator-inline"></div>
          <p class="searchstax-disclaimer">Generative AI is Experimental</p>
        </div>
      </div>
    </ng-template>
      </app-searchstax-answer>
```

### Input Widget ###

The SearchStax Site Search solution offers an Angular input widget to assist with your custom search page.

The SearchstaxInputWidget component for Angular provides a search input with autosuggest/autocomplete functionality.
**Usage**
```
<app-searchstax-input
        [afterAutosuggest]="afterAutosuggest"
        [beforeAutosuggest]="beforeAutosuggest"
        [suggestAfterMinChars]="3"
      ></app-searchstax-input>
```

**Props**

- suggestAfterMinChars - default 3. Number of characters needed for autosuggest to start triggering
- beforeAutosuggest - callback function that gets called before firing autosuggest. autosuggestProps are being passed as a property and can be modified, if passed along further search will execute with modified properties, if null is returned then event gets canceled and search never fires.
- afterAutosuggest - callback function that gets called after autosuggest has values but before rendering. It needs to return same type of data but it can be modified.
- templateOverride - template override. look at examples below


**inputWidgetTemplate**

The templateOverride prop allows customizing the input UI.

It receives the following props:

- suggestions – Array of autosuggestion results
- onMouseLeave - Handler for mouse leave event
- onMouseOver - Handler for mouse over event
- onMouseClick - Handler for mouse click event

**example**
```
<app-searchstax-input
        [afterAutosuggest]="afterAutosuggest"
        [beforeAutosuggest]="beforeAutosuggest"
        [suggestAfterMinChars]="3"
        [templateOverride]="inputTemplate"
      >
     <ng-template
      #inputTemplate
      let-suggestions="suggestions"
      let-onMouseLeave="onMouseLeave"
      let-onMouseOver="onMouseOver"
      let-onMouseClick="onMouseClick"
      let-context="context"
      let-locationWidgetConfig="locationWidgetConfig"
    >
      <div class="searchstax-search-input-wrapper">
        <input
          type="text"
          id="searchstax-search-input"
          class="searchstax-search-input"
          placeholder="SEARCH FOR..."
          aria-label="search"
        />
        <div
          class="searchstax-autosuggest-container"
          [ngClass]="{ hidden: suggestions.length === 0 }"
          (mouseleave)="onMouseLeave(context)"
        >
          <div
            class="searchstax-autosuggest-item"
            *ngFor="let suggestion of suggestions"
          >
            <div
              class="searchstax-autosuggest-item-term-container"
              [innerHtml]="suggestion.term"
              (mouseover)="onMouseOver(suggestion, context)"
              tabindex="0"
              (click)="onMouseClick($event, context)"
            ></div>
          </div>
        </div>
      </div>
      <app-searchstax-search-location
        [locationDecode]="locationWidgetConfig.locationDecode"
        [locationDecodeCoordinatesToAddress]="
          locationWidgetConfig.locationDecodeCoordinatesToAddress
        "
        [locationSearchEnabled]="locationWidgetConfig.locationSearchEnabled"
        [locationValuesOverride]="locationWidgetConfig.locationValuesOverride"
      >
      </app-searchstax-search-location>
      <button
        class="searchstax-spinner-icon"
        id="searchstax-search-input-action-button"
        role="button"
        aria-label="search"
      ></button>
    </ng-template>
      </app-searchstax-input>
```
### Location Widget ###

SearchStax Site Search solution offers a Angular search-location widget to assist with your custom search page.

The SearchstaxLocationWidget provides a location search input with location-based search functionality.

**Usage**

```
const config = {
  appId: "APP_ID",
  relatedSearchesAPIKey: "KEY"
}

const locationWidgetConfig = {
    locationDecode: (term: string): Promise<ISearchstaxLocation> => {
      return new Promise((resolve) => {
        // make a request to google geocoding API to retrieve lat, lon and address

        const geocodingAPIKey = "GEOCODING_API_KEY";
        const geocodingURL = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(
          term
        )}&key=${geocodingAPIKey}`;
        fetch(geocodingURL)
          .then((response) => response.json())
          .then((data) => {
            if (data.status === "OK" && data.results.length > 0) {
              const result = data.results[0];
              const location = {
                lat: result.geometry.location.lat,
                lon: result.geometry.location.lng,
                address: result.formatted_address,
              };
              resolve(location);
            } else {
              resolve({
                address: undefined,
                lat: undefined,
                lon: undefined,
                error: true,
              });
            }
          })
          .catch(() => {
            resolve({
              address: undefined,
              lat: undefined,
              lon: undefined,
              error: true,
            });
          });
      });
    },
    locationDecodeCoordinatesToAddress: (lat: string, lon: string): Promise<ISearchstaxLocation> => {
      return new Promise((resolve) => {
        fetch(
          `https://geocoding-staging.searchstax.co/reverse?location=${lat},${lon}&components=country:US&app_id=${config.appId}`,
          {
            method: "GET",
            headers: {
              Authorization: `Token ${config.relatedSearchesAPIKey}`,
            },
          }
        )
          .then((response) => response.json())
          .then((data) => {
            if (data.status === "OK" && data.results.length > 0) {
              const result = data.results[0];
              resolve({
                address: result.formatted_address,
                lat: lat,
                lon: lon,
                error: false,
              });
            } else {
              resolve({
                address: undefined,
                lat: lat,
                lon: lon,
                error: true,
              });
            }
          })
          .catch(() => {
            resolve({
              address: undefined,
              lat: lat,
              lon: lon,
              error: true,
            });
          });
      });
    },
    locationSearchEnabled: false,
    locationValuesOverride: {
      locationDistanceEnabled: true,
      filterValues: ["any", "1000"],
      filterUnit: "miles",
      locationFilterDefaultValue: "any"
    },
  }
 <app-searchstax-search-location
            [locationDecode]="locationWidgetConfig.locationDecodeFunction"
            [locationDecodeCoordinatesToAddress]="locationWidgetConfig.locationDecodeCoordinatesToAddress"
            [locationValuesOverride]="locationWidgetConfig.locationValuesOverride"
            [locationSearchEnabled]="locationWidgetConfig.locationSearchEnabled"
          >
 </app-searchstax-search-location>
```

**Props**

- locationDecode - callback function to override location decoding
- locationDecodeCoordinatesToAddress - callback function to override location decoding
- locationValuesOverride - location widget values
- locationSearchEnabled - boolean stating if locationSearch is locationSearchEnabled
- templateOverride - template override. look at examples below


**Template Override**

The searchLocationTemplate prop allows customizing the location input UI.

It receives the following props:

- locationData –  data containing info on when to show certain elements
- inputValue – value of location input
- locationBlur - Handler for location input blur
- radiusChange - Handler for location radius change
- selectValue  - Value of radius select
- inputChange - location input change handler
- locationError - boolean stating if location has error
- getCurrentLocation - Handler for getting current location from browser


**Example**

```
const config = {
  appId: "APP_ID",
  relatedSearchesAPIKey: "KEY"
}

const locationWidgetConfig = {
    locationDecode: (term: string): Promise<ISearchstaxLocation> => {
      return new Promise((resolve) => {
        // make a request to google geocoding API to retrieve lat, lon and address

        const geocodingAPIKey = "GEOCODING_API_KEY";
        const geocodingURL = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(
          term
        )}&key=${geocodingAPIKey}`;
        fetch(geocodingURL)
          .then((response) => response.json())
          .then((data) => {
            if (data.status === "OK" && data.results.length > 0) {
              const result = data.results[0];
              const location = {
                lat: result.geometry.location.lat,
                lon: result.geometry.location.lng,
                address: result.formatted_address,
              };
              resolve(location);
            } else {
              resolve({
                address: undefined,
                lat: undefined,
                lon: undefined,
                error: true,
              });
            }
          })
          .catch(() => {
            resolve({
              address: undefined,
              lat: undefined,
              lon: undefined,
              error: true,
            });
          });
      });
    },
    locationDecodeCoordinatesToAddress: (lat: string, lon: string): Promise<ISearchstaxLocation> => {
      return new Promise((resolve) => {
        fetch(
          `https://geocoding-staging.searchstax.co/reverse?location=${lat},${lon}&components=country:US&app_id=${config.appId}`,
          {
            method: "GET",
            headers: {
              Authorization: `Token ${config.relatedSearchesAPIKey}`,
            },
          }
        )
          .then((response) => response.json())
          .then((data) => {
            if (data.status === "OK" && data.results.length > 0) {
              const result = data.results[0];
              resolve({
                address: result.formatted_address,
                lat: lat,
                lon: lon,
                error: false,
              });
            } else {
              resolve({
                address: undefined,
                lat: lat,
                lon: lon,
                error: true,
              });
            }
          })
          .catch(() => {
            resolve({
              address: undefined,
              lat: lat,
              lon: lon,
              error: true,
            });
          });
      });
    },
    locationSearchEnabled: false,
    locationValuesOverride: {
      locationDistanceEnabled: true,
      filterValues: ["any", "1000"],
      filterUnit: "miles",
      locationFilterDefaultValue: "any"
    },
  }
<app-searchstax-search-location
            [locationDecode]="locationWidgetConfig.locationDecode"
            [templateOverride]="locationTemplate"
            [locationValuesOverride]="locationWidgetConfig.locationValuesOverride"
            [locationSearchEnabled]="locationWidgetConfig.locationSearchEnabled"
            [locationDecodeCoordinatesToAddress]="locationWidgetConfig.locationDecodeCoordinatesToAddress"
            [templateOverride]="locationTemplate"
          >
              <ng-template
      #locationTemplate
      let-context="context"
      let-locationData="locationData"
      let-inputValue="inputValue"
      let-selectValue="selectValue"
      let-radiusChange="radiusChange"
      let-inputChange="inputChange"
      let-locationBlur="locationBlur"
      let-locationError="locationError"
      let-getCurrentLocation="getCurrentLocation"
    >
      <div
        class="searchstax-location-input-container"
        data-test-id="searchstax-location-input-container"
        *ngIf="locationData"
      >
        <div class="searchstax-location-input-wrapper">
          <span class="searchstax-location-input-label">NEAR</span>
          <div class="searchstax-location-input-wrapper-inner">
            <input
              type="text"
              id="searchstax-location-input"
              class="searchstax-location-input"
              [ngClass]="{
                'searchstax-input-location-error': locationError
              }"
              placeholder="Zip, Postal Code or City..."
              aria-label="Search Location Input"
              data-test-id="searchstax-location-input"
              [value]="inputValue"
              (change)="inputChange($event, context)"
              (blur)="locationBlur(context)"
            />
            <button
              (click)="getCurrentLocation(context)"
              class="searchstax-get-current-location-button"
            >
              Use my current location
            </button>
          </div>
          <span
            *ngIf="locationData.shouldShowLocationDistanceDropdown"
            class="searchstax-location-input-label"
            >WITHIN</span
          >
          <select
            *ngIf="locationData.shouldShowLocationDistanceDropdown"
            id="searchstax-location-radius-select"
            class="searchstax-location-radius-select"
            aria-label="Search Location Radius Select"
            data-test-id="searchstax-location-radius-select"
            (change)="radiusChange($event, context)"
            [(ngModel)]="context.selectValue"
          >
            <option
              *ngFor="let value of locationData.locationSearchDistanceValues"
              [value]="value.value"
            >
              {{ value.label }}
            </option>
          </select>
        </div>
      </div>
    </ng-template>

          </app-searchstax-search-location>
```

### Result Widget ###

The SearchStax Site Search solution offers an Angular results widget to assist with your custom search page.

The SearchstaxResultsWidget for Angular component displays the search results.

**Usage**

```
<app-searchstax-results
            [afterLinkClick]="afterLinkClick"
            [renderMethod]="'pagination'"
            [resultsPerPage]="10"
          ></app-searchstax-results>
```

**Props**

- renderMethod – either “pagination” or “infiniteScroll”.
- resultsPerPage – number of results on a page.
- afterLinkClick – Callback function invoked when a result link is clicked. Allows modifying the result object.
- noResultTemplate - template override for no results view
- resultsTemplate - template override for result item


**Result Template Override**

The resultsTemplate prop allows customizing the result UI.

It receives following props:
- searchResults - Array of result items
- resultClicked - Handler for result click events


**No Results Template Override**

The noResultTemplate prop allows customizing the result UI.

It receives following props:

- searchResults - Array of result items
- resultClicked - Handler for result click events
- searchTerm - Search input term
- metadata - Metadata object
- executeSearch - Handler for executing srearch
- store - Main store of app

**Example of default render method**
```
<app-searchstax-results
            [afterLinkClick]="afterLinkClick"
            [resultsTemplate]="resultsTemplate"
            [noResultsTemplate]="noResultsTemplate"
          >
     <ng-template
      #resultsTemplate
      let-searchResults="searchResults"
      let-resultClicked="resultClicked"
      let-isImage="isImage"
      let-context="context"
    >
      <div class="searchstax-search-results">
        <a
          *ngFor="let searchResult of searchResults"
          [href]="searchResult.url"
          [attr.data-searchstax-unique-result-id]="searchResult.uniqueId"
          (click)="resultClicked(searchResult, $event, context)"
          (keyup.enter)="resultClicked(searchResult, $event, context)"
          (keyup.space)="resultClicked(searchResult, $event, context)"
          [attr.aria-labelledby]="'title-' + searchResult.uniqueId"
          tabindex="0"
          class="searchstax-result-item-link searchstax-result-item-link-wrapping"
        >
          <div
            class="searchstax-search-result"
            [ngClass]="{ 'has-thumbnail': searchResult.thumbnail }"
          >
            <div
              *ngIf="searchResult.promoted"
              class="searchstax-search-result-promoted"
            ></div>
            <div
              *ngIf="searchResult.ribbon"
              [innerHTML]="searchResult.ribbon"
              class="searchstax-search-result-ribbon"
            ></div>
            <img
              alt=""
              *ngIf="searchResult.thumbnail"
              [src]="searchResult.thumbnail"
              class="searchstax-thumbnail"
            />
            <div class="searchstax-search-result-title-container">
              <span
                class="searchstax-search-result-title"
                [id]="'title-' + searchResult.uniqueId"
                [innerHTML]="searchResult.title"
              ></span>
            </div>
            <p
              tabindex="0"
              *ngIf="searchResult.paths"
              class="searchstax-search-result-common"
              [innerHTML]="searchResult.paths"
            ></p>
            <p
              tabindex="0"
              *ngIf="searchResult.description"
              [innerHTML]="searchResult.description"
              class="searchstax-search-result-description searchstax-search-result-common"
            ></p>
            <div *ngFor="let unmappedField of searchResult.unmappedFields">
              <div
                *ngIf="isImage(unmappedField)"
                class="searchstax-search-result-image-container"
              >
                <img
                  alt=""
                  [src]="unmappedField.value"
                  class="searchstax-result-image"
                />
              </div>
              <div *ngIf="!isImage(unmappedField)">
                <p
                  tabindex="0"
                  class="searchstax-search-result-common"
                  [innerHTML]="unmappedField.value"
                ></p>
              </div>
            </div>
          </div>
        </a>
      </div>
    </ng-template>
     <ng-template
      #noResultsTemplate
      let-metadata="metadata"
      let-searchTerm="searchTerm"
      let-executeSearch="executeSearch"
      let-context="context"
    >
      <div class="searchstax-no-results">
        Showing <strong>no results</strong> for
        <strong>"{{ searchTerm }}"</strong>
        <br />
        <span *ngIf="metadata?.spellingSuggestion"
          >&nbsp;Did you mean
          <a
            href="#"
            [attr.aria-label]="'did you mean: ' + metadata?.spellingSuggestion"
            class="searchstax-suggestion-term"
            (click)="
              executeSearch(metadata?.spellingSuggestion ?? '', $event, context)
            "
            >{{ metadata?.spellingSuggestion }}</a
          >?</span
        >
      </div>
      <ul>
        <li>
          Try searching for search related terms or topics. We offer a wide
          variety of content to help you get the information you need.
        </li>
      </ul>
    </ng-template>

          </app-searchstax-results>
```

**Example of infinite scroll and pagination render methods**
```
<app-searchstax-results
            [afterLinkClick]="afterLinkClick"
            [renderMethod]="'infiniteScroll'"
          ></app-searchstax-results>


```

### Pagination Widget ###

The SearchStax Site Search solution offers an Angular pagination widget to assist with you custom search page.

The SearchstaxPaginationWidget for Angular displays pagination controls for search results.

**Usage**

```
<app-searchstax-search-pagination></app-searchstax-search-pagination>
```

**Props**

- infiniteScrollTemplateOverride - template override for infinite scroll view pagination
- templateOverride - template override for standard view pagination

**Main Template Override**

Main template for the pagination controls.

It receives following props:

- paginationData – Pagination info object
- previousPage – Handler for previous page click
- nextPage - – Handler for next page click


**Infinite Scroll Template Override**

Main template for the pagination controls in infinite scroll mode.

It receives following props:
- isLastPage - boolean, true if its last page
- results - results.length can be used if there are results

**Example**

```
<app-searchstax-search-pagination [templateOverride]="paginationTemplate">
     <ng-template
      #paginationTemplate
      let-paginationData="paginationData"
      let-previousPage="previousPage"
      let-nextPage="nextPage"
      let-context="context"
    >
      <div class="searchstax-pagination-container">
        <div class="searchstax-pagination-content">
          <a
            class="searchstax-pagination-previous"
            [class]="paginationData.isFirstPage ? 'disabled' : ''"
            (click)="previousPage($event, context)"
            (keyup.enter)="previousPage($event, context)"
            (keyup.space)="previousPage($event, context)"
            tabindex="0"
            id="searchstax-pagination-previous"
          >
            &lt; Previous
          </a>
          <div class="searchstax-pagination-details">
            {{ paginationData?.startResultIndex }} -
            {{ paginationData?.endResultIndex }} of
            {{ paginationData?.totalResults }}
          </div>
          <a
            class="searchstax-pagination-next"
            [class]="paginationData.isLastPage ? 'disabled' : ''"
            (click)="nextPage($event, context)"
            (keyup.enter)="nextPage($event, context)"
            (keyup.space)="nextPage($event, context)"
            tabindex="0"
            id="searchstax-pagination-next"
            >Next ></a
          >
        </div>
      </div>
    </ng-template>
</app-searchstax-search-pagination>

// infinite scroll example

<app-searchstax-search-pagination [templateOverride]="paginationTemplate"
                                  [infiniteScrollTemplateOverride]="infiniteScrollTemplate">
                 <ng-template
      #infiniteScrollTemplate
      let-paginationData="paginationData"
      let-nextPage="nextPage"
      let-context="context"
    >
      <div class="searchstax-pagination-container">
        <div class="searchstax-pagination-content">
          <a
            class="searchstax-pagination-load-more"
            (click)="nextPage($event, context)"
            (keyup.enter)="nextPage($event, context)"
            (keyup.space)="nextPage($event, context)"
            tabindex="0"
          >
            Load More
          </a>
        </div>
      </div>
    </ng-template>
                 <ng-template
      #paginationTemplate
      let-paginationData="paginationData"
      let-previousPage="previousPage"
      let-nextPage="nextPage"
      let-context="context"
    >
      <div class="searchstax-pagination-container">
        <div class="searchstax-pagination-content">
          <a
            class="searchstax-pagination-previous"
            [class]="paginationData.isFirstPage ? 'disabled' : ''"
            (click)="previousPage($event, context)"
            (keyup.enter)="previousPage($event, context)"
            (keyup.space)="previousPage($event, context)"
            tabindex="0"
            id="searchstax-pagination-previous"
          >
            &lt; Previous
          </a>
          <div class="searchstax-pagination-details">
            {{ paginationData?.startResultIndex }} -
            {{ paginationData?.endResultIndex }} of
            {{ paginationData?.totalResults }}
          </div>
          <a
            class="searchstax-pagination-next"
            [class]="paginationData.isLastPage ? 'disabled' : ''"
            (click)="nextPage($event, context)"
            (keyup.enter)="nextPage($event, context)"
            (keyup.space)="nextPage($event, context)"
            tabindex="0"
            id="searchstax-pagination-next"
            >Next ></a
          >
        </div>
      </div>
    </ng-template>
          </app-searchstax-search-pagination>
```

### Facets Widget ###

The SearchStax Site Search solution offers the SearchstaxFacetsWidget for Angular to display the search facets.

**Facet Selection and Order**

Facet lists are configured and ordered on the Site Search [Faceting Tab](https://www.searchstax.com/docs/searchstudio/faceting-tab/).

**Usage**

```
<app-searchstax-search-facets
            [facetingType]="'showUnavailable'"
            [itemsPerPageDesktop]="2"
            [itemsPerPageMobile]="99"
          ></app-searchstax-search-facets>
```

**Props**

- facetingType: "and" | "or" | "showUnavailable" | "tabs"; // type that determines how facets will behave
- specificFacets?: string[]; // optional array of facet names that if provided will only render those facets
- itemsPerPageDesktop: number; // default expanded facets for desktop
- itemsPerPageMobile: number; // default expanded facets for mobile
- templateOverrideMobile - template override for mobile view
- templateOverrideDesktop - template override for desktop view
- beforeFacetsRender `(facets: IFacetData[]) => IFacetData[]` — Called with the current facets array before the widget renders. Return the same or a modified array to filter, reorder, or transform facets (e.g., hide certain facets or change their order).


**Main Template Desktop Override**

Main wrapper template for desktop facets display.

It receives following props:
- facetsTemplateDataDesktop -  Facets data object
- isNotDeactivated - Check if facet group is active
- toggleFacetGroup - Toggle facet group active state
- isChecked - Check if facet value is selected
- selectFacet - Handler for facet select
- showMoreLessDesktop - Show more/less facets handler
- facetContainers - Object of facet DOM containers
- updateRefDesktop - Handler for keeping checkbox references


**Main Template Mobile Override**

Main wrapper template for mobile facets display.

It receives following props:
- facetsTemplateDataMobile - Facets data object
- selectedFacetsCheckboxes - Selected facet values
- isNotDeactivated - Check if facet group is active
- toggleFacetGroup - Toggle facet group active state
- isChecked - Check if facet value is selected
- selectFacet - Handler for facet select
- showMoreLessDesktop - Show more/less facets handler
- facetContainers - Object of facet DOM containers
- openOverlay - Handler to open mobile overlay
- unselectFacet - Handler to unselect specific facet
- unselectAll - Handler to unselect all facets
- closeOverlay - Handler to close mobile overlay
- updateRefMobile - Handler for keeping mobile checkbox references


**Example**
```
<app-searchstax-search-facets
            [facetingType]="'showUnavailable'"
            [itemsPerPageDesktop]="2"
            [itemsPerPageMobile]="99"
            [templateOverrideDesktop]="templateOverrideDesktop"
            [templateOverrideMobile]="templateOverrideMobile"
          >
                 <ng-template
      #templateOverrideDesktop
      let-facetsTemplateDataDesktop="facetsTemplateDataDesktop"
      let-isNotDeactivated="isNotDeactivated"
      let-context="context"
      let-toggleFacetGroup="toggleFacetGroup"
      let-facetValues="facetValues"
      let-isChecked="isChecked"
      let-selectFacet="selectFacet"
      let-showMoreLessDesktop="showMoreLessDesktop"
    >
      <div class="searchstax-facets-container-desktop">
        <div
          class="searchstax-facet-container"
          *ngFor="let facet of facetsTemplateDataDesktop?.facets"
          [class]="{ active: isNotDeactivated(facet.name, context) }"
        >
          <div>
            <div
              class="searchstax-facet-title-container"
              (click)="toggleFacetGroup(facet.name, context)"
            >
              <div class="searchstax-facet-title">
                {{ facet.label }}
              </div>
              <div class="searchstax-facet-title-arrow active"></div>
            </div>
            <div class="searchstax-facet-values-container" aria-live="polite">
              <div
                *ngFor="let facetValue of facetValues(facet); index as key"
                class="searchstax-facet-value-container"
                [class]="{
                  'searchstax-facet-value-disabled': facetValue.disabled
                }"
                #ref
              >
                <div class="searchstax-facet-input">
                  <input
                    type="checkbox"
                    class="searchstax-facet-input-checkbox"
                    [checked]="isChecked(facetValue, context)"
                    [attr.aria-label]="
                      facetValue.value + ' ' + facetValue.count
                    "
                    [disabled]="facetValue.disabled"
                    (click)="
                      selectFacet(ref, $event, facetValue, true, context)
                    "
                  />
                </div>
                <div
                  class="searchstax-facet-value-label"
                  (click)="selectFacet(ref, $event, facetValue, false, context)"
                >
                  {{ facetValue.value }}
                </div>
                <div
                  class="searchstax-facet-value-count"
                  (click)="selectFacet(ref, $event, facetValue, false, context)"
                >
                  ({{ facetValue.count }})
                </div>
              </div>
              <div
                class="searchstax-facet-show-more-container"
                *ngIf="facet.hasMoreFacets"
              >
                <div
                  class="searchstax-facet-show-more-container"
                  (click)="showMoreLessDesktop($event, facet, context)"
                  (keyup.enter)="showMoreLessDesktop($event, facet, context)"
                  (keyup.space)="showMoreLessDesktop($event, facet, context)"
                  tabindex="0"
                  role="button"
                >
                  <div
                    *ngIf="facet.showingAllFacets"
                    class="searchstax-facet-show-less-button searchstax-facet-show-button"
                  >
                    less
                  </div>
                  <div
                    *ngIf="!facet.showingAllFacets"
                    class="searchstax-facet-show-more-button searchstax-facet-show-button"
                  >
                    more
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </ng-template>
                 <ng-template
      #templateOverrideMobile
      let-isNotDeactivated="isNotDeactivated"
      let-context="context"
      let-toggleFacetGroup="toggleFacetGroup"
      let-facetValues="facetValues"
      let-isChecked="isChecked"
      let-selectFacet="selectFacet"
      let-openOverlay="openOverlay"
      let-selectedFacetsCheckboxes="selectedFacetsCheckboxes"
      let-unselectFacet="unselectFacet"
      let-unselectAll="unselectAll"
      let-facetMobileData="facetMobileData"
      let-facetsTemplateDataMobile="facetsTemplateDataMobile"
      let-closeOverlay="closeOverlay"
      let-showMoreLessDesktop="showMoreLessDesktop"
      let-showMoreLessMobile="showMoreLessMobile"
    >
      <div class="searchstax-facets-container-mobile">
        <div class="searchstax-facets-pills-container">
          <div
            class="searchstax-facets-pill searchstax-facets-pill-filter-by"
            (click)="openOverlay(context)"
          >
            <div class="searchstax-facets-pill-label">Filter By</div>
          </div>
          <div class="searchstax-facets-pills-selected">
            <div
              class="searchstax-facets-pill searchstax-facets-pill-facets"
              *ngFor="let facet of selectedFacetsCheckboxes"
              (click)="unselectFacet(facet, context)"
            >
              <div class="searchstax-facets-pill-label">
                {{ facet.value }} ({{ facet.count }})
              </div>
              <div class="searchstax-facets-pill-icon-close"></div>
            </div>
          </div>
          <div
            class="searchstax-facets-pill searchstax-clear-filters searchstax-facets-pill-clear-all"
            *ngIf="selectedFacetsCheckboxes.length"
            (click)="unselectAll(context)"
          >
            <div class="searchstax-facets-pill-label">Clear Filters</div>
          </div>
        </div>
        <div
          class="searchstax-facets-mobile-overlay"
          [class]="{
            'searchstax-show': facetMobileData(facetsTemplateDataMobile)
              ?.overlayOpened
          }"
        >
          <div class="searchstax-facets-mobile-overlay-header">
            <div class="searchstax-facets-mobile-overlay-header-title">
              Filter By
            </div>
            <div
              class="searchstax-search-close"
              (click)="closeOverlay(context)"
            ></div>
          </div>
          <div class="searchstax-facets-container-mobile">
            <div
              *ngFor="let facet of facetsTemplateDataMobile?.facets"
              class="searchstax-facet-container"
              [class]="{
                active: isNotDeactivated(facet.name, context)
              }"
            >
              <div>
                <div
                  class="searchstax-facet-title-container"
                  (click)="toggleFacetGroup(facet.name, context)"
                >
                  <div class="searchstax-facet-title">
                    {{ facet.label }}
                  </div>
                  <div class="searchstax-facet-title-arrow active"></div>
                </div>
                <div
                  class="searchstax-facet-values-container"
                  aria-live="polite"
                >
                  <div
                    *ngFor="let facetValue of facetValues(facet); index as key"
                    class="searchstax-facet-value-container"
                    [class]="{
                      'searchstax-facet-value-disabled': facetValue.disabled
                    }"
                    #ref
                  >
                    <div class="searchstax-facet-input">
                      <input
                        type="checkbox"
                        class="searchstax-facet-input-checkbox"
                        [checked]="isChecked(facetValue, context)"
                        [attr.aria-label]="
                          facetValue.value + ' ' + facetValue.count
                        "
                        [disabled]="facetValue.disabled"
                        (click)="
                          selectFacet(ref, $event, facetValue, true, context)
                        "
                      />
                    </div>
                    <div
                      class="searchstax-facet-value-label"
                      (click)="
                        selectFacet(ref, $event, facetValue, false, context)
                      "
                    >
                      {{ facetValue.value }}
                    </div>
                    <div
                      class="searchstax-facet-value-count"
                      (click)="
                        selectFacet(ref, $event, facetValue, false, context)
                      "
                    >
                      ({{ facetValue.count }})
                    </div>
                  </div>
                  <div
                    class="searchstax-facet-show-more-container"
                    *ngIf="facet.hasMoreFacets"
                  >
                    <div
                      class="searchstax-facet-show-more-container"
                      (click)="showMoreLessMobile($event, facet, context)"
                      (keyup.enter)="showMoreLessMobile($event, facet, context)"
                      (keyup.space)="showMoreLessMobile($event, facet, context)"
                      tabindex="0"
                    >
                      <div
                        *ngIf="facet.showingAllFacets"
                        class="searchstax-facet-show-less-button searchstax-facet-show-button"
                      >
                        less
                      </div>
                      <div
                        *ngIf="!facet.showingAllFacets"
                        class="searchstax-facet-show-more-button searchstax-facet-show-button"
                      >
                        more
                      </div>
                    </div>
                  </div>
                </div>
              </div>
            </div>
          </div>
          <button
            class="searchstax-facets-mobile-overlay-done"
            (click)="closeOverlay(context)"
          >
            Done
          </button>
        </div>
      </div>
    </ng-template>
          </app-searchstax-search-facets>
```

### SearchFeedback Widget ###

The SearchStax Site Search solution includes an Angular feedback widget for your custom search page.

The SearchstaxFeedbackWidget for Angular displays search feedback and stats.

**Usage**

```
<app-searchstax-search-feedback></app-searchstax-search-feedback>
```

**Props**

- templateOverride - template override for search feedback

**Main Template Override**

Main template for the search feedback message.

It receives following props:
- searchFeedbackData - Feedback data object
- onOriginalQueryClick - Handler for clicking on original query suggestion

**Example**
```
<app-searchstax-search-feedback [templateOverride]="feedbackTemplate">
                          <ng-template
      #feedbackTemplate
      let-searchFeedbackData="searchFeedbackData"
      let-onOriginalQueryClick="onOriginalQueryClick"
      let-context="context"
    >
      <h2 class="searchstax-feedback-container">
        Showing
        <b
          >{{ searchFeedbackData!.startResultIndex }} -
          {{ searchFeedbackData!.endResultIndex }}</b
        >
        of <b>{{ searchFeedbackData!.totalResults }}</b> results
        <span *ngIf="searchFeedbackData.searchTerm"
          >for "<b>{{ searchFeedbackData!.searchTerm }}</b
          >"
        </span>
        <div class="searchstax-feedback-container-suggested">
          <div *ngIf="searchFeedbackData.autoCorrectedQuery">
            Search instead for
            <a
              href="#"
              (click)="onOriginalQueryClick($event, context)"
              class="searchstax-feedback-original-query"
              [attr.aria-label]="
                'Search instead for: ' + searchFeedbackData!.originalQuery
              "
              >{{ searchFeedbackData!.originalQuery }}</a
            >
          </div>
        </div>
      </h2>
    </ng-template>
</app-searchstax-search-feedback>
```

### RelatedSearches widget ###

The SearchStax Site Search solution offers an Angular related-searches widget for your custom search page.

The SearchstaxRelatedSearchesWidget displays related searches.

**Usage**
```
 <app-searchstax-related-searches
            [relatedSearchesURL]="config.relatedSearchesURL"
            [relatedSearchesAPIKey]="config.relatedSearchesAPIKey"
          ></app-searchstax-related-searches>
```

**Props**

- relatedSearchesURL: API URL for fetching related searches
- relatedSearchesAPIKey?: API key for related searches API
- templateOverride - template override for related search

**Main Template Override**

Main template for related searches.

It receives following props:

- relatedData - Related searches data object
- executeSearch - Handler to run new search from related term

**Example**

```
<app-searchstax-related-searches
            [relatedSearchesURL]="config.relatedSearchesURL"
            [relatedSearchesAPIKey]="config.relatedSearchesAPIKey"
            [templateOverride]="relatedTemplate"
          >
                 <ng-template
      #relatedTemplate
      let-rand="rand"
      let-relatedData="relatedData"
      let-executeSearch="executeSearch"
      let-context="context"
    >
      <div
        class="searchstax-related-searches-container"
        [id]="'searchstax-related-searches-container' + rand"
      >
        Related searches: <span id="searchstax-related-searches"></span>
        <span
          class="searchstax-related-search"
          *ngIf="relatedData.relatedSearches"
        >
          <span
            *ngFor="let related of relatedData.relatedSearches"
            (click)="executeSearch(related, context)"
            (keyup.enter)="executeSearch(related, context)"
            (keyup.space)="executeSearch(related, context)"
            tabindex="0"
            class="searchstax-related-search searchstax-related-search-item"
            [attr.aria-label]="'Related search: ' + related.related_search"
          >
            {{ related.related_search }}<span *ngIf="!related.last">,</span>
          </span>
        </span>
      </div>
    </ng-template>
 </app-searchstax-related-searches>
```

### ExternalPromotions widget ###

The SearchStax Site Search solution offers an Angular external-promotions widget for your custom search page.

The SearchstaxExternalPromotionsWidget displays external promotions fetched from the API.

**Usage**

```
<app-searchstax-external-promotions></app-searchstax-external-promotions>
```

**Props**

- templateOverride - template override for external promotion

**Main Template Override**

Main template for external promotions.

It receives following props:

- externalPromotionsData – External promotions data object
- trackClick – Handler for tracking link clicks

**Example**
```
<app-searchstax-external-promotions [templateOverride]="externalPromotionsTemplate">
                            <ng-template
      #externalPromotionsTemplate
      let-context="context"
      let-externalPromotionsData="externalPromotionsData"
      let-trackClick="trackClick"
    >
      <div
        class="searchstax-external-promotions-container"
        id="searchstax-external-promotions-container"
      >
        <div
          class="searchstax-external-promotion searchstax-search-result"
          *ngFor="
            let externalPromotion of externalPromotionsData.externalPromotions
          "
        >
          <div class="icon-elevated"></div>
          <a
            *ngIf="externalPromotion.url"
            [href]="externalPromotion.url"
            (click)="trackClick(externalPromotion, $event, context)"
            class="searchstax-result-item-link"
          ></a>
          <div class="searchstax-search-result-title-container">
            <span class="searchstax-search-result-title">{{
              externalPromotion.name
            }}</span>
          </div>
          <p
            *ngIf="externalPromotion.description"
            class="searchstax-search-result-description searchstax-search-result-common"
          >
            {{ externalPromotion.description }}
          </p>
          <p
            *ngIf="externalPromotion.url"
            class="searchstax-search-result-description searchstax-search-result-common"
          >
            {{ externalPromotion.url }}
          </p>
        </div>
      </div>
    </ng-template>
          </app-searchstax-external-promotions>
```


### Sorting Widget ###

The SearchStax Site Search solution provides an Angular sorting widget for your custom search page.

The SearchstaxSortingWidget displays sorting options for search results.

**Usage**

```
<app-searchstax-search-sorting></app-searchstax-search-sorting>
```

**Props**

- templateOverride - template override for sorting widget

**Main Template Override**

Main template for sorting widget.

It receives following props:

- sortingData – Sorting data object
- orderChange – Handler for sorting change
- selectedSorting – Current selected sorting
- templateOverride - template override for sorting widget

**Example**

```
<app-searchstax-search-sorting [templateOverride]="sortingTemplateOverride">
                           <ng-template
      #sortingTemplateOverride
      let-selectedSorting="selectedSorting"
      let-orderChange="orderChange"
      let-context="context"
    >
      <div class="searchstax-sorting-container">
        <label
          class="searchstax-sorting-label"
          for="searchstax-search-order-select"
          >Sort By</label
        >
        <select
          id="searchstax-search-order-select"
          class="searchstax-search-order-select"
          [(ngModel)]="context.selectedSorting"
          (change)="orderChange($event, context)"
        >
          <option
            *ngFor="let sortOption of context.sortingData.sortOptions"
            [value]="sortOption.key"
          >
            {{ sortOption.value }}
          </option>
        </select>
      </div>
    </ng-template>
        </app-searchstax-search-sorting>
```


## Template overrides
Templates use vue templating.

## STYLING

scss styles can be imported from searchstudio-ux-js
```
 @import './../node_modules/@searchstax-inc/searchstudio-ux-js/dist/styles/scss/mainTheme.scss';
```
css can be taken from

```
./../node_modules/@searchstax-inc/searchstudio-ux-js/dist/styles/mainTheme.css
```