JS, JQuery and Observable - javascript

I`m building third party application for specific sites with Jquery.
Recently I started to use rx.Observable in my project. However, I found to use of this new JS library sometimes is hard to understand. I have tried to convert next peace of code to use with Observables, but it is not working at all;
class EventsUtils {
constructor() {
this.observable = Rx.Observable;
}
bindUserLeavePageEvent() {
var self = this;
document.addEventListener('mouseleave', (e) => {
$JQ(document).trigger('mouseleave.mo');
}, false);
/*We cannot remove document mouse over event thus we trigger Jquery registered custom event and on remove we cancel it*/
$JQ(document).off('mouseleave.mo').on('mouseleave.mo', (e) => {
if (e.clientY < 0 && !self.loaded) {
console.log('loading from screen Leave');
$JQ('.fixed-button').trigger('click');
self.loaded = true;
}
});
}
$JQ variable is came from jquery.noConflict due to i am running not on my page.
To convert second expression to Observable I have tried to use next statement:
this.observable.fromEvent(document, 'mouseleave.mo').pluck('currentTarget').subscribe(x=>console.log(x));
}
But without success.
How to convert above event statements to use with Observable and what is common pattern to do this;

It seems as if jquery.trigger does not really work with custom events - you can only catch those events through $(elem).on as they are handles internally for browser-compatibility-reasons. (https://github.com/jquery/jquery/issues/2476)
But you can relatively easy dispatch custom events (unless you want to target IE<=8)
document.addEventListener("mouseleave", () => {
console.log("Original event: Leave");
// dispatching custom events with vanilla-js (should work all the way down to IE9)
const event = document.createEvent("CustomEvent");
event.initEvent("mo.leave", true, true);
document.dispatchEvent(event);
});
Rx.Observable
.fromEvent(document, "mo.leave")
.pluck("currentTarget")
.subscribe(target => console.info("Target is", target.nodeName));
<script src="https://code.jquery.com/jquery-2.2.4.js"></script>
<script src="https://unpkg.com/rxjs/bundles/Rx.min.js"></script>

Related

Disable anonymous event listener or pass params & events to non-anonymous listener

I'm using the following listener to listen for swipe and touch events on mobile. It has the following signature:
$.fn.onSwipe = function(handlers) { // adding a jQuery prototype.
my_element.addEventListener('touchmove', function(event) {
handleSwipe(event, handlers.left, handlers.right, handlers.up, handlers.down);
});
}
I like this because it allows me to:
$("foo").onSwipe({
left: (event) => { ... }, // I can define this right here.
right: (event) => { ... },
up: (event) => { ... },
down: (event) => { ... },
})
For left, right, and so on, I can define functions in the scope of assigning the listener while also being able see the event in the listener.
I've tried doing event.preventDefault in my direction handlers, but this still prevents scroll (which I'd like to enable by removing the event listener).
Problem:
I can't remove the event since it's anonymous.
I don't know how I would create a named function while being able to pass it in the same way such that the addEventListner will pass the event and direction handlers (like handlers.left()) to my handleSwipe event.
Note: I am not interested in using other third-party libraries.
Since you're already using jQuery, one option is to attach the listeners with .on instead, allowing you to remove them all with .off, without having to save a reference to them:
$.fn.onSwipe = function(handlers) { // adding a jQuery prototype.
$(this).on('touchmove', function(event) {
handleSwipe(event, handlers.left, handlers.right, handlers.up, handlers.down);
});
};
$.fn.offSwipe = function() {
$(this).off('touchmove');
};
If you might have other touchmove listeners attached to the same element, then you'll need to save a reference to the created function when called. Without using jQuery (except for the $.fn part):
const handlersByElement = new Map();
$.fn.onSwipe = function(handlers) { // adding a jQuery prototype.
const handler = function(event) {
handleSwipe(event, handlers.left, handlers.right, handlers.up, handlers.down);
};
for (const elm of this) {
handlersByElement.set(elm, handler);
elm.addEventListener('touchmove', handler);
}
};
$.fn.offSwipe = function() {
for (const elm of this) {
handlersByElement.set(elm, handler);
elm.removeEventListener('touchmove', handlersByElement.get(elm));
}
};
You can also use event namespaces with jQuery to simplify adding and removing of events without having to save a reference to them and without removing all events of that type, thanks #VLAZ:
$.fn.onSwipe = function(handlers) { // adding a jQuery prototype.
$(this).on('touchmove.myswiper', function(event) {
handleSwipe(event, handlers.left, handlers.right, handlers.up, handlers.down);
});
};
$.fn.offSwipe = function() {
$(this).off('touchmove.myswiper');
};

Is there any way to get the function in specific event be triggered? [duplicate]

I need to find which event handlers are registered over an object.
For example:
$("#el").click(function() {...});
$("#el").mouseover(function() {...});
$("#el") has click and mouseover registered.
Is there a function to find out that, and possibly iterate over the event handlers?
If it is not possible on a jQuery object through proper methods, is it possible on a plain DOM object?
As of jQuery 1.8, the event data is no longer available from the "public API" for data. Read this jQuery blog post. You should now use this instead:
jQuery._data( elem, "events" );
elem should be an HTML Element, not a jQuery object, or selector.
Please note, that this is an internal, 'private' structure, and shouldn't be modified. Use this for debugging purposes only.
In older versions of jQuery, you might have to use the old method which is:
jQuery( elem ).data( "events" );
You can do it by crawling the events (as of jQuery 1.8+), like this:
$.each($._data($("#id")[0], "events"), function(i, event) {
// i is the event type, like "click"
$.each(event, function(j, h) {
// h.handler is the function being called
});
});
Here's an example you can play with:
$(function() {
$("#el").click(function(){ alert("click"); });
$("#el").mouseover(function(){ alert("mouseover"); });
$.each($._data($("#el")[0], "events"), function(i, event) {
output(i);
$.each(event, function(j, h) {
output("- " + h.handler);
});
});
});
function output(text) {
$("#output").html(function(i, h) {
return h + text + "<br />";
});
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="el">Test</div>
<code>
<span id="output"></span>
</code>
For jQuery 1.8+, this will no longer work because the internal data is placed in a different object.
The latest unofficial (but works in previous versions as well, at least in 1.7.2) way of doing it now is -
$._data(element, "events")
The underscore ("_") is what makes the difference here. Internally, it is calling $.data(element, name, null, true), the last (fourth) parameter is an internal one ("pvt").
Shameless plug, but you can use findHandlerJS
To use it you just have to include findHandlersJS (or just copy&paste the raw javascript code to chrome's console window) and specify the event type and a jquery selector for the elements you are interested in.
For your example you could quickly find the event handlers you mentioned by doing
findEventHandlers("click", "#el")
findEventHandlers("mouseover", "#el")
This is what gets returned:
element
The actual element where the event handler was registered in
events
Array with information about the jquery event handlers for the event type that we are interested in (e.g. click, change, etc)
handler
Actual event handler method that you can see by right clicking it and selecting Show function definition
selector
The selector provided for delegated events. It will be empty for direct events.
targets
List with the elements that this event handler targets. For example, for a delegated event handler that is registered in the document object and targets all buttons in a page, this property will list all buttons in the page. You can hover them and see them highlighted in chrome.
You can try it here
I use eventbug plugin to firebug for this purpose.
I've combined both solutions from #jps to one function:
jQuery.fn.getEvents = function() {
if (typeof(jQuery._data) === 'function') {
return jQuery._data(this.get(0), 'events') || {};
}
// jQuery version < 1.7.?
if (typeof(this.data) === 'function') {
return this.data('events') || {};
}
return {};
};
But beware, this function can only return events that were set using jQuery itself.
To check for events on an element:
var events = $._data(element, "events")
Note that this will only work with direct event handlers, if you are using $(document).on("event-name", "jq-selector", function() { //logic }), you will want to see the getEvents function at the bottom of this answer
For example:
var events = $._data(document.getElementById("myElemId"), "events")
or
var events = $._data($("#myElemId")[0], "events")
Full Example:
<html>
<head>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js" type="text/javascript"></script>
<script>
$(function() {
$("#textDiv").click(function() {
//Event Handling
});
var events = $._data(document.getElementById('textDiv'), "events");
var hasEvents = (events != null);
});
</script>
</head>
<body>
<div id="textDiv">Text</div>
</body>
</html>
A more complete way to check, that includes dynamic listeners, installed with $(document).on
function getEvents(element) {
var elemEvents = $._data(element, "events");
var allDocEvnts = $._data(document, "events");
for(var evntType in allDocEvnts) {
if(allDocEvnts.hasOwnProperty(evntType)) {
var evts = allDocEvnts[evntType];
for(var i = 0; i < evts.length; i++) {
if($(element).is(evts[i].selector)) {
if(elemEvents == null) {
elemEvents = {};
}
if(!elemEvents.hasOwnProperty(evntType)) {
elemEvents[evntType] = [];
}
elemEvents[evntType].push(evts[i]);
}
}
}
}
return elemEvents;
}
Example usage:
getEvents($('#myElemId')[0])
As of 1.9 there is no documented way to retrieve the events, other than to use the Migrate plugin to restore the old behavior. You could use the _.data() method as jps mentions, but that is an internal method. So just do the right thing and use the Migrate plugin if you need this functionality.
From the jQuery documentation on .data("events")
Prior to 1.9, .data("events") could be used to retrieve jQuery's
undocumented internal event data structure for an element if no other
code had defined a data element with the name "events". This special
case has been removed in 1.9. There is no public interface to retrieve
this internal data structure, and it remains undocumented. However,
the jQuery Migrate plugin restores this behavior for code that depends
upon it.
I created a custom jQuery selector that checks against both jQuery's cache of assigned event handlers as well as elements that use the native method for adding them:
(function($){
$.find.selectors[":"].event = function(el, pos, match) {
var search = (function(str){
if (str.substring(0,2) === "on") {str = str.substring(2);}
return str;
})(String(match[3]).trim().toLowerCase());
if (search) {
var events = $._data(el, "events");
return ((events && events.hasOwnProperty(search)) || el["on"+search]);
}
return false;
};
})(jQuery);
Example:
$(":event(click)")
This will return elements that have a click handler attached to them.
In a modern browser with ECMAScript 5.1 / Array.prototype.map, you can also use
jQuery._data(DOCUMENTELEMENT,'events')["EVENT_NAME"].map(function(elem){return elem.handler;});
in your browser console, which will print the source of the handlers, comma delimited. Useful for glancing at what all is running on a particular event.
Events can be retrieved using:
jQuery(elem).data('events');
or jQuery 1.8+:
jQuery._data(elem, 'events');
Note:
Events bounded using $('selector').live('event', handler)
can be retrieved using:
jQuery(document).data('events')
jQuery is not letting you just simply access the events for a given element.
You can access them using undocumented internal method
$._data(element, "events")
But it still won't give you all the events, to be precise won't show you events assigned with
$([selector|element]).on()
These events are stored inside document, so you can fetch them by browsing through
$._data(document, "events")
but that is hard work, as there are events for whole webpage.
Tom G above created function that filters document for only events of given element and merges output of both methods, but it had a flaw of duplicating events in the output (and effectively on the element's jQuery internal event list messing with your application).
I fixed that flaw and you can find the code below. Just paste it into your dev console or into your app code and execute it when needed to get nice list of all events for given element.
What is important to notice, element is actually HTMLElement, not jQuery object.
function getEvents(element) {
var elemEvents = $._data(element, "events");
var allDocEvnts = $._data(document, "events");
function equalEvents(evt1, evt2)
{
return evt1.guid === evt2.guid;
}
for(var evntType in allDocEvnts) {
if(allDocEvnts.hasOwnProperty(evntType)) {
var evts = allDocEvnts[evntType];
for(var i = 0; i < evts.length; i++) {
if($(element).is(evts[i].selector)) {
if(elemEvents == null) {
elemEvents = {};
}
if(!elemEvents.hasOwnProperty(evntType)) {
elemEvents[evntType] = [];
}
if(!elemEvents[evntType].some(function(evt) { return equalEvents(evt, evts[i]); })) {
elemEvents[evntType].push(evts[i]);
}
}
}
}
}
return elemEvents;
}
I have to say many of the answers are interesting, but recently I had a similar problem and the solution was extremely simple by going the DOM way. It is different because you don't iterate but aim directly at the event you need, but below I'll give a more general answer.
I had an image in a row:
<table>
<td><tr><img class="folder" /></tr><tr>...</tr></td>
</table>
And that image had a click event handler attached to it:
imageNode.click(function () { ... });
My intention was to expand the clickable area to the whole row, so I first got all images and relative rows:
tableNode.find("img.folder").each(function () {
var tr;
tr = $(this).closest("tr");
// <-- actual answer
});
Now in the actual anwer line I just did as follows, giving an answer to the original question:
tr.click(this.onclick);
So I fetched the event handler directly from the DOM element and put it into the jQuery click event handler. Works like a charm.
Now, to the general case. In the old pre-jQuery days you could get all events attached to an object with two simple yet powerful functions gifted to us mortals by Douglas Crockford:
function walkTheDOM(node, func)
{
func(node);
node = node.firstChild;
while (node)
{
walkTheDOM(node, func);
node = node.nextSibling;
}
}
function purgeEventHandlers(node)
{
walkTheDOM(node, function (n) {
var f;
for (f in n)
{
if (typeof n[f] === "function")
{
n[f] = null;
}
}
});
}
Try jquery debugger plugin if you're using chrome: https://chrome.google.com/webstore/detail/jquery-debugger/dbhhnnnpaeobfddmlalhnehgclcmjimi?hl=en
Another way to do it is to just use jQuery to grab the element, then go through actual Javascript to get and set and play with the event handlers. For instance:
var oldEventHandler = $('#element')[0].onclick;
// Remove event handler
$('#element')[0].onclick = null;
// Switch it back
$('#element')[0].onclick = oldEventHandler;
I combined some of the answers above and created this crazy looking but functional script that lists hopefully most of the event listeners on the given element. Feel free to optimize it here.
var element = $("#some-element");
// sample event handlers
element.on("mouseover", function () {
alert("foo");
});
$(".parent-element").on("mousedown", "span", function () {
alert("bar");
});
$(document).on("click", "span", function () {
alert("xyz");
});
var collection = element.parents()
.add(element)
.add($(document));
collection.each(function() {
var currentEl = $(this) ? $(this) : $(document);
var tagName = $(this)[0].tagName ? $(this)[0].tagName : "DOCUMENT";
var events = $._data($(this)[0], "events");
var isItself = $(this)[0] === element[0]
if (!events) return;
$.each(events, function(i, event) {
if (!event) return;
$.each(event, function(j, h) {
var found = false;
if (h.selector && h.selector.length > 0) {
currentEl.find(h.selector).each(function () {
if ($(this)[0] === element[0]) {
found = true;
}
});
} else if (!h.selector && isItself) {
found = true;
}
if (found) {
console.log("################ " + tagName);
console.log("event: " + i);
console.log("selector: '" + h.selector + "'");
console.log(h.handler);
}
});
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="parent-element">
<span id="some-element"></span>
</div>

jsViews $.observable(arr).insert() not triggering DOM update

I am using $.observable(array).insert() to append items to a list. This is updating my view as it should: new list items are rendered to the DOM. However, I would like to issue a click event on the new DOM node (I'm relying on the event to add a class to expand the item and attach another listener to the body so the area can be closed).
I have tried both
$.observable(_model.leadTimes).insert(leadTime);
$leadTimes.find('.lead-time-data').last().find('.start-editing').click();
...and
function watchLeadTimes() {
var changeHandler = function (ev, eventArgs) {
if (eventArgs.change === 'insert') {
$leadTimes.find('.lead-time-data').last().find('.start-editing').click();
}
};
$.observe(_model.leadTimes, changeHandler);
}
And neither of them worked, however, if I wrap the jQuery method in a setTimout, like setTimeout(function () { $leadTimes.find('.lead-time-data').last().find('.start-editing').click(); }, 400);, it does work, leading me to believe this is an issue of timing with the DOM render somehow not finishing before my jQuery click() method is invoked.
Since the odds are decent that you will see this, Borris, thank you for the library and all that you do! I think jsViews is an excellent middle ground between the monolithic frameworks out there and plain old jQuery noodling!
Edit 02/09/17
It turns out my issue was overlapping click events--I was inadvertently handling a click to deselect my element immediately after it was selected. However I took the opportunity to rewrite things to use a more declarative approach following Borris' linked example.
Now in my template I am using a computed observable, isSelected to toggle the .editing class:
{^{for leadTimes}}
<tr class="lead-time-data" data-link="class{merge:~isSelected() toggle='editing'}">
<span>{^{:daysLead}}</span>
</tr>
{{/for}}
And this JS:
function addNewLeadTimeClickHandler() {
var onNewLeadTimeClick = function () {
e.stopPropagation(); // this is what I was missing
var leadTime = {
daysLead: 1,
description: ''
};
$.observable(_model.activityMapping.leadTimes).insert(leadTime);
selectLeadtime(_model.activityMapping.leadTimes.length -1);
}
$leadTimes.on('click', '.add', onNewLeadTimeClick);
}
function selectLeadtime(index) {
var addStopEditingClickHandler = function () {
var onClickHandler = function (event) {
if ($(event.target).closest('tr').hasClass('editing')) {
setHandler();
return;
}
selectLeadtime(-1)
};
function setHandler() {
var clickEvent = 'click.ActivityChangeRequestDetailController-outside-edit-row';
$('html:not(.edit)').off(clickEvent).one(clickEvent, onClickHandler);
};
setHandler();
}
if (_model.selectedLeadtimeIndex !== index) {
$.observable(_model).setProperty('selectedLeadtimeIndex', index)
addStopEditingClickHandler();
}
}
function isSelected() {
var view = this;
return this.index === _model.selectedLeadtimeIndex;
}
// isSelected.depends = ["_model^selectedLeadtimeIndex"];
// for some reason I could not get the above .depends syntax to work
// ...or "_model.selectedLeadtimeIndex" or "_model.selectedLeadtimeIndex"
// but this worked ...
isSelected.depends = function() {return [_model, "selectedLeadtimeIndex"]};
The observable insert() method is synchronous. If your list items are rendered simply using {^{for}}, then that is also synchronous, so you should not need to use setTimeout, or a callback. (There are such callbacks available, but you should not need them for this scenario.)
See for example http://www.jsviews.com/#samples/editable/tags (code here):
$.observable(movies).insert({...});
// Set selection on the added item
app.select($.view(".movies tr:last").index);
The selection is getting added, synchronously, on the newly inserted item.
Do you have other asynchronous code somewhere in your rendering?
BTW generally you don't need to add new click handlers to added elements, if you use the delegate pattern. For example, in the same sample, a click handler to remove a movie is added initially to the container "#movieList" with a delegate selector ".removeMovie" (See code). That will work even for movies added later.
The same scenario works using {{on}} See http://www.jsviews.com/#link-events: "The selector argument can target elements that are added later"

Using RxJS Observable to change css class on drag events

HI my goal is to use Observable sequence to listen to dragEnter/dragleave events to change css classes. IE :
var myDraggableListEl = document.querySelector....
var itemDragIn$ = Rx.Observable.fromEvent(myDraggableListEl, 'dragenter').map((e)=> e.target);
var itemDragOut$ = Rx.Observable.fromEvent(myDraggableListEl, 'dragleave').map((e)=> e.target);
Basically I just want to add a 'hover' class on 'dragenter',and then remove the same class on dragleave.
I was wondering if there was a clever way to 'merge' these 2 so that the subscription would just be something like:
.subscribe( (boolAdd, className, target ) => {
boolAdd ? target.classlist.add(className): target.classlist.remove(className)
}
Maybe Im just overthinking this andI just leave the two observables as separate, but just wondering if anyone had accomplished this or if they had a better way.
Thanks!
You can totally do this using the Rx.Observable.merge() method.
Links:
merge - general rx
merge - rxjs example
Example (Here's the Fiddle):
Rx.Observable.merge(
Rx.Observable.fromEvent($el, 'dragenter').map(function(e) {
return { target: e.target, isAdd: true, className: 'dragg' }
}),
Rx.Observable.fromEvent($el, 'dragleave').map(function(e) {
return { target: e.target, isAdd: false, className: 'dragg' }
}))
.subscribe(function(obj){
if(obj.isAdd) {
obj.target.classlist.add(obj.className);
} else {
obj.target.classlist.remove(obj.className);
}
});
Although please consider that the following is probably much more readable:
// addClass and removeClass are defined somewhere (can represent jquery's functions)
Rx.Observable
.fromEvent($el, 'dragenter')
.subscribe(function(e){
addClass(e.target, 'dragg');
});
Rx.Observable
.fromEvent($el, 'dragleave')
.subscribe(function(e){
removeClass(e.target, 'dragg');
});

Bind to custom CSS animation end event with jQuery or JavaScript?

We have multiple animations against the same object. We need to take different actions when each of these animations end.
Right now, we bind to the webkitAnimationEnd event, and use a gnarly if/then statement to handle each animation differently.
Is there a way to essentially create custom webkitAnimationEnd events, allowing us to fire a specific event handler when a specific animation ends? For instance, fire handler1 when animation1 ends and fire handler2 when animation2 ends.
We're building for Webkit browsers, specifically Mobile Safari.
Thanks!
For a simple event-trigger, you can pass a function to jQuery's trigger() method and use the returned value of that function to call a trigger a specific event (which can then be listened-for:
function animEndTrigger(e) {
if (!e) {
return false;
}
else {
var animName = e.originalEvent.animationName;
return animName + 'FunctionTrigger';
}
}
$('body').on('bgAnimFunctionTrigger fontSizeFunctionTrigger', function(e){
console.log(e);
});
$('div').on('webkitAnimationEnd', function(e) {
$(this).trigger(animEndTrigger(e));
});
JS Fiddle demo.
You can, of course, also use the called function to either trigger the event itself or assess the passed parameters to determine whether or not to return an event at all:
One method to assess for a particular event to trigger is to use an object:
var animations = {
'bgAnim': 'aParticularEvent'
};
function animEndTrigger(e) {
if (!e) {
return false;
}
else {
var animName = e.originalEvent.animationName;
return animations[animName] ? animations[animName] : false;
}
}
$('body').on('aParticularEvent', function(e) {
console.log(e);
});
$('div').on('webkitAnimationEnd', function(e) {
$(this).trigger(animEndTrigger(e));
});​
JS Fiddle demo.
Though, in this case, the return false should be altered so as not to provide the error Uncaught TypeError: Object false has no method 'indexOf' (which I've not bothered, as yet, to account for).
The following causes the called-function (animEndTrigger()) to directly trigger() the custom event (which requires an element on which to bind the trigger() method) and also avoids the Uncaught TypeError above:
var animations = {
'bgAnim': 'aParticularEvent'
};
function animEndTrigger(e, el) {
if (!e || !el) {
return false;
}
else {
var animName = e.originalEvent.animationName;
if (animations[animName]) {
$(el).trigger(animations[animName]);
}
}
}
$('body').on('aParticularEvent', function(e) {
console.log(e);
});
$('div').on('webkitAnimationEnd', function(e) {
animEndTrigger(e, this);
});​
JS Fiddle demo.
Of course you're still, effectively, using an if to perform an assessment, so I can't be particularly sure that this is any tidier than your own already-implemented solution.

Categories

Resources