Vuejs para programadores jQuery.
Selects relacionados IX
Ahora usaremos dos select para mostrar en uno el contenido según la selección del otro.
JQUERY
Usaremos un js para definir el array de contenidos en ambos códigos. Lo hemos externalizado por varias razones
- Tiene 112 lineas que solo servirían para distraernos de lo que estamos haciendo
- Lo usamos exactamente igual en ambos códigos ¿Porque repetir?
- Queríamos mostrar una manera de definir una variable externa para el uso dentro del código sin necesidad de los clásicos import / export de los proyectos de Webpack / Parcel /etc.
Enlace del archivo para ambos códigos:
- Codesandbox: https://githubbox.com/Gonzalo2310/jQuery-Vuejs/blob/master/Select/Relacionados/lista.js
- GitHub: https://github.com/Gonzalo2310/jQuery-Vuejs/blob/master/Select/Relacionados/lista.js
No pondremos aquí las 112 lineas de código pero si la parte inicial:
const listaTotal = [
{
titulo: 'Animales',
index: 1,
child: [
{
titulo: 'Gato',
index: 101
}
...
Definimos una constante listaTotal de tipo array, y cada elemento tiene 3 campos: Un título, un index único y un array llamado child que contiene objetos con el mismo tipo de campo.
Si no queda claro usar los enlaces, revisar, mirar el código.
Ahora si. Empecemos con el código. Primero los enlaces:
- CodesandBox: https://githubbox.com/Gonzalo2310/jQuery-Vuejs/blob/master/Select/Relacionados/jQuerySelectRelacionados.html
- GitHub: https://github.com/Gonzalo2310/jQuery-Vuejs/blob/master/Select/Relacionados/jQuerySelectRelacionados.html
El HMTL:
<form>
<div class="form-row align-items-center">
<div class="col-auto my-1">
<label class="mr-sm-2 sr-only" for="inlineFormCustomSelect">Tema principal</label>
<select class="custom-select mr-sm-2" id="inlineFormCustomSelect">
</select>
</div>
<div class="col-auto my-1">
<label class="mr-sm-2 sr-only" for="inlineFormCustomSelect1">SubTema</label>
<select class="custom-select mr-sm-2" id="inlineFormCustomSelect1">
</select>
</div>
</div>
</form>
0 sorpresas. Un código simple con dos select totalmente vacíos.
Antes del JS hablemos de esto:
<script src="lista.js"></script>
Aquí estamos trayendo el archivo JS con la constante como explicamos antes. ¿Pero que es lo que significa realmente? En realidad <script> lo que hace es una inyección de código. O sea que el contenido de lista.js será escrito en el código cuando se procese la información (aunque el navegador seguirá mostrando la marca y no el contenido) por lo tanto es como si tuviéramos el contenido del archivo escrito en el mismo archivo donde estamos trabajando.
¿Cuál es la diferencia con un import?. Import permite traer una o varias funciones o variable o incluso una combinación de ellas desde un mismo archivo. <script> no permite esa discriminación por lo tanto si queremos tener el mismo resultado con <script> debemos tener las funciones o variables definidas cada una en un archivo diferente y cargar aquellos que sean relevantes en cada caso.
Supongamos que tenemos el código:
Y usamos:
Para el intérprete de js será lo mismo que:
Ahora si.Vayamos con el jQuery:
const selectPadre = jQuery('#inlineFormCustomSelect')
const selectHijo = jQuery('#inlineFormCustomSelect1')
const elementosPadre = extractElementosPadre()
CreateSelect(selectPadre, elementosPadre)
selectPadre.on('change', actualiza)
actualiza()
Es casi tradicional este código ya. selectPadre apunta al select principal que tendrá los elementos que no son filtrados. selectHijo apunta al select que tendrá los elementos filtrados según la selección del primer select, asociamos el evento change del select principal con la función actualiza y lanzamos actualiza por primera vez.
Pasemos a explicar el const y la función que hemos omitido.
Antes de seguir quiero explicar algo. jQuery tiene muchas maneras de hacer las cosas y no todas son la mejor forma, es típico encontrar código que mezcla en un append un largo string de marcas HTML, código js, valores, contenido de texto, etc. lo que provoca un código sumamente caótico. Por ejemplo:
jQuery('#accordion_container').append('<div class="accordian_container"><a href="#" class="accordian_trigger"><h4>Co-Borrower Information</h4></a><hr/><div class="accordian_item" id="accord_item_2"><label> First Name</label><br/><input type="text"/><br/><label>Middle Name</label><br/> <input type="text"/><br/> <label>Last Name</label><br/> <input type="text" /><br/> <label>Home Number</label><br/> <input type="text"/><br> <label>Work Number</label><br/> <input type="text"/><br> <label>Cell Number</label><br/> <input type="text"/><br> </div> </div>')
(Lo he visto bastante peores)
Esto me parece bastante desafortunado. Complejo de mantener y sobre todo de lectura confusa para lo que se quiere lograr, así que para evitar ese tipo de código usaremos funciones templates de las marcas HTML que vamos a agregar, ejemplo:
function templateOption(value, texto) {
return '<option value="' + value + '">' + texto + '</option>'
}
Esto mantiene la marca HTML <option> independiente de más código js. También nos evita tener que estar repitiendo largas cadenas de string. Devuelve un option completo con un value y un texto que hemos enviado en los parámetros de la función. Por ejemplo:
templateOption(0, 'No se ha elegido nada')
nos devolverá:
<option value='0'>No se ha elegido nada</option>
Creamos una función más pero creo que a cambio obtenemos un código más armónico.
Ahora miremos esta otra función:
function extractElementosPadre() {
let result = []
listaTotal.every(item => result.push(templateOption(item.index, item.titulo)))
return result
}
Definimos un array vacío. La siguiente linea la desmontaremos:
listaTotal.every(item => result.push(templateOption(item.index, item.titulo)))
Primero:
listaTotal.every()
¿De dónde sale listaTotal? No la estamos definiendo en ningún lugar del código. listaTotal es la constante que hemos cargado en nuestro código cuando hemos usado la marca script para traer lista.js
every() es una de las opciones que existen para acceder a cada elemento de un array.
Usamos la notación arrow. Para despistados daremos unas indicaciones:
function () {} es igual que () => {}
function (parametro) {} es igual que parametro => {}
function (parametro, posicion) es igual que (parametro, posicion) => {}
La notación arrow se usa cada vez con más frecuencia y el resumen que hemos aquí es muy muy suave. Realmente hay más cambios. Si se quiere saber más: https://developer.mozilla.org/es/docs/Web/JavaScript/Referencia/Funciones/Arrow_functions
A every le podemos pasar una función que resuelva lo que queremos hacer con cada item o definir una función anónima ahí mismo, por ejemplo:
listaTotal.every(function(item) {
result.push(templateOption(item.index, item.titulo))
})
Donde item es cada uno de los elementos del array. Y podemos reducir la función anónima:
listaTotal.every((item) => {
result.push(templateOption(item.index, item.titulo))
})
Y al ser un único parámetro lo podemos dejar así:
listaTotal.every(item => {
result.push(templateOption(item.index, item.titulo))
})
Además como solo se ejecuta una sentencia podemos reducirlo más:
listaTotal.every(item => result.push(templateOption(item.index, item.titulo)))
Expliquemos el push:
result.push(templateOption(item.index, item.titulo))
push es lo propio de un array para agregar contenido. templateOption es la función que hemos visto antes y que nos devuelve un <option>, y cuando hemos mirado el código de lista.js hemos visto que los elementos tiene index, titulo y child. Aquí usamos index y titulo para construir el option.
listaTotal.every(item => result.push(templateOption(item.index, item.titulo)))
Al final hemos visto que esta línea significa una interacción con el dato traído de lista.js para construir con cada uno de ellos un <option> que guardamos dentro de un array
El último paso de la función extractElementosPadre() es un return que devuelve el array construido.
Ahora queda más claro la definición de la constante siguiente:
const elementosPadre = extractElementosPadre()
Es un arrays de <options> creado con los datos de listaTotal (traído con lista.js)
Miremos la siguiente función:
function CreateSelect(select, element) {
select.empty().append(element.join(''))
}
Esta función es genérica, pide la constante que apunta a un select y un array de elementos y agrega esos elementos al select vaciándolo previamente
join() es una función de Js que convierte un array en una cadena de texto, por ejemplo:
[1,2,3].join() Devuelve: "1,2,3"
join() por defecto devuelve los elementos separados por comas, pero en el caso del select los options no deben tener separador para eso le damos el parámetro que será lo que lo separara, en este caso un valor null ("")
usando el ejemplo anterior:
[1,2,3].join("") Devuelve "123"
Y así completamos de explicar las definiciones del inicio del script. Solo nos queda por ver dos funciones
function extractElementosHijos(index) {
const elegido = listaTotal.find(item => item.index === index)
if (!elegido) {
return []
}
let result = []
elegido.child.every(item => result.push(templateOption(item.index, item.titulo)))
return result
}
Esta función recibe un index de un elemento de listaTotal, busca ese elemento en la lista con find, si no hay resultado devuelve un array vacío y si hay resultado crea un array de options del contenido de la característica child del elemento.
listaTotal.find(item => item.index === index)
Devuelve el primer elemento que cumpla la condición o null si no encuentra nada. Hemos dicho que cada index es único o sea que el primer elemento es también el único elemento que cumple la condición.
Con respecto a la función arrow hay una particularidad:
Esto es correcto:
find(item => item.index === index)
Pero esto no
find(item => { item.index === index })
Porque necesita si o si un return (boolean) del resultado de la operación así que para validar un find con sentencias más complejas hay que usar el return:
find(item => { return item.index === index })
Y ahora si lo podemos usar con llaves ({}) aunque repito que con una única sentencia no son necesarias
El if pregunta ¿Es elegido un valor NOT (!) válido? si es así devuelve un array vacío.
Hablemos un momento sobre los if else inútiles. Else es útil muchas veces pero se usa en exceso en mucho código, me explico:
let valor = 0
if (condicion) {
valor =1
} else {
valor =2
}
Es lo que denomino un else inutil, ya que ese if se resuelve asi:
let valor=2
if (condicion) {
valor =1
}
Lo veo más legible y sobre todo se evita un innecesario, pero el caso más frecuente de else inútil que veo es en código como este:
if (condicion) {
return
} else {
...
}
Lo veo inútil por innecesario. Si if tiene un return cualquier código escrito después del if, este o no dentro de un else se ejecutara si la condición es falsa, ya que el if termina la ejecución del código.
El if anterior hace el mismo efecto así:
if (condicion) {
return
}
...
No es una apología en contra del else, pero si en contra del exceso de uso.
Siguiendo con el código luego del if se crea un array vacío. Acto seguido se rellena con OJO los elementos del child del elemento encontrado con find. Y como hicimos antes con every y templateOptions creamos el array correspondientes como <option> y al final devolvemos el array resultante.
Y ahora la reina del baile: actualiza
function actualiza() {
CreateSelect(selectHijo, extractElementosHijos(parseInt(jQuery('#inlineFormCustomSelect option:selected').val())))
}
Desmontemos desde dentro hacia afuera:
jQuery('#inlineFormCustomSelect option:selected').val()
Esta línea selecciona, en el select padre, el elemento elegido y recoge su value (val()) que como recordamos lo estamos creando con el valor index del los elementos del array traído con lista.js
parseInt(jQuery('#inlineFormCustomSelect option:selected').val())
Convertirlos el value a un valor entero. Recordar que el value es un string y el index del elemento es un valor número entero.
extractElementosHijos(parseInt(jQuery('#inlineFormCustomSelect option:selected').val()))
Esta es la función que busca y devuelve los elementos hijos (child) según el index indicado (el value parseado a número entero)
CreateSelect nos pedía un select (selectHijo en este caso) y un array de elementos que es lo que devuelve extractElementosHijos.
De esta manera ya tenemos el codigo jQuery. Limpio y claro. Si ven que hay una manera mejor de realizar este código soy todo oídos. Sobre todo porque nos esperan varios post de variantes de Select y estaría bien tener todo optimizado.
Pasemos a Vuejs
Usaremos el mismo lista.js de antes así que pondremos solo los enlaces del código vuejs:
- CodeSandBox: https://githubbox.com/Gonzalo2310/jQuery-Vuejs/blob/master/Select/Relacionados/VueJsSelectRelacionados.html
- GitHub: https://github.com/Gonzalo2310/jQuery-Vuejs/blob/master/Select/Relacionados/VueJsSelectRelacionados.html
HTML:
<form>
<div class="form-row align-items-center">
<div class="col-auto my-1">
<label class="mr-sm-2 sr-only" for="inlineFormCustomSelect">Tema principal</label>
<select class="custom-select mr-sm-2" id="inlineFormCustomSelect"v-model="selectPadre">
<option v-for="item in elementosPadres" :key="item.index" :value="item.index">{{item.titulo}}</option>
</select>
</div>
<div class="col-auto my-1">
<label class="mr-sm-2 sr-only" for="inlineFormCustomSelect1">SubTema</label>
<select class="custom-select mr-sm-2" id="inlineFormCustomSelect1" v-model="selectHijo">
<option v-for="item in elementosHijo" :key="item.index" :value="item.index">{{item.titulo}}</option>
</select>
</div>
</div>
</form>
Las explicaciones en Vuejs serán más cortas porque estamos usando cosas que hemos visto. Repasemos.
Los selects están asociados a variables a través de v-model. Recordemos que v-model en este caso guarda el value de los options contenidos.
Antes de mirar los options vamos a poner el código JS porque estamos usando computadas y asi nos podemos explicar mejor
<script>
new Vue({
el: '#app',
data: {
selectPadre: '1',
selectHijo: '101'
},
watch: {
selectPadre:function () {
this.selectHijo = this.elementosHijo[0].index
}
},
computed: {
elementosPadres(){
return listaTotal
},
elementosHijo() {
const list = listaTotal.find(item => item.index === parseInt(this.selectPadre) )
return list ? list.child : []
}
}
})
</script>
El option del select padre:
<option v-for="item in elementosPadres" :key="item.index" :value="item.index">{{item.titulo}}</option>
Volvemos a usar v-for con el formato que ya hemos visto en ocasiones anteriores (item in array) pero en lugar de usar directamente listaTotal usamos una computada (que en este caso en particular también podría ser una variable de Vuejs) y el porqué de esto lo hemos explicado cuando hablamos del apuntado this en el post: https://comunidad.programaresunamierda.com/2020/06/vuejs-para-programadores-jquery-form_11.html.
Como explicamos en posts anteriores key debe tener un valor único en cada option, para esto tenemos a index que es un valor único así que lo asignamos con :key="item.index" y el mismo valor usamos para el value y por último mostramos el campo titulo ({{}}).
Si hacemos una comparativa con el código de jQuery se parece al selectCreate pero con otra lógica y trabajando directamente sobre el HTML.
El select hijo es igual con el v-model asignado a otra variable y el option:
<option v-for="item in elementosHijo" :key="item.index" :value="item.index">{{item.titulo}}</option>
Usamos casi casi el mismo código de antes con solo una variante (que lo cambia todo) que es el array de donde extraemos los items. Miremos el código JS de esa computada:
elementosHijo() {
const list = listaTotal.find(item => item.index === parseInt(this.selectPadre) )
return list ? list.child : []
}
Buscamos el elemento elegido exactamente igual (usando el find) que en jQuery pero cambia el valor con el que comparamos:
parseInt(this.selectPadre)
parseInt ya lo hemos explicado en numerosas veces. Convierte un carácter o cadena de caracteres a un valor entero ('0' => 0)
this.selectPadre es la variable usada en el v-model del select padre. Esta variable SIEMPRE tendrá el valor del value del option elegido. Y recordemos lo que decíamos en un post anterior:
"Las computadas son variables complejas que se actualizan cuando cualquiera de los valores que usa cambia y siempre deben devolver un valor."
Esto indica que cuando el selectPadre cambia el valor de esa variable y sin necesidad de acción extra ninguna la computada elementosHijo también cambiara porque usa esa variable para generar su resultado.
Sé que a veces es difícil de ver pero la lógica es sencilla vista así:
Cuando se crea una computada, Vuejs le da herramientas para que "vigile" los cambios de las variables involucradas. Cuando sucede un cambio se relanza automáticamente la computada.
Más adelante si es necesario podemos ahondar en el tema si hace falta. Sería un artículo técnico básicamente. Si les interesa espero comentarios al respecto.
Luego la computada hace un if ternario para resolver lo que devolverá. Si se encontró el elemento devuelve el array del child y si no un array vacío.
Con esto ya tenemos el sistema funcionando casi perfectamente. ¿Qué falla? Es apenas un detalle: cuando se actualizan los options del select hijo ningún elemento es elegido por defecto, ya que la variable del v-model selectHijo no ha sido actualizada y tiene un valor de index que no corresponde con ningún elemento de los hijos que se han elegido.
Para resolver este problema usamos watch:
watch: {
selectPadre:function () {
this.selectHijo = this.elementosHijo[0].index
}
}
watch (lo hemos comentado alguna vez) es relacionar una función con un cambio de una variable. Aunque las variables se actualizan según los eventos van sucediendo a veces (como en este caso) necesitamos hacer algo más.
En este caso estamos haciendo esto: cuando selectPadre cambie actualiza la variable selectHijo con el valor del index del primer elemento de los elementosHijo y de esa manera al cambiar el primer elemento estará seleccionado por defecto.
En resumen cuando cambiamos el selectPadre se lanzan los eventos de actualizar el select hijo tanto en los options (con la computada) como en el valor del option elegido en ese momento (con el watch)
Y ahora si lo tenemos listo.
El elemento select del HTML me parece el lugar más evidente de las diferencias entre un framework reactivo y una librería que no lo es.
Espero que los conceptos quedaran claro a pesar de lo largo del post y que ayude a comprender mejor. Seguiremos mirando cosas con los select algunos post más.
Codigo general:
- Codesandbox: https://githubbox.com/Gonzalo2310/jQuery-Vuejs/tree/master/Select/Relacionados
- GitHub: https://github.com/Gonzalo2310/jQuery-Vuejs/tree/master/Select/Relacionados
Consultas, dudas, comentarios: Slack de PEUM o en Twitter.
No hay comentarios:
Publicar un comentario