SPAC: Controller Implementation

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 JS, 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/.

In this article, I explain the central controller object. We cover the design goals, then discuss its features and implementation.

This article originally appeared at my blog.

Designing the Controller

  • Self initializing, single point of entry: The controller is the only JavaScript module that you need to load with a <script> tag from your index.html. From thereon, it handles the loading and unloading of all other JavaScript functions that are needed for the application.
  • Rendering Pages: The controller uses page classes for delivering the HTML to the clients. Pages expose a display() method that will render HTML and attach the output to the DOM. Pages themselves consist of static HTML and components that are added via mount().
  • Handling Actions: Actions are client-side external interactions like calling a backend or an external API. An action receives an args object and a updateState callback that receives the result of the action.

Now, let’s detail how the controller works.

Self-Initialization

To initialize your application, you need to do these steps:

  1. Create a script for loading the controller, e.g. in js/app.js
// js/app.js
import { Controller } from 'spacjs';
const controller = new Controller();controller.init();

2. Include this script in your index.html

<script src="js/controller.js" type="module"></script>

That is all.

Render Pages

.
└── pages
├── IndexPage.js
├── SearchApiElementsPage.js
└── SearchApiSpecPage.js

From this structure, the following map will be created:

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

At the moment, this map is used only to resolve routes and creating page instances when they should be rendered. An ongoing feature is to simplify imports, so instead of importing components directly in page declarations, the page object could request components from the controller.

Handling Actions

const actions = {
loadAPI: {
obj: LoadApiAction()
}
}

The controller exposes the actions object. Then, any page or component can execute an action with this syntax:

this.controller.action('loadAPI', {state: this.getState()}

This is all! The action object will access the state of the calling component, execute the defined application-external call, and pass the results as the new state object to the receiving component(s). Decoupling the participating components is a challenging aspect, a future article will explain how to works.

Conclusion

IT Project Manager & Developer