Modifying callback parameters in JavaScript - javascript

I'm creating a service in angular to track events, and I want the controllers to be as dumb as possible about the event tracking. For this service each controller will need to supply its own copy of the event function. My current solution is below.
//tracking module
let tracked_events = ['Event1','Event2']
.map(function(e){
return {
eventName:e,
eventProperties: {}
}
});
let finishingTracking = (event) => {
console.log("prentend this does something fancy...",event);
}
let track = (eventName,fn) => {
var event = tracked_events.filter(e => e.eventName===eventName)[0];
fn(event.eventProperties);
//some other internal tracking function
finishTracking(event);
}
//end tracking module
//called from a controller
track("Event 1",(event) => {
event["User ID"] = 123;
event["Company ID"] = 12
});
//some other controller
track("Event 2",(event) => {
event["Manager ID"] = 345;
event["Time Sent"] = Date.now()
}
This works well because each controller will only have to provide its own way to modify the event object and it won't know anything else about the tracking module. From a design perspective, is this ok? I'm not sure about modifying the callback parameters (V8 optimizations/side effects), but I can't think of another way that doesn't cause more changes to each controller that needs to do its own event tracking. Any suggestions on this?
EDIT (prior to refactor)
var eventProperties = { table: "Machines, has_rows: /*rows...*/ }
some_service.list(data)
.then(function (response) {
tracking_service.track({
eventName: 'Event 1',
eventProperties: eventProperties
});
AFTER
some_service.trackEvent(some_service.events.EVENT1, function (event) {
event["Table"] = "Machines";
event["Has Rows"] = response.data.machines.length > 0;
});
An object of shape {eventName: "Name", eventProperties:{} } is currently being defined in each controller. My solution was to pass in the eventName from a constant defined in the service and the callback function modifies the eventProperties of the object. Each controller will have a different set of properties in eventProperties.
After the callback in version two runs, the service does the actual "tracking". My aim was to have a function that prepared the eventProperties object and added whatever properties it needed before being actually tracked.
self.trackEvent = function (eventName, fn) {
//var event = TRACKED_EVENTS.filter(e => e.eventName === eventName)[0];
var event = {
eventName: self.events[eventName],
eventProperties: { }
}
fn(event.eventProperties); //this is the callback that does the prep
self.track(event); //does the actually tracking
}

Related

Call function from another function with parameters passed from both functions

I have this load-more listener on a button that calls the functions and it works fine.
let moviesPage = 1;
let seriesPage = 1;
document.getElementById('load-more').addEventListener('click', () => {
if (document.querySelector('#movies.active-link')) {
moviesPage++;
getMovies(moviesPage);
//getMovies(genreId, moviesPage);
} else if (document.querySelector('#series.active-link')) {
seriesPage++;
getSeries(seriesPage);
}
});
Now I have another listener on a list of links that calls the following code. It takes the genreId from the event parameter to sent as an argument to the api call. Also works fine so far.
document.querySelector('.dropdown-menu').addEventListener('click',
getByGenre);
function getByGenre (e) {
const genreId = e.target.dataset.genre;
movie.movieGenre(genreId)
.then(movieGenreRes => {
ui.printMovieByGenre(movieGenreRes);
})
.catch(err => console.log(err));
};
What I want to do is to call getByGenre from the load-more listener while passing also the moviesPage argument as you can see on the commented code so it can also be passed to the api call.
What would be the best way to do that? I've looked into .call() and .bind() but I'm not sure if it's the right direction to look at or even how to implement it in this situation.
Short Answer
Kludge: Global State
The simplest, though not the most elegant, way for you to solve this problem right now is by using some global state.
Take a global selection object that holds the selected genreId. Make sure you declare the object literal before using it anywhere.
So, your code might look something like so:
var selection = { };
document.querySelector('.dropdown-menu').addEventListener('click',
getByGenre);
function getByGenre (e) {
const genreId = e.target.dataset.genre;
selection.genreId = genreId;
movie.movieGenre(...);
};
...
let moviesPage = 1;
let seriesPage = 1;
document.getElementById('load-more').addEventListener('click', () => {
if (document.querySelector('#movies.active-link')) {
...
if (selection.genreId !== undefined) {
getMovies(selection.genreId, moviesPage);
}
} else if (...)) {
...
}
});
Closure
A more elegant way for you to accomplish this is by using a closure, but for that I have to know your code structure a bit more. For now, global state like the above will work for you.
Longer Answer
Your concerns have not been separated. You are mixing up more than one concern in your objects.
For e.g. to load more movies, in your load-more listener, you call a function named getMovies. However, from within the .dropdown-menu listener, you call into a movie object's method via the getByGenre method.
Ideally, you want to keep your UI concerns (such as selecting elements by using a query selector or reading data from elements) separate from your actual business objects. So, a more extensible model would have been like below:
var movies = {
get: function(howMany) {
if (howMany === undefined) {
howMany = defaultNumberOfMoviesToGetPerCall;
}
if (movies.genreId !== undefined) {
// get only those movies of the selected genre
} else {
// get all kinds of movies
}
},
genreId : undefined,
defaultNumberOfMoviesToGetPerCall: 25
};
document.get...('.load-more').addEventListener('whatever', (e) => {
var moviesArray = movies.get();
// do UI things with the moviesArray
});
document.get...('.dropdown-menu').addEventListener('whatever', (e) => {
movies.genreId = e.target.dataset.genreId;
var moviesArray = movies.get();
// do UI things with the moviesArray
});

How to create and trigger events on objects rather than elements

I'm writing a small library that essentially polls a site for data, and is then supposed to notify a consumer when it matches. In C# I'd use events, which are actually multicast delegates. I've written my own multicast delegate in javascript before, but I figure there has to be a better way.
A consumer should register a callback which should be called when data is available. Something like this:
window.MyLibrary.dataAvailable(function(data) {
// do something with data
});
In the background MyLibrary is polling for data. When it finally has something that matches, it should execute the registered function(s). Multiple functions should be able to be registered and probably unregistered too.
CustomEvent is very very close to what I want. The problem with CustomEvent is that the event has to be raised on an element - it can't be raised on an object. That is, this wouldn't work:
var event = new CustomEvent('dataAvailable', { data: 'dynamic data' });
window.MyLibrary.addEventListener('dataAvailable', function (e) {
// do something with e.data
}, false);
// From somewhere within MyLibrary
this.dispatchEvent(event, data);
How do you register handlers on objects in javascript? I need to support the usual browsers and IE11+. Ideally I wouldn't be pulling in a library to do any of this. jQuery will be available on the page and can be used if that would make things easier.
For reference, this is the Multicast Delegate implementation I've used in the past:
function MulticastDelegate(context) {
var obj = context || window,
handlers = [];
this.event = {
subscribe: function (handler) {
if (typeof (handler) === 'function') {
handlers.push(handler);
}
},
unsubscribe: function (handler) {
if (typeof (handler) === 'function') {
handlers.splice(handlers.indexOf(handler), 1);
}
}
};
this.execute = function () {
var args = Array.prototype.slice.call(arguments);
for (var i = 0; i < handlers.length; i++) {
handlers[i].apply(obj, args);
}
};
}
var myEvent = new MulticastDelegate();
myEvent.event.subscribe(function(data) { ... }); // handler 1
myEvent.event.subscribe(function(data) { ... }); // handler 2
myEvent.execute(some_data);

How to encapsulate single and temporal events in a service?

I'm trying to encapsulate the events in a service in order to implement a mechanics to subscribe / unsubscribe the listeners when a controller's scope is destroyed. This because I have been using the rootScope.$on in the following way:
if(!$rootScope.$$listeners['event']) {
$rootScope.$on('event', function(ev, data){
// do some...
});
}
or
$scope.$on('$destroy', function(ev, data){
// unsubscribe the listener
});
So I just need one listener of this event, I need to delete the existing listener when the controller is no longer alive, because the function I registered earlier is still being triggered.
So I need to implement a $destroy event listener on my controller, to destroy the listener when the scope is destroyed, but I don't want to do that code each time I create an event.
That's why I want to create a service in where I'm going to encapsulate the events.
angular.module('core').factory('event', [
function() {
var service = {};
service.events = {};
service.on = function(scope, eventId, callback) {
scope.$on('$destroy', function(ev, other){
//unsubscribe
});
service.events[eventId] = callback;
// scope = null; I guess ?
};
service.emit = function(eventId, data){
if (service.events[eventId])
service.events[eventId](data);
else
return new Error('The event is not subscribed');
};
return service;
}
]);
This could be done using $rootScope instead of my own methods but encapsulating the $on and $emit of $rootScope, but at the end I'll have the same issue here.
So these are my questions:
Is a good practice to pass the scope ref value to a service?
What is the meaning of $$destroyed? when this is true means that angularJS has no internal references to the instance?
Should I do a scope = null in my service to let GC delete the object or does angularJS handle an explicit delete?
Is there a better way to do what I want?
What you are trying to accomplish is basically an event bus.
You have also described very well what is wrong with the current implementation.
A different way to approach the problem is to decorate the $rootScope with your bus (or any other event bus for that matter). Here is how:
app.config(function ($provide) {
$provide.decorator('$rootScope', ['$delegate', '$$bus', function ($delegate, $$bus) {
Object.defineProperty($delegate.constructor.prototype, '$bus', {
get: function () {
var self = this;
return {
subscribe: function () {
var sub = $$bus.subscribe.apply($$bus, arguments);
self.$on('$destroy',
function () {
console.log("unsubscribe!");
sub.unsubscribe();
});
},
publish: $$bus.publish
};
},
enumerable: false
});
return $delegate;
}]);
});
Considering the following $$bus implementation (kept basic for simplicity):
app.factory('$$bus', function () {
var api = {};
var events = {};
api.subscribe = function (event) {
if (!events.hasOwnProperty(event.name)) {
events[event.name] = [event];
} else {
events[event.name].push(event);
}
return {
unsubscribe: function () {
api.unsubscribe(event);
}
}
};
api.publish = function (eventName, data) {
if (events.hasOwnProperty(eventName)) {
console.log(eventName);
angular.forEach(events[eventName], function (subscriber) {
subscriber.callback.call(this, data);
});
}
};
api.unsubscribe = function (event) {
if (events.hasOwnProperty(event.name)) {
events[event.name].splice(events[event.name].indexOf(event), 1);
if (events[event.name].length == 0) {
delete events[event.name];
}
}
};
return api;
});
Now all you have to do is subscribe or publish events. The unsubscribe will take place automatically (when the $scope is destroyed):
$scope.$bus.subscribe({
name: 'test', callback: function (data) {
console.log(data);
}
});
And later on publish an event:
$scope.$bus.publish('test', {name: "publishing event!"});
An important point to make is that the events themselves are subscribed to each individual $scope and not on the $rootScope. That is how you "know" which $scope to release.
I think it answers your question. With that in mind, you can obviously make this mechanism much sophisticated (such as controller event listener released when a view routed, unsubscribe automatically only to certain events, etc.).
Good luck!
** This solution is taken form Here which uses a different bus framework (other then that it is the same).

addEventListener on custom object

I've created an object that has several methods. Some of these methods are asynchronous and thus I want to use events to be able to perform actions when the methods are done. To do this I tried to add the addEventListener to the object.
jsfiddle
var iSubmit = {
addEventListener: document.addEventListener || document.attachEvent,
dispatchEvent: document.dispatchEvent,
fireEvent: document.fireEvent,
//the method below is added for completeness, but is not causing the problem.
test: function(memo) {
var name = "test";
var event;
if (document.createEvent) {
event = document.createEvent("HTMLEvents");
event.initEvent(name, true, true);
} else {
event = document.createEventObject();
event.eventType = name;
}
event.eventName = name;
event.memo = memo || { };
if (document.createEvent) {
try {
document.dispatchEvent(event);
} catch (ex) {
iAlert.debug(ex, 'iPushError');
}
} else {
document.fireEvent("on" + event.eventType, event);
}
}
}
iSubmit.addEventListener("test", function(e) { console.log(e); }, false);
//This call is added to have a complete test. The errors are already triggered with the line before this one.
iSubmit.test();
This will return an error: Failed to add eventlisterens: TypeError: 'addEventListener' called on an object that does not implement interface EventTarget."
Now this code will be used in a phonegap app and when I do, it is working on android/ios. During testing, however, it would be nice if I could get it to work in at least a single browser.
PS> I know I could enable bubbling and then listen to the document root, but I would like to have just a little bit OOP where each object can work on its own.
addEventListener is intended for DOM Elements that implements certain event-related interfaces. If you want an event system on pure JavaScript objects, you are looking for a custom event system. An example would be Backbone.Events in Backbone.js. The basic idea is using an object as a hash to keep track of registered callbacks.
Personally I use this: emitter.
It's a fairly simple and elegant solution - with sweet short method names like on(), off() and emit(). you can either create new instances with new Emitter(), or use Emitter(obj) to mix event capabilities into existing objects. Note this library is written for use with a CommonJS module system, but you can use it anywhere else by removing the module.exports = ... line.
If you don't need true event features(such as bubbling, stopPropagation), then you can implement your own events. addEventListener is just an API of the DOM, so you don't really need it for your own objects outside the DOM. If you want to create an evented pattern around an object, here's a good way to do it that does not require any extra browser APIs and should be very backwards-compatible.
Let's say you have an object where you want a bunch of events to be triggered when the dispatch method is called:
var OurDispatcher, dispatcher;
OurDispatcher = (function() {
function OurDispatcher() {
this.dispatchHandlers = [];
}
OurDispatcher.prototype.on = function(eventName, handler) {
switch (eventName) {
case "dispatch":
return this.dispatchHandlers.push(handler);
case "somethingElse":
return alert('write something for this event :)');
}
};
OurDispatcher.prototype.dispatch = function() {
var handler, i, len, ref;
ref = this.dispatchHandlers;
for (i = 0, len = ref.length; i < len; i++) {
handler = ref[i];
setTimeout(handler, 0);
}
};
return OurDispatcher;
})();
dispatcher = new OurDispatcher();
dispatcher.on("dispatch", function() {
return document.body.innerHTML += "DISPATCHED</br>";
});
dispatcher.on("dispatch", function() {
return document.body.innerHTML += "DISPATCHED AGAIN</br>";
});
dispatcher.dispatch();
It really doesn't have to be more complicated than that, for the most part. This way you have some decent control over your events and you don't need to worry about backward-compatibility or external libraries because everything there is widely supported. Technically, you could even do without setTimeout and handle your callbacks without any APIs. Anything else like stopPropagation() would have to be handled yourself.
https://jsfiddle.net/ozsywxer/
There are, of course, polyfills for CustomEvent, but unless I need advanced event features, I prefer to wrap my own eventing system into a "class" and extending other classes/functions with it.
Here's the CoffeeScript version, which is what the JavaScript is derived from:
https://jsfiddle.net/vmkkbbxq/1/
^^ A bit easier to understand.
If you want to listen a javascript object you have three ways:
Use sub/pub pattern which has a lot of implementations in javascript
Or use native implementation via Object get/set operators, Object.defineProperty, Object.prototype.watch or Proxy API
Use Object.observe. Works Chrome 25+(Jan 2014). But became deprecated in 2016
About sup/pub pattern:
You need to publish events.
About native implementations:
Object get/set operators is enough to listen add, remove, change,
get events. Operators have good support. Problems only in IE8-.
But if you want to use get/set in IE8 use Object.defineProperty but
on DOM objects or use Object.defineProperty sham.
Object.prototype.watch has the good ES5 polyfill.
Proxy API needs ES Harmony support.
Object.observe example
var o = {};
Object.observe(o, function (changes) {
changes.forEach(function (change) {
// change.object contains changed object version
console.log('property:', change.name, 'type:', change.type);
});
});
o.x = 1 // property: x type: add
o.x = 2 // property: x type: update
delete o.x // property: x type: delete
There are two problems.
First, the iSubmit.addEventListener() method is actually a method on the EventTarget DOM interface:
EventTarget
EventTarget # addEventListener()
These are inteded for use only on DOM elements. By adding it to the iSubmit object as a method, you're calling it on an object that is not an EventTarget. This is why Chrome throws an Uncaught TypeError: Illegal invocation JavaScript error.
The first problem is critical, but if you could use EventTarget#addEventListener() your code would not work because the event is being added to iSubmit but dispatched from document. Generally, the same object's methods need to be used when attaching event listeners and dispatching events (unless you're using bubbling events, which is a different story - Note: bubbling is not restricted to JavaScript or DOM related events, for example).
Using custom events with your own objects is very normal. As Evan Yu mentioned, there are libraries for this. Here are a couple:
millermedeiros / js-signals
Wolfy87 / EventEmitter
I have used js-signals and like it quite a bit. I have never used Wolfy87/EventEmitter, but it has a nice look to it.
Your example might look something like the following if you used js-signals
jsFiddle
var iSubmit = {
finished: new signals.Signal(),
test: function test(memo) {
this.finished.dispatch(memo || {});
}
};
iSubmit.finished.add(function(data) {
console.log('finished:', data);
});
iSubmit.test('this is the finished data');
// alternatively
iSubmit.finished.dispatch('this is dispatched directly from the signal');
Just speculation; I haven't tried it myself. But you can create a dummy element and fire/listen to events on the dummy element.
Also, I prefer going without libraries.
function myObject(){
//create "dummy" element
var dummy = document.createElement('dummy');
//method for listening for events
this.on = function(event, func){dummy.addEventListener(event, func);};
//you need a way to fire events
this.fireEvent = function(event, obj){
dummy.dispatchEvent(new CustomEvent(event, {detail: obj}));
}
}
//now you can use the methods in the object constructor
var obj = new myObject();
obj.on("custom", function(e){console.log(e.detail.result)});
obj.fireEvent("custom", {result: "hello world!!!"});
Here's a simple event emitter:
class EventEmitter {
on(name, callback) {
var callbacks = this[name];
if (!callbacks) this[name] = [callback];
else callbacks.push(callback);
}
dispatch(name, event) {
var callbacks = this[name];
if (callbacks) callbacks.forEach(callback => callback(event));
}
}
Usage:
var emitter = new EventEmitter();
emitter.on('test', event => {
console.log(event);
});
emitter.dispatch('test', 'hello world');
If you are in a Node.js environment then you can use Node's EventEmitter class:
CustomObject.js
const EventEmitter = require('events');
class CustomObject extends EventEmitter {
constructor() {
super();
}
doSomething() {
const event = {message: 'Hello World!'};
this.emit('myEventName', event);
}
}
module.exports = CustomObject;
Usage:
const CustomObject = require('./CustomObject');
// 1. Create a new instance
const myObject = new CustomObject();
// 2. Subscribe to events with ID "myEventName"
myObject.on('myEventName', function(event) {
console.log('Received event', event);
});
// 3. Trigger the event emitter
myObject.doSomething();
If you want to use Node's EventEmitter outside of a Node.js environment, then you can use webpack (preferably v2.2 or later) to get a bundle of your CustomClass together with an EventEmitter polyfill (built by webpack).
Here is how it works (assuming that you installed webpack globally using npm install -g webpack):
Run webpack CustomObject.js bundle.js --output-library=CustomObject
Include bundle.js in your HTML page (it will expose window.CustomObject)
There's no step three!
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Title</title>
<script src="bundle.js"></script>
</head>
<body>
<script>
// 1. Create a new instance
const myObject = new window.CustomObject();
// 2. Subscribe to events with ID "myEventName"
myObject.on('myEventName', function(event) {
console.log('Received event', event);
});
// 3. Trigger the event emitter
myObject.doSomething();
</script>
</body>
</html>
I have been able to achieve this by wrapping an element in javascript class.
Important point is that the element does not have to exist in dom. Also, the element tag name can be anything such as the custom class name.
'''
class MyClass
{
constructor(options )
{
this.el = document.createElement("MyClass");//dummy element to manage events.
this.el.obj= this; //So that it is accessible via event.target.obj
}
addEventListener()
{
this.el.addEventListener(arguments[0],arguments[1]);
}
raiseEvent()
{
//call this function or write code below when the event needs to be raised.
var event = new Event('dataFound');
event.data = messageData;
this.el.dispatchEvent(event);
}
}
let obj = new MyClass();
obj.addEventListener('dataFound',onDataFound);
function onDataFound()
{
console.log('onDataFound Handler called');
}
'''
This article explains creating custom events: http://www.sitepoint.com/javascript-custom-events/
here is an example:
create the event -
var event = new CustomEvent(
"newMessage",
{
detail: {
message: "Hello World!",
time: new Date(),
},
bubbles: true,
cancelable: true
}
);
assign the event to something -
document.getElementById("msgbox").dispatchEvent(event);
subscribe to the event -
document.addEventListener("newMessage", newMessageHandler, false);
Usage: jsfiddle
This is a naive approach but might work for some applications:
CustomEventTarget.prototype = {
'constructor': CustomEventTarget,
on: function( ev, el ) { this.eventTarget.addEventListener( ev, el ) },
off: function( ev, el ) { this.eventTarget.removeEventListener( ev, el ) },
emit: function( ev ) { this.eventTarget.dispatchEvent( ev ) }
}
function CustomEventTarget() { this.eventTarget = new EventTarget }
I think you can use Object $Deferred and promises.
It'll let you do something like this:
Stacked: bind multiple handlers anywhere in the application to the same promise event.
var request = $.ajax(url);
request.done(function () {
console.log('Request completed');
});
// Somewhere else in the application
request.done(function (retrievedData) {
$('#contentPlaceholder').html(retrievedData);
});
Parallel tasks: ask multiple promises to return a promise which alerts of their mutual completion.
$.when(taskOne, taskTwo).done(function () {
console.log('taskOne and taskTwo are finished');
});
Sequential tasks: execute tasks in sequential order.
var step1, step2, url;
url = 'http://fiddle.jshell.net';
step1 = $.ajax(url);
step2 = step1.then(
function (data) {
var def = new $.Deferred();
setTimeout(function () {
console.log('Request completed');
def.resolve();
},2000);
return def.promise();
},
function (err) {
console.log('Step1 failed: Ajax request');
}
);
step2.done(function () {
console.log('Sequence completed')
setTimeout("console.log('end')",1000);
});
Source here:
http://blog.mediumequalsmessage.com/promise-deferred-objects-in-javascript-pt2-practical-use
Here is how you do this with Node.js style syntax in the browser.
The Events class:
stores callbacks in a hash associated with event keys
triggers the callbacks with the provided parameters
To add the behavior to your own custom classes just extend the Events object (example below).
class Events {
constructor () {
this._callbacks = {}
}
on (key, callback) {
// create an empty array for the event key
if (this._callbacks[key] === undefined) { this._callbacks[key] = [] }
// save the callback in the array for the event key
this._callbacks[key].push(callback)
}
emit (key, ...params) {
// if the key exists
if (this._callbacks[key] !== undefined) {
// iterate through the callbacks for the event key
for (let i=0; i<this._callbacks[key].length; i++) {
// trigger the callbacks with all provided params
this._callbacks[key][i](...params)
}
}
}
}
// EXAMPLE USAGE
class Thing extends Events {
constructor () {
super()
setInterval(() => {
this.emit('hello', 'world')
}, 1000)
}
}
const thing = new Thing()
thing.on('hello', (data) => {
console.log(`hello ${data}`)
})
Here is a link a github gist with this code: https://gist.github.com/alextaujenis/0dc81cf4d56513657f685a22bf74893d
For anyone that's looking for an easy answer that works.
I visited this document, only to learn that most browser doesn't support it.
But at the bottom of the page, there was a link to this GitHub page that basically does what the Object.watch() and Object.unwatch() would have done, and it works for me!
Here's how you can watch for changes
/*
* object.watch polyfill
*
* 2012-04-03
*
* By Eli Grey, http://eligrey.com
* Public Domain.
* NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
* https://gist.github.com/eligrey/384583
*/
// object.watch
if (!Object.prototype.watch) {
Object.defineProperty(Object.prototype, "watch", {
enumerable: false
, configurable: true
, writable: false
, value: function (prop, handler) {
var
oldval = this[prop]
, newval = oldval
, getter = function () {
return newval;
}
, setter = function (val) {
oldval = newval;
return newval = handler.call(this, prop, oldval, val);
}
;
if (delete this[prop]) { // can't watch constants
Object.defineProperty(this, prop, {
get: getter
, set: setter
, enumerable: true
, configurable: true
});
}
}
});
}
// object.unwatch
if (!Object.prototype.unwatch) {
Object.defineProperty(Object.prototype, "unwatch", {
enumerable: false
, configurable: true
, writable: false
, value: function (prop) {
var val = this[prop];
delete this[prop]; // remove accessors
this[prop] = val;
}
});
}
And this should be your code:
var object = {
value: null,
changeValue: function(newValue) {
this.value = newValue;
},
onChange: function(callback) {
this.watch('value', function(obj, oldVal, newVal) {
// obj will return the object that received a change
// oldVal is the old value from the object
// newVal is the new value from the object
callback();
console.log("Object "+obj+"'s value got updated from '"+oldValue+"' to '"+newValue+"'");
// object.changeValue("hello world");
// returns "Object object.value's value got updated from 'null' to 'hello world'";
// and if you want the function to stop checking for
// changes you can always unwatch it with:
this.unwatch('value');
// you can retrieve information such as old value, new value
// and the object with the .watch() method, learn more here:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/watch
})
}
};
or as short as:
var object = { user: null };
// add a watch to 'user' value from object
object.watch('user', function() {
// object user value changed
});
Use the createElement to create a dummy element.
typescript
class Person {
name: string
el: HTMLElement // event listener
constructor(name: string) {
this.name = name
this.el = document.createElement("Person"); // dummy element to manage events
(this.el as any).object = this // set dummy attribute. (Optional) So that it is accessible via `event.target.object`
}
AddEventListener(type: string, listener: any) {
this.el.addEventListener(type, listener)
}
DispatchEvent(type: string, data: any = null) {
const event = new Event(type);
(event as any).data = data //dummy attribute (Optional)
this.el.dispatchEvent(event)
}
}
const carson = new Person("Carson")
carson.AddEventListener("Say", (e: Event) => {
const person = (e.target as any).object as Person // get dummy attribute
const data = (e as any).data // get dummy attribute
if (data !== undefined && data.stopImmediatePropagation === true) {
e.stopImmediatePropagation()
}
console.log(`${person.name}`, data)
})
carson.AddEventListener("Say", () => {
console.log("Say2")
})
carson.DispatchEvent("Say")
// Output:
// Carson undefined
// Say2
carson.DispatchEvent("Say", "hello world!")
// Carson hello world!
// Say2
carson.DispatchEvent("Say", {stopImmediatePropagation: true})
// Carson {stopImmediatePropagation: true}
Runnable Example
<script>
class Person {
constructor(name) {
this.name = name
this.el = document.createElement("Person") // dummy element to manage events
this.el.object = this // set dummy attribute. (Optional) So that it is accessible via `event.target.object`
}
AddEventListener(type, listener) {
this.el.addEventListener(type, listener)
}
DispatchEvent(type, data) {
const event = new Event(type)
event.data = data // set dummy attribute
this.el.dispatchEvent(event)
}
}
const carson = new Person("Carson")
carson.AddEventListener("Say", (e) => {
const person = e.target.object // get dummy attribute
const data = e.data // get dummy attribute
if (data !== undefined && data.stopImmediatePropagation === true) {
e.stopImmediatePropagation()
}
console.log(`${person.name}`, data)
})
carson.AddEventListener("Say", (e) => {
console.log("Say2")
})
carson.DispatchEvent("Say")
carson.DispatchEvent("Say", "hello world!")
carson.DispatchEvent("Say", {stopImmediatePropagation: true})
</script>
With ES6 class, object & callbacks you can create your own custom event system with the following code:
class ClassWithEvent {
//Register a new event for the class
RegisterEvent(event,Handler){
var eventName = `event_on${event}`;
if(this.hasOwnProperty(eventName) == false){
this[eventName] = [];
}
this[eventName].push(Handler);
}
//private unregister the event
#unregisterEvent(event){
var eventName = `event_on${event}`;
delete this[eventName];
}
//raise event
#dispatchEvent(name, event) {
var eventName = `event_on${name}`;
if (this.hasOwnProperty(eventName))
this[eventName].forEach(callback => callback(event));
}
//public method
sayhello(name){
this.#dispatchEvent("beforehello",{'name':name,'method':'sayhello'});
alert(`Hello ${name}`);
this.#dispatchEvent("afterhello",{'name':name,'method':'sayhello'});
}
}//EOC
Once defined you can call it as:
var ev = new ClassWithEvent();
ev.RegisterEvent("beforehello",(x)=> console.log(`Event:before ${x.name} ${x.method} oh`));
ev.RegisterEvent("afterhello",(x)=> console.log(`Event:after ${x.name} ${x.method} oh`));
ev.RegisterEvent("beforehello",(x)=> console.log(`Event2:before ${x.name} ${x.method} oh`));
ev.sayhello("vinod");
So in the code above we have registered 3 events handlers which will be invoked by #dispatchEvent() when we call the sayhello() method.
The instance of the class will look something like this:
We can see in the image above the onbeforehello event has two handlers and it will be invoke in the sequence it is defined.

Object-Oriented Javascript custom addEvent method

I have a question regarding developing object-orientated javascript and parsing variables. Please see this blog by net.tutsplus - The Basics of Object-Oriented JavaScript for more info.
I have the following code which creates an event:
$(document).ready(function() {
tools.addEvent('.someButton', 'click', user.getUserDetails);
)};
var tools = {
addEvent: function(to, type, fn) {
$(to).live(type, fn);
}
}
var user = {
getUserDetails: function(userID) {
console.log(userID);
}
}
As you can see, it calls the addEvent method with three variables; the DOM element to attach the event to, the type of the event and the function to run when the event is triggered.
The issue I am having is parsing a variable to the getUserDetails method and I know of 2 options:
I could obviously have 1 line of code at the start which could check an attribute of the sender. For example, the .someButton could have an attribute userID="12345". However, this is not ideal because the function is run from several different places - meaning this check is not always available (and the code is harder to manage).
A better option could be to have another method like user.willGetUserDetails and use this method to get the attribute userID from the DOM. This could be run from anywhere on the page, and would call getUserDetails after getting the userID. Whenever the user details comes from within another function, we would simply call getUserDetails directly.
What would be ideal, is if I could amend the code above to pass a variable directly - even an undefined one. Does anyone know how this could be achieved?
Add one more argument to your addEvent code that accepts data to pass to the event.
var tools = {
addEvent: function(to, type, data, fn) {
if ($.isFunction(data)) {
fn = data;
data = {};
}
$(to).live(type, data, fn);
}
}
Also, i'd suggest using delegate instead, or .on in 1.7+
var tools = {
addEvent: function(to, type, data, fn) {
if ($.isFunction(data)) {
fn = data;
data = {};
}
$(document).delegate(to, type, data, fn);
}
}
or
var tools = {
addEvent: function(to, type, data, fn) {
if ($.isFunction(data)) {
fn = data;
data = {};
}
$(document).on(type, to, data, fn);
}
}
Now you can use it like this:
$(document).ready(function() {
tools.addEvent('.someButton', 'click', {userID: theuserid}, user.getUserDetails);
)};
var user = {
getUserDetails: function(event) {
console.log(event.data.userID);
}
}

Categories