Skip to main content Accessibility Feedback

How to create your own search API for a static website with JavaScript and PHP (part 2)

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));
}

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.