Wait for element to render and then execute some function - javascript

I have gone through so many convoluted answers to this question and no one seems to be able to give a simple solution....
I typically use a setTimeout function to wait for all elements on a page to load before doing some work. My job requires me to wait for the angular app to load before I can manipulate things in the DOM. Using a setTimeout generally works but every now and again a customer is using a potato with a screen and when their CPU is as slow as a snail my DOM manipulation functions fail to execute in time.
The following block of code works for what I need it to do in 99% of instances but in this particular case the customer is reporting that it is not working on their end:
setTimeout(() => {
const commentLabel = document.querySelectorAll('.donation-label');
commentLabel.forEach((element) => {
if (element.textContent.includes('Leave a comment')) {
element.innerHTML = 'Tell us how to improve';
}
});
}, 750);
Instead of using a setTimeout where I am waiting for some duration of time to execute the "work", I want to use something that simply waits for the element with ".donation-label" (or any other selector of my choosing) to exist and then do something.
This needs to be in vanilla JS (no Jquery). I'm finding answers related to MutationObservers and async but can't find anything simple or anything that even works for that matter. Please help. Thank you very much!

After working with a colleague, we came up with this as the simplest and cleanest working version of what I was looking for....
function customize(_, observer) {
let commentLabel = document.querySelectorAll('.comment-label');
commentLabel.forEach((elm) => {
if (elm.textContent.includes('Leave a comment')) {
elm.innerHTML = 'Tell us how we can improve';
}
});
if (commentLabel.length > 0 && observer) {
observer.disconnect();
console.log('observer disconnected');
}
}
new MutationObserver(customize).observe(document, { childList: true, subtree: true });
Thank you all for your input!!

This worked for me once in the past. Perhaps you can adjust it for your case, It is simply a setInterval that checks for the DOM element until a maximum number of checks have been made. The interval time and attempts are hardcoded but you could pass those in the function invocation.
function elemWait () {
return new Promise( ( resolve, reject ) => {
let clear, elem, maxAttempts = 0;
try {
clear = setInterval( wait, 200 );
function wait() {
elem = document.querySelectorAll( ... );
if ( elem ) {
clearInterval( clear );
... DO SOMETHING with element...
resolve();
} else if ( ++maxAttempts === 7 ) {
clearInterval( clear );
reject(..reason...);
}
} // close wait
} catch ( e ) {
clearInterval( clear );
reject(..reason...);
}
});
}

You can use a "Mutation Observer" to watch your body (or any other target) to elements addings and removals. To watch any DOM target, you could write a watcher like this:
function watch(target, cb, options) {
/** have sure that you have no duplicated observers **/
if (window.__customChildListObserver) {
return false;
}
/** observing our body and waiting for added components **/
const observer = new MutationObserver((mutationList, observer) => {
const changings = mutationList.reduce((acc, m) => {
acc.push(...Array.from(m.addedNodes).map(node => ({ type: 'added', node })));
acc.push(...Array.from(m.removedNodes).map(node => ({ type: 'removed', node })));
return acc;
}, []);
changings.forEach(change => (cb && cb(change)));
});
/** start to watch the target, we want to observe only childlist and subtree **/
observer.observe(target, options || { attributes: false, childList: true, subtree: true });
/** informs the window of our observer **/
window.__customChildListObserver = observer;
}
And you can use your watcher to wait for a .donation-label like this:
/**
* everytime body receives a node, you will be informed with the mutation
* {type: 'added', node: p}
*
* everytime body has a node removed, you be informed with the mutation
* {type: 'removed', node: button}
*/
watch(document.body, (mutation) => {
if (mutation.type === 'added' && mutation.node.classList.contains('.donation-label')) {
/** .donation-label has been attached somewhere in the body **/
const element = mutation.node;
/** do whatever you want with the .donation-label element **/
}
});
If you have to take in account that .donation-label can be in the DOM before you have setted you watcher, you can write a function to search for this element before start the watcher, than you will be sure that you will always grab the desired element.
Reference about the Mutation Observer:
https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver

Related

Allow other JavaScript functions (including from other files) to "hook" into my function

I feel like I'm overthinking this concept, but I'm stuck on it. Does JavaScript have the ability to allow other functions (from the same or different JS files) to hook into a function?
To clarify, if I create an array in a function I am writing is it possible to allow other JS files to hook into it to modify that array before I parse it? Or would I need to build some kind of system to manage all of the "hooks"? I'd like to avoid using a global window variable if possible, but that would work.
Here's an example
For sake of argument, let's assume these run in the order I've listed them (ignore the race condition I've created here).
main.js
var animals = ['cat', 'dog'];
animals = hookIntoThis(animals); //Allow other functions or files to add/modify animals to the array here
console.log(animals); //--> "cat", "dog", "elephant"
hookIntoThis() is not actually a reference to anything- that would just be where I'd want to let other functions/files (if they exist) modify the array.
other.js
function addMoreAnimals(animals){ //This would somehow listen for that hook from main.js
animals.push('elephant');
animals.push('lion');
return animals;
}
whatever.js
function noLions(animals){ //This would also hook into that function from main.js to modify the array
if ( animals.indexOf('lion') >= 0 ){
animals.splice(animals.indexOf('lion'), 1); //Just for the sake of example
}
return animals;
}
At first I was thinking a custom DOM event/listener would work, but that is (as far as I know) a one-way thing. I could trigger, and even pass the animals array to the DOM event, but unless I use a global window variable I couldn't get any data back.
I apologize if this is an overly simple concept and/or a duplicate question. I did search around quite a bit before posting this.
The custom event listener is the right one, I think, if main doesn't know which other modules are going to be modifying animals.
If you know that all hooks will be executed synchronously, you can have main do its main work (after modification) in a microtask, once the main thread has finished, eg:
// main.js
let modifiableAnimals = ['cat', 'dog'];
window.addEventListener('customHook', ({ detail: modifyFn }) => {
modifiableAnimals = modifyFn(modifiableAnimals);
});
Promise.resolve()
.then(() => {
console.log(modifiableAnimals);
});
// other.js
window.dispatchEvent(
new CustomEvent(
'customHook',
{
detail: (animals) => {
animals.push('elephant');
animals.push('lion');
return animals;
}
}
)
);
// whatever.js
window.dispatchEvent(
new CustomEvent(
'customHook',
{
detail: (animals) => {
if ( animals.indexOf('lion') >= 0 ){
animals.splice(animals.indexOf('lion'), 1); //Just for the sake of example
}
return animals;
}
}
)
);
If you know that the existing array is only going to be mutated (and doesn't have to be reassigned), you can simplify it a bit:
// main.js
const modifiableAnimals = ['cat', 'dog'];
window.addEventListener('customHook', ({ detail: modifyFn }) => {
modifyFn(modifiableAnimals);
});
Promise.resolve()
.then(() => {
console.log(modifiableAnimals);
});
// other.js
window.dispatchEvent(
new CustomEvent(
'customHook',
{
detail: (animals) => {
animals.push('elephant');
animals.push('lion');
}
}
)
);
// whatever.js
window.dispatchEvent(
new CustomEvent(
'customHook',
{
detail: (animals) => {
if ( animals.indexOf('lion') >= 0 ){
animals.splice(animals.indexOf('lion'), 1); //Just for the sake of example
}
}
}
)
);
This is still kind of weird to do though, IMO. If possible, I'd consider having something else interface with main.js and the other files, and call main.js functions with the handlers instead:
// main.js
function changeAnimals(...changeFns) {
const modifiableAnimals = ['cat', 'dog'];
changeFns.forEach(fn => fn(modifiableAnimals));
console.log(modifiableAnimals);
}
// other.js
const pushElephantAndLion = (animals) => {
animals.push('elephant');
animals.push('lion');
};
// whatever.js
const removeOneLion = (animals) => {
if ( animals.indexOf('lion') >= 0 ){
animals.splice(animals.indexOf('lion'), 1); //Just for the sake of example
}
};
// *actual* main code:
changeAnimals(pushElephantAndLion, removeOneLion);

How to programmatically add HTML DOM breakpoints in chromium?

I've seen from this post about Chromium DevTools that there exists the possibility to add DOM breakpoints. Given that I've a full range of elements to monitor I was trying to find a way to programmatically add such breakpoints. I also read this question about DOM breakpoints but it doesn't seem to give me any useful hint.
To achieve a similar result I've used to instrument the setAttribute() function of such DOM elements replacing it with a wrapper that uses the debugger; instruction to trigger the debugger. Anyway this approach fails when dealing with innerHTML or innerText assignments given that there is no way of achieving operator overloading in js.
Can someone suggest me a practical solution?
You may want to use MutationObserver, to observe for any change to a DOM at given root element. Also you can put debugger there and if devTools is open it should break.
const targetNode = document.getElementById('observed-element');
const config = { attributes: true, childList: true, subtree: true };
// Callback function to execute when mutations are observed
const callback = (mutationList, observer) => {
for (const mutation of mutationList) {
console.log(mutation.type);
console.log(mutation.target);
debugger;
}
};
// Create an observer instance linked to the callback function
const observer = new MutationObserver(callback);
// Start observing the target node for configured mutations
observer.observe(targetNode, config);
// change class
setTimeout(()=>{
targetNode.setAttribute('class', 'some-class')
}, 0);
// change innerText
setTimeout(()=>{
targetNode.innerText = 'some text';
}, 0);
<div id="observed-element">
</div>
You need to open Devtools-Over-Devtools and get references to instances of DOMModel and DOMDebuggerModel
// open Devtools (Ctrl+Shift+I)
// open DevtoolsOverDevtools (Ctrl+Shift+I in Devtools)
// open sdk.js from Ctrl+P pane
// set breakpoint in function setDOMBreakpoint(e, t)
// set HTML breakpoint in Devtools to pause on the created breakpoint
// inside setDOMBreakpoint(e, t)
window.domModel = e.domModel()
window.domDebuggerModel = this
// resume execution, disable breakpoint
_k = [...domModel.idToDOMNode.keys()][0]
_a = await domModel.querySelectorAll(_k, 'div')
_b = _a.map(e => domModel.idToDOMNode.get(e)).filter(Boolean)
_b.map(e => domDebuggerModel.setDOMBreakpoint(e, 'node-removed'))
// 'subtree-modified' | 'attribute-modified' | 'node-removed'
// now all elements are breakpointed
window.DEBUG = true; // toggles programmatic debugging
flag with a global check debug function, like so:
window.CHECK_DEBUG = function() {
if (window.DEBUG) { debugger; }
}
And then insert the following in any function you’re concerned about debugging:
function foo() {
CHECK_DEBUG();
// foo's usual procedure ...
}
To take this a step further (and to take a page out of Firebug's debug() and undebug() wrapper functions) you can decorate the native JavaScript Function object like so:
Function.prototype.debug = function(){
var fn = this;
return function(){
if (window.DEBUG) { debugger; }
return fn.apply(this, arguments);
};
};
Then you can dynamically debug any function by calling:
foo = foo.debug();

How to show percent done in terminal

Good day all.
Today i'm working in this complex script that makes request's to a site with server-side rendering, get's the HTML, breaks and grabs some data. The script has 4 phases: phaseOne, phaseTwo, phaseThree and phaseFour.
Which phases has a similar interface:
class PhaseOne {
constructor(MAP) {
this.MAP = MAP || MAP;
}
// All code related with the phase here.
process() {}
}
So i'm working upon this MAP object in all phases, and i'm calling each phase in a stack, like this:
let phases = require('./phases');
[
// 'Initial',
'PhaseOne',
'PhaseTwo',
'PhaseThree',
'PhaseFour',
].reduce((m, fn) => {
return new phases[fn](m).process();
}, MAP);
Everything is working fine. My problem is: Some phases are REALLY slow.. all the program will take 30 minutes to finish.. and i would like to see in my terminal the percentage of each phase.
Like:
PhaseOne: 10%
PhaseOne: 11%
PhaseOne: 12%
But i don't have any idea and i can't find a good tutorial to do that..
Currently inside my process functions i have for loops, if statements.. in general i'm using imperative style..
An example of PhaseOne:
// In this phase we need the property string in MAP.aguia01 to
// assign the first context and extract the data with regex.
if (typeof this.MAP.aguia01.string === 'undefined') {
cli.fatal(
'MAP doesn\'t have the necessary properties to run in Aguia01 phase. Exiting...'
);
}
for (let month of months) {
this.MAP.aguia01.string += requests.aguia01.apply(this, [month]);
}
for (let info of patterns.aguia01.infos) {
this.MAP.aguia01.infos[info.name] = (
this.MAP.aguia01.string.match(info.pattern)
)[1];
}
for (let line of patterns.aguia01.lines) {
this.MAP.aguia01.lines[line.name] = (
this.MAP.aguia01.string.match(line.pattern)
)[1];
}
So.. Is it possible to do what i want with imperative style?
Thanks.
There is the progress package but it's only up to you how you define "progress". You define a number of ticks corresponding to the completed state and then, you just call a method on the progress bar to make it "progress". An example:
var ProgressBar = require('progress');
// 10 ticks to complete the task
var bar = new ProgressBar(':bar', { total: 10 });
var timer = setInterval(function () {
// make the bar tick(), each call will make a 10% progress
bar.tick();
if (bar.complete) {
console.log('\ncomplete\n');
clearInterval(timer);
}
}, 100);
How about keeping a context object for progress outside of your reduce call? You could make it an event emitter, and then pass it in to your process function. Inside your process function you could emit progress events, which could then be logged. Perhaps something like this:
let phases = require('./phases');
//set up
let progressMonitor = new require('events')
progressMonitor.on('progress', percentDone => {
console.log(percentDone + '% done')
})
// your existing code
[
// 'Initial',
'PhaseOne',
'PhaseTwo',
'PhaseThree',
'PhaseFour',
].reduce((m, fn) => {
return new phases[fn](m).process(progressMonitor);
}, MAP);
and then inside your process functions:
class PhaseOne {
constructor(MAP) {
this.MAP = MAP || MAP;
}
// All code related with the phase here.
process(progressMonitor) {
//do work
progressMonitor.emit('progress', 10)
//more work
progressMonitor.emit('progress', 15)
//much work
progressMonitor.emit('progress', 75)
}
}

Listen for all events in JavaScript

I'm trying to figure out how to listen for all events on a JavaScript object.
I know that I can add individual events with something like this
element.addEventListener("click", myFunction);
element.addEventListener("mouseover", myFunction);
...
I'm trying to figure out if there is a catch-all, I'd like to do something like this:
// Begin pseudocode
var myObj = document.getElementById('someID');
myObj.addEventListener(/*catch all*/, myFunction);
function myFunction() {
alert(/*event name*/);
}
// End pseudocode
A more modern rewrite of #roman-bekkiev's answer:
Object.keys(window).forEach(key => {
if (/^on/.test(key)) {
window.addEventListener(key.slice(2), event => {
console.log(event);
});
}
});
Note that you can further customize what you want to catch, for example:
/^on(key|mouse)/.test(key)
To pick up standard element's events.
var myObj = document.getElementById('someID');
for(var key in myObj){
if(key.search('on') === 0) {
myObj.addEventListener(key.slice(2), myFunction)
}
}
But as #jeremywoertink mentioned any other events are also possible.
I hate that this problem persists without a native or elegant solution.
A Better Solution?
This allows you to subscribe to a single CustomEvent for any EventTarget using target.addEventListener('*', ...).
clear();
/**
* #param : source := EventTarget
* * EventTarget.prototype
* * Node (Element, Attr, etc)
* #usage : [Node].addEventListener('*', ({ detail: e }) => {...}, false);
*/
function proxyEventTargetSource(source) {
var emit = source.dispatchEvent; // obtain reference
function proxy(event) {
var { type } = event, any = new CustomEvent('*', { detail: event }); // use original event as detail
if (!{ '*': true }[ type ]) emit.call(this, any); // only emit "any" if type is not any.type ('*')
return emit.call(this, event);
}
if ({ 'dispatchEvent': true }[ emit.name ]) source.dispatchEvent = proxy; // attempt overwrite only if not already set (avoid rewrapping)
return (source.dispatchEvent === proxy); // indicate if its set after we try to
}
// proxyEventTargetSource(EventTarget.prototype); // all targets
proxyEventTargetSource(document); // single target
var e = new CustomEvent('any!', { detail: true });
document.addEventListener('*', (e) => console.log('type: %s, original: %s, e: %O', e.type, e.detail.type, e), false);
document.dispatchEvent(e);
Granted, a more native or [perhaps] more elegant way would be to use a native Proxy on apply for the target's dispatchEvent method, but that would maybe convey less for the sake of this post.
Gist: https://gist.github.com/cScarlson/875a9fca7ab7084bb608fb66adff0463
Known Issues
Apparently, this only works while driving event-dispatching through EventTargets's dispatchEvent method. That is, naturally triggering events through mouse events (for instance) does not work. There would need to be a way to wrap the internal method being called by natural event-triggers.
That being said, if you have a way around this, please show what you have in another answer.
As far as I know, it's possible.
For all native events, we can retrieve a list of supported events by iterating over the target.onevent properties and installing our listener for all of them.
for (const key in target) {
if(/^on/.test(key)) {
const eventType = key.substr(2);
target.addEventListener(eventType, listener);
}
}
The only other way that events are emitted which I know of is via EventTarget.dispatchEvent, which every Node and thefore every Element inherits.
To listen for all these manually triggered events, we can proxy the dispatchEvent method globally and install our listener just-in-time for the event whose name we just saw ✨ ^^
const dispatchEvent_original = EventTarget.prototype.dispatchEvent;
EventTarget.prototype.dispatchEvent = function (event) {
if (!alreadyListenedEventTypes.has(event.type)) {
target.addEventListener(event.type, listener, ...otherArguments);
alreadyListenedEventTypes.add(event.type);
}
dispatchEvent_original.apply(this, arguments);
};
🔥 function snippet 🔥
function addEventListenerAll(target, listener, ...otherArguments) {
// install listeners for all natively triggered events
for (const key in target) {
if (/^on/.test(key)) {
const eventType = key.substr(2);
target.addEventListener(eventType, listener, ...otherArguments);
}
}
// dynamically install listeners for all manually triggered events, just-in-time before they're dispatched ;D
const dispatchEvent_original = EventTarget.prototype.dispatchEvent;
function dispatchEvent(event) {
target.addEventListener(event.type, listener, ...otherArguments); // multiple identical listeners are automatically discarded
dispatchEvent_original.apply(this, arguments);
}
EventTarget.prototype.dispatchEvent = dispatchEvent;
if (EventTarget.prototype.dispatchEvent !== dispatchEvent) throw new Error(`Browser is smarter than you think!`);
}
// usage example
addEventListenerAll(window, (evt) => {
console.log(evt.type);
});
document.body.click();
document.body.dispatchEvent(new Event('omg!', { bubbles: true }));
// usage example with `useCapture`
// (also receives `bubbles: false` events, but in reverse order)
addEventListenerAll(
window,
(evt) => { console.log(evt.type); },
true
);
document.body.dispatchEvent(new Event('omfggg!', { bubbles: false }));
You could use EventEmitter2 which does wildcards. The problem with doing a catchall like you're talking about is that there are so many events, and you can create your own. You'd have to make an array of specifically which events you're talking about, iterate over that, and bind each one individually.
You should probably pick the events you want to listen to, put them into an array and iterate over each:
['click','mouseover'].forEach(function(ev) {
el.addEventListener(ev, function() {
console.log('event:', ev)
})
})
//listening for all click events on the document
document.addEventListener('click', function (event) {
//filtering for only events that happen on elements that contain the class
//view_btn.
if (event.target.classList.contains( 'view_btn' )){
//logging out the id of the element
var id_of_clicked_element = event.target.getAttribute("id"); //
console.log("button clicked has is of " + id_of_clicked_element)
}
});

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.

Categories