ApiBlaze: UI-Interactions for Searching APIs

aze is a tool to explore API specifications: Search for a keyword, filter for objects, properties, or endpoints, and immediately see descriptions and code examples. ApiBlaze helps you to answer a specific question about an API lightning fast. You can try it here: apiblaze.admantium.com.

In the last article, I presented the static design of ApiBlaze index page: A centered search bar and the popup that shows search results. In this article, the concrete search function and user interaction functionality is implemented. Interaction happens immediately. After every keystroke, a search is triggered, and the best matching results are shown in the popup. Then, by using the arrow keys, the search results can be browsed and highlighted. And when the enter key is pressed, or one of the results of clicked, the selected API will be loaded in a new page.

This article originally appeared at my blog.

Handling Input in the Search Bar

Both events are cancelable, you can prevent the default behavior, e.g. not updating the input fields value. The difference is how the pressed key is passed to the listener: In keydown, it is a character string, and in keyup, it is a numeric code.

Since we need to handle non-character input like the arrow keys, we will use the keyup event and and an event listener to the input field as shown in the following example.

document
.querySelector('#api-search-query')
.addEventListener('keyup', e => this.handleKeyUp(e))

When the event is triggered, the custom method handleKeyUp() will be called. This method uses a switch-case statement to process the event. It detects which key was pressed, and handles it accordingly. The default case is to update its state and trigger the search. See below.

handleKeyUp (e) {
switch (e.keyCode) {
// ...
default:
this.updateState({ apiSearchQuery: e.target.value })
this.triggerSearch()
break
}
}

Now lets see how the search results are shown.

Showing Search Reults in the Popup

The popup component traverses the list of search results and renders its HTML representation. When multiple results are shown, the user can navigate between the entries using the arrow keys. But how to track which item is selected, and which item should be selected next?

HTML lists have a special property: The tabindex. This index is a unique identifier for each item. When a key event is triggered in this list, we need to determine the tab index from the currently selected item, and then determine the next item in the chosen order (backward and forward). We also need to check for index out of bounds.

Let’s start coding. First, we add the tabindex to all items in the popup.

Object.entries(apiElementsSearchResult).forEach((value, key) => {
const { name, containingObject, type, description } = object
var html = `
<div class="search-result" tabindex="${key}" ref="${name}">
//...
</div>`
this.addDom('#api-search-results', html)
}

For handling the arrow key presses, we again use the keyup event and define a custom handleKeyUp() function to be called. In this function, we differentiate the cases when the arrow up or arrow down is pressed.

handleKeyUp (e) {
switch (e.keyCode) {
case 40: // Arrow down
e.preventDefault()
this.focusNextElement(1)
break
case 38: // Arrow up
e.preventDefault()
this.focusNextElement(-1)
break
// ...
}
}

Both cases call the function focusNextElement() with a number. The logic to determine the very next selected element is summarized as follows. When arrow up is pressed, increase the currently selected index by one, but not beyond the list length. And when arrow down is pressed, decrease the selected index by one, but do not go beyond 0. Then, apply focus() to the element with a tab index equal to the currently selected index.

focusNextElement (incr) {
const { min, current, max } = this.tabIndex
const next = current + incr
if (next <= min) {
this.tabIndex.current = min
} else if (next >= max) {
this.tabIndex.current = max
}
document.querySelector(`.search-result[tabIndex="${next}"]`).focus()
}

Switch between Search Bar and Search Results

We need to change two things. First, if the arrow down is pressed in the search bar, and search results are not empty, focus the first child of the search results. Second, if arrow up is pressed in the search results and the next index is -1, switch the focus to the search bar.

The first change is to extend the keyup event of the search bar for the arrow down.

handleKeyUp (e) {
switch (e.keyCode) {
case 40: // Arrow down
e.preventDefault()
const searchResultDom = document.querySelector('div.search-result')
searchResultDom && searchResultDom.focus({ preventScroll: false })
break
//...
}
}

The change for the search results is only a slight modification: In focusNextElement(), we need to add a new case to handle when next would be equal to the value -1.

focusNextElement (incr) {
const { min, current, max } = this.tabIndex
const next = current + incr
if (next == -1) {
document
.querySelector('#api-elements-search-query')
.focus({ preventScroll: false })
return
}
if (next <= min) {
this.tabIndex.current = min
} else if (next >= max) {
this.tabIndex.current = max
}
document.querySelector(`.search-result[tabIndex="${next}"]`).focus()
}

With these changes, we obtain a full navigable presentation of the search bar and search results.

Review: ApiBlaze Project Requirements

  • SEA02 — Show search results in a popup
  • SEA03 — Select a search result with arrow keys, enter and mouse click

Overall, following requirements are completed:

Searching for APIS

  • ✅ SEA01 — Search for APIs by Keyword
  • ✅ SEA02 — Show search results in a popup
  • ✅ SEA03 — Select a search result with arrow keys, enter and mouse click

Framework

  • ✅ FRAME01 — Controller & Routing
  • ✅ FRAME02 — Stateful Pages & Components
  • ✅ FRAME03 — Actions
  • ✅ FRAME04 — Optimized Bundling

Technologies

  • ✅ TECH01 — Use PlainJS & Custom Framework
  • ✅ TECH02 — Use SAAS for CSS

Conclusion

IT Project Manager & Developer