SPAC: Controller Self-Initialization & Object API

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:

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.

Ongoing Example: ApiBlaze Index Page

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:

└── 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:

"pages": ["/src/pages/IndexPage.js", "/src/pages/SearchApiSpecAction.js"],
"components": [
"actions": ["/src/actions/SearchApiSpecAction.js"]

Self-Initialization Process

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.

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


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'PageName'), and the method controller.display('PageName') renders the page.


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 = {
SearchApiSpec: {
clazz: SearchApiSpecAction()

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

Assembling Pages

The manual import looks like this:

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>
constructor (rootDom) {
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.

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.

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


IT Project Manager & Developer