@searchstax-inc/searchstudio-ux-react
v0.3.18
Published
Library to build Site Search page
Downloads
203
Readme
sitesearch-ux-react
Library to build Site Search page
Installation
npm install following package
npm install --save @searchstax-inc/searchstudio-ux-react
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
searchURL={config.searchURL}
suggesterURL={config.suggesterURL}
trackApiKey={config.trackApiKey}
searchAuth={config.searchAuth}
initialized={initialized}
beforeSearch={beforeSearch}
afterSearch={afterSearch}
authType="basic"
language="en"
>
// other components will go there
</SearchstaxWrapper>
Initialization
Initialization object needs to be of type: ISearchstaxConfig
Initialization example
sampleConfig = {
language: "en",
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
searchURL={sampleConfig.searchURL}
suggesterURL={sampleConfig.suggesterURL}
trackApiKey={sampleConfig.trackApiKey}
searchAuth={sampleConfig.searchAuth}
beforeSearch={sampleConfig.hooks.beforeSearch}
afterSearch={sampleConfig.hooks.afterSearch}
authType="basic"
language="en"
>
<div className="searchstax-page-layout-container">
<SearchstaxInputWidget
afterAutosuggest={afterAutosuggest}
beforeAutosuggest={beforeAutosuggest}
inputTemplate={InputTemplate}
></SearchstaxInputWidget>
<div className="search-details-container">
<div id="search-feedback-container"></div>
<div id="search-sorting-container"></div>
</div>
<div className="searchstax-page-layout-facet-result-container">
<div className="searchstax-page-layout-facet-container">
<div id="searchstax-facets-container"></div>
</div>
<div className="searchstax-page-layout-result-container">
<div id="searchstax-external-promotions-layout-container"></div>
<SearchstaxResultWidget
afterLinkClick={afterLinkClick}
noResultTemplate={noResultTemplate}
resultsTemplate={resultsTemplate}
></SearchstaxResultWidget>
<div id="searchstax-related-searches-container"></div>
<div id="searchstax-pagination-container"></div>
</div>
</div>
</div>
</SearchstaxWrapper>
widgets
Following widgets are available:
Input Widget
example of input widget initialization with minimum options
<SearchstaxInputWidget
afterAutosuggest={afterAutosuggest}
beforeAutosuggest={beforeAutosuggest}
></SearchstaxInputWidget>
example of input widget initialization with template override
function InputTemplate(
suggestions: ISearchstaxSuggestion[],
onMouseLeave: () => void,
onMouseOver: (suggestion: ISearchstaxSuggestion) => void,
onMouseClick: () => void
): React.ReactElement {
return (
<div className="searchstax-search-input-wrapper">
<input
type="text"
id="searchstax-search-input"
className="searchstax-search-input"
placeholder="SEARCH FOR..."
aria-label="search"
/>
<div
className={`searchstax-autosuggest-container ${
suggestions.length === 0 ? "hidden" : ""
}`}
onMouseLeave={onMouseLeave}
>
{suggestions.map(function (suggestion) {
return (
<div
className="searchstax-autosuggest-item"
key={suggestion.term}
>
<div
className="searchstax-autosuggest-item-term-container"
dangerouslySetInnerHTML={{ __html: suggestion.term }}
onMouseOver={() => {
onMouseOver(suggestion);
}}
onClick={() => {
onMouseClick();
}}
></div>
</div>
);
})}
</div>
<button
className="searchstax-spinner-icon"
id="searchstax-search-input-action-button"
aria-label="search"
></button>
</div>
);
}
<SearchstaxInputWidget
afterAutosuggest={afterAutosuggest}
beforeAutosuggest={beforeAutosuggest}
inputTemplate={InputTemplate}
></SearchstaxInputWidget>
Result Widget
example of results widget initialization with minimum options
<SearchstaxResultWidget
afterLinkClick={afterLinkClick}
resultsPerPage={10}
renderMethod={'pagination'}
></SearchstaxResultWidget>
example of results widget initialization with minimum options for infinite scroll behavior
<SearchstaxResultWidget
afterLinkClick={afterLinkClick}
resultsPerPage={10}
renderMethod={'infiniteScroll'}
></SearchstaxResultWidget>
example of result widget initialization with template override
function noResultTemplate(searchTerm: string, metaData: ISearchstaxSearchMetadata | null, executeSearch: (term:string) => void): React.ReactElement {
return (
<div>
<div className="searchstax-no-results">
{" "}
Showing <strong>no results</strong> for <strong>"{searchTerm}"</strong>
<br />
{metaData?.spellingSuggestion && (
<span>
Did you mean{" "}
<a href="#" className="searchstax-suggestion-term" onClick={(e) => {
e.preventDefault();
executeSearch(metaData?.spellingSuggestion);
}}>
{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>
<li>Lost? Click on the ‘X” in the Search Box to reset your search.</li>
</ul>
</div>
);
}
function resultsTemplate(
searchResults: ISearchstaxParsedResult[],
resultClicked: (results: ISearchstaxParsedResult, event: any) => ISearchstaxParsedResult[]
): React.ReactElement {
return (
<>
{searchResults && searchResults.length && (
<div className="searchstax-search-results">
{searchResults !== null &&
searchResults.map(function (searchResult) {
return (
<a href={searchResult.url ?? ''} key={searchResult.uniqueId} onClick={event => {
resultClicked(searchResult, event);
}} tabIndex={0} data-searchstax-unique-result-id={ searchResult.uniqueId} className="searchstax-result-item-link searchstax-result-item-link-wrapping">
<div
className={`searchstax-search-result ${
searchResult.thumbnail ? "has-thumbnail" : ""
}`}
key={searchResult.uniqueId}
>
{searchResult.promoted && (
<div className="searchstax-search-result-promoted"></div>
)}
{searchResult.ribbon && (
<div className="searchstax-search-result-ribbon">
{searchResult.ribbon}
</div>
)}
{searchResult.thumbnail && (
<img
src={searchResult.thumbnail}
className="searchstax-thumbnail"
/>
)}
<div className="searchstax-search-result-title-container">
<span className="searchstax-search-result-title">
{searchResult.title}
</span>
</div>
{searchResult.paths && (
<p className="searchstax-search-result-common">
{searchResult.paths}
</p>
)}
{searchResult.description && (
<p className="searchstax-search-result-description searchstax-search-result-common">
{searchResult.description}
</p>
)}
{searchResult.unmappedFields.map(function (
unmappedField: any
) {
return (
<div key={unmappedField.key}>
{unmappedField.isImage &&
typeof unmappedField.value === "string" && (
<div className="searchstax-search-result-image-container">
<img
src={unmappedField.value}
className="searchstax-result-image"
/>
</div>
)}
{!unmappedField.isImage && (
<div>
<p className="searchstax-search-result-common">
{unmappedField.value}
</p>
</div>
)}
</div>
);
})}
</div>
</a>
);
})}
</div>
)}
</>
);
}
<SearchstaxResultWidget
afterLinkClick={afterLinkClick}
noResultTemplate={noResultTemplate}
resultsTemplate={resultsTemplate}
></SearchstaxResultWidget>
Pagination Widget
example of pagination widget initialization with minimum options
<SearchstaxPaginationWidget></SearchstaxPaginationWidget>
example of pagination widget initialization with various options
function paginationTemplate(
paginationData: IPaginationData | null,
nextPage: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void,
previousPage: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void
) {
return (
<>
{paginationData && paginationData?.totalResults !== 0 && (
<div className="searchstax-pagination-container">
<div className="searchstax-pagination-content">
<a
className="searchstax-pagination-previous"
style={
paginationData?.isFirstPage ? { pointerEvents: "none" } : {}
}
onClick={(e) => {
previousPage(e);
}}
tabIndex={0}
id="searchstax-pagination-previous"
>
{" "}
< Previous{" "}
</a>
<div className="searchstax-pagination-details">
{" "}
{paginationData?.startResultIndex} -{" "}
{paginationData?.endResultIndex} of{" "}
{paginationData?.totalResults}
</div>
<a
className="searchstax-pagination-next"
style={
paginationData?.isLastPage ? { pointerEvents: "none" } : {}
}
onClick={(e) => {
nextPage(e);
}}
tabIndex={0}
id="searchstax-pagination-next"
>
Next >
</a>
</div>
</div>
)}
</>
);
}
<SearchstaxPaginationWidget paginationTemplate={paginationTemplate}></SearchstaxPaginationWidget>
example of pagination widget for infinite scroll
function infiniteScrollTemplate(
paginationData: IPaginationData | null,
nextPage: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void
) {
return (
<>
{paginationData &&
paginationData.isInfiniteScroll &&
paginationData?.totalResults !== 0 &&
!paginationData?.isLastPage && (
<div className="searchstax-pagination-container">
<a
className="searchstax-pagination-load-more"
onClick={(e) => {
nextPage(e);
}}
onKeyDown={(e) => {
if(e.key === 'Enter' || e.key === ' ') {
nextPage(e as any);
}
}}
>
Show More >
</a>
</div>
)}
</>
);
}
<SearchstaxPaginationWidget infiniteScrollTemplate={infiniteScrollTemplate}></SearchstaxPaginationWidget>
Facets Widget
example of facets widget initialization with minimum options
<SearchstaxFacetsWidget
facetingType="and"
itemsPerPageDesktop={2}
itemsPerPageMobile={3}
specificFacets={undefined}
></SearchstaxFacetsWidget>
example of facets widget initialization with template overrides
function facetsTemplateDesktop(
facetsTemplateDataDesktop: IFacetsTemplateData | null,
facetContainers: {
[key: string]: React.LegacyRef<HTMLDivElement> | undefined;
},
isNotDeactivated: (name: string) => boolean,
toggleFacetGroup: (name: string) => void,
selectFacet: (
index: string,
event: React.MouseEvent<HTMLDivElement, MouseEvent>,
data: IFacetValueData,
isInput: boolean
) => void,
isChecked: (facetValue: IFacetValueData) => boolean | undefined,
showMoreLessDesktop: (
e: React.MouseEvent<HTMLDivElement, MouseEvent>,
data: IFacetData
) => void
) {
return (
<div className="searchstax-facets-container-desktop">
{facetsTemplateDataDesktop?.facets.map((facet) => {
return (
<div
className={`searchstax-facet-container ${
isNotDeactivated(facet.name) ? "active" : ""
}`}
key={facet.name + "desktop"}
>
<div>
<div
className="searchstax-facet-title-container"
onClick={() => {
toggleFacetGroup(facet.name);
}}
>
<div className="searchstax-facet-title"> {facet.label}</div>
<div className="searchstax-facet-title-arrow active"></div>
</div>
<div className="searchstax-facet-values-container">
{facet.values.map(
//@ts-ignore
(facetValue: IFacetValueData, key) => {
facetContainers[key + facet.name] = createRef();
return (
<div
key={facetValue.value + facetValue.parentName}
className={`searchstax-facet-value-container ${
facetValue.disabled
? "searchstax-facet-value-disabled"
: ""
}`}
ref={facetContainers[key + facet.name]}
>
<div className={
"searchstax-facet-input" +
" desktop-" +
key +
facet.name
}>
<input
type="checkbox"
className="searchstax-facet-input-checkbox"
checked={isChecked(facetValue)}
readOnly={true}
aria-label={facetValue.value + ' ' + facetValue.count}
disabled={facetValue.disabled}
onClick={(e) => {
selectFacet(
key + facet.name,
e,
facetValue,
true
);
}}
/>
</div>
<div
className="searchstax-facet-value-label"
onClick={(e) => {
selectFacet(
key + facet.name,
e,
facetValue,
false
);
}}
>
{facetValue.value}
</div>
<div
className="searchstax-facet-value-count"
onClick={(e) => {
selectFacet(
key + facet.name,
e,
facetValue,
false
);
}}
>
({facetValue.count})
</div>
</div>
);
}
)}
{facet.hasMoreFacets && (
<div className="searchstax-facet-show-more-container">
<div
className="searchstax-facet-show-more-container"
onClick={(e) => {
showMoreLessDesktop(e, facet);
}}
>
{facet.showingAllFacets && (
<div className="searchstax-facet-show-less-button searchstax-facet-show-button">
less
</div>
)}
{!facet.showingAllFacets && (
<div className="searchstax-facet-show-more-button searchstax-facet-show-button">
more
</div>
)}
</div>
</div>
)}
</div>
</div>
</div>
);
})}
</div>
);
}
function facetsTemplateMobile(
facetsTemplateDataMobile: IFacetsTemplateData | null,
selectedFacetsCheckboxes: IFacetValue[],
facetContainers: {
[key: string]: React.LegacyRef<HTMLDivElement> | undefined;
},
isNotDeactivated: (name: string) => boolean,
toggleFacetGroup: (name: string) => void,
selectFacet: (
index: string,
event: React.MouseEvent<HTMLDivElement, MouseEvent>,
data: IFacetValueData,
isInput: boolean
) => void,
isChecked: (facetValue: IFacetValueData) => boolean | undefined,
unselectFacet: (facet: IFacetValue) => void,
showMoreLessMobile: (
e: React.MouseEvent<HTMLDivElement, MouseEvent>,
data: IFacetData
) => void,
openOverlay: () => void,
closeOverlay: () => void,
unselectAll: () => void
) {
return facetsTemplateDataMobile ? (
<div className="searchstax-facets-container-mobile">
<div className="searchstax-facets-pills-container">
<div
className="searchstax-facets-pill searchstax-facets-pill-filter-by"
onClick={() => {
openOverlay();
}}
>
<div className="searchstax-facets-pill-label">Filter By</div>
</div>
<div className="searchstax-facets-pills-selected">
{selectedFacetsCheckboxes.map((facet) => {
return (
<div
className="searchstax-facets-pill searchstax-facets-pill-facets"
key={facet.value}
onClick={() => {
unselectFacet(facet);
}}
>
<div className="searchstax-facets-pill-label">
{facet.value} ({facet.count})
</div>
<div className="searchstax-facets-pill-icon-close"></div>
</div>
);
})}
{selectedFacetsCheckboxes.length !== 0 && (
<div
className="searchstax-facets-pill searchstax-clear-filters searchstax-facets-pill-clear-all"
onClick={() => {
unselectAll();
}}
>
<div className="searchstax-facets-pill-label">
Clear Filters
</div>
</div>
)}
</div>
<div
className={`searchstax-facets-mobile-overlay ${
//@ts-ignore
facetsTemplateDataMobile.overlayOpened ? "searchstax-show" : ""
}`}
>
<div className="searchstax-facets-mobile-overlay-header">
<div className="searchstax-facets-mobile-overlay-header-title">
Filter By
</div>
<div
className="searchstax-search-close"
onClick={() => {
closeOverlay();
}}
></div>
</div>
<div className="searchstax-facets-container-mobile">
{facetsTemplateDataMobile?.facets.map((facet) => {
return (
<div
key={facet.name + "mobile"}
className={`searchstax-facet-container ${
isNotDeactivated(facet.name) ? `active` : ``
}`}
>
<div>
<div
className="searchstax-facet-title-container"
onClick={() => {
toggleFacetGroup(facet.name);
}}
>
<div className="searchstax-facet-title">
{" "}
{facet.label}{" "}
</div>
<div className="searchstax-facet-title-arrow active"></div>
</div>
<div className="searchstax-facet-values-container">
{facet.values.map(
//@ts-ignore
(facetValue: IFacetValueData, key) => {
facetContainers[key + facet.name] = createRef();
return (
<div
key={facetValue.value + facetValue.parentName}
className={`searchstax-facet-value-container ${
facetValue.disabled
? `searchstax-facet-value-disabled`
: ``
}`}
ref={facetContainers[key + facet.name]}
>
<div className={
"searchstax-facet-input" +
" mobile-" +
key +
facet.name
}>
<input
type="checkbox"
className="searchstax-facet-input-checkbox"
checked={isChecked(facetValue)}
readOnly={true}
aria-label={facetValue.value + ' ' + facetValue.count}
disabled={facetValue.disabled}
onClick={(e) => {
selectFacet(
key + facet.name,
e,
facetValue,
true
);
}}
/>
</div>
<div
className="searchstax-facet-value-label"
onClick={(e) => {
selectFacet(
key + facet.name,
e,
facetValue,
false
);
}}
>
{facetValue.value}
</div>
<div
className="searchstax-facet-value-count"
onClick={(e) => {
selectFacet(
key + facet.name,
e,
facetValue,
false
);
}}
>
({facetValue.count})
</div>
</div>
);
}
)}
<div
className="searchstax-facet-show-more-container"
v-if="facet.hasMoreFacets"
>
<div
className="searchstax-facet-show-more-container"
onClick={(e) => {
showMoreLessMobile(e, facet);
}}
>
{facet.showingAllFacets && (
<div className="searchstax-facet-show-less-button searchstax-facet-show-button">
less
</div>
)}
{!facet.showingAllFacets && (
<div className="searchstax-facet-show-more-button searchstax-facet-show-button">
more
</div>
)}
</div>
</div>
</div>
</div>
</div>
);
})}
</div>
<button
className="searchstax-facets-mobile-overlay-done"
onClick={() => {
closeOverlay();
}}
>
Done
</button>
</div>
</div>
</div>
) : (<></>);
}
<SearchstaxFacetsWidget
facetingType="and"
itemsPerPageDesktop={2}
itemsPerPageMobile={3}
specificFacets={undefined}
facetsTemplateDesktop={facetsTemplateDesktop}
facetsTemplateMobile={facetsTemplateMobile}
></SearchstaxFacetsWidget>
SearchFeedback Widget
example of search feedback widget initialization with minimum options
<SearchstaxOverviewWidget></SearchstaxOverviewWidget>
example of search feedback widget initialization with template overrides
function searchOverviewTemplate(
searchFeedbackData: null | ISearchstaxSearchFeedbackData,
onOriginalQueryClick: (event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void
) {
return (
<>
{searchFeedbackData && searchFeedbackData?.searchExecuted && searchFeedbackData?.totalResults > 0 && (
<>
Showing{" "}
<b>
{searchFeedbackData.startResultIndex} - {searchFeedbackData.endResultIndex}
</b>{" "}
of
<b> {searchFeedbackData.totalResults}</b> results
{searchFeedbackData.searchTerm && (
<span>
for "<b>{searchFeedbackData.searchTerm}</b>"{" "}
</span>
)}
<div className="searchstax-feedback-container-suggested">
{searchFeedbackData.autoCorrectedQuery && (
<div>
{" "}
Search instead for{" "}
<a
href="#"
onClick={(e) => {
onOriginalQueryClick(e);
}}
className="searchstax-feedback-original-query"
>
{searchFeedbackData.originalQuery}
</a>
</div>
)}
</div>
</>
)}
</>
);
}
<SearchstaxOverviewWidget searchOverviewTemplate={searchOverviewTemplate}></SearchstaxOverviewWidget>
RelatedSearches widget
example of search feedback widget initialization with minimum options
<SearchstaxRelatedSearchesWidget
relatedSearchesURL={config.relatedSearchesURL}
relatedSearchesAPIKey={config.relatedSearchesAPIKey}
></SearchstaxRelatedSearchesWidget>
example of search feedback widget initialization with template overrides
function searchRelatedSearchesTemplate(
relatedData: null | ISearchstaxRelatedSearchesData,
executeSearch: (result: ISearchstaxRelatedSearchResult) => void
) {
return (
<>
{relatedData && relatedData?.searchExecuted && relatedData?.hasRelatedSearches && (
<div className="searchstax-related-searches-container" id="searchstax-related-searches-container">
{" "}
Related searches: <span id="searchstax-related-searches"></span>
{relatedData.relatedSearches && (
<span className="searchstax-related-search">
{relatedData.relatedSearches.map((related) => (
<span
key={related.related_search}
onClick={() => {
executeSearch(related);
}}
className="searchstax-related-search searchstax-related-search-item"
>
{" "}
{related.related_search}
{!related.last && <span>,</span>}
</span>
))}
</span>
)}
</div>
)}
</>
);
}
<SearchstaxRelatedSearchesWidget
relatedSearchesURL={config.relatedSearchesURL}
relatedSearchesAPIKey={config.relatedSearchesAPIKey}
searchRelatedSearchesTemplate={searchRelatedSearchesTemplate}
></SearchstaxRelatedSearchesWidget>
ExternalPromotions widget
example of search feedback widget initialization with minimum options
<SearchstaxExternalPromotionsWidget></SearchstaxExternalPromotionsWidget>
example of search feedback widget initialization with template overrides
function searchExternalPromotionsTemplate(
externalPromotionsData: null | ISearchstaxExternalPromotionsData,
trackClick: (externalPromotion: IExternalPromotion, event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void
) {
return (
<>
{externalPromotionsData &&
externalPromotionsData?.searchExecuted &&
externalPromotionsData?.hasExternalPromotions &&
externalPromotionsData.externalPromotions.map((externalPromotion) => (
<div className="searchstax-external-promotion searchstax-search-result" key={externalPromotion.id}>
<div className="icon-elevated"></div>
{externalPromotion.url && (
<a
href={externalPromotion.url}
onClick={(event) => {
trackClick(externalPromotion, event);
}}
className="searchstax-result-item-link"
></a>
)}
<div className="searchstax-search-result-title-container">
<span className="searchstax-search-result-title">{externalPromotion.name}</span>
</div>
{externalPromotion.description && (
<p className="searchstax-search-result-description searchstax-search-result-common">
{" "}
{externalPromotion.description}{" "}
</p>
)}
{externalPromotion.url && (
<p className="searchstax-search-result-description searchstax-search-result-common">
{" "}
{externalPromotion.url}{" "}
</p>
)}
</div>
))}
</>
);
}
<SearchstaxExternalPromotionsWidget
searchExternalPromotionsTemplate={searchExternalPromotionsTemplate}
></SearchstaxExternalPromotionsWidget>
Sorting Widget
example of sorting widget initialization with minimum options
<SearchstaxSortingWidget></SearchstaxSortingWidget>
example of sorting widget initialization with template override
function searchSortingTemplate(
sortingData: null | ISearchstaxSearchSortingData,
orderChange: (value: string) => void,
selectedSorting: string
) {
return (
<>
{sortingData && sortingData?.searchExecuted && sortingData?.hasResultsOrExternalPromotions && (
<div className="searchstax-sorting-container">
<label className="searchstax-sorting-label" htmlFor="searchstax-search-order-select">
Sort By
</label>
<select
id="searchstax-search-order-select"
className="searchstax-search-order-select"
value={selectedSorting}
onChange={(e) => {
orderChange(e.target.value);
}}
>
{sortingData.sortOptions.map(function (sortOption) {
return (
<option key={sortOption.key} value={sortOption.key}>
{sortOption.value}
</option>
);
})}
</select>
</div>
)}
</>
);
}
<SearchstaxSortingWidget searchSortingTemplate={searchSortingTemplate}></SearchstaxSortingWidget>
Template overrides
Templates use vue slots.
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