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.
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).
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
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.
Life cycle hooks are an essential part of any serious component (Figure 5-3).
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.
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.
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).
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
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.
Install the plug-in.
$npm install vue-router
2.
Add VueRouter in main.js.
...
import VueRouter from 'vue-router'
...
Vue.use(VueRouter);
3.
Create routes in a new routes.js file.
import HelloWorld from './components/HelloWorld.vue'
export const routes = [
{path: ", component: HelloWorld}
];
4.
Create a VueRouter instance.
const router = new VueRouter({routes});
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.
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.
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.
<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'
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:
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.
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'
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.
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).
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).
Select the web app button (the third button from the left in Figure 5-18).
At the end, we see our firebaseConfig (Figure 5-20).
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).
Then select Create Realtime Database (Figure 5-22).
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.
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
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.