-
Notifications
You must be signed in to change notification settings - Fork 4
RoboJS :: the idea behind the code
During my frontend developer life, I worked more than once on multipage projects, where frontend templates should have been integrated with backend guys. Most of the time we worked together in the same team. We could speak to eachother, and we always created some sort of dependency in our applications.
Javascript structure was almost the same, one common file for all dependencies, one file dedicated to specific template dependencies and a file for each template. The structure looked like this
- common.js
- my-template-dependencies.js
- my-template.js
Page rendering was server-side, and "my-template" was substituted dynamically with the right template name.
This situation created a strong dependency between frontend and backend, but it was hidden by the fact we coded in the same room.
Everything was fine, until while I worked with an external backend team.
One must was..."we can't add logic on rendering process".
It meant their system could write <script>
tags in a static way.
Frontend was in charge, in order to load any specific javascript.
By other hand the project had some dynamic tools. I mean a template had a set of modules that could or could not be loaded, depending on what CMS editors would add or remove.
Based on a configuration, the markup was rendered by the server.
Base HTML layout looked like this.
<html>
<!-- no CSS stuff included in this example! -->
<body>
<!-- HTML generated server-side -->
<script src="entry-point.js"></script>
</body>
</html>
Wow, so I could use just one entry point script that, depending on how DOM was composed, should have choose which JS request.
2 things were important:
- recognize which modules were in DOM,
- dynamically load scripts.
As far as the first point, I added an attribute data-mediator
on each html element that would identify a module.
For instance a search module looked like this
<div data-mediator="search-module">
<input type="text">
<button>SEARCH</button>
</div>
At that point I could know how many modules were in page.
Next step was to load the specific JS files. RequireJS
rescued me.
I could map data-mediator
attributes over the js files name and use RequireJS to request them.
RequireJS
was introduced as dependencies manager.
An Object with all the modules allowed was created.
var definitions={
"my-module":"path/to/my/module",
"search-module":"path/to/search-module"
}
I was able to find and load all the scripts I needed.
var getAllElements = node=>[].slice.call(node.querySelectorAll("[data-mediator]"), 0);
var hasMediator = definitions=>node=>(definitions[node.getAttribute("data-mediator")]);
I created a loader
Object, that wrapped the RequireJS
callback into a Promise
.
var loader={
load: id=> new Promise((resolve, reject)=> window.require([id],resolve))
}
and then I created a function to find the right mediator
.
var findMediators = (definitions, loader)=>node=> loader.load(definitions[node.getAttribute("data-mediator")]));
Finally I composed all together to get a Promise
var getMediators = compose(
Promise.all.bind(Promise),
map(findMediators(definitions, loader)),
filter(hasMediator(definitions)),
flatten(),// I need flatten elements
map(getAllElements)
)
Now then I could use getMediators
function starting from body, to search my modules.
getMediators([document.body])
End of story...almost, because previously I lied to you to keep things simpler.
Loading the necessary scripts is not enough, you need to teel the requested script which DOM node is the right one. To achieve this goal you need a function as return from request. So you can decide when to invoke it and which parameters to pass it, the DOM element in our case.
The loaded script should look like this.
define(()=>node=>({
initialize(){},
destroy(){}
}));
define
function is used because loader
Object uses RequireJS
.
You also should add a new function create
var create=node=>Mediator=>{
var id=nextUID();
node.setAttribute("data-mediatorid",id);
var mediator=Mediator(node);
mediator.initialize && mediator.initialize();
CACHE[id]=mediator;
}
create
function gets DOM
node and the lazy loaded function from requested file.
A new random id
is created and assigned to a node's attribute. This attribute will be used to destroy the mediator
Object.
Mediator function is invoked and the node reference is passed to it. The mediator
Object is added to the cache
. mediator
Object has two methods, initialize
and destroy
, both optional. initialize is immediately executed, while destroy
will be invoked when the node will be removed from DOM.
findMediators
function is modified too. You chain a then
and when the Promise is resolved, create function is invoked.
var findMediators = (definitions, loader)=>node=> loader.load(definitions[node.getAttribute("data-mediator")]).then(create(node));
Now you have created and executed your Mediator
.
If the node would be removed from DOM, a callback will takes care to get the mediator
from CACHE
and invoke destroy method if it is present.
Some details have been omitted in order to keep focused on the main idea, for instance, the Mediator accepts an EventDispatcher
as second parameter. It can be your messaging system between modules.
Thanks to MutationObserver you can listen to DOM mutations and keep all modules up to date.
There is a variant that lazy load and register your Custom Elements when they appear in DOM the first time.
This tool has been inspired by Robotlegs AS3 framework. That's the reason for the name robojs.
Repository is on github, with some samples.