Traversing Elements page
Learn how to move from one element to the next and a bit of meta programming.
Overview
We will learn about:
- Creating next/prev/parent/children functions
- Re-factoring methods
Video
Slides
Setup
Run the following example in CodePen:
<div id="qunit"></div>
<div id="qunit-fixture"></div>
<link rel="stylesheet" href="//code.jquery.com/qunit/qunit-1.12.0.css" />
<script src="//code.jquery.com/qunit/qunit-1.12.0.js"></script>
<script src="//bitovi.github.io/academy/static/scripts/jquery-test.js"></script>
<script type="module">
(function () {
$ = function (selector) {
if (!(this instanceof $)) {
return new $(selector);
}
var elements;
if (typeof selector === "string") {
elements = document.querySelectorAll(selector);
} else if ($.isArrayLike(selector)) {
elements = selector;
}
[].push.apply(this, elements);
};
$.extend = function (target, object) {
for (var prop in object) {
if (object.hasOwnProperty(prop)) {
target[prop] = object[prop];
}
}
return target;
};
// Static methods
$.extend($, {
isArray: function (obj) {
return Object.prototype.toString.call(obj) === "[object Array]";
},
isArrayLike: function (obj) {
return (
obj &&
typeof obj === "object" &&
(obj.length === 0 ||
(typeof obj.length === "number" &&
obj.length > 0 &&
obj.length - 1 in obj))
);
},
each: function (collection, cb) {
if ($.isArrayLike(collection)) {
for (var i = 0; i < collection.length; i++) {
if (cb.call(this, i, collection[i]) === false) {
break;
}
}
} else {
for (var prop in collection) {
if (collection.hasOwnProperty(prop)) {
if (cb.call(this, prop, collection[prop]) === false) {
break;
}
}
}
}
return collection;
},
makeArray: function (arr) {
if ($.isArray(arr)) {
return arr;
}
var array = [];
$.each(arr, function (i, item) {
array[i] = item;
});
return array;
},
proxy: function (fn, context) {
return function () {
return fn.apply(context, arguments);
};
},
});
function makeSimpleGetterSetter(prop) {
return function (value) {
if (arguments.length) {
return $.each(this, function (i, element) {
element[prop] = value;
});
} else {
return this[0][prop];
}
};
}
$.extend($.prototype, {
html: makeSimpleGetterSetter("innerHTML"),
val: makeSimpleGetterSetter("value"),
text: makeSimpleGetterSetter("textContent"),
find: function (selector) {
var elements = [];
$.each(this, function (i, element) {
var result = element.querySelectorAll(selector);
elements.push(...result);
// Or elements.push.apply(elements, result);
});
return new $(elements);
},
parent: function () {},
next: function () {},
prev: function () {},
children: function () {},
});
})();
</script>
Each exercise builds on the previous exercise. There is a completed solution at the end of this page.
Exercise: collection.parent() -> collection
The problem
collection.parent gets the parent of each element in the current set of matched elements.
<ul class="level-1">
<li class="item-i">I</li>
<li class="item-ii">
II
<ul class="level-2">
<li class="item-a">A</li>
<li class="item-b">
B
<ul class="level-3">
<li class="item-1">1</li>
<li class="item-2">2</li>
<li class="item-3">3</li>
</ul>
</li>
<li class="item-c">C</li>
</ul>
</li>
<li class="item-iii">III</li>
</ul>
<script type="module">
import "https://unpkg.com/jquery@3/dist/jquery.js";
$("li.item-a").parent().css("border", "solid 1px red");
</script>
Click to see test code
QUnit.test('$.fn.parent', function(){
var $lis = $('#qunit-fixture')
.html('<ul><li/><li/></ul><ul><li/><li/></ul>')
.find('li:first-child');
equal($lis.length, 2, 'got 2 uls');
equal($lis.parent().length, 2, 'got 2 lis');
});
What you need to know
- The parentNode property returns an elements parent node.
The solution
Click to see the solution
parent: function() {
var parents = [];
$.each(this, function(i, element){
parents.push(element.parentNode)
});
return $(parents);
},
Exercise: collection.next() -> collection
The problem
collection.next gets the immediately following sibling of each element in the set of matched elements.
<ul>
<li>list item 1</li>
<li>list item 2</li>
<li class="third-item">list item 3</li>
<li>list item 4</li>
<li>list item 5</li>
</ul>
<script type="module">
import "https://unpkg.com/jquery@3/dist/jquery.js";
$("li.third-item").next().css("border", "solid 1px red");
</script>
Click to see test code
QUnit.test('$.fn.next', function(){
var $lis = $('#qunit-fixture')
.html('<ul> <li></li> <li></li> </ul> <ul> <li></li> <li></li> </ul>')
.find('li:first-child');
equal($lis.length, 2, 'got 2 lis');
equal($lis.next().length, 2, 'got 2 lis');
equal($lis.next().next().length, 0, 'got 0 lis');
});
What you need to know
- nextElementSibling
returns the element immediately following the specified one in its parent’s children list, or
null
if the specified element is the last one in the list.
The solution
Click to see the solution
next: function() {
var nexts = [];
$.each(this, function(i, element){
if(element.nextElementSibling) {
nexts.push( element.nextElementSibling );
}
});
return $(nexts);
},
Exercise: collection.prev() -> collection
The problem
collection.prev get the immediately preceding sibling of each element in the set of matched elements.
Click to see test code
QUnit.test('$.fn.prev', function(){
var $lis = $('#qunit-fixture')
.html('<ul><li/><li/></ul><ul><li/><li/></ul>')
.find('li:last-child');
equal($lis.length, 2, 'got 2 uls');
equal($lis.prev().length, 2, 'got 2 lis');
equal($lis.prev().prev().length, 0, 'got 2 lis');
});
What you need to know
- previousElementSibling
returns the Element immediately prior to the specified one in its parent’s children list, or
null
if the specified element is the first one in the list.
The solution
Click to see the solution
prev: function() {
var prevs = [];
$.each(this, function(i, element){
if(element.previousElementSibling) {
prevs.push( element.previousElementSibling );
}
});
return $(prevs);
},
Exercise: collection.children() -> collection
The problem
collection.children get the children of each element in the set of matched elements.
<ul class="level-1">
<li class="item-i">I</li>
<li class="item-ii">
II
<ul class="level-2">
<li class="item-a">A</li>
<li class="item-b">
B
<ul class="level-3">
<li class="item-1">1</li>
<li class="item-2">2</li>
<li class="item-3">3</li>
</ul>
</li>
<li class="item-c">C</li>
</ul>
</li>
<li class="item-iii">III</li>
</ul>
<script type="module">
import "https://unpkg.com/jquery@3/dist/jquery.js";
$("ul.level-2").children().css("border", "solid 1px red");
</script>
Click to see test code
QUnit.test('$.fn.children', function(){
var $ul = $('#qunit-fixture')
.html('<ul><li/><li/></ul><ul><li/><li/></ul>')
.children();
equal($ul.length, 2, 'got 2 uls');
equal($ul.children().length, 4, 'got four lis');
});
What you need to know
- children returns the child elements of the node upon which it was called.
The solution
Click to see the solution
children: function() {
var children = [];
$.each(this, function(i, element){
children.push(...element.children );
});
return $(children);
},
Exercise: eliminate duplicate code in .find
, .parent
, .next
, .prev
, and .children
.
The problem
Use meta programming techniques to reduce the duplicate code in the .find
, .parent
, .next
, .prev
, and .children
functions.
find: function(selector) {
var elements = [];
$.each(this, function(i, element){
var result = element.querySelectorAll(selector);
elements.push(...result);
// Or elements.push.apply(elements, result);
})
return new $(elements);
},
parent: function() {
var parents = [];
$.each(this, function(i, element){
parents.push(element.parentNode)
});
return $(parents);
},
next: function() {
var nexts = [];
$.each(this, function(i, element){
if(element.nextElementSibling) {
nexts.push( element.nextElementSibling );
}
});
return $(nexts);
},
prev: function() {
var prevs = [];
$.each(this, function(i, element){
if(element.previousElementSibling) {
prevs.push( element.previousElementSibling );
}
});
return $(prevs);
},
children: function() {
var children = [];
$.each(this, function(i, element){
children.push(...element.children );
});
return $(children);
},
Make a makeTraverser(traverse)
function that takes a traverse
function and
produces a function.
The traverse
function should be called for each element in
the source collection. When calling traverse
, its this
should be each element.
The traverse
functions should be able to return null
and element
or an array-like
object of elements
.
What you need to know
You know everything you need to know. You can do it!
The solution
Click to see the solution
function makeTraverser(traverser) {
return function () {
var elements = [],
args = arguments;
$.each(this, function (i, element) {
var els = traverser.apply(element, args);
if ($.isArrayLike(els)) {
elements.push.apply(elements, els);
} else if (els) {
elements.push(els);
}
});
return $(elements);
};
}
find: makeTraverser(function(selector) {
return this.querySelectorAll(selector);
}),
parent: makeTraverser(function() {
return this.parentNode;
}),
next: makeTraverser(function() {
return this.nextElementSibling;
}),
prev: makeTraverser(function() {
return this.previousElementSibling;
}),
children: makeTraverser(function() {
return this.children;
})
Completed Solution
Click to see completed solution
<div id="qunit"></div>
<div id="qunit-fixture"></div>
<link rel="stylesheet" href="//code.jquery.com/qunit/qunit-1.12.0.css">
<script src="//code.jquery.com/qunit/qunit-1.12.0.js"></script>
<script src="//bitovi.github.io/academy/static/scripts/jquery-test.js"></script>
<script type="module">
(function() {
$ = function(selector) {
if ( !(this instanceof $) ) {
return new $(selector);
}
var elements;
if (typeof selector === "string") {
elements = document.querySelectorAll(selector);
} else if ($.isArrayLike(selector)) {
elements = selector;
}
[].push.apply(this, elements);
};
$.extend = function(target, object) {
for (var prop in object) {
if (object.hasOwnProperty(prop)) {
target[prop] = object[prop];
}
}
return target;
};
// Static methods
$.extend($, {
isArray: function(obj) {
return Object.prototype.toString.call(obj) === "[object Array]";
},
isArrayLike: function(obj) {
return obj &&
typeof obj === "object" &&
( obj.length === 0 ||
typeof obj.length === "number" &&
obj.length > 0 &&
obj.length - 1 in obj );
},
each: function(collection, cb) {
if ($.isArrayLike(collection)) {
for (var i = 0; i < collection.length; i++) {
if (cb.call(this, i, collection[i]) === false) {
break;
}
}
} else {
for (var prop in collection) {
if (collection.hasOwnProperty(prop)) {
if (cb.call(this, prop, collection[prop]) === false) {
break;
}
}
}
}
return collection;
},
makeArray: function(arr) {
if ($.isArray(arr)) {
return arr;
}
var array = [];
$.each(arr, function(i, item) {
array[i] = item;
});
return array;
},
proxy: function(fn, context) {
return function() {
return fn.apply(context, arguments);
};
}
});
function makeTraverser(traverser) {
return function() {
var elements = [], args = arguments;
$.each(this, function(i, element) {
var els = traverser.apply(element, args);
if ($.isArrayLike(els)) {
elements.push.apply(elements, els);
} else if (els) {
elements.push(els);
}
});
return $(elements);
};
}
$.extend($.prototype, {
html: function(newHtml) {
if(arguments.length) {
return $.each(this, function(i, element) {
element.innerHTML = newHtml;
});
} else {
return this[0].innerHTML;
}
},
val: function(newVal) {
if(arguments.length) {
return $.each(this, function(i, element) {
element.value = newVal;
});
} else {
return this[0].value;
}
},
text: function(newText) {
if (arguments.length) {
return $.each(this, function(i, element) {
element.textContent = newText;
});
} else {
return this[0].textContent;
}
},
find: makeTraverser(function(selector) {
return this.querySelectorAll(selector);
}),
parent: makeTraverser(function() {
return this.parentNode;
}),
next: makeTraverser(function() {
return this.nextElementSibling;
}),
prev: makeTraverser(function() {
return this.previousElementSibling;
}),
children: makeTraverser(function() {
return this.children;
})
});
})();
</script>