I am writing vanilla javascript project. I have multiple independent components. I like them to communicate without coupling them. Event based message bus is ideal for me. I implemented something trivial like following and I would like to ask whether is there any existing library that can do this more efficiently or whether language itself provides any capabilities to achieve this or not?
PS: I do not have any dom events in this case. I would like to create my own events so that I can write components pretty generic and clean way. I think React has similar concept and I will check that later on but for now I like to use vanilla javascript.
Thanks!
// event message bus interface
define(function(require) {
"use strict";
function CustomEventDispatcher() {
this._subscribers = {}
CustomEventDispatcher.prototype.on = function(event, callback) {
if(!this._subscribers[event])
this._subscribers[event] = [];
this._subscribers[event].push(callback);
}
CustomEventDispatcher.prototype.trigger = function(event, params) {
if (this._subscribers[event]) {
for (let i in this._subscribers[event]) {
this._subscribers[event][i](params);
}
}
}
CustomEventDispatcher.Instance = null;
CustomEventDispatcher.GetInstance = function () {
if (!CustomEventDispatcher.Instance) {
CustomEventDispatcher.Instance = new CustomEventDispatcher();
}
return CustomEventDispatcher.Instance;
}
return CustomEventDispatcher;
});
// UI interaction triggers or backend worker triggers event
let params = {
new_dir : '/dat',
};
CustomEventDispatcher.GetInstance().trigger('onUpdateDataSource', params);
// Directory Module registers directory update events and updates itself
function DirectorLister() {
CustomEventDispatcher.GetInstance().on('onUpdateDirectoryListing', (params) => this.change_content(params));
}
Related
Lately i have taken the tutorials D3 from here D3 - Data Driven Documents tutorials
anyway, i have a project i need to build some charts for and i was planning to create a library to generate charts based on project requirements with professional approach of code.
like jquery library has:
$('#someSelector').someFunction();
so far i know how to objectify the functionalities like:
someModuleFunctionality = {
getStuff = function(objParams){//bring data},
sendStuff = function(objParams){//save data},
someCalculations = function(i,j,k){//some calculations}
}
but it doesn't let me use these things as i want them to use like:
myProjectLibrary('#someSelector').buildBarChart();
I am not willing my library to be dependent on Jquery, just like D3. I will appreciate the help, thanks.
You need to create a function named myProjectLibrary which returns an object which has a function buildBarChart()
function myProjectLibrary(selector) {
let element = document.querySelector(selector);
if (element) {
element.buildBarChart = () => {
// code to build bar chart
}
}
return element;
}
There are two ways to do this based on your requirements if you want to be able to access the methods only. Then you can just return the functions as objects or someModuleFunctionality that you have. for e.g:
function myLibrary() {
let someModuleFunctionality = {
/** Your functions here */
}
return someModuleFunctionality;
}
But if you want to be able to chain functions like in jquery you need to return the reference of the mainLIbrary function in each methods for e.g:
function myProjectLibrary(selector) {
getStuff = () => {
this.data = selector.data;
return this;
}
logStuff = msg => {
console.log(`${msg} ${data}`);
return this;
}
return {
getStuff,
logStuff
}
}
myProjectLibrary({data: 'World'}).getStuff().logStuff("Hello");
You can then continue chaining functions as much as you like.
You need to return an object from your myProjectLibraryMethod function.
function myProjectLibraryMethod(arg) {
doStuff.with(arg);
return new MyProject(arg);
}
Then you just attach the methods to MyProject.
function MyProject() {
this.buildBarChart = function() { /* Build bar chart */ };
//Other methods...
}
The problem:
In one of our tests we have a "long click"/"click and hold" functionality that we solve by using:
browser.actions().mouseDown(element).perform();
browser.sleep(5000);
browser.actions().mouseUp(element).perform();
Which we would like to ideally solve in one line by having sleep() a part of the action chain:
browser.actions().mouseDown(element).sleep(5000).mouseUp(element).perform();
Clearly, this would not work since there is no "sleep" action.
Another practical example could be the "human-like typing". For instance:
browser.actions().mouseMove(element).click()
.sendKeys("t").sleep(50) // we should randomize the delays, strictly speaking
.sendKeys("e").sleep(10)
.sendKeys("s").sleep(20)
.sendKeys("t")
.perform();
Note that these are just examples, the question is meant to be generic.
The Question:
Is it possible to extend browser.actions() action sequences and introduce custom actions?
Yes, you can extend the actions framework. But, strictly speaking, getting something like:
browser.actions().mouseDown(element).sleep(5000).mouseUp(element).perform();
means messing with Selenium's guts. So, YMMV.
Note that the Protractor documentation refers to webdriver.WebDriver.prototype.actions when explaining actions, which I take to mean that it does not modify or add to what Selenium provides.
The class of object returned by webdriver.WebDriver.prototype.actions is webdriver.ActionSequence. The method that actually causes the sequence to do anything is webdriver.ActionSequence.prototype.perform. In the default implementation, this function takes the commands that were recorded when you called .sendKeys() or .mouseDown() and has the driver to which the ActionSequence is associated schedule them in order. So adding a .sleep method CANNOT be done this way:
webdriver.ActionSequence.prototype.sleep = function (delay) {
var driver = this.driver_;
driver.sleep(delay);
return this;
};
Otherwise, the sleep would happen out of order. What you have to do is record the effect you want so that it is executed later.
Now, the other thing to consider is that the default .perform() only expects to execute webdriver.Command, which are commands to be sent to the browser. Sleeping is not one such command. So .perform() has to be modified to handle what we are going to record with .sleep(). In the code below I've opted to have .sleep() record a function and modified .perform() to handle functions in addition to webdriver.Command.
Here is what the whole thing looks like, once put together. I've first given an example using stock Selenium and then added the patches and an example using the modified code.
var webdriver = require('selenium-webdriver');
var By = webdriver.By;
var until = webdriver.until;
var chrome = require('selenium-webdriver/chrome');
// Do it using what Selenium inherently provides.
var browser = new chrome.Driver();
browser.get("http://www.google.com");
browser.findElement(By.name("q")).click();
browser.actions().sendKeys("foo").perform();
browser.sleep(2000);
browser.actions().sendKeys("bar").perform();
browser.sleep(2000);
// Do it with an extended ActionSequence.
webdriver.ActionSequence.prototype.sleep = function (delay) {
var driver = this.driver_;
// This just records the action in an array. this.schedule_ is part of
// the "stock" code.
this.schedule_("sleep", function () { driver.sleep(delay); });
return this;
};
webdriver.ActionSequence.prototype.perform = function () {
var actions = this.actions_.slice();
var driver = this.driver_;
return driver.controlFlow().execute(function() {
actions.forEach(function(action) {
var command = action.command;
// This is a new test to distinguish functions, which
// require handling one way and the usual commands which
// require a different handling.
if (typeof command === "function")
// This puts the command in its proper place within
// the control flow that was created above
// (driver.controlFlow()).
driver.flow_.execute(command);
else
driver.schedule(command, action.description);
});
}, 'ActionSequence.perform');
};
browser.get("http://www.google.com");
browser.findElement(By.name("q")).click();
browser.actions().sendKeys("foo")
.sleep(2000)
.sendKeys("bar")
.sleep(2000)
.perform();
browser.quit();
In my implementation of .perform() I've replaced the goog... functions that Selenium's code uses with stock JavaScript.
Here is what I did (based on the perfect #Louis's answer).
Put the following into onPrepare() in the protractor config:
// extending action sequences
protractor.ActionSequence.prototype.sleep = function (delay) {
var driver = this.driver_;
this.schedule_("sleep", function () { driver.sleep(delay); });
return this;
};
protractor.ActionSequence.prototype.perform = function () {
var actions = this.actions_.slice();
var driver = this.driver_;
return driver.controlFlow().execute(function() {
actions.forEach(function(action) {
var command = action.command;
if (typeof command === "function")
driver.flow_.execute(command);
else
driver.schedule(command, action.description);
});
}, 'ActionSequence.perform');
};
protractor.ActionSequence.prototype.clickAndHold = function (elm) {
return this.mouseDown(elm).sleep(3000).mouseUp(elm);
};
Now you'll have sleep() and clickAndHold() browser actions available. Example usage:
browser.actions().clickAndHold(element).perform();
I think it is possible to extend the browser.actions() function but that is currently above my skill level so I'll lay out the route that I would take to solve this issue. I would recommend setting up a "HelperFunctions.js" Page Object that will contain all of these Global Helper Functions. In that file you can list your browser functions and reference it in multiple tests with all of the code in one location.
This is the code for the "HelperFunctions.js" file that I would recommend setting up:
var HelperFunctions = function() {
this.longClick = function(targetElement) {
browser.actions().mouseDown(targetElement).perform();
browser.sleep(5000);
browser.actions().mouseUp(targetElement).perform();
};
};
module.exports = new HelperFunctions();
Then in your Test you can reference the Helper file like this:
var HelperFunctions = require('../File_Path_To/HelperFunctions.js');
describe('Example Test', function() {
beforeEach(function() {
this.helperFunctions = HelperFunctions;
browser.get('http://www.example.com/');
});
it('Should test something.', function() {
var Element = element(by.className('targetedClassName'));
this.helperFunctions.longClick(Element);
});
});
In my Test Suite I have a few Helper files setup and they are referenced through out all of my Tests.
I have very little knowledge of selenium or protractor, but I'll give it a shot.
This assumes that
browser.actions().mouseDown(element).mouseUp(element).perform();
is valid syntax for your issue, if so then this would likely do the trick
browser.action().sleep = function(){
browser.sleep.apply(this, arguments);
return browser.action()
}
I want to write a javascript/jquery plugin so that it is generic enough to be used in any framework such as angularjs, backbonejs, ember etc. I should be generic enough so that it should use directives if it is used with angular and backbone native functionality when it is used with backbone. Is it possible if yes then could someone guide me how?
The most natural way I can think of is just to write it in vanilla JS. That will make it work in every framework without needing to worry about it.
If you want to go ahead with this route though, I'd use a driver-style implementation where you pipe everything to a specific driver for a particular framework. You'd define every method you want for each Driver, then the calls get forwarded on automatically to the correct Driver.
var myPlugin;
(function() {
myPlugin = function(framework) {
var me = {},
framework = framework || 'angular';
me.frameworks = {
angular: new AngularDriver,
backbone: new BackboneDriver,
ember: new EmberDriver
};
// Call a method framework-agnostically
me.fire = function(method, args) {
if (!me.frameworks.hasOwnProperty(framework)) {
console.log('Error: Framework not recognised.');
return;
}
if (!me.frameworks[framework].hasOwnProperty(method)) {
console.log('Error: Method not found in ' + framework + '.');
return;
}
me.frameworks[framework][method].apply(this, args);
}
return me;
}
function AngularDriver() {
var me = {};
me.test = function() {
console.log('Hello from the Angular Driver');
}
return me;
}
function BackboneDriver() {
var me = {};
me.test = function() {
console.log('Hello from the Backbone Driver');
}
return me;
}
function EmberDriver() {
var me = {};
me.test = function(arg) {
console.log('Hello from the ' + arg + ' Ember Driver');
}
return me;
}
})();
var instance = new myPlugin();
instance.fire('test');
instance = new myPlugin('ember');
instance.fire('test', ['best']);
It's entirely possible that there's a slightly cleaner way to implement the myPlugin.fire function, if anyone else can improve that bit so the syntax of instance.fire('test', ['best']) is a bit cleaner, feel free :-)
I am trying implement async event leveraging YUI3 library. So the application had been notified about event passed even with late subscription, simular like load or ready events do.
Here it is what I have so far, but no luck around.
YUI().use('event', 'event-custom', function(Y){
function onCustomEvent () {
Y.Global.on('custom:event', function(){
alert('custom fired');
});
}
window.setTimeout(onCustomEvent, 2000);
});
YUI().use('event', 'event-custom', function(Y){
Y.publish('custom:event', {
emitFacade: true,
broadcast: 2,
fireOnce: true,
async: true
});
function fireCustomEvent () {
Y.Global.fire('custom:event');
}
window.setTimeout(fireCustomEvent, 1000);
});
If anyone could give a hint what's wrong with this code? Thank you.
UPD:
After a bit investigations it turns out that async events work fine inside one use() instance and when not using Global broadcasting. So that's something either bug or limitation. Still discovering
Okay, at the high level the inconsistency with global events (how I understood it) lays in the sandbox nature of Y object. So at some point you could fire only sync events globally cause async parameters you subscribe to custom event made on Y instance and not passed further (and than YUI uses some defaults or whatever). This possibly makes sense but than why such kind of events should be fireable globally? Either I miss some substantial part of YUI and this is candidate for bug report.
Anyway I do not have time to dive deeper in YUI and what I really practically need could be wrapped in 40 lines of code:
YUI.add('async-pubsub', function(Y) {
var subscribers = {};
if ( !YUI.asyncPubSub ) {
YUI.namespace('asyncPubSub');
YUI.asyncPubSub = (function(){
var eventsFired = {};
function doPublishFor(name) {
var subscriber;
for ( subscriber in subscribers ) {
if ( subscriber === name ) {
(subscribers[name]).call();
delete ( subscribers[name] ); // Keep Planet clean
}
}
}
return {
'publish': function(name, options) {
eventsFired[name] = options || {};
doPublishFor(name);
},
'subscribe': function(name, callback) {
if ( subscribers[name] ) {
Y.log('More than one async subscriber per instance, overriding it.', 'warning', 'async-pubsub');
}
subscribers[name] = callback || function() {};
if ( eventsFired[name] ) {
window.setTimeout(
function () {
doPublishFor(name);
},0
);
}
}
};
})();
}
Y.asyncPubSub = YUI.asyncPubSub;
}, '1.0', {requires: []});
There is some limitation and room for optimization here, like ability subscribe only one action for one event per use instance, but I do not need more. I will also try to debug and enhance this snippet in future if there will be interest.
Still curious about YUI behavior, is it bug or something?
Suppose there are objects making subscriptions to a socket server like so:
socket.on('news', obj.socketEvent)
These objects have a short life span and are frequently created, generating many subscriptions. This seems like a memory leak and an error prone situation which would intuitively be prevented this way:
socket.off('news', obj.socketEvent)
before the object is deleted, but alas, there isn't an off method in the socket. Is there another method meant for this?
Edit: having found no answer I'm assigning a blank method to overwrite the wrapper method for the original event handler, an example follows.
var _blank = function(){};
var cbProxy = function(){
obj.socketEvent.apply(obj, arguments)
};
var cbProxyProxy = function(){
cbProxy.apply ({}, arguments)
}
socket.on('news', cbProxyProxy);
// ...and to unsubscribe
cbProxy = _blank;
From looking at the source of socket.io.js (couldn't find it in documentation anywhere), I found these two functions:
removeListener = function(name, fn)
removeAllListeners = function(name)
I used removeAllListeners successfully in my app; you should be able to choose from these:
socket.removeListener("news", cbProxy);
socket.removeAllListeners("news");
Also, I don't think your solution of cbProxy = _blank would actually work; that would only affect the cbProxy variable, not any actual socket.io event.
If you want to create listeners that "listens" only once use socket.once('news',func). Socket.io automatically will distroy the listener after the event happened - it's called "volatile listener".
Looking at the code of current version of Socket.io Client (1.4.8) it seems that off, removeAllListeners, removeEventListener are all pointing to the same function.
Calling any of those, providing event name and/or callback, gives the desired result. Not providing anything at all seems to reset everything.
Please do be cautious about the fn/callback argument. It has to be the same instance used in the code.
Example:
var eventCallback = function(data) {
// do something nice
};
socket.off('eventName', eventCallback);
Would work as expected.
Example (will also work):
function eventCallback(data) {
// do something nice
}
socket.off('eventName', eventCallback);
Please be cautious that the callback you are trying to remove is the one that you passed in (this one can bring a lot of confusion and frustration).
This example implements a wrapper around initial callback, trying to remove that would not work as the real callback being added is an undisclosed closure instance: http://www.html5rocks.com/en/tutorials/frameworks/angular-websockets/
Here is the link to that specific line in the codebase: https://github.com/socketio/socket.io-client/blob/master/socket.io.js#L1597
Socket.io version 0.9.16 implements removeListener but not off.
You can use removeListener instead of off when unsubscribing, or simply implement off as follows:
var socket = io.connect(url);
socket.off = socket.removeListener;
If you are using the Backbone listenTo event subscription approach, you'll need to implement the above as Backbone calls off when unsubscribing events.
I found that in socket.io 0.9.11 and Chrome24 socket.io removeListener doesn't work.
this modified version works for me:
EventEmitter.prototype.removeListener = function (name, fn) {
if (this.$events && this.$events[name]) {
var list = this.$events[name];
if (io.util.isArray(list)) {
var pos = -1;
for (var i = 0, l = list.length; i < l; i++) {
if (list[i].toString() === fn.toString() || (list[i].listener && list[i].listener === fn)) {
pos = i;
break;
}
}
if (pos < 0) {
return this;
}
list.splice(pos, 1);
if (!list.length) {
delete this.$events[name];
}
} else {
if (list.toString() === fn.toString() || (list.listener && list.listener === fn)) {
delete this.$events[name];
}
}
}
return this;
};
Since I had a spot of troubles making this work figured I'd chime in here as well, along with a nice updated answer for 2017. Thanks to #Pjotr for pointing out that it has to be the same callback instance.
Example with Angular2 TypeScript in a socket-io.subscriber service. Note the "newCallback" wrapper
private subscriptions: Array<{
key: string,
callback: Function
}>;
constructor() {
this.subscriptions = [];
}
subscribe(key: string, callback: Function) {
let newCallback = (response) => callback(response);
this.socket.on(key, newCallback);
return this.subscriptions.push({key: key, callback: newCallback}) - 1;
}
unsubscribe(i: number) {
this.socket.removeListener(this.subscriptions[i].key, this.subscriptions[i].callback);
}
Removing an event listener on the client
var Socket = io.connect();
Socket.removeListener('test', test);
Also on java client, it can be done the same way with the Javascript client. I've pasted from socket.io.
// remove all listeners of the connect event
socket.off(Socket.EVENT_CONNECT);
listener = new Emitter.Listener() { ... };
socket.on(Socket.EVENT_CONNECT, listener);
// remove the specified listener
socket.off(Socket.EVENT_CONNECT, listener);
Pre-store the events using an array, and by the time you need to unsubscribe them, use the off method, which is a built in method from socket.io:
// init
var events = []
// store
events.push("eventName")
// subscribe
socket.on("eventName", cb)
// remove
events = events.filter(event => event!="eventName")
// unsubscribe
socket.off("eventName")
To add to #Andrew Magee, here is an example of unsubscribing socket.io events in Angular JS, and of course works with Vanilla JS:
function handleCarStarted ( data ) { // Do stuff }
function handleCarStopped ( data ) { // Do stuff }
Listen for events:
var io = $window.io(); // Probably put this in a factory, not controller instantiation
io.on('car.started', handleCarStarted);
io.on('car.stopped', handleCarStopped);
$scope.$on('$destroy', function () {
io.removeListener('car.started', handleCarStarted);
io.removeListener('car.stopped', handleCarStopped);
});
This has helped me in both Angular 8 and React 16.8:
receiveMessage() {
let newCallback = (data) => {
this.eventEmitter.emit('add-message-response', data);
};
this.socket.on('add-message-response', newCallback);
this.subscriptions.push({key: 'add-message-response', callback: newCallback});
}
receiveMessageRemoveSocketListener() {
this.findAndRemoveSocketEventListener('add-message-response');
}
findAndRemoveSocketEventListener (eventKey) {
let foundListener = this.subscriptions.find( (subscription) => subscription.key === eventKey );
if(!foundListener) {
return;
}
this.socket.removeListener(foundListener.key, foundListener.callback);
this.subscriptions = this.subscriptions.filter( (subscription) => subscription.key !== eventKey );
}
Reason for using an Array of Subscriptions is that when you Subscribe to an event multiple times and you don't remove an unsubscribed subscription from the Subscription list you will most probably be right at first time you remove the subscription from the list, but later subscriptions will not be removed as you will be finding first instance only every time you unsubscribe the event.
You can simply call receiveMessage(); to subscribe to an the event and receiveMessageRemoveSocketListener(); to Unsubscribe.