Vuejs para programadores jQuery. Una mini y sencilla hoja de calculo XII

Vuejs para programadores jQuery.

Una mini y sencilla hoja de calculo XII


En este post aflojaremos el ritmo de cosas nuevas para utilizar un poco lo que sabemos y realizaremos el inicio de una hoja de cálculo o por lo menos seguiremos esa lógica.


Haremos un cálculo de gastos mensuales y cada ítem tendrá 4 campos:

  • Si está activo: Eso nos permitirá excluir un valor del cálculo
  • Un título: Para distinguirlo visualmente y saber de que hablamos
  • Un precio: Es el gasto que significa.
  • Mensual: Si el precio es un gasto mensual o si es un gasto diario (es muy grosso modo, podríamos afinar más pero para el post nos servirá)

JQUERY


Esta vez lo único externo que usaremos es el CSS.


Comencemos con el código:

HTML:


<div class="top20 content contenedor">

<div class="row">

<div class="col-12">

<ul class="list-group" id="tablaExcel">

<li>

<div class="columnExcel head">

<div>¿Activo?</div>

<div>Indice</div>

<div>Titulo</div>

<div>Precio</div>

<div>Calculo</div>

<div>¿Mensual?</div>

</div>

</li>

<li id="tablaBody">

</li>

<li>

<div class="columnExcel footer">

<div>&nbsp;</div>

<div>&nbsp;</div>

<div>Total:</div>

<div>&nbsp;</div>

<div id="bodyTotal">0</div>

<div>&nbsp;</div>

</div>

</li>

</ul>

</div>

<div class="col-12">

<div class="form-group controlExcel">

<div class="form-group form-check">

<input type="checkbox" class="form-check-input" id="formActive">

<label class="form-check-label" for="formActive">¿Activo?</label>

</div>

<div class="input-group mb-2">

<label class="form-check-label" for="formTitle">Titulo&nbsp;</label>

<input type="text" class="form-control" id="formTitle">

<div class="invalid-feedback">

Se necesita el titulo

</div>

</div>

<div class="input-group mb-2">

<label class="form-check-label" for="formPrice">Precio&nbsp;</label>

<input type="number" min="0" class="form-control" id="formPrice">

<div class="invalid-feedback">

Se necesita el precio

</div>

</div>

<div class="form-group form-check">

<input type="checkbox" class="form-check-input" id="formMonth">

<label class="form-check-label" for="formMonth">¿Mensual?</label>

</div>

</div>

</div>

<div class="col-12 contenedor">

<button type="button" class="btn btn-primary" id="formAction">Agregar</button>

</div>

</div>

</div>

</div>


Resumido lo que tenemos aquí es un <li> vacío, dos checkbox (activo y mensual) dos input (titulo y precio) y por último un botón.


Antes del código una aclaración:

¿Por qué usamos &nbsp; ? Lo primero es explicar que es &nbsp; en el contexto del HTML es un espacio en blanco. Lo tenemos que poner así porque el renderizado del HTML (menos en algunos elementos) elimina los espacios continuados al principio y al final de la cadena. Y en este caso, con un único espacio, dependiendo del motor de renderizado puede devolver un <div> vacío y muchos navegadores eliminan los <div> vacíos.


¿Por qué necesitamos los <div> vacíos? porque estamos usando la clase columnExcel que usa el display grid que espera 6 <div> para renderizar y si el navegador elimina alguno ya no cuadra el diseño.


¿Hay otras maneras de hacer esto? Si. Muchas. Como estamos usando Bootstrap podríamos usar su layout, pero como no es tema del post solo dejaré un link para los que quieran saciar su curiosidad: https://getbootstrap.com/docs/4.5/layout/grid/


Si se quiere saber más de display grid: https://css-tricks.com/snippets/css/complete-guide-grid/


Código js:


Iremos por partes. Primero la formación de la tabla y los cálculos relacionados y posteriormente como agregamos un nuevo valor.

La variable que necesitamos para esto es:


const productos = [

{

activo: true,

titulo: 'Luz',

precio: 30,

mensual: true,

index: 1

},

{

activo: true,

titulo: 'Cafe',

precio: 1.5,

mensual: false,

index: 2

}

]


Simplemente es un array inicial.

Y las dos constantes:


const tabla = jQuery('#tablaBody') // Apunta al <li> vacío donde mostraremos los elementos

const tablaTotal = jQuery('#bodyTotal') // Apunta al <div> que contiene el valor total de todos los ítems.


Veamos el template relacionado


function templateBody({activo, titulo, precio, index, total, mensual}) {

let response = `<div class="columnExcel body">`

response += `<div onclick="changeActive(${index})" class="pointer">${activo}</div>`

response += `<div>${index}</div>`

response += `<div>${titulo}</div>`

response += `<div><input type="number" id="inputPrice${index}" value="${precio}" oninput="changePrice(${index})"/></div>`

response += `<div>${total}</div>`

response += `<div onclick="changeMonth(${index})" class="pointer">${mensual}</div>`

response += `</div>`

return response

}


Esta función tiene cosas que hemos usado antes y algunas nuevas. Primero que nada volvemos a usar la desestructuración en el lugar de los parámetros. En la segunda y séptima linea estamos usando onclick para asociar funciones al evento click de esos <div> (el evento click puede darse en cualquier elemento HTML no solo en botones o selects) y en la quinta linea ponemos un input asociando el evento oninput a una función para cambiar el precio.


Veamos un detalle: Tendremos que recoger el valor del form input pero además modificar el valor en el array. Para eso necesitamos el id del input y además el index del elemento para poder identificarlo dentro del array. Por otro lado tenemos que lograr id's únicos para cada input cuando este template estará en un loop del array de los productos.


Vamos a extraer el input y ver como resolvemos todos esos puntos:


<input type="number" id="inputPrice${index}" value="${precio}" oninput="changePrice(${index})"/>


Veamos el id: inputPrice${index}, como cada elemento tiene un index único esta conjunción de palabra+index nos dará el id único que necesitamos


Veamos el evento: oninput="changePrice(${index})" Enviamos a la función únicamente el index. Ese dato es suficiente para resolver todos los problemas. Veamos la función:


function changePrice(index) {

const result = productos.findIndex(item => item.index === index)

if (result < 0) {

return

}

productos[result].precio = parseFloat(jQuery('#inputPrice' + index).val() ? jQuery('#inputPrice' + index).val() : 0)

actualizar()

}


Lo primero que hacemos es buscar el index que coincide y usar findIndex para encontrar la posición en el array que corresponde.

Verificamos que el resultado es valido, si no lo es (un valor negativo) salimos de la funcion


De no ser así cambiamos el valor del precio en el array en la posición encontrada, revisemos el contenido de parseFloat:


jQuery('#inputPrice' + index).val() ? jQuery('#inputPrice' + index).val() : 0


Identificábamos al input con inputPrice${index} y aquí usamos el mismo método para identificar el form input involucrado pero validamos que no sea null, ya que al ser un valor editable si borramos todo su contenido el valor será null. En caso de ser null guardamos en el elemento el valor 0


Luego actualizamos.


Veamos las dos funciones que cambian los <div> de activo y de mensual.


function changeActive(index) {

const result = productos.findIndex(item => item.index === index)

if (result < 0) {

return

}

productos[result].activo = !productos[result].activo

actualizar()

}


function changeMonth(index) {

const result = productos.findIndex(item => item.index === index)

if (result < 0) {

return

}

productos[result].mensual = !productos[result].mensual

actualizar()

}

Son casi idénticas las 3 solo que en estos dos casos no necesitamos saber el valor que trae ya que al hacer click sobre los <div> se busca cambiar un valor boolean. Ya hablamos de esta forma de hacerlo anteriormente en un código Vuejs, miremos el primer caso como ejemplo:


productos[result].activo = !productos[result].activo

Estamos diciendo que guardaremos en activo el valor actual negado(!). En el caso de boolean Si tiene TRUE su valor negado es FALSE y lo mismo a la inversa.

Para los cálculos de resultados usamos dos funciones. Una es para el calculo de precio de cada ítem (y cambia los valores boolean por mensajes) y es la siguiente:

function calcItem(item) {

return {

activo: item.activo ? 'Si' : 'No',

index: item.index,

precio: item.precio ? item.precio : 0,

titulo: item.titulo,

total: item.activo ? item.mensual ? item.precio : parseFloat(item.precio) * 30 : 0,

mensual: item.mensual ? 'Si' : 'No'

}

}

La función recibe un ítem y devuelve otro creado con los valores de ese ítem. Cambia activo y mensual por mensajes de texto, index y titulo los copia idénticos y precio verifica que no sea null por lo que comentábamos antes y si lo es guarda 0, en caso contrario guarda el valor que tiene en ese momento.


Además agrega un nuevo valor: total. Que es el cálculo del precio dependiendo de si es un precio mensual o no. Aquí usamos un if terciario dentro de otro. Estas técnicas pueden ser confusas y hay otras maneras de resolverlo pero a mi parecer es sencillo una vez se pilla el truco:


total: item.activo ? item.mensual ? item.precio : parseFloat(item.precio) * 30 : 0


Lo primero es si el ítem está activo, si es así entra en el segundo if terciario:


item.mensual ? item.precio : parseFloat(item.precio) * 30


Si es un precio mensual lo devuelve tal cual. Si no es un precio mensual multiplica el valor por 30 y devuelve el resultado


La otra función calcula el precio del total de todos los ítems:


function calcTotal() {

let total = 0

productos.forEach(item => {

if (item.activo && item.precio) {

total += item.mensual ? parseFloat(item.precio) : parseFloat(item.precio) * 30

}

})

return total

}

Recorremos todo el array, verificamos que el item este activo y que el precio no sea null y le sumamos el precio según sea un precio mensual o no.

Devolvemos el resultado.


La ultima función de esta parte es actualizar


function actualizar() {

tabla.empty()

tablaTotal.empty()

productos.every(item => tabla.append(templateBody(calcItem(item))))

tablaTotal.append(calcTotal())

}


Vaciamos el <li> donde mostraremos los datos y vaciamos el <div> donde mostramos el total calculado de todos los ítems.


Luego recorremos el array y por cada ítem creamos un nuevo objeto con calcItem procesado por templateBody


El resultado de calcTotal() lo guardamos en el <div> que vaciamos antes.


Hemos saltado la parte de agregar un nuevo elemento. Vayamos ahora a ello.


Hagamos un repaso de las constantes creadas:


const formAction = jQuery('#formAction') // Apunta al botón de agregar

const formTitle = jQuery('#formTitle') // Apunta al form input que tiene el nuevo titulo

const formPrice = jQuery('#formPrice') // Apunta al form input que tiene el precio

const formMonth = jQuery('#formMonth') // Apunta al checkbox de ¿Mensual?

const formActive = jQuery('#formActive') // Apunta al checkbox de si está activo o no


Y volvemos a crear la función findIndex como en el post anterior:


function findIndex() {

let index = 0

productos.forEach(item => {

if (item.index > index) {

index = item.index

}

})

return ++index

}


Lo primero es recordar como usar Bootstrap para el mensaje de error:


<input type="text" class="form-control" id="formTitle">

<div class="invalid-feedback">

Se necesita el titulo

</div>


Para que el mensaje de error se muestre el form input debe contener la clase is-invalid.


Para crear un nuevo elemento tenemos dos checkbox (Activo y mensual) y dos form input, uno de texto para el título y uno numérico para el precio.


El control que se hace es que tanto título como precio deben tener algún contenido.


function validate() {

let out = false

if (!formTitle.val()) {

formTitle.addClass('is-invalid')

out = true

}

if (!formPrice.val()) {

formPrice.addClass('is-invalid')

out = true

}

if (out) {

return

}

productos.push({

activo: formActive.prop('checked'),

titulo: formTitle.val(),

precio: parseFloat(formPrice.val()),

mensual: formMonth.prop('checked'),

index: findIndex()

})

formActive.prop('checked', false)

formMonth.prop('checked', false)

formTitle.val('')

formPrice.val('')

actualizar()

}

Primero que nada creamos una variable out con el valor de FALSE. Esto es para poder validar los dos input y en caso de error de uno de ellos no salir de la función hasta validar la siguiente.


Validadas ambas si una dio error se sale. Si ambas dieron error se habrá puesto el mensaje correspondiente en ambos form input's.


Esta es la tarea de los siguientes 3 if que le continúan. Si entro en alguno de los dos primeros if (o sea si titulo o precio es null) o en ambos out tendrá true y por lo tanto saldrá de la función en el tercer if.


Si ambos campos están bien creamos directamente en el push un nuevo objeto cuyos campos son:


  • activo: el estado del checkbox correspondiente
  • titulo: el contenido del form input correspondiente
  • precio: usamos parseFloat (porque el valor permite números decimales) para dar formato al valor de form input correspondiente
  • mensual: El estado del checkbox correspondiente
  • index: el resultado de findIndex()

Luego vaciamos los form input y quitamos el check a los chekcbox

Al final actualizamos


Ahora explicaremos estas funciones anónimas asociadas a dos eventos:


formAction.on('click', validate)

formTitle.on('input', function () {

formTitle.removeClass('is-invalid')

})

formPrice.on('input', function () {

formPrice.removeClass('is-invalid')

})

Lo que hacemos aquí es quitar el is-invalid cuando se escribe en los form input's. Es más una cuestión estética. Al escribir después de tener el mensaje de error el mensaje desaparece.

También es una cuestión de sentido común, ya que si conservamos el mensaje y se intenta crear otra vez pero falla algo los mensajes seguirían ahí y no podríamos saber que ha fallado si es que ha fallado algo.


VUEJS


Y con esto hemos terminado la parte de jQuery. Pasemos a Vuejs.

Enlaces:

En este caso el HTML es tremendamente importante porque casi todo lo resolvemos allí así que iremos parte a parte e iremos desvelando código Js a medida que avancemos:


Empecemos con la tabla:


<ul class="list-group" id="tablaExcel">

<li>

<div class="columnExcel head">

<div v-for="item in tableTitle" :key="item">{{item}}</div>

</div>

</li>

<li v-for="element in productos" :key="element.index">

<div class="columnExcel body">

<div @click="element.activo =!element.activo" class="pointer">{{element.activo ? 'Si' : 'No'}}</div>

<div>{{element.index}}</div>

<div>{{element.titulo}}</div>

<div>

<input type="number" v-model="element.precio"/>

</div>

<div>{{element.activo ? element.mensual ? element.precio : element.precio * 30 : 0}}</div>

<div @click="element.mensual =!element.mensual" class="pointer">{{element.mensual ? 'Si' : 'No'}}</div>

</div>

</li>

<li>

<div class="columnExcel footer">

<div>&nbsp;</div>

<div>&nbsp;</div>

<div>Total:</div>

<div>&nbsp;</div>

<div>{{fullTotal}}</div>

<div>&nbsp;</div>

</div>

</li>

</ul>


Revisemos los <li>. El primero:


<li>

<div class="columnExcel head">

<div v-for="item in tableTitle" :key="item">{{item}}</div>

</div>

</li>


tableTitle es un simple array que contiene los títulos de cabecera:


tableTitle: ['¿Activo?', 'Indice', 'Titulo', 'Precio', 'Calculo', '¿Mensual?']


El tercer <li> (no, no me he saltado el segundo, será el siguiente):


<li>

<div class="columnExcel footer">

<div>&nbsp;</div>

<div>&nbsp;</div>

<div>Total:</div>

<div>&nbsp;</div>

<div>{{fullTotal}}</div>

<div>&nbsp;</div>

</div>

</li>


Todo el HTML es igual que en jQuery pero usamos una computada: fullTotal. Veamos el código:

fullTotal() {

let total = 0

this.productos.forEach(item => {

if (item.activo && item.precio) {

total += item.mensual ? parseFloat(item.precio) : parseFloat(item.precio) * 30

}

})

return total

}


Recorre el array. Si el ítem está activo y el precio no es null (por el problema explicado en la parte de jQuery) se suma el valor del precio dependiendo si es un gasto mensual o no y se devuelve el resultado. Es casi idéntica que la que usamos en jQuery


Y ahora si el <li> del medio:

<li v-for="element in productos" :key="element.index">

<div class="columnExcel body">

<div @click="element.activo =!element.activo" class="pointer">{{element.activo ? 'Si' : 'No'}}</div>

<div>{{element.index}}</div>

<div>{{element.titulo}}</div>

<div>

<input type="number" v-model="element.precio"/>

</div>

<div>{{element.activo ? element.mensual ? element.precio : element.precio * 30 : 0}}</div>

<div @click="element.mensual =!element.mensual" class="pointer">{{element.mensual ? 'Si' : 'No'}}</div>

</div>

</li>


Usamos v-for para extraer los elementos del array de productos. Los cambios mas notables son la resolución de problemas sin funciones. En la línea segunda y novena cambiamos el estado de activo y mensual sin necesidad de funciones extra. Y en el input usamos el v-model directamente con la variable del ítem de precio, por lo que el cambio no requiere más nada.


Volvemos a usar el if ternario dentro del if ternario para mostrar el precio. Es igual al que usamos en jQuery pero en este caso puesto directamente.


Con esto resolvemos la parte de cálculos de la tabla. Veamos la parte de creación de uno nuevo.


<div class="col-12">

<div class="form-group controlExcel">

<div class="form-group form-check">

<input type="checkbox" class="form-check-input" id="formActive" v-model="active">

<label class="form-check-label" for="formActive" >¿Activo?</label>

</div>

<div class="input-group mb-2">

<label class="form-check-label" for="formTitle">Titulo&nbsp;</label>

<input type="text" class="form-control" :class="{'is-invalid': errorTitle}" id="formTitle" v-model="title" @input="errorTitle = false">

<div class="invalid-feedback">

Se necesita el titulo

</div>

</div>

<div class="input-group mb-2">

<label class="form-check-label" for="formPrice">Precio&nbsp;</label>

<input type="number" min="0" class="form-control" id="formPrice" v-model="precio" :class="{'is-invalid': errorPrecio}" @input="errorPrecio = false">

<div class="invalid-feedback">

Se necesita el precio

</div>

</div>

<div class="form-group form-check">

<input type="checkbox" class="form-check-input" id="formMonth" v-model="mensual">

<label class="form-check-label" for="formMonth">¿Mensual?</label>

</div>

</div>

</div>

<div class="col-12 contenedor">

<button type="button" class="btn btn-primary" id="formAction" @click="addProduct">Agregar</button>

</div>

Las variables definidas para esto son:

active: false, // checkbox activo

title: '', // form input titulo

precio: 0, // form input precio

mensual: false, // checkbox mensual

errorTitle: false, // controla el error del título

errorPrecio: false, // controla el error de precio

Los check box tienen poca historia y tenemos un post dedicado a ellos. Sin embargo en los form input de precio y titulo hemos agregado un control para hacer el mismo efecto que hacemos en jQuery de eliminar el mensaje de error cuando escribimos en el form input después de que halla sido señalado. Si usáramos la variable que guarda el contenido, esto lo hemos visto, entonces cada vez que el elemento tuviera un valor null se mostraría y la idea es que solo lo muestre cuando se valida al pulsar el boton de agregar y luego se borre si escribimos en el campo.

Antes de avanzar recordar que también aquí tenemos la función findIndex()

findIndex() {

let index = 0

this.productos.forEach(item => {

if (item.index > index) {

index = item.index

}

})

return ++index

}


Y es igual a las anteriores usadas en Vuejs y jQuery


Ahora veamos la función de validación y creación de nuevos elementos de la tabla


addProduct() {

if (!this.title) {

this.errorTitle = true

}

if (!this.precio) {

this.errorPrecio = true

}

if (this.errorTitle || this.errorPrecio) {

return

}

this.productos.push({

activo: this.active,

titulo: this.title,

precio: parseFloat(this.precio),

mensual: this.mensual,

index: this.findIndex()

})

this.active = false

this.title = ''

this.precio = ''

this.mensual = false

}


Primero miremos el sistema del error. Si el contenido del form input del título o del precio esta vacío pone la variable correspondiente a true y un tercer if verifica que si una de las dos es verdadera sale de la función. Ahora está mostrando el error. Esto se complementa con:


<input type="text" class="form-control" :class="{'is-invalid': errorTitle}" id="formTitle" v-model="title" @input="errorTitle = false">


El evento @input pone el error correspondiente a false. Lo que quiere decir que vemos el mensaje pero cuando escribamos en el desaparecerá.


Si no hay errores el proceso es crear un nuevo elemento usando las variables relacionadas. Lo creamos directamente en el push.


Y para terminar ponemos los checkbox a false y vaciamos los input form.


Creo que este post (como otros) evidencian el cambio de paradigma. Son ejemplos claros de que la forma de resolver los problemas en su mayoría cambian pero que todo lo relacionado con Js puro se conserva igual. De hecho siempre que se pueda resolver una situación con Js puro antes que con una particularidad del framework o librería es aconsejable hacerlo


Espero que quedara todo claro y que fuera de utilidad

Enlaces generales:


Consultas, dudas, comentarios: Slack de PEUM o en Twitter.


2 comentarios:

Vuejs para programadores jQuery. Galería. Load More XVI

Vuejs para programadores jQuery. Galería. Load More XVI En el artículo de hoy vamos a tratar el tema de plugins de jQuery (crearemos dos) ...