Vuejs para programadores jQuery.
Tablas simple. Paginación. XV
Hoy veremos el código de una tabla sencilla con paginación y modal para ver más datos del user elegido. Se ha usado el servicio: https://www.mockaroo.com/ que permitió crear 1000 datos fakes de diferente complejidad.
El archivo generado lo tenemos en el proyecto:
- Codesanbox: https://githubbox.com/Gonzalo2310/jQuery-Vuejs/blob/master/Table/Simple/centerData.js
- GitHub: https://github.com/Gonzalo2310/jQuery-Vuejs/blob/master/Table/Simple/centerData.js
Datos de los usuarios: id, first_name, last_name, email, phone, avatar, language, animal, color, company. Los tipos de datos no son muy importantes para este proyecto pero es bueno tenerlos claro, ya que hasta que no usemos ajax crearemos datos fakes.
JQUERY
Comencemos con el código:
- Codesandbox: https://githubbox.com/Gonzalo2310/jQuery-Vuejs/blob/master/Table/Simple/jQueryTableSimple.html
- GitHub: https://github.com/Gonzalo2310/jQuery-Vuejs/blob/master/Table/Simple/jQueryTableSimple.html
Dividiremos el trabajo en 3 partes: tabla, paginación y modal.
Tabla: html:
<table class="table table-striped" id="centralTable">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Nombre</th>
<th scope="col">Apellido</th>
<th scope="col">Compañía</th>
<th scope="col">Ver</th>
</tr>
</thead>
<tbody id="dataTable">
</tbody>
</table>
Solo mostraremos algunos datos y los demás en un modal.
Tenemos el cuerpo de la tabla con un id y necesitamos controlar que datos de los 1000 vamos a mostrar así que empezamos por el objeto paginación
const pagination = {
current: 1,
views: 10,
counter: 5,
total: information.length
}
pagination.limit = Math.abs(information.length / pagination.views)
if (information.length !== (pagination.limit * pagination.views)) {
++pagination.limit
}
Está en dos partes. La primera es una asignación directa de valores y el campo limit necesita el objeto creado para hacer cálculos así que lo agregamos después. Agregar un campo nuevo a un objeto JS es tan rápido como tramposo. Veamos
let data = {
name: '',
surname: ''
}
Tenemos el objeto creado. ¿Cómo agregamos un campo?
data.telefono: '34434'
Y listo. Ahora el objeto tiene 3 variables: name / surname / telefono
Esto es lo que hemos hecho con el campo limit en pagination. Miremos los valores:
current // Tendrá la posición de la página actual que veremos.
views // Cuantos usuarios se mostrarán por página
counter // la cantidad máxima de números que aparecen en la paginación
total // El total de usuarios
information es el objeto que hemos importado del js externo
limit es la página máxima. Se calcula el total dividido en la cantidad por página. No obstante el total normalmente no es redondo así que se comprueba la operación inversa y si no coinciden se suma uno a limit. Veamos esto con números. Primero los reales.
limit = Math.abs( 1000 / 10) // limit tendrá 100
El if dará como válido el error y no entrara.
Ahora si el largo del array no fuera un número redondo. Supongamos que tenemos 933 datos.
limit = Math.abs( 933 / 10) // limit tendra 93
en el if calcula: 93 * 10 < 1000 // limit +1
Ahora habrá 94 páginas. 93 con 10 resultados y 1 con 3 resultados restantes.
Vamos directo al templateTableData que es un template mayor que los que hemos desarrollado hasta ahora. Pero aun manteniendo código limpio.
function templateTableData() {
let result = []
let start = pagination.current >1 ? ((pagination.current -1) * pagination.views ): 0
let end = start + pagination.views
if (end > pagination.total) {
end = pagination.total + 1
}
for (let i = start; i < end; i++) {
let item = information[i]
result.push(
`<tr>
<td>${item.id}</td>
<td>${item.first_name}</td>
<td>${item.last_name}</td>
<td>${item.company}</td>
<td><button onclick="expand(${item.id})" class="pointer" type="button" data-toggle="modal" data-target="#dataModal">${iconEye}</button></td>
</tr>`
)
}
return result.join('')
}
Primero creamos un array vacío. Luego calculamos el punto de inicio (recordar que current es la página actual NO el elemento actual)
pagination.current >1 ? ((pagination.current -1) * pagination.views ): 0
Si la página actual es mayor que uno, le quitamos uno y multiplicamos por el tamaño de la cantidad que vamos a mostrar. Si no es mayor que uno guardamos 0. Esto ultimo es porque las páginas, listas id's y demás lo contamos desde 1 pero el array cuenta desde 0
Veamos el resultado si current es mayor que uno con numeros. Para current 2
(2-1)*10 = 10 o sea empezará desde el elemento 10
Eso es correcto, ya que si era 1, guardamos 0 habremos mostrado del 0-9
Ahora el valor final:
end = start + pagination.views // en el caso de current 2 tendrá 10+10. Pero el último elemento no se mostrará como lo veremos ahora así que sencillamente se le suma la cantidad configurada para mostrar. El siguiente control es que no se pase del total y si es así se le suma uno más del total porque:
for (let i = start; i < end; i++) {
se usa desde start hasta end -1
Ahora que tenemos el rango de datos sencillamente recogemos el ítem del array externo y le damos formato guardando cada fila en el array.
Los datos usados son de forma tradicional menos él ultimo td:
<td><button onclick="expand(${item.id})" class="pointer" type="button" data-toggle="modal" data-target="#dataModal">${iconEye}</button></td>
Este paso envía el id a una función expand (para mostrarlo en el modal) y es un botón con un contenido SVG que es un dibujo de un ojo (iconEye) cuya variable hemos definido así:
const iconEye = '<svg class="bi bi-eye-fill" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">\n' +
' <path d="M10.5 8a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z"/>\n' +
' <path fill-rule="evenodd" d="M0 8s3-5.5 8-5.5S16 8 16 8s-3 5.5-8 5.5S0 8 0 8zm8 3.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z"/>\n' +
' </svg>'
Este código fue bajado de los iconos de Bootstrap.
Al final devolvemos el array como cadena de texto.
El primer uso que hacemos de esto es en:
dataTable.empty().append(templateTableData())
dataTable es una constante que apunta al body de la tabla:
const dataTable = jQuery('#dataTable')
Pasemos a la función expand y todo lo que tiene que ver con el modal. Primero que nada diremos que hemos tenido dificultades con las clases por defecto del modal de Bootstrap asi que creamos nuestro popio sistema para mostrarlo con un pequeño desplazamiento y un efecto fade. Efecto que se revierte cuando cierra.
El css es un archivo externo.
function expand(id) {
let item = information.find(item => item.id === id)
if (item) {
bodyModal.empty().append(templateCard(item))
dataModal.addClass('showModal')
}
}
expand espera un unico item. Sin arrays ni nada. lo que se envia es un id. Lo busca y si lo encuentra vacia el cuerpo del modal y lo rellena con el resultado de formatear dicho dato. Luego le asignamos la clase para mostrarlo.
bodyModal lo definimos así:
const bodyModal = jQuery('#dataModalBody')
y apunta al modal tal que:
<div class="modal fade" tabindex="-1" role="dialog" id="dataModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Información del usuario</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" onclick="closeModal()">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body" id="dataModalBody">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal" onclick="closeModal()">Cerrar</button>
</div>
</div>
</div>
</div>
solo toca el cuerpo del modal (dataModalBody)
Antes de seguir observemos que tenemos funciones para cerrar el modal. Miremos eso:
function closeModal() {
dataModal.removeClass('showModal').addClass('hideModal')
setTimeout(function () {
dataModal.removeClass('hideModal')
}, 400)
}
Quitamos la clase que usamos para mostrarlo. Lo cambiamos por una clase para hacer el efecto contrario y luego de 400 milisegundos quitamos esa clase también.
Veamos el formato del Modal:
function templateCard(item) {
return `<div class="card mb-3" style="max-width: 540px;">
<div class="row no-gutters">
<div class="col-md-4">
<img src="${item.avatar.replace('50x50', '150x150')}" class="card-img-top" alt="avatar del usuario: ${item.first_name}">
</div>
<div class="col-md-8">
<div class="card-body">
<h5 class="card-title">${item.first_name} ${item.last_name}</h5>
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">Compañia: ${item.company}</li>
<li class="list-group-item">Email: ${item.email}</li>
<li class="list-group-item">Idioma: ${item.language}</li>
<li class="list-group-item">Telefono: ${item.phone}</li>
<li class="list-group-item">Animal: ${item.animal}</li>
<li class="list-group-item">Color: ${item.color}</li>
</ul>
</div>
</div>
</div>`
}
Hay algunas cosas interesantes: Primero. Lo estamos mostrando dentro de una card de Bootstrap. Segundo: A la imagen le corregimos el tamaño. Eso es porque es un servicio externo que te devuelve una imagen tomando el tamaño que le envíes en la url.
Y ahora nos queda la paginación:
Primero el template:
function templatePaginationView() {
let result = []
let previous = pagination.current === 1 ? 'disabled' : ''
let posterior = (pagination.current * pagination.views) >= pagination.total ? 'disabled' : ''
let start = pagination.current > 1 ? pagination.current - 1 : pagination.current
let end = pagination.current + pagination.counter
if (end > pagination.limit) {
end = pagination.limit
}
for (let i = start; i < (end + 1); i++) {
if (i === pagination.current) {
result.push(
`<li class="page-item active min-item" aria-current="page">
<a class="page-link" href="#">${i}<span class="sr-only">(current)</span></a>
</li>`
)
} else {
result.push(
`<li class="page-item min-item" onclick="paginationClick(${i})"><a class="page-link" href="#" >${i}</a></li>`
)
}
}
return `<li class="page-item ${previous}">
<a class="page-link" href="#" tabindex="-1" aria-disabled="true" onclick="paginationClick(${pagination.current -1})">Previo</a>
</li>
${result.join('')}
<li class="page-item ${posterior}">
<a class="page-link" href="#" onclick="paginationClick(${pagination.current + 1})">Proximo</a>
</li>`
}
El formato es el siguiente:
Tenemos un botón de previo, unos botones para cambiar de página y al final un próximo. Las variables previous y posterior calculan si tiene utilidad:
previous = pagination.current === 1 ? 'disabled' : ''
Los disables en los <li> de la paginación se hacen con clases. Aquí preguntamos si estamos en la primer página de ser así guardamos un valor string (disabled) que agregaremos a las clases, ya que no tiene sentido ese botón al inicio de la tabla
Hacemos lo mismo para el botón siguiente solo que ahora los cálculos son diferentes:
let posterior = (pagination.current * pagination.views) >= pagination.total ? 'disabled' : ''
Si la página actual + la cantidad de datos a mostrar es mayor o igual que el total de datos entonces el boton no nos llevara a ningún sitio por lo tanto lo ponemos a disabled.
Vemos que tenemos una función paginationClick para resolver el cambio de página. Veamos:
function paginationClick(value) {
if (value === pagination.current) {
return
}
pagination.current = value
dataTable.empty().append(templateTableData())
paginationView.empty().append(templatePaginationView())
}
Si el valor enviado es el mismo que el actual no se hace nada y se sale de la función.
Si no es así se cambia el valor del pagination.current y se vuelve a rellenar la tabla de datos como la paginación.
Aquí lo tenemos todo resuelto. Pasemos a Vuejs.
VUEJS
Al igual que con jQuery iremos por partes.
Enlaces:
- Codesandbox: https://githubbox.com/Gonzalo2310/jQuery-Vuejs/blob/master/Table/Simple/VueJsTableSimple.html
- GitHub: https://github.com/Gonzalo2310/jQuery-Vuejs/blob/master/Table/Simple/VueJsTableSimple.html
<table class="table table-striped" id="centralTable">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Nombre</th>
<th scope="col">Apellido</th>
<th scope="col">Compañía</th>
<th scope="col">Ver</th>
</tr>
</thead>
<tbody id="dataTable">
<tr v-for="item in tableData" :key="item.index">
<td>{{item.id}}</td>
<td>{{item.first_name}}</td>
<td>{{item.last_name}}</td>
<td>{{item.company}}</td>
<td>
<button @click="expand(item)" class="pointer" type="button" data-toggle="modal" data-target="#dataModal"
v-html="iconEye"></button>
</td>
</tr>
</tbody>
</table>
Vemos la parte de los títulos igual. En cambio el cuerpo usamos, como ya es frecuente, el v-for refiriendo a tableData. Veamos esa computada.
tableData() {
let result = Array.from(information)
let start = this.pagination.current > 1 ? ((this.pagination.current - 1) * this.pagination.views) : 0
let end = start + this.pagination.views
if (end > this.pagination.total) {
end = this.pagination.total
}
return result.splice(start, end-start)
}
Primero creamos un array nuevo a partir del array original que traemos del js externo.
En el anterior post repasábamos el caso de la asignación de los arrays y como su valor se mantenía por referencia cambiando el contenido del original cuando se cambiaba el valor de un array asignado por igualdad.
Array.from es una de las maneras de resolver esto. Ya que crea un array totalmente nuevo a partir de otro.
Seguimos la misma lógica que en jQuery para los cálculos de start y end y usamos splice para extraer los datos que nos interesan. Esta es la razón por la que hemos creado un array nuevo, ya que si no los estaríamos eliminando del array original.
Revisemos el objeto pagination:
pagination: {
current: 1,
views: 10,
counter: 5,
total: information.length
}
Usamos los mismos valores y limit que usa valores del objeto para resolver su valor lo creamos en el hook, que ya conocimos cuando creamos un componente, created()
created () {
this.pagination.limit = Math.abs(information.length / this.pagination.views)
if (information.length !== (this.pagination.limit * this.pagination.views)) {
++this.pagination.limit
}
}
Exactamente igual que en jQuery.
Revisemos él ultimo td de la lista de datos del HTML:
<button @click="expand(item)" class="pointer" type="button" data-toggle="modal" data-target="#dataModal"
v-html="iconEye"></button>
Una de las grandes diferencias entre la forma de agregar datos de jQuery y Vuejs es que append procesa los símbolos HTML en cambio cuando en Vue usamos el {{}} se sobreentiende que el resultado es en formato de texto (aunque incluya símbolos HTML) por lo que si queremos que procese HTML debemos usar una marca especial llamada v-html como hacemos en este elemento.
Tanto en jQuery como en Vuejs podría haber usado el SVG directamente en el código. Son marcas reconocidas por el HTML. Pero me pareció interesante aprender este detalle.
Recordemos que iconEye es:
iconEye: '<svg class="bi bi-eye-fill" width="1em" height="1em" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">\n' +
' <path d="M10.5 8a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0z"/>\n' +
' <path fill-rule="evenodd" d="M0 8s3-5.5 8-5.5S16 8 16 8s-3 5.5-8 5.5S0 8 0 8zm8 3.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7z"/>\n' +
' </svg>'
Vayamos a expand:
expand(item) {
this.userSelect = item
this.modalShow = true
}
Recordemos que el modal tiene 3 estados: * con la clase para mostrar, * con la clase para ocultar, * sin clase. Veamos las variables relacionadas:
userSelect: {
avatar: ' '
}
modalShow: false
modalHide: false
Y lo explicaremos con el body del modal:
<div class="modal-body" id="dataModalBody" >
<div class="card mb-3" style="max-width: 540px;">
<div class="row no-gutters">
<div class="col-md-4">
<img :src="userSelect.avatar.replace('50x50', '150x150')" class="card-img-top" :alt="'avatar del usuario: ' + userSelect.first_name">
</div>
<div class="col-md-8">
<div class="card-body">
<h5 class="card-title">{{userSelect.first_name}} {{userSelect.last_name}}</h5>
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item">Compañía: {{userSelect.company}}</li>
<li class="list-group-item">Email: {{userSelect.email}}</li>
<li class="list-group-item">Idioma: {{userSelect.language}}</li>
<li class="list-group-item">Telefono: {{userSelect.phone}}</li>
<li class="list-group-item">Animal: {{userSelect.animal}}</li>
<li class="list-group-item">Color: {{userSelect.color}}</li>
</ul>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal" @click="closeModal()">Cerrar</button>
</div>
Los datos los estamos mostrando, con los cambios habituales, igual que en jQuery pero nos basta cambiar el contenido de userSelect para que el contenido del modal cambie.
Los boolean se usan en la cabecera del modal:
<div class="modal fade" tabindex="-1" role="dialog" id="dataModal" :class="{showModal: modalShow, hideModal: modalHide}">
Asociando cada uno con cada clase para lograr el efecto deseado.
Veamos entonces closeModal:
closeModal() {
this.modalHide = true
this.modalShow = false
const me = this
setTimeout(function () {
me.modalHide = false
}, 400)}
}
Exactamente la mismo lógica que en jQuery. Efecto de salida y eliminación de la clase 400 milisegundos después.
Revisemos la paginación:
El botón de previo:
<li class="page-item" :class="{disabled: pagination.current === 1}">
<a class="page-link" href="#" tabindex="-1" aria-disabled="true" @click="pagination.current--">Previo</a>
</li>
Resolvemos la clase en el elemento y también la acción en el click
<li class="page-item " :class="{disabled: (pagination.current * pagination.views) >= pagination.total}">
<a class="page-link" href="#" @click="pagination.current++">Proximo</a>
</li>
Y lo mismo para el botón de próximo.
<li class="page-item min-item"
@click = "pagination.current = item + paginationView.start"
:class="{active: (item+paginationView.start)===pagination.current}"
:aria-current="(item+paginationView.start)===pagination.current ? 'page' : ''"
v-for="(item) in paginationView.diff" :key="item">
<a class="page-link" href="#" >{{item + paginationView.start}}
<span :class="{'sr-only': (item+paginationView.start)===pagination.current}">
{{(item+paginationView.start)===pagination.current ? '(current)' : ''}}</span>
</a>
</li>
Pondremos aquí la computada a la que se hace referencia en el HTML:
paginationView() {
let start = this.pagination.current > 1 ? this.pagination.current - 1 : this.pagination.current
let end = this.pagination.current + this.pagination.counter
if (end > this.pagination.limit) {
end = this.pagination.limit +1
}
return {start: start > 1 ? start -1 : 0, end, diff: end === this.pagination.limit ? (end-start) +1 : end-start}
}
La computada en si no es sorpresa. Es la resolución de que valores mostrar en la paginación y devuelve un objeto con start, end y diff (diferencia entre ambas)
Vamos por partes con el <li> que parece confuso.
v-for
Usamos un rango de valores de 0 a diff
click
Asignamos a pagination.current el valor del item + start dela computada (que es el valor del primero de los números mostrados en la paginación)
aria-current
Se le asigna la semántica que pide Bootstrap si la pagína es la actual
<a>
Se muestra el valor del ítem + start
<span>
Se resuelve la semántica si el ítem + start es igual a pagination.current (pagína actual)
Espero haber dejado todo claro sino estoy abierto a preguntas y sugerencias.
Enlaces:
- Codesandbox: https://github.com/Gonzalo2310/jQuery-Vuejs/blob/master/Table/Simple/indexTableSimple.html
- GitHub: https://github.com/Gonzalo2310/jQuery-Vuejs/blob/master/Table/Simple/indexTableSimple.html
Consultas, dudas, comentarios: Slack de PEUM o en Twitter.