© Carlos Rojas 2020
C. RojasBuilding Progressive Web Applications with Vue.js https://doi.org/10.1007/978-1-4842-5334-2_5

5. Working with Vue.js

Carlos Rojas1 
(1)
Medellin, Colombia
 

Vue.js is an open source, progressive JS framework for building UIs that aim to be incrementally adoptable. The core library of Vue.js is focused on the view layer only, and is easy to pick up and integrate with other libraries or existing projects.

We’ll see how to take advantage of its features to build fast, high-performance PWAs that work offline. We’ll also build our notes app and examine key concepts for understanding the components of Vue.js.

What Are the Major Features of Vue.js?

Vue.js has all the features that a framework to build SPAs should have, but some stand out from all the others:
  • Virtual DOM: Virtual DOM is a lightweight in-memory tree representation of the original HTML DOM and is updated without affecting the original DOM.

  • Components: Components are used to create reusable custom elements in Vue.js applications.

  • Templates: Vue.js provides HTML-based templates that bind the DOM with the Vue instance data.

  • Routing: Navigation between pages is achieved through vue-router.

  • Lightweight: Vue.js is a lightweight library compared to other frameworks.

What Are Components?

Components are reusable elements for which we define their names and behavior. For an overview of the concept of components, look at Figure 5-1.
../images/483082_1_En_5_Chapter/483082_1_En_5_Fig1_HTML.jpg
Figure 5-1

Components in a web application

You can see in Figure 5-1 that we have six components at different levels. Component 1 is the parent of components 2 and 3, and the grandparent of components 4, 5, and 6. We can make a hierarchical tree with this relationship (Figure 5-2).
../images/483082_1_En_5_Chapter/483082_1_En_5_Fig2_HTML.jpg
Figure 5-2

Hierarchy of components

Each component can be whatever we want it to be: a list, an image, a button, text, or whatever we define.

The fundamental way to define a simple component is
Vue.component('my-button', {
  data: function () {
    return {
      counter: 0
    }
  },
  template: '<button v-on:click="counter++">Clicks {{ counter }}.</button>'
})
We can add it to our app as a new HTML tag:
<div id="app">
  <my-button></my-button>
</div>

What Are the Life Cycle Hooks in a Component?

The process of creating and destroying components is called a life cycle. There are some methods used to run functions at specific moments. Consider the following component:
<script>
export default {
  name: 'HelloWorld',
  created: function () {
    console.log('Component created.');
  },
  mounted: function() {
    fetch('https://randomuser.me/api/?results=1')
    .then(
      (response) => {
        return response.json();
      }
    )
    .then(
      (reponseObject) => {
        this.email = reponseObject.results[0].email;
      }
    );
    console.log('Component is mounted.');
  },
  props: {
    msg: String,
    email:String
  }
}
</script>

Here, we added an e-mail property; we used created() and mounted(). These methods are called life cycle hooks, and we use them to do actions at specific moments in our component. For example, we make a call to an API when our component is mounted and get the e-mail at that moment.

You can go there from the repo (https://github.com/carlosrojaso/appress-book-pwa) with
$git checkout lifecycles-chapter
Life cycle hooks are an essential part of any serious component (Figure 5-3).
../images/483082_1_En_5_Chapter/483082_1_En_5_Fig3_HTML.jpg
Figure 5-3

Component life cycle hooks

beforeCreate

The beforeCreate hook runs at the initialization of a component.
new Vue({
  beforeCreate: function () {
    console.log('Initialization is happening');
})

created

The created hook runs the component when it is initialized. You can then access reactive data, and events are active.
new Vue({
  created: function () {
    console.log('The Component is created');
})

beforeMount

The beforeMount hook runs right before the initial render happens and after the template or render functions have been compiled.
new Vue({
  beforeMount: function () {
    console.log('The component is going to be Mounted');
  }
})

mounted

With the mounted hook, you have full access to the reactive component, templates, and rendered DOM.
new Vue({
  mounted: function () {
    console.log('The component is mounted');
  }
})

beforeUpdate

The beforeUpdate hook runs after data changes in the component and the update cycle begins, right before the DOM is patched and rerendered.
new Vue({
  beforeUpdate: function () {
    console.log('The component is going to be updated');
  }
})

updated

The updated hook runs after data changes in the component and the DOM rerenders.
new Vue({
  updated: function () {
    console.log('The component is updated');
  }
})

beforeDestroy

The beforeDestroy hooks happens right before the component is destroyed. Your component is still fully present and functional.
new Vue({
  beforeDestroy: function () {
    console.log('The component is going to be destroyed');
  }
})

destroyed

The destroyed hook happens when everything attached to it has been destroyed. You might use the destroyed hook to do any last-minute cleanup.
new Vue({
  destroyed: function () {
    console.log('The component is destroyed');
  }
})

Communicating between Components

Components usually need to share information between them. For basic scenarios (Figure 5-4), we can use the props or ref attributes if we want to pass data to child components, emitters if we want to pass data to a parent component, and two-way data binding to have data sync between child and parents.
../images/483082_1_En_5_Chapter/483082_1_En_5_Fig4_HTML.jpg
Figure 5-4

A parent component communicating with a child component

What Are Props?

Props are custom attributes you can register on a component. When a value is passed to a prop attribute, it becomes a property on that component instance. The basic structure is as follows:
Vue.component('some-item', {
  props: ['somevalue'],
  template: '<div>{{ somevalue }}</div>'
})
Now you can pass values like this:
<some-item somevalue="value for prop"></some-item>

What Is a ref attribute?

ref is used to register a reference to an element or a child component. The reference is registered under the parent component’s $refs object. For more information, go to https://vuejs.org/v2/api/#ref. The basic structure is
<input type="text" ref="email">
<script>
    const input = this.$refs.email;
</script>

Emitting an Event

If you want a child to communicate with a parent, use $emit(), which is the recommended method to pass information or a call (Figure 5-5). A prop function is another way to pass information, but it’s considered a bad practice and I do not discuss it.
../images/483082_1_En_5_Chapter/483082_1_En_5_Fig5_HTML.jpg
Figure 5-5

A child component communicating with a parent component

In Vue, we have the method $emit(), which we use to send data to parent components. The basic structure for emitting an event is
Vue.component('child-custom-component', {
  data: function () {
    return {
      customValue: 0
    }
  },
  methods: {
    giveValue: function () {
      this.$emit('give-value', this.customValue++)
    }
  },
  template: `
    <button v-on:click="giveValue">
      Click me for value
    </button>
  `
})

Using Two-way Data Binding

An easy way to maintain communication between components is to use two-way data binding. In this scenario, Vue.js makes communication between components for us (Figure 5-6).
../images/483082_1_En_5_Chapter/483082_1_En_5_Fig6_HTML.jpg
Figure 5-6

Components communicating both ways

Two-way data binding means that Vue.js syncs data properties and the DOM for you. Changes to a data property update the DOM, and changes made to the DOM update the data property; data move both ways. The basic structure to use two-way data binding is
new Vue({
      el: '#app',
      data: {
      somevalue: 'I am a two-way data value.'
      }
});
<input type="text" v-model="somevalue" value="A value">

What Is Vue Router?

Vue Router is an official routing plug-in for SPAs designed for use with the Vue.js framework. A router is a way to jump from one view to another in an SPA. Some of its features include the following:
  • Nested route/view mapping

  • Modular, component-based router configuration

  • Route parameters, query, wildcards

  • Fine-grained navigation control

  • HTML5 history mode or hash mode, with autofallback in IE9

It is easy to integrate Vue Router in our Vue application.
  1. 1.

    Install the plug-in.

     
$npm install vue-router
  1. 2.

    Add VueRouter in main.js.

     
...
import VueRouter from 'vue-router'
...
Vue.use(VueRouter);
  1. 3.

    Create routes in a new routes.js file.

     
import HelloWorld from './components/HelloWorld.vue'
export const routes = [
  {path: ", component: HelloWorld}
];
  1. 4.

    Create a VueRouter instance.

     
const router = new VueRouter({routes});
  1. 5.

    Add VueRouter to Vue instance.

     
const app = new Vue({
  router
}).$mount('#app')

Building VueNoteApp

In Chapter 1, we started to develop VueNoteApp (Figure 5-7). In this section, we add the features we need in a basic note app using Vue.js.
../images/483082_1_En_5_Chapter/483082_1_En_5_Fig7_HTML.jpg
Figure 5-7

VueNoteApp design

Creating Notes

First of all, we need to create a button that allows us to add new notes and a view that shows us general information. We also need to show these notes in the main view. To do this, we need two extra components: Notes.vue and About.vue. The structure we need to create is

App.vue
components/Notes.vue
components/About.vue

Next we need to modify Apps.vue and tell it we need to use the Notes component here.

App.vue
<template>
  <v-app>
    <v-toolbar app>
        <v-icon>arrow_back</v-icon>
      <v-toolbar-title >
        <span>VueNoteApp</span>
      </v-toolbar-title>
      <v-spacer></v-spacer>
    </v-toolbar>
    <v-content>
      <img alt="Vue logo" src="./assets/logo.png">
      <Notes msg="Notes"/>
    </v-content>
  </v-app>
</template>
<script>
import Notes from './components/Notes.vue'
export default {
  name: 'app',
  components: {
    Notes
  }
}
</script>
<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

Then we must create a new components/Notes.vue file and write the necessary things for a component as you can see in the following code.

Notes.vue
<template>
  <div class="hello">
    <h1>{{ msg }}</h1>
  </div>
</template>
<script>
export default {
  name: 'Notes',
  props: {
    msg: String
  }
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
  margin: 40px 0 0;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
</style>

Now we are going to use an array (called pages) and save the notes there. In addition, We going to use v-for in our template to iterate in this array and add the elements in the DOM. In the Notes component, we need to emit an Add button to allow users to add a new note.

Notes.vue
<template>
  <div class="notes">
  <ul>
    <li v-for="(page, index) of pages" class="page">
      <div>{{page.title}} tit</div>
      <div>{{page.content}} cont</div>
     </li>
    <li class="new-note">
      <v-btn fab dark color="indigo" @click="newNote()">
        <v-icon dark>add</v-icon>
      </v-btn>
    </li>
  </ul>
  </div>
</template>
<script>
export default {
  name: 'Notes',
  props: ['pages'],
  methods: {
    newNote () {
    this.$emit('new-note')
    }
  }
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
  margin: 40px 0 0;
}
ul {
  list-style-type: none;
  padding: 0;
}
li {
  display: inline-block;
  margin: 0 10px;
}
a {
  color: #42b983;
}
</style>

Now we need to modify the App.vue component to send the pages array to the Notes component. Then we create a newNote function to add a new element to this array.

App.vue
<template>
  <v-app>
    <v-toolbar app>
      <v-toolbar-title >
        <span>VueNoteApp</span>
      </v-toolbar-title>
      <v-spacer></v-spacer>
      <v-toolbar-items>
        <v-btn flat>About</v-btn>
      </v-toolbar-items>
    </v-toolbar>
    <v-content>
      <Notes :pages="pages" @new-note="newNote"/>
    </v-content>
  </v-app>
</template>
<script>
import Notes from './components/Notes.vue'
export default {
  name: 'app',
  components: {
    Notes
  },
  data: () => ({
    pages:[],
    index:0
  }),
  methods:  {
    newNote () {
      this.pages.push({
        title: ",
        content: "
      });
      console.log(this.pages);
    },
    saveNote () {
    },
    deleteNote () {
    }
  }
}
</script>
<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
At this point, use $npm run serve and make sure you see something like what is shown in Figure 5-8.
../images/483082_1_En_5_Chapter/483082_1_En_5_Fig8_HTML.jpg
Figure 5-8

VueNoteApp creating notes

You can go there from the repo (https://github.com/carlosrojaso/appress-book-pwa) with
$git checkout v1.1.0

Next we are going to polish the styles in our app using Vuetify components and CSS.

Notes.vue
<template>
  <div class="notes">
    <v-card v-for="(page, index) in pages" :key="index">
      <v-card-title primary-title>
        <div>
          <h3 class="headline mb-0">{{page.title}} tit + {{index}}</h3>
          <div> {{page.content}} tit </div>
        </div>
      </v-card-title>
    </v-card>
    <v-btn fab dark color="indigo" @click="newNote()">
      <v-icon dark>add</v-icon>
    </v-btn>
  </div>
</template>
<script>
export default {
  name: 'Notes',
  props: ['pages'],
  methods: {
    newNote () {
    this.$emit('new-note')
    }
  }
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
a {
  color: #42b983;
}
</style>
App.vue
<template>
  <v-app>
    <v-toolbar app>
      <v-toolbar-title >
        <span>VueNoteApp</span>
      </v-toolbar-title>
      <v-spacer></v-spacer>
      <v-toolbar-items>
        <v-btn flat>About</v-btn>
      </v-toolbar-items>
    </v-toolbar>
    <v-content>
      <Notes :pages="pages" @new-note="newNote"/>
    </v-content>
  </v-app>
</template>
<script>
import Notes from './components/Notes.vue'
export default {
  name: 'app',
  components: {
    Notes
  },
  data: () => ({
    pages:[],
    index:0
  }),
  methods:  {
    newNote () {
      this.pages.push({
        title: 'Title',
        content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus et elit id purus accumsan lacinia. Suspendisse nulla urna, facilisis ac tincidunt in, accumsan sit amet enim. Donec a ante dolor'
      });
    },
    saveNote () {
    },
    deleteNote () {
    }
  }
}
</script>
<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
With these small changes, you should now see something like what is shown in Figure 5-9.
../images/483082_1_En_5_Chapter/483082_1_En_5_Fig9_HTML.jpg
Figure 5-9

Polishing styling

You can go there from the repo (https://github.com/carlosrojaso/appress-book-pwa) with
$git checkout v1.1.1

Adding a Form

Thus far we’ve provided the capability of adding notes by pushing a button, but now we need to allow users to enter their notes. To do this, we need to add a form. And to keep our app simple, we are going to use v-dialog—which is a component that shows us a beautiful modal window where we can put the form—in the App.vue component. In addition, we need to sync the data in the form with the component. To do this, we need to create two variables—newTitle and newContent—and use two-way data binding in App.vue using the directive v-model with the form:
            <v-flex xs12 sm12 md12>
       <v-text-field v-model="newTitle" value="" label="Title*" required></v-text-field>
            </v-flex>
            <v-flex xs12 sm12 md12>
              <v-textarea v-model="newContent" value="" label="Content"></v-textarea>
            </v-flex>
As you can see, we added v-model=newTitle” and v-model=newContent”, and created two properties with the same name in the component data.
  data: () => ({
    ...
    newTitle: ",
    newContent: ",
    ...
  }),

By adding v-model, Vue.js takes care of updating the data and the template for us. Together, all the code looks like this:

App.vue
<template>
  <v-app>
    <v-toolbar app>
      <v-toolbar-title >
        <span>VueNoteApp</span>
      </v-toolbar-title>
      <v-spacer></v-spacer>
      <v-toolbar-items>
        <v-btn flat>About</v-btn>
      </v-toolbar-items>
    </v-toolbar>
    <v-content>
      <Notes :pages="pages" @new-note="newNote"/>
    </v-content>
    <v-dialog v-model="dialog">
    <v-card>
      <v-card-title>
        <span class="headline">New Note</span>
      </v-card-title>
      <v-card-text>
        <v-container grid-list-md>
          <v-layout wrap>
            <v-flex xs12 sm12 md12>
               <v-text-field v-model="newTitle" value="" label="Title*" required></v-text-field>
            </v-flex>
            <v-flex xs12 sm12 md12>
              <v-textarea v-model="newContent" value="" label="Content"></v-textarea>
            </v-flex>
          </v-layout>
        </v-container>
        <small>*indicates required field</small>
      </v-card-text>
      <v-card-actions>
        <v-spacer></v-spacer>
        <v-btn color="blue darken-1" flat @click="closeModal()">Close</v-btn>
        <v-btn color="blue darken-1" flat @click="saveNote()">Save</v-btn>
      </v-card-actions>
    </v-card>
  </v-dialog>
  </v-app>
</template>
<script>
import Notes from './components/Notes.vue'
export default {
  name: 'app',
  components: {
    Notes
  },
  data: () => ({
    pages:[],
    newTitle: ",
    newContent: ",
    index:0,
    dialog: false
  }),
  methods:  {
    newNote () {
      this.dialog = true;
    },
    saveNote () {
      this.pages.push({
        title: this.newTitle,
        content: this.newContent
      });
      this.resetForm();
      this.closeModal();
    },
    closeModal () {
      this.dialog = false;
    },
    deleteNote () {
    },
    resetForm () {
      this.newTitle = ";
      this.newContent = ";
    }
  }
}
</script>
<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
}
</style>
Notes.vue
<template>
  <div class="notes">
    <v-card v-for="(page, index) in pages" :key="index">
      <v-card-title primary-title>
        <div>
          <h3 class="headline mb-0">{{page.title}}</h3>
          <div>{{page.content}}</div>
        </div>
      </v-card-title>
    </v-card>
    <v-btn
      fab
      dark
      absolute
      right
      color="indigo"
      class="floatButton"
      @click="newNote()">
      <v-icon dark>add</v-icon>
    </v-btn>
  </div>
</template>
<script>
export default {
  name: 'Notes',
  props: ['pages'],
  methods: {
    newNote () {
    this.$emit('new-note');
    }
  }
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.floatButton {
  margin: 10px;
}
</style>
Now if you run our app, you can add notes, as shown in Figure 5-10.
../images/483082_1_En_5_Chapter/483082_1_En_5_Fig10_HTML.jpg
Figure 5-10

Form to add a new note

You can go there from the repo (https://github.com/carlosrojaso/appress-book-pwa) with
$git checkout v1.1.2

Deleting a Note

Users can now add notes in our app, but we also need to give them the ability to delete them as well. To do this, we need to modify our array pages where we keep our notes, remove the element that our user select to delete, and calculate the new size for our array. JS has two methods that can help us with this: splice() and Math.max().

The splice() method allows us to change the content in our array. We use the deleteNote(item) method to pass the item index we want delete. We use splice() to remove it from the array.
deleteNote (item) {
       this.pages.splice( item, 1);
       ...
}

When we remove an item from the array, we need to update this.index data when we save the size of our pages array.

The Math.max() method allows us to get the largest value between values. We use it to avoid an error when we delete the last item. If we remove the last item, our index value will be –1, which results in unexpected behavior in our app. With Math.max(), we can assign a value of 0 to prevent this from happening.
deleteNote (item) {
       ...
       this.index = Math.max(this.index - 1, 0);
}

Challenge

Add an action button and accompanying functionality to our app to edit a note.

All the code together looks like this:

App.vue
<template>
  <v-app>
    <v-toolbar app>
      <v-toolbar-title >
        <span>VueNoteApp</span>
      </v-toolbar-title>
      <v-spacer></v-spacer>
      <v-toolbar-items>
        <v-btn flat>About</v-btn>
      </v-toolbar-items>
    </v-toolbar>
    <v-content>
      <Notes :pages="pages" @new-note="newNote" @delete-note="deleteNote"/>
    </v-content>
    <v-dialog v-model="dialog">
    <v-card>
      <v-card-title>
        <span class="headline">New Note</span>
      </v-card-title>
      <v-card-text>
        <v-container grid-list-md>
          <v-layout wrap>
            <v-flex xs12 sm12 md12>
              <v-text-field v-model="newTitle" value="" label="Title*" required></v-text-field>
            </v-flex>
            <v-flex xs12 sm12 md12>
              <v-textarea v-model="newContent" value="" label="Content"></v-textarea>
            </v-flex>
          </v-layout>
        </v-container>
        <small>*indicates required field</small>
      </v-card-text>
      <v-card-actions>
        <v-spacer></v-spacer>
        <v-btn color="blue darken-1" flat @click="closeModal()">Close</v-btn>
        <v-btn color="blue darken-1" flat @click="saveNote()">Save</v-btn>
      </v-card-actions>
    </v-card>
  </v-dialog>
  </v-app>
</template>
<script>
import Notes from './components/Notes.vue'
export default {
  name: 'app',
  components: {
    Notes
  },
  data: () => ({
    pages:[],
    newTitle: ",
    newContent: ",
    index: 0,
    dialog: false
  }),
  methods:  {
    newNote () {
      this.dialog = true;
    },
    saveNote () {
      this.pages.push({
        title: this.newTitle,
        content: this.newContent
      });
      this.index = this.pages.length - 1;
      this.resetForm();
      this.closeModal();
    },
    closeModal () {
      this.dialog = false;
    },
    deleteNote (item) {
      this.pages.splice( item, 1);
      this.index = Math.max(this.index - 1, 0);
    },
    resetForm () {
      this.newTitle = ";
      this.newContent = ";
    }
  }
}
</script>
<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
}
</style>
Notes.vue
<template>
  <div class="notes">
    <v-card v-for="(page, index) in pages" :key="index">
      <v-card-title primary-title>
        <div>
          <h3 class="headline mb-0">{{page.title}}</h3>
          <div>{{page.content}}</div>
        </div>
      </v-card-title>
      <v-card-actions>
            <v-btn
            flat
            @click="deleteNote(index)"
            color="orange">Delete</v-btn>
      </v-card-actions>
    </v-card>
    <v-btn
      fab
      dark
      absolute
      right
      color="indigo"
      class="floatButton"
      @click="newNote()">
      <v-icon dark>add</v-icon>
    </v-btn>
  </div>
</template>
<script>
export default {
  name: 'Notes',
  props: ['pages'],
  methods: {
    newNote () {
    this.$emit('new-note');
    },
    deleteNote (item) {
    this.$emit('delete-note', item++);
    }
  }
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.floatButton {
  margin: 10px;
}
</style>
You can go there from the repo (https://github.com/carlosrojaso/appress-book-pwa) with
$git checkout v1.1.3
Check out Figure 5-11.
../images/483082_1_En_5_Chapter/483082_1_En_5_Fig11_HTML.jpg
Figure 5-11

Delete button

What Is the PRPL Architecture Pattern?

PRPL is an experimental web app architecture developed by Google for building web sites and apps that work exceptionally well with unreliable network connections.

PRPL stands for
  • Push critical resources for the initial URL route.

  • Render the initial route.

  • Precache the remaining routes.

  • Lazy-load and create remaining resources on demand.

The main objectives of this pattern are
  • Minimum interaction time

  • Maximum caching efficiency

  • Making development and implementation easy

For most web app projects, it is too early to achieve all the PRPL requirements because the modern Web APIs aren’t supported in all the major browsers.

To apply the PRPL pattern to a PWA, the following must be present:
  • The main entry point

  • The app shell

  • The rest of the fragments of our app loaded with lazy loading

The Main Entry Point

The main entry point is in charge of loading the shell and any necessary polyfill1 quickly. It also uses absolute paths for all its dependencies. In VueNoteApp, our main entry point is index.html.

The App Shell

The app shell is responsible for routing and includes minimal HTML and CSS code to interact with users as quickly as possible. In VueNoteApp, the app shell is generated with Workbox and is present in precache-manifest.xxxxxx.js, which is the name that is autogenerated each time we run $npm run build.

Fragments Loaded with Lazy Loading

When building apps with a bundler, the JS bundle can become quite large, and thus affect page load time. It is more efficient if we split each route’s components into separate chunks and then load them only when the route is visited.

We need to load one fragment on demand in our app when users click the About link. To do this, we develop an about view in About.vue, which looks something like this:
const About = () => import('./About.vue')
Then we add this new route to the router:
export const routes = [
  {path: ", component: HelloWorld},
  {path: '/about', component: About},
];

We add this to our app forward when we create our file routes.js, and delegate the lazy loading and bundle responsibility to Vue CLI.

The PRPL pattern is a paradigm designed to alleviate a stressful user experience while users browse the Web from their mobile device.

Adding a Router

Usually in our app, we switch between views. To do this, we need a routing mechanism. Vue.js has an official plug-in we need to add:
$npm install vue-router

We then follow the steps we took in the section “What Is a Vue Router?”

main.js
import Vue from 'vue'
import App from './App.vue'
import Vuetify from 'vuetify'
import VueRouter from 'vue-router'
import { routes } from "./routes"
import 'vuetify/dist/vuetify.min.css'
import './registerServiceWorker'
Vue.config.productionTip = false
Vue.use(Vuetify)
Vue.use(VueRouter);
const myRouter = new VueRouter({
  routes: routes
});
new Vue({
  router: myRouter,
  render: h => h(App),
}).$mount('#app')

We create a routes.js file to handle the router we are going to use in our app. Something important here is that, if you remember the PRPL pattern, we need to use lazy loading in our app. Here you can see that we are loading About.vue in a lazy way:

routes.js
import Dashboard from './components/Dashboard.vue'
const lazyAbout = () => import('./components/About.vue')
export const routes = [
  {path: ", component: Dashboard},
  {path: '/dashboard', component: Dashboard},
  {path: '/about', component: lazyAbout}
];

All the code together looks like this:

App.vue
<template>
  <v-app>
    <v-toolbar app>
      <v-toolbar-title >
        <router-link to="/">VueNoteApp</router-link>
      </v-toolbar-title>
      <v-spacer></v-spacer>
      <v-toolbar-items>
        <v-btn
        to="/about"
        flat
        >About</v-btn>
      </v-toolbar-items>
    </v-toolbar>
    <router-view></router-view>
  </v-app>
</template>
<script>
export default {
  name: 'app'
}
</script>
<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
}
a {
  text-decoration: none;
}
</style>
components/About.vue
<template>
  <div class="about">
    <v-content>
      <br/><b>Building PWAs with VueJS.</b><br/><br/>
      repo: https://github.com/carlosrojaso/appress-book-pwa<br/>
      2019
    </v-content>
  </div>
</template>
<script>
export default {
  name: 'About'
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
components/Dashboard.vue
<template>
  <div class="dashboard">
    <v-content>
      <Notes :pages="pages" @new-note="newNote" @delete-note="deleteNote"/>
    </v-content>
    <v-dialog v-model="dialog">
        <v-card>
        <v-card-title>
            <span class="headline">New Note</span>
        </v-card-title>
        <v-card-text>
            <v-container grid-list-md>
            <v-layout wrap>
                <v-flex xs12 sm12 md12>
                <v-text-field v-model="newTitle" value="" label="Title*" required></v-text-field>
                </v-flex>
                <v-flex xs12 sm12 md12>
                <v-textarea v-model="newContent" value="" label="Content"></v-textarea>
                </v-flex>
            </v-layout>
            </v-container>
            <small>*indicates required field</small>
        </v-card-text>
        <v-card-actions>
            <v-spacer></v-spacer>
            <v-btn color="blue darken-1" flat @click="closeModal()">Close</v-btn>
            <v-btn color="blue darken-1" flat @click="saveNote()">Save</v-btn>
        </v-card-actions>
        </v-card>
    </v-dialog>
  </div>
</template>
<script>
import Notes from './Notes.vue'
export default {
  name: 'Dashboard',
  components: {
    Notes
  },
  data: () => ({
    pages:[],
    newTitle: ",
    newContent: ",
    index: 0,
    dialog: false
  }),
  methods:  {
    newNote () {
      this.dialog = true;
    },
    saveNote () {
      this.pages.push({
        title: this.newTitle,
        content: this.newContent
      });
      this.index = this.pages.length - 1;
      this.resetForm();
      this.closeModal();
    },
    closeModal () {
      this.dialog = false;
    },
    deleteNote (item) {
      this.pages.splice( item, 1);
      this.index = Math.max(this.index - 1, 0);
    },
    resetForm () {
      this.newTitle = ";
      this.newContent = ";
    }
  }
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
components/Notes.vue
<template>
  <div class="notes">
    <v-card v-for="(page, index) in pages" :key="index">
      <v-card-title primary-title>
        <div>
          <h3 class="headline mb-0">{{page.title}}</h3>
          <div>{{page.content}}</div>
        </div>
      </v-card-title>
      <v-card-actions>
            <v-btn
            flat
            @click="deleteNote(index)"
            color="orange">Delete</v-btn>
      </v-card-actions>
    </v-card>
    <v-btn
      fab
      dark
      absolute
      right
      color="indigo"
      class="floatButton"
      @click="newNote()">
      <v-icon dark>add</v-icon>
    </v-btn>
  </div>
</template>
<script>
export default {
  name: 'Notes',
  props: ['pages'],
  methods: {
    newNote () {
    this.$emit('new-note');
    },
    deleteNote (item) {
    this.$emit('delete-note', item++);
    }
  }
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.floatButton {
  margin: 10px;
}
</style>
Now when we load our app, Vue.js just loads the resources for the main view, as shown in Figure 5-12.
../images/483082_1_En_5_Chapter/483082_1_En_5_Fig12_HTML.jpg
Figure 5-12

VueNoteApp with router

Other views, such as about, wait until the user navigates to the /about route to load that resource (Figure 5-13).
../images/483082_1_En_5_Chapter/483082_1_En_5_Fig13_HTML.jpg
Figure 5-13

VueNoteApp /about route

As you can see in Figure 5-13, we have now an additional view. You can go there from the repo (https://github.com/carlosrojaso/appress-book-pwa) with
$git checkout v1.1.4

Adding Firebase

At this point, if we try to reload our app, we will see that we lost all our notes. Therefore, we need an external storage system that keeps our data and syncs them among all our clients.

Firebase Database is a perfect solution for syncing our data in real time to all our clients, and we can save the data easily with its JS software development kit.

To get started, sign in to https://firebase.google.com/ and use your Google account to log in (Figure 5-14).
../images/483082_1_En_5_Chapter/483082_1_En_5_Fig14_HTML.jpg
Figure 5-14

Firebase web site

Then go to the console (Figure 5-15).
../images/483082_1_En_5_Chapter/483082_1_En_5_Fig15_HTML.jpg
Figure 5-15

Firebase console link

We create a new project (as described in Chapter 1) and, in just minutes, we can start to use our new project (Figure 5-16).
../images/483082_1_En_5_Chapter/483082_1_En_5_Fig16_HTML.jpg
Figure 5-16

Creating a new project

When our project is ready in the Firebase console, we must provide information that allows us to connect our app and Firebase. Select Project Overview ➤ Project settings (Figure 5-17).
../images/483082_1_En_5_Chapter/483082_1_En_5_Fig17_HTML.jpg
Figure 5-17

Firebase project overview

Select the web app button (the third button from the left in Figure 5-18).
../images/483082_1_En_5_Chapter/483082_1_En_5_Fig18_HTML.jpg
Figure 5-18

Firebase project settings view

Firebase starts a setup wizard (Figure 5-19).
../images/483082_1_En_5_Chapter/483082_1_En_5_Fig19_HTML.jpg
Figure 5-19

Firebase web app wizard

At the end, we see our firebaseConfig (Figure 5-20).
../images/483082_1_En_5_Chapter/483082_1_En_5_Fig20_HTML.jpg
Figure 5-20

Firebase configuration summary

Copy this information.

The last thing we need to do in the console is to create a new database and open the security rules to be accessed without authentication (we do this to keep our app simple).

From Project Overview, select Database (Figure 5-21).
../images/483082_1_En_5_Chapter/483082_1_En_5_Fig21_HTML.jpg
Figure 5-21

Firebase Database link

Then select Create Realtime Database (Figure 5-22).
../images/483082_1_En_5_Chapter/483082_1_En_5_Fig22_HTML.jpg
Figure 5-22

Firebase security rules

Make sure “Start in test mode” is selected. In this mode, we can write data to our database without having authentication. This is a handy feature during development, but it is unsafe during production. Now we can go back to our app.

In the terminal, run
$npm install firebase --save

Then, create a new file—firebase.js —and paste in the data from your Firebase project.

firebase.js
import Firebase from 'firebase';
let config = {
  apiKey: "AIzaSyDT2_jZiasDK017UicoakK1gn93FIZnJ5o",
  authDomain: "appress-book-pwa.firebaseapp.com",
  databaseURL: "https://appress-book-pwa.firebaseio.com",
  projectId: "appress-book-pwa",
  storageBucket: "",
  messagingSenderId: "217988350403",
  appId: "1:217988350403:web:cd5e278739086fd1"
};
export const app = Firebase.initializeApp(config);

Next, import in main.js .

main.js
import './firebase';
import Vue from 'vue'
import App from './App.vue'
import Vuetify from 'vuetify'
import VueRouter from 'vue-router'
import { routes } from "./routes"
...

Now, in Dashboard.vue , we need to use the life cycle method mounted() to recover all the notes in our real-time database. We also need to update saveNote() and deleteNote() to update the new notes in Firebase.

We import fireApp from firebase.js to keep a reference in our app with
const db = fireApp.database().ref();

With db.push, we can add data to Firebase; with remove(), we can delete data from Firebase. For more information, go to https://firebase.google.com/docs/reference/js/firebase.database.

Dashboard.vue
<template>
  <div class="dashboard">
    <v-content>
      <Notes :pages="pages" @new-note="newNote" @delete-note="deleteNote"/>
    </v-content>
    <v-dialog v-model="dialog">
        <v-card>
        <v-card-title>
            <span class="headline">New Note</span>
        </v-card-title>
        <v-card-text>
            <v-container grid-list-md>
            <v-layout wrap>
                <v-flex xs12 sm12 md12>
                <v-text-field v-model="newTitle" value="" label="Title*" required></v-text-field>
                </v-flex>
                <v-flex xs12 sm12 md12>
                <v-textarea v-model="newContent" value="" label="Content"></v-textarea>
                </v-flex>
            </v-layout>
            </v-container>
            <small>*indicates required field</small>
        </v-card-text>
        <v-card-actions>
            <v-spacer></v-spacer>
            <v-btn color="blue darken-1" flat @click="closeModal()">Close</v-btn>
            <v-btn color="blue darken-1" flat @click="saveNote()">Save</v-btn>
        </v-card-actions>
        </v-card>
    </v-dialog>
  </div>
</template>
<script>
import {fireApp} from'../firebase.js'
import Notes from './Notes.vue'
const db = fireApp.database().ref();
export default {
  name: 'Dashboard',
  components: {
    Notes
  },
  data: () => ({
    pages:[],
    newTitle: ",
    newContent: ",
    index: 0,
    dialog: false
  }),
  mounted() {
    db.once('value', (notes) => {
      notes.forEach((note) => {
        this.pages.push({
          title: note.child('title').val(),
          content: note.child('content').val(),
          ref: note.ref
        })
      })
    })
  },
  methods:  {
    newNote () {
      this.dialog = true;
    },
    saveNote () {
      const newItem = {
        title: this.newTitle,
        content: this.newContent
      };
      this.pages.push(newItem);
      this.index = this.pages.length - 1;
      db.push(newItem);
      this.resetForm();
      this.closeModal();
    },
    closeModal () {
      this.dialog = false;
    },
    deleteNote (item) {
      let noteRef = this.pages[item].ref;
      if(noteRef) { noteRef.remove(); }
      this.pages.splice( item, 1);
      this.index = Math.max(this.index - 1, 0);
    },
    resetForm () {
      this.newTitle = ";
      this.newContent = ";
    }
  }
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
Now we should be able to see our data store in Firebase (Figure 5-23).
../images/483082_1_En_5_Chapter/483082_1_En_5_Fig23_HTML.jpg
Figure 5-23

Firebase Database console

And when we refresh our data, they are saved (Figure 5-24).
../images/483082_1_En_5_Chapter/483082_1_En_5_Fig24_HTML.jpg
Figure 5-24

VueNoteApp with the data in Firebase

You can go there from the repo (https://github.com/carlosrojaso/appress-book-pwa) with
$git checkout v1.1.5

Summary

The core library of Vue.js is focused on the view layer only, and it is easy to pick up and integrate with other libraries or existing projects.

Components are created and destroyed during their life cycle, and there are methods we can use to run functions at specific times. These methods are called life cycle hooks. Life cycle hooks are an essential part of any serious component.

Components usually need to share information. To effect this, we can use props, the ref attribute, emitters, and the two-way data binding.

Vue Router is an official routing plug-in for SPAs. It was designed for use with the Vue.js framework. A router is a way to jump from one view to another in an SPA.

We can use Firebase Database to keep our notes synced among all the clients.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.137.164.241