Comunicación con documentos embebidos (JavaScript)

TEORÍA

En ocasiones, una página HTML puede contener sub-secciones dentro de algún «pop up» (también llamado «ventana modal» o «div modal»).

Lo normal es que este pop up sea otra página HTML embebida dentro de un iframe de la página HTML padre para que el contenido sea reutilizable en otros lugares de la aplicación Web.

En HTML tendríamos un iframe que contiene el documento HTML embebido:

<!-- padre.html -->
  <div class="popupBackground">
    <div class="popup">
    <!-- El iframe contiene al documento HTML hijo.html y lo usa como ventana modal -->
      <iframe id="dochijo" src="hijo.html" type="text/html"></iframe>
  </div>
</div>

Y podríamos acceder al DOM del documento embebido desde el documento padre a través de JavaScript:

//Después de "window.onload"
var iframeHijo = document.getElementById(dochijo);
var docHijo = iframeHijo.contentDocument || iframeHijo.contentWindow.document;

Pero el problema viene cuando el propio pop up tiene código javascript que puede/debe interactuar con el código de la página padre (y viceversa).

Como el código JS no se comparte para los dos documentos (el documento padre tiene unas variables y unas funciones y el documento embebido tiene las suyas propias) lo habitual que se hace al principio cuando nos enfrentamos a este problema, es llamar directamente a la función que se encuentra en la página padre. Por ejemplo:

//Padre.js
function foo(){...}

//Hijo.js
parent.foo();

La idea de llamar a «parent.foo()» es avisarle al padre de que el pop up ha modificado algo que puede afectar a la vista o a las variables JS de padre.html (por ejemplo, desde el pop up se ha cambiado el valor de un atributo que se muestra también en padre.html).

Pero ¿Qué ocurre si la página padre no tiene ese método implementado? ¿Y si dicha función se llama de otra manera en otros archivos HTML que hagan uso del pop up? Puede incluso que dicha función no exista porque el padre no necesite reaccionar a lo que haya creado/cambiado el código JS del pop up (cosa que también ocurriría si se usa el propio código HTML del pop up como el documento principal de la página HTML).

Un ejemplo, en la página HTML padre tenemos una tabla con nombres de clientes. Para modificar un cliente se abre un pop up con los datos completos del mismo y sólo se puede modificar desde ahí el nombre del cliente. Como es lógico también debe actualizarse el nombre del cliente en la tabla de la página padre.

Pero luego, en otra parte de la aplicación Web, queremos tener la ficha del cliente sin más; es decir, usar la página HTML del pop up como si fuese la página padre directamente. En ese caso, cuando llamemos a la función padre que modificaba la fila de los clientes va a producirse un error.

Podemos preguntar si existe esa función para que no falle, pero igualmente, el pop up puede estar en 50 páginas más, y en todas ellas el método que modifica la fila del cliente se puede llamar de una forma diferente. No podemos preguntar si existe cada una de las funciones de las 50 páginas para saber si podemos ejecutar el método, porque, a parte de que esto produce un acoplamiento altísimo en la comunicación entre los scripts JS de las páginas, «ensuciaría» mucho el código JS de «hijo.html«.

//Ensuciando código de hijo.html
if(typeof parent.foo === "function") parent.foo();
else if(typeof parten.foo_2 === "function") parent.foo_2();
//...

Lo ideal es elevar un evento global desde el hijo al padre que contenga la información necesaria para que el padre la procese como es debido (o bien que la ignore si no necesita usar los datos).

Recordemos que en JS no existen los eventos globales como tal, todos los eventos se asocian a un elemento HTML del DOM. Para simular un evento global, se suele usar el elemento «document» que contienen todos los DOM de los documentos HTML. Así pues, el hijo va a lanzar un evento al elemento «document» de «padre.html» y este va a escuchar eventos que se produzcan sobre dicho elemento.

PRÁCTICA

Para ejemplificar la solución, voy a crear dos documentos HTML que sean «padre.html» e «hijo.html«. En este caso, el tipo de evento a lanzar va a llamarse «evento-inter-documentos«, y se va a mandar en él un objeto JS para que el padre haga con ello lo que sea necesario.

El hijo lanza el evento con el objeto «datosEvento» dentro de él (el cual, como se puede observar, no hace falta ser serializado previamente):

//hijo.html
var datosEvento = {
  'clave1':'valor1',
  'clave2':'valor2',
  'clave3':'valor3',
  'clave4':'valor4',
  'clave5':'valor5',
};

//Dependiendo del navegador usado, los eventos
//se crean usando el objeto "CustomEvent" ó
//la función document.createEvent('CustomEvent')
if(typeof(Event) === 'function') {
  var event = new CustomEvent(
    'evento-inter-documentos',
    {
    detail: datosEvento
  });
}else{
  var event = document.createEvent('CustomEvent');
  event.initCustomEvent(
    'evento-inter-documentos',
    true,
    true,
    datosEvento
  );
}
parent.document.dispatchEvent(event);

El padre escucha los eventos de tipo «evento-inter-documentos» en el elemento «document«:

//padre.html
document.addEventListener('evento-inter-documentos',
  function(event){
    alert('Capturado evento en padre.html');
    //event.detail contiene el objeto "datosEvento"
    console.log(event.detail);
  }, false);

Nótese que si abrimos el documento «hijo.html» directamente y provocamos que se lance el evento, no se producirá ningún error, ya que «parent.document» va a devolver su propio «document«, sencillamente nadie recogerá el evento.

DESCARGA EL EJEMPLO

Puedes descargar el ejemplo que se encuentra en ESTE archivo comprimido.

NOTA: No hace falta ejecutar los documentos HTML del ejemplo bajo ningún servidor HTTP, ya que se pueden interpretar localmente por el propio navegador; pero hay navegadores como Chrome que son un poco molestos con el hecho de que un iframe intente comunicarse con nuestro documento HTML principal cuando no tienen el mismo servidor de origen. Como el origen de ambos documentos («padre.html» e «hijo.html«) son desconocidos por no estar corriendo bajo ningún servidor HTTP, Chrome lo detecta como una infracción de «cross-origin» y no permite que se comuniquen. Sencillamente tienes que ejecutar los documentos a través de Apache para que Chrome «se calle» (o utilizar otro navegador).


Créditos de fuentes externas:

Iconos:

  • Broadcast by Sylvain A. from the Noun Projectt

Deja una respuesta