Handle React event delegation with stopPropagation() - javascript

I have a project in React which should be able to be placed on any website. The idea is that I host a javascript file, people place a div with a specific ID, and React renders in that div.
So far this works, except click-events. These evens are handled at the top level. This is all good, but one of the sites where the app should be placed, has stopPropagation() implemented for a off-canvas menu. Because of this the events aren't working properly.
I tried catching all events at the root-element, and dispatching them manually:
this.refs.wrapper.addEventListener('click', (event) => {
console.log(event);
console.log(event.type);
const evt = event || window.event;
evt.preventDefault();
evt.stopImmediatePropagation();
if (evt.stopPropagation) evt.stopPropagation();
if (evt.cancelBubble !== null) evt.cancelBubble = true;
document.dispatchEvent(event);
});
This doesn't work, because the event is already being dispatched:
Uncaught InvalidStateError: Failed to execute 'dispatchEvent' on 'EventTarget': The event is already being dispatched.
What would be the right way to fix this problem? Not using the synthetic events from React doesn't seem the right way to go for me..

Argument 'event h'as already been dispatched.
You should clone a new eventobject with old event.
var newevent = new event.constructor(event.type, event)

Ther is no solution yet. React, as you say, listen events on the root of DOM, and filter events if their event.target not inside react's mounted node.
You can try:
1. Redispatch new event in the Reract component, but it will be stopped at outside handler too.
2. Dispatch new event outside Reract component, higher (closest to BODY) then node with stopPropagation callback. But event.target will point to node, which not inside React's component and you can not change it, beacause it is readonly.
Maybe in next versions they will fix it.

But you can listen for events in the document, no?
Let say your root component for the whole app is named app. Then, inside it's componentDidMount you can have:
// when the main App component mounts - we'll add the event handlers ..
componentDidMount() {
var appComponent = this;
document.addEventListener('click', function(e) {
var clickedElement = e.target;
// Do something with the clickedElement - by ID or class ..
// You'll have reference to the top level component in `appComponent` ..
});
};

As you said - React handles all events at the top-level node (document), and to determine which react component relates to some event react uses event.target property. So to make everything work you should manually dispatch stopped event on document node and set proper "target" property to this event.
There is 2 problems to solve:
You can't trigger event that is already dispatched. To solve this you have to create a fresh copy of this event.
After you do dispatchEvent() on some node browser automatically set "target" property of this event to be the node on which event is fired. To solve this you should set proper target before dispatchEvent(), and make this property read-only using property descriptors.
General solution:
Solution tested in all modern browsers and IE9+
Here is a source code of solution:
https://jsbin.com/mezosac/1/edit?html,css,js,output . (Sometimes it hangs, so if you don't see UI elements in preview area - click on "run win js" button on top right corner)
It is well commented, so I will not describe here all of that stuff, but I will quickly explain main points:
Event should be redispatched immediately after it was stopped, to achieve this I extended native stopPropagation and stopImmediatePropagation methods of event to call my redispatchEventForReact function before stopping propagation:
if (event.stopPropagation) {
const nativeStopPropagation = event.stopPropagation;
event.stopPropagation = function fakeStopPropagation() {
redispatchEventForReact();
nativeStopPropagation.call(this);
};
}
if (event.stopImmediatePropagation) {
const nativeStopImmediatePropagation = event.stopImmediatePropagation;
event.stopImmediatePropagation = function fakeStopImmediatePropagation() {
redispatchEventForReact();
nativeStopImmediatePropagation.call(this);
};
}
And there is another one possibility to stop event - setting "cancelBubble" property to "true". If you take a look at cancalBubble property descriptor - you will see that this property indeed is a pair of getters/setters, so it's easy to inject "redispatchEventForReact" call inside setter using Object.defineProperty:
if ('cancelBubble' in event) {
const initialCancelBubbleDescriptor = getPropertyDescriptor(event, 'cancelBubble');
Object.defineProperty(event, 'cancelBubble', {
...initialCancelBubbleDescriptor,
set(value) {
redispatchEventForReact();
initialCancelBubbleDescriptor.set.call(this, value);
}
});
}
redispatchEventForReact function:
2.1 Before we dispatch event for react we should remove our customized stopPropagation and stopImmediatePropagation methods (because in react code some component in theory can invoke e.stopPropagation, which will trigger redispatchEventForReact again, and this will lead to infinite loop):
delete event.stopPropagation;
delete event.stopImmediatePropagation;
delete event.cancelBubble;
2.2 Then we should make a copy of this event. It's easy to do in modern browsers, but take a looot of code for IE11-, so I moved this logic in separate function (see attached source code on jsbin for details):
const newEvent = cloneDOMEvent(event);
2.3 Because browser set "target" property of event automatically when event is dispatched we should make it read-only. Important bit here - setting value and writeable=false will not work in IE11-, so we have to use getter and empty setter:
Object.defineProperty(newEvent, 'target', {
enumerable: true,
configurable: false,
get() { return event.target; },
set(val) {}
});
2.4 And finally we can dispatch event for react:
document.dispatchEvent(newEvent);
To guarantee that hacks for react will be injected in event before something stopped this event we should listen to this event on root node in capturing phase and do injections:
const EVENTS_TO_REDISPATCH = ['click'];
EVENTS_TO_REDISPATCH.forEach(eventToRedispatch => {
document.addEventListener(eventToRedispatch, prepareEventToBeRedispatched, true);
});

Related

Attaching an Event to an element and dispatching it correctly

I need to attach an Event called render to a panel element, that does nothing but being dispatched to warn all the listeners whenever panel is rendering.
Following the The old-fashioned way section of this link, I came up with this code:
/**
* **Static** Re-draw the layer panel to represent the current state of the layers.
* #param {Element} panel The DOM Element into which the layer tree will be rendered
*/
static renderPanel(panel) {
// Create the event.
var render_event = document.createEvent('Event');
// Define that the event name is 'render'.
render_event.initEvent('render', true, true);
// Listen for the event.
panel.addEventListener('render', function (e) {
// e.target matches panel
}, false);
panel.dispatchEvent(render_event);
This seems to have worked but as this is my first time doing this, I am not quite sure how to check the correctness of this method.
Looking inside the console I can see my panel element dispatching the render Event, but I'd like to ask if there's something I am missing or to be worried about before moving on.
To debug the result, I tried add an event listener to the document element like document.addEventListener("render",console.log("ciao")), which in turn printed ciao once in the console, but only just once.
I thought I would be able to see as many "ciao" in the console as the times the render Event was triggered, but this does not seem the case.
If you're trying to check everytime your event is fired, the second argument of addEventListener (taking into account what you're willing to achieve) should be a function callback using an event object as argument, like this for example:
document.addEventListener("render", function(e) { console.log("ciao"); });
In your example you're executing console.log("ciao"), not passing a function reference (anonymous or not), this is why it executes only one time: when the page loads/evaluates your script.
mdn guide on creating and dispatching custom events (same as your link)
The old fashioned method seems to still be working fine when I tried it, I saw the document event listener console log each time I triggered the event.
The updated way is:
panel.dispatchEvent(new CustomEvent('render'));
let div = document.querySelector('div');
div.addEventListener('old-event', () => {console.log('Old-fashinoed event caught')});
div.addEventListener('new-event', () => {console.log('New-fashioned event caught')});
let oldEvent = document.createEvent('Event');
oldEvent.initEvent('old-event', true, true);
let newEvent = new CustomEvent('new-event');
setInterval(() => {
div.dispatchEvent(oldEvent);
div.dispatchEvent(newEvent);
}, 1000);
<div>I emit an old-fashioned and a new-fashioned event every 1 second</div>

Adding an object to a fabric.Canvas without dispatching the `object:added` event

Is there a way of adding an object to a fabric.Canvas instance without dispatching the object:added event?
Currently I'm forced to use the following ugly approach:
var dispatchObjectAdded = true;
canvas.on('object:added', function () {
if (!dispatchObjectAdded) {
return;
}
// handle event..
});
// add normally with dispatch
canvas.add(object);
// add without dispatch
dispatchObjectAdded = false;
canvas.add(object);
dispatchObjectAdded = true;
This works because the object:added event is dispatched synchronously, but I'm sure there's a better way – I just can't find what it is.
You can define a custom property on your object, and check in the event handler if the object that triggered the event has that property.
Here is a small fiddle example.

Scala.js: Using addEventListener to add events to objects

In JavaScript, the addEventListener() method is used like this:
object.addEventListener("click", myScript);
In Scala.js: I have a canvas, and I want to listen to clicks only on the canvas, not the entire document. In the Scala.js.dom library, addEventListener is defined as:
def addEventListener(`type`: String, listener: js.Function1[Event, _], useCapture: Boolean = ???): Unit = ???
I'm not sure what "useCapture" refers to. But I tried:
dom.document.getElementById("canvas").addEventListener("click", {
(e:dom.MouseEvent) => { /*do something*/ }
}, false)
And the error message I got:
found : org.scalajs.dom.MouseEvent => Unit
required: scala.scalajs.js.Function1[org.scalajs.dom.Event, _]
Can someone explain what "useCapture" refers to, and how to correctly use addEventListener in Scala.js?
The error message you get is a perfectly normal type error: addEventListener expects a js.Function1[dom.Event, _], but you're giving it a js.Function1[dom.MouseEvent, _]. Since the first argument is in contravariant position, this does not typecheck.
There are two solutions: either you make your function take a dom.Event, that you then cast to dom.MouseEvent, like this:
canvas.addEventListener("click", { (e0: dom.Event) =>
val e = e0.asInstanceOf[dom.MouseEvent]
...
}, false)
or you use onclick which is more precisely typed:
canvas.onclick = { (e: dom.MouseEvent) =>
...
}
Event capture is the process by which an EventListener registered on
an ancestor of the event's target can intercept events of a given type
before they are received by the event's target. Capture operates from
the top of the tree, generally the Document, downward, making it the
symmetrical opposite of bubbling which is described below. The chain
of EventTargets from the top of the tree to the event's target is
determined before the initial dispatch of the event. If modifications
occur to the tree during event processing, event flow will proceed
based on the initial state of the tree..
http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-flow-basic
window.addEventListener("click", function(){console.log('something1')}, false);
window.addEventListener("click", function(){console.log('something2')}, true);
The order will be
something 2 (defined first, using capture=true)
something 1 (defined second using capture=true)
Ref 2
Note on the second ref: The order is not guaranteed!
capture phase
The process by which an event can be handled by one of the target ancestors before being handled by the target node.
REF 3

Tracking Clicks; Performance Implications to Searching the DOM

I have a Javascript plugin that searches the DOM for any elements starting with the class name "tracking" and adds a click event listener (or another type of listener, if specified) to that element. The idea is that every time that event occurs on that element, that it runs a Javascript function that sends data to our traffic servers. Here's what the code looks like:
// Once the page is completed loaded
window.mmload(function() {
// Get the container object
obj = document.getElementById(name);
if ( obj.length < 0 )
throw ("The Id passed into the tracker does not exist ("+name+")");
// Find all the elements belonging to the tracking class
var trackingClass = new RegExp( /tracking\[[a-zA-Z0-9\.\-_]+\]/g );
var myElements = getElementsByRegex( trackingClass, obj );
//For each of those elements...
for( var i in myElements ) {
var elm = myElements[i];
var method = elm.className.match( /tracking\[[a-zA-Z0-9\.\-_]+\]/ )[0].split('[')[1].replace(']','').split('.')[2];
method = typeof( method ) == 'undefined' ? 'click' : method;
// Add a click event listener
myElements[i].addEventListener( method, function(e){
// Get the element, the link (if any), and the args of the event
var link = elm.getAttribute('href') == null ? "" : elm.getAttribute('href');
var args = elm.className.match( /tracking\[[a-zA-Z0-9\.\-_]+\]/ )[0].split('[')[1].replace(']','').split('.');
// If a link existed, pause it, for now
if ( link != '' )
e.preventDefault();
// Track the event
eventTracker( args[0], args[1], ( method == 'click' ? 'redirect' : 'default' ), link );
return false;
}, true);
}
});
Right now I've got this chuck of code running once the window has completely loaded (window.mmload() is a function I made for appending window.onload events). However, there maybe times when I need to run this function again because I added new elements to the DOM via Javascript with this class name and I want to track them too.
My initial solution was to run this function using setInterval to check the DOM every few milliseconds or second or whatever makes the most sense. However, I was worried if I took this approach that it might slow down the website, especially since this is running on a mobile website for smartphones. I'm not sure what kind of a performance hit I might take if I'm searching to DOM every so often.
The other approach I had in mind was to simply call the function after adding traceable elements to the DOM. This is probably the most efficient way of handling it. However, the people that I'm working with, granted very smart individuals, are Web Designers who don't often think about nor understand very well code. So the simpler I can make this, the better. That's why I liked the setInterval approach because nothing additional would be required of them. But if it noticeably slows down the site, I might have to take the other approach.
You should consider even delegation.
You just add one event listener to the document root and check the class of the element the event originated from (event.target). If you want to include also clicks from descendants, you'd have to traverse the DOM up form the target and check whether any of the ancestors contains the class.
I see two main advantages:
It works for newly generated elements without any extra steps (so the other developers don't have to do anything special).
It adds only one event handler instead of potentially many, which saves memory.
Disadvantages:
If other event handlers are registered along the path and they prevent the event from bubbling up, you cannot register this event.
A bit more information:
An event handler gets an event object as first argument. This object has several properties, among others, which element the event originated form.
E.g. to get the target element:
var element = event.target || event.srcElement;
This will be a DOM element and you can access the classes via element.className.
So your event listener could look like this (note that IE uses another method to attach event listeners and the event object is not passed but available via window.event):
function handler(event) {
event = event || window.event;
var target = event.target || event.srcElement;
if(target.className.match(/tracking\[[a-zA-Z0-9\.\-_]+\]/g) {
// do your stuff
}
}
if(document.addEventListener) {
document.addEventListener('click', handler, false);
}
else {
document.attachEvent('onclick', handler);
}
But as I said, this would miss events that are prevented from bubbling up. At least in the browsers following the W3C model (so not IE), you can handle the events in the capture phase by setting the last parameter to true:
document.addEventListener('click', handler, true);
If you can live without IE, then there is a change event which you can hook into for the window/document/dom element. Simply hook into the event at the document level, and it'd fire anytime something's changed in the page (stuff inserted, deleted, changed). I believe the event's context contains what got changed, so it should be fairly trivial to find any new trackable elements and attach your spy code to it.
A third option would be to write a method for manipulating the innerHTML of an element. At the end of that method simply call your function that refreshes everything.
example:
var setHtml = function(element, newHtml){
element.innerhtml = newHtml;
yourRefreshFunction();
}
So obviously this requires that you have your web developers user this method to update the dom. And you'll have to do it for anything that is more complicated than simple html edits. But that gives you the idea.
Hope that helps!

jQuery watch for domElement changes?

I have an ajax callback which injects html markup into a footer div.
What I can't figure out is how to create a way to monitor the div for when it's contents change. Placing the layout logic I'm trying to create in the callback isn't an option as each method (callback and my layout div handler) shouldn't know about the other.
Ideally I'd like to see some kind of event handler akin to $('#myDiv').ContentsChanged(function() {...}) or $('#myDiv').TriggerWhenContentExists( function() {...})
I found a plugin called watch and an improved version of that plugin but could never get either to trigger. I tried "watching" everything I could think of (i.e. height property of the div being changed via the ajax injection) but couldn't get them to do anything at all.
Any thoughts/help?
The most effective way I've found is to bind to the DOMSubtreeModified event. It works well with both jQuery's $.html() and via standard JavaScript's innerHTML property.
$('#content').bind('DOMSubtreeModified', function(e) {
if (e.target.innerHTML.length > 0) {
// Content change handler
}
});
http://jsfiddle.net/hnCxK/
When called from jQuery's $.html(), I found the event fires twice: once to clear existing contents and once to set it. A quick .length-check will work in simple implementations.
It's also important to note that the event will always fire when set to an HTML string (ie '<p>Hello, world</p>'). And that the event will only fire when changed for plain-text strings.
You can listen for changes to DOM elements (your div for example) by binding onto DOMCharacterDataModified tested in chrome but doesn't work in IE see a demo here
Clicking the button causes a change in the div which is being watched, which in turn fills out another div to show you its working...
Having a bit more of a look Shiki's answer to jquery listen to changes within a div and act accordingly looks like it should do what you want:
$('#idOfDiv').bind('contentchanged', function() {
// do something after the div content has changed
alert('woo');
});
In your function that updates the div:
$('#idOfDiv').trigger('contentchanged');
See this as a working demo here
There is a neat javascript library, mutation-summary by google, that lets you observe dom changes concisely. The great thing about it, is that if you want, you can be informed only of the actions that actually made a difference in the DOM, to understand what I mean you should watch the very informative video on the project's homepage.
link:
http://code.google.com/p/mutation-summary/
jquery wrapper:
https://github.com/joelpurra/jquery-mutation-summary
You might want to look into the DOMNodeInserted event for Firefox/Opera/Safari and the onpropertychange event for IE. It probably wouldn't be too hard to utilize these events but it might be a little hack-ish. Here is some javascript event documentation: http://help.dottoro.com/larrqqck.php
Now we can use a MutationObserver ; Well, apparently we must.
Use of Mutation Events is deprecated. Use MutationObserver instead.
jquery.min.js:2:41540
From https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver :
// Select the node that will be observed for mutations
const targetNode = document.getElementById('some-id');
// Options for the observer (which mutations to observe)
const config = { attributes: true, childList: true, subtree: true };
// Callback function to execute when mutations are observed
const callback = function(mutationsList, observer) {
// Use traditional 'for loops' for IE 11
for(const mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('A child node has been added or removed.');
}
else if (mutation.type === 'attributes') {
console.log('The ' + mutation.attributeName + ' attribute was modified.');
}
}
};
// 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);
// Later, you can stop observing
observer.disconnect();

Categories