Create a complete website from a json
file based of configurable and extensible components!
The real power of this platform is to extend the existing components and create custom e2e features.
Create an Express server and install the configured components:
import {AppComponents} from 'express-dynamic-components';
const app = require('express')();
(async () => {
console.log('installing...');
try {
const components = new AppComponents(app, require('./components.json'));
await components.install();
console.log('listening...');
app.listen(8080);
}
catch (e) {
console.log(e);
}
})();
components.json
file:
{
"config": [
{
"key": "value"
}
],
"code": [
{
"type": "server",
"desc": "async initializing app",
"code": "(app) => new Promise(r => setTimeout(() => {app.tools = {isOk: true}; r();}, 0))"
},
{
"type": "endpoint",
"name": "myEndpoint",
"desc": "test endpoint",
"code": "(req) => ({ isOk: this.context.app.tools.isOk, query: req.query })"
},
{
"type": "html",
"name": "myPage.html",
"desc": "test html page",
"head": [
{
"type": "dom",
"desc": "test dom element",
"code": "<title id=\"my-title\">test page</title>"
},
{
"type": "script",
"desc": "test script element",
"code": "(req, config) => `alert('${config.key}');`"
}
],
"body": [
{
"type": "script",
"desc": "test script that calls test endpoint",
"code": "fetch(`myEndpoint${location.search}`).then(r => alert(r.json()));"
},
{
"type": "scoped-script",
"desc": "test scoped client script",
"code": "(config) => alert(config.key)"
}
]
}
]
}
Other than the Express.Application
object use to create the server, the second parameter required to the AppComponents
constructor must follow the following interface (components.json
from the above example):
interface IAppComponentsConfig {
config?: ConfigNode[];
code: CodeNode[];
}
export type ConfigNode = {
[key: string]: string;
};
export type CodeNode = {
type?: string; // detailed below
desc: string;
code?: string|Function;
};
A component contains a piece of code that will be executed in a context according to its type.
- Default type:
'server'
- Type:
'server'
- Description: code executed when the server is created.
- Code signature:
(app: Express.Application, config: ConfigNode) => Promise<any>
- Type:
'endpoint'
- Description: a server endpoint that returns a JSON object per request.
- Code signature:
(req: Express.Request, config: ConfigNode) => Promise<any>|any
- Additional fields:
name: string
: endpoint's name, for example:'my-path/endpoint'
- Type:
'html'
- Description:
- Code signature: not needed - default HTML structure (with
head
andbody
). - Additional fields:
name: string
- page's name, for example:'my-path/myPage.html'
head?: CodeNode[]
- an array of Frontend code-nodesbody?: CodeNode[]
- an array of Frontend code-nodesdisableConfigInjection?: boolean
(default:false
) Iftrue
, prevents serialization of theconfig
object into the page'swindow.config
.
- Default type:
'dom'
- Frontend components are rendered inside their host HTML page according to its HTTP request.
- Type:
'dom'
- Description:
- Code signature:
string | (req: Express.Request, config: ConfigNode) => Promise<string>
- the resolved
string
will be placed in the hosting page
- the resolved
- Type:
'script'
- Description: an HTML script tag
- Code signature:
string | (req: Express.Request, config: ConfigNode) => Promise<string>
- the resolved
string
will be the content of the<script>
tag rendered in the page
- the resolved
- Type:
'scoped-script'
- Description: a function that will be invoked in the HTML page (as an IIFE).
- Code signature:
(config: ConfigNode) => void
- Notice that this is useful especially for extending to Scoped Custom Scripts (see below).
Extending an existing component usually consists of 4 simple steps:
- Create a class that extends the wanted component
- Set the component's type name
- By overriding the static
typeName
getter:public static get typeName()
- By overriding the static
- Set the component's logic
- By overriding the
run
method:- for backend components:
public run(options: CodeNode, fn: Function): Promise<any>
- for frontend components:
public run(options: CodeNode, fn: Function, req: Express.Request): Promise<string>
- for backend components:
- Important(!): always call and return the
super.run
's return value - see example.
- By overriding the
- Add it to the relevant factory:
frontendFactory
orbackendFactory
.
Example:
import {frontendFactory, DomComponent} from 'express-dynamic-components';
class MyDomComponent extends DomComponent {
public static get typeName() {
return 'my-dom';
}
public run(options, fn, req) {
return super.run(options, (req, config) => {
return `<my-dom></my-dom>`;
}, req);
}
}
frontendFactory.addType(MyDomComponent);
When we want to allow our implementing users to easily use custom injected objects.
Usage example:
{
"type": "my-script",
"desc": "my scoped custom script",
"code": "async (config, myApi) => await myApi.doAction()"
}
This component will usually be hosted inside an html page component - and its script will be invoked immediately, supplying myApi
object to the implementation use.
Implementing this requirement:
import {frontendFactory, ScopedScriptComponent} from 'express-dynamic-components';
frontendFactory.addType(class MyScript extends ScopedScriptComponent {
public static get typeName() {
return 'my-script';
}
protected getScopeArgs() {
return [
'window.config',
'window.myCompany.getApiObject()'
];
}
});
Notice that getScopeArgs
method returns an array of strings that indicate from where to inject the the arguments.
A Feature component brings e2e abilities - backend and frontend components.
Usage example:
{
"type": "my-feature",
"desc": "a custom feature"
}
A new feature component must extend from the following abstract class:
export abstract class FeatureComponent {
public abstract get backend(): CodeNode[];
public abstract get frontend(): PageNodesDictionary;
}
Implmentation example:
import {backendFactory, FeatureComponent, JsonEndpointComponent, JsonEndpointNode, ScriptTagComponent} from 'express-dynamic-components';
backendFactory.addType(class MyFeature extends FeatureComponent {
public static get typeName() {
return 'my-feature';
}
private readonly _endpointName = 'myFeatureEndpoint';
public get backend() {
return [
{
type: JsonEndpointComponent.typeName,
desc: 'my-feature endpoint',
name: this._endpointName,
code: async (req, config) => ({myFeature: true, query: req.query})
} as JsonEndpointNode
];
}
public get frontend() {
return {
defaultPage: {
head: [],
body: [
{
type: ScriptTagComponent.typeName,
desc: 'my-feature script',
code: `
fetch('${this._endpointName}' + location.search)
.then(r => JSON.stringify(r.json())).then(console.log);`
}
]
}
}
}
});
- The
backend
getter returns an array ofCodeNode
s that describe backend components as the example above.- In this example, we're simply creating a new endpoint that returns an object.
- The
frontend
getter returns a dictionary of page type'sstring
toHTMLPageComponent
.- page type:
defaultPage
- will be the firstHTMLPageComponent
found.- Other page types can be set freely and mapped to an actual
HTMLPageComponent
in the feature's code node:
{ "type": "my-feature", "desc": "a custom feature", "frontend": { "defaultPage": "index.html", "loginPage": "login.html", "accountPage": "account.html" } }
HTMLPageComponent
: The frontend components of it will be added to the relevant page.
- page type:
When extending a component, you can re-implemented the following interfaces to override functionality in the component's life cycle:
export interface IValidate {
validate(node: CodeNode): void;
}
export interface IGetFn {
getFn(code: string|Function): Function;
}
export interface IGetActivator<T> {
getActivator(fn : Function, options: CodeNode): ICodeActivator<T>
}
- clone the repo
npm i
npm run example
- navigate to
http://localhost:8080/myPage.html