Yesterday, I shared how I created my own search API for my static website with PHP. Today, I'm going to share how I connect to that API in the browser using JavaScript.
Let's dig in!
The search form
In my HTML, I include a basic search form.
By default, the form actually makes a request to DuckDuckGo.com
, with results restricted to GoMakeThings.com
. This way, if the JavaScript fails, users can still search.
<form action="https://duckduckgo.com/" method="get"> <label for="input-search">Enter your search criteria:</label> <input type="text" name="q" id="input-search"> <input type="hidden" name="sites" value="gomakethings.com"> <button> Search </button> </form>
As part of this update, I decided to switch from traditional DOM manipulation to a web component.
I wrap my form
in a search-form
custom element, with an api
attribute that points to the search API endpoint.
<search-form api="/path/to/search.php"> <form action="https://duckduckgo.com/" method="get"> <!-- ... --> </form> </search-form>
There are certainly other ways you can do this, but I absolutely love Web Components for DOM manipulation.
The Web Component
First, I define a new search-form
custom element using the customElements.define()
method. I also pass in a class
that extends
the HTMLElement
, which is how you create a new Web Component.
customElements.define('search-form', class extends HTMLElement { // ... });
Next, I create a constructor()
method to instantiate each custom element. As is required, I run the super()
method first to gain access to the inherited class properties.
customElements.define('search-form', class extends HTMLElement { /** * The class constructor object */ constructor () { // Always call super first in constructor super(); } });
Now, I need to setup the Web Component basics.
I get the [api]
attribute value, and save it as a property of the component. If one isn't defined, I end the constructor()
early, since the Web Component can't do anything without it.
/** * The class constructor object */ constructor () { // Always call super first in constructor super(); // Define properties this.api = this.getAttribute('api'); if (!this.api) return; }
Next, I get the form
element and save that as a property. Then, I create two div
elements using the document.createElement()
method.
The first, this.notify
, is where I'll display status notifications while the script is doing things and the UI updates. I add a role
of status
on this one so that screen readers will announce changes to the text inside it.
The second, this.results
, is where I'll render the search results once I get them back from the API.
I use the Element.append()
method to inject both of them into my Web Component (this
) after the form
.
/** * The class constructor object */ constructor () { // Always call super first in constructor super(); // Define properties this.api = this.getAttribute('api'); if (!this.api) return; this.form = this.querySelector('form'); this.notify = document.createElement('div'); this.results = document.createElement('div'); // Generate HTML this.notify.setAttribute('role', 'status'); this.append(this.notify, this.results); }
Adding interactivity
In the constructor
, I use the Element.addEventListener()
method to listen for submit
events.
I pass in this
, the Web Component itself, as my second argument. This allows me to use the handleEvent()
method, a very elegant way of managing event listeners in Web Components.
/** * The class constructor object */ constructor () { // Always call super first in constructor super(); // ... // Listen for events this.form.addEventListener('submit', this); }
Inside the handleEvent()
method, I run the event.preventDefault()
method to stop the form from actually submitting to Duck Duck Go.
Then, I pass the form
into the new FormData()
method, and pass the returned FormData
object into a search()
method that will actually run the search tasks.
/** * Handle events */ handleEvent (event) { event.preventDefault(); this.search(new FormData(this.form)); }
Running the search
Because I'm calling an API and I've grown quite fond of async
and await
, I use the async
keyword to define my search()
method as asynchronous.
The, I create a try...catch()
block to call the API and catch any errors that happen.
If there's any sort of error, I'll display an error message inside my notify
element, which will announce the error to screen readers as well as displaying visually in the UI.
/** * Search the API * @param {FormData} query The form data to search form */ async search (query) { try { // ... } catch (error) { this.notify.innerHTML = '<p>Sorry, no matches were found.</p>'; } }
Next, I display a "Search…" message in the notify
element, and wipe the contents of the results
element (in case there was a previous search with results already displayed).
Then, I use the fetch()
method (with the await
keyword) to call the search api
.
I use POST
as the method, and pass along the provided query
(the FormData
object we created in the handleEvent()
method) as the body
.
try { // Show status message this.notify.innerHTML = '<p<em>Searching...</em></p>'; this.results.innerHTML = ''; // Call the API let response = await fetch(this.api, { method: 'POST', body: query }); } catch (error) { this.notify.innerHTML = '<p>Sorry, no matches were found.</p>'; }
Once the API responds, I use the Response.json()
method to get the JSON data object from it (again using the await
keyword, since this is an async method).
If the response
is not ok
or there are no results
to display, I throw
the results
, which will trigger the catch()
method to run.
Otherwise, I display a message in the notify
element about how many results were found. This causes an announcement for screen reader users letting them know.
Then, I render the search results
into the this.results
element. I created a createResultsHTML()
function for that, just to keep things a bit more neat-and-tidy.
// Call the API let response = await fetch(this.api, { method: 'POST', body: query }); // Get the results let results = await response.json(); // If there aren't any, show an error if (!response.ok || !results.length) throw results; // Render the response this.notify.innerHTML = `<p>Found ${results.length} matching items</p>`; this.results.innerHTML = this.createResultsHTML(results);
Rendering the search results into the HTML
My favorite way to create an HTML string from an array of data is to use the Array.prototype.map()
and Array.prototype.join()
methods.
A lot of my students prefer to use a forEach()
or for...of
loop and push items into a new array, and that's fine to. Use whatever approach works best for you!
Inside my createResultsHTML()
method, I run the Array.prototype.map()
method on my results
array, then join()
the resulting array of HTML strings. I return
the result.
/** * Create the markup for results * @param {Array} results The results to display * @return {String} The results HTML */ createResultsHTML (results) { return results.map(function (article) { // An HTML string for the article will get created here... return ''; }).join(''); }
Inside the map()
method's callback function, I generate the HTML for each specific article
that was returned by the API.
Your HTML structure will vary based on how you want your results to look.
I include the article's type
and publication date
. I display the title
as a link that points to the article url
. And I show a short summary
, which I restrict to 150
characters using the String.prototype.slice()
method.
/** * Create the markup for results * @param {Array} results The results to display * @return {String} The results HTML */ createResultsHTML (results) { return results.map(function (article) { 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(''); }
Moar features!!!
On day 1, this was what my search Web Component looked like.
Since then, I added the ability to filter the results by type
. If the user gets back hundreds of results, they can filter the results to only show articles, or courses, or items from the toolkit. Once I migrate podcasts here, that will be a filter as well.
I also update the search page URL to include a query string with their search parameter.
This can be used to bookmark searches or deep-link to them. When the page loads, I check for that query string and automatically run a search if it exists.
Tomorrow, I'll write about how I added both of those features.
And if you need help added a search component to your site, or want to explore how Web Components can make your website architecture easier-to-manage, get in touch! I'd love to help you out.
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 2)"