Instantly share code, notes, and snippets.
Created
October 20, 2020 10:22
-
Star
0
(0)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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"> </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