Agenda
Introducción
Vue - Vuex - firebase - autorización
PWA Vue Webpack Material Design Lite - Parte 1
SPA - vue - firebase - vuex - vuetify
El objetivo va a ser crear una aplicación que sea una agenda que:
- Sign-up
- Sign-in
- Routing
- Guardar información firebase
Estructura
La estructura creada:
-
./src/
: código fuente de la aplicación. -
./src/main.js
: es el punto de entrada a nuestra aplicación. ./src/App.vue
: is a single file component attached to your application../src/assets/
: para static assets./src/components
: contains other single file components./src/router/
: is for routing.
El resto de subdirectorios creados:
- build: contains webpack and vue-loader configuration files
- config: contains our app config (environments, parameters…)
- static: images, css and other public assets
- test: unit test files propelled by Karma & Mocha
PWA
El fichero static/manifest.json es el que permite que instalemos la aplicación.
y finalmente:
ngrok http 8080
Así podremos testear tanto en el navegador como directamente en un móvil o tablet.
Firebase
- Firebase — for authentication, real-time database and hosting
UI
Página por defecto
Página principal
Con vuetify, tenemos <template>
y dentro v-app
.
Toolbar
En el ejemplo viene:
<v-toolbar app> <v-toolbar-title class="headline text-uppercase"> <span>Vuetify</span> <span class="font-weight-light">MATERIAL DESIGN</span> </v-toolbar-title> <v-spacer></v-spacer> <v-btn flat href="https://github.com/vuetifyjs/vuetify/releases/latest" target="_blank" > <span class="mr-2">Latest Release</span> </v-btn> </v-toolbar>
Información sobre toolbars. Vemos que estamos incluyendo:
- v-toolbar-title: título.
- v-spacer: hacemos espacio entre ambos componentes.
- v-btn: botón.
El botón se ha hecho con:
<v-btn flat href="https://github.com/vuetifyjs/vuetify/releases/latest" target="_blank" > <span class="mr-2">Latest Release</span> </v-btn>
Contenido
En el contenido se referencia otro componente:
<v-content> <HelloWorld/> </v-content>
Ese componente se carga posteriormente en el <script>
:
import HelloWorld from './components/HelloWorld'
Nuevo UI
Crear vistas
Lo primero es crear un pequeño "borrador" de las vistas que vamos a necesitar.
La idea es tener una página principal que tiene cierto layaut:
- toolbar
- contenido
- sidebar
En el sidebar daremos la opción de sign-up y sign-in en firebase:
- sign-up: no requiere autentificación
- sign-in: no requiere autentificación
- hola: requiere autentificación
Página principal
El layout consistirá en:
- v-toolbar: será la barra superior.
- v-navigation-drawer: es un sidebar. Se ocultará o mostrará pulsando un botón.
- v-content: aquí meteremos las diferentes vistas.
La sección <script>
exporta los valores por defecto (la inicialización). Básicamente pone el título de la toolbar y pone la sidebar en falso.
Vemos que al hacer click en
<v-toolbar-side-icon>
el valor de sidebar le asigna el opuesto de lo que tuviera asignado.
El código será:
<template> <v-app> <v-navigation-drawer v-model="sidebar" app> </v-navigation-drawer> <v-toolbar app> <span class="hidden-sm-and-up"> <v-toolbar-side-icon @click="sidebar = !sidebar"> </v-toolbar-side-icon> </span> <v-toolbar-title>{{ appTitle }}</v-toolbar-title> <v-spacer></v-spacer> </v-toolbar> <v-content> </v-content> </v-app> </template> <script> export default { data () { return { appTitle: 'Awesome App', sidebar: false } } } </script>
Dentro de src/components/:
<template> <ul class="list"> </ul> </template> <script> export default { } </script> <style scoped> .list { width: 100%; padding: 0; } </style>
<template> <div class="card-image"> </div> </template> <script> export default { } </script> <style scoped> </style>
<template> <div class="waiting"> Not yet available </div> </template> <script> export default { } </script> <style scoped> .waiting { padding: 10px; color: #555; } </style>
Hay que borrar el fichero Hello.vue que ya no es necesario.
View: Sign up
En src/components:
Rutado de las vistas
import Vue from 'vue' import Router from 'vue-router' import HomeView from '@/components/HomeView' import DetailView from '@/components/DetailView' import PostView from '@/components/PostView' Vue.use(Router) export default new Router({ routes: [ { path: '/', name: 'home', component: HomeView }, { path: '/detail/:id', name: 'detail', component: DetailView }, { path: '/post', name: 'post', component: PostView } ] })
Material Design Lite
Instalación
npm install material-design-lite --save
```vue tag="src/App.vue"
Y se actualiza a: ```vue tab="src/App.vue" <template> <div class="mdl-layout mdl-js-layout mdl-layout--fixed-header"> <header class="mdl-layout__header"> <div class="mdl-layout__header-row"> <span class="mdl-layout-title">CropChat</span> </div> </header> <div class="mdl-layout__drawer"> <span class="mdl-layout-title">CropChat</span> <nav class="mdl-navigation"> <router-link class="mdl-navigation__link" to="/" @click.native="hideMenu">Home</router-link> <router-link class="mdl-navigation__link" to="/post" @click.native="hideMenu">Post a picture</router-link> </nav> </div> <main class="mdl-layout__content"> <div class="page-content"> <router-view></router-view> </div> </main> </div> </template>
Routing
Introducción
El objetivo es presentar.
Contenido por defecto
Es:
import Vue from 'vue' import Router from 'vue-router' import Home from './views/Home.vue' Vue.use(Router) export default new Router({ mode: 'history', base: process.env.BASE_URL, routes: [ { path: '/', name: 'home', component: Home }, { path: '/about', name: 'about', // route level code-splitting // this generates a separate chunk (about.[hash].js) for this route // which is lazy-loaded when the route is visited. component: () => import(/* webpackChunkName: "about" */ './views/About.vue') } ] })
Nuevo contenido
Será:
import Vue from 'vue' import Router from 'vue-router' import Signin from './components/Signin' import Signup from './components/Signup' import Home from './components/Home' import Landing from './components/Landing' Vue.use(Router) export default new Router({ mode: 'history', base: process.env.BASE_URL, routes: [ { path: '/', name: 'landing', component: Landing }, { path: '/signin', name: 'signin', component: Signin }, { path: '/signup', name: 'signup', component: Signup }, { path: '/home', name: 'home', component: Home }, { path: '/about', name: 'about', // route level code-splitting // this generates a separate chunk (about.[hash].js) for this route // which is lazy-loaded when the route is visited. component: () => import(/* webpackChunkName: "about" */ './views/About.vue') } ] })
Podemos probar que funciona con:
- http://localhost:8080/: landing
- http://localhost:8080/signin: signin
- http://localhost:8080/signup: signup
- http://localhost:8080/home: home (una vez autentificado)
Añadimos un menú
Así evitamos tener que usar las rutas anteriores.
Asociamos el título a la página de landing:
<v-toolbar-title class="headline text-uppercase"> <router-link to="/" tag="span" style="cursor: pointer"> <span>Agenda</span> <span class="font-weight-light">Prueba</span> </router-link> </v-toolbar-title>
Añadimos la lista de menuItems en App.vue.
Estado - Layout + Routing + Vistas
La app más el routing:
import Vue from 'vue' import './plugins/vuetify' import App from './App.vue' import router from './router' import store from './store' Vue.config.productionTip = false new Vue({ router, store, render: h => h(App) }).$mount('#app')
import Vue from 'vue' import Router from 'vue-router' import Signin from './components/Signin' import Signup from './components/Signup' import Home from './components/Home' import Landing from './components/Landing' Vue.use(Router) export default new Router({ mode: 'history', base: process.env.BASE_URL, routes: [ { path: '/', name: 'landing', component: Landing }, { path: '/signin', name: 'signin', component: Signin }, { path: '/signup', name: 'signup', component: Signup }, { path: '/home', name: 'home', component: Home }, { path: '/about', name: 'about', // route level code-splitting // this generates a separate chunk (about.[hash].js) for this route // which is lazy-loaded when the route is visited. component: () => import(/* webpackChunkName: "about" */ './views/About.vue') } ] })
<template> <v-app> <!-- SIDEBAR --> <!--span class="hidden-sm-and-up"--> <v-navigation-drawer v-model="sidebar" app> <v-toolbar flat> <v-list> <v-list-tile> <v-list-tile-title class="title"> Menu </v-list-tile-title> </v-list-tile> </v-list> </v-toolbar> <v-divider></v-divider> <v-list> <v-list-tile v-for="item in menuItems" :key="item.title" :to="item.path"> <v-list-tile-action> <v-icon>{{ item.icon }}</v-icon> </v-list-tile-action> <v-list-tile-content>{{ item.title }}</v-list-tile-content> </v-list-tile> </v-list> </v-navigation-drawer> <!--/span--> <!-- TOOLBAR --> <v-toolbar app> <v-icon @click="sidebar = !sidebar">menu</v-icon> <v-toolbar-title class="headline text-uppercase"> <router-link to="/" tag="span" style="cursor: pointer"> <span>Agenda</span> <span class="font-weight-light">Prueba</span> </router-link> </v-toolbar-title> </v-toolbar> <v-content> <router-view></router-view> </v-content> </v-app> </template> <script> export default { name: 'App', components: { }, data () { return { // sidebar: false, menuItems: [ { title: 'Home', path: '/home', icon: 'home' }, { title: 'Sign Up', path: '/signup', icon: 'face' }, { title: 'Sign In', path: '/signin', icon: 'lock_open' } ] } } } </script>
Y las cuatro vistas definidas en los componentes:
<template> <v-container fluid> <v-layout column> <v-flex xs12 class="text-xs-center" mt-5> <h1>Landing page</h1> </v-flex> </v-layout> </v-container> </template> <script> export default {} </script>
<template> <v-container fluid> <v-layout row wrap> <v-flex xs12 class="text-xs-center" mt-5> <h1>Sign Up</h1> </v-flex> <v-flex xs12 sm6 offset-sm3 mt-3> <form> <v-layout column> <v-flex> <v-text-field name="email" label="Email" id="email" type="email" required></v-text-field> </v-flex> <v-flex> <v-text-field name="password" label="Password" id="password" type="password" required></v-text-field> </v-flex> <v-flex> <v-text-field name="confirmPassword" label="Confirm Password" id="confirmPassword" type="password" required ></v-text-field> </v-flex> <v-flex class="text-xs-center" mt-5> <v-btn color="primary" type="submit">Sign Up</v-btn> </v-flex> </v-layout> </form> </v-flex> </v-layout> </v-container> </template> <script> export default {} </script>
<template> <v-container fluid> <v-layout row wrap> <v-flex xs12 class="text-xs-center" mt-5> <h1>Sign In</h1> </v-flex> <v-flex xs12 sm6 offset-sm3 mt-3> <form> <v-layout column> <v-flex> <v-text-field name="email" label="Email" id="email" type="email" required></v-text-field> </v-flex> <v-flex> <v-text-field name="password" label="Password" id="password" type="password" required></v-text-field> </v-flex> <v-flex class="text-xs-center" mt-5> <v-btn color="primary" type="submit">Sign In</v-btn> </v-flex> </v-layout> </form> </v-flex> </v-layout> </v-container> </template> <script> export default {} </script>
<template> <v-container fluid> <v-layout row wrap> <v-flex xs12 class="text-xs-center" mt-5> <h1>Home page</h1> </v-flex> <v-flex xs12 class="text-xs-center" mt-3> <p>This is a user's home page</p> </v-flex> </v-layout> </v-container> </template> <script> export default {} </script>
Vuex
En el fichero ./src/store.js
tenemos el código asociado a Vuex.
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export default new Vuex.Store({ state: { }, mutations: { }, actions: { } })
- state is an object with application data.
- mutations are needed to change that state.
- actions are needed to dispatch mutations.
- And getters are needed to get the store.
Firebase
Instalación
Inslamos firebase-tools:
yaourt -S firebase-tools
Otra opción:
npm i -g firebase-tools
Hacemos:
npm install --save firebase
y añadimos en src/main.js
lo siguiente:
import firebase from 'firebase' firebase.initializeApp({ apiKey: 'YOUR_API_KEY', authDomain: 'YOUR_AUTH_DOMAIN', databaseURL: 'YOUR_DATABASE_URL', projectId: 'YOUR_PROJECT_ID' })
Si falla la instalación
https://github.com/grpc/grpc/issues/15288
Precompilados:
https://storage.googleapis.com/grpc-precompiled-binaries/node/grpc/v1.10.1/node-v64-linux-x64-glibc.tar.gz
Workaround
env CXXFLAGS="-Wno-ignored-qualifiers -Wno-stringop-truncation -Wno-cast-function-type" npm install grpc@1.11.3
Consola Firebase
Vamos a Firebase. Arriba a la derecha vamos a Ir a la consola.
Creamos un proyecto y accedemos a él.
- Autentificación > Método de acceso: habilitamos el método email/password.
- Autentificación: arriba a la derecha Configuración Web y copiamos los detalles.
Vuex - Funcionalidad de autorización
Note: I highly recommend to install extension for Chrome/Firefox called Vue.js devtools. This extension will help you in debugging your Vue.js application and to see what is happening in Vuex store.
Estado
El estado lo asignamos:
state: { user: null, error: null, loading: false }
mutations: { setUser (state, payload) { state.user = payload }, setError (state, payload) { state.error = payload }, setLoading (state, payload) { state.loading = payload } }
en donde:
- user: contiene información de usuario. Por defecto
null
- error: almacena mensajes de error. Por defecto
null
- loading: indica si la aplicación está cargando información. Por defecto
false
Las mutaciones sirven para cambiar el estado.
Vistas con datos
Para que la vista guarde su información en variables, le asignamos un v-model
.
<template> <v-container fluid> <v-layout row wrap> <v-flex xs12 class="text-xs-center" mt-5> <h1>Sign Up</h1> </v-flex> <v-flex xs12 sm6 offset-sm3 mt-3> <form> <v-layout column> <v-flex> <v-text-field name="email" label="Email" id="email" type="email" v-model="email" required></v-text-field> </v-flex> <v-flex> <v-text-field name="password" label="Password" id="password" type="password" v-model="password" required></v-text-field> </v-flex> <v-flex> <v-text-field name="confirmPassword" label="Confirm Password" id="confirmPassword" type="password" v-model="passwordConfirm" required ></v-text-field> </v-flex> <v-flex class="text-xs-center" mt-5> <v-btn color="primary" type="submit">Sign Up</v-btn> </v-flex> </v-layout> </form> </v-flex> </v-layout> </v-container> </template> <script> export default { data () { return { email: '', password: '', passwordConfirm: '' } } } </script>
Chequeo de las claves
Prueba:
computed: { comparePasswords () { return this.password === this.passwordConfirm ? true : 'Passwords do not match' } }
Submitting
Modificamos form
Ello implica que modifiquemos store.js
: