On Tuesday, I shared how I created my own search API for my static website with PHP. And yesterday, I walked you through how I built a custom web component to call the API and render search results.
Today, I wanted to wrap up the series by sharing how I added two features:
- Filtering search results by type.
- Custom URLs for search queries and bookmarking.
Let's dig in!
Filtering search results by type
For this feature, I first added a createFilterHTML()
method.
It accepts an object of types
that exist in the search results, and how many results are that type. It might look like this…
let types = { Articles: 42, Courses: 3, Toolkit: 7 };
Inside my function use the Object.keys()
method to get the keys from the types
object. If there are none, I return
an empty string (''
).
Otherwise, I return an HTML string with a collection of checkbox
inputs, one for each type
. I have mine styled as an inline list, but you can style them however you want.
Each input has a name
property of search-filter
, it's value
is the type
, and it's checked
by default.
/** * Create the filter HTML * @param {Object} types The content types * @return {String} The HTML string */ createFilterHTML (types) { let keys = Object.keys(types); if (keys.length < 1) return ''; return ` <fieldset> <legend>Filter results by type</legend> ${keys.sort().map(function (type) { let count = types[type]; return ` <label> <input type="checkbox" name="search-filter" value="${type}" checked>${type} (${count}) </label>`; }).join('')} </fieldset>`; }
Next, I updated my createResultsHTML()
function.
First, I created a types
object ({}
) to hold my type data. Instead of immediately returning my HTML string, I assign it to a variable.
Inside the Array.prototype.map()
callback function, I either add the article.type
to my types
object with a value of 1
, or increase the type count by 1
. This is how I track how many of each type are in the results.
Finally, I pass my types
object into the createFilterHTML()
method, and return the resulting string and my html
string for the results.
/** * Create the markup for results * @param {Array} results The results to display * @return {String} The results HTML */ createResultsHTML (results) { let types = {}; let html = results.map(function (article) { types[article.type] = types[article.type] ? types[article.type] + 1 : 1; return ` <div> <aside> <strong>${article.type}</strong> - <time datetime="${article.datetime}" pubdate>${article.date}</time> </aside> <h2> <a href="${article.url}">${article.title}</a> </h2> ${article.summary.slice(0, 150)}... </div>`; }).join(''); return this.createFilterHTML(types) + html; }
Now, the filters are displayed in the UI.
Hiding and showing search results when filters change
Next, I updated my handleEvent()
method. Instead of just running my search code, I dynamically run an on*
method on my Web Component instance.
For example, a submit
event would trigger the onsubmit()
method to run.
/** * Handle events */ handleEvent (event) { this[`on${event.type}`](event); }
Then, I copy/pasted the code that was previously in the handleEvent()
method into an onsubmit()
method.
/** * Handle submit events */ onsubmit (event) { event.preventDefault(); this.search(new FormData(this.form)); }
Back in my createResultsHTML()
method, I added a [data-search-type]
attribute to each search result, with the article.type
as its value.
return ` <div data-search-type="${article.type}"> ... </div>`;
Then, I created an oninput()
event to handle changes to my filters.
In this method, I use the Element.querySelectorAll()
method to get all of the [name="search-filter"]
items that are :checked
. Then I use the Array.from()
, Array.prototype.map()
, and Array.prototype.join()
methods to create a [data-search-type="*"]
selector string, where *
is the type of item that should be visible but is currently [hidden]
.
/** * Handle checkbox changes */ oninput (event) { // Get the values to show and hide let show = Array.from(this.results.querySelectorAll('[name="search-filter"]:checked')).map(input => `[data-search-type="${input.value}"][hidden]`).join(','); }
For example, let's say Articles
and Toolkit
are checked, but Courses
is not.
The resulting show
string would look like this…
let show = '[data-search-type="Articles"][hidden], [data-search-type="Toolkit"][hidden]';
I repeat this process, this time looking for filters that are :not(:checked)
, and getting the matching search results that are :not([hidden])
.
/** * Handle checkbox changes */ oninput (event) { // Get the values to show and hide let show = Array.from(this.results.querySelectorAll('[name="search-filter"]:checked')).map(input => `[data-search-type="${input.value}"][hidden]`).join(','); let hide = Array.from(this.results.querySelectorAll('[name="search-filter"]:not(:checked)')).map(input => `[data-search-type="${input.value}"]:not([hidden])`).join(','); }
If there are items to show
, I pass the select into the Element.querySelectorAll()
method. Then, I use a for...of
loop to loop through them, and the Element.removeAttribute()
method to remove the [hidden]
attribute.
I do the same thing for items that I should hide
, this time adding the [hidden]
attribute with the Element.setAttribute()
method.
// Show hidden elements if (show) { for (let elem of this.results.querySelectorAll(show)) { elem.removeAttribute('hidden'); } } // Hide visible elements if (hide) { for (let elem of this.results.querySelectorAll(hide)) { elem.setAttribute('hidden', ''); } }
One final touch to make this work: in my constructor()
, I add an input
listener on the this.results
element.
// Listen for events this.form.addEventListener('submit', this); this.results.addEventListener('input', this);
Updating the URL
Inside the search()
function, I use the history.pushState()
method to update the URL without reloading the page.
I pass in an empty object for the state
, though if your app actually uses browser state, you could pass in history.state
instead. I pass in an empty string for the deprecated second argument, and the current URL with ?s
and the query value as the query string parameter.
/** * Search the API * @param {FormData} query The form data to search form */ async search (query) { try { // Show status message this.notify.innerHTML = '<p<em>Searching...</em></p>'; this.results.innerHTML = ''; // Update the URL history.pushState({}, '', window.location.origin + window.location.pathname + '?s=' + query.get('q')); // ... } catch (error) {} }
Now, I have a URL query string parameter I can check for when the page loads to automatically run a search.
I created one last method, onload()
. In it, I use the new URLSearchParams()
object to get the value of the s
query string from the window.location.search
property.
If no query
exists, I can return
to end early.
/** * If there's a query string search term, search it on page load */ onload () { let query = new URLSearchParams(window.location.search).get('s'); if (!query) return; }
Otherwise, I create a new FormData()
object, and assign my query
to the q
property (the same as if someone had typed it into the form).
I pass the formData
into the search()
method to kick off a call to the search API. Then, I get the search input field and update its value to the query
.
/** * If there's a query string search term, search it on page load */ onload () { let query = new URLSearchParams(window.location.search).get('s'); if (!query) return; let formData = new FormData(); formData.set('q', query); this.search(formData); let input = this.form.querySelector('[name="q"]'); input.value = query; }
Now, when someone visits a search page they had bookmarked, the site automatically displays the saved search query results.
Want to build cool stuff like this?
I can help!
I offer consulting services to help developers and developer teams write code that's faster, simpler, and easier to maintain.
I also teach developers how to build a simpler web through courses and workshops.
Feel free to reach out with any questions or comments.
Cheers,
Chris
Want to share this with others or read it later? View it in a browser.
0 Komentar untuk "[Go Make Things] How to create your own search API for a static website with JavaScript and PHP (part 3)"