I'm attempting to use KnockoutJS and jQuery UI Sortable together. I know this has been done before (particularly, knockout-sortable), but my use case has some pretty specific behavior and I'm hoping to avoid trying to make the switch.
Anyway, the problem is pretty straightforward - after moving a DOM element with jQuery UI Sortable, Knockout behaves strangely when removing the observableArray element bound to that DOM element. It will fail to remove the moved element, and if the element that fell into the moved element's place is removed, it will remove both that and the originally moved element. Hard to put into words, but demonstrated by this fiddle.
The problem seems actually take place in the following block in knockout-2.1.0.js:
function fixUpVirtualElements(contiguousNodeArray) {
// Ensures that contiguousNodeArray really *is* an array of contiguous siblings, even if some of the interior
// ones have changed since your array was first built (e.g., because your array contains virtual elements, and
// their virtual children changed when binding was applied to them).
// This is needed so that we can reliably remove or update the nodes corresponding to a given array item
if (contiguousNodeArray.length > 2) {
// Build up the actual new contiguous node set
var current = contiguousNodeArray[0], last = contiguousNodeArray[contiguousNodeArray.length - 1], newContiguousSet = [current];
while (current !== last) {
current = current.nextSibling;
if (!current) // Won't happen, except if the developer has manually removed some DOM elements (then we're in an undefined scenario)
return;
newContiguousSet.push(current);
}
// ... then mutate the input array to match this.
// (The following line replaces the contents of contiguousNodeArray with newContiguousSet)
Array.prototype.splice.apply(contiguousNodeArray, [0, contiguousNodeArray.length].concat(newContiguousSet));
}
}
This call is adding the moved DOM element to the list of elements to be removed when the shifted element is removed.
So an open call to any jQuery UI / Knockoutjs geniuses - is there a way to resolve this conflict, or do I need to do something entirely different to make these tools play nicely together?
I think the "best" solution is to remove the element from DOM and change its position in KO. You can do this in the stop event of the sortable. http://jsfiddle.net/vgrTY/4/
I went ahead and changed your array-contents text to a computed as well so it'll properly display.
Related
I am making a project in Javascript that is mostly on run on a canvas, but also involves dynamically creating a lot of menus in the DOM. I am not using JQuery, instead I am using a custom function that creates an element using document.createElement and then adds it to a parent with appendChild. To remove elements, I use element.parentNode.removeChild(element). Elements created can be of any type; inputs, images, and so on.
The problem is that after creating and removing elements a lot of times, the page starts to slow down significantly, and this worsens steadily the more elements are created and removed, even though there is no point where an especially large number of elements exist at once. The Javascript does not slow down, though; the main update loop is fine, but mouse events and everything related to manipulating the DOM becomes slow until the page is reloaded.
I have experienced similar issues before, but have generally ignored them because they did not involve creating and removing large numbers of elements to the same degree as this one.
The only guess I have for a cause is that elements created by document.createElement continue to exist in memory even if their reference is cleared and they are removed from the visible part of the DOM. Or perhaps removing a parent element does not properly remove all of its children, even though they seem to be gone.
My question is: Are created elements retained by the DOM even when they are not visible and no JS variable points to them, and can this be the cause of slowdown? If so, how do I destroy a DOM element properly?
As it is described here:
The removed child node still exists in memory, but is no longer part
of the DOM. With the first syntax-form shown, you may reuse the
removed node later in your code, via the oldChild object reference.
In the second syntax-form however, there is no oldChild reference
kept, so assuming your code has not kept any other reference to the
node elsewhere, it will immediately become unusable and irretrievable,
and will usually be automatically deleted from memory after a short
time.
And you can try something described as here:
var garbageBin;
window.onload = function ()
{
if (typeof(garbageBin) === 'undefined')
{
//Here we are creating a 'garbage bin' object to temporarily
//store elements that are to be discarded
garbageBin = document.createElement('div');
garbageBin.style.display = 'none'; //Make sure it is not displayed
document.body.appendChild(garbageBin);
}
function discardElement(element)
{
//The way this works is due to the phenomenon whereby child nodes
//of an object with it's innerHTML emptied are removed from memory
//Move the element to the garbage bin element
garbageBin.appendChild(element);
//Empty the garbage bin
garbageBin.innerHTML = "";
}
}
Where you can delete your dom element like:
discardElement(yourDomElement);
I'm trying to select the dom object of an item just before deletion. I hook up the function below to a Delete button's ng-click inside the ng-repeated item, passing the $event object as an argument.
$c = $($event.target)
$c.css('background','red');
$scope.post[$postIndex].comments.splice($index,1);
The select is properly made, but after deletion/splice line (after the original DOM object is destroyed), $c becomes the selector of the next item's delete button - the styling is transferred. How is this possible?
Suspecting that Angular is faster than how jQuery selects the item, I've tried to wrap the deletion line in a $timeout, to no avail.
(I've tried to recreate this problem in a simpler jsfiddle unsuccesfully, after a possible answer, I'll update this post with possible interference)
That should solve it:
var targetID = '#' + $event.target.id;
$c = $(targetID);
$scope.post[$postIndex].comments.splice($index,1);
As my research currently stands, this is just the way ng-repeat (and Angular, really) works: it reuses previous DOM elements (along with all their attributes). Editing DOM outside of Angular is not supported. Sometimes you can bypass this condition, but sometimes not...
Update: in this particular case the problem was that I used track by $index in my repeat, removing that will cure the DOM mismatch. However, this is still a messy solution, only advised if you don't want to edit that element anymore, and also includes performance penalties.
I am creating an array of div tags inside the player table div. I'm getting all div tags with class .players. The divs with class name .players have input fieds and a link field inside. I want to be able to manipulate these (remove, add class, etc...)
What I thought would work would be something like:
$(divarray[j]+' .link').hide();
$(divarray[j]+' a').remove('.link');
But it's not working. Any thoughts? I'm sure it's something simple but it's my first time at JS :)
var divarray = $('#player-table > .players');
for( var j = 0; j < 10; j++){
$(divarray[j]).removeClass("players");
$(divarray[j]).addClass("selected_players");
$('#debug').append(divarray[j]);
$(divarray[j]+' a').hide();
}
First of all, you cannot just concatenate jQuery objects or DOM nodes with strings to create new selectors. jQuery provides methods for this kind of situations, where you already have an object or DOM node and want to find other related nodes.
Second, with jQuery there are much better ways to process a set of elements. Here is your code in more jQuery-like way. This is just an example, because I don't know the HTML structure. You have to adjust it so that it selects and applies to the correct elements.
$('#player-table > .players').slice(0,10) // gets the first 10 elements
.removeClass("players") // removes the class from all of them
.addClass("selected_players") // adds the class
.find('a').hide().end() // finds all descendant links and hides them
.appendTo('#debug'); // appends all elements to `#debug`
As you maybe see, there is only one semicolon at the last line. That means this whole code block is just one statement, but splitting it up over several lines increases readability.
It works because of the fluent interface, a concept which jQuery heavily makes use of. It lets you avoid creating jQuery objects over and over again, like you do ($(divarray[j])).
Another advantage is that you can work on the whole set of elements at once and don't have to iterate over every element explicitly like you have to do with "normal" DOM manipulation methods.
For learning JavaScript, I recommend the MDN JavaScript Guide.
jQuery has a couple of tutorials and a very good API documentation.
Read them thoroughly to understand the basics. You cannot expect to be able to use a tool without reading its instructions first.
Try this istructions
$(divarray[j]).find('.link').hide();
$(divarray[j]).find('a').remove('.link');
Try also
$(divarray[j]).find('.link:first').hide();
If you need to work only on the first element
Hope it helps
Can I determine the number of jQuery objects on a page?
I want to use the number of elements as a sort of weak benchmark for page complexity. I'm thinking that if I can reduce the number of elements that jQuery knows about , the page might run more efficiently.
Does this make sense?
Is it as simple as doing a * select and counting the results?
related:
How can I clear content without getting the dreaded “stop running this script?” dialog?
http://api.jquery.com/size/
var elementCount = $('*').size();
Although this might be more what you want:
var elementCount = $('body').find('*').size()
var n= 0;
for (var i in jQuery.cache)
n++;
Now n holds the number of elements jQuery has ‘touched’ (added data to, such as event handlers).
Previously this used to be a whole lot, as it would ‘touch’ every element it was even checking for data. This unpleasantness is fixed in jQuery 1.4.
As for clearing content, yes, you can use innerHTML= '' to remove all the content without giving jQuery the chance to detach its data so very slowly. If you know there are no ‘touched’ elements inside the element that's a win, but otherwise it's a potential memory leak until the page is reloaded, as unused jQuery.cache data stays around.
Using live()/delegate() event binding avoids adding data to its target elements, which can allow you to use this short-cut more freely. However if you have a lot of events to bind and they're not selectors that are very easy to match quickly, this can make event handling slow.
(Because there is no browser-native speedup like querySelectorAll for matching elements against a particular selector as delegation needs to do; this is proposed for Selectors-API level 2.)
var elementCount = $('*').length;
This doesn't really have much to do with jQuery except insofar as it's a handy way to get the answer.
I'm performance-tuning my code, and am surprised to find that the bottleneck is not dom node insert, but selection.
This is fast:
var row = jquery(rowHTML).appendTo(oThis.parentTable);
but the subsequent getting of an element inside "row" is slow:
var checkbox = jquery(".checkbox input", row);
I need to get the checkbox in every row so I can attach an event handler to it. Selecting the checkbox is ALMOST 10X AS SLOW as inserting the entire parent row.
What am I doing wrong here?
DOM manipulation uses native functions to perform simple operations. Browser vendors optimize these. You are building the row from HTML. Internally jQuery is using .innerHTML to build the collection which then patches into the browser's mega-fast parser.
Selection is slow in comparison because JS code needs to loop through the DOM repeatedly. Newer browsers have native selection handling which provides dramatic speedups to selector based JS. As time moves on this will be less of a problem.
Here is how the query in question, $(".checkbox input", row), breaks down:
row.getElementsByTagName('*');
for-loop through every element returned (all elements within the row) and test elements[i].className with /(\s|^)checkbox(\s|$)/.
for-loop every element still remaining and collect matched[i].getElementsByTagName('input');
unique the final collection.
This is different for jQuery 1.3 as it's engine moves through the selector the other way around, beginning with getting all input elements and then testing the parent elements.
Rremember that the JS selector engines implement a lot more of the CSS selector spec than is actually usable with CSS (or implemented by current browsers). Exploiting this, and knowledge of the engines, we can optimize selector can be optimized in a few different ways:
If you know what element type the .checkbox is:
$("td.checkbox input", row);
It is faster for filter first for type and then for the class for only those matches. This doesn't apply for a very small subset of elements, but that is almost never the case in praxis.
The single class test is the slowest of the common selectors people actually use.
Simpler selection:
$("input[type=checkbox]", row);
One loop is faster than two loops. This only finds input elements and then directly filters them by type attribute. Since sub/child-elements are never used, unique may also be skipped (and smart engines will try to do this because unique is slow).
A more direct selector:
$("td:first.checkbox input", row);
A more complex selector may actually be faster if it is more direct (YMMV).
If possible, move the search context up to the table level:
By this I mean that instead of looping through the rows, and searching for the checkbox in every one, leave them alone until after the loop and then select them all at a time:
$("tr td:first.checkbox input", table);
The point of this is to eliminate the overhead of firing the selector engine up repeatedly, but instead do everything in one haul. This is presented here for completeness rather than something that I think would return massive speedups.
Don't select:
Build the row from bits, assigning events as you go.
var row = $( '<tr></tr>' );
var cell = $( '<td class="checkbox"></td>' ).appendTo( row );
$( '<input type="checkbox" name="..."/>' ).appendTo( cell ).click(/* ... */);
This may be impossible for reasons of Ajax or other templates out of your control. Additionally, the speed may not be worth turning your code into this sort of mess, but sometimes this may make sense.
Or, if none of these work for you, or return too performance gain, it may be time to rethink the method entirely. You can assign an event listener higher up the tree and grab the events there, instead of per-element instance:
$('table').change(function(e){
// you may want a faster check...
if ( $(e.target).is('input[type=checkbox]') ) {
// do some stuff ...
}
});
This way you don't do anything unless, and until, the user actually requests it. Fastest. :-)
var checkbox = jquery(".checkbox input", row);
This is traversing the entire dom tree to find the checkbox. You could possibly speed it up by changing the selector to an ID which can use the browsers native getElementById functionality.
var checkbox = jquery("#checkbox input", row);
You could also use your row as a starting point for the DOM search like the following example. Now your not parsing through the entire DOM tree again to find the matched element.
var row = jquery(rowHTML).appendTo(oThis.parentTable);
row.children(".checkbox input");
Use event delegation and add a single handler to a parent element and not the checkboxes themselves.
jQuery supports this via the live() function.
Try putting a class name on the input field itself. That may prove to be faster.
The reason for that is your code goes through all .checkbox classes, tries to find the input child of that element and returns that. I think that action might be your culprit.
By simply just looking for all elements with the class the input field has, you might see some speedup.
Try using Sly, it has an emphasis on performance.
If you're looking for performance, jQuery selectors are very slow. In the example there, it has to scan the full DOM tree and check CSS classes and so on to find the relevant nodes.
It is significantly faster to use native DOM methods. There are some interesting library performance comparisons here:
http://ajaxian.com/archives/taskspeed-more-benchmarks-for-the-libraries-and-browsers
The fastest way to get the DOM element is to use pure JavaScript and calling it by ID.
var element = document.getElementById('element);