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

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"

Related

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>

Knockout/JavaScript Ignore Multiclick

I'm having some problems with users clicking buttons multiple times and I want to suppress/ignore clicks while the first Ajax request does its thing. For example if a user wants add items to their shopping cart, they click the add button. If they click the add button multiple times, it throws a PK violation because its trying to insert duplicate items into a cart.
So there are some possible solutions mentioned here: Prevent a double click on a button with knockout.js
and here: How to prevent a double-click using jQuery?
However, I'm wondering if the approach below is another possible solution. Currently I use a transparent "Saving" div that covers the entire screen to try to prevent click throughs, but still some people manage to get a double click in. I'm assuming because they can click faster than the div can render. To combat this, I'm trying to put a lock on the Ajax call using a global variable.
The Button
<span style="SomeStyles">Add</span>
Knockout executes this script on button click
vmProductsIndex.AddItemToCart = function (item) {
if (!app.ajaxService.inCriticalSection()) {
app.ajaxService.criticalSection(true);
app.ajaxService.ajaxPostJson("#Url.Action("AddItemToCart", "Products")",
ko.mapping.toJSON(item),
function (result) {
ko.mapping.fromJS(result, vmProductsIndex.CartSummary);
item.InCart(true);
item.QuantityOriginal(item.Quantity());
},
function (result) {
$("#error-modal").modal();
},
vmProductsIndex.ModalErrors);
app.ajaxService.criticalSection(false);
}
}
That calls this script
(function (app) {
"use strict";
var criticalSectionInd = false;
app.ajaxService = (function () {
var ajaxPostJson = function (method, jsonIn, callback, errorCallback, errorArray) {
//Add the item to the cart
}
};
var inCriticalSection = function () {
if (criticalSectionInd)
return true;
else
return false;
};
var criticalSection = function (flag) {
criticalSectionInd = flag;
};
// returns the app.ajaxService object with these functions defined
return {
ajaxPostJson: ajaxPostJson,
ajaxGetJson: ajaxGetJson,
setAntiForgeryTokenData: setAntiForgeryTokenData,
inCriticalSection: inCriticalSection,
criticalSection: criticalSection
};
})();
}(app));
The problem is still I can spam click the button and get the primary key violation. I don't know if this approach is just flawed and Knockout isn't quick enough to update the button's visible binding before the first Ajax call finishes or if every time they click the button a new instance of the criticalSectionInd is created and not truely acting as a global variable.
If I'm going about it wrong I'll use the approaches mentioned in the other posts, its just this approach seems simpler to implement without having to refactor all of my buttons to use the jQuery One() feature.
You should set app.ajaxService.criticalSection(false); in the callback methods.
right now you are executing this line of code at the end of your if clause and not inside of the success or error callback, so it gets executed before your ajax call is finished.
vmProductsIndex.AddItemToCart = function (item) {
if (!app.ajaxService.inCriticalSection()) {
app.ajaxService.criticalSection(true);
app.ajaxService.ajaxPostJson("#Url.Action("AddItemToCart", "Products")",
ko.mapping.toJSON(item),
function (result) {
ko.mapping.fromJS(result, vmProductsIndex.CartSummary);
item.InCart(true);
item.QuantityOriginal(item.Quantity());
app.ajaxService.criticalSection(false);
},
function (result) {
$("#error-modal").modal();
app.ajaxService.criticalSection(false);
},
vmProductsIndex.ModalErrors);
}
}
you could use the "disable" binding from knockout to prevent the click binding of the anchor tag to be fired.
here is a little snippet for that. just set a flag to true when your action starts and set it to false again when execution is finished. in the meantime, the disable binding prevents the user from executing the click function.
function viewModel(){
var self = this;
self.disableAnchor = ko.observable(false);
self.randomList = ko.observableArray();
self.loading = ko.observable(false);
self.doWork = function(){
if(self.loading()) return;
self.loading(true);
setTimeout(function(){
self.randomList.push("Item " + (self.randomList().length + 1));
self.loading(false);
}, 1000);
}
}
ko.applyBindings(new viewModel());
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.0.0/knockout-min.js"></script>
Click me
<br />
<div data-bind="visible: loading">...Loading...</div>
<br />
<div data-bind="foreach: randomList">
<div data-bind="text: $data"></div>
</div>

Implementation knockout custom binding with afterRender functionality

I'm working with a list of images. The images are loaded dynamically; the list of references is stored in observableArray.
After a full load of the image list I want to connect handlers of DOM-elements. My implementation at the moment:
in View:
<div class="carousel_container" data-bind="template: { 'name': 'photoTemplate', 'foreach': ImageInfos, 'afterRender': renderCarousel }">
<script type="text/html" id="photoTemplate">
//...content of template
</script>
in ViewModel:
self.counterCarousel = 0;
self.renderCarousel = function (elements) {
var allImagesCount = self.ImageInfos().length;
self.counterCarousel++;
if (self.counterCarousel >= allImagesCount) {
self.counterCarousel = 0;
// ... add handlers here
}
}
This is a very ugly approach. In addition, user can add / delete images, so after each addition or removal is required remove all handlers and connect it again. How can I organize a custom binding to handle this scenario?
I don't see why this approach would not work -
ko.utils.arrayForEach(ImageInfos(), function (image) {
// ... add handlers here
});
Or better yet, bind an event to each item with a class of 'image-info' so that you don't have to redo the bindings when items are added or changed -
var afterRender = function (view) {
bindEventToImages(view, '.image-info', doSomething);
};
var bindEventToImages= function (rootSelector, selector, callback, eventName) {
var eName = eventName || 'click';
$(rootSelector).on(eName, selector, function () {
var selectedImage = ko.dataFor(this);
callback(selectedImage);
return false;
});
};
function doSomething(sender) {
alert(sender);
// handlers go here
}
This binds an event to every class 'image-info' and on-click handles the calling element, executing doSomething.

Event object eaten by jQuery animation callback

I have a problem with event object passed to the function in drop event. In my code, div#dropArea has it's drop event handled by firstDrop function which does some animations and then calls the proper function dropFromDesktop which handles the e.dataTransfer.files object. I need this approach in two separate functions because the latter is also used further by some other divs in the HTML document (no need to duplicate the code). First one is used only once, to hide some 'welcome' texts.
Generally, this mechanism lets you drag files from desktop and drop them into an area on my website.
Here's, how it looks (in a shortcut):
function firstDrop(ev) {
var $this = $(this);
//when I call the function here, it passes the event with files inside it
//dropFromDesktop.call($this, ev);
$this.children('.welcomeText').animate({
opacity: '0',
height: '0'
}, 700, function() {
$('#raw .menu').first().slideDown('fast', function() {
//when I call the function here, it passes the event, but 'files' object is empty
dropFromDesktop.call($this, ev);
});
});
}
function dropFromDesktop(ev) {
var files = ev.originalEvent.dataTransfer.files;
(...) //handling the files
}
$('#dropArea').one('drop', firstDrop);
$('some_other_div').on('drop', dropFromDesktop);
The problem is somewhere in jQuery.animation's callback - when I call my function inside it, the event object is passed correctly, but files object from dataTransfer is empty!
Whole script is put inside $(document).ready(function() { ... }); so the order of function declarations doesn't matter, I guess.
I suspect your problem is related with the lifetime of the Event object. Unfortunately, I have no clue about the cause of it. But, there is a way to workaround it that I can think of and it is keeping a reference to Event.dataTransfer.files instead.
var handleFileList = function(fn) {
return function(evt) {
evt.preventDefault();
return fn.call(this, evt.originalEvent.dataTransfer.files);
};
};
var firstDrop = function(fileList) { ... }
var dropFromDesktop = function(fileList) { ... }
$('#dropArea').one('drop', handleFileList(firstDrop));
$('some_other_div').on('drop', handleFileList(dropFromDesktop));​

Only last item has bound click/hover events?

I'm a newbie to Javascript & HTML5. I'm iterating through a set of objects called requests and creating divs for them. I'm trying to have it so that if any of the items are hovered over the style class changes, and if they are clicked on that I will later invoke a function but for now just want an alert. Only the last item gets it.
I've looked at what seemed like similar issues other people have had, but I can't see where I am going wrong.
for (i= 0; i<reqs.length; i++) {
var requestID = "request"+i;
// Build the DIV for each request
element.innerHTML += "<div id="+requestID+" class=request><img class=requestImage src=images/"
+reqs[i].image+" alt=Face /> "+reqs[i].name+"</div>";
var requestElement = $('#'+requestID);
requestElement.hover(
function() {
$(this).removeClass().addClass("requestHover");
},
function() {
$(this).removeClass().addClass("request");
}
);
requestElement.click(
// if the request is clicked, then alert me - testing
function() {
alert('Handler for .click() called.');
}
);
}// end for
From my understanding the $('#request1') should reference the first div item, and $('#request2') the second, etc. It behaves like each .hover and .click assignment overwrites the previous one.
I'd write it this way
for (i= 0; i<reqs.length; i++) {
var requestID = "request"+i;
// Build the DIV for each request
element.innerHTML += "<div id="+requestID+" class=request><img class=requestImage src=images/"
+reqs[i].image+" alt=Face /> "+reqs[i].name+"</div>";
}// end for
var requestElement = $(".request");
requestElement.hover(
function() {
$(this).removeClass().addClass("requestHover");
},
function() {
$(this).removeClass().addClass("request");
}
);
requestElement.click(
// if the request is clicked, then alert me - testing
function() {
alert('Handler for .click() called.');
}
);
Edit to answer to your comment:
No, it's a matter of closure. At the end of the loop, requestID is always the same, so $("#"+requestID); is always the same. It's has if there is only one bind.
To overcome such a thing, you have some option:
using $.each to loop (see http://forum.jquery.com/topic/binding-event-to-element-dynamically , http://forum.jquery.com/topic/binding-click-event-in-a-loop, http://api.jquery.com/jQuery.each/ )
using the live() method (see http://api.jquery.com/live/, http://jsfiddle.net/VrzUb/1/ (used for the click))
assigning the events thanks to a selector common to each object (e.g. using classes, as above)
You can assign the event listener after the for loop, maybe that solves your problem:
for (...) {
}
$('div.request').hover( ... );

Categories

Resources