Skip to content

NodeList implementation/library - Use the Native DOM APIs as easily as jQuery

Notifications You must be signed in to change notification settings

qw3n/NodeList.js

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 

Repository files navigation

Overview

What this is:

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

NodeList.js Usage:

The HTML we'll manipulate in the following:

<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>

Let's start of by querying #container's children:

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

Getting properties of each Node:

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.

Setting properties on each node:

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';

Calling methods on each node:

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().

Looping

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';
});

Array Methods

Slice

// Returns NodeList containing first Node
$$('.child').slice(0, 1);

Map

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

// Filter out the #container div
$$('div').filter(function(div) {
	return !div.matches('#container');
});

Reduce

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()

Concat

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.

Push

let divs = $$('div');

// Pushes the document.body element, and returns the same NodeList to allow method chaining.
divs.push(document.body);

Pop

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);

Shift

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);

Unshift

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);

Splice

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);

Sort

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;
});

Reverse

// Returns the same NodeList, but reversed
$$('div').reverse();

Join

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();

Includes

// Returns true if passed Node is included in the NodeList
$$('body').includes(document.body);

Find

// Returns body element: <body>
$$('body').find(function(el) {
	return el === el;
});

FindIndex

// 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:

The asArray property

$$('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.

Special Methods

Get

// 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');

Set

// 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'
});

Call

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 Item Method

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

The owner property:

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

Adding Your Own Methods:

$$.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
	});
}

NodeListJS Compatability

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

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.

About

NodeList implementation/library - Use the Native DOM APIs as easily as jQuery

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • JavaScript 100.0%