ApiBlaze: UI-Interactions for Searching APIs
Tags: javascript, openapi
Published: 2021-02-01 05:40:47


_ApiBlaze 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](https://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](https://admantium.com/blog/api06_search_api_interactions/)_.

## Handling Input in the Search Bar

A search is triggered when any new letter or number is entered in the search bar. The search bar is an input field. In JavaScript, you can use two different event types to be notified about changes in an input field: The `keydown` event is triggered when a key is pressed, and the `keyup` event triggers when the key is released.

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.

```js
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.

```js
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

When the search is triggered, the current term in the search bar will be passed to the backend, which returns a list of matches. These matches are stored in the page state. All components will be refreshed, including the popup component.

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]( https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/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.

```js
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.

```js
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.

```js
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

The final interactivity is to move between the search bar and the search results popup by using the arrow keys.

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.

```js
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.

```js
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

With the additions in this article, the remaining two ApiBlaze requirements for search API specifications are fulfilled:

- 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

The search bar is a visually and conceptually central element of ApiBlaze: the primary element for user interactions and displaying the results. This article explained how interactions in the search bar API spec are implemented. Every keystroke in the search bar triggers an update of the apps state and sends a search request to the backend. Because of the WebSocket connection, search results are returned to the client and rendered instantaneous. In these results, the user can browse via the arrow keys, and load any entry by hitting enter. The key is effective use of JavaScript key handlers.