Skip to content

RoboJS :: the idea behind the code

Taitu Lizenbaum edited this page Apr 25, 2016 · 3 revisions

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.

Clone this wiki locally