How to Add Real-Time In-App Notifications in Nuxt3

Tosin Moronfolu - Oct 4 '22 - - Dev Community

Real-time notifications help users know when a change occurs in your app. You can implement real-time notifications in different ways.

This article teaches you how to implement notifications using Appwrite, an open-source backend server, and Nuxt3, a hybrid vue framework.

Repository

https://github.com/folucode/real-time-appwrite-nuxt3

Prerequisites

  • Basic understanding of CSS, JavaScript, and Nuxt.js.
  • Docker Desktop installed on your computer (run the docker -v command to verify if you have the docker desktop installed); if not, install it from here.
  • An Appwrite instance running on your computer. Check out the documentation to create a local Appwrite instance. You can also install using digital ocean or gitpod.

Setting up Nuxt3

Run the command below to set up a Nuxt3 app:

npx nuxi init appwrite-nuxt-app
Enter fullscreen mode Exit fullscreen mode

Change directory:

cd appwrite-nuxt-app
Enter fullscreen mode Exit fullscreen mode

Install the dependencies:

npm install
Enter fullscreen mode Exit fullscreen mode

Now you'll be able to start a Nuxt3 development server with this command:

npm run dev 
Enter fullscreen mode Exit fullscreen mode

A browser window will automatically open at http://localhost:3000.

Also, install these dependencies below as you will need them later in the course of this tutorial:

npm install @nuxtjs/tailwindcss @tailvue/nuxt @iconify/vue appwrite --save
Enter fullscreen mode Exit fullscreen mode

Setting up Appwrite Database

Follow the instructions below to create the Database:

  1. Go to your Appwrite Console.
  2. Click on Create Project and give your project a name.
  3. On the left side of the dashboard, click on Database and create a database.
  4. Click on the Add Collection button. This action redirects us to a Permissions page.
  5. At the Collection level, assign Read and Write Access with role:all.

AppWrite dashboard

  1. On the right of your Permissions page, copy the Collection ID, which you would need to perform operations on documents in this collection.
  2. Next, go to the attributes tab to create the properties you want a document to have.
  3. Click on Add Attribute and then New String Attribute. Give it an ID called message and a Size of your choice.

AppWrite dashboard

Creating the Application

Open App.vue, located at the root of the project folder, and replace the content with the code below. It is a template with two buttons referencing two methods you would create later.



<template>
      <div>
        <button
          type="button"
          class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
          @click="createDocument"
        >
          Create Document
        </button>
        <button
          type="button"
          class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded"
          @click="deleteDocument"
        >
          Delete Document
        </button>
      </div>
</template>


Enter fullscreen mode Exit fullscreen mode

Open a script tag in the same file and then import and initialize the Appwrite modules like so:



    ...

    <script>
    import { Client, Account, Databases } from 'appwrite';

    const client = new Client();
    client
      .setEndpoint('http://localhost/v1') // Your Appwrite Endpoint
      .setProject('[PROJECT_ID]');

    const account = new Account(client);
    const database = new Databases(client, '[DATABASE_ID]');
    </script>


Enter fullscreen mode Exit fullscreen mode

You can get the PROJECT_ID and DATABASE_ID from your Appwrite console.

Interacting with the Database

Only signed-in users can interact with the Appwrite database but you can create an anonymous session as a workaround for this.

In the same App.vue file, you can create an anonymous session using the method below. Add the code in the script tag, just below the initialization of the Appwrite modules:



    ...

    account.createAnonymousSession().then(
      (response) => {
        console.log(response);
      },
      (error) => {
        console.log(error);
      }
    );

    </script>


Enter fullscreen mode Exit fullscreen mode

Setting up a Realtime Connection

In the App.vue file, export a default object. The data method returns an object with a message property and gives it a 'Welcome!' value.

In the mounted method, check to make sure there is an active user session. If so, set up a real-time connection with Appwrite by subscribing to the documents channel.

In the callback, assign the message variable a new value containing the timestamp of the database event.



    ...

    export default {
      data() {
        return {
          message: 'Welcome!',
        };
      },
      mounted() {
        if (account.get !== null) {
          try {
            client.subscribe('documents', (response) => {
              this.message = `This event was called at ${response.timestamp}`;
            });
          } catch (error) {
            console.log(error, 'error');
          }
        }
      },
    };
    </script>


Enter fullscreen mode Exit fullscreen mode

Listing the Documents

In the current file, add a new property called notifications and give it a value of an empty array. In the Nuxt.js methods object, define a new method called listDocuments and paste the code below in it:



    ...
    export default {
      data() {
        return {
          message: 'Welcome!',
          notifications: [],
        };
      },
      methods: {
        async listDocuments() {
          try {
            this.notifications = [];
            let response = await database.listDocuments('[COLLECTION_ID]');
            response.documents.map((document) =>
              this.notifications.push(document.$id)
            );
            console.log(this.notifications);
          } catch (error) {
            console.log(error);
          }
      },
      mounted() {
        ...
    </script>


Enter fullscreen mode Exit fullscreen mode

This method does the following:

  1. First, empty the notifications array. Then, you use Appwrite’s listDocuments method with your COLLECTION_ID to get all the documents.
  2. Map through the documents, push them into the notifications array, and log them to the console.

Creating new Documents

Create a new method called createDocument just below the listDocuments method and paste the code below into it:



    async createDocument() {
      try {
        await database.createDocument('[COLLECTION_ID]', 'unique()', {
          message: 'Welcome!',
        });
        this.listDocuments();
      } catch (error) {
        console.log(error);
      }
    },


Enter fullscreen mode Exit fullscreen mode

This method does the following:

  1. The Appwrite createDocument method takes in the collection ID and the document payload to create a new document. The unique() param generates a random document ID.
  2. Calls the listDocuments method.
  3. Logs an error if creating the document fails.

Deleting Documents in the Collection

Create a new method called deleteDocument and paste the code below into it:



    async deleteDocument() {
      if (this.notifications.length > 0) {
        try {
          let documentID = this.notifications[this.notifications.length - 1];
          await database.deleteDocument('[COLLECTION_ID]', documentID);
          this.listDocuments();
        } catch (error) {
          console.log(error);
        }
      } else {
        alert('database is empty');
      }
    },


Enter fullscreen mode Exit fullscreen mode

The deleteDocument function does the following:

  1. Checks if the notifications array length is greater than zero. If it is, it gets the last document ID in the array and stores it in a documentID variable. If the notifications array length is less than zero, the method shows an alert that the Database is empty.
  2. Deletes the document using the Appwrite deleteDocument() method. This deleteDocument method receives a collection ID and the document ID parameter.
  3. Calls the listDocuments function.
  4. Logs an error if deleting the document fails.

After following all these steps, your App.vue file should look like this:



    <template>
      <div>
        <button
          type="button"
          class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
          @click="createDocument"
        >
          Create Document
        </button>
        <button
          type="button"
          class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded"
          @click="deleteDocument"
        >
          Delete Document
        </button>
      </div>
    </template>

    <script>
    import { Client, Account, Databases } from 'appwrite';
    const client = new Client();
    client
      .setEndpoint('http://localhost/v1') // Your Appwrite Endpoint
      .setProject('[PROJECT_ID]');

    const account = new Account(client);
    const database = new Databases(client, '[DATABASE_ID]');

    account.createAnonymousSession().then(
      (response) => {
        console.log(response);
      },
      (error) => {
        console.log(error);
      }
    );

    export default {
      data() {
        return {
          message: 'Welcome!',
          notifications: [],
        };
      },
      methods: {
        async listDocuments() {
          try {
            this.notifications = [];
            let response = await database.listDocuments('[COLLECTION_ID]');
            response.documents.map((document) =>
              this.notifications.push(document.$id)
            );
            console.log(this.notifications);
          } catch (error) {
            console.log(error);
          }
        },
        async createDocument() {
          try {
            await database.createDocument('[COLLECTION_ID]', 'unique()', {
              message: 'Welcome!',
            });
            this.listDocuments();
          } catch (error) {
            console.log(error);
          }
        },
        async deleteDocument() {
          if (this.notifications.length > 0) {
            try {
              let documentID = this.notifications[this.notifications.length - 1];
              await database.deleteDocument('[COLLECTION_ID]', documentID);
              this.listDocuments();
            } catch (error) {
              console.log(error);
            }
          } else {
            alert('database is empty');
          }
        },
      },
      mounted() {
        if (account.get !== null) {
          try {
            client.subscribe('documents', (response) => {
              this.message = `This event was called at ${response.timestamp}`;
            });
          } catch (error) {
            console.log(error, 'error');
          }
        }
      },
    };
    </script>


Enter fullscreen mode Exit fullscreen mode

Creating the Notifications

In the nuxt.config.ts, replace the contents with the code below:



    import { defineNuxtConfig } from 'nuxt';

    // https://v3.nuxtjs.org/api/configuration/nuxt.config
    export default defineNuxtConfig({
      meta: [{ name: 'description', content: 'My Appwrite site.' }],
      modules: ['@nuxtjs/tailwindcss', '@tailvue/nuxt'],
    });


Enter fullscreen mode Exit fullscreen mode

'@nuxtjs/tailwindcss' and '@tailvue/nuxt' are the packages you installed at the beginning of this article. You'll use them to create the toast notifications.

Create a new file in the project root called tailwind.config.js and paste the code below into it:



    /** @type {import('tailwindcss').Config} */
    module.exports = {
      content: ['node_modules/tailvue/dist/tailvue.es.js'],
      theme: {
        extend: {},
      },
      plugins: [],
    };


Enter fullscreen mode Exit fullscreen mode

The toast package uses tailwindcss.

Showing the Notifications

First, add a new property in the data, return the object called toast and give it the value of an empty string. Destructure $toast from the useNuxtApp in the mounted method and assign it to the **toast** ****property you added to the data method:



    mounted() {
        const { $toast } = useNuxtApp();
        this.toast = $toast;
        ...
      },


Enter fullscreen mode Exit fullscreen mode

In a watch method which Nuxt.js provides, watch for changes on the message property and then show the new message in a toast:




    watch: {
      message: function (message) {
        this.toast.show({
          type: 'success',
          message,
          timeout: 2,
        });
      },
    },


Enter fullscreen mode Exit fullscreen mode

The final App.vue final should look like this:



    <template>
      <div>
        <button
          type="button"
          class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
          @click="createDocument"
        >
          Create Document
        </button>
        <button
          type="button"
          class="bg-red-500 hover:bg-red-700 text-white font-bold py-2 px-4 rounded"
          @click="deleteDocument"
        >
          Delete Document
        </button>
      </div>
    </template>

    <script>
    import { Client, Account, Databases } from 'appwrite';
    const client = new Client();
    client
      .setEndpoint('http://localhost/v1') // Your Appwrite Endpoint
      .setProject('[PROJECT_ID]');

    const account = new Account(client);
    const database = new Databases(client, '[DATABASE_ID]');

    account.createAnonymousSession().then(
      (response) => {
        console.log(response);
      },
      (error) => {
        console.log(error);
      }
    );

    export default {
      data() {
        return {
          message: 'Welcome!',
          notifications: [],
          toast: '',
        };
      },
      methods: {
        async listDocuments() {
          try {
            this.notifications = [];
            let response = await database.listDocuments('[COLLECTION_ID]');
            response.documents.map((document) =>
              this.notifications.push(document.$id)
            );
            console.log(this.notifications);
          } catch (error) {
            console.log(error);
          }
        },
        async createDocument() {
          try {
            await database.createDocument('[COLLECTION_ID]', 'unique()', {
              message: 'Welcome!',
            });
            this.listDocuments();
          } catch (error) {
            console.log(error);
          }
        },
        async deleteDocument() {
          if (this.notifications.length > 0) {
            try {
              let documentID = this.notifications[this.notifications.length - 1];
              await database.deleteDocument('[COLLECTION_ID]', documentID);
              this.listDocuments();
            } catch (error) {
              console.log(error);
            }
          } else {
            alert('database is empty');
          }
        },
      },
      mounted() {
        const { $toast } = useNuxtApp();
        this.toast = $toast;

        if (account.get !== null) {
          try {
            client.subscribe('documents', (response) => {
              this.message = `This event was called at ${response.timestamp}`;
            });
          } catch (error) {
            console.log(error, 'error');
          }
        }
      },
      watch: {
        message: function (message) {
          this.toast.show({
            type: 'success',
            message,
            timeout: 2,
          });
        },
      },
    };
    </script>


Enter fullscreen mode Exit fullscreen mode

Here is what the app should look like:

final app look

Conclusion

This article showed you how Appwrite's real-time feature creates notifications in Nuxt3.

Resources

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .