Skip to content

Instantly share code, notes, and snippets.

@teppokoivula
Last active March 29, 2025 09:24
Show Gist options
  • Save teppokoivula/a3c9fe6080ab2207847170559a4b569c to your computer and use it in GitHub Desktop.
Save teppokoivula/a3c9fe6080ab2207847170559a4b569c to your computer and use it in GitHub Desktop.
Search Engine live AJAX search example
// this is an example of how to use the search engine module for ProcessWire to provide an AJAX
// "live search"; likely not a perfect solution, but should give you a general idea of how to
// set this type of functionality up :)
// see https://processwire.com/talk/topic/21941-searchengine/?do=findComment&comment=248152 for
// alternative solution using htmx
const searchForm = document.getElementById('se-form')
if (searchForm) {
const searchInput = searchForm.querySelector('input[name="q"]')
const searchCache = {}
let searchTimeout
let searchResults
const findResults = () => {
window.clearTimeout(searchTimeout)
searchTimeout = window.setTimeout(() => {
if (searchResults) {
searchResults.setAttribute('hidden', 'true')
}
if (searchInput.value.length > 2) {
if (searchCache[searchInput.value]) {
renderResults(searchForm, searchCache[searchInput.value])
return
}
if (searchInput.hasAttribute('data-request')) {
return
}
searchInput.setAttribute('data-request', 'true')
searchInput.setAttribute('disabled', 'true')
const searchParams = new URLSearchParams()
searchParams.append('q', searchInput.value)
fetch(`${searchForm.getAttribute('action')}?${searchParams}`, {
headers: {
// set the request header to indicate to ProcessWire that this is an AJAX request; this
// way we can check $config->ajax in the template file and return JSON instead of HTML
// by calling $modules->get('SearchEngine')->renderResultsJSON()
'X-Requested-With': 'XMLHttpRequest',
},
})
.then((response) => {
if (!response.ok) {
throw new Error('Network response was not ok')
}
return response.json()
})
.then((data) => {
searchCache[searchInput.value] = data
renderResults(searchForm, data)
searchInput.removeAttribute('data-request')
searchInput.removeAttribute('disabled')
searchInput.focus()
})
.catch((error) => {
console.error('Error fetching search results:', error)
searchInput.removeAttribute('data-request')
searchInput.removeAttribute('disabled')
searchInput.focus()
})
}
}, 300)
}
const maybeHideResults = () => {
if (searchResults) {
window.setTimeout(() => {
if (!searchResults.querySelector(':focus')) {
searchResults.setAttribute('hidden', 'true')
}
}, 100)
}
}
const hideResults = () => {
if (resultsContainer) {
resultsContainer.setAttribute('hidden', 'true')
}
}
const renderResults = (form, data) => {
searchResults = document.getElementById('search-results')
if (!searchResults) {
searchResults = document.createElement('ul')
searchResults.id = 'search-results'
searchResults.addEventListener('focusout', maybeHideResults)
form.insertAdjacentElement('afterend', searchResults)
}
searchResults.innerHTML = ''
if (data.results.length > 0) {
data.results.forEach((item) => {
const resultItem = document.createElement('li')
resultItem.innerHTML = `<a href="${item.url}">${item.title}</a>`
searchResults.appendChild(resultItem)
})
searchResults.removeAttribute('hidden')
} else {
searchResults.innerHTML = '<li>No results found</li>'
searchResults.removeAttribute('hidden')
}
}
searchInput.addEventListener('keyup', findResults)
searchInput.addEventListener('focus', (event) => {
if (searchInput.value.length > 2) {
findResults(event)
}
})
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
hideResults(event)
}
})
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment