@searchstax-inc/searchstudio-ux-vue
v4.2.19
Published
Library to build Site Search page
Keywords
Readme
sitesearch-ux-vue
Library to build Site Search page
changelog
changelog is documented at CHANGELOG.md
Installation
npm install following package
npm install --save @searchstax-inc/searchstudio-ux-vue
Add following code to <head>
<script type="text/javascript">
var _msq = _msq || []; //declare object
var analyticsBaseUrl = 'https://analytics-us-east.searchstax.co';
(function () {
var ms = document.createElement('script');
ms.type = 'text/javascript';
ms.src = 'https://static.searchstax.co/studio-js/v3/js/studio-analytics.js';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(ms, s);
})();
</script>Usage
After importing SearchstaxWrapper component needs to wrap all other components:
<SearchstaxWrapper
:language="sampleConfig.language"
:model="sampleConfig.model"
:searchURL="sampleConfig.searchURL"
:suggesterURL="sampleConfig.suggesterURL"
:trackApiKey="sampleConfig.trackApiKey"
:searchAuth="sampleConfig.searchAuth"
:authType="sampleConfig.authType"
:router="sampleConfig.router"
:beforeSearch="sampleConfig.hooks.beforeSearch" // callback function to intercept search object before search is fired
:afterSearch="sampleConfig.hooks.afterSearch" // callback function to handle results after search is fired
>
// other components will go there
</SearchstaxWrapper>Initialization
Initialization object needs to be of type: ISearchstaxConfig
Initialization example
sampleConfig = {
language: "en",
model: "Main Profile",
searchURL: "",
suggesterURL: "",
trackApiKey: "",
searchAuth: "",
authType: "basic",
router: {
enabled: true,
routeName: "searchstax",
title: (result: ISearchObject) => {
return "Search results for: " + result.query;
},
ignoredKeys: [],
},
hooks: {
beforeSearch: function (props: ISearchObject) {
const propsCopy = { ...props };
return propsCopy;
},
afterSearch: function (results: ISearchstaxParsedResult[]) {
const copy = [...results];
return copy;
},
}
};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.
<SearchstaxWrapper
:language="sampleConfig.language"
:model="sampleConfig.model"
:searchURL="sampleConfig.searchURL"
:suggesterURL="sampleConfig.suggesterURL"
:trackApiKey="sampleConfig.trackApiKey"
:searchAuth="sampleConfig.searchAuth"
:authType="sampleConfig.authType"
:router="sampleConfig.router"
:beforeSearch="sampleConfig.hooks.beforeSearch" // callback function to intercept search object before search is fired
:afterSearch="sampleConfig.hooks.afterSearch" // callback function to handle results after search is fired
>
<template #default>
<div class="searchstax-page-layout-container">
<SearchstaxInputWidget
:afterAutosuggest="afterAutosuggest"
:beforeAutosuggest="beforeAutosuggest"
:suggestAfterMinChars="3"
>
</SearchstaxInputWidget>
<div class="search-details-container">
<SearchstaxSearchFeedbackWidget></SearchstaxSearchFeedbackWidget>
<SearchstaxSortingWidget></SearchstaxSortingWidget>
</div>
<div class="searchstax-page-layout-facet-result-container">
<div class="searchstax-page-layout-facet-container">
<SearchstaxFacetsWidget
:facetingType="'or'"
:itemsPerPageDesktop="3"
:itemsPerPageMobile="99"
>
</SearchstaxFacetsWidget>
</div>
<div class="searchstax-page-layout-result-container">
<div id="searchstax-external-promotions-layout-container"></div>
<SearchstaxResultWidget :afterLinkClick="afterLinkClick">
</SearchstaxResultWidget>
<div id="searchstax-related-searches-container"></div>
<SearchstaxPaginationWidget>
</SearchstaxPaginationWidget>
</div>
</div>
</div>
</template>
</SearchstaxWrapper>widgets
Following widgets are available:
Answer Widget
SearchStax Site Search solution offers Vue widgets to assist in building your custom search page.
The SearchstaxAnswerWidget component for Vue provides an AI answer widget for your searches.
Usage
<SearchstaxAnswerWidget :showMoreAfterWordCount="10"></SearchstaxAnswerWidget>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
<SearchstaxAnswerWidget :showMoreAfterWordCount="100" :feedbackwidget="feedbackConfig">
<template #answer="{ answerData, showMore }">
<div v-if="answerData && answerData?.searchExecuted && answerData.shouldShowAnswer">
<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"
v-if="answerData.shouldShowAnswerError"
v-html="answerData.answerErrorMessage"
></div>
<div class="searchstax-answer-description" v-html="answerData.fullAnswerFormatted"></div>
<div v-if="answerData.answerLoading" class="searchstax-answer-loading"></div>
</div>
<div
v-if="answerData.showMoreButtonVisible"
class="searchstax-answer-load-more-button-container"
>
<button class="searchstax-answer-load-more-button" @click="showMore">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>
</div>
</template>
</SearchstaxAnswerWidget>Input Widget
SearchStax Site Search solution offers a Vue search-input widget to assist with your custom search page.
The SearchstaxInputWidget provides a search input box with autosuggest/autocomplete functionality.
Usage
<SearchstaxInputWidget
:afterAutosuggest="afterAutosuggest"
:beforeAutosuggest="beforeAutosuggest"
:suggestAfterMinChars="3"
></SearchstaxInputWidget>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.
inputWidgetTemplate
The inputTemplate 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
<SearchstaxInputWidget
:afterAutosuggest="afterAutosuggest"
:beforeAutosuggest="beforeAutosuggest"
:suggestAfterMinChars="3"
>
<template #input="{ suggestions, onMouseLeave, onMouseOver, onMouseClick }">
<div class="searchstax-search-input-wrapper">
<input
type="text"
id="searchstax-search-input"
class="searchstax-search-input"
placeholder="SEARCH FOR..."
aria-label="search"
/>
<button v-if="hasInputValue" @click="onClearClick" @keyup.enter="onClearClick" id="searchstax-clear-input-action-button" class="searchstax-cross-icon " aria-label="clear input" role="button"></button>
<span v-if="hasInputValue" id="searchstax-separator-icon" class="searchstax-separator "></span>
<div
class="searchstax-autosuggest-container"
:class="{ hidden: suggestions.length === 0 }"
@mouseleave="onMouseLeave"
>
<div
class="searchstax-autosuggest-item"
v-for="suggestion in suggestions"
:key="suggestion.term"
>
<div
class="searchstax-autosuggest-item-term-container"
v-html="suggestion.term"
tabindex="0"
@mouseover="onMouseOver(suggestion)"
@click.stop="onMouseClick()"
></div>
</div>
</div>
</div>
<SearchstaxLocationWidget />
<button
class="searchstax-spinner-icon"
id="searchstax-search-input-action-button"
aria-label="search"
role="button"
></button>
</template>
</SearchstaxInputWidget>Location Widget
SearchStax Site Search solution offers a Vue 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"
},
}
<SearchstaxLocationWidget :locationDecode="locationWidgetConfig.locationDecodeFunction" :locationDecodeCoordinatesToAddress="locationWidgetConfig.locationDecodeCoordinatesToAddress" :locationValuesOverride="locationWidgetConfig.locationValuesOverride" :locationSearchEnabled="locationWidgetConfig.locationSearchEnabled">
</SearchstaxLocationWidget>Props
- locationDecode - callback function to override location decoding
- locationDecodeCoordinatesToAddress - callback function to override location decoding
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"
},
}
<div
class="searchstax-location-input-container"
data-test-id="searchstax-location-input-container"
>
<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"
:class="{ 'searchstax-input-location-error': locationError }"
placeholder="Zip, Postal Code or City..."
aria-label="Search Location Input"
data-test-id="searchstax-location-input"
@change="inputChange"
:value="inputValue"
@blur="locationBlur"
/>
<button @click="getCurrentLocation" class="searchstax-get-current-location-button">
Use my current location
</button>
</div>
<span
v-if="locationData.shouldShowLocationDistanceDropdown"
class="searchstax-location-input-label"
>WITHIN</span
>
<select
v-if="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"
:value="selectValue"
>
<option
v-for="value in locationData.locationSearchDistanceValues"
:value="value.value"
:key="value.value"
>
{{ value.label }}
</option>
</select>
</div>
</div>
<SearchstaxLocationWidget :locationDecode="locationWidgetConfig.locationDecode" :locationDecodeCoordinatesToAddress="locationWidgetConfig.locationDecodeCoordinatesToAddress" :locationValuesOverride="locationWidgetConfig.locationValuesOverride" :locationSearchEnabled="locationWidgetConfig.locationSearchEnabled">
<template
#location="{
locationData,
inputValue,
locationBlur,
radiusChange,
selectValue,
inputChange,
locationError,
getCurrentLocation
}"
>
<div
class="searchstax-location-input-container"
data-test-id="searchstax-location-input-container"
>
<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"
:class="{ 'searchstax-input-location-error': locationError }"
placeholder="Zip, Postal Code or City..."
aria-label="Search Location Input"
data-test-id="searchstax-location-input"
@change="inputChange"
:value="inputValue"
@blur="locationBlur"
/>
<button @click="getCurrentLocation" class="searchstax-get-current-location-button">
Use my current location
</button>
</div>
<span
v-if="locationData.shouldShowLocationDistanceDropdown"
class="searchstax-location-input-label"
>WITHIN</span
>
<select
v-if="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"
:value="selectValue"
>
<option
v-for="value in locationData.locationSearchDistanceValues"
:value="value.value"
:key="value.value"
>
{{ value.label }}
</option>
</select>
</div>
</div>
</template>
</SearchstaxLocationWidget>Result Widget
The SearchStax Site Search solution offers a Vue results widget to assist with your custom search page.
The SearchstaxResultsWidget component displays the search results.
Usage
<SearchstaxResultWidget :afterLinkClick="afterLinkClick" :renderMethod="'“pagination”'" :resultsPerPage="10"></SearchstaxResultWidget>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.
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
<SearchstaxResultWidget :afterLinkClick="afterLinkClick">
<template #results="{ searchResults, resultClicked }">
<div class="searchstax-search-results" v-if="searchResults && searchResults.length">
<a
:key="searchResult.uniqueId"
v-for="searchResult in searchResults"
:href="searchResult.url"
:data-searchstax-unique-result-id="searchResult.uniqueId"
@click="resultClicked(searchResult, $event)"
@keyup.enter="resultClicked(searchResult, $event)"
@keyup.space="resultClicked(searchResult, $event)"
aria-labelledby="title-{{searchResult.uniqueId}}"
class="searchstax-result-item-link searchstax-result-item-link-wrapping"
tabindex="0"
>
<div
class="searchstax-search-result"
:class="{
'has-thumbnail': searchResult.thumbnail,
promoted: searchResult.promoted
}"
>
<div v-if="searchResult.promoted" class="searchstax-search-result-promoted"></div>
<div
v-if="searchResult.ribbon"
v-html="searchResult.ribbon"
class="searchstax-search-result-ribbon"
></div>
<img
alt=""
v-if="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" v-html="searchResult.title"></span>
</div>
<p
v-if="searchResult.paths"
v-html="searchResult.paths"
tabindex="0"
class="searchstax-search-result-common"
></p>
<p
v-if="searchResult.description"
tabindex="0"
v-html="searchResult.description"
class="searchstax-search-result-description searchstax-search-result-common"
></p>
<div :key="unmappedField.key" v-for="unmappedField in searchResult.unmappedFields">
<div
v-if="unmappedField.isImage && typeof unmappedField.value === 'string'"
class="searchstax-search-result-image-container"
>
<img alt="" :src="unmappedField.value" class="searchstax-result-image" />
</div>
<div v-else>
<p tabindex="0" class="searchstax-search-result-common" v-html="unmappedField.value"></p>
</div>
</div>
</div>
</a>
</div>
</template>
<template #noResult="{ searchResults, resultClicked, searchTerm, metadata, executeSearch, store }">
<div
v-if="
searchResults &&
searchResults.length === 0 &&
searchTerm !== '' &&
searchTerm !== '*' &&
store.searchstax.dataLayer.parsedData.firstSearchHappened
"
>
<div class="searchstax-no-results">
Showing <strong>no results</strong> for <strong>"{{ searchTerm }}"</strong>
<br />
<span v-if="metadata?.spellingSuggestion"
>Did you mean
<a
href="#"
class="searchstax-suggestion-term"
@click="executeSearch(metadata?.spellingSuggestion, $event)"
>
{{ 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>
</div>
</template>
</SearchstaxResultWidget>Example of infinite scroll and pagination render methods
<SearchstaxResultWidget :afterLinkClick="afterLinkClick" :renderMethod="'infiniteScroll'" :resultsPerPage="10">
<template #results="{ searchResults, resultClicked }">
<div class="searchstax-search-results" v-if="searchResults && searchResults.length">
<a
:key="searchResult.uniqueId"
v-for="searchResult in searchResults"
:href="searchResult.url"
:data-searchstax-unique-result-id="searchResult.uniqueId"
@click="resultClicked(searchResult, $event)"
@keyup.enter="resultClicked(searchResult, $event)"
@keyup.space="resultClicked(searchResult, $event)"
aria-labelledby="title-{{searchResult.uniqueId}}"
class="searchstax-result-item-link searchstax-result-item-link-wrapping"
tabindex="0"
>
<div
class="searchstax-search-result"
:class="{
'has-thumbnail': searchResult.thumbnail,
promoted: searchResult.promoted
}"
>
<div v-if="searchResult.promoted" class="searchstax-search-result-promoted"></div>
<div
v-if="searchResult.ribbon"
v-html="searchResult.ribbon"
class="searchstax-search-result-ribbon"
></div>
<img
alt=""
v-if="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" v-html="searchResult.title"></span>
</div>
<p
v-if="searchResult.paths"
v-html="searchResult.paths"
tabindex="0"
class="searchstax-search-result-common"
></p>
<p
v-if="searchResult.description"
tabindex="0"
v-html="searchResult.description"
class="searchstax-search-result-description searchstax-search-result-common"
></p>
<div :key="unmappedField.key" v-for="unmappedField in searchResult.unmappedFields">
<div
v-if="unmappedField.isImage && typeof unmappedField.value === 'string'"
class="searchstax-search-result-image-container"
>
<img alt="" :src="unmappedField.value" class="searchstax-result-image" />
</div>
<div v-else>
<p tabindex="0" class="searchstax-search-result-common" v-html="unmappedField.value"></p>
</div>
</div>
</div>
</a>
</div>
</template>
<template #noResult="{ searchResults, resultClicked, searchTerm, metadata, executeSearch, store }">
<div
v-if="
searchResults &&
searchResults.length === 0 &&
searchTerm !== '' &&
searchTerm !== '*' &&
store.searchstax.dataLayer.parsedData.firstSearchHappened
"
>
<div class="searchstax-no-results">
Showing <strong>no results</strong> for <strong>"{{ searchTerm }}"</strong>
<br />
<span v-if="metadata?.spellingSuggestion"
>Did you mean
<a
href="#"
class="searchstax-suggestion-term"
@click="executeSearch(metadata?.spellingSuggestion, $event)"
>
{{ 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>
</div>
</template>
</SearchstaxResultWidget>
Pagination Widget
The SearchStax Site Search solution offers a Vue pagination widget to assist with you custom search page.
The SearchstaxPaginationWidget component displays pagination controls for search results.
Usage
<SearchstaxPaginationWidget></SearchstaxPaginationWidget>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
<SearchstaxPaginationWidget>
<template #pagination="{ paginationData, previousPage, nextPage }">
<div class="searchstax-pagination-container" v-if="paginationData">
<div class="searchstax-pagination-content">
<a
class="searchstax-pagination-previous"
:class="paginationData.isFirstPage ? 'disabled' : ''"
:disabled="paginationData?.isFirstPage"
@click="previousPage"
@keyup.enter="previousPage"
@keyup.space="previousPage"
tabindex="0"
id="searchstax-pagination-previous"
>
< Previous
</a>
<div class="searchstax-pagination-details">
{{ paginationData?.startResultIndex }} - {{ paginationData?.endResultIndex }} of
{{ paginationData?.totalResults }}
</div>
<a
class="searchstax-pagination-next"
:class="paginationData.isLastPage ? 'disabled' : ''"
:disabled="paginationData?.isLastPage"
tabindex="0"
@click="nextPage"
@keyup.enter="nextPage"
@keyup.space="nextPage"
id="searchstax-pagination-next"
>Next ></a
>
</div>
</div>
</template>
</SearchstaxPaginationWidget>
// infinite scroll example
<SearchstaxPaginationWidget>
<template #infiniteScroll="{ nextPage }">
<div className="searchstax-pagination-container">
<a
class="searchstax-pagination-load-more"
tabindex="0"
@click="nextPage"
@keyup.enter="nextPage"
@keyup.space="nextPage"
>Load More</a
>
</div>
</template>
</SearchstaxPaginationWidget>Facets Widget
The SearchStax Site Search solution offers a Vue SearchstaxFacetsWidget component to display facets on your custom search page.
Facet Selection and Order
Facet lists are configured and ordered on the Site Search Faceting Tab.
Usage
<SearchstaxFacetsWidget
:facetingType="'or'"
:itemsPerPageDesktop="3"
:itemsPerPageMobile="99"
></SearchstaxFacetsWidget>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
- 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
<SearchstaxFacetsWidget
:facetingType="'or'"
:itemsPerPageDesktop="3"
:itemsPerPageMobile="99"
>
<template
#desktopFacets="{ facetsTemplateDataDesktop, isNotDeactivated, toggleFacetGroup, isChecked, selectFacet, showMoreLessDesktop, facetContainers, updateRefDesktop }"
>
<div
v-if="facetsTemplateDataDesktop?.hasResultsOrExternalPromotions"
class="searchstax-facets-container-desktop"
>
<div
v-for="facet in facetsTemplateDataDesktop.facets"
:key="facet.name + 'desktop'"
class="searchstax-facet-container"
:class="{ active: isNotDeactivated(facet.name) }"
>
<div>
<div class="searchstax-facet-title-container" @click="toggleFacetGroup(facet.name)">
<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
v-for="(facetValue) in (facet.values as IFacetValueData[])"
:key="facetValue.value + facetValue.parentName"
class="searchstax-facet-value-container"
:class="{ 'searchstax-facet-value-disabled': facetValue.disabled }"
:ref="(el) => updateRefDesktop(el, facetValue)"
>
<div class="searchstax-facet-input">
<input
type="checkbox"
class="searchstax-facet-input-checkbox"
:checked="isChecked(facetValue)"
:disabled="facetValue.disabled"
:aria-label="facetValue.value + ' ' + facetValue.count"
@click="selectFacet(facetValue.value + '-desktop', $event, facetValue, true)"
/>
</div>
<div
class="searchstax-facet-value-label"
@click="selectFacet(facetValue.value + '-desktop', $event, facetValue, false)"
>
{{ facetValue.value }}
</div>
<div
class="searchstax-facet-value-count"
@click="selectFacet(facetValue.value + '-desktop', $event, facetValue, false)"
>
({{ facetValue.count }})
</div>
</div>
<div class="searchstax-facet-show-more-container" v-if="facet.hasMoreFacets">
<div
class="searchstax-facet-show-more-container"
@click="showMoreLessDesktop($event, facet)"
@keyup.space="
$event.preventDefault();
showMoreLessDesktop($event, facet)
"
@keyup.enter="showMoreLessDesktop($event, facet)"
tabindex="0"
role="button"
>
<div
v-if="facet.showingAllFacets"
class="searchstax-facet-show-less-button searchstax-facet-show-button"
>
less
</div>
<div v-else class="searchstax-facet-show-more-button searchstax-facet-show-button">
more
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<template
#mobileFacets="{ facetsTemplateDataMobile, selectedFacetsCheckboxes, isNotDeactivated, toggleFacetGroup, isChecked, selectFacet, showMoreLessDesktop, facetContainers, openOverlay, unselectFacet, unselectAll, closeOverlay, updateRefMobile }"
>
<div
class="searchstax-facets-container-mobile"
v-if="facetsTemplateDataMobile?.hasResultsOrExternalPromotions"
>
<div class="searchstax-facets-pills-container">
<div class="searchstax-facets-pill searchstax-facets-pill-filter-by" @click="openOverlay">
<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"
v-for="facet in selectedFacetsCheckboxes"
:key="facet.value"
@click="unselectFacet(facet)"
>
<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"
v-if="selectedFacetsCheckboxes.length"
@click="unselectAll"
>
<div class="searchstax-facets-pill-label">Clear Filters</div>
</div>
</div>
<div
class="searchstax-facets-mobile-overlay"
:class="{ 'searchstax-show': (facetsTemplateDataMobile as any)?.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"></div>
</div>
<div class="searchstax-facets-container-mobile">
<div
v-for="facet in facetsTemplateDataMobile?.facets"
:key="facet.name + 'mobile'"
class="searchstax-facet-container"
:class="{ active: isNotDeactivated(facet.name) }"
>
<div>
<div class="searchstax-facet-title-container" @click="toggleFacetGroup(facet.name)">
<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
v-for="(facetValue) in facet.values as IFacetValueData[]"
:key="facetValue.value + facetValue.parentName"
class="searchstax-facet-value-container"
:class="{ 'searchstax-facet-value-disabled': facetValue.disabled }"
:ref="
(el) => {
updateRefMobile(el, facetValue)
}
"
>
<div class="searchstax-facet-input">
<input
type="checkbox"
class="searchstax-facet-input-checkbox"
:checked="isChecked(facetValue)"
:disabled="facetValue.disabled"
:aria-label="facetValue.value + ' ' + facetValue.count"
@click="selectFacet(facetValue.value + '-mobile', $event, facetValue, true)"
/>
</div>
<div
class="searchstax-facet-value-label"
@click="selectFacet(facetValue.value + '-mobile', $event, facetValue, false)"
>
{{ facetValue.value }}
</div>
<div
class="searchstax-facet-value-count"
@click="selectFacet(facetValue.value + '-mobile', $event, facetValue, false)"
>
({{ facetValue.count }})
</div>
</div>
<div class="searchstax-facet-show-more-container" v-if="facet.hasMoreFacets">
<div
class="searchstax-facet-show-more-container"
@click="showMoreLessMobile($event, facet)"
@keyup.space="showMoreLessMobile($event, facet)"
@keyup.enter="showMoreLessMobile($event, facet)"
tabindex="0"
>
<div
v-if="facet.showingAllFacets"
class="searchstax-facet-show-less-button searchstax-facet-show-button"
>
less
</div>
<div
v-else
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">Done</button>
</div>
</div>
</template>
</SearchstaxFacetsWidget>SearchFeedback Widget
The SearchStax Site Search solution provides a search feedback widget to support your Vue search pages.
The SearchstaxSearchFeedbackWidget displays search feedback and stats.
Usage
<SearchstaxSearchFeedbackWidget></SearchstaxSearchFeedbackWidget>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
<SearchstaxSearchFeedbackWidget>
<template #searchFeedback="{ searchFeedbackData, onOriginalQueryClick }">
<h2
class="searchstax-feedback-container"
v-if="
searchFeedbackData && searchFeedbackData?.searchExecuted && searchFeedbackData?.totalResults
"
>
Showing
<b>{{ searchFeedbackData.startResultIndex }} - {{ searchFeedbackData.endResultIndex }}</b> of
<b>{{ searchFeedbackData.totalResults }}</b> results
<span v-if="searchFeedbackData.searchTerm"
>for "<b>{{ searchFeedbackData.searchTerm }}</b
>"
</span>
<div class="searchstax-feedback-container-suggested">
<div v-if="searchFeedbackData.autoCorrectedQuery">
Search instead for
<a
href="#"
@click.prevent="onOriginalQueryClick($event)"
class="searchstax-feedback-original-query"
:aria-label="'Search instead for ' + searchFeedbackData.originalQuery"
>{{ searchFeedbackData.originalQuery }}</a
>
</div>
</div>
</h2>
</template>
</SearchstaxSearchFeedbackWidget>RelatedSearches widget
The SearchStax Site Search solution offers a Vue widget for displaying related searches.
The SearchstaxRelatedSearchesWidget for Vue component displays related searches.
Usage
<SearchstaxRelatedSearchesWidget
:relatedSearchesURL="config.relatedSearchesURL"
:relatedSearchesAPIKey="config.relatedSearchesAPIKey"
></SearchstaxRelatedSearchesWidget>Props
- relatedSearchesURL: API URL for fetching related searches
- relatedSearchesAPIKey?: API key for related searches API
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
<SearchstaxRelatedSearchesWidget
:relatedSearchesURL="config.relatedSearchesURL"
:relatedSearchesAPIKey="config.relatedSearchesAPIKey"
>
<template #related="{ relatedData, executeSearch }">
<div
class="searchstax-related-searches-container"
id="searchstax-related-searches-container"
v-if="relatedData && relatedData?.searchExecuted && relatedData?.hasRelatedSearches"
>
Related searches: <span id="searchstax-related-searches"></span>
<span class="searchstax-related-search" v-if="relatedData.relatedSearches">
<span
v-for="related in relatedData.relatedSearches"
:key="related.related_search"
@click="executeSearch(related)"
@keyup.enter="executeSearch(related)"
@keyup.space="executeSearch(related)"
tabindex="0"
class="searchstax-related-search searchstax-related-search-item"
:aria-label="'Related search: ' + related.related_search"
>
{{ related.related_search }}<span v-if="!related.last">,</span>
</span>
</span>
</div>
</template>
</SearchstaxRelatedSearchesWidget>ExternalPromotions widget
The SearchStax Site Search solution offers a Vue external-promotions widget for your custom search page.
The SearchstaxExternalPromotionsWidget component displays external promotions fetched from the API.
Usage
<SearchstaxExternalPromotionsWidget></SearchstaxExternalPromotionsWidget>Main Template Override
Main template for external promotions.
It receives following props:
- externalPromotionsData – External promotions data object
- trackClick – Handler for tracking link clicks
Example
<SearchstaxExternalPromotionsWidget>
<template #externalPromotions="{ externalPromotionsData, trackClick }">
<div
class="searchstax-external-promotions-container"
id="searchstax-external-promotions-container"
v-if="
externalPromotionsData &&
externalPromotionsData?.searchExecuted &&
externalPromotionsData?.hasExternalPromotions
"
>
<div
class="searchstax-external-promotion searchstax-search-result"
v-for="externalPromotion in externalPromotionsData.externalPromotions"
:key="externalPromotion.id"
>
<div class="icon-elevated"></div>
<a
v-if="externalPromotion.url"
href="{{externalPromotion.url}}"
@click="trackClick(externalPromotion, $event)"
class="searchstax-result-item-link"
></a>
<div class="searchstax-search-result-title-container">
<span class="searchstax-search-result-title">{{ externalPromotion.name }}</span>
</div>
<p
v-if="externalPromotion.description"
class="searchstax-search-result-description searchstax-search-result-common"
>
{{ externalPromotion.description }}
</p>
<p
v-if="externalPromotion.url"
class="searchstax-search-result-description searchstax-search-result-common"
>
{{ externalPromotion.url }}
</p>
</div>
</div>
</template>
</SearchstaxExternalPromotionsWidget>Sorting Widget
The SearchStax Site Search solution offers a Vue sorting widget for your custom search page.
The SearchstaxSortingWidget component displays sorting options for search results.
Usage
<SearchstaxSortingWidget></SearchstaxSortingWidget>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
Example
<SearchstaxSortingWidget>
<template #sorting="{ sortingData, orderChange, selectedSorting }">
<div
class="searchstax-sorting-container"
v-if="sortingData && sortingData?.searchExecuted && sortingData?.hasResultsOrExternalPromotions && sortingData.sortOptions.length > 0"
>
<label class="searchstax-sorting-label" for="searchstax-search-order-select">Sort By</label>
<select
id="searchstax-search-order-select"
class="searchstax-search-order-select"
:value="selectedSorting"
@change="orderChange($event)"
>
<option
v-for="sortOption in sortingData.sortOptions"
:key="sortOption.key"
:value="sortOption.key"
>
{{ sortOption.value }}
</option>
</select>
</div>
</template>
</SearchstaxSortingWidget>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