Marathon runner system for vanilla JavaScript
Marathon aims to simplify web JavaScript development thanks to separation of concern and a reducer-like single entry point. This eases the development of isolated, pure components (provided they are coded this way) and enables a basic, HTTP multi-page website to be turned into a single-page application with the use of libraries such as Swup, Barba.js, InstantClick, Turbolinks and others, with very minimal code change.
yarn install @mashvp/marathon
A Marathon application is composed of one or several runners, which are isolated units of code that should handle a single feature of your application.
Runners are pure functions, which must return an object containing a register
and unregister
function.
These functions are used in place of a constructor and destructor, in order to enable and disable the runner's features as required.
Runners should not depend on each-other, and should handle an isolated state. You can implement message passing between runners using a library such as PubSub. Generally speaking, runners should never be tightly coupled.
Runners can be joined together into a root runner, forming a tree structured application.
A basic runner can be created with the createRunner
function.
import { createRunner } from '@mashvp/marathon';
// Runners are allowed to use external libraries
import SomeLibrary from 'some-library';
export default createRunner('MyRunner', ({ /* globals here */ }, { /* Builtins here */ }) => {
const libraryInstance = new SomeLibrary();
const register = () => {
console.log('MyRunner was registered');
libraryInstance.run();
};
const unregister = () => {
console.log('MyRunner was unregistered');
libraryInstance.stop();
};
return { register, unregister };
});
In the example above, the runner will log a message to the console when the application loads, and log another one when the application unloads. The runner also sets up an instance of an imaginary library, and starts and stops it when relevant.
Please note that the unregister
function is not required if it does not need to perform any action.
You can use a combined runner to join together runners which handle similar features to simplify your application structure. The createCombinedRunner
returns a runner which forwards calls to register
and unregister
to its children.
import { createCombinedRunner } from '@mashvp/marathon';
import SomeRunner from './some-runner';
import OtherRunner from './other-runner';
export default createCombinedRunner(SomeRunner, OtherRunner);
Scoped runners are useful for implementing component-like features. For example, a modal system which could be found in multiple places in the website could be handled with a scoped runner. The createScopedRunner
takes in a selector or list of elements along with the definition of the runner, creates a runner for each element, and returns of combined runner of them.
<button class="alert-button" data-text="This is the alert text">Click me</button>
<button class="alert-button" data-text="This is another alert">No, click me instead!</button>
import { createScopedRunner } from '@mashvp/marathon';
const alertButton = createScopedRunner(
'AlertButton',
'.alert-button',
({ scope }, { bind }) => {
const register = () => {
const text = scope.getAttribute('data-text');
// Use the `bind` builtin to add event listeners to elements.
// The listeners will be automatically unbound when the runner is unregistered.
bind(scope, 'click', () => {
alert(text);
});
};
return { register };
}
);
The root runner can be thought of as your application itself. It works much like a combined runner, except that it returns a runner instance which is ready to be used as is. The combineRoot
function takes in your application runners the same way createCombinedRunner
does.
You can then use the register
function on your root runner to start the application, passing in any globals as required.
import { combineRoot } from '@mashvp/marathon';
import Bowser from 'bowser';
import SomeRunner from './runners/some-runner';
import OtherRunner from './runners/other-runner';
import MobileRunners from './runners/mobile';
(() => {
const browser = Bowser.getParser(window.navigator.userAgent);
const rootRunner = combineRoot(SomeRunner, OtherRunner, MobileRunners);
const init = () => {
// Start the root runner on DOMContentLoaded, passing in browser info to the children
rootRunner.register({ browser });
};
document.addEventListener('DOMContentLoaded', init);
})();
This structure allows to easily swap in a single-page AJAX library, such as Swup, and make sure that no stray event listeners and such are still in place when transitioning between pages.
import { combineRoot } from '@mashvp/marathon';
import Swup from 'swup';
import Bowser from 'bowser';
import SomeRunner from './runners/some-runner';
import OtherRunner from './runners/other-runner';
import MobileRunners from './runners/mobile';
(() => {
const swup = new Swup(/* ... */);
const browser = Bowser.getParser(window.navigator.userAgent);
const rootRunner = combineRoot(SomeRunner, OtherRunner, MobileRunners);
const init = () => {
// Start the root runner on Swup contentReplaced, passing in browser info to the children
rootRunner.register({ browser });
};
const unload = () => {
// Stop and clear all runners before Swup page change
rootRunner.unregister();
};
swup.on('contentReplaced', init);
swup.on('willReplaceContent', unload);
})();
Marathon provides runners with a few builtin functions, which simplify handling event listeners between register
and unregister
calls.
This function works just like addEventListener
, but listeners are cleared automatically when the runner unregisters.
import { createRunner } from '@mashvp/marathon';
export default createRunner('ExampleRunner', ({ /* globals */ }, { bind }) => {
const register = () => {
const button = document.querySelector('button.example-runner');
bind(button, 'click', () => {
alert('The button was clicked');
});
};
// No `unregister` is needed here since `bind` clears the event listener automatically
return { register };
});
This function works just like removeEventListener
, but you don't need to give a reference to the original handler, since they are memorized.
Please note that unbind
will remove all event listeners of a given type on an element.
unbind(element, eventType);
This function will unbind all events that were bound using bind
on a given element.
unbindAll(element);
👤 Mashvp
- Website: mashvp.com
- Twitter: @mashvp
- Github: @mashvp
- LinkedIn: @mashvp
Give a ⭐️ if this project helped you!