Blobs are powerful configuration objects which all modules can listen for. This allows for customization far beyond what is available directly through the api. Okwolo is purposefully built to handle the addition of blobs at any time in an application's lifecycle.
Blobs are added to an application with the "use" function. It takes the name of the blob as the first parameter and the rest of the arguments will be passed to the blob handling functions.
app.use('blobName', arg1, arg2, ...);
This page lists the blobs understood by at least one module in the okwolo package, even if that module is not necessarily present in all kits.
Note that there is no return value and that blobs which are not listened for will not produce any errors.
Actions are received by the state handler module and can be used to modify the state. Action handlers are given a copy of the state and must return the modified value.
const action = {type, target, handler};
// type: string which names the action
// target: array to restrict the scope of an action to a specific "address" in the state
// OR function which returns the action's target [(state, params) => target]
// handler: function that makes the change on the target [(target, params) => modifiedTarget]
app.setState({
name: 'John',
hobbies: ['tennis', 'gardening'],
});
app.use('action', {
type: 'REMOVE_HOBBY',
target: ['hobbies'],
handler: (hobbies, index) => {
hobbies.splice(index, 1);
return hobbies;
},
});
app.act('REMOVE_HOBBY', 0);
app.getState(); // {name: 'John', hobbies: ['gardening']}
More than one actions can be added at the same time by passing an array of action objects instead of a single one.
The path base is received by the router module and can be useful when the app is not located in the website's base directory.
app.use('base', '/subdir/myapp');
app.redirect('/users');
// navigates to '/subdir/myapp/users'
// matches routes for '/users'
The build function is responsible for generating vdom from the input layout syntax.
app.use('build', (element, queue) => {
// ...
return vdom;
});
const element = ( => const vdom = {
['div#title', {}, [ => tagName: 'div',
'Hello World!', => attributes: {
]] => id: 'title',
); => },
=> children: {
=> 0: {
=> text: 'Hello World!',
=> },
=> },
=> childOrder: ['0'],
=> };
The builder is the function which generates layout. This is the blob being sent at startup and when a redirect is issued.
app.use('builder', (state) => (
['div | background-color: red;', {}, [
['span.title', {} [
state.title,
]],
]]
));
The draw function handles the initial drawing to a new target. It should return a view object that will be given to the update function. The format of this object is not enforced. Note that the view module does not verify the type of the target and it is therefore good practice to make these checks before interacting with the target.
app.use('draw', (target, vdom) => {
target.innerHTML = magicallyStringify(vdom);
return vdom;
});
Middleware is received and applied by the state handler. It is given control of all actions before they are executed and can be asynchronous.
app.use('middleware', (next, state, actionType, params) => {
if (params.test) {
console.log('action changed to TEST');
next(state, 'TEST');
} else {
next();
}
});
If "next" is called with arguments, they will override the ones issued by the act call.
Middleware functions are called in the order that they are added.
More than one middleware can be added at the same time by passing an array of middleware functions instead of a single one.
const route = {path, handler}
// path: string pattern to match paths (using express' syntax)
// handler: function that is called with the route params as argument [(routeParams) => ...]
app.use('route', {
path: '/user/:uid/profile',
handler: ({uid}) => {
// ...
},
});
app.redirect('/user/123/profile');
// handler is called with {uid: '123'}
The target will be given to the view's drawing and updating functions and can be of any type. In the client-side kits, the target should be a dom node whereas the ssr kit expects a callback.
app.use('target', document.querySelector('.app-wrapper'));
The update function updates the target with new vdom. It also receives the update address and the view object from a draw or a previous render as third and fourth arguments respectively. However, there is no defined format for either of these values and they can be omitted if not necessary.
By overriding both the draw and update functions, it is possible to "render" to any target in any way. Here is an example of an app that renders to a string instead of a DOM node.
let realTarget = '';
app.use('draw', (target, vdom) => {
realTarget = magicallyStringify(vdom);
});
app.use('update', (target, vdom, address, view, identity) => {
realTarget = magicallyStringify(vdom);
});
Watchers are functions that get called after all state changes. They cannot modify the state directly since they are given a copy, but they can safely issue more actions.
app.use('watcher', (state, actionType, params) => {
console.log(`action of type ${actionType} was performed`);
});
More than one watchers can be added at the same time by passing an array of watcher functions instead of a single one.
To accommodate the addition of plugins, the use function can also accept an object as the first argument. This object can contain multiple types of blobs.
app.use({
watcher: [myFirstWatcher, mySecondWatcher],
route: myRoute
});
Plugins can also be named to ensure they are only ever used once in a single app instance. This is done by adding a name key to the plugin.
let myPlugin = {
name: 'myPluginName',
middleware: myMiddleware,
}
app.use(myPlugin);
app.use(myPlugin); // will not add the middleware again