SPA con Laravel y VueJS

Una SPA (Single Page Application) es un tipo de aplicación web dónde el navegador no necesita refrescar la página mientra navegas por ella. En este artículo vamos crear una SPA con Laravel y VueJS, para brindar una buena experiencia de usuario en nuestras webs y aplicaciones.

Instalando VueJS en Laravel

Para comenzar, instalaremos VueJS en nuestro proyecto Laravel. En este artículo no vamos a profundizar sobre el funcionamiento de Vuejs en Laravel, pero si te interesa conocerlo más a fondo, te recomendamos nuestros articulos anteriores: Instalar Vue.js en Laravel Crud vuejs en laravel y nuestra categoría de VueJS.

Instalaremos VueJS en nuestro proyecto Laravel mediante el siguiente comando:

npm install

Una vez ejecutado el comando que instalará todas las dependencias, ejecutaremos otro comando que será el que refresque nuestros archivos JS cada vez que hagamos un cambio, éste lo mantendremos siempre en ejecución:

npm run watch-poll

Creando nuestras vistas

Como ya sabemos, una SPA es una página que en una sola muestra todo el contenido de nuestra web, simulando una navegación sin que recargue la página. Para conseguir éste mecanismo, tendremos dos vistas, una será welcome.blade.php que será dónde añadiremos nuestra plantilla y se irá refrecando nuestra web. También tendremos otra vista llamada content.blade.php, que extenderá de welcome y será la que irá controlando qué contenido mostrar y cual no.

Comenzamos con welcome.blade.php, dónde añadiremos un pequeño tema de landing page que hemos sacado de la web w3schools.com.

<!DOCTYPE html>
<html lang="es">
<title>W3.CSS Template</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Montserrat">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<!-- Añadimos la libreria animate -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.7.0/animate.min.css">

<link href="{{asset('css/app.css')}}" rel="stylesheet">
<style>
body,h1,h2,h3,h4,h5,h6 {font-family: "Lato", sans-serif}
.w3-bar,h1,button {font-family: "Montserrat", sans-serif}
.fa-anchor,.fa-coffee {font-size:200px}
.header-page{
    background-color: #F44336;
    padding-bottom: 4rem;
    padding-top: 4rem;
    margin-bottom: 4rem;
    color: white;
    font-weight: bold;
}
.btn-theme{
    background-color:#F44336; 
    color:white;
}
.img{
    width:100%;
}
.img-about{
    width:700px;
    text-align: center;
}
</style>
<body>
<div id="app" >
    <!-- Navbar -->
    <div class="w3-top">
    <div class="w3-bar w3-red w3-card w3-left-align w3-large">
        <a class="w3-bar-item w3-button w3-hide-medium w3-hide-large w3-right w3-padding-large w3-hover-white w3-large w3-red" href="javascript:void(0);" onclick="myFunction()" title="Toggle Navigation Menu"><i class="fa fa-bars"></i></a>
        <a href="#"  @click="menu=0" class="w3-bar-item w3-button w3-padding-large w3-hover-white" v-bind:class="{ 'w3-white': menu==0}">Landing page</a>
        <a href="#" @click="menu=1" class="w3-bar-item w3-button w3-hide-small w3-padding-large w3-hover-white" v-bind:class="{ 'w3-white': menu==1}">Sobre nosotros</a>
        <a href="#" @click="menu=2" class="w3-bar-item w3-button w3-hide-small w3-padding-large w3-hover-white" v-bind:class="{ 'w3-white': menu==2}">Contacto</a>
    </div>

    <!-- Navbar on small screens -->
    <div id="navDemo" class="w3-bar-block w3-white w3-hide w3-hide-large w3-hide-medium w3-large">
        <a href="#" @click="menu=0" class="w3-bar-item w3-button w3-padding-large">Landing page</a>
        <a href="#" @click="menu=1" class="w3-bar-item w3-button w3-padding-large">Sobre nosotros</a>
        <a href="#" @click="menu=2" class="w3-bar-item w3-button w3-padding-large">Contacto</a>

    </div>
    </div>
    <!--Contenido dinamico que irá cambiando sin refrescar la pagina-->
    <div>
        @yield('content')
    </div>
    <!-- Footer -->
    <footer class="w3-container w3-padding-64 w3-center w3-opacity">  
    <div class="w3-xlarge w3-padding-32">
        <i class="fa fa-facebook-official w3-hover-opacity"></i>
        <i class="fa fa-instagram w3-hover-opacity"></i>
        <i class="fa fa-snapchat w3-hover-opacity"></i>
        <i class="fa fa-pinterest-p w3-hover-opacity"></i>
        <i class="fa fa-twitter w3-hover-opacity"></i>
        <i class="fa fa-linkedin w3-hover-opacity"></i>
    </div>
    <p>Powered by <a href="https://www.w3schools.com/w3css/default.asp" target="_blank">w3.css</a></p>
    </footer>
</div>
<script src="js/app.js"></script>
<script>
// Used to toggle the menu on small screens when clicking on the menu button
function myFunction() {
  var x = document.getElementById("navDemo");
  if (x.className.indexOf("w3-show") == -1) {
    x.className += " w3-show";
  } else { 
    x.className = x.className.replace(" w3-show", "");
  }
}
</script>

Lo más destacado de está vista, es lo siguiente, de arriba a abajo:

  • Como podemos leer en el comentario, antes de los estilos, hemos añadido la librería animatecss, que utilizaremos más adelante para dar un efecto de «fade in» al contenido de nuestra web.
  • Hay un div con el id app, que es el que tiene nuestro archivo app.js, que indica dónde utilizaremos Vuejs y sus componentes.
  • Si seguimos hacia abajo, podemos fijarnos que tenemos tanto en la navbar como en el menú movil, las directivas @click de VueJs, que lo que hará será cambiar el valor de la variable menú de Vuejs, que crearemos más adelante. Utilizaremos esta variable para mostrar un contenido u otro, en la siguiente vista y además la directiva -bind:class para mostrar la opción seleccionada del navbar.
  • Vemos que tenemos un yield con el nombre content, que será el que cargue la section de nuestro archivo content.blade.php
  • Y por último, añadimos el archivo javascript app.js, para añadir Vuejs a nuestra plantilla.

Ahora crearemos la vista content.blade.php:

@extends('welcome')

@section('content')
<template v-if="menu==0">
   <landing-page></landing-page>
</template>
<template v-if="menu==1">
   <contact-page></contact-page>
</template>
<template v-if="menu==2">
    <about-page></about-page>
</template>

@endsection

Como vemos, en esta vista que extiende de welcome.blade.php , es donde estará la lógica de navegación de nuesta SPA mediante las condicionales de VueJS, para controlar mediante la variable menú, que componente VueJS mostrar.

Ahora, lo que haremos, será dirigirnos a nuestro archivo web.php para indicar a nuestra aplicación que la ruta raiz sea content:

Route::get('/', function () {
    return view('content');
});

Creando nuestros componentes VueJS

Ahora que tenemos creadas nuestras plantillas blade, vamos a crear los tres componentes VueJS a los que hemos hecho referencia en la vista content, en la carpeta resource/js/components.

Si quieres descargate las imagenes utilizadas en los componentes: Descargar imágenes en este enlace 

Landing.vue

Esta es la página que veremos cuando la variable menú este en 0, es decir, la landig page (página de aterrizaje).

<template>
<!-- Header -->
<div class="animated fadeIn">
<header class="w3-container w3-red w3-center" style="padding:128px 16px">
  <h1 class="w3-margin w3-jumbo">START PAGE</h1>
  <p class="w3-xlarge">Template by w3.css</p>
  <button class="w3-button w3-black w3-padding-large w3-large w3-margin-top">Get Started</button>
</header>

<!-- First Grid -->
<div class="w3-row-padding w3-padding-64 w3-container">
  <div class="w3-content">
    <div class="w3-twothird">
      <h1>Lorem Ipsum</h1>
      <h5 class="w3-padding-32">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</h5>

      <p class="w3-text-grey">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Excepteur sint
        occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
        laboris nisi ut aliquip ex ea commodo consequat.</p>
    </div>

    <div class="w3-third w3-center">
      <i class="fa fa-anchor w3-padding-64 w3-text-red"></i>
    </div>
  </div>
</div>

<!-- Second Grid -->
<div class="w3-row-padding w3-light-grey w3-padding-64 w3-container">
  <div class="w3-content">
    <div class="w3-third w3-center">
      <i class="fa fa-coffee w3-padding-64 w3-text-red w3-margin-right"></i>
    </div>

    <div class="w3-twothird">
      <h1>Lorem Ipsum</h1>
      <h5 class="w3-padding-32">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</h5>

      <p class="w3-text-grey">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Excepteur sint
        occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
        laboris nisi ut aliquip ex ea commodo consequat.</p>
    </div>
  </div>
</div>

<div class="w3-container w3-black w3-center w3-opacity w3-padding-64">
    <h1 class="w3-margin w3-xlarge">Quote of the day: live life</h1>
</div>
</div>
</template>

Contact.vue

En contact.vue simularemos un formularía típico de una página web, cuando el menú esté en 1.

<template>
    <div class="w3-row-padding w3-padding-64 w3-container animated fadeIn">
        <div class="container-fluid">
            <div class="row">
                <div class="col-md-12">
                    <h1 class="text-center header-page">Contacto</h1>
                </div>

                <div class="col-md-6">
                    <form>
                        <div class="form-group">
                            <label for="exampleInputEmail1">Email</label>
                            <input type="email" class="form-control" id="exampleInputEmail1" aria-describedby="emailHelp" placeholder="Email">
                        </div>
                        <div class="form-group">
                            <label for="exampleInputPassword1">Nombre</label>
                            <input type="name" class="form-control" id="name" placeholder="Nombre">
                        </div>
                        <div class="form-group">
                            <label for="exampleFormControlTextarea1">Mensaje</label>
                            <textarea class="form-control" id="exampleFormControlTextarea1" rows="3"></textarea>
                        </div>
                        <button type="submit" class="btn btn-theme">Enviar</button>
                    </form>
                </div>
                    <div class="col-md-6">
                    <img class="img" src="img/contact.jpg" alt="contactus">
                </div>
            </div>
        </div>
    </div>
</template>

About.vue

En este componente, añadiremos una foto de un grupo de trabajadores y un texto de lorem ipsum para rellenar su contenido, cuando el menú esté en 2.

<template>
    <div class="w3-row-padding w3-padding-64 w3-container animated fadeIn">
        <div class="container-fluid">
            <div class="row">
                <div class="col-md-12 text-center">
                    <h1 class="text-center header-page">Sobre nosotros</h1>
                    <h5>
                        <b>Lorem Ipsum is simply dummy text of the printing and typesetting industry.</b> 
                        Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when 
                        an unknown printer took a galley of type and scrambled it to make a type specimen book. 
                        It has survived not only five centuries, but also the leap into electronic typesetting, remaining 
                        essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages,
                        and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
                    </h5>
                    <img class="img-about" src="img/about.jpg" alt="about">
                </div>
            </div>
        </div>
    </div>
</template>

Si nos fijamos, los tres compontes cuentan con las clases animated fadeIn que les dará un efecto cuando se muestre su contenido, gracías a animate.css, que añadimos en la vista welcome.blade.php anteriormente.

Por último, en nuestro archivo app.js (resources/js), que como sabras, si has trabajado ya con vuejs en laravel o has leido los anteriores artículos relacionados, será donde añadiremos nuestros componentes y crearemos la variable menú:

require('./bootstrap');

window.Vue = require('vue');

Vue.component('landing-page', require('./components/Landing.vue').default);
Vue.component('contact-page', require('./components/Contact.vue').default);
Vue.component('about-page', require('./components/About.vue').default);


const app = new Vue({
    el: '#app',
    data:{
        menu:0,
    },
    methods: {
        changePage(menu){
            this.menu = menu;
        }
    },
});

¡De esta forma, ya tendríamos creada y lista nuestra SPA! Recuerda tener el comando npm run watch-poll o npm run dev ejecutandose para que se vayan refrescando los archivos js que vamos creando.