Showing posts with label jQuery. Show all posts
Showing posts with label jQuery. Show all posts

Friday, January 10, 2025

Custom JavaScript Plugin

 

1. Introduction

Plugins are a powerful mechanism for avoiding repetitive tasks. Once you develop a plugin, you can reuse it in multiple projects. That’s why learning how to build custom plugins is a valuable skill for developers. It enables you to extend the functionality of existing software, tailor solutions to specific requirements, and optimize your workflow.

In this article, we’ll walk you through the fundamental steps of developing and using a custom plugin in JavaScript. We will explain an example of a custom plugin that creates a tooltip that displays the data of the attached element. The tooltip opens and closes if you click on the corresponding element. The full example of this custom tooltip and a demo of its usage is on GitHub:

https://github.com/noura-github/custom-plugin-tooltip-blog


For more examples, including another custom tooltip implementation, you can explore our repository on GitHub:

https://github.com/noura-github/custom-plugin-translate-blog


This custom plugin enables a tooltip that displays the translation of a word when hovered over. The translation is rendered in the language selected from a dropdown menu.


2. JavaScript Plugin

A jQuery plugin is a reusable code which adds new functionalities to jQuery. Plugins are a kind of pre-built solution for common tasks. They help reduce repetitive codes. They utilize existing libraries and frameworks to streamline development and reduce redundant code. 


Technically, a plugin is code written in a JavaScript file and consists of one or multiple functions that extend the jQuery object. To use the plugin’s functionality, you can call a custom method on a jQuery object.


2.2. Use Cases for jQuery Plugins

jQuery plugins can be used for a variety of purposes, including:

UI Components and Widgets: Modals, tables, date pickers, etc.

Form Enhancements: Form validation, processing, auto-complete, etc.

DOM manipulation and traversal

Effects and Animations: Fade effects, etc.

Ajax Enhancements: Simplified data loading and displaying.


2.3. Plugin Definition

We usually start by creating a new JavaScript file to create a new plugin. This file defines the plugin code, including its properties and functions. Then, we attach these functions to the jQuery object using the $.fn property.


Here’s a simple example of a jQuery plugin that adds a ‘myPlugin()’ method:


(function($) {

  $.fn.myPlugin = function(options) {

    this.css('background-color', options.bgColor);

    this.css('border', options.border);

    this.css('font-size', options.fontSize);

    return this;

  };

})(jQuery);


Now we can attach the plugin ‘myPlugin()’ to an element (paragraph) like this:


$("p").myPlugin({

     bgColor: "red",

     fontSize: "18px",

     border: "2px solid blue"

});


3. Example of Custom Plugin

The plugin we want to suggest to you in this section is useful for creating visually rich and interactive tooltips. The tooltip opens and closes when you click on the corresponding element.


Note. Here, we focus exclusively on the key aspects of defining and using a custom plugin. For the complete example, visit the GitHub repository:

https://github.com/noura-github/custom-plugin-tooltip-blog


3.1. Tooltip Plugin Code

The tooltip plugin code is developed in jQuery. But for the demonstration, we need a back-end and front-end code. The back-end code is written in Python to send dummy data from a database to the front-end code. We also need HTML and CSS code to display and style the data respectively. Finally, the JavaScript code is used to create the tooltip plugin and demonstrate how to use it.


Python Code

For this example, we need some data. So, we created a Python SQLite database named ‘custom_plugin.db’ with two tables: ‘employee’ and ‘files’. The table ‘employee’ stores the data about an employee such as their first name, last name, email, and so on. 

The ‘files’ table stores a file with a file name and description. A file can be associated with an employee using the foreign key ‘file_id’ which corresponds to a primary key ‘id’ in the ‘files’ table. Here is the code for the ‘create_database()’ function:


# Function to create the database and tables

def create_database():

    # Connect to SQLite database (or create it if it doesn't exist)

    conn = sqlite3.connect('custom_plugin.db')

    cursor = conn.cursor()


    # Create the files table

    cursor.execute('''

    CREATE TABLE IF NOT EXISTS files (

        id INTEGER PRIMARY KEY AUTOINCREMENT,

        filename TEXT NOT NULL,

        description TEXT,

        file BLOB

    )

    ''')

    print("Table 'files' created successfully.")


    # Create the employee table

    cursor.execute('''

    CREATE TABLE IF NOT EXISTS employee (

        id INTEGER PRIMARY KEY,

        firstname TEXT NOT NULL,

        lastname TEXT NOT NULL,

        email TEXT NOT NULL,

        phone TEXT NOT NULL,

        departmentId INTEGER NOT NULL,

        departmentName TEXT NOT NULL,

        companyName TEXT NOT NULL,

        file_id INTEGER,

        FOREIGN KEY (file_id) REFERENCES files (id)

    )

    ''')

    print("Table 'employee' created successfully.")


    # Commit changes and close the connection

    conn.commit()

    conn.close()


We need then to insert some dummy data. Thus, to add some data to the ‘files’ table, we suggest the following functions:


# Function to insert a file into the database

def insert_file(filename, description, filepath):

    # Connect to SQLite database

    conn = sqlite3.connect('custom_plugin.db')

    cursor = conn.cursor()


    # Read the file as binary data

    with open(filepath, 'rb') as file:

        file_data = file.read()


    # Insert the file into the table

    cursor.execute('''

        INSERT INTO files (filename, description, file)

        VALUES (?, ?, ?)

    ''', (filename, description, file_data))


    print(f"File '{filename}' inserted successfully.")


    # Commit changes and close the connection

    conn.commit()

    conn.close()


# Function to populate the files table

def populate_files():

    # Example: Insert a file into the table

    insert_file('Dock.jpg', 'A dock over a lake at night.', 'static/images/Dock.jpg')

    insert_file('Fields.jpg', 'A road in the fields of flowers leading to the mountains.', 'static/images/Fields.jpg')

    insert_file('Waterfall.jpg', 'A Cascading Waterfall under pink trees.', 'static/images/Waterfall.jpg')

    insert_file('Lake.jpg', 'A a blue water lake.', 'static/images/Lake.jpg')


The ‘insert_file()’ Function: This function inserts a file into the SQLite database. It reads the file as binary data, then executes an SQL INSERT statement to store the file’s name, description, and contents in the ‘files’ table.


The ‘populate_files()’ Function: This function inserts four image files into the database using the ‘insert_file()’ function, with their respective names, descriptions, and file paths.


In this Python code, we need a ‘get_data()’ function as a Flask route that handles GET requests to the ‘/data’ endpoint by returning the employee data dictionary as a JSON object.


@app.route('/data', methods=['GET'])

def get_data():

    # Sample data to send to the frontend

    return jsonify(get_employee_data())


Moreover, we need another Flask API endpoint that handles a POST request to the ‘/imagedata’ route. The endpoint expects a JSON payload with an ‘id’ field, which is used to retrieve a file’s binary data.


@app.route('/imagedata', methods=['POST'])

def get_image_data():

    # Ensure request contains JSON data

    data = request.get_json()


    if not data or 'id' not in data:

        return Response("Missing 'id' in request body", status=400)


    file_id = data.get('id')


    # Your logic to retrieve the file data

    file_row = find_image_file(file_id)

    if not file_row or not file_row[0]:

        return Response("File not found", status=404)


    blob_data = file_row[0]


    # Return binary data with appropriate headers

    response = Response(blob_data, mimetype='application/octet-stream')

    response.headers['Content-Disposition'] = 'attachment; filename="output_file.bin"'

    return response


The ‘get_image_data()’ Function: This function handles POST requests to the ‘/imagedata’ route, validates the request for a file ‘id’, retrieves the corresponding binary file data from the database, and returns it as a downloadable response.


The ‘find_image_file()’ Function: This function connects to the ‘custom_plugin.db’ SQLite database and retrieves the file associated with the given ‘file_id’ from the files table.


def find_image_file(file_id):

    # Connect to SQLite database

    conn = sqlite3.connect('custom_plugin.db')

    cursor = conn.cursor()


    # Query the table for the file

    cursor.execute('''

        SELECT file FROM files WHERE id = ?

    ''', (file_id,))


    # Fetch the file data

    file_data = cursor.fetchone()


    # Close the connection

    conn.close()


    return file_data


HTML Code

In HTML code, we need a table to display the data:

<table id="dataTable">

    <thead>

        <tr>

            <th>First Name</th>

            <th>Last Name</th>

            <th>Department Name</th>

            <th>Company Name</th>

        </tr>

    </thead>

    <tbody class="containment">

    </tbody>

</table>


JavaScript Code

We have crafted two files in JavaScript. The first one, tooltipdata.js, houses our custom plugin, while the other file, main.js, is designed to populate the data and activate the ‘tooltipdata’ plugin. Let’s dive into the details of the custom plugin and how to utilize it.


Custom Tooltip Plugin. An overview of the code for this plugin is as follows:


(function($) {


    function __tooltipdata(elem, options) {

        var that = this;

        this.id = options.id;

        this.title = options.title;

        this.customClass = options.customClass;

        this.containerClass = options.containerClass;

        this.tooltipImgClass = options.tooltipImgClass;

        this.headerClass = options.headerClass;

        this.menuClass = options.menuClass;

        this.contentClass = options.contentClass;

        this.footerClass = options.footerClass;


// Rest of the code (properties, methods, or events handler)

...

    }

    // Plugin function

    $.fn.tooltipData = function(data) {

        return this.each(function() {

            // Check if the element already has tooltip data initialized

            if (!$.data(this, "tooltipdata")) {

                // Initialize the tooltip data and store it in the element's data

                $.data(this, "tooltipdata", new __tooltipdata($(this), data));

            }

        });

    };

}(jQuery));


Plugin Properties. The code defines a function named ‘__tooltipdata’ that takes two arguments: ‘elem’ and ‘options’. This function creates and manages tooltip data for a given element. The ‘__tooltipdata’ function sets up several properties for the tooltip data, including:

id: an identifier for image file of the tooltip.

title: the title of the tooltip

customClass: a custom CSS class for the tooltip.

containerClass: a CSS class for the tooltip container.

tooltipImgClass: a CSS class for the tooltip image.

headerClass: a CSS class for the tooltip header.

menuClass: a CSS class for the tooltip menu.

contentClass: a CSS class for the tooltip content.

footerClass: a CSS class for the tooltip footer.

image: the URL of the tooltip image.

info_head, info_foot, and info_menu: HTML content for the tooltip header, footer, and menu, respectively.


Tooltip Creation. The code creates a tooltip element using jQuery and appends it to the elem element. It also sets up event listeners for the tooltip, including a click event handler that toggles the tooltip’s visibility.


elem.tooltip({

  content: this.content,

  position: this.position,

  show: this.show,

  hide: this.hide,

  track: this.track,

  classes: this.classes,

  open: function(event, ui) {

    // code here

  }

});


In the event handler of the click event, if the title property is set, the code checks the ‘isOpened’ property of that object. This property is used to keep track of whether the tooltip is currently open or not. If the ‘isOpened’ property is true, the code sets it to false and hides the tooltip using the tooltip("disable") method. This method is part of the tooltip plugin and is used to disable the tooltip.


$(elem).click(function () {

   if (that.title) {

       if (that.isOpened) {

           that.isOpened = false;

           // Hide the tooltip using the API

           $(elem).tooltip("disable");

       } else {

           that.isOpened = true;

           // Temporarily update the title and enable the tooltip

           $(elem).attr("title", that.title).tooltip("enable");

           // Show the tooltip programmatically

           $(elem).tooltip("open");

       }

   }

});


The function ‘onOpen()’: If the image is not cached, the code makes an AJAX request to the URL specified in the ‘imageurl’ property of the ‘tooltipdata’ plugin. The request is made using the $.ajax method and has the following properties:

type: The request method is set to POST.

url: The request URL is set to the value of the ‘imageurl’ property.

dataType: The expected data type of the response is set to binary.

processData: This property is set to false. This means that the data sent in the request body will not be processed by jQuery.

contentType: This property is set to ‘application/json’, which means that the request body will be sent as JSON data.

xhrFields: This property is set to an object with a single property ‘responseType’ set to blob. This means that the response will be treated as a binary blob.

data: The request body is set to a JSON object with a single property ‘id’ set to the value of the ‘id’ property of the plugin.

When the response is received, the code checks if the response was successful. If it was, the code creates a URL for the response blob using the ‘URL.createObjectURL’ method and sets the image property to the created URL. The code also caches the image URL in the ‘imageCache’ object using the ‘id’ property as the key. Finally, the code updates the ‘src’ attribute of the ‘tooltipImg’ element with the created image URL.


this.onOpen = function() {

   if (that.image) return;

   if (that.imageCache[that.id]) {

       that.image = that.imageCache[that.id];

       that.tooltipImg.attr("src", that.image);

   } else {

       $.ajax({

          type: 'POST',

          url: that.imageurl,

          dataType: 'binary',

          processData: false,

          contentType: 'application/json',

          xhrFields: { responseType: 'blob' },

          data: JSON.stringify({ 'id': that.id }),

          success: function(blob) {

              // Create a URL for the blob

              var imageUrl = URL.createObjectURL(blob);

              that.image = imageUrl;

             // Cache the image

             that.imageCache[that.id] = imageUrl;

             // Set the src attribute of the img element

             that.tooltipImg.attr("src", imageUrl);

           },

           error: function(xhr, status, error) {

              console.error("Error: ", error, "Status: ", status);

           }

        });

    }

}


Plugin Function. The code defines a plugin function named ‘tooltipData’ that can be used to create and manage tooltips for a given element.


To work with this plugin, we first need to load some data. For this, we create a function called ‘getEmployeeData()’ that we call once the document is ready as follows:


$(document).ready(function () {

    getEmployeeData();

});


The ‘getEmployeeData()’ Function: This function makes a GET request to the ‘/data’ endpoint to retrieve employee data in JSON format. Upon successful retrieval, it calls two other functions: ‘displayEmployeeData()’ to display the received data and ‘initializeTooltipData()’ to set up tooltip data.


function getEmployeeData() {

    $.ajax({

        type: 'GET',

        url: '/data',

        dataType: 'json',

        success: function(data) {

            displayEmployeeData(data);

            initializeTooltipData();

        },

        error: function(xhr, status, error) {

            console.error("Error: ", error, "Status: ", status);

        }

    });

}


The ‘displayEmployeeData()’ Function: This function takes an array of employee data and populates the table with the data. It loops through each employee object in the data array. For each employee, it appends a new table row (tr) to the table body. Within the table row, it creates four table cells (td) containing the employee’s: First name (with a tooltip trigger), Last name (with a tooltip trigger), Department name, and Company name. The tooltip triggers (span elements with class ‘tooltip_trigger’) contain additional data attributes (e.g. data-firstname, data-lastname, etc.) that will be used to display more information about the employee when the tooltip is triggered.

function displayEmployeeData(data) {

    const $tableBody = $("#dataTable tbody");

    $tableBody.empty();


    $.each(data, function(index, employee) {

        $tableBody.append(`

            <tr>

                <td><span class="tooltip_trigger" data-firstname="${employee.firstname}" data-lastname="${employee.lastname}" data-id="${employee.file_id}" data-filename="${employee.filename}" data-description="${employee.description}" data-email="${employee.email}" data-phone="${employee.phone}">${employee.firstname}</span></td>

                <td><span class="tooltip_trigger" data-firstname="${employee.firstname}" data-lastname="${employee.lastname}" data-id="${employee.file_id}" data-filename="${employee.filename}" data-description="${employee.description}" data-email="${employee.email}" data-phone="${employee.phone}">${employee.lastname}</span></td>

                <td><span>${employee.departmentName}</span></td>

                <td><span>${employee.companyName}</span></td>

            </tr>

        `);

    });

}


The ‘initializeTooltipData()’ Function: This function initializes tooltips for each employee data row in the table. It selects all span elements within the table body that contain employee data. For each span element, it extracts various data attributes (e.g. firstname, lastname, email, phone, etc.). If the firstname or lastname attribute is present, it creates a tooltip with the following content:

- Header with employee name.

- Footer with file title and description.

- Menu with email and phone number.

The function then initializes a tooltip for each span element using the ‘tooltipData’ method, passing in various options for the tooltip’s behavior, appearance, and content.

function initializeTooltipData() {

    const $tableBody = $("#dataTable tbody");

    let $spanElem = $tableBody

                .children("tr")

                .find("td")

                .find("span");


    $spanElem.each(function () {

        const $span = $(this);

        let firstname = $span.data("firstname");

        let lastname = $span.data("lastname");

        if (firstname || lastname) {

            let email = $span.data("email");

            let phone = $span.data("phone");

            let fileName = $span.data("filename");

            let description = $span.data("description");

            let file_id = $span.data("id");

            let info_head = "<span class='menu-head'>" + firstname + " " + lastname + "</span>";

            let info_foot = "<span class='menu-head'>" + "Title: " + "</span>"

                            + "<span class='menu-body'>" + getFileBaseName(fileName) + "</span>"

                            + "<br>"

                            + "<span class='menu-head'>" +  "Description: " + "</span>"

                            + "<span class='menu-body'>" + description + "</span>";

            let info_menu = "<span class='menu-head'>" + "Email: " + "</span>"

                            + "<span class='menu-body'>" + email + "</span>"

                            + "<br>"

                            + "<span class='menu-head'>" + "Phone: " + "</span>"

                            + "<span class='menu-body'>" + phone + "</span>";

            $span.tooltipData({

                id: file_id,

                title: $span.text(),

                contentClass: "content-tooltip",

                containerClass: "tooltip-container",

                tooltipImgClass: "tooltip_img",

                headerClass: "header",

                menuClass: "menu",

                contentClass: "content",

                footerClass: "footer",

                image: "",

                info_head: info_head,

                info_foot: info_foot,

                info_menu: info_menu,

                imageurl: "/imagedata",

                position: { my: "left+10 center", at: "right center" },

                show: {

                    effect: "fadeIn", 

                    duration: 400   

                },

                hide: {

                    effect: "fadeOut",

                    duration: 400   

                },

                track: true,

                classes: {

                    "ui-tooltip": "highlight"

                },

            });

        }

    });

}


3.2. Example Running

When you run the example, and you click on the last name (‘Smith’), you get its tooltip as shown in the following figure:










Friday, November 29, 2024

Cache in JavaScript: IndexedDB Database



Caching is a technique for storing data in a temporary storage area called a cache. It aims to retrieve data quickly and avoid costly processes such as recalculating or searching it in the database, especially for frequently asked resources. In the first article, I introduced the different possibilities for caching data in JavaScript and how to choose the appropriate caching possibility according to your needs. In a previous article, I introduced an example of using the Web Storage API (localStorage and sessionStorage) to cache data. Then, I dedicated another article to using objects and data-* attributes to temporarily cache data in JavaScript. Also, I elucidated the use of service workers to cache static resources like HTML, CSS, and JavaScript files or API responses. In this article, I talk about caching data using the IndexedDB database.


1. IndexedDB Database and IndexedDB API

IndexedDB database is a client-side database that stores large amounts of structured data including files and blobs in browsers. This means that a web application can use IndexedDB to cache data so that it can be accessed quickly and avoid frequent requests to the server. 


IndexedDB API is a low-level API that manages the corresponding database. It is good for complex applications that need to cache large JSON responses or images and allows working offline.


To avoid blocking web applications, IndexedDB operations are executed asynchronously.


2. IndexedDB Operations

You can perform several operations on IndexedDB. Some can be applied to the database itself, some can be applied to object stores, and some operations are used to manage the data stored in the database. Let’s look at the different IndexedDB operations based on their types.


Database Operations and Event Handling: Database operations are asynchronous and allow you to create, manage, and delete the database. Here is the list of major operations and events for managing the IndexedDB database.


Database Operations

Operation

Definition

Syntax

Create/Open

Opens a database if it exists. Otherwise, it is created.

indexedDB.open()

Delete

Deletes a database.

deleteDatabase()

Close

Closes the connection to the database.

close()

Events

DB Structure upgrade or change

Triggered when the structure of the database changes.

versionchange Event

Operation successfully performed

Triggered upon successful operation completion.

onsuccess

Error occurred

Triggered when an error occurs.

onerror

DB version changed

- Triggered when the database version changes.

- It is used for schema updates.

onupgradeneeded


Object Store Operations: Object stores are analogous to relational database tables, serving as a structured repository for data organization. The following table explains the main operations of creating and deleting object stores.


Object Store Operations

Operation

Definition

Syntax

Create

- Creates an object store as part of the database upgrade process.

- This method can only be invoked within the context of a version change transaction.

createObjectStore()

Delete

Deletes an object store.

deleteObjectStore()


Transaction Operations: Transactions allow you to perform operations atomically. Here is the list of the main ones you can apply to transactions.

Transaction Operations

Operation

Definition

Syntax

Create a transaction

Establishes a transaction for reading, writing, or a combination of both, guaranteeing data consistency.

Two modes are defined:

- readonly: read-only operations.

- readwrite: read and write operations.

transaction()


CRUD (Create, Read, Update, Delete) Operations: Crud operations refer to the four basic functions that can be performed on data in the data storage system.

CRUD (Create, Read, Update, Delete) Operations

Operation

Definition

Syntax

Read

- Retrieves a record by its key (unique identifier).

- Retrieves all records present in the object store.

- Traverses each record in the object store or index.

- get()

- getAll()

- openCursor()

Insert

- Adds a new record.

- Fails if a record with the same key already exists.

add()

Update

Inserts a new record or updates an existing one in the object store.

put()

Delete

- Deletes a record using its key.

- Deletes all records in an object store.

- delete()

- clear()


Index Operations: Indexes enable fast and optimized querying by providing a quick way to locate specific data. The following table summarizes the principal operations on indexes.

Index Operations

Operation

Definition

Syntax

Create index

Creates an index on a particular field or attribute of records.

createIndex()

Delete Index

Deletes an index.

deleteIndex()

Get Index

Utilizes an existing index to retrieve and filter data.

index()


Key Range Queries: Key ranges are used to filter data during queries. They allow you to retrieve only the specific subset of data that falls within a defined range.

Key Range Queries

Operation

Definition

Syntax

Define a range

Specifies a key range boundary (inclusive or exclusive), allowing for targeted data retrieval within a defined scope.

IDBKeyRange.bound()

Define a range with a lower bound

Specifies a range with a lower bound.

IDBKeyRange.lowerBound()

Define a range with an upper bound

Defines a range with an upper limit.

Specifies a range with an upper bound

IDBKeyRange.upperBound()

Define a specific key

Specifies an exact key value, allowing for the retrieval of a single, specific record.

IDBKeyRange.only()


3. Example of Caching Data Using IndexedDB Database

IndexedDB database is a suitable mechanism for caching large amounts of data. Let’s take an example of how you can cache the result of getting data from the back-end using IndexedDB. For this, we need some back-end code to return data. An easy way to do this is to use Python’s Flask framework. Additionally, we develop HTML, CSS, and JavaScript code for the front-end.


Note. We only write the relevant part here to explain how to use IndexedDB to cache data. If you want the full example, you can find it on GitHub here:

https://github.com/noura-github/indexeddb-app-blog


Python Code

In the back-end code, we first define ‘company_data’ which stores data about companies, departments, and employees. The dictionary has three main keys: ‘companies’, ‘departments’, and ‘employees’. Each dictionary in the ‘companies’ list represents a company with an ‘id’ and a ‘name’. Each dictionary in the ‘departments’ list represents a department with an ‘id’, a ‘name’, and a ‘companyId’ that references its company. Finally, each dictionary in ‘employees’ is an employee with an ‘id’, a ‘firstname’, a ‘lastname’, an ‘email’, a ‘departmentId’, a ‘departmentName’, and a ‘companyName’.


company_data = {

    "companies": [

        {"id": 1, "name": "TechCorp"},

        {"id": 2, "name": "TechMicro"}

    ],

    "departments": [

        {"id": 1, "name": "Engineering", "companyId": 1},

        {"id": 2, "name": "Marketing", "companyId": 1},

        {"id": 3, "name": "Sales", "companyId": 2},

        {"id": 4, "name": "HR", "companyId": 2}

    ],

    "employees": [

        {"id": 1, "firstname": "Alice", "lastname": "Smith", "email": "alice@techcorp.com", "departmentId": 1, "departmentName": "Engineering", "companyName": "TechCorp"},

        {"id": 2, "firstname": "Bob", "lastname": "Brown", "email": "bob@techcorp.com", "departmentId": 1, "departmentName": "Engineering", "companyName": "TechCorp"},

        {"id": 3, "firstname": "Charlie", "lastname": "Davis", "email": "charlie@techcorp.com", "departmentId": 2, "departmentName": "Marketing", "companyName": "TechCorp"},

        {"id": 4, "firstname": "David", "lastname": "Wilson", "email": "david@techmicro.com", "departmentId": 3, "departmentName": "Sales", "companyName": "TechMicro"},

        {"id": 5, "firstname": "Mary", "lastname": "Johnson", "email": "mary@techmicro.com", "departmentId": 4, "departmentName": "HR", "companyName": "TechMicro"}

    ]

}


In this Python code, we need a ‘get_data’ function as a Flask route that handles GET requests to the /data endpoint by returning the ‘company_data’ dictionary as a JSON object.


@app.route('/data', methods=['GET'])

def get_data():

    return jsonify(company_data)


HTML Code

In HTML code, we need mainly a table to display the data:

    <h1>Company Data</h1>

    <table id="dataTable">

        <thead>

            <tr>

                <th>First Name</th>

                <th>Last Name</th>

                <th>Email</th>

                <th>Department Name</th>

                <th>Company Name</th>

            </tr>

        </thead>

        <tbody>

            <!-- Data will be dynamically inserted here -->

        </tbody>

    </table>


JavaScript Code

In the JavaScript code, we show how to initialize an IndexedDB by opening it and creating its stores. We will also show how we store the data in IndexedDB, and how to retrieve and display it.


To initialize an IndexedDB database, we create a function called ‘initializeDatabase()’ which we call once the document is ready as follows:


$(document).ready(function () {

    initializeDatabase();

});


Here is an overview of the purpose of the ‘initializeDatabase()’ function:


Opening the Database: The function starts by opening the IndexedDB database using the ‘indexedDB.open()’ method. The database name is set to ‘CompanyDB’ and the version is set to 1.

Handling the ‘onupgradeneeded’ Event: The ‘onupgradeneeded’ event is triggered when the database is created for the first time or when the version number is incremented. In this case, the function creates three object stores: ‘companyStore’, ‘departmentStore’, and ‘employeeStore’. Each object store has a key path set to ‘id’.

Handling the ‘onsuccess’ Event: When the database is successfully opened, the ‘onsuccess’ event is triggered. The function checks if data already exists in the database by calling the ‘checkExistingData()’ function. If data exists, it calls the ‘loadCachedData()’ function to display the cached data. If no data exists, it calls the ‘fetchDataFromBackend()’ function to fetch data from the back-end.

Handling the ‘onerror’ Event: If an error occurs while opening the database, the ‘onerror’ event is triggered. The function logs the error to the console.


function initializeDatabase() {

    const request = indexedDB.open("CompanyDB", 1);

    request.onupgradeneeded = function (event) {

        const db = event.target.result;

        if (!db.objectStoreNames.contains("companyStore")) {

            db.createObjectStore("companyStore", { keyPath: "id" });

        }

        if (!db.objectStoreNames.contains("departmentStore")) {

            db.createObjectStore("departmentStore", { keyPath: "id" });

        }

        if (!db.objectStoreNames.contains("employeeStore")) {

            db.createObjectStore("employeeStore", { keyPath: "id" });

        }

    };

    request.onsuccess = function (event) {

        const db = event.target.result;

        checkExistingData(db).then(function (dataExists) {

            if (dataExists) {

                console.log("Using cached data");

                loadCachedData(db);

            } else {

                console.log("Fetching data from backend...");

                fetchDataFromBackend(db);

            }

        });

    };

    request.onerror = function (event) {

        console.error("Error opening database:", event.target.error);

    };

}


The ‘checkExistingData()’ Function: This function checks if the ‘companyStore’ object store contains data of a given IndexedDB database (parameter ‘db’). The function creates a read-only transaction on the ‘companyStore’ object store. It gets a reference to the ‘companyStore’ object store. Then, it generates a Promise that will be resolved with a boolean value. It sends a count() request to the object store to retrieve the number of records.

If the request is successful, it resolves the Promise with true if there is at least one record (request.result > 0), and false otherwise. If the request fails (e.g., due to an error), it resolves the Promise with false.


function checkExistingData(db) {

    const transaction = db.transaction(["companyStore"], "readonly");

    const store = transaction.objectStore("companyStore");

    return new Promise(function (resolve) {

        const request = store.count();

        request.onsuccess = function () {

            resolve(request.result > 0);

        };

        request.onerror = function () {

            resolve(false);

        };

    });

}


The function ‘fetchDataFromBackend()’: This is an asynchronous function that takes an IndexedDB database object (‘db’) as an argument. In summary, the function sends a GET request to the /data endpoint using the fetch API. It waits for the response and parses it as JSON using ‘response.json()’. The resulting data is expected to be an object with three properties: companies, departments, and employees, each containing an array of objects. It creates a read-write transaction on three object stores: ‘companyStore’, ‘departmentStore’, and ‘employeeStore’.

It gets references to each of these object stores. It loops through each array of data (companies, departments, employees) and adds each object to its corresponding object store using the ‘add()’ method. It sets up an event handler for when the transaction is complete (‘transaction.oncomplete’). When the transaction is complete, it calls another function named ‘loadCachedData()’, passing the ‘db’ object as an argument.

If any error occurs during the execution of this function, it catches the error and logs it to the console with a message.

async function fetchDataFromBackend(db) {

    try {

        const response = await fetch("/data");

        const data = await response.json();

        const transaction = db.transaction(["companyStore", "departmentStore", "employeeStore"], "readwrite");

        const companyStore = transaction.objectStore("companyStore");

        const departmentStore = transaction.objectStore("departmentStore");

        const employeeStore = transaction.objectStore("employeeStore");

        data.companies.forEach((company) => companyStore.add(company));

        data.departments.forEach((department) => departmentStore.add(department));

        data.employees.forEach((employee) => employeeStore.add(employee));

        transaction.oncomplete = () => loadCachedData(db);

    } catch (error) {

        console.error("Error fetching data from backend:", error);

    }

}


The function ‘loadCachedData()’: This function loads data from an IndexedDB database (‘db’) to display it. In summary, this function creates a read-only transaction on three object stores: ‘companyStore’, ‘departmentStore’, and ‘employeeStore’. It gets references to each of these object stores. It retrieves all data from each object store using ‘getAll()’. When the data is retrieved, it calls the ‘displayData()’ function to display the data, passing the type of data (‘Company’, ‘Department’, or ‘Employee’) and the retrieved data as arguments.


function loadCachedData(db) {

    const transaction = db.transaction(["companyStore", "departmentStore", "employeeStore"], "readonly");

    const companyStore = transaction.objectStore("companyStore");

    const departmentStore = transaction.objectStore("departmentStore");

    const employeeStore = transaction.objectStore("employeeStore");

    companyStore.getAll().onsuccess = function (event) {

        displayData("Company", event.target.result);

    };

    departmentStore.getAll().onsuccess = function (event) {

        displayData("Department", event.target.result);

    };

    employeeStore.getAll().onsuccess = function (event) {

        displayData("Employee", event.target.result);

    };

}


The function ‘displayData()’: This function displays data in an HTML table. It takes two arguments: type (not used in the function) and data (an array of objects). It clears the contents of a table body element using ‘empty()’. It loops through each object in the data array, and for each object, it creates a table row (<tr>) with five columns containing the object’s properties: ‘firstname’, etc. Finally, it appends each table row to the table body element using ‘append()’.


function displayData(type, data) {

    const $tableBody = $("#dataTable tbody");

    $tableBody.empty();

    $.each(data, function(index, employee) {

        const row = `

            <tr>

                <td>${employee.firstname}</td>

                <td>${employee.lastname}</td>

                <td>${employee.email}</td>

                <td>${employee.departmentName}</td>

                <td>${employee.companyName}</td>

            </tr>

        `;

        $tableBody.append(row);

    });

}


Let’s take an example of how to query data from an indexedDB database.


The function ‘queryCompanyDB_getAllEmployees()’: This function retrieves all employees from ‘CompanyDB’. First, it opens the ‘CompanyDB" database. When the database is opened, the function creates a read-only transaction on the ‘employeeStore’ object store. It retrieves all employees from the ‘employeeStore’ using ‘getAll()’. When the data is retrieved, it logs the array of employees to the console.


function queryCompanyDB_getAllEmployees() {

    const dbRequest = indexedDB.open("CompanyDB");

    dbRequest.onsuccess = function(event) {

        const db = event.target.result;

        const tx = db.transaction("employeeStore", "readonly");

        const store = tx.objectStore("employeeStore");

        const request = store.getAll();

        request.onsuccess = function(event) {

            const employees = event.target.result;

            console.log(employees);

        };

    };

}


4. Running the example

If you run the example for the first time, you will see the following web page. The data is initially retrieved from the back-end.


If you go to Chrome DevTools ‘Application’, you can perform some operations like checking the contents of the stores or deleting the database.


Blog Posts

Enhancing Performance of Java-Web Applications

Applications built with a Java back-end, a relational database (such as Oracle or MySQL), and a JavaScript-based front-end form a common and...