SPAC: Designing Pages and Components

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 design rationale and highlight implementation details about pages and components.

This article originally appeared at my blog.

Pages and Components are First Class Citizens

Pages form the container of components. First of all, they provide a DOM structure with dedicated nodes to which components are attached. Second, pages contain the application state, and allow components to read and update this state. Components are added to pages. When a page unloads, it also unloads all of its components.

Essentials: Interfaces

Let’s take a look at the implementation of this interface itself.

const Interface = (...methods) => {
return class AbstractInterface {
constructor () {
methods.forEach(methodName => {
if (!this[methodName]) {
const constructorName = this.constructor.name
throw new (class ClassCreationError extends Error {
message = `Class ${constructorName} requires method '${methodName}' to be implemented`
})()
}
})
}
}
}
  • Line 1: Defines the Interface method, receiving an array of method names, that returns a named class expression
  • Line 3,4: The classes constructor() runs a check for each of the passed method names
  • Line 5: If the created class has no property of the method name, then…
  • Line 6: … save the newly created classes name
  • Line 7: … and throw a custom error, detailing which method name is missing from the class

This interface is a simple tool to verify that all of the main entities, which are controller, pages, components and interfaces, have all required methods.

Now, let’s see how the page entity is implemented.

Implementing Pages

Interface

class PageInterface extends Interface(
'render',
'mount',
'refresh',
'getState',
'updateState',
'addComponents'
) {}

These methods are detailed in the next sections.

Constructor

  • querySelector: A query-string that will be resolved to determine where in the DOM the rendered HTML should be attached.
  • state: The overall application state
  • components: An internal map of all registered components for this page.

These properties are set in the constructor.

class Page extends PageInterface {
constructor (querySelector) {
super()
if (!querySelector) {
throw new (class ClassCreationError extends Error {
message = `Class ${this.constructor.name} requires a valid 'querySelector' element`
})()
}
this.querySelector = querySelector
this.state = {}
this.components = new Map()
}
}

The constructor also checks that a valid querySelector element is provided, and throws an error otherwise.

Managing Components

  • addComponent(): Adds a new component to this page
  • removeComponent(): Destroys a registered component, and also triggers a re-rendering of the page

Components are not passed as objects, but they are requested from the controller. Here is the how too:

addComponents (...comps) {
comps.forEach(name => {
try {
const component = Controller.component(name)
component.updateState = this.updateState.bind(this)
component.getState = this.getState.bind(this)
this.components.set(component.name, component)
} catch (e) {
console.error(`Could not load ${component}`)
}
})
this.refresh()
}

Rendering a Page

  • render(): Renders the dynamic created HTML. SPAC relies on quoted templates in which you can attach e.g. variables contained in the state.
  • mount(): Attaches the output from render() to the querySelector DOM element. Because of timing issues - the original page and its DOM needs to be fully visable - the contencte DOM element is determined at the moment the mount happens. Afterwards, a mount() to all registered components is send.

Once loaded, the following methods apply to handle the components life cycle:

  • refresh() - Triggers a re-rendering of the page, for new components apply mount(), otherwise apply refresh() of these pages
  • destroy() - Destroy all registered components and associated JavaScript functions

The difference between mount() and refresh() are about a convention: mount() should include all other JS that needs to be applied to the registered DOM, and refresh() should only re-render the actual HTML, e.g. because of a new state, but not touching things like registering new event listeners.

Managing the State

  • updateState(): Update the state of the page.
  • getState(): Reads the state of the page

The state itself is an object — you can store any key-value pairs that you need to store. SPAC does not provide any scoping for data stored inside the state, but you can invent your own schema, e.g. prefixing component-related data with the component name, like this: updateState( {myComponent: {searchQuery: 'API'}})

Implementing components

Interface

class ComponentInterface extends Interface(
'render',
'mount',
'refresh',
'getState',
'updateState'
) {}

Constructor

  • querySelector: The root DOM element to which they attach themselves.
  • name: The unique name of the component

Similar to a page, the querySelector needs to be passed during initialization, or otherwise no instance will be created.

class Component extends ComponentInterface {
constructor (querySelector) {
super()
if (!querySelector) {
throw new (class ClassCreationError extends Error {
message = `Class ${this.constructor.name} requires a valid 'querySelector' element`
})()
}
this.querySelector = querySelector
this.name = this.constructor.name
}
}

Rendering HTML

  • mount(): Attaches the output from render() to the querySelector DOM element, which is evaluated at runtime. Also, this method should define any additional JavaScript code, e.g. for handling events.
  • render(): Render the template string, which can include references to state variables
  • refresh(): Re-applies the output of render to its DOM element.

State Management

Conclusion

IT Project Manager & Developer