Tool to count the bindings on a web page? - javascript

I have been tasked with optimizing a web site recently. One thing I suspect is that there are way too many events bound to different objects on the web site. Like each item in a grid having an event bound to it's double click event. This slows down the rendering as it has to set up a binding for each item as things are added. I would expect we could do something like a doubleclick on a grid binding and then determine the item clicked.
Anyway, the client is asking for metrics on things. I need to be able to give them some kind of report that says there are X events bound on the page currently. My change takes it down to Y number of bindings.
Is there any tool out there that you know of that can give me that kind of count? I thought about the event listeners tab on the elements view of chrome developer tools, but I wasn't sure if that listed all the events and if so, how to export the list out to get a count.

Visual Event gives you an overview of all events bound on a particular page.
If you are running chrome you can also get bound events on a DOM node using the command line API:
getEventListeners(el); // {click: [..], mouseover: [..], ...}
This feature can be wrapped in something like:
// Run this in your console:
var count = 0;
// forEach on all elements in the DOM:
[].slice.call(document.getElementsByTagName('*')).forEach(function(el) {
// count all bound events using `getEventListeners`
var events = getEventListeners(el);
for ( var prop in events ) count += events[prop].length;
});
console.log('Total events bound: %d', count);
Hope you get the general idea.

To count bindings on an element:
var count = 0;
$('#myTable *').each(function () {
$.each(getEventListeners(this), function () {
count += this.length;
});
});
console.log(count + ' bindings in #myTable');
To optimize, you will be able to do this:
With raw JS:
document.getElementById('myTable').onclick = function (event) {
console.log(event.target); // Contains the clicked TD tag element
};
With jQuery:
$('#mayTable').on('click', 'td', function() {
console.log(this); // Contains the clicked TD tag element
});
This will decrease bindings from columns * lines of your grid to 1.

Related

Handling event listeners on multiple pages

I've written a javascript file which has some event listeners in for things like tabs, accordions and so on. However these aren't present in every single page so it's looking for elements that don't exist and throws the entire js out of wack.
I know I could get around it by using multiple if statements, but it doesn't sound like it would be correct.
// Accordion
const accordions = document.querySelectorAll('.accordion li');
accordions.forEach(accordion =>{
accordion.addEventListener('click', (e) => {
e.preventDefault();
accordion.classList.toggle('open');
})
});
// Inline toggle
const inlineToggle = document.getElementById('inline-toggle');
inlineToggle.addEventListener('click', () => {
inlineToggle.nextElementSibling.classList.toggle('active');
});
const inlineToggleOptions = document.querySelectorAll('.inline-toggle-options button');
inlineToggleOptions.forEach(option => {
option.addEventListener('click', (e) => {
// Prevent default
e.preventDefault();
// Update sentence text
inlineToggle.innerHTML = option.dataset.payType;
// Remove selected class from options
inlineToggleOptions.forEach(option => {
option.classList.remove('selected');
});
// Add selected class to chosen option
option.classList.add('selected');
// Close dialog
inlineToggle.nextElementSibling.classList.remove('active');
})
});
// Cover bubbles
// Create the slidepanel
const placeholder = document.getElementById('slidepanel');
// Find all buttons
const button = document.querySelectorAll('.trigger-aside');
button.forEach((button => {
// Listen for clicks on buttons
button.addEventListener('click',(e) => {
// Prevent default
e.preventDefault();
// Get the target
const target = button.dataset.target;
console.log(target);
// Call the API
fetch(`http://****.****.uk/****/****/****/${target}`)
.then((res) => res.json())
.then(function(res) {
// Load HTML into slider panel
placeholder.innerHTML = res.object.content;
// Stop body overflow
document.body.classList.add('overflow-hidden');
// Create overlay and append
const overlay = document.querySelector('.overlay');
overlay.classList.add('active');
document.body.appendChild(overlay);
// Show the panel
placeholder.classList.add('active');
document.body.appendChild(placeholder);
// Listen for close
overlay.addEventListener('click', (e) =>{
// Close requested
document.body.classList.remove('overflow-hidden');
placeholder.classList.remove('active');
overlay.classList.remove('active');
});
})
.catch(function(err) {
// Log error
console.log(err);
});
})
}));
How do other people generally get around this issue? Any guidance appreciated!!
Event Delegation Pattern
If you have these UI elements grouped under a parent element in common across all pages(like a div wrapper), you could try making use of the Event Delegation pattern. Essentially, you can assign a click event to that parent element and make use of a function to only take action if the desired element is returned - i.e. your buttons. It would go something like...
const parent = document.querySelector('div.wrapper'); //Change selector to suit a common parent
const buttons = [...document.querySelectorAll('.inline-toggle-options button')]; // convert to array to make it easier to work with
const elementsToChange = document.querySelectorAll('.elements .to .change');
parent.addEventListener('click', toggleOptions);
function getEventTarget(e) {
e = e || window.event;
return e.target || e.srcElement; // For IE compatibility
}
function toggleOptions {
let target = getEventTarget(e);
if(buttons.includes(target)) {
// Trigger options on UI elements if any of the buttons are among the clicked elements
// Target refers to the buttons in particular, not the UI elements you want to change
}
}
Whichever way you want to refactor the code to take action on specific elements is up to you. You can group buttons by specific functionality into distinct arrays. If you had 2 or 3 arrays, you'd only need to write 2 or 3 options from a conditional statement.
For this purpose, you'll also save memory with this pattern since you're only assigning one event handler and letting child events on child elements bubble up to be taken care of by that single handler. Also, you shouldn't run into errors since the initial event handler is assigned to a parent element that's common across all pages.
Some Caveats
From the guide linked above:
Not all events bubble. The blur, focus, load and unload events don’t
bubble like other events. The blur and focus events can actually be
accessed using the capturing phase (in browsers other than IE) instead
of the bubbling phase but that’s a story for another day.
You need caution when managing some mouse events. If your code is
handling the mousemove event you are in serious risk of creating a
performance bottleneck because the mousemove event is triggered so
often. The mouseout event has a quirky behaviour that is difficult to
manage with event delegation.
I'd generally advise to think first about code organisation and second about efficiency. I agree with StevenB.'s suggestion and Luke Tubby's answer, to make the best of your current situation with a minimum of effort.
For more elaborate solutions, I would suggest to familiarise with build / packaging tools (e.g. Webpack ), that offer you ways to structure your code (and assets) in files and directories of your choice, and create page specific minified packages.
Another (completely different and independent) approach to the problem (of improving efficiency and code organisation) would be to build a single page application...

click event with each loop on dynamicly added elements javascript / jquery

my problem is that the $('input[name=isDefault]').length equal 2, but the each runs ony ones.
$(saveProduct).click(function (e) {
$('input[name=isDefault]').each(function (index) {
var that = this;
console.log(index, 'index');
console.log($('input[name=isDefault]').length);
// do something...
})
})
$('input[name=isDefault]') is dynamicly addes obj during another logic.
I know there is a solution like $(upper container).on("event", dynamic obj inside upper container) - but this causes the click event to be almost everywhere, because .on is attaching event on big area container
EDIT:
I made some changes for testing purpose:
var imgs = imgs = $('div[name=fileContainer]');
console.log('before loop', imgs.length); // = 1
for (var i = 0; i < imgs.length; i++) {
console.log('inside loop', imgs.length); // = 1
//do something - imgs is dynamicly added html element by jquery, when I load page there is 1 element with that name
}
,but when I write $('div[name=fileContainer]').length; in console it's equal 2 (of course when I dynamicly added + 1 fileContainer element)
EDIT:
this isn't the answer, but I omitted the need to use the above loop.
Elsewhere in the code, I already have $(something).on("change", dynamiclyAddedElements function() {}) with difrent main purpouse..., but! there I add addintional function where I created javascipt obj with needed data. Then I can use them in above click event.
$(upper container).on("event", dynamic obj inside upper container) - but this causes the click event to be almost everywhere, because .on is attaching event on big area container
thats not right, you can use the second parameter to specify the selector
e.g
$('#upper_container').on('click', '.saveProduct', function(){...})
will add the click handler to (also dynamically added) .saveProduct and not #upper_container as you claim

Adding a click function to cells in a dynamically created table - JavaScript

var tableData = document.getElementsByTagName("td");
for(var i = 0; i < tableData.length; i++){
var x = tableData[i];
x.addEventListener("click", fun1(x.textContent));
}
function fun1(y){
document.getElementById("testB").textContent = y;
}
I am trying to add a click function to every cell in a dynamically created table (i have no way of knowing how many cells are in the table or what their id would be but i only need the text contained in the cell)
I want to update a button with the content of the cell and as of right now it does not update on click and instead updates the button to the last value of the table.
I am fairly new to JavaScript and would appreciate any help you could offer.
here is the solution i used thanks to the answer provided below
var tables = document.getElementsByTagName("table");
for(var i = 0; i < tables.length; i++){
tables[i].addEventListener("click",
function(e) {
if(e.target && e.target.nodeName == "TD") {
fun1(e.target.textContent);
}
}
);
}
function fun1(y){
document.getElementById("testB").textContent = y;
}
i went with this solution because the script generating the tables is not mine and as such i do not know what the id of the table i want is and do not know how many tables it actually creates.
You almost certainly do not want to add an event handler individually to each table cell if all (or even many) of them should do the same thing when clicked.
The DOM event model was designed to support delegation, which is a much better solution in cases like this. In brief: individual cells do not respond to clicks, but some ancestor element that is considered a permanent fixture (say, the <table> they are in) detects the clicks on cells and responds to them appropriately.
Here's how to set up delegation:
document.getElementById("#mytable").addEventListener("click",
function(e) {
if(e.target && e.target.nodeName == "TD") {
fun1(e.target.textContent);
}
}
);
Of course you can choose any ancestor element you want, even <body> if that's what is required. Also note that this way of doing things means your code behaves as expected even while cells are added to or removed from the table -- you just do this once on page initialization.
x.addEventListener("click", fun1(x.textContent));
This line is your issue -- addEventListener looks for a function reference, you're just calling the function (which does not return a function). Wrap it in an anonymous function, or bind the value to the reference.
x.addEventListener("click", function() {
fun1(x.textContent)
}, false); //also a good idea to add the event bubbling boolean param
Alternatively, ES6 arrow syntax:
x.addEventListener("click", () => fun1(x.textContent), false);

Jquery Mobile detect which row was clicked in a listview

I have looked high and low and can't seem to find this anywhere. Does anyone know how to get the value of a row tapped in a listview? This can be anything from the name to the index in the object. Right now I have a function that handles the tap. I need to be able to pass a value to the new page I am loading when it transitions. I thought I could do it here:
$('#taskListTable').delegate('li', 'tap', function () {
console.log('clicked');
//Insert code here to pull a value from either an index or a name and save it
});
I thought maybe it would be good to do it in the hash? I am not sure what the standard practice is here on the web coming from native iOS dev though. Anyone have any pointers? Thanks.
This is how I am populating my listview:
$.each(tasks, function(index, task) {
$taskList.append("<li><a href='taskDetails.html'> <h3>"+task.name+"</h3><p>"+task.description+"</p></a></li>");
});
taskDetails.html needs the index of the task so I can pull the details down from the server. What is the standard practice for doing that?
To get the index of the taped list-item you can do this:
$('#taskListTable').delegate('li', 'tap', function () {
console.log('clicked');
var index = $(this).index();
});
Yup, that's it. Although this assumes that the <li> element are all siblings.
Docs for .index(): http://api.jquery.com/index
If you want to then transition to the new page:
$('#taskListTable').delegate('li', 'tap', function () {
console.log('clicked');
$.mobile.changePage($(this).find('a').attr('href'), {
data : { selectedIndex : $(this).index() }
});
});
This will get the new page and attach the selectedIndex variable as a query string parameter that is set to the index of the tapped list-item.
Also, to be able to prevent the default behavior of clicking on the link in the list-item, I would attach this event handler to the link elements:
$('#taskListTable').delegate('a', 'tap', function (event) {
event.preventDefault();
console.log('link clicked');
$.mobile.changePage($(this).attr('href'), {
data : { selectedIndex : $(this).closest('li').index() }
});
});
The "delegate" method is deprecated on new versions of jQuery.
Consider using this instead:
$('#taskListTable').on('tap', 'a', function (event) {
}
Here is another method of getting an id, especially useful if you have a database enabled app and the id you are searching for is the database's id, not the row index:
When filling the table consider this:
<ul id="taskListTable">
<li id="task400" class="tasks">Task with db id 400</li>
<li id="task295" class="tasks">Task with db id 295</li>
</ul>
$('#taskListTable').on('tap', 'li', function (event) {
variable = $(this).attr('id').substr(4);
}
That will get the database id which you can pass on to your other pages.
To get the name out, you could do:
$(this).find('h3').html()
Alternatively, you can use something in the markup like the id or a data attribute to provide a better handle than the name.
What about this approach?
$taskList.append("<li><a href='taskDetails.html?id=" + task.id + "'></a><h3>" + task.name + "</h3>...</li>");
And then retreive id parameter.
Jasper's solution is good, but if you have any other code in your event handler that edits the DOM, the "this" keyword could be pointed somewhere entirely different by the time it gets used in your code.
I've cracked my head on my keyboard a number of times because my users said "I click on item one, but that always opens the edit screen for the last item in the list." So somehow using 'this' was not the right way to go.
Also, when I tried event.target, event.currentTarget or event.originalTarget, those didn't work either. They all pointed to the jQueryMobile "page" that was visible by the time the code got to run (which wasn't even the same page where the table was located).
The safe and/or intended approach is to:
use event.originalEvent.srcElement instead of 'this'
not use .delegate(), but use .on()
bind the event using $(document).on(), not $('#table').on()
So that would result in:
$(document).on('tap','#taskListTable li',function(event){
//The properties of 'event' relate to $(document), not to '#taskListTable li',
//so avoid .currentTarget, .target etc.
//But fortunately 'event' does have one property that refers to the original
//user event.
var theOriginalTapEvent = event.originalEvent;
var theRealClickedElement = theOriginalTapEvent.srcElement;
//Note that the clicked element could be a sub-element of the li
//whose index you want. So just referencing theRealClickedItem could
//could still be getting you bad results.
var theRelevantListItem = $(theRealClickedElement).parents('li');
//Now you're ready to get the item's index or whatever.
})

jQuery: Get reference to click event and trigger it later?

I want to wrap an existing click event in some extra code.
Basically I have a multi part form in an accordion and I want to trigger validation on the accordion header click. The accordion code is used elsewhere and I don't want to change it.
Here's what I've tried:
//Take the click events off the accordion elements and wrap them to trigger validation
$('.accordion h1').each(function (index, value) {
var currentAccordion = $(value);
//Get reference to original click
var originalClick = currentAccordion.click;
//unbind original click
currentAccordion.unbind('click');
//bind new event
currentAccordion.click(function () {
//Trigger validation
if ($('#aspnetForm').valid()) {
current = parseInt($(this).next().find('.calculate-step').attr('data-step'));
//Call original click.
originalClick();
}
});
});
jQuery throws an error because it's trying to do this.trigger inside the originalClick function and I don't think this is what jQuery expects it to be.
EDIT: Updated code. This works but it is a bit ugly!
//Take the click events off the accordion elements and wrap them to trigger validation
$('.accordion h1').each(function (index, value) {
var currentAccordion = $(value);
var originalClick = currentAccordion.data("events")['click'][0].handler;
currentAccordion.unbind('click');
currentAccordion.click(function (e) {
if ($('#aspnetForm').valid()) {
current = parseInt($(this).next().find('.calculate-step').attr('data-step'));
$.proxy(originalClick, currentAccordion)(e);
}
});
});
I think this:
var originalClick = currentAccordion.click;
Isn't actually doing what you think it is - you're capturing a reference to the jQuery click function, rather than event handler you added, so when you call originalClick() it's equivalent to: $(value).click()
I finally came up with something reliable:
$(".remove").each(function(){
// get all our click events and store them
var x = $._data($(this)[0], "events");
var y = {}
for(i in x.click)
{
if(x.click[i].handler)
{
y[i] = x.click[i].handler;
}
}
// stop our click event from running
$(this).off("click")
// re-add our click event with a confirmation
$(this).click(function(){
if(confirm("Are you sure?"))
{
// if they click yes, run click events!
for(i in y)
{
y[i]()
}
return true;
}
// if they click cancel, return false
return false;
})
})
This may seem a bit weird (why do we store the click events in the variable "y"?)
Originally I tried to run the handlers in x.click, but they seem to be destroyed when we call .off("click"). Creating a copy of the handlers in a separate variable "y" worked. Sorry I don't have an in depth explanation, but I believe the .off("click") method removes the click event from our document, along with the handlers.
http://www.frankforte.ca/blog/32/unbind-a-click-event-store-it-and-re-add-the-event-later-with-jquery/
I'm not a jQuery user, but in Javascript, you can set the context of the this keyword.
In jQuery, you use the $.proxy() method to do this.
$.proxy(originalClick, value);
originalClick();
Personally, I'd look at creating callback hooks in your Accordion, or making use of existing callbacks (if they exist) that trigger when opening or closing an accordion pane.
Hope that helps :)
currentAccordion.click is a jQuery function, not the actual event.
Starting with a brute-force approach, what you'd need to do is:
Save references to all the currently bound handlers
Unbind them
Add your own handler, and fire the saved ones when needed
Make sure new handlers bound to click are catched too
This looks like a job for an event filter plugin, but I couldn't find one. If the last point is not required in your application, then it's a bit simpler.
Edit: After some research, the bindIf function shown here looks to be what you'd need (or at least give a general direction)

Categories

Resources