En mi post anterior senté las bases para seguir construyendo; ahora es el momento de empezar "de verdad".
Había oído hablar mucho de Vue.js. Además, un amigo que pasó de desarrollador a gerente me contó cosas buenas sobre Vue, lo que despertó aún más mi interés. Decidí echarle un vistazo: será el primer framework JavaScript "liviano" que estudiaré, desde el punto de vista de un novato, que es lo que soy.
Expliqué WebJars y Thymeleaf en la última publicación. Aquí se muestra la configuración, tanto del lado del servidor como del cliente.
Así es como integro ambos en el POM:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <!--1--> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> <!--2--> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>webjars-locator</artifactId> <!--3--> <version>0.52</version> </dependency> <dependency> <groupId>org.webjars.npm</groupId> <artifactId>vue</artifactId> <!--4--> <version>3.4.34</version> </dependency> </dependencies>
Estoy usando el enrutador Kotlin y los DSL de Bean en el lado de Spring Boot:
fun vue(todos: List<Todo>) = router { //1 GET("/vue") { ok().render("vue", mapOf("title" to "Vue.js", "todos" to todos)) //2-3 } }
Todo
Si estás acostumbrado a desarrollar APIs, estarás familiarizado con la función body()
; devuelve la carga útil directamente, probablemente en formato JSON. La render()
pasa el flujo a la tecnología de visualización, en este caso, Thymeleaf. Acepta dos parámetros:
/templates
y el prefijo es .html
; en este caso, Thymeleaf espera una vista en /templates/vue.html
Aquí está el código en el lado HTML:
<script th:src="@{/webjars/axios/dist/axios.js}" src="https://cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script> <!--1--> <script th:src="@{/webjars/vue/dist/vue.global.js}" src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script> <!--2--> <script th:src="@{/vue.js}" src="../static/vue.js"></script> <!--3--> <script th:inline="javascript"> /*<![CDATA[*/ window.vueData = { <!--4--> title: /*[[${ title }]]*/ 'A Title', todos: /*[[${ todos }]]*/ [{ 'id': 1, 'label': 'Take out the trash', 'completed': false }] }; /*]]>*/ </script>
Como se explicó en el artículo de la semana pasada, uno de los beneficios de Thymeleaf es que permite tanto la representación estática de archivos como la representación del lado del servidor. Para que la magia funcione, especifico una ruta del lado del cliente, es decir , src
, y una ruta del lado del servidor, es decir , th:src
.
Ahora, profundicemos en el código Vue.
Queremos implementar varias características:
Todo
Todo
completado, debe activar o desactivar el atributo completed
Todo
completadas.Todo
a la lista de Todo
con los siguientes valores:id
: ID calculado del lado del servidor como el máximo de todos los demás ID más 1label
: valor del campo Etiqueta para label
completed
: establecido en false
El primer paso es iniciar el framework. Ya hemos configurado la referencia para nuestro archivo vue.js
personalizado más arriba.
document.addEventListener('DOMContentLoaded', () => { //1 // The next JavaScript code snippets will be inside the block }
El siguiente paso es dejar que Vue administre parte de la página. En el lado HTML, debemos decidir qué parte de nivel superior administra Vue. Podemos elegir un <div>
arbitrario y cambiarlo más tarde si es necesario.
<div id="app"> </div>
En el lado de JavaScript, creamos una aplicación , pasando el selector CSS del HTML anterior <div>
.
Vue.createApp({}).mount('#app');
En este punto, lanzamos Vue cuando se carga la página, pero no sucede nada visible.
El siguiente paso es crear una plantilla Vue. Una plantilla Vue es una <template>
HTML normal administrada por Vue. Puedes definir Vue en Javascript, pero yo prefiero hacerlo en la página HTML.
Comencemos con una plantilla raíz que pueda mostrar el título.
<template id="todos-app"> <!--1--> <h1>{{ title }}</h1> <!--2--> </template>
title
; aún queda por configurar
En el lado de JavaScript, debemos crear el código de administración.
const TodosApp = { props: ['title'], //1 template: document.getElementById('todos-app').innerHTML, }
title
, la utilizada en la plantilla HTML
Por último, debemos pasar este objeto cuando creamos la aplicación:
Vue.createApp({ components: { TodosApp }, //1 render() { //2 return Vue.h(TodosApp, { //3 title: window.vueData.title, //4 }) } }).mount('#app');
render()
h()
para hiperíndice crea un nodo virtual a partir del objeto y sus propiedadestitle
con el valor generado en el servidor
En este punto, Vue muestra el título.
En este punto, podemos implementar la acción cuando el usuario hace clic en una casilla de verificación: debe actualizarse en el estado del lado del servidor.
Primero, agregué una nueva plantilla anidada de Vue para la tabla que muestra la Todo
. Para no alargar la publicación, evitaré describirla en detalle. Si te interesa, echa un vistazo al código fuente .
Aquí está el código de la plantilla de línea de partida, respectivamente JavaScript y HTML:
const TodoLine = { props: ['todo'], template: document.getElementById('todo-line').innerHTML }
<template id="todo-line"> <tr> <td>{{ todo.id }}</td> <!--1--> <td>{{ todo.label }}</td> <!--2--> <td> <label> <input type="checkbox" :checked="todo.completed" /> </label> </td> </tr> </template>
Todo
Todo
completed
es true
Vue permite el manejo de eventos a través de la sintaxis @
.
<input type="checkbox" :checked="todo.completed" @click="check" />
Vue llama a la función check()
de la plantilla cuando el usuario hace clic en la línea. Definimos esta función en un parámetro setup()
:
const TodoLine = { props: ['todo'], template: document.getElementById('todo-line').innerHTML, setup(props) { //1 const check = function (event) { //2 const { todo } = props axios.patch( //3 `/api/todo/${todo.id}`, //4 { checked: event.target.checked } //5 ) } return { check } //6 } }
props
para que podamos acceder a ella más tarde.event
que activó la llamadaEn la sección anterior cometí dos errores:
Lo haremos implementando la siguiente función, que es la limpieza de tareas completadas.
Ahora sabemos cómo manejar eventos a través de Vue:
<button class="btn btn-warning" @click="cleanup">Cleanup</button>
En el objeto TodosApp
, agregamos una función con el mismo nombre:
const TodosApp = { props: ['title', 'todos'], components: { TodoLine }, template: document.getElementById('todos-app').innerHTML, setup() { const cleanup = function() { //1 axios.delete('/api/todo:cleanup').then(response => { //1 state.value.todos = response.data //2-3 }) } return { cleanup } //1 } }
state
es donde almacenamos el modelo.
En la semántica de Vue, el modelo de Vue es un contenedor de datos que queremos que sean reactivos . Reactivo significa un enlace bidireccional entre la vista y el modelo. Podemos hacer que un valor existente sea reactivo pasándolo al método ref()
:
En Composition API, la forma recomendada de declarar el estado reactivo es usando la función
ref()
.
ref()
toma el argumento y lo devuelve envuelto dentro de un objeto ref con una propiedad .value.
Para acceder a las referencias en la plantilla de un componente, declárelas y devuélvalas desde la función
setup()
de un componente.
--Declaración del estado reactivo
Vamos a hacerlo:
const state = ref({ title: window.vueData.title, //1-2 todos: window.vueData.todos, //1 }) createApp({ components: { TodosApp }, setup() { return { ...state.value } //3-4 }, render() { return h(TodosApp, { todos: state.value.todos, //5 title: state.value.title, //5 }) } }).mount('#app');
title
. No es necesario ya que no hay un enlace bidireccional: no actualizamos el título del lado del cliente, pero prefiero mantener la coherencia en el manejo de todos los valores.state
En este punto, tenemos un modelo reactivo del lado del cliente.
En el lado HTML, utilizamos los atributos Vue relevantes:
<tbody> <tr is="vue:todo-line" v-for="todo in todos" :key="todo.id" :todo="todo"></tr> <!--1-2--> </tbody>
Todo
is
es fundamental para gestionar la forma en que el navegador analiza el código HTML. Consulte la documentación de Vue para obtener más detalles.He descrito la plantilla correspondiente más arriba.
Ahora podemos implementar una nueva función: agregar un nuevo Todo
desde el cliente. Al hacer clic en el botón Agregar , leemos el valor del campo Etiqueta , enviamos los datos a la API y actualizamos el modelo con la respuesta.
Aquí está el código actualizado:
const TodosApp = { props: ['title', 'todos'], components: { TodoLine }, template: document.getElementById('todos-app').innerHTML, setup() { const label = ref('') //1 const create = function() { //2 axios.post('/api/todo', { label: label.value }).then(response => { state.value.todos.push(response.data) //3 }).then(() => { label.value = '' //4 }) } const cleanup = function() { axios.delete('/api/todo:cleanup').then(response => { state.value.todos = response.data //5 }) } return { label, create, cleanup } } }
create()
propiamente dichaTodo
En el lado HTML, agregamos un botón y lo vinculamos a la función create()
. Asimismo, agregamos el campo Label y lo vinculamos al modelo.
<form> <div class="form-group row"> <label for="new-todo-label" class="col-auto col-form-label">New task</label> <div class="col-10"> <input type="text" id="new-todo-label" placeholder="Label" class="form-control" v-model="label" /> </div> <div class="col-auto"> <button type="button" class="btn btn-success" @click="create">Add</button> </div> </div> </form>
Vue vincula la función create()
al botón HTML. La llama de forma asincrónica y actualiza la lista reactiva Todo
con el nuevo elemento devuelto por la llamada. Hacemos lo mismo con el botón Limpiar para eliminar los objetos Todo
marcados.
Tenga en cuenta que no implementé intencionalmente ningún código de manejo de errores para evitar que el código fuera más complejo de lo necesario. Me detendré aquí porque obtuvimos suficientes conocimientos para una primera experiencia.
En esta publicación, di mis primeros pasos para ampliar una aplicación SSR con Vue. Fue bastante sencillo. El mayor problema que encontré fue que Vue reemplazara la plantilla de línea: no leí la documentación en profundidad y pasé por alto el atributo is
.
Sin embargo, tuve que escribir bastantes líneas de JavaScript, aunque utilicé Axios para ayudarme con las llamadas HTTP y no administré los errores.
En la próxima publicación, implementaré las mismas características con Alpine.js.
El código fuente completo de esta publicación se puede encontrar en GitHub:
Ir más allá: