En mi post anterior senté las bases para seguir construyendo; ahora es el momento de empezar "de verdad".
<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>
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
<script th:src="@{/webjars/axios/dist/axios.js}" src="//cdn.jsdelivr.net/npm/[email protected]/dist/axios.min.js"></script> <!--1--> <script th:src="@{/webjars/vue/dist/vue.global.js}" src="//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
.
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');
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.
<template id="todos-app"> <!--1--> <h1>{{ title }}</h1> <!--2--> </template>
title
; aún queda por configurar
const TodosApp = { props: ['title'], //1 template: document.getElementById('todos-app').innerHTML, }
title
, la utilizada en la plantilla HTML
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
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 .
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 llamada
<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
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.
<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 para obtener más detalles. 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.
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.
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
.
Ir más allá: