I am trying to find the closest element with a specific tag name without jquery. When I click on a <th> I want to get access to the <tbody> for that table. Suggestions? I read about offset but didn't really understand it too much. Should I just use:
Assume th is already set to clicked th element
th.offsetParent.getElementsByTagName('tbody')[0]
Very simple:
el.closest('tbody')
Supported on all browsers except IE.
UPDATE: Edge now support it as well.
No need for jQuery.
More over, replacing jQuery's $(this).closest('tbody') with $(this.closest('tbody')) will increase performance, significantly when the element is not found.
Polyfill for IE:
if (!Element.prototype.matches) Element.prototype.matches = Element.prototype.msMatchesSelector;
if (!Element.prototype.closest) Element.prototype.closest = function (selector) {
var el = this;
while (el) {
if (el.matches(selector)) {
return el;
}
el = el.parentElement;
}
};
Note that there's no return when the element was not found, effectively returning undefined when the closest element was not found.
For more details see:
https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
Little (very) late to the party, but nonetheless. This should do the trick:
function closest(el, selector) {
var matchesFn;
// find vendor prefix
['matches','webkitMatchesSelector','mozMatchesSelector','msMatchesSelector','oMatchesSelector'].some(function(fn) {
if (typeof document.body[fn] == 'function') {
matchesFn = fn;
return true;
}
return false;
})
var parent;
// traverse parents
while (el) {
parent = el.parentElement;
if (parent && parent[matchesFn](selector)) {
return parent;
}
el = parent;
}
return null;
}
Here's how you get the closest element by tag name without jQuery:
function getClosest(el, tag) {
// this is necessary since nodeName is always in upper case
tag = tag.toUpperCase();
do {
if (el.nodeName === tag) {
// tag name is found! let's return it. :)
return el;
}
} while (el = el.parentNode);
// not found :(
return null;
}
getClosest(th, 'tbody');
There exists a standardised function to do this: Element.closest.
Most browsers except IE11 support it (details by caniuse.com). The MDN docs also include a polyfill in case you have to target older browsers.
To find the closest tbody parent given a th you could do:
th.closest('tbody');
In case you want to write the function yourself - here is what I came up with:
function findClosestParent (startElement, fn) {
var parent = startElement.parentElement;
if (!parent) return undefined;
return fn(parent) ? parent : findClosestParent(parent, fn);
}
To find the closest parent by tag name you could use it like this:
findClosestParent(x, element => return element.tagName === "SECTION");
function closest(el, sel) {
if (el != null)
return el.matches(sel) ? el
: (el.querySelector(sel)
|| closest(el.parentNode, sel));
}
This solution uses some of the more recent features of the HTML 5 spec, and using this on older/incompatible browsers (read: Internet Explorer) will require a polyfill.
Element.prototype.matches = (Element.prototype.matches || Element.prototype.mozMatchesSelector
|| Element.prototype.msMatchesSelector || Element.prototype.oMatchesSelector
|| Element.prototype.webkitMatchesSelector || Element.prototype.webkitMatchesSelector);
Here's the simple function I am using:-
function closest(el, selector) {
var matches = el.webkitMatchesSelector ? 'webkitMatchesSelector' : (el.msMatchesSelector ? 'msMatchesSelector' : 'matches');
while (el.parentElement) {
if (el[matches](selector)) return el;
el = el.parentElement;
}
return null;
}
To extend #SalmanPK answer
it will allow to use node as selector, useful when you working with events like mouseover.
function closest(el, selector) {
if (typeof selector === 'string') {
matches = el.webkitMatchesSelector ? 'webkitMatchesSelector' : (el.msMatchesSelector ? 'msMatchesSelector' : 'matches');
while (el.parentElement) {
if (el[matches](selector)) {
return el
};
el = el.parentElement;
}
} else {
while (el.parentElement) {
if (el === selector) {
return el
};
el = el.parentElement;
}
}
return null;
}
Summary:
For finding a particular ancestor we can use:
Element.closest();
This function takes a CSS selector string as an argument. it then returns the closest ancestor of the current element (or the element itself) which matches the CSS selector which was passed in the arguments. If there is no ancestor it will return null.
Example:
const child = document.querySelector('.child');
// select the child
console.dir(child.closest('.parent').className);
// check if there is any ancestor called parent
<div class="parent">
<div></div>
<div>
<div></div>
<div class="child"></div>
</div>
</div>
Get closest DOM element up the tree that contains a class, ID, data attribute, or tag. Includes the element itself. Supported back to IE6.
var getClosest = function (elem, selector) {
var firstChar = selector.charAt(0);
// Get closest match
for ( ; elem && elem !== document; elem = elem.parentNode ) {
// If selector is a class
if ( firstChar === '.' ) {
if ( elem.classList.contains( selector.substr(1) ) ) {
return elem;
}
}
// If selector is an ID
if ( firstChar === '#' ) {
if ( elem.id === selector.substr(1) ) {
return elem;
}
}
// If selector is a data attribute
if ( firstChar === '[' ) {
if ( elem.hasAttribute( selector.substr(1, selector.length - 2) ) ) {
return elem;
}
}
// If selector is a tag
if ( elem.tagName.toLowerCase() === selector ) {
return elem;
}
}
return false;
};
var elem = document.querySelector('#some-element');
var closest = getClosest(elem, '.some-class');
var closestLink = getClosest(elem, 'a');
var closestExcludingElement = getClosest(elem.parentNode, '.some-class');
Find nearest Elements childNodes.
closest:function(el, selector,userMatchFn) {
var matchesFn;
// find vendor prefix
['matches','webkitMatchesSelector','mozMatchesSelector','msMatchesSelector','oMatchesSelector'].some(function(fn) {
if (typeof document.body[fn] == 'function') {
matchesFn = fn;
return true;
}
return false;
});
function findInChilds(el){
if(!el) return false;
if(el && el[matchesFn] && el[matchesFn](selector)
&& userMatchFn(el) ) return [el];
var resultAsArr=[];
if(el.childNodes && el.childNodes.length){
for(var i=0;i< el.childNodes.length;i++)
{
var child=el.childNodes[i];
var resultForChild=findInChilds(child);
if(resultForChild instanceof Array){
for(var j=0;j<resultForChild.length;j++)
{
resultAsArr.push(resultForChild[j]);
}
}
}
}
return resultAsArr.length?resultAsArr: false;
}
var parent;
if(!userMatchFn || arguments.length==2) userMatchFn=function(){return true;}
while (el) {
parent = el.parentElement;
result=findInChilds(parent);
if (result) return result;
el = parent;
}
return null;
}
Here.
function findNearest(el, tag) {
while( el && el.tagName && el.tagName !== tag.toUpperCase()) {
el = el.nextSibling;
} return el;
}
Only finds siblings further down the tree. Use previousSibling to go the other way
Or use variables to traverse both ways and return whichever is found first.
You get the general idea, but if you want to traverse through parentNodes or children if a sibling doesn't match you may as-well use jQuery. At that point it's easily worth it.
A little late to the party, but as I was passing by and just answer back a very similar question, I drop here my solution - we can say it's the JQuery closest() approach, but in plain good ol' JavaScript.
It doesn't need any pollyfills and it's older browsers, and IE (:-) ) friendly:
https://stackoverflow.com/a/48726873/2816279
I think The easiest code to catch with jquery closest:
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script>
$(document).ready(function () {
$(".add").on("click", function () {
var v = $(this).closest(".division").find("input[name='roll']").val();
alert(v);
});
});
</script>
<?php
for ($i = 1; $i <= 5; $i++) {
echo'<div class = "division">'
. '<form method="POST" action="">'
. '<p><input type="number" name="roll" placeholder="Enter Roll"></p>'
. '<p><input type="button" class="add" name = "submit" value = "Click"></p>'
. '</form></div>';
}
?>
Thanks much.
Related
I am writing a micro-library instead of using jQuery. I need only 3-4 methods ( for DOM traversal, Adding Eventlisteners etc). So I decided to write them myself instead of bloating the site with jQuery.
Here is the snippet from the code:
lib.js
window.twentyFourJS = (function() {
let elements;
const Constructor = function(selector) {
elements = document.querySelectorAll(selector);
this.elements = elements;
};
Constructor.prototype.addClass = function(className) {
elements.forEach( item => item.classList.add(className));
return this;
};
Constructor.prototype.on = function(event, callback, useCapture = false){
elements.forEach((element) => {
element.addEventListener(event, callback, useCapture);
});
return this;
}
const initFunction = function(selector){
return new Constructor(selector);
}
return initFunction;
})(twentyFourJS);
script.js
(function($){
$('.tab-menu li a').on('click', function(event) {
console.log('I am clicked'); // This works
this.addClass('MyClass'); // This does NOT work (as expected)
// I want to be able to do this
$(this).addClass('MyClass');
event.preventDefault();
});
})(twentyFourJS);
Basically I want to be able to use $(this) like we use it in jQuery.
this.addClass('MyClass') and $(this).addClass('MyClass') won't work and this is the expected behaviour.
As per my understanding this is referring to the plain HTML element. So it does not have access to any Constructor methods. it won't work.
And I have not written any code that will wrap element in Constructor object in $(this). I will have to do some changes to my Constructor so that I can access the Constructor functions using $(this). What are those changes/addition to it?
Kindly recommend only Vanilla JS ways instead of libraries.
in your constructor you need to see what you have and handle it in different ways.
const Constructor = function(selector) {
if (typeof selector === 'string') {
elements = document.querySelectorAll(selector);
} else {
// need some sort of check to see if collection or single element
// This could be improved since it could fail when someone would add a length property/attribute
elements = selector.length ? selector : [selector];
}
this.elements = elements;
};
All you really need to do is make sure your Constructor argument can distinguish between a string selector being passed in, and an object.
const Constructor = function(selector) {
if(typeof selector == "string"){
elements = document.querySelectorAll(selector);
this.elements = elements;
}
else{
this.elements = selector;
}
};
You can go further than this, but at a very minimum for the example given that works.
Live example below:
window.twentyFourJS = (function() {
let elements;
const Constructor = function(selector) {
if(typeof selector == "string"){
elements = document.querySelectorAll(selector);
this.elements = elements;
}
else{
this.elements = selector;
}
};
Constructor.prototype.addClass = function(className) {
elements.forEach( item => item.classList.add(className));
return this;
};
Constructor.prototype.on = function(event, callback, useCapture = false){
elements.forEach((element) => {
element.addEventListener(event, callback, useCapture);
});
return this;
}
const initFunction = function(selector){
return new Constructor(selector);
}
return initFunction;
})();
(function($){
$('.btn').on('click', function(event) {
console.log('I am clicked'); // This works
// I want to be able to do this
$(this).addClass('MyClass');
event.preventDefault();
});
})(twentyFourJS);
.MyClass{
background-color:red
}
<button class="btn">Click me</btn>
first You Need to Check for a string
case 1. $("div")
Then You need to Check for it's NodeType and for a window
case 1. var elm = document .getElementById("ID")
$(elm)
case 2. $(this) -- window
function $(selector){
var element;
if (typeof selector === 'string') {
element = document.querySelectorAll(selector)
}
if (element.nodeType || element=== window) element= [selector];
return element ;
}
So I'm building a small custom JS library like jQuery but I ran into an wall.
I have build a event delegation function which I will be using for a simple click event.
Within this prototype part I will run some logic(in this example a addClass) but it does not work with the this keyword. As I need to add for example a class to the the clicked element.
Constructor.prototype.on = function (eventName , elementSelector, callback) {
document.addEventListener(eventName, function(e) {
for (var target = e.target; target && target != this; target = target.parentNode) {
if (target.matches(elementSelector)) {
callback.call(target, e);
break;
}
}
}, false);
};
// X is the plugin
X('body').on('click','.someClass',function(){
X(this).addClass('clicked');// not going to work
});
You need to change your Constructor function so that it allows you to provide a DOM element, not just a selector, as a parameter. Then you can use X(this) to create an instance of your class that contains this element.
var Constructor = function(selector) {
if (!selector) return;
if (typeof selector != string) {
this.nodes = [selector];
} else if (selector === 'document') {
this.nodes = [document];
} else if (selector === 'window') {
this.nodes = [window];
} else {
this.nodes = document.querySelectorAll(selector);
}
this.length = this.nodes.length;
};
I spent some time searching but have only seen too many regular "walk the DOM" blogs or answers that only go one level UP with getRootnode()
Pseudo code:
HTML
<element-x>
//# shadow-root
<element-y>
<element-z>
//# shadow-root
let container = this.closest('element-x');
</element-z>
</element-y>
</element-x>
The standard element.closest() function does not pierce shadow boundaries;
So this.closest('element-x') returns null because there is no <element-x> within <element-z> shadowDom
Goal:
Find <element-x> from inside descendant <element z> (any nested level)
Required:
A (recursive) .closest() function that walks up the (shadow) DOMs and finds <element-x>
Note: elements may or may not have ShadowDOM (see <element y>: only lightDOM)
I can and will do it myself tomorrow; just wondered if some bright mind had already done it.
Resources:
https://developer.mozilla.org/en-US/docs/Web/API/Node/getRootNode
https://developer.mozilla.org/en-US/docs/Web/API/ShadowRoot/host
Update
This is the UNminified code from the answer below:
closestElement(selector, base = this) {
function __closestFrom(el) {
if (!el || el === document || el === window) return null;
let found = el.closest(selector);
if (found)
return found;
else
__closestFrom(el.getRootNode().host);
}
return __closestFrom(base);
}
Update #2
I changed it to a method on my BaseElement:
closestElement(selector, el = this) {
return (
(el && el != document && el != window && el.closest(selector)) ||
this.closestElement(selector, el.getRootNode().host)
);
}
Events
As Intervalia comments; yes Events are another solution.
But then... an Event needs to be attached to an ancestor... How to know which ancestor to use?
This does the same as .closest() from inside any child (shadow)DOM
but walking up the DOM crossing shadowroot Boundaries
Optimized for (extreme) minification
//declared as method on a Custom Element:
closestElement(
selector, // selector like in .closest()
base = this, // extra functionality to skip a parent
__Closest = (el, found = el && el.closest(selector)) =>
!el || el === document || el === window
? null // standard .closest() returns null for non-found selectors also
: found
? found // found a selector INside this element
: __Closest(el.getRootNode().host) // recursion!! break out to parent DOM
) {
return __Closest(base);
}
Note: the __Closest function is declared as 'parameter' to avoid an extra let declaration... better for minification, and keeps your IDE from complaining
Called from inside a Custom Element:
<element-x>
//# shadow-root
<element-y>
<element-z>
//# shadow-root
let container = this.closestElement('element-x');
</element-z>
</element-y>
</element-x>
Excellent examples! Wanted to contribute a TypeScript version that has a minor difference -- it follows assignedSlot while traversing up the shadow roots, so you can find the closest matching element in a chain of nested, slotted custom elements. It's not the fanciest way to write the TypeScript, but it gets the job done.
closestElement(selector: string, base: Element = this) {
function __closestFrom(el: Element | Window | Document): Element {
if (!el || el === document || el === window) return null;
if ((el as Slotable).assignedSlot) el = (el as Slotable).assignedSlot;
let found = (el as Element).closest(selector);
return found
? found
: __closestFrom(((el as Element).getRootNode() as ShadowRoot).host);
}
return __closestFrom(base);
}
The equvalent in JS is:
closestElement(selector, base = this) {
function __closestFrom(el) {
if (!el || el === document || el === window)
return null;
if (el.assignedSlot)
el = el.assignedSlot;
let found = el.closest(selector);
return found
? found
: __closestFrom(el.getRootNode().host);
}
return __closestFrom(base);
}
Something like this should do the trick
function closestPassShadow(node, selector) {
if (!node) {
return null;
}
if (node instanceof ShadowRoot) {
return this.closestPassShadow(node.host, selector);
}
if (node instanceof HTMLElement) {
if (node.matches(selector)) {
return node;
} else {
return this.closestPassShadow(node.parentNode, selector);
}
}
return this.closestPassShadow(node.parentNode, selector);
}
just a to endolge legibility / code style. this should be typescript friendly as well.
const closestElement = (selector, target) => {
const found = target.closest(selector);
if (found) {
return found;
}
const root = target.getRootNode();
if (root === document || !(root instanceof ShadowRoot)) {
return null;
}
return closestElement(selector, root.host);
};
I'm trying to select a the parent form element of an input. The form element isn't necessarily the direct parent node. Currently this outputs "undefined" to my log.
var anInputElement = document.querySelector(...);
var formElement = getFormElement(anInputElement);
console.log(formElement);
function getFormElement(elem) {
//if we've traversed as high as the `body` node then
//we aint finding the `form` node
if(elem.nodeName.toLowerCase() !== 'body') {
var parent = elem.parentNode;
if(parent.nodeName.toLowerCase() === 'form') {
return parent;
} else {
getFormElement(parent);
}
} else {
return false;
}
}
Why am I getting undefined in my console log?
not just
getFormElement(parent);
but
return getFormElement(parent);
and simplified, just for fun:
function getFormElement(elem) {
if(elem.nodeName.toLowerCase() !== 'body') {
var parent = elem.parentNode;
return parent.nodeName.toLowerCase() === 'form' ? parent : getFormElement(parent);
}
return false;
}
Is there a pure JS equivalent of jQuery .is() on modern browsers?
I know there is the querySelector method, but I want to check the node itself, rather than finding child nodes.
Looks like matchesSelector is what I want.
https://developer.mozilla.org/en-US/docs/Web/API/Element.matches
Polyfill is here:
https://gist.github.com/jonathantneal/3062955
this.Element && function(ElementPrototype) {
ElementPrototype.matchesSelector = ElementPrototype.matchesSelector ||
ElementPrototype.mozMatchesSelector ||
ElementPrototype.msMatchesSelector ||
ElementPrototype.oMatchesSelector ||
ElementPrototype.webkitMatchesSelector ||
function (selector) {
var node = this, nodes = (node.parentNode || node.document).querySelectorAll(selector), i = -1;
while (nodes[++i] && nodes[i] != node);
return !!nodes[i];
}
}(Element.prototype);
You've already answered your own question, but as per my comment above I looked through the jQuery.fn.is function. This isn't a strip from the source, because the function they're using is more generalized so it can be used across multiple other functions, But I've boiled it down to this function:
function is(elem, selector){ //elem is an element, selector is an element, an array or elements, or a string selector for `document.querySelectorAll`
if(selector.nodeType){
return elem === selector;
}
var qa = (typeof(selector) === 'string' ? document.querySelectorAll(selector) : selector),
length = qa.length,
returnArr = [];
while(length--){
if(qa[length] === elem){
return true;
}
}
return false;
}
DEMO
Another approach: Wrap the element you're testing in a parent then run querySelector from that
function is(el, selector) {
var div = document.createElement('div');
div.innerHTML = el.outerHTML;
return div.querySelector(selector);
}
I ran one test and it worked:
JS
var a = document.querySelector('a');
if(is(a, '.foo[name=foo]')) {
console.log('YES');
} else {
console.log('Nope');
}
HTML
Meow
I am sure this can be done a lot prettier.
According to youmightnotneedjquery.com depending on your IE compatibility requirement, you can even end up with simpler version:
var is = function(element, selector) {
return (element.matches || element.matchesSelector || element.msMatchesSelector ||
element.mozMatchesSelector || element.webkitMatchesSelector ||
element.oMatchesSelector).call(element, selector);
};
is(element, '.my-class');
With ES6 this would be:
const is = (element, selector) =>
(element.matches || element.matchesSelector || element.msMatchesSelector ||
element.mozMatchesSelector || element.webkitMatchesSelector ||
element.oMatchesSelector).call(element, selector);
};
is(element, '.my-class');
Following the concept from #AdamMerrifield it could be useful building the method is on any element through the Element.prototype chain by doing:
Element.prototype.is = function(match) {
...
};
Element is supported by all major browsers, even by IE 8+.
Here is a DEMO.