Casos de uso prácticos para el método más cercano() de JavaScript
¿Alguna vez ha tenido el problema de encontrar el padre de un nodo DOM en JavaScript, pero no está seguro de cuántos niveles debe atravesar para llegar a él? Veamos este HTML, por ejemplo:
div data-id="123" buttonClick me/button/div
Eso es bastante sencillo, ¿verdad? Supongamos que desea obtener el valor de data-id
después de que un usuario haga clic en el botón:
var button = document.querySelector("button");
button.addEventListener("click", (evt) = { console.log(evt.target.parentNode.dataset.id); // prints "123"});
En este mismo caso, la API Node.parentNode es suficiente. Lo que hace es devolver el nodo padre de un elemento determinado. En el ejemplo anterior, evt.target
¿se hace clic en el botón? su nodo padre es el div con el atributo de datos.
Pero ¿qué pasa si la estructura HTML está anidada más profundamente que eso? Incluso podría ser dinámico, dependiendo de su contenido.
div data-id="123" article header h1Some title/h1 buttonClick me/button /header !-- ... -- /article/div
Nuestro trabajo se volvió considerablemente más difícil al agregar algunos elementos HTML más. Claro, podríamos hacer algo como element.parentNode.parentNode.parentNode.dataset.id
, pero vamos… eso no es elegante, reutilizable o escalable.
A la antigua usanza: usando un whilebucle
Una solución sería utilizar un while
bucle que se ejecute hasta que se encuentre el nodo principal.
function getParentNode(el, tagName) { while (el el.parentNode) { el = el.parentNode; if (el el.tagName == tagName.toUpperCase()) { return el; } } return null;}
Usando nuevamente el mismo ejemplo HTML anterior, se vería así:
var button = document.querySelector("button");
console.log(getParentNode(button, 'div').dataset.id);// prints "123"
Esta solución está lejos de ser perfecta. Imagínese si desea utilizar ID o clases o cualquier otro tipo de selector, en lugar del nombre de la etiqueta. Al menos permite un número variable de nodos secundarios entre el padre y nuestra fuente.
También está jQuery
En el pasado, si no querías escribir el tipo de función que hicimos anteriormente para cada aplicación (y seamos realistas, ¿quién quiere eso?), entonces una biblioteca como jQuery era útil (y todavía lo es). ). Ofrece un .closest()
método para exactamente eso:
$("button").closest("[data-id='123']")
La nueva forma: usarElement.closest()
Aunque jQuery sigue siendo un enfoque válido (bueno, algunos de nosotros estamos en deuda con él), agregarlo a un proyecto solo para este método es excesivo, especialmente si puedes tener lo mismo con JavaScript nativo.
Y ahí es donde Element.closest
entra en acción:
var button = document.querySelector("button");
console.log(button.closest("div"));// prints the HTMLDivElement
¡Aquí vamos! Así de fácil puede ser y sin bibliotecas ni código adicional.
Element.closest()
nos permite recorrer el DOM hasta que obtengamos un elemento que coincida con el selector dado. Lo genial es que podemos pasar cualquier selector al que también le daríamos Element.querySelector
o Element.querySelectorAll
. Puede ser un ID, una clase, un atributo de datos, una etiqueta o lo que sea.
element.closest("#my-id"); // yepelement.closest(".some-class"); // yepelement.closest("[data-id]:not(article)") // hell yeah
Si Element.closest
encuentra el nodo principal según el selector dado, lo devuelve de la misma manera que document.querySelector
. De lo contrario, si no encuentra un padre, regresa null
, lo que facilita su uso con if
condiciones:
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(":(");}
¿Listo para ver algunos ejemplos de la vida real? ¡Vamos!
Nuestra primera demostración es una implementación básica (y lejos de ser perfecta) de un menú desplegable que se abre después de hacer clic en uno de los elementos del menú de nivel superior. ¿Observa cómo el menú permanece abierto incluso cuando hace clic en cualquier lugar dentro del menú desplegable o selecciona texto? Pero haga clic en algún lugar del exterior y se cerrará.
La Element.closest
API es la que detecta ese clic externo. El menú desplegable en sí es un ul
elemento con una .menu-dropdown
clase, por lo que al hacer clic en cualquier lugar fuera del menú se cerrará. Esto se debe a que el valor de evt.target.closest(".menu-dropdown")
será null
ya que no hay ningún nodo principal con esta clase.
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"); }}
Dentro de la handleClick
función de devolución de llamada, una condición decide qué hacer: cerrar el menú desplegable. Si se hace clic en algún otro lugar dentro de la lista desordenada, lo Element.closest
buscará y lo devolverá, lo que hará que el menú desplegable permanezca abierto.
Caso de uso 2: tablas
Este segundo ejemplo representa una tabla que muestra información del usuario, digamos como un componente en un panel. Cada usuario tiene un ID, pero en lugar de mostrarlo, lo guardamos como un atributo de datos para cada tr
elemento.
table !-- ... -- tr data-userid="1" td input type="checkbox" data-action="select" /td tdJohn Doe/td tdjohn.doe@gmail.com/td td button type="button" data-action="edit"Edit/button button type="button" data-action="delete"Delete/button /td /tr/table
La última columna contiene dos botones para editar y eliminar un usuario de la tabla. El primer botón tiene el data-action
atributo de edit
y el segundo botón es delete
. Cuando hacemos clic en cualquiera de ellos, queremos activar alguna acción (como enviar una solicitud a un servidor), pero para eso, se necesita el ID de usuario.
Se adjunta un detector de eventos de clic al objeto de ventana global, por lo que cada vez que el usuario hace clic en algún lugar de la página, handleClick
se llama a la función de devolución de llamada.
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}`); } }}
Si se hace clic en otro lugar que no sea uno de estos botones, no data-action
existe ningún atributo y, por lo tanto, no sucede nada. Sin embargo, al hacer clic en cualquiera de los botones, se determinará la acción (eso, por cierto, se llama delegación de eventos) y, como siguiente paso, se recuperará la identificación del usuario llamando a getUserId
:
function getUserId(target) { // `target` is always a button or checkbox. return target.closest("[data-userid]").dataset.userid;}
Esta función espera un nodo DOM como único parámetro y, cuando se llama, lo utiliza Element.closest
para buscar la fila de la tabla que contiene el botón presionado. Luego devuelve el data-userid
valor, que ahora se puede utilizar para enviar una solicitud a un servidor.
Caso de uso 3: tablas en React
Sigamos con el ejemplo de la tabla y veamos cómo lo manejaríamos en un proyecto de React. Aquí está el código de un componente que devuelve una tabla:
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 );}
Encuentro que este caso de uso surge con frecuencia: es bastante común asignar un conjunto de datos y mostrarlos en una lista o tabla, y luego permitir que el usuario haga algo con ellos. Mucha gente usa funciones de flecha en línea, así:
button onClick={() = handleClick(user.id)}Edit/button
Si bien esta también es una forma válida de resolver el problema, prefiero utilizar la data-userid
técnica. Uno de los inconvenientes de la función de flecha en línea es que cada vez que React vuelve a representar la lista, necesita crear la función de devolución de llamada nuevamente, lo que genera un posible problema de rendimiento al manejar grandes cantidades de datos.
En la función de devolución de llamada, simplemente manejamos el evento extrayendo el objetivo (el botón) y obteniendo el tr
elemento principal que contiene el data-userid
valor.
function handleClick(evt) { var userId = evt.target .closest("[data-userid]") .getAttribute("data-userid");
// do something with `userId`}
Caso de uso 4: modales
Este último ejemplo es otro componente que estoy seguro que todos habéis encontrado en algún momento: un modal. Los modales suelen ser difíciles de implementar, ya que deben proporcionar muchas funciones y al mismo tiempo ser accesibles y (idealmente) atractivos.
Queremos centrarnos en cómo cerrar el modal. En este ejemplo, eso es posible presionando Esc
un teclado, haciendo clic en un botón en el modal o haciendo clic en cualquier lugar fuera del modal.
En nuestro JavaScript, queremos escuchar clics en algún lugar del modal:
var modal = document.querySelector(".modal-outer");modal.addEventListener("click", handleModalClick);
El modal está oculto de forma predeterminada mediante una .is-hidden
clase de utilidad. Solo cuando un usuario hace clic en el gran botón rojo se abre el modal eliminando esta clase. Y una vez que el modal está abierto, hacer clic en cualquier lugar dentro de él (con la excepción del botón de cerrar) no lo cerrará inadvertidamente. La función de devolución de llamada del detector de eventos es responsable de eso:
function handleModalClick(evt) { // `evt.target` is the DOM node the user clicked on. if (!evt.target.closest(".modal-inner")) { handleModalClose(); }}
evt.target
es el nodo DOM en el que se hace clic y que, en este ejemplo, es el fondo completo detrás del modal div
. Este nodo DOM no está dentro div
, por lo tanto, Element.closest()
puede generar todo lo que quiera y no lo encontrará. La condición lo comprueba y activa la handleModalClose
función.
Al hacer clic en algún lugar dentro del nodo, digamos el encabezado, se creará div
el nodo padre. En ese caso, la condición no es verdadera, dejando el modal en su estado abierto.
Al igual que con cualquier API de JavaScript “nueva” y interesante, la compatibilidad con el navegador es algo a considerar. La buena noticia es que Element.closest
no es tan nuevo y es compatible con todos los principales navegadores desde hace bastante tiempo, con una enorme cobertura de soporte del 94%. Yo diría que esto se considera seguro de usar en un entorno de producción.
El único navegador que no ofrece soporte alguno es Internet Explorer (todas las versiones). Si tiene que admitir IE, es posible que le resulte mejor utilizar el enfoque jQuery.
Como puede ver, existen algunos casos de uso bastante sólidos para Element.closest
. Lo que las bibliotecas, como jQuery, nos hicieron relativamente fácil en el pasado ahora se pueden usar de forma nativa con JavaScript básico.
Gracias al buen soporte del navegador y a la API fácil de usar, dependo en gran medida de este pequeño método en muchas aplicaciones y todavía no me ha decepcionado.
¿Tiene otros casos de uso interesantes? No dudes en hacérmelo saber.
Deja un comentario