How to check if element has any children in Javascript? - javascript

Simple question, I have an element which I am grabbing via .getElementById (). How do I check if it has any children?

A couple of ways:
if (element.firstChild) {
// It has at least one
}
or the hasChildNodes() function:
if (element.hasChildNodes()) {
// It has at least one
}
or the length property of childNodes:
if (element.childNodes.length > 0) { // Or just `if (element.childNodes.length)`
// It has at least one
}
If you only want to know about child elements (as opposed to text nodes, attribute nodes, etc.) on all modern browsers (and IE8 — in fact, even IE6) you can do this: (thank you Florian!)
if (element.children.length > 0) { // Or just `if (element.children.length)`
// It has at least one element as a child
}
That relies on the children property, which wasn't defined in DOM1, DOM2, or DOM3, but which has near-universal support. (It works in IE6 and up and Chrome, Firefox, and Opera at least as far back as November 2012, when this was originally written.) If supporting older mobile devices, be sure to check for support.
If you don't need IE8 and earlier support, you can also do this:
if (element.firstElementChild) {
// It has at least one element as a child
}
That relies on firstElementChild. Like children, it wasn't defined in DOM1-3 either, but unlike children it wasn't added to IE until IE9. The same applies to childElementCount:
if (element.childElementCount !== 0) {
// It has at least one element as a child
}
If you want to stick to something defined in DOM1 (maybe you have to support really obscure browsers), you have to do more work:
var hasChildElements, child;
hasChildElements = false;
for (child = element.firstChild; child; child = child.nextSibling) {
if (child.nodeType == 1) { // 1 == Element
hasChildElements = true;
break;
}
}
All of that is part of DOM1, and nearly universally supported.
It would be easy to wrap this up in a function, e.g.:
function hasChildElement(elm) {
var child, rv;
if (elm.children) {
// Supports `children`
rv = elm.children.length !== 0;
} else {
// The hard way...
rv = false;
for (child = element.firstChild; !rv && child; child = child.nextSibling) {
if (child.nodeType == 1) { // 1 == Element
rv = true;
}
}
}
return rv;
}

As slashnick & bobince mention, hasChildNodes() will return true for whitespace (text nodes). However, I didn't want this behaviour, and this worked for me :)
element.getElementsByTagName('*').length > 0
Edit: for the same functionality, this is a better solution:
element.children.length > 0
children[] is a subset of childNodes[], containing elements only.
Compatibility

You could also do the following:
if (element.innerHTML.trim() !== '') {
// It has at least one
}
This uses the trim() method to treat empty elements which have only whitespaces (in which case hasChildNodes returns true) as being empty.
NB: The above method doesn't filter out comments. (so a comment would classify a a child)
To filter out comments as well, we could make use of the read-only Node.nodeType property where Node.COMMENT_NODE (A Comment node, such as <!-- … -->) has the constant value - 8
if (element.firstChild?.nodeType !== 8 && element.innerHTML.trim() !== '' {
// It has at least one
}
let divs = document.querySelectorAll('div');
for(element of divs) {
if (element.firstChild?.nodeType !== 8 && element.innerHTML.trim() !== '') {
console.log('has children')
} else { console.log('no children') }
}
<div><span>An element</span>
<div>some text</div>
<div> </div> <!-- whitespace -->
<div><!-- A comment --></div>
<div></div>

You can check if the element has child nodes element.hasChildNodes(). Beware in Mozilla this will return true if the is whitespace after the tag so you will need to verify the tag type.
https://developer.mozilla.org/En/DOM/Node.hasChildNodes

Try the childElementCount property:
if ( element.childElementCount !== 0 ){
alert('i have children');
} else {
alert('no kids here');
}

Late but document fragment could be a node:
function hasChild(el){
var child = el && el.firstChild;
while (child) {
if (child.nodeType === 1 || child.nodeType === 11) {
return true;
}
child = child.nextSibling;
}
return false;
}
// or
function hasChild(el){
for (var i = 0; el && el.childNodes[i]; i++) {
if (el.childNodes[i].nodeType === 1 || el.childNodes[i].nodeType === 11) {
return true;
}
}
return false;
}
See:
https://github.com/k-gun/so/blob/master/so.dom.js#L42
https://github.com/k-gun/so/blob/master/so.dom.js#L741

A reusable isEmpty( <selector> ) function.
You can also run it toward a collection of elements (see example)
const isEmpty = sel =>
![... document.querySelectorAll(sel)].some(el => el.innerHTML.trim() !== "");
console.log(
isEmpty("#one"), // false
isEmpty("#two"), // true
isEmpty(".foo"), // false
isEmpty(".bar") // true
);
<div id="one">
foo
</div>
<div id="two">
</div>
<div class="foo"></div>
<div class="foo"><p>foo</p></div>
<div class="foo"></div>
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
returns true (and exits loop) as soon one element has any kind of content beside spaces or newlines.

<script type="text/javascript">
function uwtPBSTree_NodeChecked(treeId, nodeId, bChecked)
{
//debugger;
var selectedNode = igtree_getNodeById(nodeId);
var ParentNodes = selectedNode.getChildNodes();
var length = ParentNodes.length;
if (bChecked)
{
/* if (length != 0) {
for (i = 0; i < length; i++) {
ParentNodes[i].setChecked(true);
}
}*/
}
else
{
if (length != 0)
{
for (i = 0; i < length; i++)
{
ParentNodes[i].setChecked(false);
}
}
}
}
</script>
<ignav:UltraWebTree ID="uwtPBSTree" runat="server"..........>
<ClientSideEvents NodeChecked="uwtPBSTree_NodeChecked"></ClientSideEvents>
</ignav:UltraWebTree>

Related

Elegant way of checking if one of the parentNodes has a certain class

I have a menu that expands and retracts on hover. The problem is the menu has many elements and to trigger my expand function I need to write something like below. My actual code includes more code and I was wondering if there would be a better way to do this.
var e = event.target
if(
e.parentNode.className.split(" ")[0] === "main-section" ||
e.parentNode.parentNode.className.split(" ")[0] === "main-section" ||
e.parentNode.parentNode.parentNode.className.split(" ")[0] === "main-section"){
//do somehtings}
In modern environments you can use the DOM's closest method:
if (e.closest(".main-section")) {
// One was found...
}
It looks at the current element to see if it matches the selector, then its parent element, then its parent, etc. to the root of the tree. It returns the element it finds, or null if it doesn't find one.
For slightly older environments, Element#closest can be polyfilled. Or if you don't like polyfilling, you can give yourself a utility function instead that uses closest if it exists, or uses matches if not:
function closest(el, selector) {
if (el.closest) {
return el.closest(selector);
}
var matches = el.matches || el.matchesSelector;
while (el) {
if (matches.call(el, selector)) {
return el;
}
el = el.parentNode;
}
return null;
}
...which you'd use like this:
if (closest(e, ".main-section")) {
// One was found...
}
Method closest() is not supported in some browsers, so I took this function for you from this answer
function findAncestor (el, sel) {
while ((el = el.parentElement) && !((el.matches || el.matchesSelector).call(el,sel)));
return el;
}
Use classList with a recursive function like so.
const start = document.getElementById("start");
function recursiveCheck(ele, className, limit = 3, current = 0){
return ele.classList.contains(className) ? true : current >= limit ? false : recursiveCheck(ele.parentNode, className, limit, current + 1);
}
console.log(
recursiveCheck(start, "test")
);
<div class="test">
<div>
<div id="start"><div>
</div>
</div>

Best way to extract unformatted text from DOM preserving line breaks?

Let's say I have the following element TEXT in HTML:
<div id="TEXT">
<p>First <strong>Line</strong></p>
<p>Seond <em>Line</em></p>
</div>
How should one extract the raw text from this element, without HTML tags, but preserving the line breaks?
I know about the following two options but neither of them seems to be perfect:
document.getElementById("TEXT").textContent
returns
First LineSecond Line
problem: ignores the line break that should be included between paragraphs
document.getElementById("TEXT").innerText
returns
First Line
Second Line
problem: is not part of W3C standard and is not guaranteed to work in all browsers
Here's a handy function for getting text contents of any element and it works well on all platforms, and yes, it preserves line breaks.
function text(e){
var t = "";
e = e.childNodes || e;
for(var i = 0;i<e.length;i++){
t+= e[i].nodeType !=1 ? e[i].nodeValue : text(e[i].childNodes);
}
return t;
}
You can check how jQuery does it. It uses sizzle js. Here is the function that you can use.
<div id="TEXT">
<p>First <strong>Line</strong></p>
<p>Seond <em>Line</em></p>
</div>
<script>
var getText = function( elem ) {
var node,
ret = "",
i = 0,
nodeType = elem.nodeType;
if ( !nodeType ) {
// If no nodeType, this is expected to be an array
while ( (node = elem[i++]) ) {
// Do not traverse comment nodes
ret += getText( node );
}
} else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {
// Use textContent for elements
// innerText usage removed for consistency of new lines (jQuery #11153)
if ( typeof elem.textContent === "string" ) {
return elem.textContent;
} else {
// Traverse its children
for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
ret += getText( elem );
}
}
} else if ( nodeType === 3 || nodeType === 4 ) {
return elem.nodeValue;
}
// Do not include comment or processing instruction nodes
return ret;
};
console.log(getText(document.getElementById('TEXT')));
<script>

Why Does IE11 Handle Node.normalize() Incorrectly for the Minus Symbol?

I have been experiencing an issue where DOM text nodes with certain characters behave strangely in IE when using the Node.normalize() function to concatenate adjacent text nodes.
I have created a Codepen example which allows you to reproduce the bug in IE11: http://codepen.io/anon/pen/BxoKH
Output in IE11: '- Example'
Output in Chrome and earlier versions of IE: 'Test - Example'
As you can see, this truncates everything prior to the minus symbol which is apparently treated as a delimiting character, apparently due to a bug in the native implementation of normalize() in Internet Explorer 11 (but not IE10, or IE8, or even IE6).
Can anyone explain why this happens, and does anyone know of other sequences of characters which cause this issue?
Edit - I have written a codepen that will test sections of Unicode characters to identify characters that cause this behavior. It appears to affect many more characters than I originally realized:
http://codepen.io/anon/pen/Bvgtb/
This tests Unicode characters from 32-1000 and prints those that fail the test (truncate data when nodes are normalized) You can modify it to test other ranges of characters, but be careful of increasing the range too much in IE or it will freeze.
I've created an IE bug report and Microsoft reports being able to reproduce it based on the code sample I provided. Vote on it if you're also experiencing this issue:
https://connect.microsoft.com/IE/feedback/details/832750/ie11-node-normalize-dom-implementation-truncates-data-when-adjacent-text-nodes-contain-a-minus-sign
The other answers here are somewhat verbose and incomplete — they do not walk the full DOM sub-tree. Here's a more comprehensive solution:
function normalize (node) {
if (!node) { return; }
if (node.nodeType == 3) {
while (node.nextSibling && node.nextSibling.nodeType == 3) {
node.nodeValue += node.nextSibling.nodeValue;
node.parentNode.removeChild(node.nextSibling);
}
} else {
normalize(node.firstChild);
}
normalize(node.nextSibling);
}
I created a workaround by simply reimplementing the normalize method in JS, but struggled with this for many hours, so I figured I'd make a SO post to help other folks out, and hopefully get more information to help satisfy my curiosity about this bug which wasted most of my day, haha.
Here is a codepen with my workaround which works in all browsers: http://codepen.io/anon/pen/ouFJa
My workaround was based on some useful normalize code I found here: https://stackoverflow.com/a/20440845/1504529 but has been tailored to this specific IE11 bug rather than the one discussed by that post:
Here's the workaround, which works in all browsers I've tested, including IE11
function isNormalizeBuggy(){
var testDiv = document.createElement('div');
testDiv.appendChild(document.createTextNode('0-'));
testDiv.appendChild(document.createTextNode('2'));
testDiv.normalize();
return testDiv.firstChild.length == 2;
}
function safeNormalize(DOMNode) {
// If the normalize function doesn't have the bug relating to minuses,
// we use the native normalize function. Otherwise we use our custom one.
if(!isNormalizeBuggy()){
el.normalize();
return;
}
function getNextNode(node, ancestor, isOpenTag) {
if (typeof isOpenTag === 'undefined') {
isOpenTag = true;
}
var next;
if (isOpenTag) {
next = node.firstChild;
}
next = next || node.nextSibling;
if (!next && node.parentNode && node.parentNode !== ancestor) {
return getNextNode(node.parentNode, ancestor, false);
}
return next;
}
var adjTextNodes = [], nodes, node = el;
while ((node = getNextNode(node, el))) {
if (node.nodeType === 3 && node.previousSibling && node.previousSibling.nodeType === 3) {
if (!nodes) {
nodes = [node.previousSibling];
}
nodes.push(node);
} else if (nodes) {
adjTextNodes.push(nodes);
nodes = null;
}
}
adjTextNodes.forEach(function (nodes) {
var first;
nodes.forEach(function (node, i) {
if (i > 0) {
first.nodeValue += node.nodeValue;
node.parentNode.removeChild(node);
} else {
first = node;
}
});
});
};
Not the exact answer, but helped in my case.
function safeNormalize(el) {
function recursiveNormalize(elem)
{
for (var i = 0; i < elem.childNodes.length; i++) {
if (elem.childNodes[i].nodeType != 3) {
recursiveNormalize(elem.childNodes[i]);
}
else {
if (elem.childNodes[i].nextSibling != null && elem.childNodes[i].nextSibling.nodeType == 3) {
elem.childNodes[i].nodeValue = elem.childNodes[i].nodeValue + elem.childNodes[i].nextSibling.nodeValue;
elem.removeChild(elem.childNodes[i].nextSibling);
i--;
}
}
}
}
recursiveNormalize(el);
}
The normalise code looks a little convoluted, the following is a bit simpler. It traverses the siblings of the node to be normalised, collecting the text nodes until it hits an element. Then it calls itself and collects that element's text nodes, and so on.
I think seprating the two functions makes for cleaner (and a lot less) code.
// textNode is a DOM text node
function collectTextNodes(textNode) {
// while there are text siblings, concatenate them into the first
while (textNode.nextSibling) {
var next = textNode.nextSibling;
if (next.nodeType == 3) {
textNode.nodeValue += next.nodeValue;
textNode.parentNode.removeChild(next);
// Stop if not a text node
} else {
return;
}
}
}
// element is a DOM element
function normalise(element) {
var node = element.firstChild;
// Traverse siblings, call normalise for elements and
// collectTextNodes for text nodes
while (node) {
if (node.nodeType == 1) {
normalise(node);
} else if (node.nodeType == 3) {
collectTextNodes(node);
}
node = node.nextSibling;
}
}
function mergeTextNode(elem) {
var node = elem.firstChild, text
while (node) {
var aaa = node.nextSibling
if (node.nodeType === 3) {
if (text) {
text.nodeValue += node.nodeValue
elem.removeChild(node)
} else {
text = node
}
} else {
text = null
}
node = aaa
}
}

Native javascript equivalent of jQuery :contains() selector

I am writing a UserScript that will remove elements from a page that contain a certain string.
If I understand jQuery's contains() function correctly, it seems like the correct tool for the job.
Unfortunately, since the page I'll be running the UserScript on does not use jQuery, I can't use :contains(). Any of you lovely people know what the native way to do this is?
http://codepen.io/coulbourne/pen/olerh
This should do in modern browsers:
function contains(selector, text) {
var elements = document.querySelectorAll(selector);
return [].filter.call(elements, function(element){
return RegExp(text).test(element.textContent);
});
}
Then use it like so:
contains('p', 'world'); // find "p" that contain "world"
contains('p', /^world/); // find "p" that start with "world"
contains('p', /world$/i); // find "p" that end with "world", case-insensitive
...
Super modern one-line approach with optional chaining operator
[...document.querySelectorAll('*')].filter(element => element.childNodes?.[0]?.nodeValue?.match('❤'));
And better way is to search in all child nodes
[...document.querySelectorAll("*")].filter(e => e.childNodes && [...e.childNodes].find(n => n.nodeValue?.match("❤")))
If you want to implement contains method exaclty as jQuery does, this is what you need to have
function contains(elem, text) {
return (elem.textContent || elem.innerText || getText(elem)).indexOf(text) > -1;
}
function getText(elem) {
var node,
ret = "",
i = 0,
nodeType = elem.nodeType;
if ( !nodeType ) {
// If no nodeType, this is expected to be an array
for ( ; (node = elem[i]); i++ ) {
// Do not traverse comment nodes
ret += getText( node );
}
} else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {
// Use textContent for elements
// innerText usage removed for consistency of new lines (see #11153)
if ( typeof elem.textContent === "string" ) {
return elem.textContent;
} else {
// Traverse its children
for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
ret += getText( elem );
}
}
} else if ( nodeType === 3 || nodeType === 4 ) {
return elem.nodeValue;
}
// Do not include comment or processing instruction nodes
return ret;
};
SOURCE: Sizzle.js
The original question is from 2013
Here is an even older solution, and the fastest solution because the main workload is done by the Browser Engine NOT the JavaScript Engine
The TreeWalker API has been around for ages, IE9 was the last browser to implement it... in 2011
All those 'modern' and 'super-modern' querySelectorAll("*") need to process all nodes and do string comparisons on every node.
The TreeWalker API gives you only the #text Nodes, and then you do what you want with them.
You could also use the NodeIterator API, but TreeWalker is faster
function textNodesContaining(txt, root = document.body) {
let nodes = [],
node,
tree = document.createTreeWalker(
root,
4, // NodeFilter.SHOW_TEXT
{
node: node => RegExp(txt).test(node.data)
});
while (node = tree.nextNode()) { // only return accepted nodes
nodes.push(node);
}
return nodes;
}
Usage
textNodesContaining(/Overflow/);
textNodesContaining("Overflow").map(x=>console.log(x.parentNode.nodeName,x));
// get "Overflow" IN A parent
textNodesContaining("Overflow")
.filter(x=>x.parentNode.nodeName == 'A')
.map(x=>console.log(x));
// get "Overflow" IN A ancestor
textNodesContaining("Overflow")
.filter(x=>x.parentNode.closest('A'))
.map(x=>console.log(x.parentNode.closest('A')));
This is the modern approach
function get_nodes_containing_text(selector, text) {
const elements = [...document.querySelectorAll(selector)];
return elements.filter(
(element) =>
element.childNodes[0]
&& element.childNodes[0].nodeValue
&& RegExp(text, "u").test(element.childNodes[0].nodeValue.trim())
);
}
Well, jQuery comes equipped with a DOM traversing engine that operates a lot better than the one i'm about to show you, but it will do the trick.
var items = document.getElementsByTagName("*");
for (var i = 0; i < items.length; i++) {
if (items[i].innerHTML.indexOf("word") != -1) {
// Do your magic
}
}
Wrap it in a function if you will, but i would strongly recommend to use jQuery's implementation.

How do I find if an element contains a specific class with jQuery?

I need to check if an element contains a certain child class using JQUERY.
I tried:
if ($('#myElement').has('.myClass')) {
do work son
}
Didn't work.
My html code is laid out like this:
<div id="myElement">
<img>
<span>something</span>
<span class="myClass">Hello</span>
</div>
The easiest way would be to search for .myClass as a child of #myElement:
if($('#myElement .myClass')).length > 0)
If you only want first level children, you'd use >
if($('#myElement > .myClass')).length > 0)
Another way would be passing a selector to find and checking for any results:
if($('#myElement').find('.myClass').length > 0)
Or for first level children only:
if($('#myElement').children('.myClass').length > 0)
Just use QS
var hasClass = document.getElementById("myElement").querySelector(".myClass");
or you could recurse over the children
var element = document.getElementById("myElement");
var hasClass = recursivelyWalk(element.childNodes, function hasClass(node) {
return node.classList.contains("myClass");
});
function recursivelyWalk(nodes, cb) {
for (var i = 0, len = nodes.length; i < len; i++) {
var node = nodes[i];
var ret = cb(node);
if (ret) {
return ret;
}
if (node.childNodes && node.childNodes.length) {
var ret = recursivelyWalk(node.childNodes, cb);
if (ret) {
return ret;
}
}
}
}
Using recursivelyWalk and .classList (which can be shimmed).
Alternatively you can use jQuery
$("#myElement .myClass").hasClass("myClass");
or if you want composite operations without jQuery then try NodeComposite
NodeComposite.$("#myElement *").classList.contains("myClass");
Try:
if($('#myElement').children('.myClass').length) {
// Do what you need to
}
The jQuery object returns an array, which has the .length property. The above code checks if there are any .myClass children in #myElement and, if there are (when .length isn't 0), executes the code inside the if() statement.
Here's a more explicit version:
if($('#myElement').children('.myClass').length > 0) {
// Do what you need to
}
You could always use $('#myElement .myClass').length too, but $.children() is clearer to some. To find elements that aren't direct children, use $.find() in place of $.children().
if($.contains($('#myElement'), $('.myClass'))){
alert("True");
}
else{alert("False")};

Categories