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 :

  1. Navigator: Proporciona información sobre el navegador.

    • navigator.userAgent

    • navigator.language

    • navigator.geolocation

    • navigator.getBattery()

  2. Screen: Proporciona información sobre la pantalla del usuario.

    • screen.width

    • screen.height

    • screen.availWidth

    • screen.availHeight

  3. History: Permite la manipulación del historial del navegador.

    • history.back()

    • history.forward()

    • history.go()

  4. Location: Proporciona la URL actual de la ventana.

    • location.href

    • location.hostname

    • location.pathname

    • location.search

    • location.hash

    • location.reload()

  5. Storage APIs: LocalStorage, sessionStorage, IndexedDB

  6. Network APIs: XMLHttpRequest, fetch

  7. Console: Proporciona acceso a la consola de depuración del navegador.

  8. WebSocket: Proporciona una interfaz para las conexiones WebSocket.

  9. Worker: Permite la ejecución de scripts en segundo plano.

DOM: 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 un setTimeout 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.

DOM: Buscar Nodos#

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 un Nodelist 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 a firstElementChild, pero retorna el último hijo elemento HTML.

  • elemento.lastChild: similar a firstChild, 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 a nextElementSibling, pero retorna el hermano anterior que es un elemento HTML.

  • elemento.previousSibling: similar a nextSibling, 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.

DOM: Modificar Nodos#

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.

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();

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 Inputo 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", "");

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 como value 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.

DOM: 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.

Creación de elementos (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, extraido 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.

function htmlEscape(str) {
  return str.replace(/&/g, '&amp;') // first!
            .replace(/>/g, '&gt;')
            .replace(/</g, '&lt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#39;')
            .replace(/`/g, '&#96;');
}
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>&lt;Jane&gt;</td></tr>
        <tr><td>Bond</td></tr>
    
        <tr><td>Lars</td></tr>
        <tr><td>&lt;Croft&gt;</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 los custom 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.

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 defera 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 o load porque al ser ejecutado al principio, ya hay cosas que se puede ir ejecutando sin necesidad de DOM antes de que cargue totalmente. En cualquier caso, los script en Módulos siempre se ejecutan en modo defer.

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

Formularios en JavaScript#

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.

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

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>

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>

Validación y estilo#

En caso de que un campo no sea válido, se puede manipular las clases CSS o mostrar un mensaje de error. 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.

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 con fetch usando el método POST.

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étodo POST y el encabezado Content-Type establecido como application/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.ipynb 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 hereda de Blob y, por lo tanto, tiene todos los atributos y métodos de un Blob, además de algunos atributos específicos para los archivos. Aquí tenemos una lista de los atributos principales del objeto File y su utilidad:

  1. name

    • Descripción: El nombre del archivo, incluido su extensión.

    • Utilidad: Utilizado para mostrar o manipular el nombre del archivo. Por ejemplo, se puede mostrar al usuario el nombre del archivo que se ha seleccionado o utilizarlo para enviarlo al servidor.

  2. lastModified

    • Descripción: El timestamp (en milisegundos desde el 1 de enero de 1970) de la última vez que el archivo fue modificado.

    • Utilidad: Permite conocer la fecha y hora de la última modificación del archivo. Puede ser útil para sincronización, control de versiones o simplemente para mostrar esta información al usuario.

  3. lastModifiedDate (obsoleto, usar lastModified en su lugar)

    • Descripción: Un objeto Date que representa la fecha de la última modificación del archivo.

    • Utilidad: Similar a lastModified, pero como un objeto Date. Este atributo está obsoleto y no se recomienda su uso.

  4. size

    • Descripción: El tamaño del archivo en bytes.

    • Utilidad: Permite conocer el tamaño del archivo. Es útil para validar si el archivo cumple con los requisitos de tamaño antes de subirlo o para mostrar esta información al usuario.

  5. type

    • Descripción: El tipo MIME del archivo (por ejemplo, “image/png” o “application/pdf”).

    • Utilidad: Utilizado para determinar el tipo de contenido del archivo. Esto es útil para validar el tipo de archivo que se ha seleccionado, para decidir cómo procesarlo o para enviarlo al servidor con el tipo correcto.

El objeto File también hereda todos los métodos de Blob, lo que permite manipular el contenido del archivo de varias maneras. Algunos de estos métodos incluyen:

  1. slice([start[, end[, contentType]]])

    • Descripción: Crea un nuevo Blob representando una porción del archivo original.

    • Utilidad: Permite trabajar con una parte específica del archivo, lo cual puede ser útil para subir archivos en partes o para manipular solo una sección del archivo.

  2. text()

    • Descripción: Devuelve una promesa que se resuelve con el contenido del Blob como una cadena de texto.

    • Utilidad: Permite leer el contenido de un archivo como texto, lo que es útil para archivos de texto, CSV, JSON, etc.

  3. arrayBuffer()

    • Descripción: Devuelve una promesa que se resuelve con el contenido del Blob como un ArrayBuffer.

    • Utilidad: Útil para trabajar con datos binarios de bajo nivel.

  4. stream()

    • Descripción: Devuelve un ReadableStream del contenido del Blob.

    • Utilidad: Permite trabajar con el contenido del archivo como un flujo de datos, lo que puede ser útil para grandes archivos que necesitan ser procesados por partes.

El objeto FileReader#

El objeto FileReader en JavaScript proporciona una forma de leer archivos de forma asíncrona desde el cliente, utilizando el API File de HTML5. Estos son los principales atributos y métodos del objeto FileReader:

  1. readyState:

    • Descripción: Indica el estado actual de la operación de lectura del archivo.

    • Valores posibles:

      • EMPTY (0): Objeto recién creado, sin archivo asignado.

      • LOADING (1): Archivo está siendo leído.

      • DONE (2): Lectura del archivo completada correctamente.

    • Uso: Puede ser útil para controlar el flujo de trabajo y saber cuándo ha finalizado la lectura del archivo.

  2. result:

    • Descripción: Contiene los datos del archivo leído, representados como una cadena de caracteres o como un ArrayBuffer, dependiendo del método utilizado para leer el archivo.

    • Uso: Después de que la lectura del archivo sea exitosa, este atributo contiene los datos del archivo en el formato especificado.

Métodos del objeto FileReader

  1. readAsArrayBuffer(file):

    • Descripción: Lee el contenido del archivo como un ArrayBuffer.

    • Uso: Útil cuando se trabaja con datos binarios, como imágenes o archivos PDF.

  2. readAsBinaryString(file):

    • Descripción: Lee el contenido del archivo como una cadena binaria (String).

    • Uso: Aunque está en desuso, puede ser útil para leer archivos en formatos antiguos que no son compatibles con readAsText.

  3. readAsDataURL(file):

    • Descripción: Lee el contenido del archivo y devuelve una URL de datos (data URL) que representa los datos del archivo.

    • Uso: Muy común para leer imágenes y otros tipos de archivos que pueden ser representados como URLs de datos en el navegador.

  4. readAsText(file, encoding):

    • Descripción: Lee el contenido del archivo como texto (String), utilizando una codificación opcional.

    • Uso: Ideal para archivos de texto como archivos de configuración o documentos de texto plano.

  5. abort():

    • Descripción: Cancela la operación de lectura del archivo en curso.

    • Uso: Útil si se desea interrumpir la lectura de un archivo antes de que se complete, por ejemplo, en respuesta a una acción del usuario.

  • Asincronía: Todas las operaciones de lectura del FileReader son asíncronas, lo que significa que se debe manejar el resultado (o error) en los callbacks adecuados (onload, onerror).

  • Seguridad: Debido a las políticas de seguridad del navegador, la lectura de archivos locales puede estar limitada. Es importante entender y respetar estas limitaciones al desarrollar aplicaciones web que interactúan con archivos del cliente.

Eventos y Handlers#

Los eventos y los manejadores (handlers) son elementos para capturar interacciones del usuario y realizar acciones en respuesta a estas interacciones.

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.

Eventos (Events)#

Los eventos son mecanismos que se activan cuando el usuario interactúa con la página web. Estas interacciones pueden ser clics del ratón, pulsaciones de teclado, desplazamientos, cambios en el tamaño de la ventana, etc. Los eventos permiten que JavaScript responda dinámicamente a las acciones del usuario.

Características principales:

  • Son disparados por el navegador o por el usuario.

  • Pueden estar relacionados con elementos específicos del DOM, como un botón o un campo de entrada.

  • El objeto Event contiene información detallada sobre el evento que ocurrió, como qué elemento lo originó y detalles específicos del tipo de evento.

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.

Características principales:

  • Es una función que se asigna a un evento particular en un elemento del DOM.

  • Define la acción o comportamiento que se debe llevar a cabo cuando el evento ocurre.

  • Los manejadores pueden ser definidos directamente en el HTML, usando atributos como onclick, o pueden ser asignados dinámicamente desde JavaScript.

Ejemplo de uso en HTML y JavaScript#

<!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);

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 y mantenible, 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 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.

La propagación de eventos tiene dos fases principales:

  1. Capturing Phase: La fase en la que el evento se propaga desde el documento raíz hasta el objetivo del evento.

  2. 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#

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:

  1. Elemento hijo (child): Cuando se hace clic en el botón, se dispara un evento personalizado childEvent con algunos datos en la propiedad detail.

  2. Elemento padre (parent): El padre escucha el evento childEvent 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.

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);
});

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.