Tag: Method

Practical Use Cases for JavaScript’s closest() Method

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 data-id 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, evt.targetis 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 element.parentNode.parentNode.parentNode.dataset.id, but come on… that isn’t elegant, reusable or scalable.

The old way: Using a while-loop

One solution would be to make use of a while 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 .closest() method for exactly that:

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

The new way: Using Element.closest()

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 Element.closest 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.

Element.closest() 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 Element.querySelector or Element.querySelectorAll. 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 Element.closest finds the parent node based on the given selector, it returns it the same way as  document.querySelector. Otherwise, if it doesn’t find a parent, it returns null instead, making it easy to use with if 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

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 Element.closest API is what detects that outside click. The dropdown itself is a <ul> element with a .menu-dropdown class, so clicking anywhere outside the menu will close it. That’s because the value for evt.target.closest(".menu-dropdown") is going to be null 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 handleClick callback function, a condition decides what to do: close the dropdown. If somewhere else inside the unordered list is clicked, Element.closest will find and return it, causing the dropdown to stay open.

Use Case 2: Tables

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 <tr> element.

<table>   <!-- ... -->   <tr data-userid="1">     <td>       <input type="checkbox" data-action="select">     </td>     <td>John Doe</td>     <td>john.doe@gmail.com</td>     <td>       <button type="button" data-action="edit">Edit</button>       <button type="button" data-action="delete">Delete</button>     </td>   </tr> </table>

The last column contains two buttons for editing and deleting a user from the table. The first button has a data-action attribute of edit, and the second button is delete. 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 handleClick is called.

function handleClick(evt) {   var { action } = evt.target.dataset;      if (action) {     // `action` only exists on buttons and checkboxes in the table.     let userId = getUserId(evt.target);          if (action == "edit") {       alert(`Edit user with ID of $  {userId}`);     } else if (action == "delete") {       alert(`Delete user with ID of $  {userId}`);     } else if (action == "select") {       alert(`Selected user with ID of $  {userId}`);     }   } }

If a click happens somewhere else other than one of these buttons, no data-action 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 getUserId:

function getUserId(target) {   // `target` is always a button or checkbox.   return target.closest("[data-userid]").dataset.userid; }

This function expects a DOM node as the only parameter and, when called, uses Element.closest to find the table row that contains the pressed button. It then returns the data-userid 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:

function TableView({ users }) {   function handleClick(evt) {     var userId = evt.currentTarget     .closest("[data-userid]")     .getAttribute("data-userid"); 
     // do something with `userId`   } 
   return (     <table>       {users.map((user) => (         <tr key={user.id} data-userid={user.id}>           <td>{user.name}</td>           <td>{user.email}</td>           <td>             <button onClick={handleClick}>Edit</button>           </td>         </tr>       ))}     </table>   ); }

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:

<button onClick={() => handleClick(user.id)}>Edit</button>

While this is also a valid way of solving the issue, I prefer to use the data-userid 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 <tr> element that contains the data-userid value.

function handleClick(evt) {   var userId = evt.target   .closest("[data-userid]")   .getAttribute("data-userid"); 
   // do something with `userId` }

Use Case 4: Modals

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 Esc 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 modal = document.querySelector(".modal-outer");  modal.addEventListener("click", handleModalClick);

The modal is hidden by default through a .is-hidden 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:

function handleModalClick(evt) {   // `evt.target` is the DOM node the user clicked on.   if (!evt.target.closest(".modal-inner")) {     handleModalClose();   } }

evt.target is the DOM node that’s clicked which, in this example, is the entire backdrop behind the modal, <div class="modal-outer">. This DOM node is not within <div class="modal-inner">, hence Element.closest() can bubble up all it wants and won’t find it. The condition checks for that and triggers the handleModalClose function.

Clicking somewhere inside the nodal, say the heading, would make <div class="modal-inner"> 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 Element.closest 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 Element.closest. 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.

Do you have any other interesting use cases? Feel free to let me know.


The post Practical Use Cases for JavaScript’s closest() Method appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

CSS-Tricks

, , , ,

Weekly Platform News: Contrast Ratio Range, replaceAll Method, Native File System API

In this week’s roundup: Firefox’s new contrast checker, a simpler way to lasso substrings in a string, and a new experimental API that will let apps fiddle with a user’s local files.

Firefox shows the contrast ratio range for text on a multicolored background

According to Success Criterion 1.4.3 of the Web Content Accessibility Guidelines (WCAG), text should have a contrast ratio of at least 4.5. (A lower contrast ratio is acceptable only if the text is 24px or larger.)

If the background of the text is not a solid color but a color gradient or photograph, you can use the special element picker in Firefox’s Accessibility panel to get a range of contrast ratios based on the element’s actual background.

(via Šime Vidas)

Replacing all instances of a substring in a string

The new JavaScript replaceAll method makes it easier to replace all instances of a substring in a string without having to convert the substring to a regex first, which is “hard to get right since JavaScript doesn’t offer a built-in mechanism to escape regular expression patterns.”

// BEFORE str = str.replace(/foo/g, "bar");  // AFTER str = str.replaceAll("foo", "bar");

This new string method has not yet shipped in browsers, but you can start using it today via Babel (since it’s automatically polyfilled by @babel/preset-env).

(via Mathias Bynens)

Try out the Native File System API in Chrome

The Native File System API, which is experimentally supported in Chrome, allows web apps to read or save changes directly to local files on the person’s computer. The app is granted permission to view and edit files in a specific folder via two separate prompts.

You can try out this new feature by visiting labs.vaadin.com in Chrome on desktop.

(via Thomas Steiner)

More news…

sunday-issue-18.png

Read more news in my weekly newsletter for web developers. Pledge as little as $ 2 per month to get the latest news from me via email every Monday.

More News →

The post Weekly Platform News: Contrast Ratio Range, replaceAll Method, Native File System API appeared first on CSS-Tricks.

CSS-Tricks

, , , , , , , , , ,
[Top]

Weekly Platform News: Mozilla’s AV1 Encoder, Samsung One UI CSS, DOM Matches Method

Šime posts regular content for web developers on webplatform.newshttps://webplatform.news).

In this week’s weekly roundup, Vimeo and Mozilla partner up on a video encoding format, how to bind instructions to to form fields using aria labels, the DOM has a matching function, and Samsung is working on its own CSS library.


Vimeo partners with Mozilla to use their rav1e encoder

Vittorio Giovara: AV1 is a royalty-free video codec designed by the Alliance for Open Media and the the most anticipated successor of H.264. Vimeo is contributing to the development of Mozilla’s AV1 encoder.

In order for AV1 to succeed, there is a need of an encoder like x264, a free and open source encoder, written by the community, for the community, and available to everyone: rav1e. Vimeo believes in what Mozilla is doing.

Use aria-describedby to bind instructions to form fields

Raghavendra Satish Peri: If you provide additional instructions for a form field, use the aria-describedby attribute to bind the instruction to the field. Otherwise, assistive technology users who use the Tab key might miss this information.

<label for="dob">Date of Birth</label> <input type="text" aria-describedby="dob1" id="dob" /> <span id="dob1">Use DD/MM/YY</span>

Samsung Internet announces One UI CSS

Diego González: Samsung is experimentally developing a CSS library based on its new One UI design language. The library is called One UI CSS and includes styles for common form controls such as buttons, menus, and sliders, as well as other assets (web fonts, SVG icons, polyfills).

Some of the controls present in One UI CSS.

DOM elements have a matches method

Sam Thorogood: You can use the matches method to test if a DOM element has a specific CSS class, attribute or ID value. This method accepts a CSS selector and returns true if the element matches the given selector.

el.classList.has('foo')  /* becomes */ el.matches('.foo'); el.hasAttribute('hello') /* becomes */ el.matches('[hello]'); el.id === 'bar'          /* becomes */ el.matches('#bar');

The post Weekly Platform News: Mozilla’s AV1 Encoder, Samsung One UI CSS, DOM Matches Method appeared first on CSS-Tricks.

CSS-Tricks

, , , , , , ,
[Top]