SPAC: Controller Self-Initialization & Object API

Ongoing Example: ApiBlaze Index Page

src
└── actions
│ └── SearchApiSpecAction.js
└── components
├── ApiSearchComponent.js
└── ApiSearchResultsComponent.js
└── pages
│ ├── IndexPage.js
│ ├── SearchApiElementsPage.js
└── index.js
{
"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

import { Controller } from 'spac'
import inventory from './inventory.json'
const controller = new Controller({ inventory })controller.init()
  • 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
  • pagesMap: Define entries with route and obj properties
  • actionsMap: Define entries with obj properties
  • componentsMap: Define entries with obj properties
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) {
// ...
}
})
}
  • 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

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

Components

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

Actions

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

Assembling Pages

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')
)
}
}
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' }
}
}
}
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

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store