Skip to content

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

Me gustaría algo así: dynalist gingkoapp

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

ejemplo

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.

  1. Autentificación > Método de acceso: habilitamos el método email/password.
  2. 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:

Enlaces interesantes

Cómo escalar a aplicaciones grandes