Vamos a ver un ejemplo de como generar elementos dinámicos en nuestro documento unicamente con javaScript, sin recurrir a librerias adicionales.
En nuestro documento, cada título con la etiqueta <h3> es un elemento desplegable que permite mostrar u ocultar su texto anidado al hacer clic sobre él.
El ejemplo va a ser progresivo, yendo de menos a más complejidad, para tratar de explicar mejor cómo se realiza el proceso de desarrollo. Para eso vamos a partir de un planteamiento inicial sencillo, usando estrategias con las que estamos más familiarizados, pero más largo en su desarrollo, y menos optimizado y funcional. Este código inicial lo vamos a ir modificando, para acabar reemplazándolo por un desarrollo más complejo, pero más funcional.
Hacemos una función para cada uno de los botones(<h3>), que se encargue de ocultar su elemento correspondiente. Para controlar si el elemento ya está visible en el momento de pinchar sobre él, y entonces la función lo oculte en lugar de mostrarlo, usamos una variable.
Trabajando de esta manera nos vemos obligados a tener tantas funciones, y tantas variables de control de visibilidad, como elementos tenemos en el documento. Es decir, una función y una variable para cada par título / contenido. Esto no es productivo y sólo nos sería útil para contenidos muy pequeños. Tenemos que buscar otra opción más automatizada.
let visible = true;
function cambiaPar(){
let parrafo = document.getElementById("rsp1");
if(visible==true){
parrafo.style.height = "0px";
visible = false;
} else {
parrafo.style.height = "120px";
visible = true;
}
}
Comentario: Aquí necesitamos usar la altura específica de cada respuesta para que la
animación sea correcta. Es lo que definimos en la línea
parrafo.style.height = "120px";. Sabiendo la altura de cada respuesta ajustamos este
valor para que cada función muestre exactamente el tamaño de su contenido, sin espacio adicional.
Está claro que este planteamiento es muy poco funcional. Necesitamos una función con su correspondiente variable para cada elemento, además de conocer la altura de la caja de respuesta de cada uno de ellos para que la animación se ajuste correctamente en cada caso. Vamos a mejorarlo:
Si no usamos variables en el caso anterior, simplificamos nuestro trabajo a la mitad. De lo que se trata es de comprobar en el mismo elemento, mediante alguna de sus propiedades, si al pinchar sobre su título tiene que ser mostrado (porque está oculto) u ocultado (porque ya está visible).
En este caso nos fijamos en la clase que tiene asignada el elemento a mostrar u ocultar. Y se la intercambiamos, de forma que si está visible lo ocultamos, y al revés. De este modo, usamos las clases CSS para saber cuál es el estado actual del elemento (visible u oculto), y al mismo tiempo las propias clases nos sirven para controlar su visibilidad, sin recurrir a las propiedades de javaScript.
Aunque ya no necesitamos las variables, seguimos trabajando de forma que necesitaríamos tantas funciones como elementos a mostrar y ocultar tengamos.
function cambiaPar(){
let parrafo = document.getElementById("rsp1");
let clases = parrafo.classList;
if (clases.contains("visible")){
parrafo.classList.add("invisible");
parrafo.classList.remove("visible");
} else {
parrafo.classList.add("visible");
parrafo.classList.remove("invisible");
}
}
Comentario: Usamos la clase classList, con sus métodos add y
remove para añadir o quitar clases específicas del conjunto de clases CSS que pueda tener
el elemento. Si usáramos className, reemplazaríamos todas las clases del elemento por la
clase actual, lo que nos limitaría en cuanto al fomato que podemos aplicar a los elementos.
El
problema de definir la altura de las cajas de respuestas mediante la clase CSS "visible" es
que aplicamos la misma altura a todas ellas, con independencia de su contenido.
Al parametrizar la función conseguimos que una única función nos sirva para mostrar y ocultar todos los elementos (de forma individual). Ahora, al mismo tiempo que llamamos a la función desde cada elemento <h3>, le indicamos sobre qué caja de respuesta tiene actuar.
function cambiaPar(elem){
let id = "rsp" + elem;
let parrafo = document.getElementById(id);
let clases = parrafo.classList;
if (clases.contains("visible")){
parrafo.classList.add("invisible");
parrafo.classList.remove("visible");
} else {
parrafo.classList.add("visible");
parrafo.classList.remove("invisible");
}
}
Y su llamada en el título
<h3 id="prg1" class="preguntas" onclick="cambiaPar(1)">Primer caso...</h3>
Comentario: Aquí el parámetro es únicamente el número de bloque con el que vamos a
trabajar. Añadimos el número a la raiz "rsp" y con eso generamos el id del elemento a
seleccionar.
Ya nos hemos quitado de encima el duplicar funciones para hacer lo mismo en cada
grupo!
Aunque trabajar con parámetros es bastante útil, no resulta todo lo eficiente que quisiéramos, y a veces nos podemos encontrar en un callejón sin salida al tratar de asignar funciones con parámetros a muchos elementos usando un bucle. Es mejor almacenar en el propio elemento que llama a la función (<h3>) el dato con el que acceder a su objetivo.
Para esto almacenamos el id del elemento de destino en un atributo data del <h3> disparador. La función accede a ese data, y desde ahí al id correspondiente para mostrarlo u ocultarlo. La forma de indicarle a la función donde debe buscar el data es usando la palabra clave this como parámetro de la función. Esta palabra hace referencia al elemento desde el cuál se llama a la función.
Al trabajar de esta forma podemos hacer una asignación de funciones a cada <h3> mediante un bucle en el script, sin necesidad de establecer el atributo onclick en el HTML de cada uno de ellos
function cambiaPar(elem){
let id = elem.dataset.iden
let parrafo = document.getElementById(id);
let clases = parrafo.classList;
if (clases.contains("visible")){
parrafo.classList.add("invisible");
parrafo.classList.remove("visible");
} else {
parrafo.classList.add("visible");
parrafo.classList.remove("invisible");
}
}
//BUCLE PARA ASIGNAR FUNCIÓN A CADA H3
let gr_h3 = document.getElementsByClassName("preguntas");
for(let cadah3 of gr_h3) {
cadah3.onclick = function() {
cambiaPar(this);
}
}
Comentario: De esta forma tenemos una funcionalidad básica de lo que buscábamos. Veamos ahora cómo mejorar el desarrollo.
Para hacerlo más funcional, queremos que al mostrar un elemento, en primer lugar oculte el que esté visible en ese momento, de forma que sólo tengamos un elemento abierto cada vez
Para eso dividimos el proceso en dos funciones, una para mostrar el elemento seleccionado, y otra para ocultar el que esté visible. La llamada a esta función será la primera tarea de la función que muestra los elementos.
let el_visi;
function muestraPar(elem){
ocultaPar(el_visi);
let id = elem.dataset.iden
let parrafo = document.getElementById(id);
let clases = parrafo.classList;
parrafo.classList.add("visible");
parrafo.classList.remove("invisible");
el_visi = parrafo;
}
function ocultaPar(elem){
if (elem != undefined) {
elem.classList.add("invisible");
elem.classList.remove("visible");
}
}
Comentario: Dividimos elpreceso en 2 funciones, una para mostrar y otra para ocultar. Además creamos una variable que compartan las dos funciones (por eso lo hacemos fuera de ellas) que se encargue de identificar qué respuesta está visible en cada momento.
muestraPar(elem): La primera tarea de la función mostrar es llamar a la que oculta la respuesta que esté visible en ese momento. A partir de ahí, se encarga de mostrar el elemento definido en la función, y por último establece que ese es el elemento visible actual.
ocultaPar(elem): Esta función se encarga de ocultar el elemento visible actual. Dos cosas a tener en cuenta: Contiene un if para comprobar que sólo actúa si hay un elemento visible, de lo contrario, al no estar definido el elemento visible y tratar de ocultarlo devolvería un error. El parámetro que recibe la función ahora es un objeto (y no un número para formar un id que nos ayude a seleccionar el elemento). Por eso esta función es más breve, ya tenemos identificado el elemento sobre el que trabajar en el parámetro y sólo tenemos que cambiar sus clases.
De lo que se trata en este caso es de controlar si estamos pinchando sobre el elemento que ya está abierto, y en ese caso lo ocultamos.
Para esto, simplemente tenemos que comprobar que cuando llamamos a la función el elemento visible no coincida con el elemento sobre el que estamos llamando.
let el_visi;
function muestraPar(elem){
let id = elem.dataset.iden
let parrafo = document.getElementById(id);
if (parrafo == el_visi){
ocultaPar(parrafo);
el_visi = undefined;
} else {
ocultaPar(el_visi);
let clases = parrafo.classList;
parrafo.classList.add("visible");
parrafo.classList.remove("invisible");
el_visi = parrafo;
}
}
Comentario: El if se encarga de comprobar si el elemento sobre el que
pincho es el elemento ya visible. En ese caso llama a la función para cerrar ese elemento y
establece que ya no hay ningún elemento visible (el_visi = undefined). En caso
contrario trabaja como siempre.
Ahora vamos a ajustar la presentación inicial, de forma que la aplicación arranque con todos los elementos cerrados menos el primero, que debe mostrar su contenido al cargar la página. Esto lo conseguimos con unas instrucciones adicionales:
let inicio = document.getElementById("rsp1");
inicio.classList.add("visible");
inicio.classList.remove("invisible");
el_visi = inicio;
Comentario: De lo que se trata aquí es simplemente de indicar que el primer elemento tenga la clase "visible", y de inidcarle a la variable que controla la visibilidad que ese es el visible.
En este caso ya no utilizamos una clase para cambiar de estado visible a invisible, y de este modo podremos asignar una altura personalizada a cada elemento. Trabajar con la clase "visible" nos forzaba a definir la misma altura para cada respuesta, lo que supone un fallo de diseño cuando la diferencia entre los tamaños de cada respuesta es bastante grande.
El proceso consiste en acceder a todas las respuestas al principio del script, obtener su altura y almacenar cada una de ellas en su h3.preguntas correspondiente, creando mediante JS un atributo data-altura donde se guarda. Una vez almacenadas las alturas dejamos a cero la altura de todos lo bloques de respuesta para empezar la aplicación.
Por otro lado, las funciones de mostrar y ocultar los párrafos ya no van a intercambiar las clases CSS, sino que van a aplicar la altura correspondiente de cada respuesta para mostrarla, y la altura = cero para ocultarlas.
Código para guardar las alturas:
let gr_resp = document.getElementsByClassName("respuestas");
let j = 0;
let altura = [];
for (let cada_rsp of gr_resp) {
altura[j] = cada_rsp.offsetHeight;
gr_h3[j].dataset.altura = altura[j];
cada_rsp.style.height = 0;
j++;
}
Las funciones para mostrar y ocultar
function muestraPar(elem){
let id = elem.dataset.iden
let parrafo = document.getElementById(id);
let alto = parseInt(elem.dataset.altura) + 10;
if (parrafo == el_visi){
ocultaPar(parrafo);
el_visi = undefined;
} else {
ocultaPar(el_visi);
parrafo.style.height = alto +"px";
el_visi = parrafo;
}
}
function ocultaPar(elem){
if (elem != undefined) {
elem.style.height = 0;
}
}
Código de inicio
let inicio = document.getElementById("rsp1");
let alt_ini = parseInt(altura[0]) + 10;
alt_ini = alt_ini + "px";
inicio.style.height = alt_ini;
el_visi = inicio;
Comentario:Lo interesante de este caso, además de que supone trabajar de forma totalmente precisa, es ver cómo asignamos un atributo data- a los <h3> de forma dinámica mediante javaScript para luego recuperarlo en la función y definir la altura final del elemento.