How To Set Up User Notifications For Your Django App - Part 2

Introduction

Notifications provide updates about the activity on an application. In this guide, you will learn how to set up simple notifications for your Django application. This is Part 2 of this series.

In this tutorial, we will be focusing on creating components for our frontend app.

Tutorials in this series:

Part 1 - Setting up the backend

Part 2 - Frontend components with TailwindCSS and AlpineJS (this tutorial)

Prerequisites

To complete this tutorial, you will need

LEVEL - 💻 Beginner to Intermediate

Basic knowledge of Django, Django Templates, and Alpine JS. This is not a step-by-step tutorial. We will cover high-level concepts. You may need to do further reading on your own.

Recap

In the last tutorial, we set up a backend to store and manage the business logic for user notification/activity. We will pick up where we left off and create frontend components for displaying user information.

As a reminder, our goal is to create a simple, reactive dropdown component that shows user notifications or activity. Looks something like this 👇.

user_notifications dropdown component

Folder structure & Base Templates

It is considered best practice to set up a generic base template from which other pages can inherit from. We use template inheritance to write clean DRY Html templates which can easily be extended. We have written an in-depth post about Django templates and best practices for structuring project templates.

Project folder structure

├── my_proj
|   └── config ...
|   └── apps # From Part 1
├── templates
|   ├── base.html # Step 1
|   ├── dashboard.html #  Step 3 - bringing it together
|   └── components
|      ├── navbar.html # Step 2 - Creating components
|      ├── user_notifications.html # Step 2 - Creating components
└── manage.py

Set up our generic template - base

base.html

<!--base.html-->
<html>
<head>
    <!--TailwindCSS-->
    <link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
    
    <!--Alpinejs--->
    <script defer src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v3.0.1/dist/alpine.min.js"></script>
    
     <!--Other common js for example --->
    function getCookie(name) {
    let cookieValue = null;
    if (document.cookie && document.cookie !== '') {
        const cookies = document.cookie.split(';');
        for (let i = 0; i < cookies.length; i++) {
            const cookie = cookies[i].trim();
            // Does this cookie string begin with the name we want?
            if (cookie.substring(0, name.length + 1) === (name + '=')) {
                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                break;
            }
        }
      }
      return cookieValue;
    }
</head>
<body>
    {% include "components/navbar.html" %}
    {% block content %}{% endblock %}
    {% block footer_js %}{% endblock %}
</body>
</html>

Notes

  • Our base template includes styles and scripts that we will use for creating our components. 
  • Notice the {% include .. %} tag which allows us to inject Html templates or components that we have defined in other files. 
  • {%block %} tag  - we can use this to override parent tags from child templates. See our post on Django templates if you need to learn more about this. 
  • We are using the AlpineJS library, a lightweight Javascript framework to provide reactivity to our page. 
  • TailwindCSS for styling, in this example, we are using the CDN version. In your project, you may want to use a build tool to optimize file size. We will add a post on how you can set that up later. For now, you can refer to the Tailwind docs.

Create your components

templates/components/navbar.html

<!--navbar.html-->
{% load static i18n %}
 
<div class="w-screen flex flex-row items-center p-1 justify-between bg-white shadow-xs">

  <div class="ml-8 text-lg text-gray-700 hidden md:flex">My Website</div>

  <div class="flex flex-row-reverse mr-8 hidden md:flex">
      {% if not request.user.is_authenticated %}
        <a class="text-gray-700 text-center bg-gray-400 px-4 py-2 m-2" href="{% url 'account_signup' %}">{% trans "Sign Up" %}</a>
        <a class="text-gray-700 text-center bg-gray-400 px-4 py-2 m-2" href="{% url 'account_login' %}">{% trans "Sign In" %}</a>
      {% else %}
        {% include "components/dropdowns/user_notifications.html" %}
      {% endif %}
  </div>
 
</div>

Notes

  • This is a simple navbar component.
  • We use a number of tags on this template including the {% url ".." %} tag for reversing URLs, {% if .. %}{% else %}{% endif %} for control flow and {%include ""%} once more for our user_notifications component defined below.
  • Our user notifications component will only be included if the user is authenticated.

templates/components/user_notifications.html

<div class="relative flex align-center justify-center mr-4"
     x-data="user_notification_dropdown"
>
<!-- navbar button -->  
<button @click="toggle()"
          class="p-0 rounded-full focus:outline-none focus:ring-indigo">
    <span class="sr-only">View notifications</span>
    <svg class="h-6 w-6 m-1" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"
         aria-hidden="true">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
            d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002
             6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 
             6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 
             0 11-6 0v-1m6 0H9"/>
    </svg>
</button>

  <!-- purple dot showing user has unread notifications -->
  <div x-show="hasUnreadNotifications" class="absolute right-0 p-1 
       bg-indigo-300 rounded-full bottom-3 animate-ping"
       x-cloak></div>
  <div x-show="hasUnreadNotifications"
       class="absolute right-0 p-1 bg-indigo-500 border border-white r
       rounded-full bottom-3" x-cloak></div>

  <div x-show="open"  
       x-cloak
       @click.away="open = false"
       class="absolute z-10 mt-12 w-screen max-w-md sm:px-0 transform 
       -translate-x-48"
    >
    <div class="rounded-lg shadow-lg ring-1 ring-black ring-opacity-5 
    max-h-dash-sm overflow-y-scroll relative">
      <div class="px-5 py-5 bg-gray-50 space-y-6 sm:flex sm:space-y-0 
      sm:space-x-10 sm:px-8 sticky top-0 z-30">
        <div class="flow-root">
          <a href="#"
             class="-m-3 p-3 flex items-center rounded-md text-base 
             font-medium text-gray-900 hover:bg-gray-100">
            <span class="ml-3">Activity</span>
          </a>
        </div>
      </div>
      <!-- list notifications --> 
      <div class="relative grid gap-6 bg-white px-5 py-6 sm:gap-8 sm:p-8">
        <template x-for="(item, index) in notifications">
           <a @click="" class="-m-3 p-3 flex justify-between rounded-lg 
           hover:bg-gray-50">
              <div class="ml-4">
                <p class="text-base font-medium text-gray-900" 
                x-text="item.verb"></p>
                <p class="mt-1 text-sm text-gray-500" 
                x-text="`From ${item.actor}`"></p>
              </div>
              <div>
                <template x-if="item.unread">
                  <div class="badge badge-sm badge-success">unread</div>
                </template>
                <template x-if="!item.unread">
                  <div class="badge badge-sm badge-ghost">read</div>
                </template>
              </div>
           </a>
        </template>
        <template x-if='notifications.length === 0'>
          <a class="-m-3 p-3 flex justify-between rounded-lg
           hover:bg-gray-50">
              <div class="ml-4">
                <p class="text-base font-medium text-gray-900">
                You have notifications</p>
                <p class="mt-1 text-sm text-gray-500">No Notifications</p>
              </div>
           </a>
        </template>
      </div>
      
      <div class="px-2 py-2 space-y-6 flex bg-white justify-center
       sm:space-y-0 sm:space-x-10 sm:px-8 z-30 border-t">
        <div class="flow-root">
          <a href="{% url "view-name-to-reverse-to-some-view--here" %}"
             class="-m-3 p-3 flex items-center rounded-md blue-500
              font-medium hover:bg-gray-100">
            <span class="ml-3">Go to notifications page</span>
          </a>
        </div>
      </div>
    </div>
  </div>
</div>

Notes

  • The template includes several AlpineJS directives x-data, x-show, @click, x-if. These attributes help us compose component behaviour directly in our markup. To explain briefly
    • x-data - declares 'user_notification_dropdown component and its data for an HTML block. In our case, we have declared the actual component in a separate section of the page within our scripts section (see step 3 below).
    • x-show="open" - show dropdown when 'open' == true
    • @click.away="open = false" & @click="open = !open" - listen to click events and either set the 'open' variable to true or false depending on the action.
    • x-for="(item, index) in notifications - display notifications that have been fetched from the backend. Notice that this is used on the template tag.

Connect your component to the backend

Now that we have declared our HTML, let's add the logic to fetch user notifications from the backend.

<script>
    const csrf_token = getCookie('csrftoken');
    document.addEventListener('alpine:initializing', () => {
        Alpine.data('user_notifications_dropdown', () => ({
            
            /**
              Component data
            **/
            open: false, //drop down state
            hasUnreadNotifications: false, // purple dot will show if this is true
            notifications: [], // list of notifications
            
            /**
              Component methods
             **/

            init(){ 
              fetch('inbox/notifications/api/all_list/', {
                method: 'GET',
                credentials: 'same-origin',
                headers:{
                  'Accept': 'application/json',
                  'X-Requested-With': 'XMLHttpRequest', 
                  'X-CSRFToken': csrftoken, 
                },
                return response.json() 
              }).then(data => {
                  this.notifications = data.all_list
                  this.hasUnreadNotifications = data.all_list.(
                  item => item.unread === true).length > 0
              }).catch(err => console.log(err)
            },
            toggle() {
                this.open = ! this.open
            }
        }))
    })
</script>

Notes

  • getCookie - Checkout the Django docs for more information on ajax requests.
  • Alpine.data('user_notifications_dropdown', - declares a new Alpine component and related data and methods.
  • init() - will fetch the notifications component and add them to the notifications variable. Notice how we have included the csrftoken in the header.
  • toggle() - toggle user_notifications dropdown

Bringing it together

Finally, let's set up our page so we see the fruits of our labour.

Add another route to your urls.py file. We set this up in the previous tutorial

urls.py


from django.urls import path
from django.views.generic import TemplateView
import notifications.urls

urlpatterns = [
    ...
    path('inbox/notifications/', include(notifications.urls, namespace='notifications')),
    path( "/dashboard",
        TemplateView.as_view(template_name="layouts/dashboard.html"),
        name="dashboard",
]

templates/layouts/dashboard.html

{% extends 'base.html' %}

{% block title %}Dashboard {% endblock %}

{% block content %}
  {# Include other content here for your dashboard #}
  <h1>Dashboard</h1>
{% end block}

{% block footer_js %}
 // include the scripts tag from above.
 <script>
    const csrf_token = getCookie('csrftoken');
    document.addEventListener('alpine:initializing', () => {
        Alpine.data('user_notifications_dropdown', () => ({
        .....
        SEE STEP #3 ABOVE FOR THIS SECTION
        ......
 </script>
{% endblock %}

Notes

  • To keep things simple, we have included our script in our dashboard.html template. In practice, we would keep this logic in a separate .js file.
  • Our dashboard.html file extends the base.html file we created in Step 1. This file can also be used as a base template for other pages that use the same layout.

Are you building a SaaS product and want to launch like ASAP? Check out the Vanty Starter Kit. Our Django boilerplate has everything you need to launch your app today!

Learn more →

Conclusion

And that's it. We now have a working user_notifications dropdown component. As our app grows we can easily extend this to improve functionality, for example, add real-time capability using WebSockets or improve UX e.g. mark notifications as 'read' on hover. If you want to learn how to quickly deploy a production-ready WebSockets server, check out our article on this.

Why we used TailwindCSS and AlpineJS

  • Using AlpineJS to add reactivity to your Django templates is a trivial task. This can be a huge time saver when you are prototyping. The Django-notifications library comes with a ton of features including template tags for live notifications, badges, etc. We opted out of those as they are based on bootstrap, we also prefer to have full control over how the frontend template components are designed.

Further reading

  • AlpineJS docs - Getting started with AlpineJS TailwindCSS docs - 
  • Getting started with tailwindcss

Last Updated 25 Sep 2022