Creating a module
It may be useful to expand the functionality of Webstrates with additional client modules; modules that can provide functionality to all webstrates.
Creating a new module can be useful both for providing functionality that is used in many webstrates, lest having to reimplement the same thing over and over. Modules can also provide new functionality that can’t be implemented solely in client code, either because the module will depend on internal events or it needs to use the websocket, possibly because it needs to handle new kinds of websocket data. In this case, there is currently no documentation on how to expand the server side code, only the client code.
We’ll now go over two example modules that will demonstrate how to create a module for Webstrates.
Party module
They say that “Three’s a party”, so let’s implement a very basic module that creates a global party
event which gets triggered when a third user joins a webstrate.
Modules are located in the ./client/webstrates/
folder in the Webstrates installation directory, so let’s start out by creating a new JavaScript file at ./client/webstrates/party.js
.
After creating the module file, we need to tell Webstrates to use the module. This is done by adding 'party'
to the end of the modules
list in the client configuration ./client/config.js
.
Our module will need to include two of the built-in modules, namely coreEvents
and globalObject
.
We want to listen for the clientJoin
event, so we need coreEvents
. We also need to create a new event on the global object (i.e. window.webstrate
) for which we need the globalObject
module.
'use strict';
const coreEvents = require('./coreEvents');
const globalObject = require('./globalObject');
Now, all we need to do is to create the party
event.
globalObject.createEvent('party');
And trigger the event when the number of clients reaches 3:
coreEvents.addEventListener('clientJoin', (clientId) => {
if (webstrate.clients.length === 3) {
globalObject.triggerEvent('party', clientId);
}
});
And that’s it. After rebuilding the client application code (by calling npm run build
), the module
will be active and may be used in any webstrate.
Adding the following script to any webstrate will thus cause “Three’s a party” – followed by the joining client’s clientId
– to get printed to the console whenever a third user joins the webstrate.
webstrate.on('loaded', () => {
webstrate.on('party', (clientId) => {
console.log('Three\'s a party!', clientId);
});
});
Smash module
Let’s make it possible to draw attention to a specific div
element in the DOM on other clients.
We’ll do this by allowing users to run DOMNode.webstrate.smash()
on a div
element, and let other clients listen for these smashes with DOMNode.webstrate.on('smash', fn)
.
“Smashing” an element seems very similar to signaling, so we will in fact be building this module on top of signaling.
To create the event, we need to wait for all webstrate objects to have been added, so we again need coreEvents
.
Since we’re building this on top of signaling, we also need the signaling
module.
'use strict';
const coreEvents = require('./coreEvents');
const signaling = require('./signaling');
Before we can create the smash
event and smash()
function, we need to wait for the webstrate
objects to get added to the DOM elements.
Once the document has been created initially, and webstrate objects have been added, the webstrateObjectsAdded
event gets triggered with a list of all DOM nodes (and event objects) in the document with webstrate objects on.
When later on, a new element gets added to the document, the webstrateObjectAdded
event gets triggered with the node and event object as well.
Note that the event names differ slightly; the former is plural (Objects) and the latter is singular (Object).
If we listen for these two events, we can thus ensure that our code will get called whenever a webstrate object gets added.
coreEvents.addEventListener('webstrateObjectsAdded', (nodes) => {
nodes.forEach((eventObject, node) => makeSmashable(node, eventObject));
}, coreEvents.PRIORITY.IMMEDIATE);
coreEvents.addEventListener('webstrateObjectAdded', (node, eventObject) => {
makeSmashable(node, eventObject);
}, coreEvents.PRIORITY.IMMEDIATE);
What does coreEvents.PRIORITY.IMMEDIATE
mean?
The second argument to coreEvents.addEventListener()
is a priority, which determines the order in which the event handlers are triggered.
This can be useful if, for instance, one event handler has to have finished its job before another event handler can perform its own job.
There's a total of 5 priorities, namely IMMEDIATE
, HIGH
, MEDIUM
, LOW
, and LAST
.
Any event handler with IMMEDIATE
priority gets executed straight away in the same execution.
The use of the IMMEDIATE
priority should be limited as much as possible, as doing too much work in the same task can cause the browser to become briefly unresponsive.
However, in this case, we need to run our code immediately, as otherwise clients may attempt to add an event listener for the smash
event before it has been created.
Event handlers attached with the other 4 priorities are run through setImmediate()
(or setTimeout()
if setImmediate
isn't supported by the browser), thus getting executed in their own task.
Tasks with HIGH
priority are the first to be executed (after IMMEDIATE
), followed by tasks with MEDIUM
, LOW
, and finally LAST
priorities, respectively. Event handlers added without priority default to LOW
priority.
In the above, we make sure that makeSmashable(node, eventObject)
(a function we’ll implement now) gets run for all DOM nodes in the document.
const widEventMap = new Map();
function makeSmashable(node, eventObject) {
if (node instanceof HTMLDivElement) {
eventObject.createEvent('smash');
Object.defineProperty(node.webstrate, 'smash', {
value: () => {
node.webstrate.signal('__smash');
}
});
widEventMap.set(node.webstrate.id, eventObject);
signaling.subscribe(node.webstrate.id);
}
}
In the above, we check if our node is a div
elemnt, and if so we create the smash
event and define the smash()
function.
The smash
function simply sends a signal with the contents __smash
on the node.
Lastly, we subscribe to signals on the node and create and populate a map (widEventMap
) from the elements’ __wids
to their respective event objects, which we will be using in the next and final part of our module.
What are those __wid
s? To keep better track of DOM, Webstrates
adds unique IDs to each DOM elements. These are usually not visible in the DOM, even though they
exist in the JsonML representation. While it should rarely be necessary for users to access
the __wid
of an element, it is available as id
on all non-transient
DOM nodes' webstrate objects (i.e. DOMNode.webstrate.id
).
If you want to know more about JsonML, check out How it works in the Developer guide.
In order to avoid signal event handlers getting triggered by our smash
event, we install an interceptor on all signals.
The interceptor works by receiving all signals we’re subscribed to, and then returning true
if the signal needs to be interceptor (i.e. not propagated further) or not.
If the interceptor returns a falsy value, the usual signaling related events (both system and userland) get triggered.
signaling.addInterceptor(payload => {
if (payload.m !== '__smash') return false;
const eventObject = widEventMap.get(payload.id);
if (!eventObject) return false;
eventObject.triggerEvent('smash', payload.s);
return true;
});
In our interceptor, we access the raw payload object. This object contains a few pieces of information, such as the message itself (payload.m
), the sender’s clientId payload.s
, and the __wid
of the element the signal was sent to (payload.id
).
We first make sure that the signal received is in fact a smash, or otherwise return.
Afterwards, the map we created before comes into play. We look up the __wid
in the map to find the event object associated with the node to whom the __wid
belongs.
If we can’t find the event object, the shouldn’t intercept, either.
On the other hand, if we can find the event object, we trigger the event (with the sender’s clientId as argument) and return true
to intercept the signal.
After it has been installed and enabled (as with the Party module), it can be used in a webstrate as below.
<html>
<head>
<script>
webstrate.on('loaded', () => {
Array.from(document.querySelectorAll('div'), node => {
node.webstrate.on('smash', senderId => {
console.log('smash on', node.innerText);
});
node.addEventListener('click', () => {
node.webstrate.smash();
});
});
});
</script>
</head>
<body>
<div>A</div>
<div>B</div>
<div>C</div>
</body>
</html>
In the above webstrate, clicking on one of the div
s will result in smash on
, followed by either A
, B
, or C
to be printed in the console of all connected clients.