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) y de componentes de Vue (haremos lo mismo que los plugins pero en componentes).


Y el proyecto es una galería de imágenes traído por medio de ajax. Esto nos acercará a fetch que es la implementación que JS de forma nativa nos propone. fetch y jQuery.ajax() tienen diferencias conceptuales y si se quiere saber más del tema: https://developer.mozilla.org/es/docs/Web/API/Fetch_API/Utilizando_Fetch, aunque iremos implementando más posts del tema.




Contaremos con dos archivos js externos que serán dos plugins / componentes y de esta manera podremos apreciar un poco más el funcionamiento.No soy experto en plugin de jQuery así que me disculpo por las cosas que se podrían haber hecho mejor. Los demás errores son pura torpeza.


JQUERY - PLUGIN MODAL


Vamos con el código del primer plug-in. Un Modal.

Enlaces:

(function ($) {

$.fn.imageModal = function (options) {

let settings = $.extend({

url: '',

open: true

}, options)

if (!settings.url && settings.open) {

console.error('Url es necesario para mostrar la imagen')

return

}

if (jQuery('#linkeado').length === 0) {

jQuery('head').append('<link id="linkeado" rel="stylesheet" href="./plugins/modal/modal.css">')

}


if (settings.open) {

return this.append(`<div class="modal showModal" tabindex="-1" role="dialog" id="modalImagen" >

<div>&nbsp;</div>

<div class="modal-dialog">

<div class="modal-content">

<div class="modal-header">

<h5 class="modal-title">Datos de la imagen</h5>

<button type="button" class="close" data-dismiss="modal" aria-label="Close" id="buttonClose">

<span aria-hidden="true">&times;</span>

</button>

</div>

<div class="modal-body">

<img src="${settings.url}" id="imageModalCenter">

</div>

<div class="modal-footer">

<button type="button" class="btn btn-secondary" data-dismiss="modal" id="modalClose">Cerrar</button>

</div>

</div>

</div>

<div>&nbsp;</div>

</div>`)

}

jQuery('#modalImagen').removeClass('showModal').addClass('hideModal')

setTimeout(function () {

jQuery('#modalImagen').remove()

jQuery('#linkeado').remove()

}, 350)

}

}(jQuery))


Vayamos por partes.


La primera linea function ($) es para evitar conflictos con el símbolo de dólar. Esto es porque no es un símbolo exclusivo de jQuery y si se usara alguna otra librería que también la hiciera función de ese símbolo podría provocar conflictos. Es una buena costumbre definirla siempre así aunque aquí no haga falta. Sería más lógico en entornos más impredecibles como un CMS donde no se puede saber exactamente que librerías pueden estar cargadas cuando uno escribe un plugin o theme genérico.


La linea:


$.fn.imageModal


define el nombre del plug-in (imageModal)

Veamos como pasamos los parámetros:


function (options) {

let settings = $.extend({

url: '',

open: true

}, options)

Aquí se esta asociando a una variable local el objeto options pero parseado de manera que si falta un valor se le asigne uno por defecto. En este caso se espera un objeto con dos campos: url y open. Open es boolean y url es string. Como el modal es para mostrar una imagen url tendrá la url de la imagen. Open por el contrario controlará si se abre o cierra el modal para usar las clases correspondientes y otras acciones que iremos viendo.


Un ejemplo de uso es:


jQuery('body').imageModal({

url: jQuery(evt.target).data('full'),

open: true

})


Estas líneas las explicaremos más tarde. Continuemos.


if (!settings.url && settings.open) {

console.error('Url es necesario para mostrar la imagen')

return

}


Un plug-in de jQuery no deja de ser una función por lo que un return termina su ejecución. Aquí verificamos que la url no sea null ('') cuando open es true. Eso es porque se necesita la url de la imagen para mostrarla. En cambio cuando open es false es que el modal se cerrara por lo que la imagen ya se está mostrando.


En la siguiente linea se usa una solución personal al siguiente problema: Necesitamos el css cargado desde el plug-in. Por lo que debemos cargarlo pero SOLO una vez. Hay muchas maneras de verificar su existencia pero hemos optado por la menos tradicional en este caso pero la que vemos más sencilla: asignarle un id al elemento link del head del elemento HTML


if (jQuery('#linkeado').length === 0) {

jQuery('head').append('<link id="linkeado" rel="stylesheet" href="./plugins/modal/modal.css">')

}


Siempre que usamos jQuery('#id') nos devolverá un resultado aunque no exista. La forma de verificar que es un valor existente (o una de las maneras) es usar el campo length y compararlo a 0. Si es igual a ese valor es que no hay elementos con ese id.


En este caso verificamos que no halla un elemento HTML con ese id. Si no existe creamos un <link> en <head> y le asignamos el id correspondiente.


Lo siguiente es a creación del modal:


if (settings.open) {

return this.append(`<div class="modal showModal" tabindex="-1" role="dialog" id="modalImagen" >

<div>&nbsp;</div>

<div class="modal-dialog">

<div class="modal-content">

<div class="modal-header">

<h5 class="modal-title">Datos de la imagen</h5>

<button type="button" class="close" data-dismiss="modal" aria-label="Close" id="buttonClose">

<span aria-hidden="true">&times;</span>

</button>

</div>

<div class="modal-body">

<img src="${settings.url}" id="imageModalCenter">

</div>

<div class="modal-footer">

<button type="button" class="btn btn-secondary" data-dismiss="modal" id="modalClose">Cerrar</button>

</div>

</div>

</div>

<div>&nbsp;</div>

</div>`)

}


Si settings.open (valor del parámetro que se pasa cuando se usa el plug-in) es true RETORNA ( o sea el plug-in termina aquí si open es true ) this.append(). Sabemos lo que es append porque lo hemos estado usando durante todos los post anteriores, pero ¿a qué objeto apunta this?. Revisemos este otro código un segundo:


El ejemplo de uso:

jQuery('body').imageModal({

url: jQuery(evt.target).data('full'),

open: true

})


Observamos que jQuery usa la referencia a un elemento HTML (body) y sobre él se usa el plug-in. Entonces el this.append() es lo mismo que jQuery('body').append() en este caso. La parte anterior a imageModal podría ser cualquier elemento HTML usando las múltiples opciones de jQuery para referirlos.


Lo que esta dentro de append (nótese que se están usando los tildes / comas especiales) es la estructura del modal. Destacaremos un par de cosas.


* Incluimos la clase showModal que es la clase que lo muestra con cierto efecto de movimiento.

* Hay dos ID importantes que llevaran a las funciones de cierre cuando se use el plug-in: buttonClose y modalClose El x en la parte superior derecha y el botón cerrar.

* Se usa la semántica ${settings.url} para referenciar la url (valor del parámetro cuando se usa el plug-in) en el src de la imagen. Funciona porque todo está encerrado con las tildes / comas especiales.


El siguiente código solo es válido cuando open es false.


jQuery('#modalImagen').removeClass('showModal').addClass('hideModal')

setTimeout(function () {

jQuery('#modalImagen').remove()

jQuery('#linkeado').remove()

}, 350)

Se elimina la clase showModal, se le asigna hideModal que hace el efecto contrario a la apertura y posteriormente se elimina el contenido HTML del modal y el <link> que referencia al css 350 milisegundos después (0.35s) (tiempo suficiente para ver el efecto de cierre)


JQUERY - PLUGIN GALLERY


En el plug-in siguiente veremos su implementación, ya que lo usaremos dentro del plug-in de la galería:


Enlaces

Lo primero es igual que el plug-in anterior:


(function ($) {

Por las razones antes explicadas.


Aquí estamos llamando al plug-in gallery


$.fn.gallery


Y definimos una lista de parámetros un poco mayor y de diferente complejidad asignados al objeto interno settings.


function (options) {

let settings = $.extend({

url: '', // Url para el primer llamado de imágenes

reload: '', // url para las siguientes llamadas

first: 6, // Cantidad de imágenes a traer la primera vez

step: 2, // cantidad de imágenes a traer en cada "cargar más..."

page: 1, // página inicial

loading: true, // Es un valor que usamos para reconocer la primera vez que se usa el plug-in

fields: {

page: 'page', // valor para parsear la url

limit: 'limit', // valor para parsear la url

image: 'download_url', // valor para parsear la url y el tratamiento de datos recibidos

full: 'download_url', // Valor para parsear la url y el tratamiento de datos recibidos

text: 'author' // valor para parsear la utl y el tratamiento de datos recibidos

}

}, options)


Comprenderemos el uso de cada valor a medida que avancemos en el código. Vamos a ello.

Primero parámetros requeridos como indispensables:


if (!settings.url) {

console.error('Url es necesario para recoger las imágenes')

return

}


Url es necesario porque contiene la url de la cual se obtendrán las primeras imágenes.


if (settings.page < 1) {

settings.page = 1

}

El sitio de donde obtendremos los datos nos pide un page === 1 como mínimo (en realidad acepta un page = 0 pero devuelve lo mismo que page = 1) así que nos aseguramos que si envían 0 o un número negativo se convierta en 1.

if (!settings.url.includes(`{${settings.fields.page}`) || !settings.url.includes(`{${settings.fields.limit}`)) {

console.error(`Son necesarios los parámetros ${settings.fields.page} y ${settings.fields.limit} para paginar el resultado.

Ejemplo: https://picsum.photos/v2/list?page={${settings.fields.page}}&limit={${settings.fields.limit}}`)

return

}

Este código lo iremos viendo poco a poco porque tiene que ver son él parseo que hacemos a la url. Trabajemos con la primera condición para entenderla usando un código que veremos en detalle después pero adelantaremos en este momento. Es el uso del plug-in.


jQuery('#playground').empty().gallery({

url: 'https://picsum.photos/v2/list?page={page}&limit={limit}',

reload: 'https://picsum.photos/v2/list?page={page}&limit={limit}',

first: 6,

fields: {

page: 'page',

limit: 'limit',

image: 'https://picsum.photos/id/{id}/400/300',

text: 'author',

full: 'download_url'

}

})


Aquí estamos vaciando el contenido del elemento HTML con id playground y le agregamos el HTML (que aún no hemos visto) del plug-in y para eso enviamos los parámetros que se muestran ahí. Vayamos de nuevo a la condición del plug-in:


settings.url.includes(`{${settings.fields.page}}`)


settings.url es la variable enviada con el parámetro. En este caso: 'https://picsum.photos/v2/list?page={page}&limit={limit}'


includes busca una subcadena en una cadena. En este caso settings.fields.page que es: 'page'


Traducido: settings.url.includes('{page}') Esto es lo que busca en la url y es así porque la url necesita un parámetro con un nombre que indique la página actual. En otras url podríamos usar otros nombres: pages, páginas, o lo que pida la url simplemente cambiando el settings.fields.page y el valor enviado en la url. Esto le da un poder de adaptación para poder usarla con otras apis.


En definitiva se busca que la url traiga {page} y {limit} porque luego cambiara esos valores por los valores numéricos necesarios en cada llamada.


Si esos cambios no llegan en la url se emite un console.error y un mensaje explicativo de como se debe enviar la información. El console.error se verá en el navegador en la parte de developer pero si trabajamos el plug-in de forma mas profesional a lo mejor tendríamos que pensar en agregar algún sistema de notificación más visual para estos fallos.


El siguiente código es una alternativa a lo que hacemos en el plug-in de modal y explicaré por qué nos parece más seguro el del modal:


if (settings.loading) {

jQuery('head').append('<link rel="stylesheet" href="./plugins/gallery/gallery.css">')

settings.loading = false

}


Esto funciona porque aunque no enviamos un valor para el parámetro loading lo definimos por defecto como true. Creara el link en head y cargara el css correctamente.

El problema surge, o puede surgir, si usamos más de una galería en la misma página. Volveríamos a usar el plug-in como nuevo y por lo tanto se volvería a crear el link en el head que a primeras no es grave pero estaríamos cargando innecesariamente dos veces el mismo archivo.


¿Por qué aquí se ha usado este método? Porque ha servido de excusa para compararlo con la forma anterior. Y porque seguramente es una lógica que más de uno ha pensado al leer este post.


Una vez dentro del if para evitar volver a crear el <link> en esta instancia del plug-in cambiamos el valor de settings.loading


Luego definimos un array vacío:


let elementos = []


y vamos a la primera función


function updateElementos(urlAjax, steps) {

let localUrl = urlAjax.replace(`{${settings.fields.page}}`, settings.page).replace(`{${settings.fields.limit}}`, steps)

jQuery.ajax({

async: false,

type: 'GET',

url: localUrl,

success: function (data) {

data.forEach(item => {

elementos.push(`<div class="elemento">

<figure style="float: right">

<img src="${process_url_image(item, settings.fields.image)}" alt="${item[settings.fields.text]}" data-full="${item[settings.fields.full]}" class="imagefull">

<figcaption>${item[settings.fields.text]}</figcaption>

</figure>

</div>`)

})

settings.page ++

}

})

}

Esta función contiene unas cuantas cositas así que vamos paso a paso. Parámetros:


updateElementos(urlAjax, steps) // la url que usaremos y cuantas imágenes se pedirán.


La primera linea:


let localUrl = urlAjax.replace(`{${settings.fields.page}}`, settings.page).replace(`{${settings.fields.limit}}`, steps)


Es un parseo para sustituir los valores en la cadena enviada por valores reales. Miremos el proceso con un envío real:


updateElementos(settings.url, settings.first)


recordemos que settings.url = 'https://picsum.photos/v2/list?page={page}&limit={limit}' y settings.first = 6


repasemos el replace con esos valores


let localUrl = urlAjax.replace('{page}', 1).replace('{limit}', 6) => 'https://picsum.photos/v2/list?page=1&limit=6'


Por eso verificamos que ambos parámetros existieran en el if del inicio del plug-in, ya que si no el plug-in se rompería.


Se pide la página 1 y 6 elementos que traiga. Ahora que hemos mostrado varias veces la url diremos que es una api gratuita y sencilla muy valida para ejemplos de post como este o programas simples. Si se quiere aprende más de esta url visitar: https://picsum.photos/


El tema del Ajax es un tema muy amplio al cual no veo necesidad de entrar ahora mismo pero si quieren saber más de ajax conceptual y el uso en jQuery: https://www.arkaitzgarro.com/jquery/capitulo-7.html


Aquí estamos usando los siguientes parámetros:


async:false // Esto detiene la ejecución del código hasta obtener respuesta (buena o mala) de la url a la que se pide información. Esto se hace así porque al no ser reactivo y tener que modificar el HTML necesito tener los datos para proseguir.

type: 'GET' // GET es lo que pide la api.

url: localUrl // La url recibida y ya parseada

success: es lo que se hará cuando todo ha ido bien. Vayamos a la función que se define ahí:


function (data) {

data.forEach(item => {

elementos.push(`<div class="elemento">

<figure style="float: right">

<img src="${process_url_image(item, settings.fields.image)}" alt="${item[settings.fields.text]}" data-full="${item[settings.fields.full]}" class="imagefull">

<figcaption>${item[settings.fields.text]}</figcaption>

</figure>

</div>`)

})

settings.page ++

}


Como procesemos los datos recibidos depende de como son enviados. No hay una receta mágica para esta respuesta. Siempre dependerá de como lo envía la Api por lo que a veces se tiene que hacer determinadas pruebas (por ejemplo usar console.log()) con los datos recibidos para ver que ha venido o usar la vista de develop de los navegadores para verificar los datos enviados y recibidos en la red.


En este caso recibimos lo siguiente:


(6) [{…}, {…}, {…}, {…}, {…}, {…}]

0:

author: "Alejandro Escamilla"

download_url: "https://picsum.photos/id/0/5616/3744"

height: 3744

id: "0"

url: "https://unsplash.com/photos/yC-Yzbqy7PY"

width: 5616

__proto__: Object

1: {id: "1", author: "Alejandro Escamilla", width: 5616, height: 3744, url: "https://unsplash.com/photos/LNRyGwIJr5c", …}

2: {id: "10", author: "Paul Jarvis", width: 2500, height: 1667, url: "https://unsplash.com/photos/6J--NXulQCs", …}

3: {id: "100", author: "Tina Rataj", width: 2500, height: 1656, url: "https://unsplash.com/photos/pwaaqfoMibI", …}

4: {id: "1000", author: "Lukas Budimaier", width: 5626, height: 3635, url: "https://unsplash.com/photos/6cY-FvMlmkQ", …}

5: {id: "1001", author: "Danielle MacInnes", width: 5616, height: 3744, url: "https://unsplash.com/photos/1DkWWN1dr-s", …}

length: 6


Un array con tantos objetos como hallamos indicado en limit, con los campos author / download_url / height / id / url y width.


Para este proyecto solo nos interesan: author / download_url e id


Una vez recibidos los datos hacemos un recorrido por el array con forEach y agregamos al array de elementos


<div class="elemento">

<figure style="float: right">

<img src="${process_url_image(item, settings.fields.image)}" alt="${item[settings.fields.text]}" data-full="${item[settings.fields.full]}" class="imagefull">

<figcaption>${item[settings.fields.text]}</figcaption>

</figure>

</div>


El div y el figure no tiene misterio. Recordar que las habilidades de CSS aquí no serán nunca brillantes porque no destaco en eso ni queriendo (y no quiero) así que la estética será siempre mejorable. El uso de figure para este proyecto es la mejor manera que brinda HTML de forma natural para asociar una imagen a un texto (en este caso el autor de la imagen). Para saber más de figure: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/figure


Revisemos el img:


<img src="${process_url_image(item, settings.fields.image)}" alt="${item[settings.fields.text]}" data-full="${item[settings.fields.full]}" class="imagefull">


Volvamos a repetir el código con que estamos usando el plug-in:


jQuery('#playground').empty().gallery({

url: 'https://picsum.photos/v2/list?page={page}&limit={limit}',

reload: 'https://picsum.photos/v2/list?page={page}&limit={limit}',

first: 6,

fields: {

page: 'page',

limit: 'limit',

image: 'https://picsum.photos/id/{id}/400/300',

text: 'author',

full: 'download_url'

}

})


y los valores por defecto:


function (options) {

let settings = $.extend({

url: '',

reload: '',

first: 6,

step: 2,

page: 1,

loading: true,

fields: {

page: 'page',

limit: 'limit',

image: 'download_url',

full: 'download_url',

text: 'author'

}

}, options)


y revisemos alt.


alt="${item[settings.fields.text]}"


settings.fields.text = 'author'


Y aquí una aclaración sobre los objetos de JS. Los campos de los objetos js son accesibles como si fueran array con el siguiente formato: objeto.campo = objeto['campo']. Usar una u otra manera es exactamente lo mismo.


Volviendo al alt y si recordamos lo que devuelve la api para el primer ítem:


author: "Alejandro Escamilla"

download_url: "https://picsum.photos/id/0/5616/3744"

height: 3744

id: "0"

url: "https://unsplash.com/photos/yC-Yzbqy7PY"

width: 5616


La conversión sería:


alt="${item[settings.fields.text]}" => alt="${item['author']}" => alt="Alejandro Escamilla"


Y sería lo mismo para data-full:


data-full="${item[settings.fields.full]}" => data-full="${item['download_url']}" => data-full="https://picsum.photos/id/0/5616/3744"


Este atributo del elemento HTML es importante porque será la url que enviaremos al modal.


Ahora revisemos el src:


src="${process_url_image(item, settings.fields.image)}"


Miremos la función que usamos aquí:


function process_url_image(item, field) {

if (field.includes('{')) {

let inicio = field.indexOf('{')

let final = field.indexOf('}')

let fieldFind = field.substring(inicio + 1, final)

return field.substring(0, inicio) + item[fieldFind] + field.substring(final + 1)

}

return item[field]

}


Recordemos el valor que hemos enviado en el parámetro fields.image


image: 'https://picsum.photos/id/{id}/400/300'


Esta es la manera que tiene la api de devolvernos una imagen con unas medidas de ancho y alto determinadas.


/id/{id} significa que debe ir /id/ de forma literal y luego el número de id de la imagen que queremos. /400/300 son las medidas en las que necesitamos esa imagen.


Se usa esta opción de la api porque las imágenes con muy diferentes entre si en cuanto a tamaño.

A la función enviamos el ítem completo y la url a parsear.

La función lo primero que hace es comprobar que la cadena de al url tiene el símbolo { porque si no no será parseable


if (field.includes('{')) {


Si no lo tiene devuelve


return item[field]


Ya que se debe considerar que si no será parseado debe ser un valor ya válido. Una vez dentro del if:


let inicio = field.indexOf('{')

let final = field.indexOf('}')


Buscamos las posiciones donde empieza y termina en la cadena el valor a parsear. El siguiente paso:


let fieldFind = field.substring(inicio + 1, final)


Obtiene el valor a parsear. O sea en la siguiente cadena: 'https://picsum.photos/id/{id}/400/300' obtiene 'id' que deberá ser, no solo el valor a sustituir, sino que también el nombre de un campo válido de los objetos recibidos de la api. Recordemos que si recibimos un valor id por cada imagen que llega.


substring toma la posición del carácter a extraer desde el inicio (inicio +1), hasta el final (final) sin incluir este ultimo.


Al final devolvemos la url reconstruida:


return field.substring(0, inicio) + item[fieldFind] + field.substring(final + 1)


En la cadena que estamos usando de ejemplo esto seria:


field.substring(0, inicio) => 'https://picsum.photos/id/'


item[fieldFind] => item['id']


field.substring(final + 1) => '/400/300'


y de esa manera devolvemos una url valida.


El texto que ponemos en el figcaption del figure HTML que guardamos en el array es el campo que hemos elegido en el parámetro que enviamos al usar el plug-in:


<figcaption>${item[settings.fields.text]}</figcaption>


settngs.fields.text = 'author'

<figcaption>${item['author']}</figcaption>


Y así hemos construido la llamada a la api, el parseo de la url de la url para mostrar la imagen y el texto que se mostrara cuando el puntero del raton este encima de la imagen. Por último y antes de salir de la función sumamos uno a la página actual:


settings.page ++


Ahora pasemos a un posible fallo en la estructura de este proyecto.


Este plug-in usa el de modal para funcionar no obstante cargar un js dentro de otro no me dado resultado en este proyecto (hay múltiples maneras según el tipo de entorno) así que por lo tanto, y temporalmente hasta que tenga una solución mejor, los scripts deben cargarse en el HTML de forma ordenada y antes de usar el código js:


<script src="plugins/modal/modal.js"></script>

<script src="plugins/gallery/gallery.js"></script>


Modal debe cargarse antes que gallery porque gallery usara modal en su código. De esa manera el plug-in del modal existirá para gallery.


Como lo tenemos disponible crearemos dos funciones. Una para la apertura del modal y otra para el cierre del mismo.


function modalOpen(evt) {

jQuery('body').imageModal({

url: jQuery(evt.target).data('full'),

open: true

})

jQuery('#modalClose').on('click', modalClose)

jQuery('#buttonClose').on('click', modalClose)

}


evt / evt.target y jQuery(evt.target) los hemos visto en detalle en anteriores post y usamos data('full') para acceder al atributo data-full del elemento que es donde hemos guardado la url que necesitamos.


Luego asociamos el evento clock de los id's del botón close y de la x de la parte superior del modal a una función modalClose:


function modalClose() {

jQuery('body').imageModal({

open: false

})

}


Que sencillamente envía la señal de open false al modal para que efectivamente comience el proceso de cierre del modal como lo hemos explicado más arriba.

Sigamos con gallery:

Ejecutamos el proceso del ajax por primera vez:


updateElementos(settings.url, settings.first)


y ahora tenemos el array elementos con los primeros 6 resultado ya parseados y preparados para presentar en el HTML.

Antes actualizamos variables para las siguientes llamadas:


if (settings.first !== settings.step) {

settings.page = Math.floor(settings.first / settings.step) + 1

} else {

++settings.page

}


Esto es para verificar una sola vez (la primera) cuanta información se ha traído esa primera vez, ya que las siguientes siempre será la misma cantidad de información y en el siguiente paso no repetir ni saltar datos.


Recordemos que first es para traer la primera vez y step es para las siguientes llamadas. Por lo tanto si son iguales sencillamente a page se le suma uno. Esto es porque en esta api las páginas se calculan en relación a total / limit. Y no aportamos total. Ese dato no viene de la api pero si limit. Si siempre pedimos la misma cantidad de datos iremos avanzando página a página pero si pedimos más o menos tendremos que recalcular en que página estamos.


Si son diferentes se divide la cantidad pedida la primera vez entre los valores que se irán pidiendo ahora y nos dará la página actual. Como queremos la siguiente sumamos una.


Este cálculo no es muy genérico. De hecho esta función es muy explícita para el funcionamiento de esta api en particular. Hay varios cálculos que se han dejado de lado, como por ejemplo el posible caso que step fuera superior a first o que las solicitudes tuvieran una petición par / impar. Eso daría una posición de página que podría o saltar datos o repetirlos.


Estos cálculos no son necesarios para este proyecto en particular pero si quisiéramos hacer más genérico el plug-in deberíamos repensarla.


Superado esto ya agregamos el HTML inicial:


this.append(`<div class="layout">

<div class="list">

<div class="container-list">

<div>&nbsp;</div>

<div class="elementos" id="elementos">

${elementos.join('')}

</div>

<div>&nbsp;</div>

<div><button class="btn btn-success" id="buttonUpload" type="button" >Cargar mas...</button></div>

<div>&nbsp;</div>

</div>

</div>

</div>

`)


Recordar que this apunta al elemento HTML que se eligió cuando se usó el plug-in, en este caso:


jQuery('#playground').empty().gallery({}) // this apuntaría a un elemento HTML con el id playground


No hay mucho truco aquí y casi toda la magia pertenece al css. No obstante vemos que usamos elementos.join('') para escribir el HTML generado anteriormente desde la api y que además hemos creado un botón para cargar más imágenes.


Un plug-in no deja de ser una función que ejecuta un proceso y luego termina. Sin embargo necesitamos que siga haciendo cosas (en este caso traer nuevas imágenes, abrir y cerrar el modal, etc. ) así que el camino para mantener el código vivo son los eventos.


El código asociado a eventos seguirá funcionando más allá de que el plug-in termine su ejecución. Por lo tanto el siguiente paso es crear los eventos que necesitamos para las situaciones adecuadas.


jQuery('.imagefull').on('click', modalOpen)


Cada elemento del array elementos tiene una clase imagefull y hemos asociado a todas ellas el modalOpen.


Ya hemos visto como el cierre se asocia al botón del modal una vez creado y el proceso de cierre.


Luego en realidad solo nos queda un evento más. Load more o cargar más.


jQuery('#buttonUpload').on('click', function () {

jQuery('.imagefull').off('click')

elementos = []

updateElementos(settings.reload, settings.step)

jQuery('#elementos').append(elementos.join(''))

jQuery('.imagefull').on('click', modalOpen)

if (settings.page > 490) {

jQuery('#buttonUpload').attr('disabled', 'true')

}

})


Hemos visto que hemos creado un botón para cargar más. Aquí asociamos el click a una función que hace lo siguiente:


jQuery('.imagefull').off('click')


Lo primero es eliminar el código asociado al evento click de las imágenes. Esto es un dato que no siempre se tiene en cuenta pero on es acumulativo. Eso quiere decir que si no se eliminan las funciones asociadas al evento que ya existen cuando actualicemos con nuevo contenido y volvamos a usar el on abriremos (en las imágenes que ya estaba) varias veces el modal de forma superpuesta.


Para evitar esa acumulación escribimos esa línea.

Vaciamos elementos (elementos=[]) porque el contenido que tenía ya ha sido puesto en el HTML y si lo conservamos cuando volquemos el contenido nuevo (se hace agregando al array lo que llega de la api) repetiremos los datos ya agregados.


Luego hacemos la llamada a la api usando la url para el cargar más y las cantidades de datos que vamos a traer


updateElementos(settings.reload, settings.step)


Los elementos que volcamos con el array.join('') la primera vez están dentro de un div con id elementos. Así que ahora agregamos a ese contenido el nuevo resultado traído de la api:


jQuery('#elementos').append(elementos.join(''))


Luego volvemos a asociar todas las imágenes existentes con el modal nuevamente:


jQuery('.imagefull').on('click', modalOpen)


Y aquí hay un tema interesante: Se usan en la misma función dos veces: jQuery('.imagefull'). ¿Por qué no asociarlo con una constante como hemos hecho en anteriores post? La razón es sencilla. Si lo hiciéramos antes de la llamada ajax los elementos nuevos no estarían agregados a donde la variable apunta, ya que solo habría tomado los que existían antes. Por lo tanto, para tener siempre actualizado el resultado, es necesario reconstruir la lista de objetos HTML a los que se apunta en cada ocasión.


Por último un detalle solo válido para esta api:

if (settings.page > 490) {

jQuery('#buttonUpload').attr('disabled', 'true')

}


Trayendo de dos elementos hay unas 490 páginas más o menos. Se va agregando contenido y por lo tanto podrían ser más cada día. Lo lógico con una api es que se nos envíe un valor del total para poder saber que tan cerca del final estamos o si lo hemos pasado. Pero como estamos usando esta api a modo de prueba podemos aceptar no tener ese dato y por eso inhabilitamos el botón cuando la página es mayor de 490


JQUERY - HTML


Ahora solo nos queda el HTML del cual casi que lo hemos visto todo:

Enlaces:

<div class="container">

<div class="row">

<div class="col-12" id="playground">

</div>

</div>

</div>


No hay nada destacable y en cuanto al js:


<script src="plugins/modal/modal.js"></script>

<script src="plugins/gallery/gallery.js"></script>

<script>

jQuery('#playground').empty().gallery({

url: 'https://picsum.photos/v2/list?page={page}&limit={limit}',

reload: 'https://picsum.photos/v2/list?page={page}&limit={limit}',

first: 6,

fields: {

page: 'page',

limit: 'limit',

image: 'https://picsum.photos/id/{id}/400/300',

text: 'author',

full: 'download_url'

}

})


</script>


Ya lo hemos revisado antes. Así que la parte de jQuery la tendríamos completa. Vayamos a Vuejs.


VUEJS - COMPONENTE MODAL


Lo primero es una diferencia en cuanto al ciclo de vida. Cuando un plugin jQuery termina su ejecución su ciclo de vida culmina. En cambio un componente es parte del ecosistema de Vuejs de manera permanente pudiéndose eliminar (v-if) o solo ocultar (v-show) y se puede controlar eventos en su ciclo de vida constantemente. Incluso con la posibilidad de conectar con la instancia principal o un componente padre es capaz de tener variables que no se reinicien con cada ciclo. La flexibilidad es diferente y por eso ya entramos en un mundo totalmente diferente al de los plugins jQuery


Hemos conservado los CSS idénticos.


Empezaremos igual. Con los componentes uno a uno. Primero el modal:

Enlaces

Empezamos con algo propio de Js y no de Vuejs:


const head = document.getElementsByTagName('head')[0]

const style = document.createElement('link')

style.href = 'components/modal/modal.css'

style.type = 'text/css'

style.rel = 'stylesheet'

head.append(style)


Para agregar el css correspondiente en la cabecera. El mismo método No me funciono en el plug-in de jQuery pero allí lo resolvimos de otra manera.


Igual que con el plug-in debemos darle un nombre único:


Vue.component('modal', { // En este caso modal


Parámetros:


props: {

url: {

type: String,

default: '',

validation: (value) => {

if (!value) {

console.error('Url es necesario para mostrar la imagen')

return false

}

return true

}

},

open: {

type: Boolean,

require: false,

default: false

}

}


Explico algo personal aquí. Yo tengo un TOC con la definición de parámetros en Vuejs. Me gusta poner siempre de que tipo son, si son requeridos o no y un valor por default en caso que no sea requeridos. Me parece la manera más clara de "documentar" con código una prop. Sin embargo hay maneras más sencillas de hacerlo. Si le interesa el tema puede revisar aquí: https://es.vuejs.org/v2/guide/components-props.html


En este proyecto, sin embargo, los parámetros necesitan validación (url) y un valor por defecto por si no se envía (open)


Revisemos los dos:

url: {

type: String,

default: '',

validator: (value) => {

if (!value) {

console.error('Url es necesario para mostrar la imagen')

return false

}

return true

}

}


Lo definimos de tipo String (cadena de caracteres), le damos un valor por defecto y aunque podríamos definirlo como require hemos optado por una validación. Esto lo he realizado porque no he encontrado otra forma (puede existir) de cambiar el mensaje de require. Así que como es un parámetro requerido le haremos una validación inicial.


La función de validación es sencilla. Si el valor es null emitimos un console.error con un mensaje y devolvemos false para informar de que algo ha pasado y se emita el seguimiento correspondiente también en console. Esto provoca algo así:




Donde además del mensaje se envía información de donde y porque ha sucedido el error.


El return de validator SIEMPRE debe ser un boolean


Open es sencillo:


open: {

type: Boolean,

require: false,

default: false

}


Tipo boolean (TRUE o FALSE), no requerido, y por defecto FALSE.


Solo usaremos una variable: Status:


data () {

return {

status: 3

}

}


Y la usaremos para controlar el estado de apertura / cierre del modal, ya que open es una prop y no podemos modificar su valor de forma directa. Pero al cambiar status veremos de cual manera externa SI cambiamos el estado de open.


Ese valor 3 es sobreescrito inmediatamente cuando el componente es creado:


created() {

this.status = this.open ? 0 : 1

}


Y será la lógica de esa variable durante todo el proceso. Si tiene 0 es que el modal está abierto y si tiene 1 es que el modal debe cerrarse. Miremos el ciclo de esa variable.


watch: {

open: function(val) {

this.status = val ? 0 : 1

},

status: function(val) {

if (val === 1) {

this.waitDestroy()

}

}

}

Traeremos la función y parte del HTML principal para entender el concepto de watch con más claridad.


waitDestroy() {

const me = this

setTimeout(()=>{

me.$emit('close')

}, 450)

}

<modal :url="url" :open="open" v-if="open" @close="closeModal"></modal>


Función del Vuejs del HTML principal:


closeModal() {

this.open=false

}

Ahora hablaremos de todo el proceso. En created sé le asigna un valor a status que dependerá del valor de la prop (parámetro) open, cuando es true el modal se abrirá. status cambiará a 0 y luego cuando se pulse el botón de cerrar o la x del modal a 1 y se activa el watch. Eso comprueba el valor (solo 1 nos interesa porque es el estado para cerrar el modal) y dispara un evento a los 450 milisegundos (0.45s). Ese evento que se llama close es pillado en el HTML principal y se ejecuta una función llamada CloseModal que cambia el valor de open del HTML principal y que también cambia de esa manera la prop. En el componente de nuevo se dispararía el watch pero no pasará porque tiene un v-if. El v-if eliminará el componente si open del HTML principal es false.

Aunque se disparara el watch de open pasaría lo siguiente: como open es false se le asignaría a status 1 pero no se dispararía el watch de status. Porque watch solo se dispara si el valor cambia. No se dispara a cada asignación. Eso quiere decir que si asignamos el mismo valor que tenía watch no hará nada. Y ahí terminaría el proceso.

Volvamos al orden.

Antes de ver el HTML del modal nos falta ver una computada:


computed: {

modalStatus() {

return ['showModal', 'hideModal', ''][this.status]

}

}


Me pareció interesante presentar esta opción de respuesta. Es un array creado en el momento y que devolverá la clase correspondiente según el valor de status. Como siempre es solo una forma de resolver un problema pero si no convence se pueden usar las maneras tradicionales.


Ahora veamos el HMTL del modal.


template: `<div class="modal" :class="modalStatus" tabindex="-1" role="dialog" id="modalImagen" >

<div>&nbsp;</div>

<div class="modal-dialog">

<div class="modal-content">

<div class="modal-header">

<h5 class="modal-title">Datos de la imagen</h5>

<button type="button" class="close" data-dismiss="modal" aria-label="Close" id="buttonClose" @click="status = 1">

<span aria-hidden="true">&times;</span>

</button>

</div>

<div class="modal-body">

<img :src="url" id="imageModalCenter">

</div>

<div class="modal-footer">

<button type="button" class="btn btn-secondary" data-dismiss="modal" id="modalClose" @click="status = 1">Cerrar</button>

</div>

</div>

</div>

<div>&nbsp;</div>

</div>`

})


Recordar el tema de las comillas / acentos. Aquí usamos la computada en :class. Tenemos dos @click="status = 1" que dispararan lo explicado antes para cerrar el modal. y un img con el :src que usa la prop url.


Al llegar aquí lo tenemos todo explicado de este componente o eso espero.


VUEJS - COMPONENTE GALLERY


Enlaces:

Volvemos a repetir la operación de cargar el CSS relacionado pero cuidando de no repetir nombres de variable o constantes usadas en el componente anterior:


const head2 = document.getElementsByTagName('head')[0]

const style2 = document.createElement('link')

style2.href = 'components/gallery/gallery.css'

style2.type = 'text/css'

style2.rel = 'stylesheet'

head2.append(style2)


llamamos al componente gallery:


Vue.component('gallery', {


y ahora revisemos las props:


props: {

url: {

type: String,

default: '',

validator: url => {

if (!url) {

console.error('La url es necesario para recoger las imágenes')

return false

}

return true

}

},

reload: {

type: String,

default: '',

validator: url => {

if (!url) {

console.error('reload es necesario para realizar el load more')

return false

}

return true

}

},

first: {

type: Number,

default: 6

},

step: {

type: Number,

default: 2

},

page: {

type: Number,

default: 1,

required: false

},

loading: true,

fields: {

type: Object,

default: () => {

return {

page: 'page',

limit: 'limit',

image: 'download_url',

full: 'download_url',

text: 'author'

}

}

}

}



url y reload requeridas con una validación propia como hemos visto en modal. first / step y page son Number con valores por default y ninguna es requerida. loading con la semántica más simple. Por el valor asignado por default se define el type. No es requerida. Y fields está definida como Object y default (al igual que con las props Array que en este proyecto no hay) debe devolver el resultado de una función. Así que devolvemos los campos que necesitamos (siguiendo la lógica de los parámetros del plug-in de jQuery)

Y necesitaremos dos variables:


data () {

return {

elementos: [],

localPage: 1

}

}


Elementos que es el array con las respuestas desde la api sin darle formato HTML ni parseo, ya que eso lo haremos directamente en el HTML al usar el array con v-for. Y localPage que sera la posición de la página actual, ya que deberemos ir cambiando su valor y page es una prop por lo que no podemos modificarla libremente.


Tenemos 3 métodos / funciones


process_url_image(item, field) {

if (field.includes('{')) {

let inicio = field.indexOf('{')

let final = field.indexOf('}')

let fieldFind = field.substring(inicio + 1, final)

return field.substring(0, inicio) + item[fieldFind] + field.substring(final + 1)

}

return item[field]

}


Esta función es idéntica a la correspondiente para procesar la url de la imagen del plug-in de jQuery de gallery. Es puro js. Es lógico que conservemos el código tal cual. Simplemente sustituimos el texto entre {} por el valor correspondiente. En este caso {id} por el valor del id del ítem. En la parte de jQuery lo explicamos en detalle.


updateElementos(urlAjax, steps, reload = false) {

let localUrl = urlAjax.replace(`{${this.fields.page}}`, this.localPage).replace(`{${this.fields.limit}}`, steps)

const me = this

fetch(localUrl)

.then(response => response.json())

.then(data => {

if (reload) {

me.elementos = me.elementos.concat(data)

me.localPage ++

} else {

me.elementos = data

}

})

}

Esta función si tiene variantes. Primero debemos aclarar que Vuejs no aporta ninguna solución para hacer llamadas Ajax. La filosofía de Vuejs es aportar una solución centrada en la reactividad sin sustituir el js nativo y sin abarcar más que lo necesario. Para usar ajax con Vuejs se debe usar una librería externa. Lo más normal es usar Axios, sin embargo en este proyecto usaremos fetch. Una solución nativa de JS. Optar por una u otra opción depende de con cual nos sintamos más cómodos. La solución de Axios es una solución potente sin dudas. fetch es nativo y durante los últimos años ha ido evolucionando a mejor.

Información de fetch: https://developer.mozilla.org/es/docs/Web/API/Fetch_API/Utilizando_Fetch

Axios: (el readme del repositorio de axios me ha parecido, hasta ahora, la mejor manera de aprenderlo a usar) https://github.com/axios/axios


fecth trabaja con promesas (https://developer.mozilla.org/es/docs/Web/JavaScript/Guide/Usar_promesas) que es un tema bastante amplio que no trataremos aquí pero sabiendo eso miremos el código:


const me = this

fetch(localUrl)

.then(response => response.json())

.then(data => {

if (reload) {

me.elementos = me.elementos.concat(data)

me.localPage ++

} else {

me.elementos = data

}

})


fetch hace una llamada ajax (GET es la petición predefinida) y recibe una promesa en el primer then. Definiremos ahí el tipo de respuesta esperada response.json() y con el segundo then resolvemos la promesa. data es ese resultado.


Habiendo aclarado (levemente) este punto pasemos a toda la función. Recibiremos 3 parámetros (uno más que en la función de jQuery). urlAjax es la url a parsear y cuya resolución es idéntica a la empleada en jQuery. steps es la cantidad de datos que solicitaremos y reload es un boolean con false por defecto que nos indicara si es una primera carga o no.


Creamos localUrl de la misma manera que en jQuery. Creamos una constante que apunta al this de la instancia del componente. Realizamos la operación ajax.


En data usamos el parámetro reload para comprobar si es o no una primera carga (reload false). Si es la primera llamada ajax que hacemos asignamos el resultado a la variable elementos y si no unimos arrays.


Concat es una función de los arrays js que permite unirlos. Además de concadenarlos sumamos uno a la variable localPage.


El último método / función:


moreLoad() {

this.updateElementos(this.reload, this.step, true)

}


Sencillamente hará la llamada ajax cuando se pulse le botón de cargar mas.


Por último la parte del HTMl de la galería:


template: `<div class="layout">

<div class="list">

<div class="container-list">

<div>&nbsp;</div>

<div class="elementos" id="elementos">

<div class="elemento" v-for="item in elementos" :key="item.id" @click="$emit('open', item[fields.full])">

<figure style="float: right">

<img :src="process_url_image(item, fields.image)" :alt="item[fields.text]" :data-full="item[fields.full]" class="imagefull">

<figcaption>{{item[fields.text]}}</figcaption>

</figure>

</div>

</div>

<div>&nbsp;</div>

<div><button class="btn btn-success" id="buttonUpload" type="button" @click="moreLoad" :disabled="page > 490">Cargar mas...</button></div>

<div>&nbsp;</div>

</div>

</div>

</div>`

})


La clave del renderizado está en el v-for por supuesto:


<div class="elemento" v-for="item in elementos" :key="item.id" @click="$emit('open', item[fields.full])">

<figure style="float: right">

<img :src="process_url_image(item, fields.image)" :alt="item[fields.text]" :data-full="item[fields.full]" class="imagefull">

<figcaption>{{item[fields.text]}}</figcaption>

</figure>

</div>


Hay varios cambios en cuanto a jQuery y los paradigmas. Lo primero está en las llamadas ajax. Mientras en jQuery vaciamos el array y lo completados con lo nuevo para agregar al HTML el contenido nuevo en Vuejs simplemente agregamos al array el contenido nuevo dejando el que teníamos de antes. Esto es porque al modificar el array el renderizado se actualiza por la reactividad. De esa manera no tenemos que estar manipulando el DOM. Si queremos eliminar un elemento lo hacemos del array, si queremos sumar uno lo agregamos al array. Si queremos eliminarlos todos basta un elementos=[].


Lo siguiente está en el renderizado. Más allá de todo lo que hemos visto de v-for y el uso de variables en el renderizado los eventos no los estamos relacionando con funciones internas (que se podría hacer) sino que emitimos eventos propios que el componente padre (en este caso el HTML principal) puede capturar y usar. Esto permite que el componente simplemente trabaje con su contenido, resuelva sus propios problemas (llamada ajax, renderizado, control de página actual, etc.) sin tener que preocuparse de acciones extra (en este caso mostrar la imagen en un modal) dándole la oportunidad al componente padre de hacer lo que necesita cuando un evento se produce (incluso de no hacer nada)

Esto sucede así:


<div class="elemento" v-for="item in elementos" :key="item.id" @click="$emit('open', item[fields.full])">


Cuando se hace un click en el div que contiene un ítem se dispara un evento llamado open y envía el item[fields.full] que es la url de la imagen.


En el HTML principal sucede esto:


<gallery url="https://picsum.photos/v2/list?page={page}&limit={limit}" reload='https://picsum.photos/v2/list?page={page}&limit={limit}'

:first="6" :fields="{ page: 'page', limit: 'limit', image: 'https://picsum.photos/id/{id}/400/300', text: 'author', full: 'download_url'}"

@open="openModal">

</gallery>


Ya iremos explicando todo pero ahora nos concentraremos en:


@open="openModal"


Ahí capturamos el evento open del componente y ejecutamos la función openModal del HTML principal:


openModal(url) {

this.url = url

this.open = true

}


En el parámetro url recibimos lo que enviamos en el emit del componente gallery (item[fields.full]) y simplemente actualizamos dos variables del HTML principal.


Cuando open pasa a true se abre el modal porque también tenemos este componente:


<modal :url="url" :open="open" v-if="open" @close="closeModal"></modal>


Que ha recibido el open = true y la url con el valor que enviamos en el evento. Y listo. Hemos comunicado dos componentes haciendo que actúen según eventos propios.


En cambio con el botón de cargar más asociamos el evento click con una función del mismo componente:


<button class="btn btn-success" id="buttonUpload" type="button" @click="moreLoad" :disabled="page > 490">Cargar más...</button>


La decisión de emitir un evento o solucionarlo internamente depende de como se quiera resolver cada componente. También es interesante que si se hace de una manera u otra se tenga algún tipo de documentación (un simple txt en la carpeta) que nos sirva de referencia para no perder el norte cuando usamos muchos componentes / plugins / archivos externos.

Este componente está completo. Miremos el HTML principal de Vuejs.


VUEJS - HTML


Enlaces:

HTML:

<div id="app">

<modal :url="url" :open="open" v-if="open" @close="closeModal"></modal>

<div class="container">

<div class="row">

<div class="col-12" id="playground">

<gallery url="https://picsum.photos/v2/list?page={page}&limit={limit}" reload='https://picsum.photos/v2/list?page={page}&limit={limit}'

:first="6" :fields="{ page: 'page', limit: 'limit', image: 'https://picsum.photos/id/{id}/400/300', text: 'author', full: 'download_url'}"

@open="openModal">

</gallery>

</div>

</div>

</div>

</div>


y el js:


<script>

new Vue({

el: '#app',

data: {

url: '',

open: false

},

methods: {

openModal(url) {

this.url = url

this.open = true

},

closeModal() {

this.open=false

}

}

})

</script>


El HTML de VueJs es diferente en el código de ambos componentes. Lo demás es igual. Miremos cada uno:


<modal :url="url" :open="open" v-if="open" @close="closeModal"></modal>


Aquí pasamos dos parámetros, validamos el componente (v-if) solo cuando open sea true y relacionamos el evento close con una función.


<gallery url="https://picsum.photos/v2/list?page={page}&limit={limit}" reload='https://picsum.photos/v2/list?page={page}&limit={limit}'

:first="6" :fields="{ page: 'page', limit: 'limit', image: 'https://picsum.photos/id/{id}/400/300', text: 'author', full: 'download_url'}"

@open="openModal">

Este (aunque parezca más complejo) es muy parecido al anterior componente. Solo estamos atentos a un evento (open) relacionado con una función (openModal) y enviamos 4 parámetros:


url="https://picsum.photos/v2/list?page={page}&limit={limit}" // sin los : delante porque estamos enviando una cadena de testo literal. No es necesario que Vuejs la procese porque no es una variable, un número, ni una función.


reload='https://picsum.photos/v2/list?page={page}&limit={limit}' // idéntica que la anterior. Explicamos por qué teníamos dos campos iguales cuando hablamos del plug-in correspondiente.


:first="6" // Con los : porque no es una cadena si no un número y por lo tanto debemos decirle que no es literal.


:fields="{ page: 'page', limit: 'limit', image: 'https://picsum.photos/id/{id}/400/300', text: 'author', full: 'download_url'}" // Una manera poco estética de enviar un objeto. Se podría haber creado una variable con los valores y enviado la variable. Pero como la idea es presentar variantes y opciones quisimos mostrar como hacer el envío de un objeto creado directamente. Además estos valores solo son válidos dentro del componente no es necesario manipularlos luego.


Las dos variables son los valores que cambiaran para abrir y mostrar contenido en el modal

Y las dos funciones las hemos descrito cuando explicamos el componente de modal.


Y aquí terminamos este "cortito" post. Espero que fuera sido útil y ya saben que estamos a la escucha de dudas o sugerencias. Gracias.


Enlaces


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



2 comentarios:

  1. No sabía yo que tuvieses un blog, de lo que se entera uno.. 😹😹😹

    ResponderEliminar
    Respuestas
    1. Es el blog de la comunidad de PEUM. Gracias por la difusión.

      Eliminar

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) ...