Практические примеры использования метода closest() в JavaScript

Практические примеры использования метода closest() в JavaScript
Были ли у вас когда-нибудь проблемы с поиском родительского узла DOM в JavaScript, и вы не были уверены, сколько уровней вам необходимо пройти, чтобы добраться до него? Взглянем, к примеру, на приведенный ниже HTML-код.

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

Это довольно просто, правда? Допустим, вы хотели бы приобрести значение data-id после того, как посетитель нажмет кнопку.

var button = document.querySelector("button");
button.addEventListener("click",(evt) => { console.log(evt.target.parentNode.dataset.id); // отображает "123"});

В этом случае достаточно API Node.parentNode. Он возвращает родительский узел данного элемента. В приведенном выше примере evt.target – это нажатая кнопка. Ее родительский узел — это div с атрибутом data.

Но что, если структура HTML глубже? Она может быть даже динамичной, в зависимости от содержимого.

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

Наша работа значительно усложнилась из-за добавления ещё нескольких HTML-элементов. Конечно, мы могли бы сделать что-то наподобие element.parentNode.parentNode.parentNode.dataset.id, но… это не изящно, и не пригодно для повторного использования или масштабирования.

Устаревший метод: использование while-loop

Одним из решений будет использование цикла while, который выполняется до тех пор, пока не будет найден родительский узел.

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

Используя пример HTML, приведенный выше, это будет выглядеть следующим образом:

var button = document.querySelector("button");
console.log(getParentNode(button, 'div').dataset.id);// отображает "123"

Это решение далеко не идеально. Представьте, что вы хотели бы использовать идентификаторы, классы или любой другой тип селектора вместо имени тега. По крайней мере, это может использовать переменное число дочерних узлов между родительским узлом и нашим источником.

Не забывайте jQuery

Раньше, если вы не хотели иметь дело с написанием той возможности, которую мы выполнили выше для каждого приложения(и давайте будем честными, кому это необходимо?), то вам пригодилась бы такая библиотека как jQuery(и она все ещё работает). Она предлагает способ .closest(), предназначенный для этого.

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

Новый метод: использование Element.closest()

Несмотря на то, что jQuery по-прежнему будет допустимым подходом, его добавление в проект только для этого способа будет излишним, особенно если вы можете приобрести то же самое при помощи нативного JavaScript.

И вот тут в игру вступает Element.closest.

var button = document.querySelector("button");
console.log(button.closest("div"));// отображает HTMLDivElement

Ну вот! Вот как это может быть просто, и без каких-либо библиотек или лишнего кода.

Element.closest() может перемещаться по DOM, пока мы не получим элемент, соответствующий заданному селектору. Замечательно то, что мы можем передать любой селектор, который мы также передали бы Element.querySelector или Element.querySelectorAll. Это может быть идентификатор, класс, атрибут данных, тег или что-то ещё.

element.closest("#my-id"); // даelement.closest(".some-class"); // даelement.closest("[data-id]:not(article)") // черт побери, да!

Если Element.closest находит родительский узел на основе данного селектора, он возвращает его так же, как document.querySelector. В противном случае, если он не находит родителя, то возвращается null, что упрощает использование с условиями if.

var button = document.querySelector("button");
console.log(button.closest(".i-am-in-the-dom"));// отображает HTMLElement
console.log(button.closest(".i-am-not-here"));// отображает null
if(button.closest(".i-am-in-the-dom")) {  console.log("Hello there!");} else {  console.log(":(");}

Готовы к нескольким примерам из реальной жизни? Поехали!

Пример использования 1: раскрывающиеся списки

Наша первая демонстрация представляет собой базовую(и далеко не идеальную) реализацию раскрывающегося меню, которое открывается после клика по одному из пунктов меню верхнего уровня. Обратите внимание, что меню остается открытым даже при клике в любом месте раскрывающегося списка или выборе текста. Но при клике где-нибудь снаружи оно закроется.

Element.closest API обнаруживает, что клик был за пределами списка. Сам выпадающий список представляет собой элемент <ul> с классом.menu-dropdown, так что клик в любом месте вне меню закроет его. Это потому, что значение для evt.target.closest(«.menu-dropdown») будет null, поскольку у нас нет родительского узла с данным классом.

function handleClick(evt) {  //...    // если клик был выполнен где-либо за пределами выпадающего меню, закрываем его.  if(!evt.target.closest(".menu-dropdown")) {    menu.classList.add("is-hidden");    navigation.classList.remove("is-expanded");  }}

Внутри возможности обратного вызова handleClick условие определяет, что делать: закрыть раскрывающийся список. Если кликнуть где-то ещё внутри неупорядоченного списка, Element.closest найдет и вернет его, в результате чего раскрывающийся список останется открытым.

Пример использования 2: таблицы

Во втором примере у нас есть таблица, которая выводит информацию о посетителе, скажем, как компонент в панели инструментов. У каждого посетителя есть идентификатор, но вместо того, чтобы показывать его, мы сохраняем его как атрибут data для каждого элемента <tr>.

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

Последний столбец включает две кнопки для редактирования и удаления посетителя из таблицы. Первая кнопка имеет в себя атрибут data-action edit, а вторая — delete. Когда мы кликаем по любой из них, мы хотим вызвать какое-то действие(к примеру, отправку запроса на сервер), но для этого необходим идентификатор посетителя.

Прослушиватель события клика прикреплен к глобальному объекту окна, так что всякий раз, когда посетитель кликает где-нибудь на странице, вызывается функцию обратного вызова handleClick.

function handleClick(evt) {  var { action } = evt.target.dataset;    if(action) {    // `action` существует только для кнопок и чек-боксов в таблице.    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}`);    }  }}

Если клик происходит где-то ещё, кроме одной из данных кнопок, атрибут data-action не существует, следовательно, ничего не происходит. Однако при нажатии любой кнопки будет определено действие(кстати, это называется делегированием события), и на следующем шаге идентификатор посетителя будет получен путем вызова getUserId.

function getUserId(target) {  // `target` - это  кнопка или чек-бокс.  return target.closest("[data-userid]").dataset.userid;}

Эта функцию ожидает, что узел DOM будет единственным настройкою, и при вызове использует его Element.closest для поиска строки таблицы, содержащей нажатую кнопку. Далее он возвращает значение data-userid, которое сейчас можно использовать для отправки запроса на сервер.

Пример использования 3: таблицы в React

Давайте остановимся на примере таблицы и посмотрим, как мы справимся с данным в React-проекте. Ниже приведен код компонента, возвращающего таблицу.

function TableView({ users }) {  function handleClick(evt) {    var userId = evt.currentTarget.closest("[data-userid]").getAttribute("data-userid");
    // делаем что-то с `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>  );}

Я обнаружил, что этот вариант использования применяется действительно часто — довольно часто можно легко сопоставить набор данных и отобразить его в списке или таблице, а далее позволить посетителю что-то с ним делать. Многие разработчики используют встроенные стрелочные возможности, к примеру:

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

Хотя это возможный метод решения проблемы, я предпочитаю использовать технику data-userid. Одним из недостатков встроенной стрелочной возможности будет то, что каждый раз, когда React повторно визуализирует список, ему надо снова создавать возможность обратного вызова, что может привести к проблемам с производительностью при работе с большими объемами данных.

В возможности обратного вызова мы просто обрабатываем событие, извлекая цель (кнопку) и получая родительский элемент <tr>, содержащий значение data-userid.

function handleClick(evt) {  var userId = evt.target  .closest("[data-userid]")  .getAttribute("data-userid");
  // делаем что-то с `userId`}

Пример использования 4: модальные окна

Этот последний пример — ещё один компонент, с которым, я уверен, вы все когда-то сталкивались: модальные окна. Модальные окна часто сложно реализовать, так как они должны обеспечивать несколько возможностей, и при этом быть доступными, а также (в идеале) красивыми.

Мы хотим сосредоточиться на том, как закрыть модальное окно. В этом примере это возможно при помощи нажатия клавиши Esc на клавиатуре, нажатия кнопки в модальном окне или клика в любом месте за пределами модального окна.

В JavaScript мы хотим отслеживать клики где-нибудь в модальном окне.

var modal = document.querySelector(".modal-outer");modal.addEventListener("click", handleModalClick);

По умолчанию модальное окно скрыто при помощи служебного класса .is-hidden. Модальное окно открывается, удаляя этот класс, только когда посетитель нажимает большую красную кнопку. И когда модальное окно открыто, клик в любом месте внутри него — за исключением кнопки закрытия — не приведет к его случайному закрытию. За это отвечает функцию обратного вызова прослушивателя событий.

function handleModalClick(evt) {  // `evt.target` - это узел DOM, на который кликнул посетитель.  if (!evt.target.closest(".modal-inner")) {    handleModalClose();  }}

evt.target — это узел DOM, по которому кликнул посетитель, в данном примере, это весь фон за модальным окном, <div class=»modal-outer»>. Этот узел DOM не находится внутри <div class=»modal-inner»>, так что Element.closest() может проходить все, что хочет, и не может его найти. Условие проверяет это и запускает возможность handleModalClose.

Клик где-нибудь внутри узла <div class=»modal-inner»>, скажем по заголовку, создает родительский узел. В этом случае условие не соответствует действительности, оставляя модальное окно в открытом состоянии.

Да, и насчет поддержки браузерами…

Как и в случае с любым классным «новым» JavaScript API, надо учитывать поддержку браузерами. Хорошая новость заключается в том, что Element.closest — это не новая функцию, и она поддерживается во всех основных браузерах в течение достаточно долгого времени, с огромным охватом поддержки в 94% .

Единственный браузер, не предлагающий никакой поддержки — это Internet Explorer (все версии). Если вам необходимо поддерживать IE, — лучше использовать подход с использованием jQuery.

Как видите, есть сразу же пару довольно надежных вариантов использования Element.closest. Такие библиотеки, как jQuery, относительно без труда использовались нами в прошлом, сейчас же можно без труда использовать обычный JavaScript.

Благодаря хорошей поддержке браузерами и простоте в использовании API, я сильно зависим от этого небольшого способа во многих приложениях и пока не разочарован.

Есть ли у вас иные интересные варианты использования? Не стесняйтесь, расскажите о них в комментариях к данной статье.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *