Notifications compel users to pay attention to specific pieces of information and encourage user interaction with our content or website.
What we will be building
This post will discuss managing real-time app notifications in Vue.js to create a food ordering notification service. We will subscribe to channels in our database and display notifications when changes occur in the channels.
GitHub URL
https://github.com/Iheanacho-ai/appwrite-food-notification
Prerequisites
To get the most out of this project, we require the following:
- A basic understanding of CSS, JavaScript, and Vue.js
- Docker Desktop is installed on the computer, run the
docker -v
command to verify if we have Docker Desktop installed, if not, install it from the Get Docker documentation - An Appwrite instance running on our computer, check out this article to create a local Appwrite instance, we will use Appwrite’s robust database and Realtime service to manage our application
Setting up our Vue.js application
We install the Vue CLI by running these terminal commands to create a new Vue project.
npm install -g @vue/cli
# OR
yarn global add @vue/cli
After installing the Vue CLI, we navigate to our preferred directory and create a new project.
vue create <name of our project>
We change the directory to the project and start a development server with:
npm run serve
To see the app, we go to http://localhost:8080/
Installing Dependencies
Installing Tailwind CSS
Tailwind CSS is a "utility-first" CSS framework that allows us to create user interfaces for web applications rapidly.
To install Tailwind CSS in our project, we run these terminal commands.
npm install -D tailwindcss@latest postcss@latest autoprefixer@latest
npx tailwindcss init -p
These commands create two files in the root directory of our project, tailwind.config.js
and postcss.config.js
.
In our tailwind.config.js
, we add the paths to all our template files with this code below.
module.exports = {
purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
content: [],
theme: {
extend: {},
},
plugins: [],
}
Next, we add the tailwind directives in our src/index.css
file.
@tailwind base;
@tailwind components;
@tailwind utilities;
Installing Vue Router
Routing is the technology used to switch between different pages in our application based on the changes in the current URL.
We run this terminal command to enable our application to use Vue Router.
npm install vue-router@4
#or
yarn add vue-router@4
Installing Mosha Vue Toastify
Mosha Vue Toastify is a lightweight Vue library that allows us to create super customizable notifications.
We run these commands to install the Mosha Vue Toastify library in our project.
npm install mosha-vue-toastify
#or
yarn add mosha-vue-toastify
Installing Appwrite
Appwrite is an open-source, end-to-end, back-end server solution that allows developers to build applications faster.
To use Appwrite in our Vue application, we install the Appwrite client-side SDK for web applications.
npm install appwrite
Creating a new Appwrite project
During the creation of the Appwrite instance, we specified what hostname and port we see our console. The default value is localhost:80.
We go to localhost:80 and create a new account to see our console.
On our console, there is a Create Project button. Click on it to start a new project.
Our project dashboard appears once we have created the project. At the top of the page, there is a Settings bar. Click it to access the Project ID and API Endpoint.
We copy the Project ID and API Endpoint, which we need to initialize the Appwrite Web SDK.
In our utils/web-init.js
file, we initialize our Web SDK with this piece of code.
import { Appwrite } from 'appwrite';
export const sdk = new Appwrite();
sdk
.setEndpoint('http://localhost/v1') // Replace this with your endpoint
.setProject('projectID'); // Replace this with your ProjectID
Creating an anonymous user session
Appwrite requires a user to sign in before reading or writing to a database to enable safety in our application. However, they allow us to create an anonymous session to bypass that policy for simple projects.
We create an anonymous user session in our utils.js
file.
import { Appwrite } from 'appwrite';
export const sdk = new Appwrite();
sdk
.setEndpoint('http://localhost/v1') // Replace this with your endpoint
.setProject('62532ce64d84a58176da'); // Replace this with your ProjectID
// Create an anonymous user session
sdk.account.createAnonymousSession().then(
(response) => {
console.log(response);
},
(error) => {
console.log(error);
}
);
Creating the collection and attributes
Next, we set up our database that will store our order status. In the Appwrite web Console, we click on Database on the left side of the dashboard.
We create a collection in our database tab by clicking on the Add Collection button. This action redirects us to a Permissions page.
At the Collection Level, we want to assign a Read Access and Write Access with a role:all value. We can modify the permissions to specify who has access to read or write to our database.
On the right of the Permissions page, we copy the Collection ID, which we need to perform operations on the collection’s documents.
Next, we go to our Attributes tab to create the properties we want a document to have.
We create a string attribute of orderStatus.
Creating our food ordering notification application.
Our food notification application will have two pages. One page will place an order and update the order’s status, which could either be accepted, prepared, en route, or delivered. The second page subscribes to the order status and notifies us about any updates to it in real-time.
Creating our Order Page
Our order page will consist of a button to place an order and a select list to update our order status.
We create a views
folder in our src
folder to create this user interface. This src/views
folder will contain a Home.vue
file.
We create our order page in our src/views/Home.vue
file.
<template>
<div class="home rounded overflow-hidden shadow-lg">
<div class="home-container">
<button type="button" class="bg-gray-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Place Order</button>
<h3>Change Order Status to follow your order</h3>
<select name="order-status" id="order-status" class ="relative w-150 bg-white border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<option value="Order Placed" >Order Placed</option>
<option value="Order Accepted" >Order Accepted</option>
<option value="Prepared" >Prepared</option>
<option value="En-route" >En-route</option>
<option value="Delivered">Delivered</option>
</select>
</div>
</div>
</template>
<style>
#app {
display: flex;
justify-content: center;
align-items: center;
height: 80vh;
text-align: center;
}
.app-container{
height: 400px;
}
.page-link{
margin: 10px;
text-decoration: underline;
}
</style>
Routing to our pages
In our src/main.js
file, we enable our application to use router
to navigate to different pages with this piece of code.
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import './index.css';
createApp(App).use(router).mount('#app')
Next, we create a router
folder in our src
folder. Our router
folder will contain an index.js
file. This file will be responsible for defining what routes lead to specific components.
// src/router/index.js
import { createRouter,createWebHistory } from 'vue-router'
import Home from '@/views/Home'
import Notification from '@/views/Notifications'
const routes = [
{
path: '/',
name: 'Home',
component: Home
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router;
This code block above does the following:
- Creates a
routes
array containing the route's path and the component we want the route to lead to. - Facilitate routing using Vue
createRouter
method, which receives an object as a parameter, this object has two fields, a history field and a route field whose value is our route array.
Next, in our App.vue
file, we add the links that will link to these pages.
<template>
<div class="app-container">
<router-view ></router-view>
<router-link class="page-link" to="/"> Home Page</router-link>
</div>
</template>
<script>
export default {
name: 'App',
}
</script>
<style>
#app {
display: flex;
justify-content: center;
align-items: center;
height: 80vh;
text-align: center;
}
.app-container{
height: 400px;
}
.page-link{
margin: 10px;
text-decoration: underline;
}
</style>
- The router-view component represents our component. In the code block above, we specify what position our app is situated.
- The
router-link
component acts as an anchor tag by creating links leading to different routes.
To avoid errors resulting from non-multi-level component names, create an ..eslintrc.js
file in our views
folder.
This views/..eslintrc.js
file will contain this code.
module.exports = {
rules: {
'vue/multi-word-component-names': 0,
},
}
Go to our http://localhost:8080/ to see our Home page.
Creating our Notifications page
In our src/views
folder, we create a Notifications.vue
file to display our order’s status.
<template>
<div class="notifications rounded overflow-hidden shadow-lg">
<div class="order-container">
<h2>Order Placed</h2>
</div>
</div>
</template>
<style scoped>
.notifications{
width: 350px;
height: 100px;
display: flex;
justify-content: center;
align-items: center;
}
</style>
We add a route to the routes
object in the src/router/index.js
file to view our notifications page in the browser.
// src/router/index.js
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/notification-page',
name: 'Notification',
component: Notification
}
]
Here is how our src/router/index.js
file looks.
import { createRouter,createWebHistory } from 'vue-router'
import Home from '@/views/Home'
import Notification from '@/views/Notifications'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/notification-page',
name: 'Notification',
component: Notification
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router;
Next, we add another router-link
in our App.vue
file as an anchor tag to link to the notifications page.
<template>
<div class="app-container">
<router-view ></router-view>
<router-link class="page-link" to="/"> Home Page</router-link>
<!-- link to notification page -->
<router-link class="page-link" to="/notification-page">Notification Page</router-link>
</div>
</template>
To see our notifications page, go to http://localhost:8080/notification-page.
Add interaction with our database
When a user places an order, we create a document in our database that holds the string "Order Placed". Any updates to the order status using the select list will automatically update the document on our Appwrite database.
Our src/views/Home.vue
file will be responsible for creating and updating our database document.
Creating our database document
<script>
import { sdk } from "../../utils";
export default {
name: 'Home',
data(){
return{
documentID: null,
}
},
methods: {
placeOrder: async function(){
try {
let promise = await sdk.database.createDocument('collectionID', 'unique()', {
"orderStatus": "Order Placed"
})
this.documentID = promise.$id;
alert("order successfully placed")
} catch (error) {
console.log(error)
}
},
updateOrder: async function(e){
try {
let orderValue = e.target.value
await sdk.database.updateDocument('collectionID', this.documentID, {
"orderStatus": orderValue
})
alert(orderValue);
} catch (error) {
console.log(error)
}
}
}
}
</script>
In the code block above, we do the following:
- Create a
documentID
variable in ourdata
object to contain our newly created document ID - Create a
placeOrder
function to create a document in our database using Appwrite’screateDocument
- Create a
updateOrder
function that we will pass into the select list later in this tutorial where the function collects the value of an option element and uses it to update our document on the database
In our Home.vue
file, we pass the placeOrder
function to an on-click event listener on our button and our updateDocument
function to an on-change event listener on our select
list.
<template>
<div class="home rounded overflow-hidden shadow-lg">
<div class="home-container">
<button type="button" @click = 'placeOrder' class="bg-gray-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">Place Order</button>
<h3>Change Order Status to follow your order</h3>
<select name="order-status" id="order-status" v-on:change = "updateOrder" class ="relative w-150 bg-white border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<option value="Order Placed" >Order Placed</option>
<option value="Order Accepted" >Order Accepted</option>
<option value="Prepared" >Prepared</option>
<option value="En-route" >En-route</option>
<option value="Delivered">Delivered</option>
</select>
</div>
</div>
</template>
Here is how our Home.vue
file looks.
(https://gist.github.com/Iheanacho-ai/9e47a71adb7fc17b1c5df85c7387dfef)
We place an order to see our document in our database.
Subscribing to updates on the document
We subscribe to our Appwrite database document in our src/views/Notifications.vue
.
<script>
import { sdk } from "../../utils";
export default {
name: 'Notification',
data(){
return{
orderStatus: ""
}
},
mounted(){
if(sdk.account.get !== null){
try {
sdk.subscribe('collections.collectionID.documents', response => {
this.orderStatus = response.payload.orderStatus
});
} catch (error) {
console.log(error, 'error')
}
}
}
}
</script>
In the code block above, we do the following:
- Create an
orderStatus
variable to hold our response payload - Check if we have an active user session before subscribing to our collections channel
- Subscribe to a channel using Appwrite's
subscribe
method, which receives two parameters, the channel we subscribe to and a callback function; to understand more about the various channels we can subscribe to, check out Appwrite's Realtime Channels - Updates our
orderStatus
variable
Next, we render our orderStatus
variable as text in our Notifications.vue
file.
<template>
<div class="notifications rounded overflow-hidden shadow-lg">
<div class="order-container">
<h2>{{orderStatus}}</h2>
</div>
</div>
</template>
Here is how our Notifications.vue
file looks.
(https://gist.github.com/Iheanacho-ai/0f23cf72b2a43be4251f2fb2aa22b7d8)
Creating our Notification
To use the mosha-vue-toastify notification in our application, we import it into our src/views/Notifications.vue
import { sdk } from "../../utils";
import { createToast } from 'mosha-vue-toastify';
import 'mosha-vue-toastify/dist/style.css'
We use the watch
object to listen for a variable change in our application. By listening to the orderStatus
variable, we can fire off the createToast
method every time the variable is updated.
export default {
name: 'Notification',
data(){
return{
orderStatus: ""
}
},
watch: {
orderStatus: function(){
createToast(this.orderStatus)
}
},
mounted(){
...
}
}
Here is how our application looks.
Conclusion
This article discussed using Appwrite’s Realtime feature to subscribe to application events.