Author: Mikołaj Smoleński
Working Offline
Now, we are going to add an indicator to inform our users that the internet connection has been lost. Firstly, though, let's have an overview of what's needed for the component to work. Let's start with the underlying logic.
How does an app even know that the digital flow that has brought it to browser is no longer there? With JavaScript it's fairly simple. The Navigator interface represents the state and the identity of the user agent. It allows scripts to query it and to register themselves to carry on some activities. To ask it whether we're using internet to connect to the current page, try:
navigator.onLine
The above returns a boolean answer, that should be false for stuff served from
our own webserver (localhost). The solution is not that useful, though - we do not want to be monitoring the
status every given interval - in fact, we do not care about it until the very moment it changes. And when it
finally does, we want to react and vue the notification. Yes, it was a pun. Haha. So, how do we do that?
When the navigator.onLine's value changes, the Window interface fires an event, either
online or offline. That means that we can listen to it!
window.addEventListener('online', (event) => {
console.log("You are now connected to the network.");
});
Note
Please note that this event is inherently unreliable. 'Nuff said that a computer can be connected to a network and not have Internet access.
Time to get to work! We will begin with gearing up our App.vue file with some
useful logic. Let's start by adding event listeners to the Window interface on the App's created
life-cycle hook, so they can start listening to the online / offline events. The second
thing are the callbacks - we want them to help us monitor changes in connection. For that, we need a legit
data object property on our App component. I called it status,
but obviously you can call it whatever you want - as long as you stay consistent throughout the code.
// very bottom of the data object
status: ''
};
},
created() {
window.addEventListener('online', () => {
this.status = 'online';
});
window.addEventListener('offline', () => {
this.status = 'offline';
});
},
// component's methods...
Our app can already tell when it gets disconnected! You can check it out yourself by first
going offline (the Network tab of devTools) and then checking out how the data inside changes
(through console.logging the this.status property in the callback or by watching the
actual data change in the Vue extension devTools panel).
That's awesome, but while the app can tell that something changed, our users cannot. We need
to consume the data to gain its powers; to pass the knowledge of the status down the DOM tree, where
hopefully we'll have a component to display it. Actually, why not crate one now?
Let's go to the src/components directory and create a
Notificaion.vue file (then again, you can call it Whatever.vue, just stick to it). There
is a number of reasons why we would want to have it in a separate file - encapsulation, making use of the elegant,
.vue Single File Components, keeping App.vue tidy are some of the most important. You
are free to design this notification as you wish, it's the underlying logic that's important. In our example, we
will use a MDBootstrap Alert component for the nice looks & animations.
<script>
import { mdbAlert } from 'mdbvue';
export default {
name: "Notification",
components: {
mdbAlert
},
props: {
status: String
},
data() {
return {
availableStatuses: {
offline: {
color: 'danger',
emoji: '☢️',
text: 'Offline mode engaged.'
},
online: {
color: 'success',
emoji: '👌',
text: 'Back online!'
}
},
isShown: false,
}
},
methods: {
showAlert() {
this.isShown = true;
const timeout = setTimeout(this.hideAlert, 3000);
},
hideAlert() {
this.isShown = false;
}
},
watch: {
status() {
this.showAlert();
}
}
}
</script>
Let's walk through the component together. We import the mdbAlert from
mdbvue library and declare it as a component used. Next, we declare the anticipated prop, which is the
status string. The data object has two properties - availableStatuses object and
isShown boolean. First one is used to store possible statuses - for now there is only online
and offline, but as your app grows, there might be a need to enhance the object, perhaps even to
import it from an external file.
The other one, isShown, will be used to toggle visibility of mdbAlert. We don't
want our notifications being shown all the time, right? To toggle the boolean we will be using the subsequent
showAlert and hideAlert methods. For our component, showing means turning the
isShown value to true and setting a timeout of 3000 miliseconds after which
it will be turned back to false.
Finally - the watcher. We need it to have an eye for the prop change. "But Vue is smart, it
can detect props change and act upon it without explicitly asking it for it!", I heard a voice coming from the
second row. It is true - Vue can indeed watch props change for our convenience, but it does not (and should not!)
do that for every single moving part. It does that by itself only when the prop seems directly involved in the
rendering process through directives or when the change can potentially affect data structure in the component, as
is the case with computed properties.
Notification's case id different - it's us who are calling the shots. The
component is consuming the status props indirectly, as rendering is controlled through an internal
setTimout mechanism. Vue needs to be told explicitly to watch for the prop changes, and
once they take place, we want to respond by calling the this.showAlert() method. That's it for our
script tag, the hardest part is behind us!
<template>
<mdb-alert v-if="isShown" :color="availableStatuses[status].color" enterAnimation="bounceIn"
leaveAnimation="slideOutRight">
<span role="img" aria-label="emoji">
{{availableStatuses[status].emoji}}
</span>
{{availableStatuses[status].text}}
</mdb-alert>
</template>
Now, as of the tamplete tag, it might seem complex, but in truth it's pretty
straight-forward. As we mentioned, we're using mdbAlert as a base. The most important thing to note
here is the v-if directive, which allows us to conditionally render the underlying component
depending on whether the isShown boolean evaluates to true in any given mement. Second,
the :color bound attribute. mdbAlert normally takes a color prop to
customize its, well, color (see available colors here).
The colon at the beginning makes the prop "bound", meaning instead of passing in a name of an actual color, the
attribute's value will be evaluated as a JavaScript expression, in a potentially dynamic fashion. We do not
know what the color the mdbAlert should be, so we are using a computed property name (the square
brackets) to access availableStatuses data object and pick a color depending on the status at hand.
The enterAnimation & leaveAnimation are props used to customize the
animations as mdbAlert component comes and goes. I used bounceIn and
slideOutRight because I think they look cool, but feel free to pick your own from MDBVue Animations page.
Lastly, we are using the same computed property name trick to fill in for notification's emoji and text.
<style scoped>
.alert {
position: absolute;
z-index: 2;
right: 0;
}
.alert span {
margin-right: 10px;
}
</style>
Finally, some styling, and most notably - the position: absolute CSS rule. Now
our component will appear in the top right corner of the screen without distracting natural flow of elements
positioned relatively. The component is ready!
<template>
<mdb-container>
<Notification :status="status"/>
...
<script>
import Notification from '@/components/Notification';
...
components: {
Notification,
...
}
There is only one thing left to do - include our baby into the parent
App.vue component. Let's open the file, import Notification from
'@/components/Notification'; and register it by including it in the components field. Let's
add it to the App.vue's <template> tag as a direct mdbContainer's
child. Make sure that our key prop, status, is passed to it in a bound (dynamic) way by preceding
prop name with a colon.
Open your app and try to turn-on and turn-off your internet connection. You should see something similar to this:


Voilà! Let's deploy with yarn deploy.
In case things went a bit south, below is the full code for both App.vue and
componens/Notification.vue:
<template>
<mdb-container>
<Notification :status="status"/>
<mdb-row>
<mdb-col col="9">
<h2 class="text-uppercase my-3">Today:</h2>
<Event
v-for="(event, index) in events"
:index="index"
:time="event.time"
:title="event.title"
:location="event.location"
:description="event.description"
:key="index"
@delete="handleDelete"
/>
<mdb-row>
<mdb-col xl="3" md="6" class="mx-auto text-center">
<mdb-btn color="info" @click.native="modal = true">Add Event</mdb-btn>
</mdb-col>
</mdb-row>
</mdb-col>
<mdb-col col="3">
<h3 class="text-uppercase my-3">Schedule</h3>
<h6 class="my-3">
It's going to be busy that today. You have
<b>{{events.length}} events</b> today.
</h6>
<h1 class="my-3">
<mdb-row>
<mdb-col col="3" class="text-center">
<mdb-icon far icon="sun"/>
</mdb-col>
<mdb-col col="9">Sunny</mdb-col>
</mdb-row>
<mdb-row>
<mdb-col col="3" class="text-center">
<mdb-icon icon="thermometer-three-quarters"/>
</mdb-col>
<mdb-col col="9">23°C</mdb-col>
</mdb-row>
</h1>
<p>
Don't forget your sunglasses. Today will dry and sunny, becoming
warm in the afternoon with temperatures of between 20 and 25
degrees.
</p>
</mdb-col>
</mdb-row>
<mdb-modal v-if="modal" @close="modal = false">
<mdb-modal-header>
<mdb-modal-title tag="h4" class="w-100 text-center font-weight-bold">Add new event</mdb-modal-title>
</mdb-modal-header>
<mdb-modal-body>
<form class="mx-3 grey-text">
<mdb-input
name="time"
label="Time"
icon="clock"
placeholder="12:30"
type="text"
@input="handleInput($event, 'time')"
/>
<mdb-input
name="title"
label="Title"
icon="edit"
placeholder="Briefing"
type="text"
@input="handleInput($event, 'title')"
/>
<mdb-input
name="location"
label="Location (optional)"
icon="map"
type="text"
@input="handleInput($event, 'location')"
/>
<mdb-textarea
name="description"
label="Description (optional)"
icon="sticky-note"
@input="handleInput($event, 'description')"
/>
</form>
</mdb-modal-body>
<mdb-modal-footer class="justify-content-center">
<mdb-btn color="info" @click.native="addEvent">Add</mdb-btn>
</mdb-modal-footer>
</mdb-modal>
</mdb-container>
</template>
<script>
import {
mdbContainer,
mdbRow,
mdbCol,
mdbIcon,
mdbBtn,
mdbModal,
mdbModalHeader,
mdbModalTitle,
mdbModalBody,
mdbModalFooter,
mdbInput,
mdbTextarea
} from "mdbvue";
import Event from "@/components/Event";
import Notification from '@/components/Notification';
export default {
name: "App",
components: {
mdbContainer,
mdbRow,
mdbCol,
mdbIcon,
mdbBtn,
mdbModal,
mdbModalHeader,
mdbModalTitle,
mdbModalBody,
mdbModalFooter,
mdbInput,
mdbTextarea,
Event,
Notification
},
data() {
return {
events: [
{
time: "10:00",
title: "Breakfast with Simon",
location: "Lounge Caffe",
description: "Discuss Q3 targets"
},
{
time: "10:30",
title: "Daily Standup Meeting (recurring)",
location: "Warsaw Spire Office"
},
{
time: "11:00",
title: "Call with HRs"
},
{
time: "12:00",
title: "Lunch with Timmoty",
location: "Canteen",
description: "Project evalutation"
}
],
modal: false,
newValues: [],
status: ''
};
},
created() {
window.addEventListener('online', () => {
this.status = 'online';
});
window.addEventListener('offline', () => {
this.status = 'offline';
});
},
methods: {
handleDelete(eventIndex) {
this.events.splice(eventIndex, 1);
},
handleInput(val, type) {
this.newValues[type] = val;
},
addEvent() {
this.events.push({
time: this.newValues["time"],
title: this.newValues["title"],
location: this.newValues["location"],
description: this.newValues["description"]
});
}
}
};
</script>
<template>
<mdb-alert
v-if="isShown"
:color="availableStatuses[status].color"
enterAnimation="bounceIn"
leaveAnimation="slideOutRight"
>
<span
role="img"
aria-label="emoji"
>
{{availableStatuses[status].emoji}}
</span>
{{availableStatuses[status].text}}
</mdb-alert>
</template>
<script>
import { mdbAlert } from 'mdbvue';
export default {
name: "Notification",
components: {
mdbAlert
},
props: {
status: String
},
data() {
return {
availableStatuses: {
offline: {
color: 'danger',
emoji: '☢️',
text: 'Offline mode engaged.'
},
online: {
color: 'success',
emoji: '👌',
text: 'Back online!'
}
},
isShown: false
}
},
methods: {
showAlert() {
this.isShown = true;
const timeout = setTimeout(this.hideAlert, 3000);
},
hideAlert() {
this.isShown = false;
}
},
watch: {
status() {
this.showAlert();
}
}
}
</script>
<style>
.alert {
position: absolute;
z-index: 2;
right: 0;
}
.alert span {
margin-right: 10px;
}
</style>
Something doesn't work for you? Check the code for this lesson on our repository
Previous lesson Download Live preview Next lesson
Spread the word:
