Desarrollo de Interfaces Dinámicas con Vue.js: Carritos Interactivos, APIs y Ciclo de Vida

Construyendo un Carrito de Compras Interactivo con Vue.js

La reactividad de Vue.js es una de sus características más potentes. Cuando los datos de una instancia de Vue cambian, la interfaz de usuario que depende de esos datos se actualiza automáticamente. Este comportamiento es fundamental para construir aplicaciones web dinámicas y, a continuación, exploraremos cómo aplicarlo en un ejemplo práctico: un carrito de compras funcional.

Carrito de Compras Básico con Gestión de Cantidades

Comenzaremos con la estructura fundamental de nuestro carrito, permitiendo la visualización de productos, la selección individual y la modificación de cantidades.

Estructura HTML y Script Vue

Consideremos la siguiente maqueta HTML y el script Vue asociado. Utilizaremos Bootstrap para un diseño básico.


<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>Carrito de Compras Vue.js</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" crossorigin="anonymous">
</head>
<body>
    <div id="app-carrito">
        <div class="container">
            <div class="row">
                <div class="col-md-8 col-md-offset-2">
                    <h1>Mi Carrito de Compras</h1>

                    <table class="table table-bordered">
                        <thead>
                            <tr>
                                <th>ID</th>
                                <th>Artículo</th>
                                <th>Precio Unitario</th>
                                <th>Cantidad</th>
                                <th>
                                    Seleccionar <input type="checkbox" v-model="todosSeleccionados" @change="manejarSeleccionTotal">
                                </th>
                            </tr>
                        </thead>
                        <tbody>
                            <tr v-for="producto in listaProductos" :key="producto.id">
                                <th scope="row">{{ producto.id }}</th>
                                <td>{{ producto.nombre }}</td>
                                <td>{{ producto.precio.toFixed(2) }} €</td>
                                <td>
                                    <button class="btn btn-sm btn-danger" @click="disminuirCantidad(producto)">-</button>
                                    {{ producto.cantidad }}
                                    <button class="btn btn-sm btn-success" @click="aumentarCantidad(producto)">+</button>
                                </td>
                                <td><input type="checkbox" v-model="productosSeleccionados" :value="producto" @change="manejarSeleccionIndividual"></td>
                            </tr>
                        </tbody>
                    </table>
                    <hr>
                    <p>Productos seleccionados: {{ productosSeleccionados.length }}</p>
                    <h3>Total a pagar: {{ totalPagar.toFixed(2) }} €</h3>
                </div>
            </div>
        </div>
    </div>

    <script>
        var vm = new Vue({
            el: '#app-carrito',
            data: {
                listaProductos: [
                    { id: 'P101', nombre: 'Bolígrafo Premium', precio: 15.50, cantidad: 1 },
                    { id: 'P102', nombre: 'Cuaderno Ecológico', precio: 8.75, cantidad: 2 },
                    { id: 'P103', nombre: 'Marcadores Colores', precio: 12.00, cantidad: 1 }
                ],
                productosSeleccionados: [],
                todosSeleccionados: false
            },
            computed: {
                totalPagar: function() {
                    let total = 0;
                    for (let producto of this.productosSeleccionados) {
                        total += producto.precio * producto.cantidad;
                    }
                    return total;
                }
            },
            methods: {
                aumentarCantidad: function(producto) {
                    // Aquí podríamos añadir una comprobación de stock
                    producto.cantidad++;
                },
                disminuirCantidad: function(producto) {
                    if (producto.cantidad > 1) {
                        producto.cantidad--;
                    } else {
                        alert('La cantidad mínima es 1.');
                    }
                },
                manejarSeleccionTotal: function() {
                    if (this.todosSeleccionados) {
                        this.productosSeleccionados = [...this.listaProductos]; // Clonar el array para evitar referencias
                    } else {
                        this.productosSeleccionados = [];
                    }
                },
                manejarSeleccionIndividual: function() {
                    this.todosSeleccionados = this.productosSeleccionados.length === this.listaProductos.length;
                }
            }
        });
    </script>
</body>
</html>

Observaciones Clave del Carrito

  • v-for para renderizado de listas: Permite iterar sobre listaProductos para mostrar cada artículo. La propiedad :key es crucial para la eficiencia de Vue en la actualización de listas.
  • v-model con arrays: Los checkboxes individuales en la columna "Seleccionar" usan v-model="productosSeleccionados". Cuando un checkbox se marca, el objeto producto completo (definido por :value="producto") se añade o elimina del array productosSeleccionados.
  • Propiedad Computada (computed): totalPagar es una propiedad computada. Esto significa que su valor se recalcula automáticamente solo cuando alguna de sus dependencias (en este caso, productosSeleccionados o la cantidad de los productos dentro de él) cambia. Esto es más eficiente que una función en el <template> que se ejecutaría en cada renderizado.
  • Botones de Cantidad: Los botones de aumentar (+) y disminuir (-) modifican directamente la propiedad cantidad del objeto producto. Gracias a la reactividad de Vue, la interfaz se actualiza instantáneamente.
  • Selección Total/Individual:
    • El checkbox "Seleccionar todo" está vinculado a todosSeleccionados. Cuando se marca, el método manejarSeleccionTotal copia todos los productos a productosSeleccionados.
    • Cuando un checkbox individual cambia, manejarSeleccionIndividual actualiza el estado de todosSeleccionados.

Es importante destacar que cuando se pasa un objeto (como producto) a un método o se modifica directamente en el template (producto.cantidad++), se está trabajando con una referencia al objeto original en listaProductos. Cualquier cambio afecta directamente al objeto en el array principal, lo cual es el comportamiento deseado en este caso para la reactividad.

Modificadores de v-model

Vue ofrece modificadores para v-model que permiten personalizar cómo se sincronizan los datos entre los elementos del formulario y la instancia de Vue.


<html lang="es">
<head>
    <meta charset="UTF-8">
    <title>Modificadores v-model</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
</head>
<body>
    <div id="app-modificadores">
        <h2>Modificadores de v-model</h2>

        <div>
            <p><strong>.lazy:</strong> Sincroniza después del evento 'change' (perder foco).</p>
            <input type="text" v-model.lazy="textoDiferido">
            <p>Valor: <strong>{{ textoDiferido }}</strong></p>
        </div>

        <div>
            <p><strong>.number:</strong> Intenta convertir la entrada a un número.</p>
            <input type="text" v-model.number="entradaNumerica">
            <p>Valor: <strong>{{ entradaNumerica }}</strong> (Tipo: {{ typeof entradaNumerica }})</p>
        </div>

        <div>
            <p><strong>.trim:</strong> Recorta los espacios en blanco de los extremos.</p>
            <input type="text" v-model.trim="textoRecortado">
            <p>Valor: '<strong>{{ textoRecortado }}</strong>' (Longitud: {{ textoRecortado.length }})</p>
        </div>
    </div>

    <script>
        var vmModificadores = new Vue({
            el: '#app-modificadores',
            data: {
                textoDiferido: '',
                entradaNumerica: 0,
                textoRecortado: ''
            }
        });
    </script>
</body>
<html>

  • .lazy: Por defecto, v-model sincroniza la entrada con los datos en cada evento input. Con .lazy, la sincronización se retrasa hasta el evento change (cuando el campo pierde el foco).
  • .number: Si se especifica, Vue intentará convertir la cadena de entrada a un número. Si la conversión falla, se devuelve la cadena original. Es útil para campos numéricos donde el tipo de dato es importante.
  • .trim: Elimina automáticamente los espacios en blanco iniciales y finales de la cadena de entrada.

Interacción con el Backend: Solicitudes AJAX

La mayoría de las aplicaciones web modernas necesitan interactuar con un backend para obtener o enviar datos. Vue.js se integra perfectamente con cualquier biblioteca de cliente HTTP.

Política de Origen Cruzado (CORS)

Antes de sumergirnos en el código, es crucial entender la Política de Origen Cruzado (CORS). Es un mecanismo de seguridad del navegador que restringe las solicitudes HTTP realizadas desde un origen (dominio, protocolo, puerto) diferente al que sirvió la página web actual. Si tu frontend (ej. http://localhost:8080) intenta hacer una solicitud a un back end (ej. http://localhost:5000), el navegador bloqueará la respuesta a menos que el servidor backend incluya las cabeceras CORS adecuadas, como Access-Control-Allow-Origin: * (para permitir cualquier origen) o Access-Control-Allow-Origin: http://localhost:8080.

Ejemplo de Backend (Flask Simplificado)

Para nuestros ejemplos, asumiremos un servidor Flask mínimo que expone un endpoint para datos de usuario y otro para películas, con las cabeceras CORS configuradas:

# app_backend.py
from flask import Flask, jsonify, make_response
from flask_cors import CORS # Para simplificar la gestión de CORS

app = Flask(__name__)
CORS(app) # Habilita CORS para todas las rutas

@app.route('/api/usuario')
def obtener_usuario():
    data = {'nombre': 'Elena', 'edad': 28, 'ciudad': 'Madrid'}
    return jsonify(data)

@app.route('/api/peliculas')
def obtener_peliculas():
    # Datos de películas simulados
    peliculas_data = {
        'data': {
            'films': [
                {'id': 'F001', 'titulo': 'Código Secreto', 'director': 'R. Jackson', 'genero': 'Thriller', 'imagen': 'https://via.placeholder.com/150/0000FF/FFFFFF?text=Pelicula1', 'actores': [{'nombre': 'Julia D.'}, {'nombre': 'Mark S.'}], 'sinopsis': 'Un thriller de espionaje.'},
                {'id': 'F002', 'titulo': 'Sueños de Invierno', 'director': 'L. Miller', 'genero': 'Drama', 'imagen': 'https://via.placeholder.com/150/FF0000/FFFFFF?text=Pelicula2', 'actores': [{'nombre': 'Anna B.'}, {'nombre': 'Tom H.'}], 'sinopsis': 'Una historia conmovedora.'}
            ]
        }
    }
    return jsonify(peliculas_data)

if __name__ == '__main__':
    app.run(port=5000, debug=True)

Ejecuta este script (necesitarás pip install Flask Flask-Cors) para tener un backend en http://localhost:5000.

Clientes AJAX en Vue.js

Ahora, veamos cómo hacer solicitudes desde Vue utilizando jQuery, Fetch API y Axios.

1. Con jQuery

Aunque no es la opción más moderna para Vue, es útil conocerla.

<!-- En tu HTML, antes del script Vue -->
<script src="https://cdn.jsdelivr.net/npm/jquery@3.6.1/dist/jquery.min.js"></script>

<div id="app-jquery">
    <h2>Datos de Usuario con jQuery AJAX</h2>
    <button @click="cargarUsuarioJquery" class="btn btn-info">Cargar Usuario (jQuery)</button>
    <div v-if="usuarioJquery.nombre">
        <p>Nombre: <strong>{{ usuarioJquery.nombre }}</strong></p>
        <p>Edad: <strong>{{ usuarioJquery.edad }}</strong></p>
    </div>
</div>

<script>
    new Vue({
        el: '#app-jquery',
        data: {
            usuarioJquery: {}
        },
        methods: {
            cargarUsuarioJquery: function() {
                let vm = this; // Capturar 'this' para usarlo dentro de la callback de jQuery
                $.ajax({
                    url: 'http://localhost:5000/api/usuario',
                    type: 'GET',
                    dataType: 'json',
                    success: function(respuesta) {
                        vm.usuarioJquery = respuesta;
                    },
                    error: function(error) {
                        console.error('Error al cargar datos con jQuery:', error);
                    }
                });
            }
        }
    });
</script>

2. Con Fetch API

La Fetch API es una interfaz nativa del navegador para realizar solicitudes de red, basada en Promesas.

<div id="app-fetch">
    <h2>Datos de Usuario con Fetch API</h2>
    <button @click="cargarUsuarioFetch" class="btn btn-primary">Cargar Usuario (Fetch)</button>
    <div v-if="usuarioFetch.nombre">
        <p>Nombre: <strong>{{ usuarioFetch.nombre }}</strong></p>
        <p>Ciudad: <strong>{{ usuarioFetch.ciudad }}</strong></p>
    </div>
</div>

<script>
    new Vue({
        el: '#app-fetch',
        data: {
            usuarioFetch: {}
        },
        methods: {
            cargarUsuarioFetch: function() {
                fetch('http://localhost:5000/api/usuario')
                    .then(response => response.json()) // Convierte la respuesta a JSON
                    .then(data => {
                        this.usuarioFetch = data;
                    })
                    .catch(error => {
                        console.error('Error al cargar datos con Fetch:', error);
                    });
            }
        }
    });
</script>

3. Con Axios (Recomendado en Vue)

Axios es una librería de cliente HTTP basada en Promesas, muy popular en el ecosistema Vue.js por su facilidad de uso y características.

<!-- En tu HTML, antes del script Vue -->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>

<div id="app-axios">
    <h2>Datos de Usuario con Axios</h2>
    <button @click="cargarUsuarioAxios" class="btn btn-warning">Cargar Usuario (Axios)</button>
    <div v-if="usuarioAxios.nombre">
        <p>Nombre: <strong>{{ usuarioAxios.nombre }}</strong></p>
        <p>Edad: <strong>{{ usuarioAxios.edad }}</strong></p>
        <p>Ciudad: <strong>{{ usuarioAxios.ciudad }}</strong></p>
    </div>
</div>

<script>
    new Vue({
        el: '#app-axios',
        data: {
            usuarioAxios: {}
        },
        methods: {
            cargarUsuarioAxios: function() {
                axios.get('http://localhost:5000/api/usuario')
                    .then(response => {
                        // Axios coloca la respuesta en la propiedad 'data'
                        this.usuarioAxios = response.data;
                    })
                    .catch(error => {
                        console.error('Error al cargar datos con Axios:', error);
                    });
            }
        }
    });
</script>

Ejemplo Complejo: Lista de Películas

Utilicemos Axios para cargar una lista de películas y mostrarlas dinámicamente.

<div id="app-peliculas">
    <h2>Películas en Cartelera</h2>
    <button @click="obtenerPeliculas" class="btn btn-success">Ver Películas</button>
    <div v-if="peliculas.length" class="row">
        <div class="col-md-6" v-for="pelicula in peliculas" :key="pelicula.id">
            <div class="panel panel-default">
                <div class="panel-heading">
                    <h3 class="panel-title">{{ pelicula.titulo }}</h3>
                </div>
                <div class="panel-body">
                    <img :src="pelicula.imagen" :alt="pelicula.titulo" class="img-responsive img-thumbnail" style="max-height: 150px;">
                    <p><strong>Director:</strong> {{ pelicula.director }}</p>
                    <p><strong>Género:</strong> {{ pelicula.genero }}</p>
                    <p><strong>Actores:</strong>
                        <span v-for="(actor, idx) in pelicula.actores" :key="idx">
                            {{ actor.nombre }}{{ idx < pelicula.actores.length - 1 ? ', ' : '' }}
                        </span>
                    </p>
                    <p><strong>Sinopsis:</strong> {{ pelicula.sinopsis }}</p>
                </div>
            </div>
        </div>
    </div>
    <p v-else>No hay películas disponibles.</p>
</div>

<script>
    new Vue({
        el: '#app-peliculas',
        data: {
            peliculas: []
        },
        methods: {
            obtenerPeliculas: function() {
                axios.get('http://localhost:5000/api/peliculas')
                    .then(response => {
                        // La respuesta del backend Flask está en response.data.data.films
                        this.peliculas = response.data.data.films;
                    })
                    .catch(error => {
                        console.error('Error al obtener películas:', error);
                        this.peliculas = [];
                    });
            }
        }
    });
</script>

El Ciclo de Vida de una Instancia Vue

Cada instancia de Vue pasa por una serie de fases durante su existencia, desde su creación hasta su destrucción. Estas fases se conocen como el ciclo de vida, y Vue proporciona "ganchos" (hooks) para ejecutar código en puntos específicos de este ciclo.

Ganchos del Ciclo de Vida

  1. beforeCreate: Se llama justo antes de que la instancia sea inicializada. No hay reactividad de datos ni eventos aún.
  2. created: La instancia ha sido creada. Los datos reactivos y los eventos están disponibles. Aquí es un buen lugar para realizar llamadas AJAX iniciales.
  3. beforeMount: Se llama justo antes de que la instancia se monte en el DOM (renderizado inicial del <template>).
  4. mounted: La instancia ha sido montada en el DOM. Ya puedes acceder al elemento DOM (this.$el). También es un lugar común para llamadas AJAX si necesitas acceder al DOM.
  5. beforeUpdate: Se llama cuando los datos reactivos asociados a la instancia cambian, justo antes de que el DOM se vuelva a renderizar.
  6. updated: Se llama después de que una actualización de datos ha causado el re-renderizado del DOM.
  7. beforeDestroy: Se llama justo antes de que una instancia de Vue sea destruida. Es el lugar ideal para limpiar recursos (timers, eventos no de Vue, etc.).
  8. destroyed: Se llama después de que una instancia de Vue ha sido destruida. Todos los enlaces reactivos y observadores han sido eliminados.

Uso de created para Carga Inicial

Podemos modificar el ejemplo de películas para que se carguen automáticamente cuando la instancia de Vue se crea, sin necesidad de un botón:

<div id="app-peliculas-auto">
    <h2>Películas (Carga Automática con created)</h2>
    <div v-if="peliculas.length" class="row">
        <!-- Mismo contenido que el ejemplo anterior -->
    </div>
    <p v-else>Cargando películas...</p>
</div>

<script>
    new Vue({
        el: '#app-peliculas-auto',
        data: {
            peliculas: []
        },
        methods: {
            obtenerPeliculas: function() {
                axios.get('http://localhost:5000/api/peliculas')
                    .then(response => {
                        this.peliculas = response.data.data.films;
                    })
                    .catch(error => {
                        console.error('Error al obtener películas:', error);
                    });
            }
        },
        created: function() {
            // Se llama a obtenerPeliculas tan pronto como la instancia es creada
            this.obtenerPeliculas();
        }
    });
</script>

Demostración con Componentes e Hilos de Vida

Para observar el ciclo de vida en acción, especialmente beforeDestroy y destroyed, es útil usar un componente que pueda ser creado y eliminado.

<div id="app-lifecycle">
    <h2>Ciclo de Vida de Componentes</h2>
    <button @click="alternarComponente" class="btn btn-info">
        {{ mostrarComponente ? 'Ocultar' : 'Mostrar' }} Contador
    </button>
    <mi-contador v-if="mostrarComponente"></mi-contador>
</div>

<script>
    Vue.component('mi-contador', {
        template: `
            <div style="border: 1px solid #ccc; padding: 10px; margin-top: 10px;">
                <h4>Componente Contador</h4>
                <p>Contador: {{ cuenta }}</p>
                <button @click="cuenta++">Incrementar</button>
            </div>
        `,
        data: function() {
            return {
                cuenta: 0,
                intervalo: null
            };
        },
        beforeCreate: function() { console.log('Contador: beforeCreate'); },
        created: function() {
            console.log('Contador: created. Datos disponibles:', this.cuenta);
            this.intervalo = setInterval(() => {
                console.log('Contador activo:', this.cuenta);
            }, 2000);
        },
        beforeMount: function() { console.log('Contador: beforeMount'); },
        mounted: function() { console.log('Contador: mounted. Elemento DOM:', this.$el); },
        beforeUpdate: function() { console.log('Contador: beforeUpdate. Valor actual:', this.cuenta); },
        updated: function() { console.log('Contador: updated. Nuevo valor:', this.cuenta); },
        beforeDestroy: function() {
            console.log('Contador: beforeDestroy. Limpiando intervalo...');
            clearInterval(this.intervalo); // Crucial para evitar fugas de memoria
        },
        destroyed: function() { console.log('Contador: destroyed.'); }
    });

    new Vue({
        el: '#app-lifecycle',
        data: {
            mostrarComponente: false
        },
        methods: {
            alternarComponente: function() {
                this.mostrarComponente = !this.mostrarComponente;
            }
        }
    });
</script>

Al hacer clic en el botón "Mostrar/Ocultar Contador", podrás ver cómo los ganchos se activan en la consola del navegador. El gancho beforeDestroy es especialmente importante para la limpieza, como detener un setInterval o eliminar escuchadores de eventos, para evitar problemas de rendimiento y fugas de memoria.

Componentes Vue

Los componentes son bloques de código reutilizables y autocontenidos que permiten construir interfaces de usuario complejas a partir de piezas más pequeñas y manejables. Facilitan la organización y la escalabilidad del código.

Tipos de Componentes

  1. Componentes Globales: Se registran globalmente en la aplicación Vue y pueden ser utilizados en cualquier parte de la aplicación, dentro de la instancia principal o de otros componentes.
  2. Componentes Locales: Se registran dentro de las opciones de otro componente (o de la instancia raíz Vue) y solo pueden ser utilizados dentro de ese componente.

Componentes Globales

Se registran usando Vue.component(). Son útiles para componentes que se usan con mucha frecuencia en toda la aplicación.

<div id="app-global-components">
    <h2>Componentes Globales</h2>
    <saludo-global></saludo-global>
    <saludo-global autor="Equipo Vue"></saludo-global>
</div>

<script>
    // Registro global de un componente
    Vue.component('saludo-global', {
        props: {
            autor: {
                type: String,
                default: 'Desconocido'
            }
        },
        template: `
            <div style="padding: 5px; border: 1px dashed blue; margin-bottom: 5px;">
                <p>Hola desde un componente global! Creado por: {{ autor }}</p>
            </div>
        `
    });

    new Vue({
        el: '#app-global-components'
    });
</script>

Se pueden pasar propiedades (props) a los componentes para hacerlos más dinámicos.

Componentes Locales

Los componentes locales se registran dentro de la opción components de otra instancia de Vue. Esto los hace más modulares y evita la contaminación del espacio de nombres global.

<div id="app-local-components">
    <h2>Componentes Locales</h2>
    <!-- Usa el componente local 'mi-boton' -->
    <mi-boton mensaje="Haga clic aquí"></mi-boton>
    <mi-boton mensaje="Enviar Formulario" color="success"></mi-boton>
</div>

<script>
    // Definición de un componente local
    const MiBoton = {
        props: {
            mensaje: String,
            color: {
                type: String,
                default: 'primary' // Color por defecto de Bootstrap
            }
        },
        template: `
            <button :class="'btn btn-' + color" @click="handleClick">
                {{ mensaje }}
            </button>
        `,
        methods: {
            handleClick: function() {
                alert('Botón local clickeado: ' + this.mensaje);
            }
        }
    };

    new Vue({
        el: '#app-local-components',
        components: {
            // Registro del componente local
            'mi-boton': MiBoton
            // Si el nombre de la variable y el nombre del tag son iguales, se puede abreviar a:
            // MiBoton
        }
    });
</script>

En este ejemplo, MiBoton solo puede usarse dentro de la instancia de Vue con el: '#app-local-components'. Si intentaras usar <mi-boton> en otra parte del HTML no controlada por esta instancia o por un componente hijo que lo registre, no funcionaría.

Etiquetas: vue.js Frontend Development JavaScript AJAX axios

Publicado el 10-27 05:52