I've implemented a routine to sort an xhtml table in place that is based on (= more or less copied from) Nicholas C. Zakas, Professional JavaScript for Web Developers, Ch. 12. It works as advertised, but I have a few questions about how the code works and would be grateful for advice. Here are the relevant bits:
var oldTable = document.getElementById('myTable');
var oldTableBody = oldTable.tBodies[0];
var oldTableRows = oldTableBody.rows;
var newTableArray = new Array();
for (var i = 0, rowCount = oldTableRows.length; i < rowCount; i++) {
newTableArray.push(oldTableRows[i]);
}
newTableArray.sort(chooseSort);
var newFragment = document.createDocumentFragment();
for (var i = 0, rowCount = newTableArray.length; i < rowCount; i++) {
newFragment.appendChild(newTableArray[i]);
}
oldTableBody.appendChild(newFragment);
I understand that I'm adding pointers to the existing table rows when I push new values onto newTableArray, but I'm not sure how the appendChild methods work. Specifically:
When I append each of the array items (pointers to the original rows) to the document fragment, am I just adding a pointer (leaving the row still in the original table), or am I removing the row object from the original table by appending it somewhere else?
When I append newFragment to oldTableBody, if I understand correctly, I'm actually appending not the fragment as an object, but the row objects that I've appended to it, and that's just the way fragments work. Is that correct?
When I append newFragment to oldTableBody, I don't do anything explicit to remove the rows that were originally there. Where did they go? Does appending them anew automatically remove any old traces of them because they can be attached only once? If I'm working with pointers, and not the objects themselves (which I thought was what let me attach them to newTableArray without automatically making them disappear from the table), does this mean that I can have multiple pointers to the same object in some cases (oldTable plus newTableArray) but not others (original rows in the original table plus the new, sorted rows that I'm adding)?
Apologies for the naive questions, but although getting the results that I want is satisfying, not understanding how the code works makes me uneasy.
I think all your questions boil down to asking what appendChild does. It removes the element from where it was in the DOM and places the same element in the new spot.
Your second assertion is correct. You're not creating a pointer, but rather relocating the object into the document fragment.
Your understanding of documenty fragments is correct. It is a generic container that never becomes part of the DOM. Rather its content is appended.
Relates to item one. You removed them from the table when appending them to the fragment. When adding them to the Array, it is just a pointer to the Row Objects getting added. Adding elements to an Array does nothing to manipulate the DOM itself.
As such, your code is ultimately:
doing an iteration to reference each table row into an Array
doing an iteration over the Array to relocate each row into a document fragment
appending the content of the fragment into the original table
...resulting in no noticeable change to the DOM.
If you were hoping to duplicate the rows, you may be interested in the .cloneNode() method.
But no, you can't have a single element appear in several different places in the DOM in such a manner that an update to the element in one location will be reflected in other locations.
Related
I'm sorting a table using jQuery, roughly following the code found here. The code sketch is as follows:
$('.sort-table').click(function(e) {
// cache objects
$table = $('#sort-table'), // cache the target table DOM element
$rows = $('tbody > tr', $table); // cache rows from target table body
// sort items
$rows.sort(<my_predicate_function(a,b){...}>);
// assign to table - what is going on?
$rows.each(function(index, row){
$table.append(row); // <-- how come $table remains the same size?
});
});
While the code works fine, I'm puzzeled by the code that appends the rows back to the table, which simply iterates over the sorted rows, appending each at the end of $table.
At no stage did we emtpy $table from its previous children.
Since $table was never emptied, how come $table remains the same size?
Does append() also enforces uniquness in the target container?
This is simply how the DOM works. You can't have an element in two different places. If an element is already in a document and you put it somewhere else, it will be moved from its current position. It is, I suppose, a little like a Set, but that is not how it is specified. So it's not removing duplicate objects, because there never are duplicate objects: it's simply moving the same object, which can only exist in one place.
From the MDN documentation for the underlying method, Node.appendChild:
The Node.appendChild() method adds a node to the end of the list of children of a specified parent node. If the given child is a reference to an existing node in the document, appendChild() moves it from its current position to the new position (there is no requirement to remove the node from its parent node before appending it to some other node).
If you want to duplicate elements, you need to clone them (DOM, jQuery).
As from JQuery doc for .append()
Description: Insert content, specified by the parameter, to the end of each element in the set of matched elements.
As from MDM doc for Set
The Set object lets you store unique values of any type, whether primitive values or object references.
Nothing gets removed.
You only will replace the existing element if it's exactly the same, which is the case in your example.
I'm displaying elements from an arraylist in table on the webpage. I want to make sure that once the user press "delete the data", the element in the table is immediately removed so the user does not have to refresh and wait to see the new table. So I'm currently doing it by removing the element from the arraylist, below is the code:
$scope.list= function(Id) {
var position = $scope.list.indexOf(fooCollection.findElementById({Id:Id}));
fooCollection.delete({Id:Id});
if (position>-1) {
$scope.list.splice(position,1);
}
$location.path('/list');
};
But I the position is always -1, so the last item is always removed from the list no matter which element I delete.
I found it strange you we're operating on two different lists to begin with so I assumed you were taking a copy of the initial list. This enabled me to reproduce your bug. On the following line you're trying to find an object that isn't present in your list.
var position = $scope.list.indexOf(fooCollection.findElementById({Id:Id}));
Eventhough we're talking about the same content, these two objects are not the same because:
indexOf compares searchElement to elements of the Array using strict
equality (the same method used by the ===, or triple-equals,
operator).
So there lies your problem. You can see this reproduced on this plunker.
Fixing it the quick way would mean looping over your $scope.list and finding out which element actually has the id that is being passed.
you can use the splice method of javascript which takes two paramete
arrayObject.splice(param1, param2);
param1 -> from this index elements will start removing
param2 -> no of elements will be remove
like if you want to remove only first element and your array object is arrayObject then we can write code as following
arrayObject.splice(0, 1);
I don't think this is strictly infinite scrolling but it was the best i could think of that compares to what i am seeing.
Anyway, we are using ng-grid to show data in a table. We have roughly 170 items (rows) to display. when we use ng-grid it creates a repeater. When i inspect this repeater from the browser its limited to 35 items and, as you scroll down the list, you start to lose the top rows from the dom and new rows are added at the bottom etc (hence why i don't think its strictly infinite scrolling as that usually just adds more rows)
Just so I'm clear there is always 35 'ng-repeat=row in rendered rows' elements in the dom no matter how far you have scrolled down.
This is great until it comes to testing. I need to get the text for every item in the list, but using element.all(by.binding('item.name')) or by.repeater or by.css doesn't help as there is only ever 35 items present on the page.
Now to my question, how can i make it so that i can grab all 170 items as an object that i can then iterate through to grab the text of and store as an array?
on other pages where we have less than 35 items iv just used the binding to create an object, then using async.js to go over each row and get text (see below for an example, it is modified extract i know it probably wouldn't work as it is, its just to give you reference)
//column data contains only 35 rows, i need all 170.
var columnData = element.all(by.binding('row.entity.name'))
, colDataTextArr = []
//prevOrderArray gets created elsewhere
, prevOrderArray = ['item1', 'item2'... 'item 169', 'item 170'];
function(columnData, colDataTextArr, prevOrderArray){
columnData.then(function(colData){
//using async to go over each row
async.eachSeries(colData, function(colDataRow, nRow){
//get the text for the current async row
colDataRow.getText().then(function(colDataText){
//add each rows text to an array
colDataTextArr.push(colDataText);
nRow()
});
}, function(err){
if(err){
console.log('Failed to process a row')
}else{
//perform the expect
return expect(colDataTextArr).toEqual(prevOrderArray);
}
});
});
}
As an aside, I am aware that iterating through 170 rows and storing the text in an array isn't very efficient so if there is a better way of doing that as well I'm open to suggestions.
I am fairly new to JavaScript and web testing so if im not making sense because I'm using wrong terminology or whatever let me know and i'll try and explain more clearly.
I think it is an overkill to test all the rows in the grid. I guess it would be sufficient to test that you get values for the first few rows and then, if you absolutely need to test all the elements, use an evaluate().
http://angular.github.io/protractor/#/api?view=ElementFinder.prototype.evaluate
Unfortunately there is no code snippet in the api page, but it would look something like this:
// Grab an element where you want to evaluate an angular expression
element(by.css('grid-selector')).evaluate('rows').then(function(rows){
// This will give you the value of the rows scope variable bound to the element.
expect(rows[169].name).toEqual('some name');
});
Let me know if it works.
I've looked through so many forum posts about this simple piece of code. And tried many variations and methods with nothing working.
I have a script that builds a table based on a database query. I limit it to 10 results and provide forward and back buttons to go to other results from the database. When you press the next button, I need it to delete all the current table rows so it can recreate them with the new results of the query. I am using the following code to remove the rows;
if (document.getElementsByClassName('shiftTrRow')) {
table = document.getElementById('resultsTable')
deleteRow = document.getElementsByClassName('shiftTrRow');
table.removeChild(deleteRow);
}
I use the if case to check if there are any rows to delete, I've added an alert to test if there are rows discovered and this works.
When I run the code I receive the following error on the line:
table.removeChild(deleteRow)
Uncaught Error: NOT_FOUND_ERR: DOM Exception 8
I've looked up the error code and it explains that the element couldn't be found, but this cannot be the case since this code will only execute if the element is found. If anyone knows what I'm doing wrong or a better way to do this I would really appreciate it.
document.getElementsByClassName will return a NodeList, not a single node. You'll need to chose a node from that list, for example document.getElementsByClassName('shiftTrRow')[0].
Also make sure that the node is actually a child of the given table. The following code is a little bit more exception safe:
if (document.getElementsByClassName('shiftTrRow'))
{
deleteRow = document.getElementsByClassName('shiftTrRow')[0];
if(deleteRow.parentNode)
deleteRow.parentNode.removeChild(deleteRow);
}
document.getElementsByClassName('shiftTrRow')
^
returns a NodeList, i.e. an array of elements. You will need to loop over them and remove every of them on its own (but watch out, the collection is live and will shrink while you remove the elements).
Also, the getElementsByClassName may return elements that are no rows of that table at all, resulting in the NOT FOUND exception you receive.
However, the simplest way to remove rows from a table is its deleteRow method:
var table = document.getElementById('resultsTable');
while (table.rows.length > 0)
table.deleteRow(0);
document.getElementsByClassName returns an array of elements. table.removeChild expects a DOM element, not an array, for its argument.
I've stumbled upon a tricky one, that I haven't been able to find any references to (except one here on Stackoverflow, that was written quite inefficiently in Plain Old Javascript - where I would like it written in jQuery).
Problem
I need to retrieve all child-elements where the attribute-name (note: not the attribute-value) starts with a given string.
So if I have:
<a data-prefix-age="22">22</a>
<a data-prefix-weight="82">82</a>
meh
My query would return a collection of two elements, which would be the first two with the data-prefix--prefix
Any ideas on how to write up this query?
I was going for something like:
$(document).find("[data-prefix-*]")
But of course that is not valid
Hopefully one of you has a more keen eye on how to resolve this.
Solution
(See accepted code example below)
There is apparently no direct way to query on partial attribute names. What you should do instead (this is just one possible solution) is
select the smallest possible collection of elements you can
iterate over them
and then for each element iterate over the attributes of the element
When you find a hit, add it to a collection
then leave the loop and move on to the next element to be checked.
You should end up with an array containing the elements you need.
Hope it helps :)
Perhaps this will do the trick -
// retrieve all elements within the `#form_id` container
var $formElements = $("form#form_id > *");
var selectedElements = [];
// iterate over each element
$formElements.each(function(index,elem){
// store the JavaScript "attributes" property
var elementAttr = $(this)[0].attributes;
// iterate over each attribute
$(elementAttr).each(function(attIndex,attr){
// check the "nodeName" for the data prefix
if (attr.nodeName.search('data-.*') !== -1){
// we have found a matching element!
if (selectedElements.length < 2){
selectedElements.push(elem);
break;
}else{
if (selectedElements.length == 2){
break(2);
}
}
}
});
});
selectedElements will now hold the first two matching elements.
jsFiddle
You can use jquerys filter() method to have a selections elements being processed by a function. Inside that function you are free to do whatever you want to.
So start with selecting all elements inside the dom tree and filter out everything you dislike. Not especially fast, but working.