ApiBlaze: SPAC Framework Refactoring
Tags: javascript
Published: 2021-01-18 06:38:31

_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).**_

Having finished the implementation of my custom JavaScript framework SPAC (Statefull Pages, Actions and Components) and exploring OpenAPI specifications and tools in my last two posts, this article continues the ApiBlaze series. I will explain how to refactor the early prototype to a SPAC app.

_This article originally appeared at [my blog](https://admantium.com/blog/api04_refactor/)_.

## Phase 0: Setup

- Install the spac NPM package
- Create the following directory structure

```bash
src
└── actions
│   └──  SearchApiSpecAction.js
└── components
    ├── ApiSearchComponent.js
    └── ApiSearchResultsComponent.js
└── pages
│   ├── IndexPage.js
│   ├── SearchApiElementsPage.js
└── index.js
```

- Create an `index.js` file with the following content:

```js
import { Controller } from 'spac'

const controller = new Controller()

controller.init()
```

The controller starts the app, but since no components, actions or pages are provided, it displays nothing. Lets’ refactor each of these sequentially.

## Phase 1: Refactor Components

In the early prototype, components had quite a lot code duplication. By moving to SPAC, this duplication is eliminated, and the remaining code is more compact.

Here is an example of the API search bar component. It includes the redundant functions `updateState()`, `getState()` and `refresh()`. Also, all these functions need to be explicitly exported.

```js
import { handleApiSearch } from '../controller.js'

let state = {}
let _$root = undefined

function updateState (newState) {
  state = { ...state, ...newState }
  console.log('new state', state)
}

function getState () {
  return state
}

function render (args) {
  const html = `
    <h2>Load an API</h2>
    <input type="text" class="api-search-bar" id="api-search-bar" value="" spellcheck="false">
    <div id="api-search-results" class="api-search-results">
  `
  return html
}

function mount ($root, ...args) {
  _$root = $root
  $root.innerHTML = render(args)

  document
    .getElementById('api-search-bar')
    .addEventListener('keydown', e => handleKeydown(e))
  // ...
}

function refresh (args) {
  mount(_$root, args)
}

export { mount, getState, refresh }
```

In the refactored component, only two main functions remain: `render()` and `mount()`. Other essential methods are all defined in the `Component` parent class and don't need to be repeated here.

```js
import { Component } from 'spac'

export default class SearchBarComponent extends Component {
  render = () => {
    return `
      <h2>Load an API</h2>
      <input type="text" class="api-search-bar" id="api-search-bar" value="${this.getState().apiSearchQuery}" spellcheck="false">
      <div id="api-search-results" class="api-search-results">
    `
  }

  mount () {
    super.mount()
    document
      .querySelector('#api-search-query')
      .addEventListener('keyup', e => this.handleKeyUp(e))
  }
}
```

The basic steps are:

- Create a class that extends `Components`
- Move the content of the `html` constant to the `render()` functions
- Move behavioral logic from the `mount()` function to the `mount()` instance function

## Phase 2: Refactor Actions

Actions are a special case, because there were no similar abstractions in the earlier prototype. Therefore, refactoring means...

- Identify all external API function calls and calls to the backend
- Encapsulate the call in an action
- In the dependent component, import and execute this action

## Phase 3: Refactor Pages

In the early prototype, pages had only few responsibilities: Rendering HTML, and mounting the rendered HTML and their component.

Here is the example of the index page.

```js
import { mount as mountSearchBar } from '../components/searchBar.js'

function layout () {
  const html = `
    <section class="search-wrapper" id="search-wrapper">
        <h2 id="heading-api-name">Search</h2>
        <div id="search-mode"></div>
        <div id="search-bar">
          <div class="input-wrapper" id="input-wrapper"></div>
        </div>
    </section>
 `

  return html
}

function render ($domRoot) {
  $domRoot.innerHTML = layout()

  mountSearchMode(document.getElementById('search-mode'))
}

export { render }
```

The refactored version includes the custom `constructor` function, and the methods `render()` and `mount()` are responsible for showing and rendering the HTML.

```js
import { Page } from 'spac'
import ApiSearchBarComponent from '../components/ApiSearchBarComponent.js'

export default class IndexPage extends Page {
  constructor (rootDom) {
    super(rootDom)
    this.addComponents(new ApiSearchBarComponent('#api-search-spec'))
  }

  render = () => {
    return `
      <h1>ApiBlaze Explorer</h1>
      <section class='api-search-page'>
          <div id='api-search-spec' class='api-search-spec'></div>
      </section>
    `
  }

  mount () {
    super.mount()
    document.querySelector('button').addEventListener('click', () => {
      // ....
    })
    return this
  }
}
```

The refactoring steps are therefore very simple:

- Move the pages HTML to the `render()` method
- Define additional DOM manipulations, like adding event handlers, in the `mount()` method

## Phase 4: Start and Host the Application

Before the application can be used, we need to do one final step: Creating an inventory of the application files, and passing it to the controller.

Add the following command to the `script` section in the `package.json`.

```json
"scripts": {
    "bootstrap": "node --input-type=module --experimental-modules --eval \"import {bootstrap} from 'spac'; bootstrap('./src')\""
  }
```

Run this command once. Then, modify the `index.js` to load the inventory file and to pass it in the configuration object to the controller.

```js
import inventory from './inventory.json'

const controller = new Controller({ inventory })
```

Now you can start your application.

## Review: ApiBlaze Project Requirements

With the refactoring completed, we have the following status with ApiBlaze requirements:

- Searching for APIS
  - ✅ SEA01 - Search for APIs by Keyword
  - ✅ SEA02 - Show search results in a popup
  - ✅ SEA03 - Select a search results 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

We can now continue with implementing the frontend components, and will start searching for APIs.

## Conclusion

This article outlines the refactoring of the early stage ApiBlaze prototype, based on individual JavaScript modules, to a version using the SPAC framework. The first step is the basic setup: Install the npm modules, create directories, add the `index.js`. Then, one by one, the components, actions and finally pages are refactored. Typically, you move all static HTML to the `render()` function, and all other DOM manipulations to the `mount()` functions. The final step is to create an `inventory.json` file, and then to load this file when creating SPAC controller instance.