DOM#
DOM en JavaScript#
El DOM (Document Object Model) es una interfaz de programación que permite a los scripts actualizar el contenido, la estructura y el estilo de un documento mientras este se está visualizando en el navegador.
Estructura del Documento HTML#
El DOM representa la estructura de un documento HTML y el entorno en el que se ejecuta como una jerarquía de objetos. Los principales componentes son:
Window: Representa la ventana del navegador y es el objeto global en los scripts del navegador.
Document: Representa el documento HTML que se carga en la ventana.
Además de window
y document
, hay varios otros objetos principales accesibles en el entorno de una página web, estos se denominan Web APIs
:
Navigator: Proporciona información sobre el navegador.
navigator.userAgent
navigator.language
navigator.geolocation
navigator.getBattery()
Screen: Proporciona información sobre la pantalla del usuario.
screen.width
screen.height
screen.availWidth
screen.availHeight
History: Permite la manipulación del historial del navegador.
history.back()
history.forward()
history.go()
Location: Proporciona la URL actual de la ventana.
location.href
location.hostname
location.pathname
location.search
location.hash
location.reload()
Storage APIs: LocalStorage, sessionStorage, IndexedDB
Network APIs: XMLHttpRequest, fetch
Console: Proporciona acceso a la consola de depuración del navegador.
WebSocket: Proporciona una interfaz para las conexiones WebSocket.
Worker: Permite la ejecución de scripts en segundo plano.
Window#
Window
es un objeto predefinido en los navegadores web que representa la ventana en la que se muestra el documento. Algunos métodos importantes de window
incluyen:
alert()
,prompt()
: Métodos para mostrar diálogos.setTimeout(función, tiempo)
: Ejecuta una función después de un tiempo especificado.setInterval(función, tiempo)
: Ejecuta una función repetidamente a intervalos de tiempo especificados.clearTimeout(identificador)
: Cancela unsetTimeout
programado.
Estos métodos no suelen invocarse con window.setTimeout()
, por ejemplo, porque son accesibles directamente al estar en el ámbito global. De hecho, cualquier función o variable var
declarada en el ámbito global pasa a ser un atributo del objeto window:
var globalVar = "I'm global!";
function globalFunction() {
console.log("I'm a global function!");
}
console.log(window.globalVar); // "I'm global!"
window.globalFunction(); // "I'm a global function!"
Esto no ocurre con variables declaradas con let
o const
. La especificación de ECMAScript busca mejorar la claridad y previsibilidad del código. Al evitar que let y const se conviertan en propiedades del objeto window, se fomenta un diseño de código más modular y con menos dependencias globales.
Window no está cuando programamos para
Node
o otros intérpretes de servidor. Por esta razón, el Javascript dedicado al DOM debería estar separado de funciones normales, de manera que estas se puedan reaprovechar si parte de la lógica se mueve al servidor.
Document#
Este Objeto representa el documento HTML resultante de interpretar el HTML estático con las modificaciones que haya hecho javascript sobre él. Este Objeto también tiene métodos para manipular los elementos del DOM. Estos elementos están organizados en forma de árbol a partir de un nodo HTML
del cual parten HEAD
y BODY
.
Muchos nodos son Element
y otros son nodos de texto. Por tanto, en ocasiones hay que distinguir entre cualquier nodo o los que son Element
, que parten de una etiqueta HTML.
Ejemplo:
<div id="contenedor">
Hola
<p>Este es un párrafo</p>
Mundo
</div>
Este div
contiene tres nodos hijos:
Un nodo de texto:
"Hola"
Un nodo
Element
:<p>Este es un párrafo</p>
Otro nodo de texto:
"Mundo"
Ahora el código JavaScript para distinguirlos:
const contenedor = document.getElementById('contenedor');
contenedor.childNodes.forEach((nodo) => {
if (nodo.nodeType === Node.ELEMENT_NODE) {
console.log('Es un Element:', nodo.tagName);
} else if (nodo.nodeType === Node.TEXT_NODE) {
console.log('Es un nodo de texto:', `"${nodo.textContent.trim()}"`);
} else {
console.log('Otro tipo de nodo:', nodo.nodeType);
}
});
Es un nodo de texto: "Hola"
Es un Element: P
Es un nodo de texto: "Mundo"
Esto demuestra que no todos los nodos son elementos HTML (Element
), y que a veces es importante filtrarlos si solo te interesan las etiquetas HTML. Para eso, se debe usar children
en lugar de childNodes
, ya que children
solo incluye nodos Element
.
Esperar a que Cargue el DOM#
Podemos asegurarnos de que el DOM esté completamente cargado antes de ejecutar nuestro script utilizando DOMContentLoaded
.
(function () {
"use strict";
document.addEventListener("DOMContentLoaded", function () {
for (let i = 0; i < 100; i++) {
let container = document.getElementById("content");
let number = document.createElement("p");
number.innerHTML = i;
container.appendChild(number);
}
});
})();
También podemos colocar nuestro script al final del cuerpo (body
) del documento HTML.
Si, además, añadimos el atributo defer
a un script, este se descargará de manera asíncrona y se ejecutará cuando el HTML haya sido totalmente interpretado y justo antes que DOMContentLoaded
.
Si es necesario esperar a que carge también todo el CSS, es decir el CSSOM
, podemos recurrir al evento load
, que espera a cargar e interpretar todo el CSS. Pero si no es necesario, es mejor esperar sólo al DOM. Esto es porque hay recursos muy pesados como imágenes o vídeos que puede incluso que no lleguen a cargar.
En general, recomendaremos usar
DOMContentLoaded
en vez de poner el script al final oload
porque al ser ejecutado al principio, ya hay cosas que se puede ir ejecutando sin necesidad de DOM antes de que cargue totalmente. Además, hace que el código sea independiente de cómo se inserta en la web. En cualquier caso, los script en Módulos siempre se ejecutan en mododefer
.
Crear Elementos#
Normalmente se crean nodos con document.createElement()
y similares. También se crean implícitamente cuando cambiamos el innerHTML
de un nodo o usando .append()
con código HTML.
Además de esas técnicas, existen otras formas de crear nodos en el DOM. Una alternativa es document.createDocumentFragment()
, que permite construir múltiples nodos en memoria antes de agregarlos al DOM, mejorando el rendimiento. También se pueden usar plantillas <template>
, que facilitan la creación de contenido estructurado sin insertarlo inmediatamente en el documento.
En cuanto a las mejores prácticas, es recomendable evitar la manipulación excesiva del DOM porque puede afectar el rendimiento. Si se usa innerHTML
, hay que tener cuidado con la seguridad, ya que puede introducir vulnerabilidades como XSS. También es preferible crear y modificar nodos en memoria antes de insertarlos en el DOM, para reducir el número de reflujo y repintado. Además, el uso de APIs modernas como cloneNode()
y importNode()
puede ser útil para manejar estructuras de manera eficiente.
Al crear un nodo se crea un objeto a partir del prototipo HTMLElement
que puede ser manipulado como cualquier otro objeto, puede ser clonado con cloneNode()
y puede ser insertado con append()
en otros elementos.
Buscar Elementos#
Para manipular elementos del DOM, primero debemos encontrarlos. Los métodos más comunes son:
document.getElementById(id)
: Encuentra un elemento por su ID.getElementsByTagName(tag)
: Encuentra todos los elementos con un nombre de etiqueta específico.getElementsByName(name)
: Encuentra todos los elementos con un nombre especificado.querySelector(selector)
: Devuelve el primer elemento que coincide con un selector CSS.querySelectorAll(selector)
: Devuelve unNodelist
de todos los elementos que coinciden con un selector CSS.
let element = document.getElementById('exampleId');
let elements = document.getElementsByTagName('p');
let elementByName = document.getElementsByName('exampleName');
let firstElement = document.querySelector('.exampleClass');
let allElements = document.querySelectorAll('.exampleClass');
El resultado de los métodos que encuentran más de un nodo es un HTMLCollection
o un NodeList
. Si queremos tratarlo como un array hay que convertirlo con Array.from()
o [...HTMLCollection]
.
A menudo, necesitamos acceder a un nodo específico a partir de uno ya existente en el DOM. Para esto, podemos utilizar los siguientes métodos aplicados a un elemento del árbol DOM:
elemento.parentElement
: retorna el elemento padre del nodo actual.elemento.children
: retorna una colección de todos los elementos hijos del nodo actual (sólo elementos HTML, no incluye comentarios ni nodos de texto).elemento.childNodes
: retorna una colección de todos los nodos hijos, incluyendo comentarios y nodos de texto, por lo cual no se usa frecuentemente.elemento.firstElementChild
: retorna el primer hijo que es un elemento HTML.elemento.firstChild
: retorna el primer nodo hijo, incluyendo nodos de texto o comentarios.elemento.lastElementChild
: similar afirstElementChild
, pero retorna el último hijo elemento HTML.elemento.lastChild
: similar afirstChild
, pero retorna el último nodo hijo.elemento.nextElementSibling
: retorna el siguiente hermano que es un elemento HTML.elemento.nextSibling
: retorna el siguiente nodo hermano, incluyendo nodos de texto o comentarios.elemento.previousElementSibling
: similar anextElementSibling
, pero retorna el hermano anterior que es un elemento HTML.elemento.previousSibling
: similar anextSibling
, pero retorna el nodo hermano anterior.elemento.hasChildNodes()
: indica si el nodo tiene nodos hijos.elemento.childElementCount
: retorna el número de elementos hijos.elemento.closest(selector)
: retorna el ancestro más cercano que coincide con el selector dado. Por ejemplo, si el elemento es un<td>
dentro de una tabla,elemento.closest('table')
retornará la tabla a la que pertenece.
El DOM proporciona accesos directos (atajos) para obtener elementos comunes:
document.documentElement
: obtiene el nodo del elemento<html>
.document.head
: obtiene el nodo del elemento<head>
.document.body
: obtiene el nodo del elemento<body>
.document.title
: obtiene el nodo del elemento<title>
.document.links
: obtiene una colección de todos los hipervínculos del documento.document.anchors
: obtiene una colección de todas las anclas del documento.document.forms
: obtiene una colección de todos los formularios del documento.document.images
: obtiene una colección de todas las imágenes del documento.document.scripts
: obtiene una colección de todos los scripts del documento.
No hay una manera mejor que otras en todas las ocasiones de encontrar los nodos. Si no queremos fallar se puede usar
querySelector
y usar selectores de CSS. De esta manera, cambiar el selector es cambiar la “query”. No obstante, los otros selectores más primitivos puede que sean más rápidos en ciertas ocasiones.
Modificar Elementos#
Una vez que hemos encontrado los nodos, podemos modificarlos. Algunos métodos útiles incluyen:
.innerHTML
,.innerText
,.outerHTML
: Para cambiar el contenido HTML o texto de un elemento..insertAdjacentHTML(position, text)
: Inserta texto HTML en una posición específica. https://lenguajejs.com/dom/crear/insertadjacent-api/.append(content, element)
,.prepend(content, element)
: Añade contenido al principio o al final de un elemento..after()
,.before()
: Inserta un elemento antes o después del elemento actual..cloneNode(deep)
: Clona un nodo, con o sin sus hijos..remove()
: Elimina un nodo..replaceChildren()
: Sustituye los nodos hijos por otros.
Métodos más antiguos pero aún en uso incluyen removeChild()
y appendChild()
.
let element = document.getElementById('exampleId');
element.innerHTML = 'Nuevo contenido';
element.insertAdjacentHTML('beforeend', '<p>Más contenido</p>');
element.append('Texto adicional');
element.remove();
En la programación de una SPA se recurre muchas veces a
innerHTML
, pero si solo nos interesa modificar el contenido del texto, es mejorinnerText
otextContent
. Estas son más seguras porque no interpretan ni ejecutan código HTML, lo que reduce el riesgo de ataquesXSS (cross-site scripting)
. Además, suelen ser más rápidas porque no requieren que el navegador analice y reprocese el árbol DOM con nuevas etiquetas.innerText
tiene en cuenta los estilos CSS y el layout mientras quetextContent
extrae o asigna el contenido textual bruto. En la mayoría de los casostextContent
es la mejor opción.
Lo que más habitualmente se hace es crear nodos y sustituir contenidos. Podemos distinguir dos metodologías diferenciadas que suelen mezclarse en proyectos reales. Un es crear todo con document.createElement()
y la otra ir generando una string
con código HTML
y insertarlo en un elemento existente con innerHTML
. Luego veremos técnicas más sofisticadas como las template literals
, <template>
o Web Components
. Para cambiar el contenido de un nodo existente se pueden añadir o quitar nodos hijos con las funciones vistas anteriormente. Si se quiere limpiar totalmente un elemento podemos usar innerHTML=''
o mejor .replaceChildren()
.
Atributos#
Los elementos suelen tener atributos. Algunos son especiales como el id
o la class
. El id
está accesible directamente como atributo del elemento, así como el className
, aunque luego veremos que es mejor manipularlo de otra manera. Otros atributos como value
en los Input
o scr
en los <img>
también pueden ser leidos y modificados como propiedades. Se trata de los atributos estándar.
Para los atributos que no tienen acceso directo porque no son estándar, podemos usar setAttribute()
getAttribute()
, hasAttribute()
o removeAttribute()
:
const button = document.querySelector("button");
button.setAttribute("name", "helloButton");
button.setAttribute("disabled", "");
Atributos de Datos#
HTML5 permite agregar atributos personalizados no visuales a las etiquetas utilizando data-*
. Estos atributos pueden ser accesibles a través de JavaScript usando dataset
.
<article
id="electriccars"
data-columns="3"
data-index-number="12314"
data-parent="cars">
...
</article>
let article = document.getElementById('electriccars');
console.log(article.dataset.columns); // 3
console.log(article.dataset.indexNumber); // 12314
Para encontrar luego un elemento con un dato determinado podemos usar querySelector
:
document.querySelectorAll(`[data-columns="3"]`);
Propiedades#
Puesto que los elementos HTML al ser parseados y convertidos al DOM se convierten en objetos, estos son manipulables como cualquier objeto, pudiendo añadir o modificar propiedades, incluso aquellas que vienen en el HTML como atributos estándar.
Atributos como
id
se sincronizan perfectamente con la propiedad. Otros comovalue
no se sincronizan directamente: https://es.javascript.info/dom-attributes-and-properties#sincronizacion-de-propiedad-y-atributo
Hay unos atributos que se sincronizan de forma especial con las propiedades. Son los que comienzan por
data-
, que se guardan en un objeto.dataset
del elemento en el DOM. Esto lo explicamos en la sección de Atributos de datos.
Cambiar la propiedad no siempre cambia el atributo.
Cambiar el atributo inicializa la propiedad al cargar la página, pero después no están sincronizados automáticamente.
Estilos#
Para manipular los estilos de un elemento, podemos usar propiedades de estilo y clases CSS.
.style.property
: Modifica un estilo CSS directamente..className
: Cambia el nombre de la clase del elemento..classList.add()
,.classList.toggle()
,.classList.remove()
,.classList.replace()
: Métodos para manipular clases CSS de manera más dinámica.
let element = document.getElementById('exampleId');
element.style.color = 'blue';
element.classList.add('new-class');
element.classList.remove('old-class');
ClassName no debería usarse, ya que puede molestar si se usa classList por otro lado. Usaremos classList en todas las ocasiones excepto para eliminar todas las clases.
Manipulación eficiente del DOM#
Para ser más eficiente hay que tener en cuenta que cada modificación provoca un reflujo y un repaint que pueden ser costosos. Así que, en general, evitaremos hacer muchas modificaciones juntas.
Evitar cambios múltiples de estilos.
Usar
Document Fragments
para ir haciendo modificaciones antes de añadir al DOM visible.Clonar nodos en vez de hacerlos de nuevo cada vez.
Cachear los
querySelector
en variables.Usar ids y clases para simplificar las búsquedas.
Usar
requestAnimationFrame
para optimizar animaciones.En ocasiones en las que se ejecutan eventos muy rápido como
scroll
oresize
, hay que usar técnicas deDebouncing
oThrottling
. (Con RxJS es mucho más sencillo)
Creación de elementos mediante plantillas#
Se pueden crear elementos totalmente de forma programática. Pero puede ser tedioso. Muchas veces, si sabemos que hay fragmentos de HTML bastante estáticos, podemos usar innerHTML
y .append()
con plantillas creadas mediante strings.
Para crear elementos del DOM mediante plantillas hay muchas formas. Obviaremos las más farragosas y nos centraremos en aquellas que son más rápidas.
Creación de Elementos: Template Literal#
Los template literals y las interpolaciones de cadenas permiten crear contenido dinámico de manera sencilla.
function generateGraphCard(graph) {
let cardTemplate = document.createElement('div');
cardTemplate.classList.add('col');
cardTemplate.innerHTML = `
<div class="card">
<div class="card-header">${graph.title}</div>
<div class="card-body">
<div class="graph"></div>
<p class="card-text">${graph.description}</p>
<a href="#/graph/${graph.id}" class="btn btn-primary">Full screen</a>
</div>
</div>`;
let graphContainer = cardTemplate.querySelector('.graph');
graphContainer.append(graph.Data ? generateBarGraph(graph.Data) : graphPlaceholder());
return cardTemplate;
}
Creación de elementos mediante Tagged Template Literals#
Los “Tagged Template Literals” son una característica de JavaScript que te permite crear funciones que aceptan una plantilla literal y sus interpolaciones. Estas funciones son invocadas de forma muy diferente, ya que no tienen (parentesis) y se entiende que el primer argumento es la plantilla y el resto de argumentos son los distintos valores de las variables interpoladas.
En lugar de recibir una sola cadena de texto con las interpolaciones ${}
, la función de etiqueta recibe dos argumentos separados: un array de strings y un array con los valores interpolados.
(()=>{
function miTaggedTemplateLiteral(strings, ...values) {
return console.log(strings, ...values);
}
let nombre = "Carlos";
let edad = 32;
miTaggedTemplateLiteral`Hola soy ${nombre} y tengo ${edad} años`;
})();
[ "Hola soy ", " y tengo ", " años" ] Carlos 32
En el siguiente ejemplo, extraído de https://exploringjs.com/es6/ch_template-literals.html#sec_html-tag-function-implementation se puede ver cómo hacer una función para tagged template literal que personalice una plantilla.
Las funciones sirven para sanitizar el texto para evitar inyecciones de código. Además, crea todo un motor de plantillas. A continuación declara una plantilla tmpl
como una función que llama a la función html
con los datos pasados como parámetros.
function htmlEscape(str) {
return str.replace(/&/g, '&') // first!
.replace(/>/g, '>')
.replace(/</g, '<')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/`/g, '`');
}
function html(templateObject, ...substs) {
// Use raw template strings: we don’t want
// backslashes (\n etc.) to be interpreted
const raw = templateObject.raw;
let result = '';
substs.forEach((subst, i) => {
// Retrieve the template string preceding
// the current substitution
let lit = raw[i];
// In the example, map() returns an Array:
// If `subst` is an Array (and not a string),
// we turn it into a string
if (Array.isArray(subst)) {
subst = subst.join('');
}
// If the substitution is preceded by an exclamation
// mark, we escape special characters in it
if (lit.endsWith('!')) {
subst = htmlEscape(subst);
lit = lit.slice(0, -1);
}
result += lit;
result += subst;
});
// Take care of last template string
result += raw[raw.length-1]; // (A)
return result;
}
const tmpl = addrs => html`
<table>
${addrs.map(addr => html`
<tr><td>!${addr.first}</td></tr>
<tr><td>!${addr.last}</td></tr>
`)}
</table>
`;
const data = [
{ first: '<Jane>', last: 'Bond' },
{ first: 'Lars', last: '<Croft>' },
];
console.log(tmpl(data));
<table>
<tr><td><Jane></td></tr>
<tr><td>Bond</td></tr>
<tr><td>Lars</td></tr>
<tr><td><Croft></td></tr>
</table>
Creación de Elementos: Interpolaciones, Wrapper, fragments#
Podemos usar funciones para extraer e implementar interpolaciones en template literals.
function extractInterpolations(template) {
let regex = /\{\{([^\{\}]*)\}\}/g;
return [...template.matchAll(regex)];
}
function applyInterpolations(template, data) {
return extractInterpolations(template).reduce((T, [I, att]) =>
T = T.replace(I, data[att]), template);
}
function wrapElement(innerHTML) {
let wrapper = document.createElement('div');
wrapper.innerHTML = innerHTML;
return wrapper.firstElementChild;
}
function renderNews(news) {
let newsTemplate = `
<article id="article_{{id}}">
<a href="{{link}}"><h2>{{headline}}</h2></a>
<time>{{date}}</time><address>{{authors}}</address>
<p>{{short_description}}</p>
<p>{{category}}</p>
</article>`;
return wrapElement(applyInterpolations(newsTemplate, news));
}
Este ejemplo es un poco más complicado de lo que se espera poder hacer a estas alturas. No obstante, es interesante intentar entender su funcionamiento. En él se usan {{}}
como interpolaciones como en Angular
. Esta puede ser una base para hacer un motor de plantillas como tienen los frameworks. Por otro lado, se crea un div que actua de Wrapper
, és dedir, que envuelve el verdadero elemento para poder trabajar sólo con strings hasta el final, pero retornar un Element
, gracias a innerHTML
.
Como mejora al ejemplo anterior, el Wrapper
puede ser un fragment
. Este tiene mejor rendimiento, no solo permite sacar el primer elemento hijo, por lo que no necesitamos un div que los contenga y no genera un nodo adicional. Es muy eficiente insertando múltiples nodos, para insertar en bucle.
function renderComments(comments) {
const fragment = document.createDocumentFragment();
comments.forEach(comment => {
const commentElement = document.createElement('div');
commentElement.className = 'comment';
commentElement.innerHTML = `
<h4>${comment.author}</h4>
<p>${comment.text}</p>
<time>${comment.date}</time>
`;
fragment.appendChild(commentElement); // Añadir cada comentario al fragmento, no al DOM
});
return fragment;
}
// Datos de ejemplo
const comments = [
{ author: "Ana", text: "¡Buen artículo!", date: "2024-11-11" },
{ author: "Luis", text: "Gracias por la información.", date: "2024-11-10" },
{ author: "Marta", text: "Me ha resultado muy útil.", date: "2024-11-09" }
];
// Llamada a la función para renderizar los comentarios
document.getElementById('comments-section').appendChild(renderComments(comments));
// Insertar todos los comentarios a la vez;
Creación de elementos con template#
La etiqueta <template>
es especial. Su interior no se renderiza como el resto, pero queda accesible para ser buscado. La utilidad es crear plantillas en HTML que puedan ser clonadas y rellenadas como se desee.
Veamos este HTML extraido de la web de referencia: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template:
<table id="producttable">
<thead>
<tr>
<td>UPC_Code</td>
<td>Product_Name</td>
</tr>
</thead>
<tbody>
<!-- datos opcionales pueden incluirse aquí opcionalmente -->
</tbody>
</table>
<template id="productrow">
<tr>
<td class="record"></td>
<td></td>
</tr>
</template>
Y con este javascript obtenemos el contenido del template, se clona y se rellena las veces que sea necesario:
// Test to see if the browser supports the HTML template element by checking
// for the presence of the template element's content attribute.
if ("content" in document.createElement("template")) {
// Instantiate the table with the existing HTML tbody
// and the row with the template
const tbody = document.querySelector("tbody");
const template = document.querySelector("#productrow");
// Clone the new row and insert it into the table
const clone = template.content.cloneNode(true);
let td = clone.querySelectorAll("td");
td[0].textContent = "1235646565";
td[1].textContent = "Stuff";
tbody.appendChild(clone);
// Clone the new row and insert it into the table
const clone2 = template.content.cloneNode(true);
td = clone2.querySelectorAll("td");
td[0].textContent = "0384928528";
td[1].textContent = "Acme Kidney Beans 2";
tbody.appendChild(clone2);
} else {
// Find another way to add the rows to the table because
// the HTML template element is not supported.
}
Cualquiera de las formas que hemos visto para crear elementos, mediante template literal o funciones tagged, creando nuestras interpolaciones o con templates es válida y combinable. Para lograr lo que los frameworks hacen con sus motores de plantilla hay mucho trecho, porque no se ha hablado del
shadow DOM
y otras técnicas avanzadas como loscustom elements
para lograr plantillas con reactividad. Si logramos hacer un motor de plantillas suficientemente genérico para la aplicación en la que estamos trabajando, el uso de frameworks queda mucho menos justificado. En la sección deWeb Components
explicaremos más técnicas y con más profundidad este tema.
Formularios en JavaScript#
En cursos anteriores de HTML se explica cómo hacer correctamente un formulario. Desde el punto de vista de la programación en Javascript, lo interesante es el tratamiento de los datos, la interacción con el usuario, validación del formulario y el posible envío de los datos al servidor.
En una aplicación web, la validación de los formularios se realiza tanto en el lado del cliente como en el del servidor.
De hecho, la única validación estrictamente necesaria se debe hacer en el servidor para evitar peticiones ilegales por clientes como postman o curl.
Pero se puede usar Javascript para mucho más que validar formularios. Por ejemplo, nos puede ayudar a autocompletar campos, descargar datos en segundo plano o tratar con imágenes o datos complejos antes de enviar al servidor.
En este apartado iremos desgranando cómo JS puede ayudar a tratar con formularios desde casi ningún control a control total por parte de JS.
En este capítulo vamos a tratar los formularios desde el punto de vista del DOM. Pero estos envían datos al servidor. Este tema es complejo por su parte y lo trataremos en el capítulo de comunicación con el servidor.
Atributos de Formularios#
El contenido de los campos de entrada en un formulario se puede visualizar y modificar utilizando el atributo value
. Otros elementos del formulario, como los botones de opción (radio button) y las casillas de verificación (checkbox), deben tener un name
común y también utilizan los atributos value
y checked
. Para los elementos select
, se utilizan los atributos options
y selectedIndex
.
Observa el ejemplo a continuación, que usa la manera (obsoleta) de asociar eventos onclick
para ejecutar una función que informe de los valores de los inputs del formulario:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Formulario de Ejemplo</title>
</head>
<body>
<form id="exampleForm">
<label for="textInput">Texto:</label>
<input type="text" id="textInput" value="Texto inicial"><br><br>
<label>Opciones:</label>
<input type="radio" name="options" value="opcion1" checked> Opción 1
<input type="radio" name="options" value="opcion2"> Opción 2<br><br>
<label for="checkboxInput">Casilla:</label>
<input type="checkbox" id="checkboxInput" checked><br><br>
<label for="selectInput">Selecciona:</label>
<select id="selectInput">
<option value="1">Opción 1</option>
<option value="2" selected>Opción 2</option>
<option value="3">Opción 3</option>
</select><br><br>
<button type="button" onclick="manipulateValues()">Ver y Manipular Valores</button>
</form>
<script src="script.js"></script>
</body>
</html>
function manipulateValues() {
// Obtener el valor del campo de texto
let textInput = document.getElementById('textInput');
console.log('Valor del campo de texto:', textInput.value);
textInput.value = 'Nuevo texto';
// Obtener el valor del radio button seleccionado
let selectedOption = document.querySelector('input[name="options"]:checked');
console.log('Valor del radio button seleccionado:', selectedOption.value);
// Cambiar la selección del radio button
document.querySelector('input[name="options"][value="opcion2"]').checked = true;
// Obtener el valor del checkbox
let checkboxInput = document.getElementById('checkboxInput');
console.log('Checkbox está marcado:', checkboxInput.checked);
// Cambiar el estado del checkbox
checkboxInput.checked = !checkboxInput.checked;
// Obtener el valor del select
let selectInput = document.getElementById('selectInput');
console.log('Valor del select:', selectInput.value);
// Cambiar la selección del select
selectInput.value = '3';
}
Ciclo Tradicional del Formulario#
Tradicionalmente, un formulario está diseñado para enviar datos mediante HTTP al servidor. Al enviar (submit) un formulario, el navegador empaqueta los datos y los envía utilizando el método HTTP especificado (como GET o POST). Los formularios pueden incluir validación interna mediante HTML, lo que es más rápido que JavaScript pero ofrece menos control y personalización. La validación interna de HTML genera pseudo-clases que pueden estilizarse con CSS.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Formulario con Validación</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<form id="validationForm" action="validar.php" method="POST">
<label for="name">Nombre:</label>
<input type="text" id="name" name="name" required><br><br>
<label for="email">Correo Electrónico:</label>
<input type="email" id="email" name="email" required><br><br>
<label for="password">Contraseña:</label>
<input type="password" id="password" name="password" required minlength="6"><br><br>
<button type="submit">Enviar</button>
</form>
<script src="script.js"></script>
</body>
</html>
/* Estilos básicos */
form {
width: 300px;
margin: 0 auto;
}
label {
display: block;
margin-bottom: 5px;
}
input {
width: 100%;
padding: 8px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
/* Pseudo-clases para la validación */
input:required {
border-left: 5px solid #0000FF; /* Borde azul para campos requeridos */
}
input:valid {
border-left: 5px solid #00FF00; /* Borde verde para campos válidos */
}
input:invalid {
border-left: 5px solid #FF0000; /* Borde rojo para campos inválidos */
}
/* Pseudo-clase para campo enfocado */
input:focus {
outline: none;
border-color: #66AFE9;
box-shadow: 0 0 8px rgba(102, 175, 233, 0.6);
}
document.getElementById('validationForm').addEventListener('submit', function(event) {
event.preventDefault(); // Evita el envío del formulario para la demostración
alert('Formulario enviado correctamente (validación exitosa)');
});
Este ejemplo demuestra cómo utilizar pseudo-clases CSS para estilizar formularios con validación interna en HTML.
Otra cosa a observar es que el form tiene action="https://ejemplo.com/api" method="POST"
. Esto es el ciclo tradicional, el cual se evita en el ejemplo con el preventDefault()
que está en la parte del código Javascript.
Ciclo del Formulario con JavaScript#
Podemos interceptar y detener el ciclo por defecto de un formulario para validarlo y enviarlo utilizando JavaScript. De esta manera, podemos evitar tener un botón submit
y controlar completamente el proceso de envío. Si el formulario envía datos al servidor y se refresca, JavaScript pierde el control del programa. Para evitar esto, podemos utilizar preventDefault()
dentro del evento submit
o devolver false
.
Ejemplo de Interceptar Submit con JavaScript#
Podemos manejar eventos de formularios para personalizar su comportamiento. Un ejemplo común es el uso del evento onsubmit
para ejecutar una función de validación antes de enviar el formulario. Si la función de validación devuelve true
, el formulario se envía; de lo contrario, se cancela el envío.
<form id="formulario" onsubmit="return validar();">
<input type="text" id="phone-number" required>
<button type="submit">Enviar</button>
</form>
<script>
function validar() {
var phoneNumber = document.getElementById('phone-number').value;
var phoneRGEX = /^[(]{0,1}[0-9]{3}[)]{0,1}[-\s\.]{0,1}[0-9]{3}[-\s\.]{0,1}[0-9]{4}$/;
var phoneResult = phoneRGEX.test(phoneNumber);
alert("phone: " + phoneResult);
return phoneResult; // Retorna true si es válido, de lo contrario false
}
</script>
Esta solución está a medio camino entre el control total por JS y el control por HTML tradicional. En aplicaciones web hechas, por ejemplo, con PHP tradicional, se puede usar pequeños fragmentos de Javascript para estas cosas. Recordemos que no todas las webs deben ser necesariamente SPAs y hay muchas opciones.
Enviar Formulario por JavaScript#
Podemos enviar un formulario mediante JavaScript utilizando el método submit()
. Esto es útil cuando queremos enviar el formulario después de realizar alguna operación adicional o validación personalizada.
Ejemplo de Envío por JavaScript#
<button onclick="enviarFormulario()">Enviar</button>
<script>
function enviarFormulario() {
let formulario = document.getElementById("formulario");
formulario.submit();
}
</script>
Pero si no nos vale con enviar el formulario con submit()
y queremos manipular sus datos antes de enviar o enviar a una API por POST manualmente, obtendremos el contenido input por input o con FormData
:
div.querySelector("#buttonSubmit").addEventListener('click', async (event) => {
event.preventDefault();
const newProfile = Object.fromEntries(new FormData(div.querySelector('form')).entries());
updateCurrentProfile(newProfile); // Le pasamos un objeto. Aunque en una petición POST tradicional podemos enviar un FormData directamente con fetch
});
Mirar el capítulo de comunicación con el servidor para más información y ejemplos con
FormData
Validación de formularios#
Puesto que podemos interceptar el ciclo del formulario o atender a cualquier evento y leer y escribir el value
de los inputs
, validar el formulario es sencillo. No obstante, cuando hay que hacer muchos formularios o muy grandes, el código puede resultar farragoso. En ese caso, hay técnicas como crear funciones validadoras que hacen el código más sencillo y fácil de mantener.
Expresiones Regulares#
Las expresiones regulares son una herramienta para validar campos de formulario, como números de teléfono, correos electrónicos, y otros patrones específicos.
<input type="text" id="phone-number">
<button onclick="validate()">Validar</button>
<script>
function validate() {
var phoneNumber = document.getElementById('phone-number').value;
var phoneRGEX = /^[(]{0,1}[0-9]{3}[)]{0,1}[-\s\.]{0,1}[0-9]{3}[-\s\.]{0,1}[0-9]{4}$/;
var phoneResult = phoneRGEX.test(phoneNumber);
alert("phone: " + phoneResult);
}
</script>
Ejercicio sobre formularios tradicionales vs con Javascript: https://jsfiddle.net/xxjcaxx/L2q8vzgn/10/
Validación y estilo#
Aquí entra el tema de la usabilidad. Estamos muy acostumbrados a un comportamiento de los formularios que nos informe de si estamos haciendo las cosas bien. Por ejemplo: un input
que no haya sido manipulado de momento, no debería mostrarse como erróneo. Pero si ya se ha escrito algo en él y saltamos de input
, ya debería mostrarse como erróneo. Esto también puede ser farragoso y siempre es mejor separar por funciones puras la validación y la aplicación de estilos.
En general, Javascript puede manipular las clases de los inputs en función de si son válidos. No se puede “forzar” directamente un input a ser :valid
desde JavaScript sin cumplir las condiciones que lo hacen válido. Pero se pueden manipular los valores o restricciones que lo hacen válido:
input.removeAttribute("required");
input.removeAttribute("pattern");
En una aplicación con muchos formularios es buena idea crear un “motor” de validación donde declarar las restricciones por un lado y validar todos los formularios mediante las mismas funciones. Esto es algo que hacen frameworks como Angular
.
Ficheros en formularios#
Enviar ficheros al servidor mediante un formulario HTML es una tarea común que se realiza utilizando un input
de tipo file
. El tratamiento de los ficheros puede diferir del de otros elementos del formulario.
Para entender mejor este apartado, también hay que dominar la comunicación
fetch
con el servidor que tratamos en el capítulo de comunicación con el servidor.
Envío de ficheros con un formulario tradicional#
Para enviar un fichero en un formulario tradicional, simplemente se crea un FormData
a partir del formulario y se envía utilizando un método HTTP como POST
. Los formularios tradicionales aceptan binarios a través del MIME, lo cual facilita este proceso.
Aquí hay un ejemplo de cómo hacerlo:
<form id="fileForm" enctype="multipart/form-data">
<input type="file" name="file" id="fileInput">
<button type="submit">Enviar</button>
</form>
document.getElementById('fileForm').addEventListener('submit', function(event) {
event.preventDefault();
const formData = new FormData(this);
fetch('/upload', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
});
Se crea un formulario con
enctype="multipart/form-data"
para manejar la subida de ficheros.En el evento
submit
del formulario, se previene el comportamiento por defecto.Se crea un objeto
FormData
a partir del formulario y se envía confetch
usando el métodoPOST
.
Envío de ficheros utilizando JSON#
Si queremos enviar los datos con JSON, el proceso es un poco más complejo, ya que JSON no puede manejar binarios directamente. Para resolver esto, se puede convertir el fichero en una cadena en formato Base64
antes de enviarlo.
Aquí hay un ejemplo de cómo hacerlo:
<form id="fileForm">
<input type="file" id="fileInput">
<button type="submit">Enviar</button>
</form>
document.getElementById('fileForm').addEventListener('submit', function(event) {
event.preventDefault();
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
const reader = new FileReader();
reader.onloadend = function() {
const base64String = reader.result.replace('data:', '').replace(/^.+,/, '');
const jsonData = {
fileName: file.name,
fileType: file.type,
fileData: base64String
};
fetch('/upload-json', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(jsonData)
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
};
reader.readAsDataURL(file);
});
Se crea un formulario sin especificar
enctype
.En el evento
submit
, se previene el comportamiento por defecto.Se obtiene el fichero del input y se utiliza
FileReader
para leer el fichero como una URL de datos.Se convierte la URL de datos en una cadena Base64.
Se crea un objeto JSON con el nombre, tipo y datos del fichero en Base64.
Se envía el objeto JSON utilizando
fetch
con el métodoPOST
y el encabezadoContent-Type
establecido comoapplication/json
.
Algunos servicios como Supabase aceptan que se les envíe los binarios con el método tradicional y los datos textuales en JSON
, eso implica dos peticiones. En el apartado de Supabase hay ejemplos.
Validación de ficheros#
Los ficheros deben ser validados en el lado del servidor, pero nosotros también podemos validarlos en la parte de cliente para mejorar la usabilidad. Validar los ficheros en el lado del cliente mejora la usabilidad al proporcionar retroalimentación inmediata a los usuarios sobre cualquier problema con sus archivos antes de que intenten subirlos al servidor. Esto puede ahorrar tiempo y frustración tanto para el usuario como para el servidor.
Aquí hay un ejemplo completo que ilustra cómo realizar estas validaciones en un formulario HTML:
<form id="fileForm">
<input type="file" id="fileInput">
<button type="submit">Enviar</button>
<div id="errorMessage" class="error"></div>
</form>
document.getElementById('fileForm').addEventListener('submit', function(event) {
event.preventDefault();
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
const errorMessage = document.getElementById('errorMessage');
errorMessage.textContent = '';
// Validación del tamaño del fichero (por ejemplo, 2MB máximo)
const maxSize = 2 * 1024 * 1024; // 2MB
if (file.size > maxSize) {
errorMessage.textContent = 'El fichero es demasiado grande. El tamaño máximo es de 2MB.';
return;
}
// Validación del tipo de fichero
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!allowedTypes.includes(file.type)) {
errorMessage.textContent = 'Tipo de fichero no permitido. Solo se permiten imágenes (jpeg, png, gif).';
return;
}
// Validación del nombre del fichero (sin caracteres especiales)
const regex = /^[a-zA-Z0-9_\-\.]+$/;
if (!regex.test(file.name)) {
errorMessage.textContent = 'El nombre del fichero contiene caracteres no permitidos.';
return;
}
// Si todas las validaciones son correctas, proceder a la subida del fichero
const reader = new FileReader();
reader.onloadend = function() {
const base64String = reader.result.replace('data:', '').replace(/^.+,/, '');
const jsonData = {
fileName: file.name,
fileType: file.type,
fileData: base64String
};
fetch('/upload-json', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(jsonData)
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
};
reader.readAsDataURL(file);
});
Imágenes en formularios#
Las imágenes son un tipo interesante de ficheros, ya que se pueden previsualizar.
function encodeFileAsURL(file, callback) {
if (file) {
const fileURL = URL.createObjectURL(file);
callback(fileURL);
} else {
console.error('No file provided or file is invalid.');
}
}
function handleFileInputChange(event, previewElementId) {
const file = event.target.files[0];
encodeFileAsURL(file, function(fileURL) {
document.getElementById(previewElementId).src = fileURL;
});
}
function attachFileInputHandler(inputElementId, previewElementId) {
document.getElementById(inputElementId).addEventListener('change', function(event) {
handleFileInputChange(event, previewElementId);
});
}
// Attach the handler for avatar file input
attachFileInputHandler('avatar', 'avatar_prev');
Puede que los usuarios envíen imágenes demasiado grandes. Si queremos reducir el tiempo de carga y el espacio en disco del servidor, las podemos reducir antes de enviar. El siguiente ejemplo recoge la imagen, la pone en un canvas virtual y vuelve a recuperar la imágen reducida para enviarla:
<input type="file" id="imageInput" accept="image/*">
<img id="imagePreview" alt="Previsualización de la imagen">
<button id="uploadButton">Subir Imagen</button>
const resizeImage = (file, maxWidth, maxHeight) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.addEventListener('load', event => {
const img = new Image();
img.src = event.target.result;
img.addEventListener('load', () => {
const canvas = document.createElement('canvas');
let width = img.width;
let height = img.height;
if (width > height) {
if (width > maxWidth) {
height *= maxWidth / width;
width = maxWidth;
}
} else {
if (height > maxHeight) {
width *= maxHeight / height;
height = maxHeight;
}
}
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob(blob => {
if (blob) {
resolve(blob);
} else {
reject(new Error('Error al redimensionar la imagen'));
}
}, file.type, 0.9);
});
});
reader.addEventListener('error', () => reject(new Error('Error al leer el archivo')));
reader.readAsDataURL(file);
});
};
Otros ficheros#
Hay muchas cosas que se pueden hacer con otro tipo de ficheros antes de subirlos al servidor, o incluso sin subirlos nunca. Por ejemplo, se puede previsualizar el contenido de un fichero de texto:
const formFileInput = fileForm.querySelector("#formFile");
formFileInput.addEventListener("change", () => {
let file = formFileInput.files[0];
let reader = new FileReader();
reader.readAsText(file);
reader.addEventListener("load", () => {
fileForm.querySelector("#previewFile").innerText = `${reader.result}`;
});
});
O incluso de un fichero PDF:
<div id="divLogin">
<input type="file" id="pdfInput" accept="application/pdf">
<embed id="pdfPreview" type="application/pdf">
</div>
function previewPDF(file, previewElementId) {
if (file && file.type === 'application/pdf') {
const fileURL = URL.createObjectURL(file);
document.getElementById(previewElementId).src = fileURL;
} else {
console.error('El archivo seleccionado no es un PDF.');
}
}
También se puede, gracias a HTML5, reproducir un fichero de audio:
<input type="file" id="audioInput" accept="audio/*">
<audio id="audioPreview" controls>
Tu navegador no soporta el elemento de audio.
</audio>
function previewAudio(file, previewElementId) {
if (file && file.type.startsWith('audio/')) {
const fileURL = URL.createObjectURL(file);
document.getElementById(previewElementId).src = fileURL;
} else {
console.error('El archivo seleccionado no es un audio válido.');
}
}
El Objeto File
#
Un objeto File
en JavaScript representa un archivo que se ha seleccionado a través de un elemento <input type="file">
o que se ha creado mediante la API de archivos.
Este objeto proporciona una forma sencilla de acceder a los metadatos del archivo (como su nombre, tamaño o tipo) y también permite manipular su contenido de forma eficiente. Dado que hereda de Blob
, también se pueden usar todos los métodos de manipulación de datos binarios que ofrece esta interfaz.
A la hora de usar un File
, es común acceder a propiedades como name
para obtener el nombre del archivo, o size
y type
para hacer validaciones antes de subirlo al servidor, asegurándonos, por ejemplo, de que no excede un tamaño máximo o que pertenece a un tipo MIME permitido. También es posible acceder a la fecha de última modificación del archivo con lastModified
, lo cual puede ser útil en tareas de sincronización o control de versiones.
Más allá de sus propiedades, lo más interesante del objeto File
es cómo nos permite acceder a su contenido. Métodos como text()
o arrayBuffer()
devuelven el contenido completo del archivo como texto o como datos binarios, respectivamente, lo que abre la puerta a leer archivos CSV, JSON, imágenes, o cualquier otro tipo de dato. Para archivos grandes o cuando se requiere un procesamiento más controlado, también es posible trabajar con flujos usando stream()
, o extraer partes específicas del archivo con slice()
.
El objeto FileReader
#
El objeto FileReader
en JavaScript permite leer archivos del sistema local del usuario de forma asíncrona. Trabaja con archivos seleccionados a través de un <input type="file">
. Es útil en aplicaciones web que necesitan acceder al contenido de archivos sin necesidad de enviarlos inmediatamente a un servidor. En lugar de bloquear la interfaz, FileReader
gestiona las operaciones de lectura en segundo plano y proporciona los resultados mediante eventos como onload
o onerror
.
Al trabajar con FileReader
, lo más importante es elegir el método de lectura adecuado según el tipo de archivo. Por ejemplo, si el archivo contiene texto (como un .txt
o .json
), lo natural es usar readAsText()
. En cambio, si se trata de una imagen o un archivo binario, se pueden usar readAsDataURL()
para mostrar una vista previa o readAsArrayBuffer()
para trabajar con sus bytes. En todos los casos, el resultado de la lectura se almacena en la propiedad result
del objeto una vez que la operación ha finalizado correctamente.
Como estas operaciones son asíncronas, se trabaja con eventos. El evento onload
se dispara cuando la lectura ha terminado con éxito, mientras que onerror
se ejecuta si ocurre un problema. Además, el estado de la lectura se puede consultar en cualquier momento usando readyState
, que indica si el lector está inactivo, leyendo o ha terminado. También se puede cancelar una lectura en curso con abort()
si el usuario cambia de archivo o decide cancelar la acción.
Eventos y Handlers#
Los eventos y los manejadores (handlers) son elementos para capturar interacciones del usuario y realizar acciones en respuesta a estas interacciones. Estas interacciones pueden ser clics del ratón, pulsaciones de teclado, desplazamientos, cambios en el tamaño de la ventana, etc.
Los eventos en JavaScript permiten capturar desde movimientos del mouse hasta pulsaciones de teclado y manipular el contenido y comportamiento de la página en respuesta a estas interacciones.
En los ejemplos anteriores era inevitable usar eventos. Los hemos usado de distintas maneras, pero ahora vamos a profundizar explicar la mejor manera de usarlos.
Eventos (Events)#
Los eventos son mecanismos que se activan cuando el usuario interactúa con la página web. Como casi todo en Javascript, se representan como objetos
de tipo Event
. Se crean a ser disparados por una interacción del usuario sobre un elemento del DOM.
Este tipo de objetos tiene mucha información sobre cada evento como el elemento original, el tipo de evento, posición donde se ha producido o datos específicos de cada tipo de evento.
Que se produzca un evento no significa que se atienda. En cualquier web se están produciendo eventos sin parar. Tan solo mover el ratón ya produce muchos eventos por segundo. Javascript está optimizado para no saturarse con eventos que no van a ser atendidos. Pero si queremos atenderlo, debemos poner un elemento del DOM a “escuchar” y asociarle una función a ejecutar si se produce. Esta función se llama manejador
o handler
.
Manejadores (Handlers)#
Un manejador, o handler en inglés, es una función que se ejecuta cuando ocurre un evento específico. Cada tipo de evento (como click
, submit
, mouseover
, etc.) puede tener asociado un manejador que define qué acción debe realizarse en respuesta al evento.
El manejador se asignará a un evento en un elemento del DOM. Puesto que es una funció, podemos hacer lo que queramos cuando ocurra este evento. Si queremos obtener información del evento, el objeto Event
generado se envía de forma implícita como primer parámetro de esa función, aunque podemos ignorarlo.
Con todo esto, aquí tenemos un ejemplo básico de asignación de manejadores de eventos:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Eventos y Manejadores</title>
</head>
<body>
<button id="myButton">Haz clic aquí</button>
<script src="script.js"></script>
</body>
</html>
// Obtener referencia al botón
const button = document.getElementById('myButton');
// Definir el manejador para el evento 'click'
function handleClick(event) {
console.log('¡Se hizo clic en el botón!');
console.log('Detalles del evento:', event);
}
// Asignar el manejador al evento 'click' del botón
button.addEventListener('click', handleClick);
Javascript permite tratar los los eventos de varias maneras muy diferenciadas, pero hay una que es mejor que las demás. Al ser relativamente más moderna, no siempre la veremos y no siempre es cómodo usarla, por lo que debemos conocer las tres maneras. Estas son eventos en línea
, registro tradicional de eventos
y registro de eventos con addEventListener
, que es la opción recomendada en cualquier aplicación mínimamente compleja.
Eventos en línea#
Ejemplo en HTML (No recomendable)#
<p onmouseover="this.style.background='#FF0000';" onmouseout="this.style.background='#FFFFFF';">HOLA</p>
Este método incrusta el código JavaScript directamente en el atributo onmouseover
y onmouseout
del elemento <p>
. Aunque es simple, no se recomienda porque mezcla lógica de presentación con el contenido y dificulta el mantenimiento.
Registro tradicional de eventos#
Para manejar eventos de manera más estructurada separando la vista de la lógica, se utiliza el método tradicional de registrar eventos en JavaScript:
window.onload = function() {
document.getElementById('hola').onmouseover = function() {
this.style.background = '#FF0000';
};
document.getElementById('hola').onmouseout = function() {
this.style.background = '#FFFFFF';
};
};
Aquí, se espera a que la ventana y todos los recursos se carguen completamente (window.onload
) antes de asignar los manejadores de eventos onmouseover
y onmouseout
al elemento con id hola
. Sin embargo, esta técnica tiene limitaciones, como la incapacidad de asignar múltiples manejadores a un mismo evento.
Registro avanzado de eventos#
El método recomendado para registrar eventos es usando addEventListener
, que ofrece más flexibilidad y mejores prácticas:
(function() {
"use strict";
document.addEventListener("DOMContentLoaded", function() {
document.getElementById('hola').addEventListener('mouseover', function() {
this.style.background = '#FF0000';
}, false);
document.getElementById('hola').addEventListener('mouseout', function() {
this.style.background = '#FFFFFF';
}, false);
});
})();
Aquí, addEventListener
permite agregar múltiples manejadores para el mismo evento, separando la lógica de la presentación del HTML.
Obtención de información del evento#
(function() {
"use strict";
document.addEventListener("DOMContentLoaded", function() {
document.getElementById('hola').addEventListener('mouseover', manejador, false);
document.getElementById('hola').addEventListener('mouseout', manejador, false);
});
function manejador(e) {
console.log(e.type, e.target);
if (e.type === 'mouseover') {
this.style.background = '#FF0000';
}
if (e.type === 'mouseout') {
this.style.background = '#FFFFFF';
}
if (e.target.id === 'hola') {
console.log('¡Hola!');
}
}
})();
En este ejemplo, manejador
es una función que maneja tanto el evento mouseover
como mouseout
. Utiliza el objeto Event
para obtener información sobre el tipo de evento (e.type
) y el objetivo del evento (e.target
), que es el elemento que disparó el evento.
Propagación y captura de eventos#
Los eventos se propagan desde el elemento que los desencadena hacia sus elementos padre. Se puede capturar un evento durante esta propagación y realizar acciones diferentes según el elemento específico que lo desencadenó. Para detener la propagación de un evento a elementos padre, se usa event.stopPropagation()
.
<div id="padre">
<div id="hijo">
<button id="boton">Haz clic aquí</button>
</div>
</div>
document.getElementById('boton').addEventListener('click', function(event) {
alert('Haz clic en el botón hijo');
event.stopPropagation(); // Detiene la propagación del evento hacia arriba
});
document.getElementById('hijo').addEventListener('click', function(event) {
alert('Haz clic en el div hijo');
});
document.getElementById('padre').addEventListener('click', function(event) {
alert('Haz clic en el div padre');
});
La propagación de eventos tiene dos fases principales:
Capturing Phase: La fase en la que el evento se propaga desde el documento raíz hasta el objetivo del evento.
Bubbling Phase: La fase en la que el evento se propaga desde el objetivo del evento hacia el documento raíz.
Enviar Datos de Hijos a Padres con Eventos Personalizados#
La propagación de eventos permite que los eventos desencadenados en elementos hijos se propaguen hacia sus elementos padres. Esta característica es muy útil para enviar datos desde elementos hijos a sus elementos padres mediante eventos personalizados y es fundamental en el funcionamiento de los componentes en varios frameworks de JavaScript como React, Vue.js y Angular.
Los eventos personalizados se pueden utilizar para comunicar datos desde un componente hijo a un componente padre. A continuación se muestra un ejemplo de cómo se puede lograr esto en un entorno sin frameworks, utilizando la propagación de eventos del DOM:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Custom Event Example</title>
</head>
<body>
<div id="parent"></div>
<script>
// Crear el elemento hijo
const child = document.createElement('button');
child.textContent = 'Click me';
// Crear el elemento padre
const parent = document.getElementById('parent');
parent.appendChild(child);
// Añadir un evento personalizado al hijo
child.addEventListener('click', () => {
const customEvent = new CustomEvent('childEvent', {
bubbles: true, // para que se propague
detail: { message: 'Hello from child' }
});
child.dispatchEvent(customEvent);
});
// Añadir un listener en el padre para capturar el evento del hijo
parent.addEventListener('childEvent', (event) => {
console.log('Received message from child:', event.detail.message);
});
</script>
</body>
</html>
En este ejemplo:
Elemento hijo (
child
): Cuando se hace clic en el botón, se dispara un evento personalizadochildEvent
con algunos datos en la propiedaddetail
.Elemento padre (
parent
): El padre escucha el eventochildEvent
y maneja los datos recibidos del hijo.
En React, los datos de un componente hijo se envían a un componente padre mediante la elevación del estado y callbacks, en lugar de utilizar directamente la propagación de eventos del DOM.
En Vue.js, la comunicación de hijos a padres se realiza mediante la emisión de eventos personalizados.
Angular También proporciona output
que crea un evento que es capturado por el componente padre.
Ejercicio de eventos y propagación: https://jsfiddle.net/xxjcaxx/wep0c2j9/1/
Eventos de teclado#
Los eventos de teclado (KeyboardEvent
) permiten capturar las pulsaciones de teclas y actuar en consecuencia. Se puede obtener el código de la tecla presionada usando event.code
, lo que proporciona una manera estandarizada de identificar cada tecla.
Aquí hay ejemplo sencillo que muestra cómo capturar eventos de teclado y obtener el código de la tecla presionada usando event.code
:
<input type="text" id="inputTexto" placeholder="Escribe algo aquí...">
document.getElementById('inputTexto').addEventListener('keydown', function(event) {
console.log('Tecla presionada:', event.code);
});
Cualquier objeto puede ser un listener
#
La función addEventListener
espera una función manejadora del evento o un objeto. Si le pasamos un objeto, este debe tener el método handleEvents()
. Esa función tendrá como this
el objeto pasado, pero aceptará el evento.
document.querySelector('#proves').addEventListener('click',
{
name: 'Merlin',
handleEvent (event) {
console.log(`The ${event.type} happened on ${this.name}.`);
}
}
);
Esto ahora puede no tener demasiado sentido, pero será muy útil con los
Web Components
.
Notas finales#
Cuando programamos para el Frontend, la manera de tratar el DOM es muy diversa. Si distinguimos entre una SPA y una web generada en el servidor con algo de Javascript, las técnicas son muy diferentes. En el caso de la SPA, es muy importante tener claro una arquitectura MVC o similar en la que unas plantillas se rellenen con los datos del servidor. La interactividad y el manejo de formularios se suele implementar toda en Javascript porque préviamente no havia nada de HTML. En una web más tradicional en la que el HTML ya está generado por el servidor, es importante saber buscar nodos y manipularlos sin romper la estructura previa. Por otro lado, está el Javascript enfocado a la parte visual: controlar el scroll, drag & drop, animaciones… En este tema se han puesto las bases, pero eso requiere un estudio por separado. Esta parte puede ser explorada más profundamente en el módulo de Diseño de Interfaces.