NodeList.js
makes using the Native DOM APIs on an Array
of Nodes
as easy as jQuery
with the benefits of it being extremely small at around 4k minified, a HUGE performance boost and the browser as a dependency (That's the most interesting part).
The first thing you'll notice is I'm using $$
, the reason I chose this for selecting DOM Nodes
is because if you open up your devtools and type in the following:
$$('div'); // Will return a NodeList
<body>
<div id="container" class="cont">
<div class="child"></div>
<div class="child"></div>
<div class="child"></div>
<div class="child"></div>
<div class="child"></div>
<div class="child"></div>
<div class="child"></div>
<div class="child"></div>
<div class="child"></div>
<div class="child"></div>
</div>
</body>
Each of the following returns an Array of Nodes
(AKA my NodeList
, not the browser's native NodeList
)
// Method 1
$$('#container div');
// Method 2
$$('#container').children;
// Method 3
$$('div div');
If you pass a query string there's a second argument you can pass as the scope:
let container = document.getElementById('container');
$$('div', container);
Which would be equivalent to:
// Just this doesn't return my NodeList, but the browser's NodeList
container.querySelectorAll('div');
You can pass nodes as arguments:
$$(document, document.body); // returns NodeList
You can also pass 1 Array
of Nodes or a NodeList
or an HTMLCollection
Will not be flattened, to flatten use concat()
:
$$([document, document.body]); // returns NodeList
How you would normally do it:
let children = document.getElementsByClassName('child');
Now you would get properties on the #container
's children:
for(let i = 0, l = children.length; i < l; i++) {
children[i].id; // ''
children[i].nodeName; // 'DIV'
children[i].className; // 'child'
}
Here's how you would do it with nodeList.js
:
$$('.child').id; // ['', '' ... x10]
$$('.child').nodeName; // ['DIV', 'DIV' ... x10]
$$('.child').className; // ['child', 'child' ... x10]
Therefore you would read each property just like you would with a single Node
:)
Notice how it returns an Array
of the property's value, meaning you can select them by index
and use any Array Methods
on them, you'll see when you get to the looping part.
Let's continue using the children
variable, so this is how you would set properties on the children
:
for(let i = 0, l = children.length; i < l; i++) {
children[i].className = 'containerChild';
children[i].textContent = 'This is some text';
}
Here's how you'd do it with NodeList.js
:
$$('.child').className = 'containerChild';
$$('.child').textContent = 'This is some text';
Still using the children
variable:
Let's add an event listener to each node, even though event delegation
would be best, but for the sake of this example:
for(let i = 0, l = children.length; i < l; i++) {
children[i].addEventListener('click', function() {
console.log(this, 'was clicked');
});
}
Here's how you'd do it with NodeList.js
:
$$('.child').addEventListener('click', function() {
console.log(this, 'was clicked');
});
So cool right? You can use any Native DOM method
:
Let's set some attributes:
$$('.child').setAttribute('class', 'child div');
// For setting the class you could just do:
$$('.child').className = 'child div';
Clicking the elements:
$$('.child').click();
Removing the elements:
$$('.child').remove();
I think you're getting the point: any Native DOM Method
that every Node/Element
inherits you could just call on the NodeList
and it'll be called on each element.
BTW: All DOM
Methods that would normally return undefined
when called on a single Node
will return the same NodeList
back to allow Method Chaining. Like setAttribute()
.
Using a for loop and ES6
for-of
:
We'll just remove the nodes from the DOM
as examples:
let nodes = $$('.child');
for(let i = 0, l = nodes.length; i < l; i++) {
nodes[i].remove();
}
for(let node of nodes) {
node.remove();
}
Using forEach
:
// Removes all Nodes and returns same the NodeList to allow method chaining
$$('.child').forEach(function(node) {
node.remove();
});
// But Just do:
$$('.child').remove();
Looping through the properties:
// Returns Array of style objects (CSSStyleDeclaration)
let styles = $$('.child').style;
for(let i = 0, l = styles.length; i < l; i++) {
styles[i].color = 'red';
}
for(let style of styles) {
style.color = 'red';
}
styles.forEach(function(style) {
style.color = 'red';
});
// OR loop through the nodes themselves
let nodes = $$('.child');
for(let i = 0, l = nodes.length; i < l; i++) {
nodes[i].style.color = 'red';
}
for(let node of nodes) {
node.style.color = 'red';
}
nodes.forEach(function(node) {
node.style.color = 'red';
});
// Returns NodeList containing first Node
$$('.child').slice(0, 1);
Mapping is easy just get the property just like you would on a single Node
// Returns an Array of the id of each Node in the NodeList
$$('#container').id;
// No need for
$$('#container').map(function(element) {
return element.id;
});
// Map() Checks if Array is fully populated with nodes so returns a NodeList populated with firstChld nodes
$$('#container div').map(function(div) {
return div.firstChild;
});
// Maps the firstChild node and removes it, and returns the NodeList of firstChild Nodes
$$('#container').map(function(div) {
return div.firstChild;
}).remove();
// Or:
$$('#container').firstChild.remove();
// Filter out the #container div
$$('div').filter(function(div) {
return !div.matches('#container');
});
I couldn't think of a better example for using Reduce on a NodeList (but it's possible)
let unique = $$('div').reduce(function(set, div) {
set.add(div.parentElement);
return set;
}, new Set());
There's also reduceRight()
The following concat()
methods all return a new concatenated NodeList
(Not affecting the NodeList
that concat()
is being called on)
let divs = $$('div');
// Method 1 passing a Node
let divsAndBody = divs.concat(document.body);
// Method 2 passing an Array of Nodes
let divsAndBody = divs.concat([document.body]);
// Method 3 passing a NodeList
let divsAndBody = divs.concat($$('body'));
// Method 4 passing an Array of NodeList
let divsAndBody = divs.concat([$$('body')]);
// Method 5 passing multiple Nodes as arguments
let divsAndBodyAndHTML = divs.concat(document.body, document.documentHTML);
// Method 6 passing multiple Arrays of Nodes as arguments
let divsAndBodyAndHTML = divs.concat([document.body], [document.documentHTML]);
// Method 7 passing multiple Arrays of NodeList as are arguments
let divsAndBodyAndHTML = divs.concat([$$('body')], [$$('html')]);
Concat()
is recursive so you can pass an Array
that is as deep as you'd like.
Now if you pass anythinng that's not a Node
, NodeList
, HTMLCollections
, Array
or deep Array of Arrays
that contain something other than a Node
, NodeList
, HTMLCollections
, Array
will Throw an Error
.
let divs = $$('div');
// Pushes the document.body element, and returns the same NodeList to allow method chaining.
divs.push(document.body);
let divs = $$('div');
// Removes last Node in the NodeList and returns a NodeList of the removed Nodes
divs.pop();
pop()
takes an optional argument of how many Nodes
to POP
// Removes last 2 Nodes in the NodeList and returns a NodeList of the removed Nodes
divs.pop(2);
let divs = $$('div');
// Removes first Node in the NodeList and returns a NodeList of the removed Nodes
divs.shift();
shift()
also takes an optional argument of how many Nodes
to SHIFT
// Removes first 2 Nodes in the NodeList and returns a NodeList of the removed Nodes
divs.shift(2);
let divs = $$('div');
// Inserts/unshifts the document.body into the beginning of the NodeList and returns the same NodeList to allow method chaining.
divs.unshift(document.body);
Let's replace the first element which would be #container with document.body
let divs = $$('div');
// Removes the first Element, inserts document.body in its place and returns a NodeList of the spliced Nodes
divs.splice(0, 1, document.body);
let divs = $$('.child');
// Gives each div a data-index attribute
divs.forEach(function(div, index) {
div.dataset.index = index;
});
// Reverse the NodeList and returns the same NodeList
divs.sort(function(div1, div2) {
return div2.dataset.index - div1.dataset.index;
});
// Returns the same NodeList, but reversed
$$('div').reverse();
I didn't put a join
method for NodeLists
because it'd be useless on the actual Nodes:
// Returns "[object HTMLDivElement], [object HTMLDivElement] ..."
$$('.child').join();
Therefore you can still use it when mapping out properties:
// Returns "child,child,child,child,child,child,child,child,child,child"
$$('.child').className.join();
// Returns true if passed Node is included in the NodeList
$$('body').includes(document.body);
// Returns body element: <body>
$$('body').find(function(el) {
return el === el;
});
// Returns 0
$$('body').findIndex(function(el) {
return el === el;
});
There may be DOM
methods that are the same name as the ones of Array.prototype
in the future, or you may just want to convert the NodeList
to an Array
therefore you can use as a native Array
:
$$('body').asArray; // returns Array
$$('body').asArray.forEach(function() {...}); // uses native Array method therefore you cannot chain
Ok now how about dealing with elements that have unique properties. Like HTMLAnchorElement(s)
they have the href
property which is not inherited from HTMLElement
. There are no HTMLAnchorElements
in this example but here's how you'll deal with it.
// Returns undefined because it's a unique property that every element does not inherit
$$('a').href
// Returns an Array of href values
$$('a').get('href');
Get()
can also be used on an Array
of properties:
// Returns an Array of the value of each node.style.color
$$('.child').style.get('color');
// Sets the href property of each Node in NodeList
$$('a').set('href', 'https://www.example.com/');
set()
will only set the properties on the Nodes
who's properties are not undefined:
$$('div, a').set('href', 'https://www.example.com/');
href
will only be set on the <a>
elements and not the <div>
s
set()
can also be used on an Array
of properties:
// Sets each element's color to red and returns the Array of styles back
$$('.child').style.set('color', 'red');
You can also set multiple properties:
$$('.child').set({
textContent: 'Hello World',
className: 'class1 class2'
});
Same with mapped properties:
$$('.child').style.set({
color: 'red',
background: 'black'
});
Remember you can chain:
$$('.child').set({
textContent: 'Hello World',
className: 'class1 class2'
}).style.set({
color: 'red',
background: 'black'
});
There are methods which are unique to certain elements. This is how you would call those methods:
$$('video').call('pause');
Or you could just loop through the elements and call the methods
What about passing arguments:
// Returns Array of `CanvasRenderingContext2D`
$$('canvas').call('getContext', '2d');
If the method called on any of the elements returns something, an Array
of those returned items would be returned from call()
otherwise the NodeList
will be returned to allow method chaining.
The browser's native item(index)
method does the same as NodeList[index]
but in mine it returns that Node
as a my NodeList
(If you know jQuery
it's the same as jQuery's eq()
method)
// returns the <html> element
$$('html, body')[0];
// returns my NodeList [<html>]
$$('html, body').item(0);
This is so that you can keep using the same properties/methods of my NodeList, instead of having to slice
out the one Node
All the owner propety does is give you back the NodeList
that the property was mapped from:
var elms = $$('.child');
elms.style.owner === elms; // true
So I can do all kinds of stuff:
Remember mapping style
returns an Array
of CSSStyleDeclarations
$$('.child').style;
This will give you back the NodeList
which style
was mapped from:
var childs = $$('.child');
childs.style.owner === childs; // true
If you know jQuery
its the same as its prevObj
property
$$.NL.myMethod = function() {
// You'll have to write your own loop here if you want to call this on each Node or use:
this.forEach(function(node) {
// do something with each node
});
}
Browser | Version |
---|---|
FireFox | 6+ |
Safari | 5.0.5+ |
Chrome | 6+ |
IE | 9+ |
Opera | 11+ |
Attention: You have to realize that my library's dependent on the browser it's running (which is awesome, so it automatically updates when the browser updates the DOM
with new properties/methods) meaning: let's say the property hidden
doesn't exist in the browser's DOM
API you can't do: $$('.child').hidden = true;
My wish would be to have all modern browsers implement NodeList
like this. I honestly don't see why this would hurt if it grows with help/feedback. As of right now NodeList
doesn't do anything but store Live Nodes
. And this is using the DOM's Native APIs so there's nothing new, now the set
and get
methods won't be needed since it would be done with ES6 Proxies
.
Getting NodeList
natively implemented like NodeList.js
I would need tons of help. I don't know anything about writing specs and getting things standardized.