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 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/.
In this article, I explain the central controller object. We cover the design goals, then discuss its features and implementation.
Designing the Controller
The controller is the central entity of the framework. Its design encompasses the following goals:
- Self initializing, single point of entry: The controller is the only JavaScript module that you need to load with a
<script>
tag from yourindex.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 viamount()
. - Handling Actions: Actions are client-side external interactions like calling a backend or an external API. An action receives an
args
object and aupdateState
callback that receives the result of the action.
Now, let’s detail how the controller works.
Self-Initialization
The controller is self-initializing. You don't need any configuration file. By following the convention over configuration paradigm, you place pages, components and actions at the appropriate directories. The controller starts, parses the directories, initializes the objects. If it encounters any errors, it complains, but continues to load the rest of the application state.
To initialize your application, you need to do these steps:
- 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();
- Include this script in your
index.html
<script src="js/controller.js" type="module"></script>
That is all.
Render Pages
During the setup phase, the controller creates a map of all pages. Consider that you have the following page files.
.
└── pages
├── IndexPage.js
├── SearchApiElementsPage.js
└── SearchApiSpecPage.js
From this structure, the following map will be created:
const pages = {
Index: {
route: '/index',
clazz: IndexPage()
},
SearchApiElements: {
route: '/search_api_elements',
clazz: SearchApiElementsPage()
},
SearchApiSpec: {
route: '/search_api_spec',
clazz: 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
Actions involve application-external backends or APIs. They are defined separately in action files. These actions are initialized by the controller and result in a similar map object.
const actions = {
loadAPI: {
clazz: 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
My custom JavaScript framework is a generic approach to structure client-side applications that need to provide complex UI interactions. This article covered the framework's heart: The controller. I explained the main motivation of the controller - a central, self-initializing JavaScript module. It loads all pages and actions of the app, then serves the Index page. From there on, it handles routing and exposes actions that are triggered by pages. The next article details the self-initialization feature.