SPAC: Controller Self-Initialization & Object API
Tags: javascript, framework
Published: 2020-11-23 06:55:10

_SPAC is a custom JavaScript framework for client-side, single-page web applications. It stands for "Stateful Pages, Actions and Components". Its design goal is to provide robust and simple entities that help you to structure apps. Pages and components provide the HTML, JavaScript functions and UI interactions. Actions govern external API calls. You define these entities in plain JavaScript, load up the central controller, and your app is ready to be served. Read the development journey of SPAC in my series: <https://admantium.com/category/spac-framework/>._

The core entity of SPAC is the controller: A self-initializing object that assembles your web application from its pages, actions and components. This article details the self-initialization phase, how it works, how it creates an internal objects API and its bindings to the browser.

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

## Ongoing Example: ApiBlaze Index Page

To explain the concepts in detail, we will use the example of ApiBlaze, an ongoing development project that enables blazing-fast searches in API descriptions. You can read more about ApiBlaze in the [project kickoff article](https://admantium.com/blog/api02_apiblaze_intro/).

ApiBlaze first screen consists of a search bar and a search result popup. When you execute a search, the appropriate action will be triggered. The directory layout for this screen is as follows:

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

Before starting the app, you need to provide an `inventory.json` file, which contains file links to all pages, actions and components. This file is generated by `npm run bootstrap`. For this example, it looks as follows:

```json
{
  "pages": ["/src/pages/IndexPage.js", "/src/pages/SearchApiSpecAction.js"],
  "components": [
    "/src/components/ApiSearchComponent.js",
    "/src/components/ApiSearchResultsComponent.js"
  ],
  "actions": ["/src/actions/SearchApiSpecAction.js"]
}
```

## Self-Initialization Process

The file `index.js` contains code for importing the controller and starting the self-initialization. Typically, it looks like this:

```js
import { Controller } from 'spac'
import inventory from './inventory.json'

const controller = new Controller({ inventory })

controller.init()
```

As you see, the controller is initialized by receiving the inventory, and then calling the async function `init()`.
During the initialization, the controller makes the following steps:

- For each file listed in the inventory, check...
  - That the file name conforms with the naming pattern (/.*Page.js/, /.*Action.js/ or \*Component.js/)
  - That the file exports a class of the appropriate type
- Each of these classes is added to an internal `Map` object:
  - `pagesMap`: Define entries with `route` and `obj` properties
  - `actionsMap`: Define entries with `obj` properties
  - `componentsMap`: Define entries with `obj` properties

Files that do not conform to the naming patterns, or files for which the type check fails, are ignored.

Let’s see the details by following an example. The following excerpt shows `init` method and how the `/pages` directory will be traversed.

```js
init () {
  this._initMap(Page, 'pages', /Page.js/)
  // ....
}

_initMap (parentClass, mapType, pattern) {
  this.inventory[mapType].forEach(async filePath => {
    try {
      if (!filePath.match(pattern)) {
        throw new Error()
      }
      const name = filePath.split('/').pop().replace(pattern, '')
      const clazz = (await import(`${filePath}`)).default

      if (clazz.prototype instanceof parentClass) {
        if (parentClass === Page) {
          const route = `/${name.replace(/([a-zA-Z])(?=[A-Z])/g, '$1-').toLowerCase()}`
          this[mapType].set(name, { route, clazz })
        } else {
          this[mapType].set(name, { clazz })
        }
      }
    } catch (e) {
      // ...
    }
  })
}
```

In this method:

- Line 2: The `init` function calls an internal helper `_initMap()`
- Line 6: For each file inside the inventory...
  - Line 8: ... check that it matches the given `pattern`
  - Line 11: ... attempt a dynamic import of the file
  - Line 13: ... check the file exports a class of the given `parentClass`
- Line 16/18: Store the name and an object containing the export in the given `mapType`

## Internal Objects API

When the initialization phase for the above mentioned example is completed, we obtain map objects that can be accessed and used in the application directly from the controller.

### Pages

The page map object:

```js
pages = {
  Index: {
    route: '/index',
    clazz: IndexPage()
  },
  SearchApiElements: {
    route: '/search_api_elements',
    clazz: SearchApiElementsPage()
  },
  SearchApiSpec: {
    route: '/search_api_spec',
    clazz: SearchApiSpecPage()
  }
}
```

Pages can be accessed with `controller.page('PageName')`, and the method `controller.display('PageName')` renders the page.

### Components

The components map object:

```js
components = {
  ApiSearch: {
    clazz: ApiSearchComponent()
  },
  ApiSearchResults: {
    clazz: ApiSearchResultsComponent()
  }
}
```

Components can be accessed with `controller.component('componentName)`. This method is used by `page` objects to fetch their components.

### Actions

The actions map object:

```js
actions = {
  SearchApiSpec: {
    clazz: SearchApiSpecAction()
  }
}
```

Actions are accessed `controller.action('ActionName')`.

## Assembling Pages

When designing pages objects, you can choose to either manually import your components, or access the component via the controller.

The manual import looks like this:

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

export default class IndexPage extends Page {
  render = () => {
    return `
      <h1>ApiBlaze Explorer</h1>
      <section class='api-search-page'>
          <div id='search-api-spec' class='search-api-spec'></div>
          <div id="search-api-results" class="search-api-results"></div>
      </section>
    `
  }

  constructor (rootDom) {
    super(rootDom)
    this.addComponents(
      new SearchBarComponent('#search-api-spec'),
      new SearchResultsComponent('#search-api-results')
    )
  }
}
```

Alternatively, the objects API can be used to import components (and actions). For this, you need to add the special method `_preloadComponents()` and pass an object with component names and their arguments, e.g. the `querySelector`.

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

export default class IndexPage extends Page {
  render = () => {
    return `<h1>Hello</h1>`
  }

  _preloadComponents = () => {
    return {
      SearchBarComponent: { querySelector: '#search-api-spec' },
      SearchResultsComponent: { querySelector: '#search-api-results' }
    }
  }
}
```

During initialization, the `Page` class will check if this special method is defined, and if yes, use the Controllers `component` method, to retrieve the class definition and create an instance of the particular component.

```js
class Page extends PageInterface {
  mount(querySelector) {
    super.mount(querySelector)
    // ...
    if (this._preloadComponents) {
      for (let [name, params] of this._preloadComponents()) {
        const instance = this.controller.component(name, params)
        this.components.set(name, instance)
      }
    }
  }
}
```

## Conclusion

This article explained how the controller's self-initialization phase works. First, we create an inventory of all pages, components and actions using the command `npm run bootstrap`. Then, when the controllers’ instance is created, it will use the inventory to define internal map objects that point to all defined classes. The controller rigorously checks that each file is named correctly and that it exports a class which is of type Page, Component or Action. Then, these map objects can be used to dynamically load the entities for assembling the pages.