Consumir una Rest Api con Vue 3 en CodeIgniter 4

Video thumbnail

Índice de contenido

Antes de comenzar a desarrollar nuestra aplicación en Vue.js, la cual va a consumir la API REST que tenemos implementada en CodeIgniter 4, me gustaría hablar un poco sobre la tecnología que vamos a emplear. Como te comentaba antes, elegimos Vue.

Puedes optar por Vue, React, Angular, o cualquier otra tecnología frontend si ya dominas alguna de ellas. Si no deseas aprender Vue, puedes perfectamente realizar esa capa de integración con otro framework o librería. En resumen, lo que haremos son peticiones mediante la librería Axios (lo cual explicaré más adelante), y construiremos ciertos componentes o bloques de HTML para el listado, además de algunos botones para las demás acciones como eliminar y otras funcionalidades. En esencia, de eso se trata.

Si no sabes nada de Vue, tengo este tutorial express en el cual puedes dar los primeros pasos con Vue.

¿Por qué Elegir Vue.js?

Yo utilizo Vue para crear Webs SPA (Single Page Application) porque, como mencioné al inicio, es una tecnología muy sencilla. Es una tecnología web, por lo tanto, encaja perfectamente con frameworks de servidor como CodeIgniter, ya que aunque Vue es una tecnología web del lado del cliente, sigue siendo una tecnología web.

Vue es sencilla de entender si la comparamos con otras alternativas. Además, al usarla, aprendemos una nueva tecnología como es Vue.js. Obviamente, Vue también tiene ciertas ventajas con respecto a, por ejemplo, React o Angular.

Nota: Esto, por cierto, es un fragmento de mi libro, el cual tiene un costo aparte. Lo empleo, al igual que el libro de CodeIgniter, para presentar ciertos aspectos teóricos o desarrollos que vayamos a realizar.

Las Ventajas de Vue y el Concepto SPA

Básicamente, la ventaja que nos da Vue (o frameworks similares como React o Angular) es el hecho de que no tenemos que manipular el DOM de manera manual. Con el DOM me refiero a la estructura de la página web.

Cuando cambiamos un estado o un status, por ejemplo, cuando un usuario pasa de no autenticado a autenticado, usualmente agregamos algunas opciones más al menú o quitamos el botón de login. Ninguna de esas acciones tenemos que hacerlas de manera manual.

Más allá de esto, también podemos construir una Web de tipo SPA (Single Page Application). Esto significa que, mediante datos que llamamos reactivos (los cuales están atados a la interfaz, como veremos más adelante), cuando cambiamos estos datos de forma programática (es decir, mediante el Script), los cambios se realizan o se ven reflejados automáticamente en la pantalla. Eso es, en esencia, una Web de tipo SPA.

Otros frameworks como React o Angular pueden hacer lo mismo, pero Vue es mucho más sencillo de entender, más ligero, y más amigable al momento de comenzar con una nueva tecnología.

Componentes, Props, y Observadores

A partir de aquí, tenemos muchas opciones, por ejemplo, el uso de Props (propiedades), que nos permiten modularizar componentes.

La palabra componente es algo que vas a escuchar bastante, ya que Vue está basado en componentes. Los componentes no son más que pequeñas piezas de código que cumplen una funcionalidad específica. Por ejemplo:

  • Puede ser un botón, el cual podemos aprovechar para que ejecute ciertas acciones, como eliminar.
  • Puede ser un modal directamente.
  • Puede ser una tabla o un listado.

Iremos creando algunos a medida que avancemos, pero quédate con esto:

  • Un componente es una pequeña pieza de código que puede ser reutilizada fácilmente.

Un componente puede ser:

  • De tipo genérico, como te comentaba, directamente un botón, y luego implementamos la acción dependiendo de lo que necesitemos.
  • Algo un poco más específico, por ejemplo, un listado de películas que luego nosotros podemos colocar en cualquier parte de la aplicación.

Propiedades, Observadores (Watchers) y Reactividad

También tenemos muchas características en Vue que, otra vez, iremos presentando poco a poco, como el uso de observadores o watchers. Estos nos permiten observar cambios sobre los datos reactivos, en este caso, enfocados desde el Script, no desde el HTML. Ya explicaré un poco más adelante cómo está formado un componente en Vue.

Lo de la Web de tipo SPA era lo que te comentaba sobre el DOM HTML (que es toda la página). Cuando cambiamos un dato reactivo (es decir, una variable en JavaScript), automáticamente se cambia en el HTML. En este caso, el observador que colocamos por acá viene siendo del lado del Script.

Puede que no me sigas mucho ahora; no importa si todavía no entiendes muy bien el concepto. Quédate simplemente con la idea de que Vue es un framework del lado del cliente de JavaScript que nos permite crear Webs de tipo SPA. Es decir, webs en las que, de manera sencilla y prácticamente automática, podemos cambiar el estado de múltiples elementos HTML. Todo depende de cómo lo implementemos a medida que vayamos cambiando determinados valores de unas variables (un conjunto de variables que también vamos a ver cómo definir).

️ Requisitos y Opciones de Instalación
Nuevamente, no es necesario tener ningún conocimiento previo en Vue; vamos a ir viendo todo desde cero. Pero en caso de que quieras profundizar más, recuerda que cuento con un libro asociado a la tecnología, y en YouTube también tengo varios videos, una lista de reproducción para comenzar a desarrollar en Vue.

Para emplear Vue, básicamente tenemos dos formas: mediante Node.js o mediante CDN (Content Delivery Network).

Estructura Básica de un Componente Vue

Un componente consta de tres elementos principales:

El HTML: Este elemento usualmente está sí o sí, que es una pequeña pieza de HTML. Como te comentaba antes, en este caso viene siendo un botón incremental.

El Script: Esta es la parte de la lógica. En este caso no se visualiza porque es una estructura muy sencilla, por lo que su inclusión es un poquito opcional.

La parte del Estilo (Style): Esta es bastante opcional, ya que usualmente se toma el estilo de una hoja madre (una hoja principal). Sin embargo, también lo puedes colocar directamente en el componente.

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<div id="app">{{ message }}</div>
<script>
  const { createApp } = Vue
  createApp({
    data() {
      return {
        message: 'Hello Vue!'
      }
    }
  }).mount('#app')
</script>

⚙️ El Arranque de la Aplicación

Y esta viene siendo el arranque de la aplicación, es decir, siempre tenemos, en este caso, una parte raíz de la aplicación en la cual arrancamos Vue.

Node.js y NPM: Elementos Clave

Recordemos que en el entorno de desarrollo tenemos dos elementos muy importantes:

Node.js: Que es como tal el entorno de ejecución de JavaScript.

NPM: Son las siglas de Node Package Manager, es decir, el manejador de paquetes de Node. Con él, nosotros podemos instalar paquetes, como en este caso viene siendo Vue.

⚠️ Solución al Error “Command Not Found”

Si no te aparece nada, te aparece como:

zsh: command not found:

O algo por el estilo (como el comando no encontrado), intenta reiniciar tu equipo. Por si las moscas, aquí te aparece por ejemplo esto (justamente el de comando no encontrado), intenta reiniciar tu equipo, y eso sería prácticamente todo.

✅ Recomendación Importante Durante la Instalación

Siempre estate pendiente, cuando estés instalando, de si te aparece algún check en el cual se indique de que si quieres agregarlo al Path del Sistema o algo por el estilo.

¡Márcalo, obviamente, para que puedas luego utilizarlo desde la terminal!

Otra vez: si en la terminal, o cuando —perdón— cuando estás instalando, te aparece (como en el caso de Python) un check que indique que si quieres agregar Node al Path del sistema (que no sé si lo pregunta), márcalo ahí, porque va a ser necesario. Si no, no lo vas a poder utilizar de esta forma y vas a tener que reinstalarlo.

Crear un nuevo proyecto en Vue (npm create vue) 

Video thumbnail

Vamos a comenzar, y para eso, recordemos que vamos a ocupar Node para instalar lo que es nuestro Vue.

Nota sobre la alternativa CDN: En caso de que no quieras emplear Node, tengas problemas con él, o quieras emplear la CDN y no sepas cómo arrancar, puedes revisar al final de la sección. Si no he agregado algo referente a esto, me lo puedes solicitar para añadir dicho desarrollo.

Partimos de lo que hicimos en la clase anterior: simplemente hablar un poquito sobre qué es Vue y por qué lo vamos a emplear.

Ventajas de Vue y Node

Vamos a emplear la versión de Node con Vue porque aquí tenemos todo el ecosistema a nuestra disposición, y ese es el que usualmente vamos a emplear cuando queramos hacer cualquier desarrollo en Vue.

La pequeña desventaja que tenemos es que no podemos vincular Vue de manera directa con el proyecto de CodeIgniter. Es decir, vamos a tener:

Un proyecto en CodeIgniter 4 (que viene siendo este).

Y, de manera independiente, la aplicación en Vue.

Por ejemplo, en Laravel, cuando creamos un proyecto, automáticamente tenemos una integración con Node, y con esto podemos instalar, en el mismo proyecto, nuestro querido Vue junto con Laravel.

Pero en CodeIgniter no podemos hacer ese tipo de implementaciones, ya que no forma parte o no nos trae el ecosistema de Node por defecto.

Proyectos Independientes

Así que lo tenemos que hacer en un proyecto aparte. Aunque, usualmente, esto es lo que se hace. Lo que nos interesa de CodeIgniter viene siendo la API que construimos anteriormente, y la queremos consumir desde aquí, así de simple.

Usualmente, esto se hace desde proyectos aparte. En este caso, estamos haciendo una demostración para consumir la API REST mediante una aplicación en Vue, pero podría ser una aplicación en React, Angular, o una aplicación móvil (en Flutter, Android nativo, etc.). En ese punto, obviamente, no va a ser el mismo proyecto.

Es importante saber que usualmente se manejan proyectos aparte.

Creación del Proyecto Vue

Aquí, lo primero que tienes que hacer es posicionarte en alguna parte donde quieras crear tu proyecto y ejecutar lo siguiente:

$ npm create vue@latest

El Rol de NPM (Node Package Manager)

Es importante que para poder instalar una dependencia como Vue, primero necesitamos un proyecto. Es lo mismo que hacíamos con CodeIgniter 4: si queremos instalar un paquete en PHP, recuerda que se instala mediante Composer.

Dato: El npm (el manejador de paquetes de Node) viene siendo el equivalente a Composer, pero en este caso, Composer es de PHP, y el manejador de paquetes en Node viene siendo de JavaScript.

Generación de Dependencias

Una vez generado el proyecto, si no tenemos la carpeta de node_modules (que es donde se encuentran todas las dependencias del proyecto), debemos generarla. Para eso, ejecutamos sobre la raíz del proyecto en Vue:

$ npm install

Estructura del proyecto en Vue

Video thumbnail

No vamos a ser excesivamente teóricos; nos centraremos en los archivos esenciales que conforman la aplicación. Recuerda que la documentación oficial debe ser siempre tu fuente principal para profundizar.

️ Archivos y Carpetas No Esenciales

  • .vscode: Esta carpeta es propia de Visual Studio Code y contiene configuraciones base para el editor (sintaxis, resaltado de errores, etc.). No es crucial para la lógica de la aplicación.
  • .gitignore: Contiene los archivos que Git debe ignorar (como la carpeta de módulos de Node). Esto es estándar para configurar un repositorio.
  • README.md: Archivo informativo para el repositorio.

public/
La carpeta public es la carpeta de acceso público. Contiene los archivos de salida de la build final y los archivos estáticos de la aplicación.

Aquí deben ir archivos como el favicon, imágenes, o videos que formen parte de la aplicación Vue.

  • index.html (El Archivo de Arranque)
    Este es el archivo de arranque. Vue es una tecnología web, y el navegador solo entiende HTML, CSS y JavaScript. Esta es la página de salida de nuestra aplicación.

Punto Clave: Fíjate en el div con el identificador id="app". Aquí es donde se monta nuestra aplicación Vue. Todo lo que desarrollemos en la carpeta src/ (nuestros componentes y la estructura del framework) finalmente se inyecta y se visualiza dentro de este div.

  • package.json
    Este archivo contiene las versiones de los paquetes que estamos empleando.
  • Aquí verás la versión de Vue y otras dependencias (como Vite y sus plugins).

Vite: El Transpilador y Servidor

Vite es una herramienta de frontend que nos permite construir nuestra aplicación.

Como mencionamos, el navegador no va a entender la estructura de la carpeta src/ (con archivos .vue y lógica compleja) por defecto.

¿Qué hace Vite?

Transpilación (Compilación): Vite permite transpilar (o compilar) nuestros archivos. Traspilar se diferencia de compilar en que, aunque pasamos de un código fuente a un código de máquina o equivalente, en el desarrollo web se utiliza el término transpilar porque transformamos un tipo de JavaScript/sintaxis a otro JavaScript/HTML/CSS que el navegador sí entiende (no estamos pasando a otro lenguaje totalmente diferente, como C a un ejecutable).

Servir la Aplicación: Por otra parte, Vite permite servir nuestra aplicación para verla en un servidor de desarrollo.

En resumen: Vite sirve para montar nuestra aplicación y para convertir (transpilar) todo lo que generamos en la carpeta src/ a algo que el navegador pueda interpretar.

Flexibilidad: Vite no solo funciona para Vue; también puedes emplearlo para React, Angular, y cada uno tiene su paquete específico.

  • ⚙️ vite.config.js (Configuración de Vite)
    Este archivo es para configurar Vite. No hay que "toquetear" mucho al inicio, pero aquí se hacen configuraciones importantes:
    • Se referencian los plugins instalados (el principal es el de Vue).
    • Se establecen configuraciones para importaciones relativas, que veremos cuando se creen los componentes.
  • ️ La Carpeta src/ (Source)
    Aquí es donde colocaremos nuestra aplicación como tal:
    • Archivos CSS: Para estilos.
    • Carpeta components/: Aquí irán todas las piezas de código reutilizables (nuestros componentes Vue).
  • ▶️ Ejecución del Proyecto: Comandos de Desarrollo
    Al igual que hacemos con CodeIgniter 4 con el comando spark serve, necesitamos levantar la aplicación Vue para poder verla y consumirla.

Esto se hace consultando el apartado scripts del archivo package.json. Por defecto, se levantan algunos comandos:

Cómo Ejecutar el Comando dev
Para ejecutar cualquiera de estos comandos de script, utilizamos el comando de Node npm run.

$ npm run dev

Explicación:

  • npm: El manejador de paquetes de Node.
  • run: Indica que vamos a ejecutar un script definido en package.json.
  • dev: Es la abreviatura del comando completo de Vite que se encuentra definido en el script.

❌ El Error Común

Al ejecutar npm run dev por primera vez sin la carpeta de módulos, te aparecerá un error de que falta una carpeta muy importante: la carpeta de node_modules (donde están todos los paquetes de Node, incluyendo Vue y Vite).

Solución: Debes ejecutar el comando $ npm install en la raíz del proyecto para descargar las dependencias y generar dicha carpeta, como vimos anteriormente.

Carpeta de los módulos de node

Nos quedamos en el punto en el que el proyecto no podía arrancar. Esto sucede porque falta la carpeta de los módulos de Node (node_modules), la cual es la equivalente a la carpeta vendor en proyectos CodeIgniter o PHP: en ella se encuentran todas las dependencias del proyecto. Por alguna razón, la instalación no la generó.

Existen varias formas de crear proyectos en Vue (empleando la CLI, o iniciando desde un proyecto de Node, etc.).

Para resolverlo, vamos a ejecutar el comando de instalación de Node:

npm install

Esperamos un momento a que termine el proceso. Esto se va a tardar un rato.

Nota sobre node_modules
Punto importante: No te asustes con el tamaño de la carpeta node_modules, ya que no la vamos a emplear cuando pasemos a producción. Esta carpeta es solamente para propósitos de desarrollo.

Dentro de node_modules vas a encontrar muchos paquetes intermedios que van a permitir que nosotros podamos desarrollar en Vue. Recuerda que, cuando pasemos a producción, ejecutaremos el comando de build (npm run build), el cual generaría aquí en la carpeta dist los archivos de salida que tendríamos que copiar y pegar.

package-lock.json y Control de Versiones

Fíjate en la cantidad de paquetes que se agregaron. Al igual que ocurre con CodeIgniter o PHP, cada uno de estos paquetes tiene dependencias.

Ahora, debería aparecer otro archivo muy importante: el package-lock.json. Este archivo contiene las versiones específicas que se estén empleando, el cual antes no estaba. Las dependencias tienen dependencias, y este archivo es esencial.

Mientras que en el package.json se encuentran las versiones, como quien dice, generales, fíjate que este sombrerito (^) indica, por ejemplo: "instale Vite en su versión 6.0.5 o cualquier versión compatible superior".

"vite": “^6.0.5

▶️ Comando de Desarrollo y Observador (Watch)

El comando de dev se encuentra en el archivo package.json bajo el script: "dev": "vite". Aquí podrás ver los comandos para el desarrollo y la producción.

Al ejecutar el comando (fíjate que esta oportunidad sí funcionó), aparte de levantar el servidor, también va a generar aquí un watch (o un observador). Esto significa que cada vez que hacemos un cambio en el código fuente de nuestra aplicación en Vue, automáticamente va a recargar o debería recargar.

Ejecución del Servidor de Desarrollo
Para levantar la aplicación en modo desarrollo colocamos:

npm run dev

⚙️ El Archivo de Arranque de Vue: main.js

Este es el archivo de arranque: main.js.

import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.config.globalProperties.$axios = axios
window.axios = axios
app.mount('#app')

Fíjate que estamos importando createApp desde vue. Es una importación con nombre ({ createApp }), es decir, si aquí le cambias el nombre, va a fallar, ya que no la va a encontrar.

Recomendación: Te recomiendo darle un F12 en tu navegador para ver los errores en el cliente. Ahí te indicará clarito si un elemento no ha sido encontrado.

El Componente Padre/Raíz (App) y su Montaje

En el main.js tenemos: app.mount('#app').

Estamos montando el componente principal (el componente padre/raíz), que es el llamado App (el componente de salida). Lo estamos montando en un div con el identificador #app, que es justamente el que teníamos aquí en la página index.html:

<div id="app"></div>

Si cambias el DIV o el identificador en index.html sin actualizarlo en main.js, Vue no encontrará dónde montar la aplicación y no funcionará.

Un poco más sobre los componentes en Vue

Video thumbnail

Los componentes son la pieza clave en todo el desarrollo con Vue, y ocuparemos la mayor parte de nuestro tiempo creando e interactuando con ellos.

Un componente en Vue no es más que un archivo con extensión .vue que puede ser:

  • Desde un sencillo botón reutilizable o no.
  • Hasta una página completa.

Los componentes que trae por defecto la aplicación recién creada son un excelente ejemplo de esto. Por ejemplo, el archivo src\App.vue representa la página principal, pero internamente emplea otros componentes reutilizables como TheWelcome.vue y HelloWorld.vue.

<script setup>
import HelloWorld from '@/components/HelloWorld.vue'
// ...
<TheWelcome />

Elementos de un Componente

Recordemos que los componentes tienen tres piezas principales:

  • HTML (<template>): La sección de la estructura, usualmente siempre presente.
  • JavaScript (<script>): La sección de la lógica (script).
  • CSS (<style>): La sección del estilo (opcional).

CSS con Ámbito (Scoped CSS)

El bloque de CSS usualmente no se define si se maneja un estilo global. Solo se define un CSS específico si se desea aplicar algo muy concreto a un componente.

El atributo scoped en la etiqueta <style> es crucial:

<style scoped>
.item {
margin-top: 2rem;
display: flex;
position: relative;
}
</style>

El scope significa que el estilo aplicado no escape de este componente. Esto evita que el estilo choque con el estilo de otro componente, ya que, aunque tengamos esta bonita estructura, todo se inyecta en una misma página de salida.

Axios/fetch Para realizar peticiones HTTP y consumir la Rest API de Vue 3

Video thumbnail

A continuación, debemos realizar una petición a nuestra aplicación en CodeIgniter 4 desde nuestra aplicación en Vue. Es fundamental recordar que son dos proyectos completamente separados con dominios distintos (por ejemplo, http://localhost:8080 para Vue y http://code4movies.test para CodeIgniter).

1. El Propósito de una API REST

El propósito principal de la API REST es servir como un mecanismo de comunicación para interconectar aplicaciones. La gran ventaja es que la API es independiente de la tecnología que la consume.

Nuestra API en CodeIgniter 4 puede ser consumida por: una aplicación en Vue (nuestro caso), una aplicación en Flutter, una aplicación nativa de Android/iOS, React, Angular, o cualquier tecnología futura.

2. Realizando Peticiones HTTP desde JavaScript

Para consumir la API, debemos hacer peticiones HTTP (como las de tipo GET o POST) desde nuestra aplicación Vue.

Afortunadamente, JavaScript nativo (Vanilla JS) cuenta con un mecanismo incorporado para esto: la función fetch.

Peticiones Asíncronas y Promesas

Toda petición HTTP (como abrir un archivo grande o solicitar datos a un servidor) es, por definición, una operación asíncrona . No se sabe exactamente cuánto tiempo tardará en completarse, ya que depende de la red, la velocidad del servidor, el tamaño de la respuesta, etc.

Para manejar estas operaciones asíncronas en JavaScript, usamos Promesas (Promise). Una Promesa puede resolverse (resolve) si la operación fue exitosa, o rechazarse (reject) si falló. El método .then() se utiliza para manejar la respuesta una vez que la promesa se resuelve.

Estructura de una Petición fetch

Una petición fetch básica se configura de la siguiente manera:

fetch('http://code4movies.test/api/pelicula')
   .then(response => response.json()) // 1. Convertir la respuesta a formato JSON
   .then(data => console.log(data))    // 2. Manejar los datos resultantes
   .catch(error => console.error(error)); // 3. Manejar cualquier error

URL Completa: Es crucial usar la URL completa (http://...), ya que estamos comunicando dos aplicaciones en dominios distintos.

Conversión a JSON: La respuesta inicial de fetch es un objeto Response. Generalmente, se necesita un primer .then() para llamar a .json() y castear el cuerpo de la respuesta al formato JSON que JavaScript puede manipular fácilmente.

3. ⚠️ El Problema de CORS

Al intentar hacer la petición, es probable que se encuentre un error de CORS en CodeIgniter (Cross-Origin Resource Sharing):

Access to fetch at '...' from origin 'http://localhost:5173' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

Esto ocurre porque el navegador bloquea por defecto las peticiones de un dominio (localhost:5173 - Vue) a otro dominio (code4movies.test - CodeIgniter) por motivos de seguridad. Este error debe resolverse en el lado del servidor (API en CodeIgniter) permitiendo explícitamente el origen de Vue.

Modos de Composición: Options API vs. Composition API

Existen varias formas de trabajar con Vue para crear la lógica de nuestros componentes: la Options API y la Composition API.

No voy a indagar mucho, pero es importante que los tengas presentes:

1. Options API (API de Opciones)

Es la sintaxis tradicional, en la cual se utiliza un export default y se definen las opciones (created(), data(), methods, etc.) en bloques separados:

<script>
export default {
   created() {
       // ...
   },
   // ...
}
</script>

2. Composition API (API de Composición)

Es una forma más moderna y expresiva, que utiliza la sintaxis <script setup>. Todo el código se coloca a un mismo nivel, facilitando la lectura y reutilización de lógica (función setup).

<script setup>
import WelcomeItem from './WelcomeItem.vue'
// ...
const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
</script>

Variables Reactivas (ref)

Dentro de la Composition API, las variables reactivas se definen de forma sencilla, por ejemplo, usando la función ref():

const count = ref(0)
console.log(count.value) // 0

El uso de ref() significa que la variable será reactiva y, por lo tanto, cada vez que hagamos un cambio sobre su valor (count.value = 1), este cambio se detectará automáticamente y se aplicará en la interfaz donde se esté empleando la variable.

️ Importaciones Relativas con @ en Vue

Cuando importamos componentes, solemos usar rutas relativas:

import WelcomeItem from './WelcomeItem.vue' // Problema de anidación

El problema con esta sintaxis es que, en una aplicación grande, los componentes pueden estar anidados en muchas carpetas (pages, helpers, users, etc.). Esto crea un lío de navegación (../../...) al querer importar archivos.

Para solucionarlo, utilizamos las importaciones relativas con el símbolo @:

import HelloWorld from ‘@/components/HelloWorld.vue’

Este @ (referenciado en la configuración de Vite) es una importación relativa a la carpeta src/. No importa dónde se encuentre el componente que estés editando, siempre apuntará a la raíz de tu código fuente (src/), facilitando la importación.

Reutilización de Componentes y Slots

Lo interesante de los componentes es la reutilización (componentes de componentes). Por ejemplo, App.vue importa a TheWelcome.vue, y TheWelcome.vue a su vez importa e utiliza a WelcomeItem.vue.

Uso de Slots

Tenemos dos formas de utilizar componentes:

Referenciarlos a secas: Simplemente se importa el contenido del componente y listo.

Usar Slots (Contenido Personalizado): El componente se define como una especie de wrapper o envoltorio para el contenido que se va a colocar dentro.

El uso de slots permite respetar la estructura definida en el componente padre, pero personalizando ciertas partes clave del contenido dinámico:

<WelcomeItem>
   <template #icon>
     <DocumentationIcon />
   </template>
   <template #heading>Documentation</template>
   
   Vue’s <a href="...">official documentation</a>...
</WelcomeItem>

Slots con nombre (#icon, #heading): Se definen para indicar partes específicas donde se puede inyectar contenido (ej. dónde va el título, dónde va un icono).

Slot por defecto: Es el contenido que no tiene un nombre específico y se coloca directamente entre las etiquetas de apertura y cierre del componente.

En el ejemplo anterior, el componente WelcomeItem define una bonita estructura HTML y CSS, y tú defines qué contenido va en el icono, qué contenido va en el encabezado (heading), y el contenido principal.

Esto es muy similar a lo que hacíamos con los templates en CodeIgniter 4, pero aquí el término técnico es slot.

Nuestro primer fetch

Video thumbnail

El siguiente paso es intentar consumir nuestra aplicación. Este listado que tenemos acá se consume vía GET —alerta de spoilers— no va a funcionar. Tenemos que configurar algo adicional, pero eso lo veremos en la siguiente clase. Aun así, es importante hacerlo para ver qué sucede, y luego te explico.

Entonces, primero tenemos que levantar la aplicación. Para eso, colocamos:

npm run dev

Realmente aquí podemos ejecutar varios comandos, pero no viene al caso ahora, ya que eso es algo de Node. No quiero complicar mucho, pero usualmente cuando colocamos este comando es porque queremos ejecutar uno de los que están definidos en el package.json. Por supuesto, estos se pueden personalizar según la herramienta, pero en nuestro caso no hace falta. Simplemente usamos dev, ya que es el comando que ya viene configurado.

Importaciones por defecto y por nombre

Recordemos un poco sobre las exportaciones en JavaScript. Aquí tenemos una exportación por nombre:

import { createApp } from 'vue'

Cuando usamos llaves ({}), significa que estamos importando un módulo por nombre. Puede que no se vea claramente dónde está esta exportación, ya que puede estar "oculta" dentro de algún otro módulo.

Y esta, en cambio, es una exportación por defecto:

import ListComponent from '@/components/CRUD/Movies/ListComponent.vue';

Cuando veas que no hay llaves, es una exportación por defecto. En este curso, vamos a usar principalmente exportaciones por defecto, porque así funcionan los componentes en Vue.

Componentes exportaciones por defecto

Peticiones HTTP con fetch y promesas

Para hacer peticiones HTTP, tenemos la API de fetch, que se encuentra disponible en vanilla JS:

console.log(fetch('http://code4movies.test/api/pelicula'))

Veremos que imprime <PROMISE>, como ya mencioné antes. Adicionalmente, veremos un error como este:

Access to fetch at 'http://code4movies.test/api/pelicula' from origin 'http://localhost:5173' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

Entonces, ya puedes ver por qué no funciona. Fíjate que fetch devuelve una promesa. Podemos resolverla con .then():

fetch('http://code4movies.test/api/pelicula')
.then(res => res.json())
.then(res => console.log(res))

Lo que sea esos lenguajes lo implementan en este caso javascript y es el por que de esta sintaxis puedes colocarla así con paréntesis si son muchos parámetros o sin digo puedes colocarlo así con por paréntesis si son varios parámetros o puedes prescindir de ellos si es solamente un parámetro:

param => expression
(param) => expression
(param1, paramN) => expression

CORS error en CodeIgniter 4

En CodeIgniter 4, por defecto, no permite conectar aplicaciones externas, como la de Vue, al hacer el fetch anterior, veremos:

Access to fetch at 'http://code4movies.test/api/pelicula' from origin 'http://localhost:5173' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

Entendiendo el Error CORS

Actualmente nos encontramos con un error, tal como puedes ver . Sin embargo, aquí ya no queda claro lo que pasa: supuestamente debería estar la respuesta, pero no devuelve nada. Es evidente que sucedió un problema, y el navegador nos indica de forma clara qué está pasando.

Este tipo de errores puedes buscarlo fácilmente en internet (copiar y pegar el mensaje ayuda mucho), pero en esencia, lo que ocurre aquí es un tema de CORS (Cross-Origin Resource Sharing), o en español, Intercambio de Recursos de Origen Cruzado.

¿Qué es el Origen Cruzado?

Aunque suena algo complicado, básicamente lo que estamos intentando hacer es intercambiar recursos (datos, información) entre aplicaciones que están en diferentes dominios (orígenes).

Ejemplo: Tu aplicación de Vue se encuentra corriendo en localhost:5173 y está intentando consumir recursos desde code4movies.test, que es otro dominio.

Aquí es donde entra en juego la política de seguridad del navegador y del servidor.

La Política de Seguridad de CodeIgniter 4

Por defecto, CodeIgniter 4 no permite este tipo de conexiones externas.

¿Por qué existe esta restricción?
La restricción existe para protegerte, ya que sería un gran riesgo permitir que cualquier aplicación (desarrollada por quien sea) pueda hacer peticiones libremente a nuestra API sin ningún tipo de autorización.

Incluso, tú puedes hacer peticiones desde cualquier parte del navegador hacia cualquier ruta de la aplicación, incluyendo la raíz. Si no hubiera restricciones, cualquier sitio malicioso podría explotar eso fácilmente.

Por eso, estas políticas existen para proteger la API. Desde la perspectiva de CodeIgniter 4, tu aplicación de Vue es vista como un posible atacante que está tratando de consumir recursos sin permiso. Por lo tanto, la API está protegida y bloquea la solicitud.

Para solucionar este error y permitir que Vue consuma la API, necesitamos configurar CORS en CodeIgniter 4. 

Pruebas con los fetch

Video thumbnail

Para entender mejor, recordemos que con fetch podemos hacer una petición HTTP a una API. Por su propia definición, al ser una petición externa a la aplicación (a otro servidor), es asíncrona.

Esto significa que la respuesta:

Puede tardar milisegundos o segundos.

Puede que simplemente no se resuelva (por problemas de red, caída de la API, etc.).

Encadenamiento de Funciones (.then())

Un punto importante es que estamos realizando funciones encadenadas mediante el método .then().

fetch('http://code4movies.test/api/pelicula')
.then(res => res.json())
.then(res => console.log(res)) 
fetch(...): Se ejecuta la petición HTTP. Esta retorna una Promesa que, al resolverse, devuelve el objeto de respuesta genérica (Response).
  • Primer .then(): Esta función se ejecuta luego de la petición. Su entrada (res) es el objeto de respuesta genérica. Aquí es donde llamamos a res.json() para castear y convertir la respuesta de la API al formato JSON (el formato que estamos empleando para compartir la Data). Esto, a su vez, retorna una nueva Promesa.
  • Segundo .then(): La entrada de esta función es el retorno del paso anterior (el JSON ya convertido). Aquí ya tenemos la data lista y podemos imprimirla o procesarla (console.log(res)).
  • Importante: La petición y las llamadas a .then() están encadenadas. Si comentas la conversión a JSON, la siguiente función recibiría el objeto de respuesta genérica, lo cual no es útil para trabajar con los datos. Si quitas los .then(), solo haces la petición y no haces nada con el resultado.

Manejo de Errores (.catch())

Es crucial manejar los casos en que la petición no se resuelve correctamente.

Para manejar cuando la respuesta no es correcta (un error de red, fallo del servidor, etc.), utilizamos el método .catch():

fetch('http://code4movies.test/api/pelicula')
.then(res => res.json())
.then(res => console.log(res))
.catch(error => console.log(error))
El bloque .catch(error => console.log(error)) se ejecuta si alguna de las Promesas en la cadena (fetch o res.json()) es rechazada, permitiéndonos manejar el fallo de manera controlada e imprimir el error.

Introducción a Axios: Reemplazo de fetch

Video thumbnail

Vamos a conocer la librería que te mencioné hace dos o tres clases: Axios, que funciona como un reemplazo de fetch.

Como te comentaba, puedes buscar comparativas como "Axios versus Fetch" para obtener más información. Sin embargo, en mi opinión, Axios tiene dos ventajas claras:

  1. Sintaxis más expresiva: Aunque es una diferencia mínima, es más simple, minimalista y fácil de entender y seguir.
  2. Mejor soporte: Aunque fetch está soportado por la mayoría de los navegadores, es importante verificarlo. Puedes buscar "fetch soporte" en Google y verás una tabla, usualmente de Mozilla, que muestra claramente en qué versiones está disponible. En general, el soporte es bastante amplio, salvo en versiones muy antiguas, así que no debería ser un problema.

Instalación de Axios en un proyecto Vue

Vamos a instalar Axios, ya que podemos hacerlo perfectamente en nuestro proyecto.

Para eso, usamos el siguiente comando:

npm install axios

Esto es algo habitual cuando trabajamos con el ecosistema de Node. Recuerda que estamos usando Vue, y que npm es el manejador de paquetes de Node.

Cuando instalamos Node, obtenemos dos herramientas:

node: el motor de JavaScript.

npm: Node Package Manager, el manejador de paquetes.

También puedes usar npm i axios, que es la versión abreviada. Con esto, Axios quedará instalado en tu proyecto Vue. Te recomiendo hacerlo con el servidor apagado por si acaso, aunque normalmente no debería dar problemas si está levantado.

Una ventaja de usar Node y npm en lugar de una CDN es que no necesitamos descargar el archivo JS, copiarlo manualmente al proyecto, configurarlo ni enlazarlo en varias partes. Es mucho más limpio y profesional.

A nivel del archivo principal de Vue, cargamos a axios como una propiedad global de Vue:

src/main.js

import './assets/main.css'
import { createApp } from 'vue'
import axios from 'axios'

import App from './App.vue'

const app = createApp(App)

app.config.globalProperties.$axios = axios
window.axios = axios

app.mount('#app')

Con esto, ya estamos listos para emplear axios con Vue.

Configuración global de Axios

Una vez instalado, hay que configurarlo globalmente para poder usarlo en toda la aplicación. Esta configuración la haremos en el archivo de arranque de Vue, es decir, en main.js.

En ese archivo, como ya vimos, se crea la instancia de Vue y se define el componente padre (App.vue), donde se cargan todos los demás componentes.

Importación del paquete

Primero importamos Axios. Me gusta colocar las importaciones de paquetes de terceros separadas, así que lo haremos así:

import axios from 'axios';

Nota que esta es una importación por defecto, por eso no usamos llaves {}.

Creación de la instancia de Vue

Luego, dividimos la operación de creación y montaje de la aplicación en dos pasos:

const app = createApp(App);
app.mount('#app');

Esto nos permite hacer configuraciones adicionales antes de montar la app.

En algunos casos, el editor puede lanzar una advertencia si importamos algo que aún no usamos. Si es tu caso, puedes comentar la línea temporalmente para hacer pruebas.

Asignación global de Axios

Ahora configuramos Axios como una propiedad global. Esto se hace así:

app.config.globalProperties.$axios = axios;

El prefijo $ es opcional, pero en Vue se usa por convención para indicar que es una propiedad especial o interna del framework. Esto ayuda a evitar conflictos con otras variables, por ejemplo, si más adelante defines algo llamado axios dentro de un componente.

De todas formas, si no te gusta el $, puedes omitirlo, aunque lo recomendado es seguir la convención.

Verificación de la configuración

Para asegurarnos de que todo funciona, podemos hacer una prueba en la consola del navegador. Primero, recuerda que ahora Axios está disponible en toda la aplicación a través de this.$axios dentro de los componentes.

Si quieres acceder a Axios desde la consola global, puedes asignarlo al objeto window así:

window.axios = axios;

Con esto, podrás acceder a él directamente en la consola del navegador:

console.log(window.axios);

Si no lo asignas al objeto window, el navegador no lo reconocerá y mostrará undefined.

Envío de Data en Peticiones Axios

Video thumbnail

Axios simplifica la forma de enviar datos al servidor, especialmente diferenciando entre peticiones GET y POST.

1. Peticiones de Tipo GET

Recuerda que en las peticiones GET, la data viaja por la URL como parámetros (query parameters). Tienes dos opciones para enviarla:

Opción 1: Directamente en la URL (Manual)

Puedes colocar los parámetros directamente en la URL.

axios.get('http://code4movies.test/api/pelicula?parameterGet=1')
Opción 2: Usando el Objeto params (Recomendado

Esta es la forma recomendada, ya que Axios se encarga de formatear la URL correctamente. Se pasa un objeto de opciones como segundo parámetro del método get.

axios.get('/user', {
   params: {
     id: 12345 // Esto se convierte a: /user?id=12345
   }
 }

El objeto params dentro de las opciones es el equivalente a colocar los parámetros directamente en la URL.

2. Peticiones de Tipo POST (Data en el Cuerpo)

Para peticiones POST, PUT, o PATCH, la data viaja en el cuerpo (body) de la petición.

En este caso, la ruta sigue siendo el primer parámetro, y el segundo parámetro es directamente el objeto que contiene la data a enviar:

axios.post('/user', {
   data1: 'Value1', // Esto se envía en el BODY de la petición
   data2: 'Value2'
 })

Recuerda que en el caso de POST, esta data viaja vía el body.

Captura de Errores con .catch()

Al igual que con fetch, las peticiones con Axios son asíncronas (son Promesas), por lo que pueden fallar por errores de servidor, cliente o de conexión.

Axios utiliza la misma estructura de Promesas (.then() y .catch()) para manejar la respuesta y los posibles errores:

axios.post('/user', {
   data1: 'Value1',
   data2: 'Value2'
 })
 .then(function (response) {
   // Se ejecuta si la petición fue exitosa (código 2xx)
   console.log(response); 
 })
 .catch(function (error) {
   // Se ejecuta si ocurrió un error (falla de red o error de servidor)
   console.log(error); 
 })

Diferencias Clave con fetch

La principal ventaja de usar Axios sobre fetch es que:

No hay que castear a JSON: Axios automáticamente deserializa la respuesta (si es JSON) en un objeto JavaScript, por lo que no necesitas el paso intermedio de res.json().

Manejo de Métodos: Puedes definir los tipos de métodos (.get(), .post(), .put()) de una manera más limpia y sencilla.

Componente de listado

Video thumbnail

Has creado la siguiente estructura modular:

  • Ruta: src/components/CRUD/Movies/ListComponent.vue

El componente ya tiene definidos los tres bloques principales: template, script (utilizando la Options API con export default), y style.

<template>
   <div class="list-container">
       <h2>Listado de Películas</h2>
       </div>
</template>
<script>
export default {
   name: 'ListComponent', // Es buena práctica darle un nombre al componente
   
   data() {
       return {
           // Definimos la variable reactiva 'movies' como un array vacío.
           // Aquí se almacenarán los datos obtenidos de la API.
           movies: [] 
       }
   },
   // Aquí irán los métodos, el 'created', etc.
}
</script>
<style scoped>
/* El 'scoped' asegura que estos estilos solo apliquen a este componente */
</style>

El Bloque data() (Options API)

En la Options API que estás empleando (export default), el bloque data() es una función que debe retornar un objeto. Este objeto contiene todas las variables de estado (o variables reactivas) del componente.

Propósito: La variable movies: [] que has definido inicializará un array vacío. Este array será reactivo, lo que significa que cuando obtengamos los datos de la API y los asignemos a this.movies, Vue automáticamente detectará el cambio y actualizará la sección del template que esté usando esa variable.

El siguiente paso sería utilizar el hook created() o mounted() para ejecutar la petición a la API y llenar este array con datos.

Integración del Componente en App.vue

El archivo src/App.vue actúa como el componente raíz de tu aplicación, y es el lugar ideal para importar y montar otros componentes principales, como tu ListComponent.

1. Limpieza e Importación

Antes de importar, es común limpiar App.vue, eliminando contenido o estilos que no vas a utilizar. Luego, importamos el componente:

<script setup>
// Importación del componente ListComponent
import ListComponent from '@/components/CRUD/Movies/ListComponent.vue'; 
</script>

Ruta Simplificada: Utilizamos el símbolo @ para indicar un acceso directo a la carpeta src/ del proyecto. Esto evita rutas largas y relativas.

Extensión Obligatoria: En versiones recientes de Vue (usando <script setup> y Vite), la extensión .vue es obligatoria al importar componentes.

2. Uso y Visualización en el Template

Una vez importado, puedes usar el componente en la sección <template> mediante su nombre (tal como lo importaste):

<template>
 <h1>Hola, componente listo</h1> 
 
 <ListComponent />
</template>

Al utilizar la etiqueta <ListComponent />, Vue inyecta el HTML definido en el template de ListComponent.vue dentro del template de App.vue. Si temporalmente colocaste el <h1>Hola, componente listo</h1> dentro de ListComponent.vue y lo viste en pantalla, la integración se realizó correctamente.

Reutilización: Una de las ventajas de los componentes es que puedes usarlos tantas veces como sea necesario. Aunque un listado principal se use una vez, puedes insertar <ListComponent /> múltiples veces si la lógica lo permitiera.

PascalCase (Recomendado)    <ListComponent />    Usa mayúsculas y minúsculas; es la forma estándar de nombrar archivos y clases en JS.
kebab-case    <list-component />    Usa minúsculas con guiones. También es válida, ya que el navegador es case-insensitive.
Cierre Completo    <list-component></list-component>    Necesaria solo si vas a usar slots para inyectar contenido.

Ciclo de vida en Vue.js

Video thumbnail

Tenemos que presentar el complicadísimo (y aburridísimo) ciclo de vida de una aplicación en Vue. También puedes buscarlo en español como "ciclo de vida en Vue". Aquí hay una página que lo explica:

https://vuejs.org/guide/essentials/lifecycle

El gráfico puede parecer una pesadilla, pero al final lo que tienes que entender es lo siguiente: cuando se crea la aplicación en Vue (lo que llaman new Vue, que es básicamente esto que estamos haciendo acá), suceden muchas cosas internamente. Pero en algún punto, se van llamando ciertos métodos del ciclo de vida que son los que nos interesan.

Métodos del ciclo de vida

Los principales métodos que debemos conocer son:

  • beforeCreate
  • created
  • beforeMount
  • mounted
  • beforeUpdate
  • updated
  • beforeUnmount
  • unmounted

No me voy a detener mucho en esto, pero básicamente, cuando cambiamos de una página a otra —por ejemplo, de una página de listado (como en CodeIgniter 4) a una de detalle—, se monta el nuevo componente y se desmonta el anterior. Ahí es donde entran en juego mounted y unmounted.

  • Cuando se monta el componente de detalle, se ejecuta mounted.
  • Cuando se desmonta el componente de listado, se ejecuta unmounted.

Los más usados en la práctica son:

  • created: cuando necesitas preparar la data.
  • mounted: cuando necesitas interactuar con el DOM.

Puedes usar cualquiera de los dos, pero como en este caso solo nos interesa cargar la data al principio, usaremos created.

<template>
    {{ this.movies }}
</template>
<script>
export default {
    created() {
        axios.get('http://code4movies.test/api/pelicula')
            .then(res => this.movies = res.data)
            .catch(error => console.log(error))
    },
    data() {
        return {
            movies: []
        }
    },
}
</script>

Reactividad de Vue y uso de funciones

¡Excelente! Modularizar el código con métodos, incluso en aplicaciones pequeñas, es una gran práctica para mantener el código limpio y comprender mejor la Options API de Vue.

Aquí tienes la explicación estructurada de los métodos y una demostración práctica de la reactividad.

️ Modularizando con Métodos en la Options API

Para mantener la lógica de la aplicación organizada, definimos la llamada a la API dentro de un método en el componente ListComponent.vue.

1. Estructura de Bloques

En la Options API, el componente se organiza en bloques funcionales (el orden no importa, pero la convención sugiere created, data, methods):

2. Creación y Uso del Método getMovies()

Definimos la función getMovies() dentro del bloque methods y la llamamos desde created().

export default {
   created() {
       // Ejecutamos el método que contiene la lógica de la petición
       this.getMovies(); 
   },
   data() {
       return {
           movies: [],
           confirmDeleteActive: false,
           deleteMovieRow: ''
       }
   },
   methods: {
       async getMovies() { // Declarado como async para usar await
           try {
               // Usamos this para acceder a variables (e.j., this.movies)
               const res = await axios.get('http://code4movies.test/api/pelicula');
               this.movies = res.data; 
           } catch (error) {
               console.log(error);
           }
       }
   }
}

Notas Importantes:
Uso de this: Para acceder a cualquier propiedad (como this.movies) o método (como this.getMovies()) definido dentro de los bloques de la Options API, es obligatorio usar la palabra clave this. Sin this, el script no reconocerá la función y lanzará un error.

async/await: Si deseas utilizar la sintaxis await para esperar la respuesta de Axios, el método debe ser declarado como async.

✨ Reactividad en Vue: La Demostración Práctica

La reactividad es la "magia" de Vue y una de sus grandes ventajas.

1. Mecanismo de Reactividad

Vue detecta automáticamente los cambios en las propiedades declaradas dentro del bloque data() (como movies). Cualquier parte de la interfaz (<template>) que esté utilizando esa variable se actualizará sin que tú tengas que manipular el DOM manualmente.

Demostración:
El ejemplo de setTimeout ilustra perfectamente esto:

setTimeout(() => {
// Después de 5 segundos, la vista se actualiza automáticamente:
this.movies = [/* nuevos datos */]; 
}, 5000);

Al pasar de un array vacío a uno lleno con registros, Vue se encarga de cambiar la vista por ti. No se necesita usar document.getElementById ni querySelector, como se hacía con librerías tradicionales como jQuery.

2. Diferencia entre data() y ref

Options API (data()): Cuando trabajas con la Options API (usando export default), todo lo que se declara dentro de la función data() es asumido como reactivo por Vue. Por lo tanto, no necesitas usar la función ref().

Composition API (ref): La palabra clave ref solo se utiliza en la Composition API (usando <script setup>) para declarar explícitamente que una variable debe ser reactiva.

SaveComponent.vue: Editar: Enviar data al servidor 

Video thumbnail

Siguiente paso sería por acá hacer la petición put aquí, retornamos y ejecutamos el post y si no entrará por acá por si no lo ves bien:

src\router.js

{
   name: 'save',
   path:'/save/:id?',
   component: Save
}
async mounted() {
   if (this.$route.params.id) {
       await this.getMovie()
       this.init()
   }
   this.getCategories()
},

Como puedes apreciar, la lógica clave en el script es verificar la presencia de un identificador para determinar la acción a ejecutar:

  • Se pregunta por el ID de la ruta o por el objeto (post o movie en este caso).
  • Si el ID está definido (es decir, ya existe), entonces se realiza una operación de actualización (generalmente con el método PUT o PATCH).
  • Si el ID no está definido, entonces se procede a la creación del nuevo registro (generalmente con el método POST).
async getMovie() {
   this.movie = await axios.get('http://code4movies.test/api/pelicula/' + this.$route.params.id)
       .then(res => res.data)
       .catch(error => error)
},

Editar: Obtener la película

La siguiente operación clave que vamos a implementar es la edición de un registro. Lo haremos de una manera eficiente, utilizando el mismo componente (SaveComponent.vue) para manejar tanto la creación como la edición de películas.

1️ Configuración de la Ruta (Vue Router)

Para lograr esto, definimos una única ruta que acepta el ID del registro como un parámetro opcional.

src/router.js

{
   name: 'save',
   path:'/save/:id?', // El '?' hace que el parámetro 'id' sea opcional
   component: Save
}

Al hacer el parámetro :id opcional, la ruta /save se utiliza para crear un nuevo registro, y la ruta /save/123 se utiliza para editar el registro con ID 123.

2. Lógica de Inicialización (Componente Vue)

Dentro del componente (SaveComponent.vue), utilizamos el hook mounted() para verificar si estamos en modo "Crear" o "Editar".

Accedemos al parámetro ID usando el objeto interno de Vue Router: this.$route.params.id.

src/components/SaveComponent.vue

async mounted() {
  // 1. Verificación: Si el ID está definido, estamos editando.
  if (this.$route.params.id) {
      await this.getMovie() // Obtener datos del post
      this.init() // Inicializar campos del formulario
  }
  
  this.getCategories() // Cargar categorías para el formulario (necesario en ambos modos)
},
Métodos Auxiliares

Recuerda que el dólar es referente a funciones o parámetros propiedades como lo quieras llamar internas en este caso sería interna al paquete de View router punto paras que es lo que queremos obtener seguido del nombre del parámetro que en este caso es como ID preguntamos si está definido si estamos si está definido está en fase de editar y y si no seguimos con la fase de crear es decir no hacemos nada adicional y por aquí vamos a implementar un par de métodos adicionales que sería para obtener el post en base al ID.

Implementamos los métodos auxiliares necesarios para el modo de edición:

getMovie(): Realiza una petición GET al endpoint de la API, usando this.$route.params.id para obtener los datos de la película.

async getMovie() {
   // Petición a la API para obtener los datos de la película por su ID
   this.movie = await axios.get('http://code4movies.test/api/pelicula/' + this.$route.params.id)
       .then(res => res.data)
       .catch(error => error)
}

init(): Una vez obtenidos los datos, este método inicializa las propiedades del formulario (this.form) con los valores existentes de la película (this.movie), rellenando los campos para la edición.

init() {
  this.form.title = this.movie.titulo
  this.form.description = this.movie.descripcion
  this.form.category_id = this.movie.categoria_id
}

3. Manejo del Envío del Formulario (send())

Finalmente, en el método send() que maneja el envío del formulario, verificamos si la variable local this.movie ha sido inicializada. Esto determina si debemos ejecutar una petición POST (Creación) o una petición PUT (Edición).

async send() {
   let res = '';
   
   // Si this.movie está vacío, significa que NO se cargó un ID: CREAR (POST)
   if (this.movie == '') {
       // Lógica para POST: Usamos FormData (por ejemplo, si hay archivos o configuraciones especiales)
       const formData = new FormData()
       // ... append data ...
       res = await axios.post('http://code4movies.test/api/pelicula', formData)
       this.cleanForm()
   } 
   // Si this.movie contiene datos, estamos EDITANDO (PUT)
   else {
       // Lógica para PUT: Enviamos el objeto de formulario
       res = await axios.put('http://code4movies.test/api/pelicula/' + this.movie.id, this.form)
       this.cleanForm()
   }
   
   // Manejo de errores de validación de la respuesta
   // ...
}

De esta manera, el mismo componente maneja de forma eficiente ambos flujos de trabajo

Configurar Oruga UI en Vue 3

Video thumbnail

Oruga es una biblioteca liviana de componentes de interfaz de usuario para Vue.js sin dependencia de CSS; al no depender de ningún estilo específico o framework CSS (como Bootstrap, Bulma, TailwindCSS, etc.) no proporciona ningún sistema de cuadrícula o utilidad CSS, solo ofrece un conjunto de componentes fáciles de personalizar con las hojas de estilos usando un framework CSS, un estilo personalizado o el estilo opcional de Oruga UI.

Ya hablamos cómo crear un proyecto en Vue 3 en Laravel, ahora, vamos a integrar Oruga UI:

Instalamos Oruga en el proyecto en Vue con:

npm install @oruga-ui/oruga-next --save

Configuramos el main.js:

import { createApp } from "vue";
import Oruga from '@oruga-ui/oruga-next'
import '@oruga-ui/oruga-next/dist/oruga.css'
import '@oruga-ui/oruga-next/dist/oruga-full.css'
import App from "./App.vue"
const app = createApp(App).use(Oruga)
app.mount("#app")

En el paso anterior, recordemos que un proyecto en Vue 3 luce como:

import { createApp } from "vue";
import App from "./App.vue"
app.mount("#app")

Y lo único que hacemos es incluir los componentes de Oruga:

import Oruga from '@oruga-ui/oruga-next'

un estilo mínimo de Oruga:

import '@oruga-ui/oruga-next/dist/oruga.css'

Y el estilo opcional de Oruga:

import '@oruga-ui/oruga-next/dist/oruga-full.css'

Luego, instalamos el plugin en Vue:

createApp(App).use(Oruga)

Listado: v-for, Reactividad y async await

Video thumbnail

Lo siguiente que nosotros vamos a hacer sería iterar nuestro listado de películas.

Aquí también conocer un poco la magia de este tipo de frameworks del cliente es decir la reactividad ya que recuerda que esto se inicializa por defecto como vacío:

data() {
   return {
       movies: []
   }
},

Y en algún momento de la vida cuando se carga el componente es que se inicializa con la Data:

async created() {
    // axios.get('http://code4movies.test/api/pelicula')
    //     .then(res => this.movies = res.data)
    //     .catch(error => console.log(error))
    
    this.movies = await axios.get('http://code4movies.test/api/pelicula')
        .then(res => res.data)
        .catch(error => error)
        console.log(this.movies) // [Movie1, ... MovieN]
},

En el código anterior, podemos ver un par de variantes para obtener los datos de nuestra Rest API, para ello, podemos obtener los datos con las funciones de promesa (los then) o la combinación de async y await, con los cuales, podemos ‘esperar’ que termine de ejecutarse la petición/la promesa y a diferencia del ejemplo con el then, automáticamente, tenemos los datos (o un error en caso de que ocurra un error) y podemos usarlos para inicializar los datos.

Datos reactivos

Si imprimimos el array anterior, veremos algo como lo siguiente:

console.log(this.movies)

>> Proxy(Array) ...
[[Handler]]
: MutableReactiveHandler
[[Target]]
: Array(20)

Lo que significa que reactivo no que te va a matar de radiación sino es que es como que un objeto que siempre va vi a observar en base a algunas operaciones que hagamos sobre ella en el caso de los array.

Eliminar item, recargar listado array.splice en Vue

Video thumbnail

Si eliminamos un registro, en la cual, llamamos simplemente a la Rest API:

remove(movie){
   axios.delete('http://code4movies.test/api/pelicula/' + movie.row.id)
       .then(res => res.data)
       .catch(error => error)
},

No se recarga el listado de la tabla:

<o-table :data="movies" :bordered="true" :striped="true" :hoverable="true" :selectable="true">
    <o-table-column field="id" label="ID" v-slot="m" sortable>
        {{ m.row.id }}
    </o-table-column>
    <o-table-column field="titulo" label="Title" v-slot="m" sortable>
        {{ m.row.titulo }}
    </o-table-column>
    <o-table-column label="Actions" v-slot="m">
        <o-button @click="$router.push({ name: 'movie.save', params: { id: m.row.id } })"
            icon-left="pencil">Edit</o-button>
        <div class="inline ms-3">
            <o-button variant="danger" size="small" @click="confirmDeleteActive = true; deleteMovieRow = m"
                icon-left="delete">Delete</o-button>
        </div>
    </o-table-column>
</o-table>

Esto se debe a que no estamos recargando el array llamado movies.

Eliminar elementos de arrays en JavaScript

Vue no puede detectar automáticamente los siguientes cambios cuando se realizan directamente sobre una propiedad reactiva de tipo array:

  1. Establecer un elemento directamente por índice:
    1. Ejemplo problemático: this.myArray[indexOfItem] = newValue
  2. Modificar la longitud del array:
    1. Ejemplo problemático: this.myArray.length = newLength

Si realizas estas operaciones, el array cambiará, pero Vue no actualizará el template, rompiendo la reactividad.

  • pop()
  • shift()
  • splice
  • Asignaciones directas
  • push()

Así que, además de llamar a la Rest API, debemos de eliminar el elemento del array, para ello:

remove(movie){
    axios.delete('http://code4movies.test/api/pelicula/' + movie.row.id)
        .then(res => res.data)
        .catch(error => error)
    this.movies.splice(movie.index,1)
},

Esta es una forma que permite eliminar un elemento mediante su índice, que es justamente lo que necesitamos. Para esto, se utiliza el método nativo de JavaScript splice().

El método splice() te permite modificar el contenido de un array eliminando o reemplazando elementos existentes.

ListComponent.vue: Diálogo de confirmación al momento de Eliminar en Vue

Video thumbnail

Es fundamental definir un diálogo de confirmación antes de realizar la operación de eliminar.

Tenemos solo un único modal aquí tenemos el componente de om modal que también aquí lo puedes ver en la documentación oficial:

<o-modal v-model:active="isActive">
     ***
</o-modal>

Fíjate que también adicionalmente tenemos la variable que va a permitir mostrar o no el diálogo de confirmación, el llamado isActive.

Y junto con Tailwind, podemos darle esos pequeños detalles, así que, queda como:

<o-modal v-model:active="confirmDeleteActive">
   <div class="m-4">
       <p class="mb-3">Are you sure you want to delete the selected record?</p>
       <div class="flex gap-2 flex-row-reverse">
           <o-button @click="confirmDeleteActive = false">Cancel</o-button>
           <o-button variant="danger" @click="remove">Delete</o-button>
       </div>
   </div>
</o-modal>

En la tabla, colocamos una referencia al indice del elemento que queremos elimiar:

<o-table-column label="Actions" v-slot="m">
    <o-button variant="danger" size="small" @click="remove(m)">Delete</o-button>
</o-table-column>
***
remove(movie){
    axios.delete('http://code4movies.test/api/pelicula/' + movie.row.id)
        .then(res => res.data)
        .catch(error => error)
    this.movies.splice(movie.index,1)
},

Y eso es todo, ya con esto, tenemos un esquema para poder eliminar un registro.

Tailwind: Container - Para evitar que el contenido aparezca todo alargado

Video thumbnail
Video thumbnail

Ya que la parte visual es importante, y aunque podrías crear un estilo personalizado (o usar librerías de prototipado rápido como Bootstrap), vamos a emplear Tailwind CSS.

Aunque para aplicaciones pequeñas como esta podría ser interesante un estilo personalizado, usaremos Tailwind por las siguientes razones:

  1. Es un estándar muy utilizado en la industria.
  2. Nos permite demostrar su integración y funcionalidad.
  3. Es una herramienta que casi siempre es útil y aplicable a cualquier proyecto.

La Filosofía de Tailwind CSS

Tailwind CSS, a diferencia de frameworks como Bootstrap, es un framework de CSS basado en clases de utilidad (utility-first).

  • ¿Qué Significa? En lugar de proporcionar componentes pre-diseñados (como un botón o una tarjeta completa con margen y padding fijo), Tailwind te da miles de clases pequeñas y atómicas que realizan una única función (ej. text-lg para texto grande, p-4 para un padding de 4 unidades, flex para usar flexbox).
  • Tu Rol: Tú eres quien arma los componentes combinando estas clases de utilidad directamente en tu HTML. Tal cual puedes ver , vas agregando clases al elemento, y vas viendo cómo cambia la estructura y el estilo del mismo.

Vamos a instalar Tailwind CSS en su versión 4 junto con un proyecto en Vue 3, los pasos son los mismos a si estas empleando un proyecto en Node sin Vue salvo por el archivo de configuración de vite que ya existe en Vue y no hay que volverlo a crear; instalamos el framework y su plugin para vite:

Agregar or Instalar Tailwind CSS 4 con Vue 3

Para agregar Tailwind al proyecto:

$ npm install tailwindcss @tailwindcss/vite

Y en la hoja de estilo agregamos:

src\css\main.css

@tailwind base;
@tailwind components;
@tailwind utilities;

Cargamos el CSS en el src\css\main.css:

import { createApp } from 'vue'
import App from './App.vue'
import "./css/main.css" // Importa tu archivo CSS con las directivas de Tailwind
createApp(App).mount('#app')

vite.config

Ya tenemos el archivo de vite.config.js, a diferencia de versiones anteriores de Tailwind, Tailwind ya autoescanea nuestros archivos para determinar cuáles clases tiene que emplear; agregamos el plugin de vite para Tailwind:

vite.config.js

***
import tailwindcss from '@tailwindcss/vite'

export default defineConfig({
 plugins: [
   vue(),
   tailwindcss(),
   vueDevTools(),
 ],
 ***
})

Contenedor

En Tailwind, tenemos también la clase container para tal fin, pero, a diferencia de Bootstrap no centra el contenido, por tal motivo, podemos colocar el margen en auto en el eje x para tal fin:

src/App.vue

<div class="container mx-auto my-3">
 <router-view></router-view>
</div>

Componente de Carta/Card

Video thumbnail

Vamos a crear un componente de carta o simplemente un montón de clases que definan una estructura en este caso para una carta una carta no es más que un contenedor usualmente de color blanco con algún borde puede con sombreado y poco más y algún espaciado:

src/css/main.css

.card {
   @apply bg-white p-6 rounded-md shadow-lg
}

Y luego usamos:

src/components/CRUD/Movies/ListComponent.vue

<h1>List</h1>
<div class="card">
   <div class="mb-4 ms-2">
    ***
  </o-table>
</div>

 Entonces esto usualmente lo colocamos como un elemento padre para colocar contenido por ejemplo todo este contenido:

src/components/CRUD/Movies/SaveComponent.vue

<div class="card">
  <o-field label="Title" :variant="errors.title ? 'danger' : ''" :message="errors.title">
  ***
  <o-button variant="primary" @click="send">Send</o-button>
</div>

Puedes personalizar a nivel del HTML o CSS el tamaño, color, entre otros aspectos que consideres importantes.

Expanded en los o-input en Oruga UI

Video thumbnail

Para poder mostrar los campos de Oruga UI expandidos, basta con colocar el atributo de expanded:

src/components/CRUD/Movies/SaveComponent.vue
***
<o-input v-model="form.title" expanded></o-input>
***
<o-input v-model="form.description" type="textarea" expanded></o-input>
***
<o-select v-model="form.category_id" expanded>

CRUD en Vue + Rest Api CodeIgniter

El resumen del CRUD queda como:

Vamos a hacer el mismo proceso para el de categorías, creando sus componentes en Vue es decir, quedando como:

src/components/CRUD/Categories/ListComponent.vue

<template>
    <o-modal v-model:active="confirmDeleteActive">
        <div class="m-4">
            <p class="mb-3">Are you sure you want to delete the <span class="font-bold">{{ deleteCategoryRow && deleteCategoryRow.row ? deleteCategoryRow.row.titulo : '' }}</span> record?</p>
            <div class="flex gap-2 flex-row-reverse">
                <o-button @click="confirmDeleteActive = false">Cancel</o-button>
                <o-button variant="danger" @click="remove">Delete</o-button>
            </div>
        </div>
    </o-modal>
    <h1>List</h1>
    <div class="card">
        <div class="mb-4 ms-2">
            <o-button @click="$router.push({ name: 'category.save' })" variant="primary" size="large">Create</o-button>
        </div>
        <o-table :data="categories" :bordered="true" :striped="true" :hoverable="true" :selectable="true">
            <o-table-column field="id" label="ID" v-slot="c" sortable>
                {{ c.row.id }}
            </o-table-column>
            <o-table-column field="titulo" label="Title" v-slot="c" sortable>
                {{ c.row.titulo }}
            </o-table-column>
            <o-table-column label="Actions" v-slot="c">
                <o-button @click="$router.push({ name: 'category.save', params: { id: c.row.id } })"
                    icon-left="pencil">Edit</o-button>
                <div class="inline ms-3">
                    <o-button variant="danger" size="small" @click="confirmDeleteActive = true; deleteCategoryRow = m"
                        icon-left="delete">Delete</o-button>
                </div>
            </o-table-column>
        </o-table>
    </div>
</template>
<script>
export default {
    created() {
        this.getCategories();
    },
    data() {
        return {
            categories: [],
            confirmDeleteActive: false,
            deleteCategoryRow: ''
        }
    },
    methods: {
        remove() {
            axios.delete('http://code4movies.test/api/categoria/' + this.deleteCategoryRow.row.id)
                .then(res => res.data)
                .catch(error => error)
            this.categories.splice(this.deleteCategoryRow.index, 1)
            this.confirmDeleteActive = false
        },
        async getCategories() {
            this.categories = await axios.get('http://code4movies.test/api/categoria')
                .then(res => res.data)
                .catch(error => error)
        },
    },
}
</script>

src/components/CRUD/Categories/SaveComponent.vue

<template>
    <h1>
        <template v-if="category">
            Update: <span class="font-bold"> {{ category.titulo }}</span>
        </template>
        <template v-else>
            Create
        </template>
    </h1>
    <div class="card max-w-lg mx-auto">
        <!-- <o-field label="Title"> -->
        <o-field label="Title" :variant="errors.title ? 'danger' : ''" :message="errors.title">
            <o-input v-model="form.title" expanded></o-input>
        </o-field>
        <o-button variant="primary" @click="send">Send</o-button>
    </div>
</template>
<script>
export default {
    data() {
        return {
            category: '',
            form: {
                title: '',
            },
            errors: {
                title: '',
            }
        }
    },
    async mounted() {
        if (this.$route.params.id) {
            await this.getCategory()
            this.init()
        }
    },
    methods: {
        init() {
            this.form.title = this.category.titulo
        },
        async getCategory() {
            this.category = await axios.get('http://code4movies.test/api/categoria/' + this.$route.params.id)
                .then(res => res.data)
                .catch(error => error)
        },
        async send() {
            let res = ''
            if (this.category == '') {
                const formData = new FormData()
                formData.append('titulo', this.form.title)
                res = await axios.post('http://code4movies.test/api/categoria', formData)
                    .then(res => res.data) // 200
                    .catch(error => error) // 500-400
                this.cleanForm()
            } else {
                res = await axios.put('http://code4movies.test/api/categoria/' + this.category.id, this.form)
                    .then(res => res.data) // 200
                    .catch(error => error) // 500-400
                this.cleanForm()
            }
            // error
            if (res.response && res.response.data.titulo) {
                this.errors.title = res.response.data.titulo
            }
        },
        cleanForm() {
            this.errors = {
                title: ''
            }
        }
    },
}
</script>

Agrupar rutas

Al poder emplear la aplicación en Vue para distintos módulos, por ejemplo, pudiéramos crear otro módulo para el usuario final en la misma aplicación que hemos creado el dashboard, resulta muy útil agrupar las rutas; para ello, podemos emplear la siguiente configuración:

const routes = [
    {
        path: '/dashboard',
        component: Base,
        children: [
            {
                name: 'movie.list',
                path: 'movie',
                component: ListMovie
            },
            {
                name: 'movie.save',
                path: 'movie/save',
                component: SaveMovie
            },
            {
                name: 'movie.save',
                path: 'movie/save/:id?',
                component: SaveMovie
            },
            {
                name: 'category.list',
                path: 'category',
                component: ListCategory
            },
            {
                name: 'category.save',
                path: 'category/save',
                component: SaveCategory
            },
            {
                name: 'category.save',
                path: 'category/save/:id?',
                component: SaveCategory
            }
        ]
    }
]

Como puedes apreciar, reemplazamos las rutas bases y las hicimos hijas de la opción children, en la cual, definimos el prefijo de las rutas que siempre debe de comenzar con un /:

path: '/dashboard',

Seguido de las rutas hijas que se les dio un mejor nombre; recuerda con esto, actualizar las referencias a los nombres en toda la aplicación, por ejemplo:

src/components/CRUD/Movies/ListComponent.vue

<o-button @click="$router.push({ name: 'movie.save' })" variant="primary" size="large">Create</o-button>

Componente específico

También, podemos crear un componente Base para estas rutas, es decir, no solamente emplear el de App.vue; lo cual es particularmente útil para poder hacer una clara distinción entre distintos módulos de nuestra aplicación utilizando un diseño distinto; para ello:

src/components/CRUD/Base.vue

<template>
 <div class="container mx-auto my-3">
    <router-view></router-view>
  </div>
</template>

Y ya que definimos de manera demostrativa el container en el nuevo componente, lo quitamos del App.vue:

src/App.vue

<template>
    <router-view></router-view>
</template>

De esta forma, ahora puedes crear otros módulos completamente personalizables pasando por su diseño.

Si tienes una Api en otros framewoks como Django, Laravel o FastApi, por mencionar algunos, es lo mismo, ya que, la Rest Api es la forma que tenemos para conectarnos mediante Vue y es independiente a este último.

Navbar: Enlaces de Navegación en Vue Router

Video thumbnail

Siguiente operación que vamos a hacer sería configurar un navbar ya vamos a configurar, en internet hay muchisima inspiración y eres libre de adaptar cualquiera:

src/components/CRUD/Base.vue

<template>

 <nav class="bg-white border-gray-200 dark:bg-gray-900">
   <div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
     <span href="" class="flex items-center space-x-3">
       <!-- <img src="" class="h-8" alt="Logo" /> -->
       <span class="self-center text-2xl font-semibold whitespace-nowrap dark:text-white">VueCode4</span>
     </span>
     <button data-collapse-toggle="navbar-default" type="button"
       class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"
       aria-controls="navbar-default" aria-expanded="false">
       <span class="sr-only">Open main menu</span>
       <svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
         <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
           d="M1 1h15M1 7h15M1 13h15" />
       </svg>
     </button>
     <div class="hidden w-full md:block md:w-auto" id="navbar-default">
       <ul
         class="font-medium flex flex-col p-4 md:p-0 mt-4 border border-gray-100 rounded-lg bg-gray-50 md:flex-row md:space-x-8 rtl:space-x-reverse md:mt-0 md:border-0 md:bg-white dark:bg-gray-800 md:dark:bg-gray-900 dark:border-gray-700">
         <li>
           <router-link 
             class="block py-2 px-3 text-white bg-blue-700 rounded-sm md:bg-transparent md:text-blue-700 md:p-0 dark:text-white md:dark:text-blue-500"
           :to="{ name:'movie.list' }">Movies</router-link>
         </li>
         <li>
           <router-link 
             class="block py-2 px-3 text-white bg-blue-700 rounded-sm md:bg-transparent md:text-blue-700 md:p-0 dark:text-white md:dark:text-blue-500"
           :to="{ name:'category.list' }">Category</router-link>
         </li>       
       </ul>
     </div>
   </div>
 </nav>


 <div class="container mx-auto my-3">
   <router-view></router-view>
 </div>
</template>

Activar Menú de hamburguesa en el Navbar

Video thumbnail

Antes de comenzar, es importante recordar que aquí había un enlace, que es donde definimos el enlace botón de hamburguesa:

<button data-collapse-toggle="navbar-default" type="button"
  class="*** md:hidden ***"
  aria-controls="navbar-default" aria-expanded="false">
  <span class="sr-only">Open main menu</span>
</button>

El Problema en Modo Móvil

Entonces, retomando un poco el menú de hamburguesa, que sería este que tenemos aquí configurado... ¿cuál es el problema?

Aquí, en modo escritorio, todo funciona perfectamente. El menú se muestra sin problemas.

Pero cuando cambiamos a modo móvil, tenemos el menú de hamburguesa. Basado en lo que te explicaba antes, lo que sucede aquí es que ya no se cumple la regla md:block. Esa clase está en alguna parte — si hacemos un Control+F y buscamos md:block, veremos que es la que se encarga de mostrar el menú en escritorio. Al no cumplirse esa condición en móvil, el menú se oculta, y a partir de ahí se aplican otras reglas específicas para móviles.

Supón que quitamos esa clase: fíjate que el menú vuelve a aparecer y podemos navegar otra vez sin problema. Sin embargo, no quiero que el menú esté visible por defecto, sino que se muestre u oculte cuando presionemos el botón de hamburguesa.

Menú móvil

Ya tenemos identificado que el "problema" entre comillas está en esta clase hidden que tenemos acá. Así que, de forma programática, tenemos que ocultarlo o mostrarlo según el criterio del usuario. 

Implementación de la Lógica de Toggle

Vamos a manejar la lógica del toggle directamente en el componente. Coloco el bloque <script> y dentro, el data:

<button @click="showMenuMobile=!showMenuMobile" data-collapse-toggle="navbar-default" type="button" ***>***</button>
***
<div class="w-full md:block md:w-auto" :class="{ 'hidden' : showMenuMobile }" id="navbar-default">
***
<script>
export default {
  data() {
    return {
      showMenuMobile: false,
    }
  },
}
</script>

Aplicar Clases Condicionales

El siguiente paso es configurar las clases, es decir, quitar o mostrar el menú según el estado:

<button data-collapse-toggle="navbar-default" type="button"
  class="*** md:hidden ***"
  aria-controls="navbar-default" aria-expanded="false">
  <span class="sr-only">Open main menu</span>
</button>

Para esto, usamos :class. Es posible que no lo hayamos visto aún en el curso, pero es muy útil. Vue es lo suficientemente inteligente como para evaluar la condición y anexar o quitar la clase automáticamente:

<div class="w-full md:block md:w-auto" :class="{ 'hidden' : showMenuMobile }" id="navbar-default">

Detalles de Estilo

Se pueden aplicar transiciones, pero eso no es el foco ahora (esto es un curso básico). Lo que estamos haciendo es seleccionar el menú, que tiene muchas clases. Fíjate que aparece el hidden al principio, y cuando hacemos click ya no aparece, gracias a la lógica que aplicamos.

Esto funciona igual que usar un atributo condicional: si se cumple, se aplica; si no, no. Vue maneja internamente este atributo :class, porque en HTML solo puede haber un atributo class por elemento.

Vue lo que hace es mezclar las clases estáticas con las dinámicas basadas en la condición. Puedes añadir más condiciones si es necesario. Por ejemplo:

:class="{ 'hidden': !showMenuMobile, 'otra-clase': otraCondicion }"

Opcional: Exponemos las variables de Oruga UI para variar el estilo

Importamos el paquete de las variables con el cual podremos variar fácilmente el tema:

resources\js\vue\main.js

//***
import '@oruga-ui/oruga-next/dist/oruga-full.css'
import '@oruga-ui/oruga-next/dist/oruga-full-vars.css'

Modificamos las variables

 Y ya con esto, ahora el CSS de los componente de Oruga UI es basado en variables, y con esto, podemos personalizar las mismas; en este ejemplo, usando Tailwind:

resources\css\vue.css

h1{
    @apply text-3xl text-center my-5
}
:root{
    --oruga-modal-content-padding:0
}

Extra: Mensaje de acción realizada, toast en Vue 3 con Oruga UI

Otro componente muy interesante que tenemos a nuestra disposición es del de mostrar mensaje tipo toast; para eso tenemos la función de:

this.$oruga.notification.open

Que recibe como parámetros:

  • message, El mensaje.
  • position, La posición que puede ser: top-right toptop-left bottom-right bottom  bottom-left
  • variant, La variante que puede ser: primary info warning danger
  • duration, La duración expresada en milésimas de segundo.
  • closable, Y si puede ser cerrado mediante un click.

Por ejemplo:

this.$oruga.notification.open({
        message: "Registro eliminado",
        position: "bottom-right",
        variant: "danger",
        duration: 4000,
        closable: true,
});

O para guardar:

this.$oruga.notification.open({
   message: "Registro procesado con éxito",
   position: "bottom-right",
   duration: 4000,
   closable: true,
});

Modal de confirmación para eliminar registros desde Vue 3 con Oruga UI

En Oruga UI tenemos un componente llamado modal, que es como un lienzo vacío listo para que coloquemos todo el contenido que queramos colocar; para el caso de un diálogo de confirmación, sería un texto para eliminar y los botones de acciones:

resources\js\vue\componets\List.vue

<o-modal v-model:active="confirmDeleteActive">
  <div class="p-4">
    <p>¿Seguro que quieres eliminar el registro seleccionado?</p>
  </div>
  <div class="flex flex-row-reverse gap-2 bg-gray-100 p-3">
    <o-button variant="danger" @click="deletePost()">Eliminar</o-button>
    <o-button @click="confirmDeleteActive = false">Cancelar</o-button>
  </div>
</o-modal>

Desde el listado, lo único que hacemos es llamar al modal mediante una propiedad booleana:

  • true, para mostrar el modal
  • false, para ocultar el modal

En la tabla de Oruga UI, ahora activamos el modal y establecemos el row del elemento que queremos eliminar; ya que, al tener un proceso de por medio, no se puede accionar directamente la eliminación:

<o-table-column field="slug" label="Acciones" v-slot="p">
        <router-link
          class="mr-3"
          :to="{ name: 'save', params: { slug: p.row.slug } }"
          >Editar</router-link
        >
 
        <o-button
          iconLeft="delete"
          rounded
          size="small"
          variant="danger"
          @click="
            deletePostRow = p;
            confirmDeleteActive = true;
          "
          >Eliminar</o-button
        >
      </o-table-column>
    </o-table>

La propiedad en cuestión:

data() {
    return {
      //***
      confirmDeleteActive: false,
      deletePostRow: "",
    };

 Y la función de eliminar que ahora elimina por el índice del registro:

deletePost() {
      this.confirmDeleteActive = false;
      this.posts.data.splice(this.deletePostRow.index, 1);
      this.$axios.delete("/api/post/" + this.deletePostRow.row.id);
    },

Recuerda que el row de la tabla en Oruga UI tiene mucha información como el índice de la tabla, además del ID que es un campo que puedes personalizar para indicar por ejemplo la PK del registro con el cual interactuar.

Extra: Cómo reutilizar fácilmente formularios en Vue

Las aplicaciones web contienen muchos formularios hoy en día. Muchas veces, tenemos el mismo diseño de formulario al crear o editar algo (puede ser cualquier cosa: usuario, proyecto, elemento de tarea pendiente, producto, etc.). Por lo general, la creación y edición de un recurso se implementa en 2 páginas separadas; pero, al ser procesos similares, siempre lo podemos reutilizar fácilmente.

Afortunadamente, si usa Vue, puede implementar componentes de formularios reutilizables fácilmente. Empecemos entonces.

Creemos un componente de formulario reutilizable

Crearemos un formulario simple para crear o editar un usuario. Sin más que decir, así es como se verá la forma reutilizable al final.

Para simplificar el proceso, el formulario solamente va a tener dos campos, pero puedes emplear la misma lógica si tienes más campos.

Vamos a conocer primero los componentes de crear y editar por separado, luego, veremos la combinación de ambos.

Componente para crear elementos

Estructura fácil de seguir, un formulario, que capturamos el evento submit con @submit.prevent, nuestros campos de formulario con el v-model y la función submit para guardar datos.

<template>
 <form @submit.prevent="submit">
   <input type="text" v-model="form.title" />
   <textarea v-model="form.description"></textarea>
   <button type="submit">Enviar</button>
 </form>
</template>
<script>
export default {
 mounted() {},
 data() {
   return {
     form: {
       title: "", //this.$root.animes[this.position].title,
       description: "",
     },
   };
 },
 methods: {
   submit: function () {
         //GUARDAR
     });
   },
 },
};
</script>

Componente para Editar

Para la inicialización, es exactamente lo mismo, salvo que inicializamos los datos, para eso el mounted, y en el proceso de guardar lo cual puedes actualizar alguna lista, enviar peticiones etc:

<template>
<form @submit.prevent="submit">
   <input type="text" v-model="form.title">
   <textarea v-model="form.description"></textarea>
   <button type="submit">Enviar</button>
</form>
</template>
<script>
export default {
   mounted() {

       this.position = this.$route.params.position
       this.form.title = this.$root.animes[this.position].title
       this.form.description = this.$root.animes[this.position].description
   },
 data() {
   return {
     position: 0,
     form: {
       title:"", 
       description: "",
     },
   };
 },
 methods: {
   submit: function () {
       // EDITAR
   },
 },
};
</script>

Componente para Editar y Crear

Ahora vamos a fusionar los componente anteriores; para eso, tenemos que recibir algun parametro desde el componente padre para saber si estamos en edición o creación; en este caso, suponemos que es un proceso que podemos llevar a cabo mediante Vue Router:

this.position = this.$route.params.position || -1;

Ya con esta inicialización, determinamos si inicializamos los datos o no, al igual de si creamos o editados cuando enviamos el formulario:

<template>
 <form @submit.prevent="submit">
   <input type="text" v-model="form.title" />
   <textarea v-model="form.description"></textarea>
   <button type="submit">Enviar</button>
 </form>
</template>
<script>
export default {
 mounted() {
   this.position = this.$route.params.position || -1;
   this.form.title =
     this.position !== -1 ? this.$root.animes[this.position].title : "";
   this.form.description =
     this.position !== -1 ? this.$root.animes[this.position].description : "";
 },
 data() {
   return {
     position: 0,
     form: {
       title: "", //this.$root.animes[this.position].title,
       description: "",
     },
   };
 },
 methods: {
   submit: function () {
     if (this.position !== -1)
// EDITAR
       this.$root.animes[this.position].title = this.form.title;
     else {
// CREAR
       this.$root.animes.push({
         id: this.$root.animes.length,
         title: this.form.title,
         description: this.form.description,
       });
     }
   },
 },
};
</script>

Extra: Iconografía Material Design en Vue 3 con oruga UI

Video thumbnail

El Material Design es una plantilla de diseño utilziada por Google que ha desarrollado para crear interfaces de usuario visuales y consistentes y es empleada diferentes plataformas como Android. Material Design se basa en principios de diseño como la profundidad, la luz y el movimiento, sombras, animaciones y transiciones para crear una sensación de profundidad y realismo en las interfaces de usuario, con el objetivo de proporcionar una experiencia de usuario intuitiva y atractiva.

También se utilizan colores vibrantes y tipografía legible para mejorar la legibilidad y la accesibilidad y como puedes suponer, tambien emplea un conjunto de íconos muy mimimalistas que podemos emplear que es lo que vamos a hablar en esta entrada.

Ya con nuestro proyecto creado en Vue 3, en estas guías sería con Laravel, aunque puedes seguir los mismos pasos con Vue Cli, vamos a instalar una iconografía para poder usar los iconos en Vue con Oruga UI.

En este caso, será la de Material Design de Google:

Para la iconografía, usaremos MaterialDesign:

$ npm install @mdi/font

Y lo referenciamos:

resources/js/vue/main.js
//Material Design
import "@mdi/font/css/materialdesignicons.min.css"

Con esto, puedes usar cualquiera de los iconos que puedes encontrar en la web oficial:

https://materialdesignicons.com/

Extra: Agrupado de rutas en Vue Router: Rutas Hijas, prefijo y Componente

Video thumbnail

La agrupación se realiza definiendo una ruta principal (/dashboard) que actúa como contenedor y utiliza el componente base (Base.vue o similar). Las rutas específicas del CRUD (películas y categorías) se definen dentro del array children.

const routes = [
    {
        path: '/dashboard',
        component: Base,
        children: [
            {
                name: 'movie.list',
                path: 'movie',
                component: ListMovie
            },
            {
                name: 'movie.save',
                path: 'movie/save',
                component: SaveMovie
            },
            {
                name: 'movie.save',
                path: 'movie/save/:id?',
                component: SaveMovie
            },
            {
                name: 'category.list',
                path: 'category',
                component: ListCategory
            },
            {
                name: 'category.save',
                path: 'category/save',
                component: SaveCategory
            },
            {
                name: 'category.save',
                path: 'category/save/:id?',
                component: SaveCategory
            }
        ]
    }
]

La sintaxis de Vue Router puede parecer un poco extraña a primera vista. Lógicamente, si utilizamos la propiedad children, esperaríamos que las rutas se colocaran al mismo nivel del objeto, pero no es así.

Realmente, la ruta agrupada es la estructura completa. Es decir, la configuración de las rutas agrupadas comienza en el objeto principal (en tu caso, desde la línea 9, que inicia el array children). Por lo tanto, la configuración anidada va ahí.

No se coloca aquí dentro el path directamente, sino que la ruta padre es la que define el layout y el prefijo. Dentro del array children solo colocamos nuestras rutas hijas, a las cuales ya podemos asignarles su propio path: relativo al padre.

{
    path: '/dashboard',

Acá aquí en caso de pudiéramos definir un componente que lo que es es sería una ruta un componente como el de como el de App un componente como el de a Vue es lo que pudiéramos colocar acá:

    {
        path: '/dashboard',
        component: Base,

De esta forma puedes definir un estilo en común para las rutas agrupadas:

src/components/CRUD/Base.vue

<template>
<div class="container mx-auto my-3">
   <router-view></router-view>
 </div>
</template>

Extra: Componente de Paginación en Oruga UI

Video thumbnail

Ya con el listado con la tabla de Oruga UI listo, lo siguiente que se va a realizar es la paginación; para ello, vamos a usar el componente de paginación de Oruga, que recibe varios parámetros; muchos de ellos de forma, para estilos y poco más:

  • @change="updatePage" Evento que se ejecuta al dar un click sobre un enlace paginado, en estos casos debes de usarlo para actualizar el listado
  • :total="posts.total" Indica el total de registros
  • v-model:current="currentPage" Indica el v-model que se actualizará con el índice de la página actual
  • :range-before="2" Cantidad de páginas que aparece antes de la página actual.
  • :range-after="2" Cantidad de páginas que aparece después de la página actual.
  • order="centered" Paginación centrada.
  • size="small" Tipo de paginación.
  • :simple="false" Diseño, si es simple o completa.
  • rounded="true" Diseño redondeado.
  • :per-page="posts.per_page" Cuantos registros vas a mostrar por páginas.

En Laravel, al usar el componente de paginación, tenemos todos estos parámetros ya definidos; por lo tanto, la integración es directa.

Finalmente, el componente de paginación con la tabla quedará como:

  ***
 </o-table>
    <br />
    <o-pagination
      v-if="posts.current_page && posts.data.length > 0"
      @change="updatePage"
      :total="posts.total"
      v-model:current="currentPage"
      :range-before="2"
      :range-after="2"
      order="centered"
      size="small"
      :simple="false"
      :rounded="true"
      :per-page="posts.per_page"
    >
    </o-pagination>
  </div>
</template>
<script>
export default {
  data() {
    return {
      posts: [],
      isLoading: true,
      currentPage:1,
    };
  },
methods: {
  updatePage(){
    setTimeout(this.listPage, 100);
  },
  listPage(){
    this.isLoading = true;
     this.$axios.get("/api/post?page="+this.currentPage).then((res) => {
      this.posts = res.data;
      console.log(this.posts);
      this.isLoading = false;
    });
  }
},
  async mounted() {
   this.listPage()
  },
};
</script>

Extra: Crear un loading o página de carga

Crear un loading para nuestra pagina es básico en cualquier aplicación que NO tenga contenido estático y necesitamos mostrarlo cuando lanzamos un proceso que va a demorar algo de tiempo.

Para ello vamos a hacer uso de un paquete que podemos instalar mediante Node:

https://www.npmjs.com/package/vue-loading-overlay

Pudiéramos emplear un SVG, Gif o un vídeo como recurso, adecuarla con CSS y mostrarlo/ocultarlo según alguna necesidad mediante alguna propiedad booleana, pero para hacerlo más interesante vamos a emplear el paquete anterior que una vez instalado, podemos hacer uso del componente de:

       <loading
           v-model:active="isLoading"
           :can-cancel="false"
           :is-full-page=true
       />

Trabajando con el loading

El cual define los siguientes componentes:

  1. v-model:active El cual recibe un booleano que indica si está o no visible.
  2. :can-cancel que indica si el usuario podrá cancelar o no el loading.
  3. :is-full-page Que indica si la mostramos en modo full screen o no.

Así que, con esto, adaptamos el siguiente script que forma parte de mi curso de Electron, en el cual mediante el model isLoading mostramos el Loading apenas cargue la página, ya que la carga que hacemos desde Firebase (Internet) lo hacemos de manera inicial apenas cargue el componente; luego, una vez cargado la data lo ocultamos:

this.groups = await getGroups()
this.isLoading=false

Por lo demás, cargamos el componente antes de usar y ya estamos:

import Loading from 'vue-loading-overlay';
import 'vue-loading-overlay/dist/vue-loading.css';
Codigo completo
<template>
   <div>
       <loading
           v-model:active="isLoading"
           :can-cancel="false"
           :is-full-page=true
       />
       <h1>Grupos</h1>
       <div class="list-group list-group-flush">
           <router-link class="list-group-item list-group-item-action" v-for="g in groups" v-bind:key="g" :to="{ name:'Chat', params:{ room: g.id } }">
               {{ g.description }}
           </router-link>
       </div>
   </div>
</template>
<script>
import { getGroups } from "../helpers/fchat"
import Loading from 'vue-loading-overlay';
import 'vue-loading-overlay/dist/vue-loading.css';
export default {
   components:{
       Loading
   },
   mounted: async function(){
       this.groups = await getGroups()
       this.isLoading=false
       /*this.groups.then((res)=> {
           console.log(res)
       })*/
   },
   data() {
       return {
           groups:[],
           isLoading:true
       }
   },
}
</script>

En el código anterior, puedes ver que realizamos una petición justamente cuando montamos la aplicación de tipo síncrona, el loading se muestra por defecto según la inicialización que hicimos, una vez cargada la data, quitamos el loading.

Extra: Create a loading page

Creating a loading for our page is basic in any application that does NOT have static content and we need to show it when we launch a process that will take some time.

For this we are going to make use of a package that we can install through Node:

https://www.npmjs.com/package/vue-loading-overlay

We could use an SVG, Gif or a video as a resource, adapt it with CSS and show/hide it according to some need through some boolean property, but to make it more interesting we are going to use the previous package that once installed, we can make use of the component:

       <loading
           v-model:active="isLoading"
           :can-cancel="false"
           :is-full-page=true
       />

Working with loading

Which defines the following components:

  1. v-model:active Which receives a boolean indicating whether or not it is visible.
  2. :can-cancel that indicates whether or not the user will be able to cancel the loading.
  3. :is-full-page That indicates if we show it in full screen mode or not.

So, with this, we adapt the following script that is part of my Electron course, in which through the isLoading model we show the Loading as soon as the page loads, since the load we do from Firebase (Internet) we do it initially just load the component; then, once the data is loaded we hide it:

this.groups = await getGroups()
this.isLoading=false

Otherwise, we load the component before using and we're done:

import Loading from 'vue-loading-overlay';
import 'vue-loading-overlay/dist/vue-loading.css';
Codigo completo
<template>
   <div>
       <loading
           v-model:active="isLoading"
           :can-cancel="false"
           :is-full-page=true
       />
       <h1>Grupos</h1>
       <div class="list-group list-group-flush">
           <router-link class="list-group-item list-group-item-action" v-for="g in groups" v-bind:key="g" :to="{ name:'Chat', params:{ room: g.id } }">
               {{ g.description }}
           </router-link>
       </div>
   </div>
</template>
<script>
import { getGroups } from "../helpers/fchat"
import Loading from 'vue-loading-overlay';
import 'vue-loading-overlay/dist/vue-loading.css';
export default {
   components:{
       Loading
   },
   mounted: async function(){
       this.groups = await getGroups()
       this.isLoading=false
       /*this.groups.then((res)=> {
           console.log(res)
       })*/
   },
   data() {
       return {
           groups:[],
           isLoading:true
       }
   },
}
</script>

In the code above, you can see that we make a request just when we mount the synchronous type application, the loading is shown by default according to the initialization we did, once the data is loaded, we remove the loading.

Extra: Manejar el token de autenticación

Vamos a partir de un proyecto creado en Laravel en la cual configuramos Vue en un proyecto en Laravel.

Instalar plugin para la cookie

Vamos a instalar un plugin para manejar las Cookies en Vue 3:

https://www.npmjs.com/package/vue3-cookies

Ejecutamos en la terminal:

$ npm install vue3-cookies --save

Configurar el plugin en el proyecto

Su uso es muy sencillo vamos a primero a inicializar el plugin de la cookie en el proyecto, para ello, usaremos el archivo de configuración de la aplicación:

import { createApp } from "vue";
//tailwind
import '../../css/vue.css'
// Oruga
import Oruga from '@oruga-ui/oruga-next'
import '@oruga-ui/oruga-next/dist/oruga.css'
import '@oruga-ui/oruga-next/dist/oruga-full.css'
import '@oruga-ui/oruga-next/dist/oruga-full-vars.css'
//Material Design
import "@mdi/font/css/materialdesignicons.min.css"
import axios from 'axios'
import App from "./App.vue"
import router from "./router"
import VueCookies from 'vue3-cookies'
const app = createApp(App).use(Oruga).use(router)
app.use(VueCookies);
app.config.globalProperties.$axios = axios
window.axios = axios
app.mount("#app")

En el script anterior, tenemos otros plugins configurados, en este caso, Oruga UI con Laravel y Vue, ya que, este es un proyecto que forma parte de mi curso completo sobre Laravel.

Uso de las cookies

Ya con esto, podemos hacer uso de la cookie y con esto, poder guardar valores:

this.$cookies.set(keyName,'value');

U obtenerlos:

$cookies.get(keyName)

Como puedes ver, su uso una vez configurado, es extremadamente sencillo.

Configurar vue3-cookies con los datos de autenticación

Vamos a crear una función para establecer los valores de usuario en el componente padre, con lo cual, podremos usarlo en cualquier parte de la aplicación mediante los componentes hijos:

resources\js\vue\App.vue

setCookieAuth(data) {
  this.$cookies.set("auth", data);
},

La misma, la usaremos desde el login:

resources\js\vue\componets\Auth\Login.vue

submit() {
  this.cleanErrorsForm();
  return this.$axios
    .post("/api/user/login", this.form)
    .then((res) => {
   this.$root.setCookieAuth({
        isLoggedIn: res.data.isLoggedIn,
        token: res.data.token,
        user: res.data.user,
      });
   ***
}

La función anterior, es la que usamos para configurar la cookie de autenticación en el proyecto al momento de hacer el login desde la aplicación; la función de login en Laravel es:

    public function login(Request $request)
    {
        $credentials = [
            'email' => $request->email,
            'password' => $request->password
        ];


        if (Auth::attempt($credentials)) {
            $token = Auth::user()->createToken('myapptoken')->plainTextToken;
            session()->put('token', $token);


            return response()->json([
                'isLoggedIn' => true,
                'user' => auth()->user(),
                'token' => $token,
            ]);
        }
        return response()->json("Usuario y/o contraseña inválido", 422);
    }

Acepto recibir anuncios de interes sobre este Blog.

Aprende a construir una aplicación CRUD (Crear, Leer, Actualizar, Eliminar) dinámica utilizando Vue 3 (Options API) y consumiendo una API RESTful desarrollada en CodeIgniter 4. Esta guía cubre la configuración de Vue, la integración de librerías clave y las mejores prácticas de desarrollo.

| 👤 Andrés Cruz

🇺🇸 In english