DOM (Document Object Model)


¿Qué es el DOM?

El DOM (Document Object Model) es una representación estructurada en memoria de un documento HTML o XML que el navegador crea cuando carga una página web. Funciona como un puente entre el HTML estático y JavaScript dinámico, convirtiendo cada elemento, atributo y texto en objetos que pueden ser manipulados programáticamente.

¿Para qué se utiliza el DOM?

El DOM es utilizado por desarrolladores para:

  • Acceder y modificar contenido de páginas web en tiempo real.
  • Cambiar estilos, atributos y propiedades de elementos HTML.
  • Responder a eventos del usuario como clics, teclado y movimientos del mouse.
  • Crear, insertar y eliminar elementos dinámicamente.
  • Implementar interfaces de usuario interactivas y aplicaciones web modernas.
  • Construir single-page applications (SPAs) y aplicaciones reactivas.

¿Cómo funciona?

El DOM funciona como un árbol genealógico de tu página web - cada elemento HTML es un nodo que puede tener padres, hijos y hermanos. JavaScript puede navegar por este árbol familiar, modificar a cualquier miembro, o incluso añadir nuevos miembros a la familia en tiempo real.

Estructura del DOM: el árbol de nodos

El DOM convierte HTML en una jerarquía de objetos:

<!DOCTYPE html>
<html>
  <head>
    <title>Mi Página</title>
  </head>
  <body>
    <div id="container">
      <h1>Título Principal</h1>
      <p class="intro">Párrafo de introducción</p>
      <ul>
        <li>Elemento 1</li>
        <li>Elemento 2</li>
      </ul>
    </div>
  </body>
</html>

Se convierte en esta estructura de árbol DOM:

Document
└── html (HTMLHtmlElement)
    ├── head (HTMLHeadElement)
    │   └── title (HTMLTitleElement)
    │       └── "Mi Página" (Text Node)
    └── body (HTMLBodyElement)
        └── div#container (HTMLDivElement)
            ├── h1 (HTMLHeadingElement)
            │   └── "Título Principal" (Text Node)
            ├── p.intro (HTMLParagraphElement)
            │   └── "Párrafo de introducción" (Text Node)
            └── ul (HTMLUListElement)
                ├── li (HTMLLIElement)
                │   └── "Elemento 1" (Text Node)
                └── li (HTMLLIElement)
                    └── "Elemento 2" (Text Node)

Tipos de nodos en el DOM

// Los diferentes tipos de nodos DOM
const nodeTypes = {
  ELEMENT_NODE: 1, // <div>, <p>, <span>, etc.
  ATTRIBUTE_NODE: 2, // class="example", id="main"
  TEXT_NODE: 3, // Contenido de texto
  CDATA_SECTION_NODE: 4, // <![CDATA[...]]>
  PROCESSING_INSTRUCTION_NODE: 7, // <?xml-stylesheet...?>
  COMMENT_NODE: 8, // <!-- comentario -->
  DOCUMENT_NODE: 9, // document
  DOCUMENT_TYPE_NODE: 10, // <!DOCTYPE html>
  DOCUMENT_FRAGMENT_NODE: 11, // DocumentFragment
};

// Ejemplo de inspección de nodos
function inspectNode(node) {
  console.log('Tipo de nodo:', node.nodeType);
  console.log('Nombre del nodo:', node.nodeName);
  console.log('Valor del nodo:', node.nodeValue);

  switch (node.nodeType) {
    case Node.ELEMENT_NODE:
      console.log('Es un elemento:', node.tagName);
      console.log(
        'Atributos:',
        [...node.attributes].map((attr) => `${attr.name}="${attr.value}"`)
      );
      break;
    case Node.TEXT_NODE:
      console.log('Es texto:', `"${node.textContent}"`);
      break;
    case Node.COMMENT_NODE:
      console.log('Es comentario:', node.data);
      break;
  }
}

// Uso
const element = document.getElementById('container');
inspectNode(element);

Selección de elementos: encontrar lo que necesitas

// Métodos de selección básicos
const byId = document.getElementById('container');
const byClass = document.getElementsByClassName('intro');
const byTag = document.getElementsByTagName('p');
const byName = document.getElementsByName('username');

// Selectores CSS modernos (más poderosos)
const single = document.querySelector('#container .intro');
const multiple = document.querySelectorAll('li');
const complex = document.querySelectorAll('div[data-active="true"] > p:first-child');

// Navegación por el árbol DOM
const element = document.querySelector('#container');

// Relaciones familiares
console.log('Padre:', element.parentNode);
console.log('Hijos:', element.children); // Solo elementos
console.log('Todos los nodos hijos:', element.childNodes); // Incluye texto
console.log('Primer hijo:', element.firstElementChild);
console.log('Último hijo:', element.lastElementChild);
console.log('Hermano siguiente:', element.nextElementSibling);
console.log('Hermano anterior:', element.previousElementSibling);

// Búsqueda hacia arriba en el árbol
const closest = element.closest('.parent-class'); // Busca ancestro
const matches = element.matches('.some-class'); // Verifica si coincide

// Función helper para navegación
function findAncestor(element, selector) {
  while (element && element !== document) {
    if (element.matches && element.matches(selector)) {
      return element;
    }
    element = element.parentNode;
  }
  return null;
}

// Selección avanzada con filtros
function selectElements(selector, filterFn) {
  const elements = document.querySelectorAll(selector);
  return Array.from(elements).filter(filterFn);
}

// Ejemplos de uso
const visibleParagraphs = selectElements('p', (el) => el.offsetWidth > 0 && el.offsetHeight > 0);

const elementsWithText = selectElements('div', (el) => el.textContent.trim().length > 0);

Manipulación de contenido

// Modificar contenido de texto
const heading = document.querySelector('h1');

// Diferentes formas de cambiar contenido
heading.textContent = 'Nuevo título'; // Solo texto, seguro contra XSS
heading.innerHTML = '<em>Título enfatizado</em>'; // HTML, cuidado con XSS
heading.innerText = 'Texto visible'; // Respeta CSS (display, visibility)

// Obtener vs establecer
console.log('Contenido:', heading.textContent);
console.log('HTML interno:', heading.innerHTML);
console.log('Texto visible:', heading.innerText);

// Manipulación segura de HTML
function safeSetHTML(element, htmlString) {
  // Crear un contenedor temporal
  const temp = document.createElement('div');
  temp.innerHTML = htmlString;

  // Limpiar scripts potencialmente maliciosos
  const scripts = temp.querySelectorAll('script');
  scripts.forEach((script) => script.remove());

  // Establecer contenido limpio
  element.innerHTML = temp.innerHTML;
}

// Trabajar con atributos
const input = document.querySelector('input[type="text"]');

// Atributos estándar
input.setAttribute('placeholder', 'Escribe algo...');
input.setAttribute('maxlength', '100');
const value = input.getAttribute('placeholder');
input.removeAttribute('disabled');

// Propiedades vs atributos
input.value = 'Valor en la propiedad'; // Propiedad (estado actual)
input.setAttribute('value', 'Valor inicial'); // Atributo (valor inicial)

// Data attributes
input.dataset.userId = '123'; // data-user-id="123"
input.dataset.lastModified = Date.now(); // data-last-modified="..."
console.log(input.dataset.userId); // Acceso a data attributes

// Clases CSS
const element = document.querySelector('.example');

// Manipulación de clases
element.classList.add('new-class');
element.classList.remove('old-class');
element.classList.toggle('active'); // Añade si no existe, quita si existe
element.classList.replace('old', 'new');
const hasClass = element.classList.contains('active');

// Múltiples clases
element.classList.add('class1', 'class2', 'class3');
element.classList.remove('class1', 'class2');

// Estilos inline
element.style.color = 'red';
element.style.backgroundColor = 'yellow';
element.style.fontSize = '18px';

// Establecer múltiples estilos
Object.assign(element.style, {
  width: '200px',
  height: '100px',
  border: '1px solid black',
  borderRadius: '5px',
});

// Obtener estilos computados
const computedStyle = window.getComputedStyle(element);
console.log('Color computado:', computedStyle.color);
console.log('Ancho computado:', computedStyle.width);

Creación y inserción de elementos

// Crear nuevos elementos
const newDiv = document.createElement('div');
const newText = document.createTextNode('Hola mundo');
const newComment = document.createComment('Este es un comentario');

// Configurar el nuevo elemento
newDiv.textContent = 'Contenido del div';
newDiv.className = 'new-element';
newDiv.id = 'dynamic-element';

// Métodos de inserción
const container = document.getElementById('container');

// Insertar al final
container.appendChild(newDiv);

// Insertar antes de un elemento específico
const referenceElement = container.querySelector('.intro');
container.insertBefore(newDiv, referenceElement);

// Métodos modernos de inserción (más flexibles)
const newElement = document.createElement('p');
newElement.textContent = 'Párrafo insertado';

// Insertar en posiciones específicas
container.insertAdjacentElement('beforebegin', newElement); // Antes del container
container.insertAdjacentElement('afterbegin', newElement); // Primer hijo
container.insertAdjacentElement('beforeend', newElement); // Último hijo
container.insertAdjacentElement('afterend', newElement); // Después del container

// Insertar HTML directamente
container.insertAdjacentHTML('beforeend', '<p>HTML insertado</p>');

// Crear elementos complejos de forma eficiente
function createElement(tag, attributes = {}, children = []) {
  const element = document.createElement(tag);

  // Establecer atributos
  Object.entries(attributes).forEach(([key, value]) => {
    if (key === 'textContent') {
      element.textContent = value;
    } else if (key === 'innerHTML') {
      element.innerHTML = value;
    } else if (key === 'dataset') {
      Object.entries(value).forEach(([dataKey, dataValue]) => {
        element.dataset[dataKey] = dataValue;
      });
    } else {
      element.setAttribute(key, value);
    }
  });

  // Añadir hijos
  children.forEach((child) => {
    if (typeof child === 'string') {
      element.appendChild(document.createTextNode(child));
    } else {
      element.appendChild(child);
    }
  });

  return element;
}

// Uso del helper
const complexElement = createElement(
  'div',
  {
    class: 'card',
    id: 'user-card',
    dataset: { userId: '123', role: 'admin' },
  },
  [
    createElement('h3', { textContent: 'Usuario Admin' }),
    createElement('p', { textContent: 'Descripción del usuario...' }),
    createElement('button', {
      textContent: 'Editar',
      class: 'btn btn-primary',
    }),
  ]
);

document.body.appendChild(complexElement);

// Document Fragments para inserciones eficientes
const fragment = document.createDocumentFragment();

// Crear múltiples elementos
for (let i = 0; i < 1000; i++) {
  const item = document.createElement('div');
  item.textContent = `Item ${i}`;
  fragment.appendChild(item); // No causa reflow
}

// Una sola inserción en el DOM (eficiente)
container.appendChild(fragment);

Eliminación y reemplazo de elementos

// Métodos de eliminación
const elementToRemove = document.querySelector('.to-remove');

// Eliminar elemento (método moderno)
elementToRemove.remove();

// Método clásico (eliminar desde el padre)
elementToRemove.parentNode.removeChild(elementToRemove);

// Eliminar todos los hijos
const parent = document.querySelector('.parent');
while (parent.firstChild) {
  parent.removeChild(parent.firstChild);
}

// Método más eficiente para limpiar contenido
parent.innerHTML = ''; // Rápido pero no ejecuta cleanup
parent.textContent = ''; // Solo para contenido de texto

// Reemplazo de elementos
const oldElement = document.querySelector('.old');
const newElement = document.createElement('div');
newElement.textContent = 'Elemento nuevo';
newElement.className = 'new';

// Reemplazar elemento
oldElement.parentNode.replaceChild(newElement, oldElement);

// Método moderno
oldElement.replaceWith(newElement);

// Función helper para reemplazo seguro
function replaceElement(oldElement, newElement) {
  if (oldElement && oldElement.parentNode) {
    oldElement.parentNode.replaceChild(newElement, oldElement);
    return true;
  }
  return false;
}

// Clonación de elementos
const original = document.querySelector('.template');
const clone = original.cloneNode(true); // true = deep clone (incluye hijos)
const shallowClone = original.cloneNode(false); // solo el elemento

// Modificar clon antes de insertar
clone.id = 'cloned-element';
clone.querySelector('.title').textContent = 'Título clonado';
document.body.appendChild(clone);

Eventos del DOM: interactividad

// Event listeners básicos
const button = document.querySelector('#my-button');

// Agregar evento
button.addEventListener('click', function (event) {
  console.log('Botón clickeado!');
  console.log('Elemento que disparó el evento:', event.target);
  console.log('Elemento que escucha el evento:', event.currentTarget);
});

// Diferentes tipos de eventos
const input = document.querySelector('#my-input');

input.addEventListener('focus', () => console.log('Input enfocado'));
input.addEventListener('blur', () => console.log('Input desenfocado'));
input.addEventListener('input', (e) => console.log('Valor:', e.target.value));
input.addEventListener('keydown', (e) => {
  if (e.key === 'Enter') {
    console.log('Enter presionado');
  }
});

// Event delegation (delegación de eventos)
const list = document.querySelector('#dynamic-list');

// En lugar de agregar eventos a cada elemento
list.addEventListener('click', function (event) {
  // Verificar si el elemento clickeado es un item de lista
  if (event.target.matches('.list-item')) {
    console.log('Item clickeado:', event.target.textContent);
  }

  // O usar closest para elementos anidados
  const listItem = event.target.closest('.list-item');
  if (listItem) {
    console.log('Item o hijo clickeado:', listItem.textContent);
  }
});

// Remover event listeners
function handleClick(event) {
  console.log('Click manejado');
}

button.addEventListener('click', handleClick);
button.removeEventListener('click', handleClick); // Se requiere la misma función

// Eventos con opciones
button.addEventListener('click', handleClick, {
  once: true, // Se ejecuta solo una vez
  passive: true, // No llamará preventDefault()
  capture: true, // Captura en fase de captura
});

// Prevenir comportamiento por defecto
const form = document.querySelector('#my-form');
form.addEventListener('submit', function (event) {
  event.preventDefault(); // Previene envío del formulario

  // Validación personalizada
  const formData = new FormData(form);
  console.log('Datos del formulario:', Object.fromEntries(formData));
});

// Custom events (eventos personalizados)
const customEvent = new CustomEvent('userAction', {
  detail: {
    userId: 123,
    action: 'login',
    timestamp: Date.now(),
  },
  bubbles: true,
  cancelable: true,
});

// Disparar evento personalizado
document.dispatchEvent(customEvent);

// Escuchar evento personalizado
document.addEventListener('userAction', function (event) {
  console.log('Acción del usuario:', event.detail);
});

Manipulación avanzada del DOM

// Observers para cambios en el DOM
const observer = new MutationObserver(function (mutations) {
  mutations.forEach(function (mutation) {
    console.log('Tipo de mutación:', mutation.type);

    if (mutation.type === 'childList') {
      console.log('Nodos añadidos:', mutation.addedNodes);
      console.log('Nodos removidos:', mutation.removedNodes);
    } else if (mutation.type === 'attributes') {
      console.log('Atributo cambiado:', mutation.attributeName);
    }
  });
});

// Observar cambios
const targetNode = document.getElementById('observed-element');
observer.observe(targetNode, {
  childList: true, // Observar cambios en hijos
  subtree: true, // Observar todo el subárbol
  attributes: true, // Observar cambios de atributos
  attributeOldValue: true, // Guardar valor anterior
  characterData: true, // Observar cambios en texto
});

// Detener observación
// observer.disconnect();

// Intersection Observer para elementos visibles
const intersectionObserver = new IntersectionObserver(
  function (entries) {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        console.log('Elemento visible:', entry.target);
        entry.target.classList.add('animate-in');
      } else {
        entry.target.classList.remove('animate-in');
      }
    });
  },
  {
    threshold: 0.5, // 50% del elemento debe ser visible
    rootMargin: '10px', // Margen adicional
  }
);

// Observar elementos para lazy loading
document.querySelectorAll('.lazy-load').forEach((el) => {
  intersectionObserver.observe(el);
});

// Resize Observer para cambios de tamaño
const resizeObserver = new ResizeObserver(function (entries) {
  entries.forEach((entry) => {
    console.log('Nuevo tamaño:', entry.contentRect.width, 'x', entry.contentRect.height);

    // Ajustar layout basado en el tamaño
    const element = entry.target;
    if (entry.contentRect.width < 300) {
      element.classList.add('compact');
    } else {
      element.classList.remove('compact');
    }
  });
});

resizeObserver.observe(document.querySelector('#responsive-element'));

Performance y optimización del DOM

// Batch DOM operations para mejor rendimiento
function optimizedDOMUpdates() {
  const container = document.getElementById('container');

  // ❌ Malo: múltiples reflows
  for (let i = 0; i < 1000; i++) {
    const div = document.createElement('div');
    div.textContent = `Item ${i}`;
    container.appendChild(div); // Reflow en cada inserción
  }

  // ✅ Bueno: una sola inserción
  const fragment = document.createDocumentFragment();
  for (let i = 0; i < 1000; i++) {
    const div = document.createElement('div');
    div.textContent = `Item ${i}`;
    fragment.appendChild(div);
  }
  container.appendChild(fragment); // Un solo reflow
}

// Debounce para eventos frecuentes
function debounce(func, wait) {
  let timeout;
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

// Aplicar debounce a scroll/resize
window.addEventListener(
  'scroll',
  debounce(function () {
    console.log('Scroll event processed');
  }, 100)
);

// RequestAnimationFrame para animaciones suaves
function animateElement(element, duration = 1000) {
  const start = performance.now();
  const initialOpacity = 0;
  const finalOpacity = 1;

  function animate(currentTime) {
    const elapsed = currentTime - start;
    const progress = Math.min(elapsed / duration, 1);

    // Interpolación lineal
    const opacity = initialOpacity + (finalOpacity - initialOpacity) * progress;
    element.style.opacity = opacity;

    if (progress < 1) {
      requestAnimationFrame(animate);
    }
  }

  requestAnimationFrame(animate);
}

// Virtual DOM simple (concepto básico)
class VirtualDOM {
  constructor() {
    this.oldTree = null;
  }

  createElement(tag, props, ...children) {
    return {
      tag,
      props: props || {},
      children: children.flat(),
    };
  }

  render(vdom, container) {
    if (this.oldTree) {
      this.updateElement(container, vdom, this.oldTree);
    } else {
      this.createDOMElement(vdom, container);
    }
    this.oldTree = vdom;
  }

  createDOMElement(vdom) {
    if (typeof vdom === 'string' || typeof vdom === 'number') {
      return document.createTextNode(vdom);
    }

    const element = document.createElement(vdom.tag);

    Object.entries(vdom.props).forEach(([key, value]) => {
      element.setAttribute(key, value);
    });

    vdom.children.forEach((child) => {
      element.appendChild(this.createDOMElement(child));
    });

    return element;
  }

  updateElement(parent, newNode, oldNode, index = 0) {
    // Implementación básica de diffing
    if (!oldNode) {
      parent.appendChild(this.createDOMElement(newNode));
    } else if (!newNode) {
      parent.removeChild(parent.childNodes[index]);
    } else if (this.hasChanged(newNode, oldNode)) {
      parent.replaceChild(this.createDOMElement(newNode), parent.childNodes[index]);
    } else if (newNode.tag) {
      const newLength = newNode.children.length;
      const oldLength = oldNode.children.length;

      for (let i = 0; i < Math.max(newLength, oldLength); i++) {
        this.updateElement(parent.childNodes[index], newNode.children[i], oldNode.children[i], i);
      }
    }
  }

  hasChanged(node1, node2) {
    return (
      typeof node1 !== typeof node2 ||
      (typeof node1 === 'string' && node1 !== node2) ||
      node1.tag !== node2.tag
    );
  }
}

// Uso del Virtual DOM
const vdom = new VirtualDOM();
const app = document.getElementById('app');

// Renderizar componente inicial
vdom.render(
  vdom.createElement(
    'div',
    { class: 'container' },
    vdom.createElement('h1', {}, 'Mi App'),
    vdom.createElement('p', {}, 'Contenido dinámico')
  ),
  app
);

Herramientas de debugging para DOM

// Utilidades de debugging
const DOMDebugger = {
  // Encontrar todos los event listeners
  getEventListeners(element) {
    return getEventListeners(element); // Chrome DevTools
  },

  // Resaltar elemento
  highlight(element, color = 'red') {
    const originalOutline = element.style.outline;
    element.style.outline = `2px solid ${color}`;

    setTimeout(() => {
      element.style.outline = originalOutline;
    }, 2000);
  },

  // Información detallada del elemento
  inspectElement(element) {
    console.group('DOM Element Inspection');
    console.log('Element:', element);
    console.log('Tag Name:', element.tagName);
    console.log('ID:', element.id);
    console.log('Classes:', [...element.classList]);
    console.log(
      'Attributes:',
      [...element.attributes].map((attr) => `${attr.name}="${attr.value}"`)
    );
    console.log('Computed Style:', window.getComputedStyle(element));
    console.log('Parent:', element.parentElement);
    console.log('Children:', [...element.children]);
    console.log('Siblings:', this.getSiblings(element));
    console.groupEnd();
  },

  getSiblings(element) {
    return [...element.parentElement.children].filter((child) => child !== element);
  },

  // Monitorear cambios en elemento
  watchElement(element, callback) {
    const observer = new MutationObserver(callback);
    observer.observe(element, {
      attributes: true,
      childList: true,
      subtree: true,
      characterData: true,
    });
    return observer;
  },

  // Medir performance de operaciones DOM
  measureDOM(operation, description = 'DOM Operation') {
    const start = performance.now();
    operation();
    const end = performance.now();
    console.log(`${description} took ${end - start} milliseconds`);
  },
};

// Ejemplos de uso
const element = document.querySelector('#test-element');

// Inspeccionar elemento
DOMDebugger.inspectElement(element);

// Resaltar elemento
DOMDebugger.highlight(element, 'blue');

// Monitorear cambios
const observer = DOMDebugger.watchElement(element, function (mutations) {
  console.log('Element changed:', mutations);
});

// Medir rendimiento
DOMDebugger.measureDOM(() => {
  for (let i = 0; i < 1000; i++) {
    const div = document.createElement('div');
    document.body.appendChild(div);
    document.body.removeChild(div);
  }
}, 'Create/Remove 1000 elements');

Mejores prácticas y patrones comunes

// Patrón Module para encapsular funcionalidad DOM
const DOMUtils = (function () {
  'use strict';

  // Funciones privadas
  function isElement(obj) {
    return obj instanceof Element || obj instanceof HTMLDocument;
  }

  function sanitizeHTML(str) {
    const temp = document.createElement('div');
    temp.textContent = str;
    return temp.innerHTML;
  }

  // API pública
  return {
    // Selección mejorada
    $(selector, context = document) {
      return context.querySelector(selector);
    },

    $$(selector, context = document) {
      return Array.from(context.querySelectorAll(selector));
    },

    // Creación de elementos con configuración
    create(tag, config = {}) {
      const element = document.createElement(tag);

      Object.entries(config).forEach(([key, value]) => {
        switch (key) {
          case 'text':
            element.textContent = value;
            break;
          case 'html':
            element.innerHTML = sanitizeHTML(value);
            break;
          case 'classes':
            element.classList.add(...(Array.isArray(value) ? value : [value]));
            break;
          case 'data':
            Object.entries(value).forEach(([dataKey, dataValue]) => {
              element.dataset[dataKey] = dataValue;
            });
            break;
          case 'events':
            Object.entries(value).forEach(([event, handler]) => {
              element.addEventListener(event, handler);
            });
            break;
          default:
            element.setAttribute(key, value);
        }
      });

      return element;
    },

    // Manipulación de clases mejorada
    toggleClasses(element, ...classes) {
      classes.forEach((cls) => element.classList.toggle(cls));
    },

    // Event delegation helper
    delegate(parent, selector, event, handler) {
      parent.addEventListener(event, function (e) {
        const target = e.target.closest(selector);
        if (target) {
          handler.call(target, e);
        }
      });
    },

    // Animaciones simples
    fadeIn(element, duration = 300) {
      element.style.opacity = '0';
      element.style.display = 'block';

      const start = performance.now();

      function animate(timestamp) {
        const progress = (timestamp - start) / duration;
        element.style.opacity = Math.min(progress, 1);

        if (progress < 1) {
          requestAnimationFrame(animate);
        }
      }

      requestAnimationFrame(animate);
    },

    fadeOut(element, duration = 300) {
      const start = performance.now();
      const initialOpacity = parseFloat(window.getComputedStyle(element).opacity);

      function animate(timestamp) {
        const progress = (timestamp - start) / duration;
        element.style.opacity = initialOpacity * (1 - Math.min(progress, 1));

        if (progress < 1) {
          requestAnimationFrame(animate);
        } else {
          element.style.display = 'none';
        }
      }

      requestAnimationFrame(animate);
    },
  };
})();

// Uso de DOMUtils
const container = DOMUtils.$('#container');

const newButton = DOMUtils.create('button', {
  text: 'Click me!',
  classes: ['btn', 'btn-primary'],
  data: { action: 'submit', userId: '123' },
  events: {
    click: function (e) {
      console.log('Button clicked!', this.dataset);
    },
  },
});

container.appendChild(newButton);

// Event delegation
DOMUtils.delegate(document.body, '.dynamic-button', 'click', function (e) {
  console.log('Dynamic button clicked:', this);
});

Integración con frameworks modernos

// Interoperabilidad con React (cuando necesites DOM directo)
function useDirectDOM(callback, deps) {
  const ref = useRef();

  useEffect(() => {
    if (ref.current) {
      callback(ref.current);
    }
  }, deps);

  return ref;
}

// Componente React que usa DOM directo
function ChartComponent({ data }) {
  const chartRef = useDirectDOM(
    (element) => {
      // Limpiar chart anterior
      element.innerHTML = '';

      // Crear chart con librería que requiere DOM directo
      const chart = new SomeChartLibrary(element);
      chart.render(data);

      // Cleanup
      return () => chart.destroy();
    },
    [data]
  );

  return <div ref={chartRef} className="chart-container" />;
}

// Custom hooks para DOM
function useElementSize(ref) {
  const [size, setSize] = useState({ width: 0, height: 0 });

  useEffect(() => {
    if (!ref.current) return;

    const resizeObserver = new ResizeObserver((entries) => {
      const { width, height } = entries[0].contentRect;
      setSize({ width, height });
    });

    resizeObserver.observe(ref.current);

    return () => resizeObserver.disconnect();
  }, [ref]);

  return size;
}

function useIntersection(ref, options) {
  const [isIntersecting, setIsIntersecting] = useState(false);

  useEffect(() => {
    if (!ref.current) return;

    const observer = new IntersectionObserver(([entry]) => {
      setIsIntersecting(entry.isIntersecting);
    }, options);

    observer.observe(ref.current);

    return () => observer.disconnect();
  }, [ref, options]);

  return isIntersecting;
}

Conclusión

El DOM es el corazón de la interactividad web - es el puente que conecta tu HTML estático con la magia dinámica de JavaScript. Dominarlo significa tener el poder de crear experiencias web verdaderamente interactivas y responsivas.

Principios clave para el DOM efectivo:

  1. Selecciona eficientemente - Usa los selectores más específicos y rápidos posibles
  2. Minimiza los reflows - Agrupa las operaciones DOM para mejor rendimiento
  3. Usa event delegation - Un listener en el padre es mejor que cientos en los hijos
  4. Observa cambios inteligentemente - MutationObserver, IntersectionObserver, ResizeObserver
  5. Sanitiza siempre - Nunca confíes en contenido externo sin limpiar

El DOM moderno no es solo manipulación - es sobre observación, reactividad y performance. Con APIs como los Observers y requestAnimationFrame, puedes crear experiencias que rivalizan con aplicaciones nativas.

Recuerda: Cada cambio en el DOM tiene un costo. La diferencia entre un desarrollador novato y uno experto está en saber cuándo y cómo tocar el DOM. A veces la mejor manipulación del DOM es la que no haces.

El DOM es tu lienzo digital - úsalo sabiamente para pintar experiencias que deleiten a tus usuarios sin sacrificar performance.