Cara menggunakan find closest sibling javascript

Have you ever had the problem of finding the parent of a DOM node in JavaScript, but aren’t sure how many levels you have to traverse up to get to it? Let’s look at this HTML for instance:

<div data-id="123">
  <button>Click me</button>
</div>

That’s pretty straightforward, right? Say you want to get the value of

var button = document.querySelector("button");


button.addEventListener("click", (evt) => {
  console.log(evt.target.parentNode.dataset.id);
  // prints "123"
});
8 after a user clicks the button:

var button = document.querySelector("button");


button.addEventListener("click", (evt) => {
  console.log(evt.target.parentNode.dataset.id);
  // prints "123"
});

In this very case, the Node.parentNode API is sufficient. What it does is return the parent node of a given element. In the above example,

var button = document.querySelector("button");


button.addEventListener("click", (evt) => {
  console.log(evt.target.parentNode.dataset.id);
  // prints "123"
});
9is the button clicked; its parent node is the div with the data attribute.

But what if the HTML structure is nested deeper than that? It could even be dynamic, depending on its content.

<div data-id="123">
  <article>
    <header>
      <h1>Some title</h1>
      <button>Click me</button>
    </header>
     <!-- ... -->
  </article>
</div>

Our job just got considerably more difficult by adding a few more HTML elements. Sure, we could do something like

<div data-id="123">
  <article>
    <header>
      <h1>Some title</h1>
      <button>Click me</button>
    </header>
     <!-- ... -->
  </article>
</div>
0, but come on… that isn’t elegant, reusable or scalable.

The old way: Using a <div data-id="123">   <article>     <header>       <h1>Some title</h1>       <button>Click me</button>     </header>      <!-- ... -->   </article> </div>1-loop

One solution would be to make use of a

<div data-id="123">
  <article>
    <header>
      <h1>Some title</h1>
      <button>Click me</button>
    </header>
     <!-- ... -->
  </article>
</div>
1 loop that runs until the parent node has been found.

function getParentNode(el, tagName) {
  while (el && el.parentNode) {
    el = el.parentNode;
    
    if (el && el.tagName == tagName.toUpperCase()) {
      return el;
    }
  }
  
  return null;
}

Using the same HTML example from above again, it would look like this:

var button = document.querySelector("button");


console.log(getParentNode(button, 'div').dataset.id);
// prints "123"

This solution is far from perfect. Imagine if you want to use IDs or classes or any other type of selector, instead of the tag name. At least it allows for a variable number of child nodes between the parent and our source.

There’s also jQuery

Back in the day, if you didn’t wanted to deal with writing the sort of function we did above for each application (and let’s be real, who wants that?), then a library like jQuery came in handy (and it still does). It offers a

<div data-id="123">
  <article>
    <header>
      <h1>Some title</h1>
      <button>Click me</button>
    </header>
     <!-- ... -->
  </article>
</div>
3 method for exactly that:

$("button").closest("[data-id='123']")

The new way: Using <div data-id="123">   <article>     <header>       <h1>Some title</h1>       <button>Click me</button>     </header>      <!-- ... -->   </article> </div>4

Even though jQuery is still a valid approach (hey, some of us are beholden to it), adding it to a project only for this one method is overkill, especially if you can have the same with native JavaScript.

And that’s where

<div data-id="123">
  <article>
    <header>
      <h1>Some title</h1>
      <button>Click me</button>
    </header>
     <!-- ... -->
  </article>
</div>
5 comes into action:

var button = document.querySelector("button");


console.log(button.closest("div"));
// prints the HTMLDivElement

There we go! That’s how easy it can be, and without any libraries or extra code.

<div data-id="123">
  <article>
    <header>
      <h1>Some title</h1>
      <button>Click me</button>
    </header>
     <!-- ... -->
  </article>
</div>
4 allows us to traverse up the DOM until we get an element that matches the given selector. The awesomeness is that we can pass any selector we would also give to
<div data-id="123">
  <article>
    <header>
      <h1>Some title</h1>
      <button>Click me</button>
    </header>
     <!-- ... -->
  </article>
</div>
7 or
<div data-id="123">
  <article>
    <header>
      <h1>Some title</h1>
      <button>Click me</button>
    </header>
     <!-- ... -->
  </article>
</div>
8. It can be an ID, class, data attribute, tag, or whatever.

element.closest("#my-id"); // yep
element.closest(".some-class"); // yep
element.closest("[data-id]:not(article)") // hell yeah

If

<div data-id="123">
  <article>
    <header>
      <h1>Some title</h1>
      <button>Click me</button>
    </header>
     <!-- ... -->
  </article>
</div>
5 finds the parent node based on the given selector, it returns it the same way as  
function getParentNode(el, tagName) {
  while (el && el.parentNode) {
    el = el.parentNode;
    
    if (el && el.tagName == tagName.toUpperCase()) {
      return el;
    }
  }
  
  return null;
}
0. Otherwise, if it doesn’t find a parent, it returns
function getParentNode(el, tagName) {
  while (el && el.parentNode) {
    el = el.parentNode;
    
    if (el && el.tagName == tagName.toUpperCase()) {
      return el;
    }
  }
  
  return null;
}
1 instead, making it easy to use with
function getParentNode(el, tagName) {
  while (el && el.parentNode) {
    el = el.parentNode;
    
    if (el && el.tagName == tagName.toUpperCase()) {
      return el;
    }
  }
  
  return null;
}
2 conditions:

var button = document.querySelector("button");


console.log(button.closest(".i-am-in-the-dom"));
// prints HTMLElement


console.log(button.closest(".i-am-not-here"));
// prints null


if (button.closest(".i-am-in-the-dom")) {
  console.log("Hello there!");
} else {
  console.log(":(");
}

Ready for a few real-life examples? Let’s go!

Use Case 1: Dropdowns

CodePen Embed Fallback

Our first demo is a basic (and far from perfect) implementation of a dropdown menu that opens after clicking one of the top-level menu items. Notice how the menu stays open even when clicking anywhere inside the dropdown or selecting text? But click somewhere on the outside, and it closes.

The

<div data-id="123">
  <article>
    <header>
      <h1>Some title</h1>
      <button>Click me</button>
    </header>
     <!-- ... -->
  </article>
</div>
5 API is what detects that outside click. The dropdown itself is a
function getParentNode(el, tagName) {
  while (el && el.parentNode) {
    el = el.parentNode;
    
    if (el && el.tagName == tagName.toUpperCase()) {
      return el;
    }
  }
  
  return null;
}
4 element with a
function getParentNode(el, tagName) {
  while (el && el.parentNode) {
    el = el.parentNode;
    
    if (el && el.tagName == tagName.toUpperCase()) {
      return el;
    }
  }
  
  return null;
}
5 class, so clicking anywhere outside the menu will close it. That’s because the value for
function getParentNode(el, tagName) {
  while (el && el.parentNode) {
    el = el.parentNode;
    
    if (el && el.tagName == tagName.toUpperCase()) {
      return el;
    }
  }
  
  return null;
}
6 is going to be
function getParentNode(el, tagName) {
  while (el && el.parentNode) {
    el = el.parentNode;
    
    if (el && el.tagName == tagName.toUpperCase()) {
      return el;
    }
  }
  
  return null;
}
1 since there is no parent node with this class.

function handleClick(evt) {
  // ...
  
  // if a click happens somewhere outside the dropdown, close it.
  if (!evt.target.closest(".menu-dropdown")) {
    menu.classList.add("is-hidden");
    navigation.classList.remove("is-expanded");
  }
}

Inside the

function getParentNode(el, tagName) {
  while (el && el.parentNode) {
    el = el.parentNode;
    
    if (el && el.tagName == tagName.toUpperCase()) {
      return el;
    }
  }
  
  return null;
}
8 callback function, a condition decides what to do: close the dropdown. If somewhere else inside the unordered list is clicked,
<div data-id="123">
  <article>
    <header>
      <h1>Some title</h1>
      <button>Click me</button>
    </header>
     <!-- ... -->
  </article>
</div>
5 will find and return it, causing the dropdown to stay open.

Use Case 2: Tables

CodePen Embed Fallback

This second example renders a table that displays user information, let’s say as a component in a dashboard. Each user has an ID, but instead of showing it, we save it as a data attribute for each

var button = document.querySelector("button");


console.log(getParentNode(button, 'div').dataset.id);
// prints "123"
0 element.

var button = document.querySelector("button");


button.addEventListener("click", (evt) => {
  console.log(evt.target.parentNode.dataset.id);
  // prints "123"
});
0

The last column contains two buttons for editing and deleting a user from the table. The first button has a

var button = document.querySelector("button");


console.log(getParentNode(button, 'div').dataset.id);
// prints "123"
1 attribute of
var button = document.querySelector("button");


console.log(getParentNode(button, 'div').dataset.id);
// prints "123"
2, and the second button is
var button = document.querySelector("button");


console.log(getParentNode(button, 'div').dataset.id);
// prints "123"
3. When we click on either of them, we want to trigger some action (like sending a request to a server), but for that, the user ID is needed.

A click event listener is attached to the global window object, so whenever the user clicks somewhere on the page, the callback function

function getParentNode(el, tagName) {
  while (el && el.parentNode) {
    el = el.parentNode;
    
    if (el && el.tagName == tagName.toUpperCase()) {
      return el;
    }
  }
  
  return null;
}
8 is called.

var button = document.querySelector("button");


button.addEventListener("click", (evt) => {
  console.log(evt.target.parentNode.dataset.id);
  // prints "123"
});
1

If a click happens somewhere else other than one of these buttons, no

var button = document.querySelector("button");


console.log(getParentNode(button, 'div').dataset.id);
// prints "123"
1 attribute exists, hence nothing happens. However, when clicking on either button, the action will be determined (that’s called event delegation by the way), and as the next step, the user ID will be retrieved by calling
var button = document.querySelector("button");


console.log(getParentNode(button, 'div').dataset.id);
// prints "123"
6:

var button = document.querySelector("button");


button.addEventListener("click", (evt) => {
  console.log(evt.target.parentNode.dataset.id);
  // prints "123"
});
2

This function expects a DOM node as the only parameter and, when called, uses

<div data-id="123">
  <article>
    <header>
      <h1>Some title</h1>
      <button>Click me</button>
    </header>
     <!-- ... -->
  </article>
</div>
5 to find the table row that contains the pressed button. It then returns the
var button = document.querySelector("button");


console.log(getParentNode(button, 'div').dataset.id);
// prints "123"
8 value, which can now be used to send a request to a server.

Use Case 3: Tables in React

Let’s stick with the table example and see how we’d handle it on a React project. Here’s the code for a component that returns a table:

var button = document.querySelector("button");


button.addEventListener("click", (evt) => {
  console.log(evt.target.parentNode.dataset.id);
  // prints "123"
});
3

I find that this use case comes up frequently — it’s fairly common to map over a set of data and display it in a list or table, then allow the user to do something with it. Many people use inline arrow-functions, like so:

var button = document.querySelector("button");


button.addEventListener("click", (evt) => {
  console.log(evt.target.parentNode.dataset.id);
  // prints "123"
});
4

While this is also a valid way of solving the issue, I prefer to use the

var button = document.querySelector("button");


console.log(getParentNode(button, 'div').dataset.id);
// prints "123"
8 technique. One of the drawbacks of the inline arrow-function is that each time React re-renders the list, it needs to create the callback function again, resulting in a possible performance issue when dealing with large amounts of data.

In the callback function, we simply deal with the event by extracting the target (the button) and getting the parent

var button = document.querySelector("button");


console.log(getParentNode(button, 'div').dataset.id);
// prints "123"
0 element that contains the
var button = document.querySelector("button");


console.log(getParentNode(button, 'div').dataset.id);
// prints "123"
8 value.

var button = document.querySelector("button");


button.addEventListener("click", (evt) => {
  console.log(evt.target.parentNode.dataset.id);
  // prints "123"
});
5

Use Case 4: Modals

CodePen Embed Fallback

This last example is another component I’m sure you’ve all encountered at some point: a modal. Modals are often challenging to implement since they need to provide a lot of features while being accessible and (ideally) good looking.

We want to focus on how to close the modal. In this example, that’s possible by either pressing

$("button").closest("[data-id='123']")
2 on a keyboard, clicking on a button in the modal, or clicking anywhere outside the modal.

In our JavaScript, we want to listen for clicks somewhere in the modal:

var button = document.querySelector("button");


button.addEventListener("click", (evt) => {
  console.log(evt.target.parentNode.dataset.id);
  // prints "123"
});
6

The modal is hidden by default through a

$("button").closest("[data-id='123']")
3 utility class. It’s only when a user clicks the big red button that the modal opens by removing this class. And once the modal is open, clicking anywhere inside it — with the exception of the close button — will not inadvertently close it. The event listener callback function is responsible for that:

var button = document.querySelector("button");


button.addEventListener("click", (evt) => {
  console.log(evt.target.parentNode.dataset.id);
  // prints "123"
});
7

var button = document.querySelector("button");


button.addEventListener("click", (evt) => {
  console.log(evt.target.parentNode.dataset.id);
  // prints "123"
});
9 is the DOM node that’s clicked which, in this example, is the entire backdrop behind the modal,
$("button").closest("[data-id='123']")
5. This DOM node is not within
$("button").closest("[data-id='123']")
6, hence
<div data-id="123">
  <article>
    <header>
      <h1>Some title</h1>
      <button>Click me</button>
    </header>
     <!-- ... -->
  </article>
</div>
4 can bubble up all it wants and won’t find it. The condition checks for that and triggers the
$("button").closest("[data-id='123']")
8 function.

Clicking somewhere inside the nodal, say the heading, would make

$("button").closest("[data-id='123']")
6 the parent node. In that case, the condition isn’t truthy, leaving the modal in its open state.

Oh, and about browser support…

As with any cool “new” JavaScript API, browser support is something to consider. The good news is that

<div data-id="123">
  <article>
    <header>
      <h1>Some title</h1>
      <button>Click me</button>
    </header>
     <!-- ... -->
  </article>
</div>
5 is not that new and is supported in all of the major browsers for quite some time, with a whopping 94% support coverage. I’d say this qualifies as safe to use in a production environment.

The only browser not offering any support whatsoever is Internet Explorer (all versions). If you have to support IE, then you might be better off with the jQuery approach.


As you can see, there are some pretty solid use cases for

<div data-id="123">
  <article>
    <header>
      <h1>Some title</h1>
      <button>Click me</button>
    </header>
     <!-- ... -->
  </article>
</div>
5. What libraries, like jQuery, made relatively easy for us in the past can now be used natively with vanilla JavaScript.

Thanks to the good browser support and easy-to-use API, I heavily depend on this little method in many applications and haven’t been disappointed, yet.