Skip to content

Instantly share code, notes, and snippets.

@PatD
Created October 20, 2020 10:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save PatD/b0b0ad2c2a2cbc07536aacb428e45223 to your computer and use it in GitHub Desktop.
Save PatD/b0b0ad2c2a2cbc07536aacb428e45223 to your computer and use it in GitHub Desktop.
Custom search interface with SharePoint REST API, Axios.js, Fuse.js, and Handlebars.js
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="">
</head>
<body>
<!-- This div wraps the search interface -->
<div id="clearSearchInterface">
<!-- Seach box-->
<input id="searchQueryInputText" class="fuzzy-search" placeholder="Search" style="min-width: 300px;padding: 10px;margin-right: 10px;" />
<button id="searchButton">Search</button>
<hr />
<em>There are <strong id="resultCounter">&nbsp;&nbsp;&nbsp;</strong> <span id="resultText">Lessons & Best Practices</span>.</em>
<div class="list ms-srch-group-content" id="resultListing">
<!-- loading spinner gets replaced with Handlebars content-->
<img src="/_layouts/15/images/gears_an.gif" />
</div>
</div>
<script src="fuse.js"></script>
<script src="handlebars-v4.0.10.js"></script>
<script src="/axios.min.js"></script>
<!--
High-level: What's happening here:
1. Data is loaded from SharePoint lists via REST API calls, and stored in an array
2. That data is being presented using Handlebars.js templates.
3. A user interface allows entering a query, which is ran against FUSE.js fuzzy search.
4. The Handlebars.js display is updated with these seach results, usually a subset of data
-->
<!-- HandleBars Templates live in here:-->
<script id="result-template" type="text/x-handlebars-template">
<!-- For each returned record, put it in an <div> tag, and call it 'this' -->
{{#each this}}
<div class='ms-srch-item'>
<div class='ms-srch-item-body'>
{{#ifEquals this.Title "Lessons Learned"}}
<div class='ms-srch-item-title'>
<h3 class="name" title="{{{ this.ID }}}">
<a class="ms-srch-item-link" href="/yoursite/DispForm.aspx?ID={{{ID}}}">{{this.Title}} #{{ID}}</a>
</h3>
</div>
<div class="shortdesc ms-srch-item-summary">
{{ trimString this.Describe_x0020_the_x0020_issue_x }}
<em> (Submitted on {{ this.Created }})</em>
</div>
<div class="ms-srch-item-summary">
<!-- If a county is listed -->
{{#if this.County}}
<em>County:</em> <strong class="county">{{this.County}}</strong> |
{{/if}}
<!-- If a county is listed -->
{{#if this.Region}}
<em>Region:</em> <strong class="county">{{this.Region}}</strong> |
{{/if}}
<!-- If an office is listed -->
{{#if this.Office}}
<em></em>Office:</em> <strong class="office">{{this.Office}}</strong>
{{/if}}
</div>
<!-- Applicable Disciplines -->
{{#if this.Applicable_x0020_Disciplines}}
<div class="ms-srch-item-summary">
<em>Applicable Disciplines:</em>
{{#each Applicable_x0020_Disciplines}}
<strong class='displinetag'>{{ this.Title }}</strong>
{{#if @last}}
{{else}}
|
{{/if}}
{{/each}}
</div>
{{/if}}
<br />
<hr />
{{/ifEquals}}
</div>
</div>
{{/each}}
</script>
<script>
// How many results are available?
var resultCounter = document.getElementById('resultCounter');
var resultText = document.getElementById('resultText');
// Search Result listing live in here:
var resultListing = document.getElementById('resultListing');
// Axios-Loaded REST data stored in this array:
var returnedResults = [];
// Helper function to remove SharePoint weirdness:
// SharePoint returns some pretty markup-filled nested results. This cleans that up
function cleanReturnedResults() {
returnedResults.forEach(function (_loadedResult) {
if(_loadedResult.Created != undefined){
var _created = new Date(_loadedResult.Created);
_loadedResult.Created = _created.toLocaleDateString()
};
if (_loadedResult.Describe_x0020_the_x0020_issue_x != undefined) {
_loadedResult.Describe_x0020_the_x0020_issue_x = _loadedResult.Describe_x0020_the_x0020_issue_x.replace(/(<([^>]+)>)/gi, "");
};
if (_loadedResult.Best_x0020_Practice_x0020_descri != undefined) {
_loadedResult.Best_x0020_Practice_x0020_descri = _loadedResult.Best_x0020_Practice_x0020_descri.replace(/(<([^>]+)>)/gi, "")
};
if (_loadedResult.Solution_x0020_to_x0020_solve_x0 != undefined) {
_loadedResult.Solution_x0020_to_x0020_solve_x0 = _loadedResult.Solution_x0020_to_x0020_solve_x0.replace(/(<([^>]+)>)/gi, "")
};
if (_loadedResult.Examples_x0020_of_x0020_solution != undefined) {
_loadedResult.Examples_x0020_of_x0020_solution = _loadedResult.Examples_x0020_of_x0020_solution.replace(/(<([^>]+)>)/gi, "")
};
})
}
// Perform a search using FUSE.js plugin
function buildSearch() {
// Indicate which fields are searchable:
var searchFuseOptions = {
// isCaseSensitive: false,
// includeScore: false,
// shouldSort: true,
// includeMatches: false,
// findAllMatches: false,
minMatchCharLength: 3,
// location: 0,
// threshold: 0.2, // <- Slide this number around to toggle specificty
// distance: 100,
// useExtendedSearch: false,
// ignoreLocation: false,
// ignoreFieldNorm: false,
keys: ['Title', 'County', 'Region', 'Office', 'Describe_x0020_the_x0020_issue_x', 'Solution_x0020_to_x0020_solve_x0', 'Applicable_x0020_Disciplines.Title']
};
var fuse = new Fuse(returnedResults, searchFuseOptions);
// Fuse looks at teh searchQuery URL and adds those results to an array
var _returnedFuseResults = fuse.search(searchQuery)
// Transform resturned results into a format for Handlebars.js
var _fuseResultsforHandlebars = []
_returnedFuseResults.forEach(function (result) {
_fuseResultsforHandlebars.push(result.item)
});
// Output to the console for debugging
console.log("These are the " + _fuseResultsforHandlebars.length + " results sent to our Handlebars template")
// console.log(_fuseResultsforHandlebars)
if(_fuseResultsforHandlebars.length === 0){
document.getElementById('resultListing').innerHTML = "<strong>No results found, please try another search</strong>";
} else{
// Update Handlebars Template in search
var resultTemplate = document.getElementById('result-template').innerHTML; // Template location
var compiled_template = Handlebars.compile(resultTemplate);
var rendered = compiled_template(_fuseResultsforHandlebars);
//Inject our template-generated HTML onto the page
document.getElementById('resultListing').innerHTML = rendered;
}
// Update the result counter listing
resultCounter.innerHTML = Object.keys(_fuseResultsforHandlebars).length;
resultText.innerHTML = "results"
}
// Hanldebars Helper functions:
// If "one or another"
Handlebars.registerHelper('ifEquals', function (arg1, arg2, options) {
return (arg1 == arg2) ? options.fn(this) : options.inverse(this);
});
// Removes HTML markup from specific strings
Handlebars.registerHelper('trimString', function (passedString) {
var theString = passedString.replace(/(<([^>]+)>)/gi, "");
// var theString = theString.substring(0,150);
return new Handlebars.SafeString(theString)
});
// Build initial listing on page load
function buildResultListing(displayResults) {
// Activate our Handlebars Template
var resultTemplate = document.getElementById('result-template').innerHTML; // Template location
var compiled_template = Handlebars.compile(resultTemplate);
var rendered = compiled_template(displayResults);
//Inject our template-generated HTML onto the page
document.getElementById('resultListing').innerHTML = rendered;
// Update the result counter listing
resultCounter.innerHTML = Object.keys(displayResults).length;
}
// Runs Axios queries to load data from lists.
var acceptedRest = "/_api/Web/Lists(guid'#######################')/Items?$select=*,Applicable_x0020_Disciplines/Title&$expand=Applicable_x0020_Disciplines/Title"
var bestpracticeREST = "/_api/Web/Lists(guid'##########################')/Items?$select=*,Applicable_x0020_Disciplines/Title&$expand=Applicable_x0020_Disciplines/Title"
var requestOne = axios.get(acceptedRest);
var requestTwo = axios.get(bestpracticeREST);
axios
.all([requestOne, requestTwo])
.then(
axios.spread(function (...responses) {
var responseOne = responses[0];
var responseTwo = responses[1];
// Add loaded data to the existing returnedResults[] array
for (var i = 0; i < responseOne.data.value.length; i++) {
returnedResults.push(responseOne.data.value[i])
};
for (var i = 0; i < responseTwo.data.value.length; i++) {
returnedResults.push(responseTwo.data.value[i])
};
})
).then(function () {
console.log('These are the returned results: ')
console.log(returnedResults)
console.log("REST calls Complete")
// All calls complete, run a funciton to start our search interface:
cleanReturnedResults()
buildResultListing(returnedResults);
})
.catch(function (errors) {
// react on errors.
alert('There was an error connecting to Search. Please refresh and try again!')
console.error(errors);
});
// Add interactivity to search interface!
// Varible to contain search text string
var searchButton = document.getElementById('searchButton');
var searchQueryInputText = document.getElementById('searchQueryInputText');
var searchQuery;
// Handle Enter key
searchQueryInputText.addEventListener("keypress", function (e) {
if (e.key === 'Enter') {
// Get value from input field and assign to variable
searchQuery = searchQueryInputText.value;
buildSearch()
}
})
// Handle button click
searchButton.addEventListener("click", function (e) {
// Button has been clicked, so get it's value and assign to variable
searchQuery = searchQueryInputText.value;
buildSearch();
e.preventDefault();
})
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment