Explanation on 'live' elements - javascript

I just read this article about NodeLists:
http://www.nczonline.net/blog/2010/09/28/why-is-getelementsbytagname-faster-that-queryselectorall/
If I understand correctly getElementsByTag name is live and querySelectorAll is not.
Then could someone please explain to my why pNotLive has the title 'stackoverflow'? :
var pLive = document.getElementsByTagName( 'p' )[3];
var pNotLive = document.querySelectorAll( 'p' )[3];
pLive.title = "stackoverflow"
console.log( pNotLive.title ); // stackoverflow
//you can run this snippet in your console to verify

As your link explains, the getElementsByTagName method returns a collection that is automatically updated when the DOM changes. So if you call the method, and then a new element is added to the DOM, your collection will automatically be updated with the new element.
Where as, if you use querySelectorAll, you will be given a static list of DOM elements, that will NOT be updated automatically.
I believe the reason for the behaviour you have used in your example is because the list is static, and not the elements themselves. So the static approach just means the list will not change, so adding/removing elements will not change your list. But when you call for the title attribute, your list is simply pointing to the element, and that element is different from when you created the list.
In short, it is more of a list of references (to the DOM nodes), than a list of data objects (with the fixed data).

Related

How to get all selectors by class name that were dynamically appended in vanilla JavaScript?

I need to get / count how many elements with a common class target name are "available". None on those elements physically exists in the DOM. Those items have been added later when the page was fully loaded.
Below
var targets = document.getElementsByClassName('target');
when I console.log(targets); I get [].
When I click those square brackets, they expand and target items seems to appear but next to them there's a message:
Object value at left was snapshotted when logged, value below was
evaluated just now.
So I assume that I did console.log when DOM hasn't been populated with target elements yet. How do I get information about dynamically added elements?
EDIT:
I checked hsh's functions and
document.body.addEventListener('DOMSubtreeModified', function(event) {
var targets = document.getElementsByClassName('target');
console.log(targets.length);
/**
* If I have 40 target elements, this will be called 40 times :/ showing first bunch of zeros then finally number will reach to 40
*/
});
/**
* So this would be ideally (called only once) but this always shows empty array and 0
*/
document.addEventListener('DOMContentLoaded', function(event) {
var targets = document.getElementsByClassName('marker');
console.log(targets); // always shows []
console.log(targets.length); // always shows 0
//while I can play with those target selectors in Chrome Dev Tools
});
PS. No jQuery please.
You can call your check script in DOMContentLoaded event:
document.addEventListener('DOMContentLoaded', function(event) {
var targets = document.getElementsByClassName('target');
console.log(targets);
});
Also you can use DOMSubtreeModified event if you're expecting that something will be added during the runtime.
document.body.addEventListener('DOMSubtreeModified', function(event) {
var targets = document.getElementsByClassName('target');
console.log(targets);
});
JSFiddle
Your question is very confusing.
I need to get / count how many selectors
A "selector" is a way of targeting/addressing elements. A selector is used to find, or match, elements. For instance, .foo is a selector, which matches elements with the class "foo". In your case, you mean you want to get/count how many elements.
I need to get / count how many elements with a common class target name are "available". None on those elements physically exists in the DOM.
So the elements are available, but not in the DOM? What does that mean?
Those items have been added later when the page was fully loaded.
What do you mean by fully loaded? Same as DOMContentLoaded? Or fully loaded in the sense that all JS to help build the page has also completed?
The easiest thing to do is to wait until all the logic which adds elements has finished running, then evaluate getElementsByClassName. Why can't you do that?
If for some reason you evaluate getElementsByClassName earlier, or on page load, you will find that at any given point in time it still reflects an up-to-date list of elements, since getElementsByClassName returns a live collection, which is updated as your document changes. That also means you don't need to run anything at load time, or watch arcane events such as DOMSubtreeModified.
At any point in time, including after your elements have been added, the value of targets will include all elements with that class, and targets.length will give the total number of such elements.
From MDN:
elements is a live HTMLCollection of found elements.

JQuery working wierdly for function 'find':

I have the following jQuery line:
$('<html>hi</html>').find('a')
I expect the result to be a wrapped set of one element. However the result is an empty array ([]). Why?
-- EDIT --
For some reason the code below works.
$('<html><div>hi</div></html>').find('a');
Why is this happening?
That's because the html element is stripped when the string is parsed:
> $('<html>hi</html>')
[​hi​​]
i.e. the current collection contains an element that you are trying to find(). As the top-level a element doesn't (and can't) have a descendants the find() call will return an empty collection.
From jQuery documentation:
When passing in complex HTML, some browsers may not generate a DOM that exactly replicates the HTML source provided. As mentioned, jQuery uses the browser's .innerHTML property to parse the passed HTML and insert it into the current document. During this process, some browsers filter out certain elements such as <html>, <title>, or <head> elements. As a result, the elements inserted may not be representative of the original string passed.
edit: The second snippet can find() a element as when the html element is stripped the top-level element of the collection is a div element that does have a descendant.
As in the Documentation of .find() descriped
Get the descendants of each element in the current set of matched elements, filtered by a selector, jQuery object, or element.
$('<html>hi</html>')
will just provide an Object of your a-tag.
Demo
If there are multiple anchor-tags inside your html-string you can filter them, e.g.:
var elem = $('<html>hihi</html>');
var filter = elem.filter(function(){
return $(this).attr('href') === "cnn.com";
});
Demo
Edit
When passing in complex HTML, some browsers may not generate a DOM
that exactly replicates the HTML source provided. As mentioned, jQuery
uses the browser's .innerHTML property to parse the passed HTML and
insert it into the current document. During this process, some
browsers filter out certain elements such as <html>, <title>, or
<head> elements. As a result, the elements inserted may not be
representative of the original string passed.
Source: http://api.jquery.com/jQuery/#jQuery2 down to the Paragraph Creating New Elements
So jQuery uses .innerHTML. According to the docs
Removes all of element's children, parses the content string and
assigns the resulting nodes as children of the element.
So the html-string <html>test</html> gets stripped to <a></a>.
When wrapping a div around the anchor, the anchor stays a descendat of an elemnt and therefore gets found by the .find()-function.
You should read the documentation at Jquery docs about find()
$('html').find('a');
Check this jsfiddle

How can I access a particular div on a page which has the same id in two places?

This is the same question as this:
Referring to a div inside a div with the same ID as another inside another
except for one thing.
The reason there are two elements with the same ID is because I'm adding rows to a table, and I'm doing that by making a hidden div with the contents of the row as a template. I make a new div, copy the innerhtml of the template to my new div, and then I just want to edit bits of it, but all the bits have the same ID as the template.
I could dynamically create the row element by element but it's a VERY complex row, and there's only a few things that need to be changed, so it's a lot easier to just copy from a template and change the few things I need to.
So how do I refer to the elements in my copy, rather than the template?
I don't want to mess up the template itself, or I'll never be able to get at the bits for a second use.
Or is there another simpler way to solve the problem?
It will probably just be easiest when manipulating the innerHtml to do a replace on the IDs for that row. Maybe something like...
var copiedRow = templateRow.innerHTML.replace(/id=/g,"$1copy")
This will make the copied divs be prefixed with "copy". You can develop this further for the case that you have multiple copies by keeping a counter and adding that count variable to the replace() call.
When you want to make a template and use it multiple times its best to make it of DOM, in a documentFragment for example.
That way it doesn't respond to document.getElementById() calls in the "live" DOM.
I made an example here: http://jsfiddle.net/PM5544/MXHRr/
id's should be unique on the page.
PM5544...
In reality, there's no use to change the ID to something unique, even though your document may not be valid.
Browsers' selector engines treat IDs pretty much the same as class names. Thus, you may use
document.querySelector('#myCopy #idToLookFor');
to get the copy.
IDs on a page are supposed to be unique, even when you clone them from a template.
If you dynamically create content on your page, then you must change the id of your newly cloned elements to something else. If you want to access all cloned elements, but not the template, you can add a class to them, so you can refer to all elements with that class:
var clonedElement = template.cloneNode(yes); // make a deep copy
clonedElement.setAttribute("id", "somethingElse"); // change the id
clonedElement.setAttribute("class",
clonedElement.getAttribute("class") + " cloned"
);
To access all cloned elements by classname, you can use the getElementsByClassName method (available in newer browsers) or look at this answer for a more in-depth solution: How to getElementByClass instead of GetElementById with Javascript?
Alternatively, if you have jQuery available, you can do this is far less lines of code:
$("#template").clone().attr("id","somethingElse")
.addClass("cloned").appendTo("#someDiv");
The class lookup is even simpler:
$(".cloned").doSomethingWithTheseElements();
Try to avoid using IDs in the child elements of the cloned structure, as all ids of the cloned element should be changed before adding the clone to the page. Instead, you can refer to the parent element using the new id and traverse the rest of the structure using classnames. Class names do not need to be unique, so you can just leave them as they are.
If you really must use ID's (or unique "name" attributes in form fields), I can strongly suggest using a framework like jQuery or Prototype to handle the DOM traversal; otherwise, it is quite a burden to resolve all the cross-browser issues. Here is an example of some changes deeper in the structure, using jQuery:
$("#template").clone().attr("id","somethingElse")
.addClass("cloned") // add a cloned class to the top element
.find("#foo").attr("id","bar").end() // find and modify a child element
.appendTo("#someDiv"); // finally, add the node to the page
Check out my ugly but functional cheese. I wrote a function that works like getelementbyid, but you give it a start node instead of the document. Works like a charm. It may be inefficient but I have great faith in the microprocessors running today's browsers' javascript engines.
function getelement(node, findid)
{
if (node)
if (node.id)
if (node.id == findid)
return node;
node = node.firstChild;
while(node)
{
var r = getelement(node, findid);
if (r != null)
return r;
node = node.nextSibling;
}
return null;
}
When you copy the row, don't you end up having a reference to it? At that point can't you change the ID?

Why does jQuery.height() work on an id, but not on a class?

For example, I have 20 items all the same height, all the same class "some_class".
The first item of the twenty has an id of "first_item".
When I do $('.some_class').height(); I get 0 (which is wrong). But when I do $('#first_item').height(); it works and I get a value.
This baffles me. Why might this be? I can't really share the web page because its a private page.
EDIT: Interesting to note that .width() works just fine, and I get the same value calling it on #first_item or '.some_class'.
Because you'll probably find that the class selector is returning more than one element. In this case, height() returns the value of the first element (this is consistent throughout the jQuery get methods); which is probably not the one you want.
If you want the height of each of the element, you should iterate over them with each;
$('.some_class').each(function () {
var myHeight = $(this).height();
});
jQuery height docs;
Get the current computed height for the first element in the set of matched elements.
$('.some_class:eq(0)').height(); should give you the proper output.
It's a basic html/css stuff, you need to understand.
'class' defines specific type of elements. Description of the class will be defined with style associated with that class. So, class is used to provide style to multiple elements of same type. For example, 20 items mentioned by you have same class. (They can not have same id).
'id' is an unique identification of an HTML element. There can not be multiple elements having same 'id'. So, 'id' is used to provide style to one uniqe element.
Ways of writing style for class 'myClass' and id 'myId' are as below.

What information about a DOM element would allow JavaScript to identify it (somewhat) uniquely? (e.g. when it doesn't have `id`)

Here's what I'm trying to do: I have a bookmarklet that is looking for elements in the current page (which can be any site) and dispatch a click event on the ones that match. I have that part working.
In some cases though, nothing matches automatically and I want to be able to show (by hovering it) what element should be activated and then save some info about it in localStorage. The next time I'm using the bookmarklet on that page, I want to retrieve that info to identify the element in the DOM and then dispatch a click event.
The question is: what information should I save to be able to identify it? (in most cases, since it will always be possible to create a case where it doesn't work)
In the best case, said-element will have an id value and I'm good to go. In some other cases, it won't and I'd like to see your suggestions as to what info and what method I should use to get it back.
So far my idea is to save some of the element's properties and traverse the DOM to find elements that match everything. Not all properties will work (e.g. clientWidth will depend on the size of the browser) and not all types of elements will have all properties (e.g. a div node won't have a src value), which means that on one hand, I can't blindly save all properties, but on the other, I need to either choose a limited list of properties that will work for any kinds of element (at the risk of losing some useful info) or have different cases for different elements (which doesn't sound super great).
Things I was thinking I could use:
id of course
className, tagName would help, though className is likely to not be a clear match in some cases
innerHTML should work in a lot of cases if the content is text
src should work in most cases if the content is an image
the hierarchy of ancestors (but that can get messy)
...?
So, my question is a bit "how would you go about this?", not necessarily code.
Thanks!
You could do what #brendan said. You can also make up a jQuery-style selector string for each element in the DOM by figuring out the element's "index" in terms of its place in its parent's list of child nodes, and then building that up by walking up the DOM to the body tag.
What you'd end up with is something that looks like
body > :nth-child(3) > :nth-child(0) > :nth-child(4)
Of course if the DOM changes that won't work so good. You could add class names etc, but as you said yourself things like this are inherently fragile if you don't have a good "id" to start with, one that's put there at page creation time by whatever logic knows what's supposed to be in the page in the first place.
an approach would be using name, tagName and className-combination. innerHTML could may be too big.
another approach would be to look for child elements of your choosen element which have an id.
check for id => check for childs with id => check for name, tagName and className-combination (if => tell user to choose a different item :-)
What about finding all elements without an ID and assigning them a unique id. Then you could always use id.
What about using the index (integer) of the element within the DOM? You could loop through every element on page load and set a custom attribute to the index...
var els = document.getElementsByTagName("*");
for(var i = 0, l = els.length; i < l; i++) {
els[i].customIndex = i;
}

Categories