Message Generator Utility

Registered members can download the FREE Get Started Apps. I initially implemented MySQL for this site and the Get Started App. The Get Started App is the project I used to compose articles about setting up VS Code and developing Node with Express and the Embedded JavaScript (EJS) view engine. I decided to migrate this site to PostgreSQL. Rather than replacing the MySQL Get Started App, I decided to add a PostgreSQL Get Started App. Both apps will run without a database but can integrate with either MySQL or PostgreSQL by configuring environment variables in the .env file.

The messaging from the client-js to the user is a critical function. I developed dynamic-modals.js to display a message modal. This Generator allows you to create and test your message modal. Edits to the Generator automatically updates the Generated code block with the fewest parameters needed. A dismiss button and/or a link is required. The message modal does not implement the close button in the head to force a redirection. Notice a modal created in the page's html can co-exists with dynamic-modals.js.

This series will focus on the utilities I use for this site which implement user security and communications crucial to user registration and user password storage.

I migrated the Message Generator utility from my ASP.NET Core websites. The dynamic-modals.js is a client-side utility which uses Bootstrap css classes and the modal component JavaScript provided by Bootstrap v5 js or Bootstrap-Native js. The dynamic-modals.js creates a basic customizable modal with JavaScript, no external html is required.

The dynamic-modals.js also provides AJAX Modal and Confirm Modal functions. The AJAX Modal function uses fetch to retrieve and display the properties of a server-side entity.

Fetch By Id

The Confirm Modal function requests confirmation before continuing with a unreversible action like deleting a server-side entity.

Confirm Modal
Confirm Function Completed
dynamic-modals.js
'use strict'

/*!
* Copyright © Ken Haggerty (https://kenhaggerty.com)
* Licensed under the MIT License.
* dynamic-modals.js - Version 2.4.0
* Bootstrap v5 Message Modal - Version 2.4.0
* Bootstrap v5 AJAX Modal - Version 2.4.0
* Bootstrap v5 Confirm Modal - Version 1.4.0
* Requires Bootstrap.css v5
* Depends on Bootstrap v5 js or Bootstrap Native
*/

let modalLibrary = '';
let modalInstance = null;

/*Bootstrap v5 Message Modal - Version 2.4.0 */
let messageModalDefaults = {
    Message: 'Thank you for supporting ExpressWithUsers.Com.',
    MessageClass: 'alert-primary',
    MessageAllowDismiss: true,
    MessageDismissText: 'OK',
    MessageDismissClass: 'btn-primary',
    MessageIncludeLink: false,
    MessageLinkHref: 'https://expresswithusers.com',
    MessageLinkTarget: '_self',
    MessageLinkText: 'ExpressWithUsers.Com',
    MessageLinkClass: 'btn-primary',
    MessageHideBackdrop: false
}

let modalBodyDiv = document.createElement('div');
modalBodyDiv.classList.add('modal-body');
modalBodyDiv.classList.add('text-center');

let messageElement = document.createElement('h4');
messageElement.classList.add('text-break');
let messageClassDiv = document.createElement('div');
messageClassDiv.appendChild(messageElement);
modalBodyDiv.appendChild(messageClassDiv);

let messageLinkButton = document.createElement('a', { href: '/' });
modalBodyDiv.appendChild(messageLinkButton);

let messageDismissButton = document.createElement('button', { type: 'button' });
messageDismissButton.dataset.bsDismiss = 'modal';
modalBodyDiv.appendChild(messageDismissButton);

let modalContentDiv = document.createElement('div');
modalContentDiv.classList.add('modal-content');
modalContentDiv.appendChild(modalBodyDiv);

let modalDialogDiv = document.createElement('div', { role: 'document' });
modalDialogDiv.classList.add('modal-dialog');
modalDialogDiv.classList.add('top-5');
modalDialogDiv.appendChild(modalContentDiv);

let modalDiv = document.createElement('div', { tabindex: '-1', role: 'dialog' });
modalDiv.classList.add('modal');
modalDiv.classList.add('message-modal');
modalDiv.classList.add('fade');
modalDiv.appendChild(modalDialogDiv);

document.body.appendChild(modalDiv);

const showMessageModal = (
    message = messageModalDefaults.Message,
    messageClass = messageModalDefaults.MessageClass,
    allowDismiss = messageModalDefaults.MessageAllowDismiss,
    dismissText = messageModalDefaults.MessageDismissText,
    dismissClass = messageModalDefaults.MessageDismissClass,
    includeLink = messageModalDefaults.MessageIncludeLink,
    linkHref = messageModalDefaults.MessageLinkHref,
    linkTarget = messageModalDefaults.MessageLinkTarget,
    linkText = messageModalDefaults.MessageLinkText,
    linkClass = messageModalDefaults.MessageLinkClass,
    hideBackdrop = messageModalDefaults.MessageHideBackdrop) => {

    if (modalLibrary.length == 0)
        return;

    if (message.length === 0) message = messageModalDefaults.Message;
    let messageParse = message.toString();
    if (messageParse.indexOf('DOMException') !== -1) message = messageParse.substring(14);
    if (messageParse.indexOf('NotAllowedError') !== -1) message = messageParse.substring(17);
    if (messageClass.length === 0) messageClass = messageModalDefaults.MessageClass;
    if (dismissText.length === 0) dismissText = messageModalDefaults.MessageDismissText;
    if (dismissClass.length === 0) dismissClass = messageModalDefaults.MessageDismissClass;
    if (linkHref.length === 0) linkHref = messageModalDefaults.MessageLinkHref;
    if (linkTarget.length === 0) linkTarget = messageModalDefaults.MessageLinkTarget;
    if (linkText.length === 0) linkText = messageModalDefaults.MessageLinkText;
    if (linkClass.length === 0) linkClass = messageModalDefaults.MessageLinkClass;

    messageElement.innerText = message;

    messageClassDiv.classList.remove(...messageClassDiv.classList);
    messageClassDiv.classList.add('alert', messageClass, 'px-2', 'py-1', 'mb-3');

    messageDismissButton.textContent = dismissText;
    messageDismissButton.classList.remove(...messageDismissButton.classList);
    messageDismissButton.classList.add('btn');
    messageDismissButton.classList.add(dismissClass);

    messageLinkButton.classList.add('d-none');

    if (includeLink) {
        messageLinkButton.setAttribute('href', linkHref);
        messageLinkButton.setAttribute('target', linkTarget);
        messageLinkButton.textContent = linkText;
        messageLinkButton.classList.remove(...messageLinkButton.classList);
        messageLinkButton.classList.add('btn');
        messageLinkButton.classList.add(linkClass);

        if (allowDismiss) {
            messageLinkButton.classList.add('me-2');
        } else {
            messageDismissButton.classList.add('d-none');
            if (modalLibrary == 'BV5') {
                modalInstance = new bootstrap.Modal(modalDiv, { backdrop: 'static', keyboard: false });
                modalInstance.show();
            } else if (modalLibrary == 'BSN') {
                modalInstance = new BSN.Modal(modalDiv, { backdrop: 'static', keyboard: false });
                modalInstance.show();
            }
            return;
        }
    }

    if (hideBackdrop) {
        if (modalLibrary == 'BV5') {
            modalInstance = new bootstrap.Modal(modalDiv, { backdrop: false, keyboard: false });
            modalInstance.show();
        } else if (modalLibrary == 'BSN') {
            modalInstance = new BSN.Modal(modalDiv, { backdrop: false, keyboard: false });
            modalInstance.show();
        }
    } else {
        if (modalLibrary == 'BV5') {
            modalInstance = new bootstrap.Modal(modalDiv);
            modalInstance.show();
        } else if (modalLibrary == 'BSN') {
            modalInstance = new BSN.Modal(modalDiv);
            modalInstance.show();
        }
    }
}

/*Bootstrap v5 AJAX Modal - Version 2.3.0 */
let defaultLoadingAjaxTitle = 'Details';
let defaultUpdateAjaxTitle = 'Edit';

let modalAjaxCloseButton = document.createElement('button');
modalAjaxCloseButton.type = 'button';
modalAjaxCloseButton.classList.add('btn-close');
modalAjaxCloseButton.dataset.bsDismiss = 'modal';

let modalAjaxTitle = document.createElement('h5');
modalAjaxTitle.classList.add('modal-title');
modalAjaxTitle.textContent = defaultLoadingAjaxTitle;

let modalHeader = document.createElement('div');
modalHeader.classList.add('modal-header');
modalHeader.appendChild(modalAjaxTitle);
modalHeader.appendChild(modalAjaxCloseButton);

let modalAjaxBody = document.createElement('div');
modalAjaxBody.classList.add('modal-body');

let modalAjaxUpdateButton = document.createElement('button');
modalAjaxUpdateButton.type = 'button';
modalAjaxUpdateButton.classList.add('btn', 'btn-success', 'ajax-btn-update', 'd-none');
modalAjaxUpdateButton.innerText = 'Update';

let modalAjaxFooterCloseButton = document.createElement('button');
modalAjaxFooterCloseButton.type = 'button';
modalAjaxFooterCloseButton.classList.add('btn', 'btn-primary');
modalAjaxFooterCloseButton.dataset.bsDismiss = 'modal';
modalAjaxFooterCloseButton.innerText = 'Close';

let modalAjaxFooter = document.createElement('div');
modalAjaxFooter.classList.add('modal-footer');

modalAjaxFooter.appendChild(modalAjaxUpdateButton);
modalAjaxFooter.appendChild(modalAjaxFooterCloseButton);

let modalAjaxContentDiv = document.createElement('div');
modalAjaxContentDiv.classList.add('modal-content');
modalAjaxContentDiv.appendChild(modalHeader);
modalAjaxContentDiv.appendChild(modalAjaxBody);
modalAjaxContentDiv.appendChild(modalAjaxFooter);

let modalAjaxDialogDiv = document.createElement('div', { role: 'document' });
modalAjaxDialogDiv.classList.add('modal-dialog');
modalAjaxDialogDiv.classList.add('top-5');
modalAjaxDialogDiv.appendChild(modalAjaxContentDiv);

let modalAjaxDiv = document.createElement('div', { tabindex: '-1', role: 'dialog' });
modalAjaxDiv.classList.add('modal', 'ajax-modal', 'fade');
modalAjaxDiv.appendChild(modalAjaxDialogDiv);

document.body.appendChild(modalAjaxDiv);

let spinnerAjaxDiv = document.createElement('div');
spinnerAjaxDiv.classList.add('spinner-border', 'text-alert', 'd-flex', 'mt-2', 'mx-auto');
spinnerAjaxDiv.setAttribute('role', 'status');

let modalAjaxSpan = document.createElement('span');
modalAjaxSpan.classList.add('visually-hidden');
modalAjaxSpan.innerText = 'Loading...';
spinnerAjaxDiv.appendChild(modalAjaxSpan);

let modalAjaxUpdateHiddenInput = document.createElement('input');
modalAjaxUpdateHiddenInput.setAttribute('type', 'hidden');
modalAjaxUpdateHiddenInput.setAttribute('id', 'AjaxModalHiddenId');

let modalAjaxUpdateHeader = document.createElement('h6');

let modalAjaxUpdateInput = document.createElement('textarea');
modalAjaxUpdateInput.setAttribute('id', 'AjaxModalUpdateInputId');
modalAjaxUpdateInput.classList.add('form-control');

const showLoadingModal = (title) => {

    if (modalLibrary.length == 0)
        return;

    modalAjaxUpdateButton.classList.add('d-none');
    modalAjaxFooterCloseButton.innerText = 'Close';
    modalAjaxFooterCloseButton.classList.remove('btn-secondary');
    modalAjaxFooterCloseButton.classList.add('btn-primary');
    if (title !== '') modalAjaxTitle.textContent = title;
    else modalAjaxTitle.textContent = defaultLoadingAjaxTitle;
    modalAjaxBody.innerHTML = '';
    modalAjaxBody.appendChild(spinnerAjaxDiv);

    if (modalLibrary == 'BV5') {
        modalInstance = new bootstrap.Modal(modalAjaxDiv);
        modalInstance.show();
    } else if (modalLibrary == 'BSN') {
        modalInstance = new BSN.Modal(modalAjaxDiv);
        modalInstance.show();
    }
}

const showUpdateModal = (id, value, header = '', title = '', rows = 1) => {
    modalAjaxUpdateButton.classList.remove('d-none');
    modalAjaxFooterCloseButton.innerText = 'Cancel';
    modalAjaxFooterCloseButton.classList.remove('btn-primary');
    modalAjaxFooterCloseButton.classList.add('btn-secondary');
    if (title !== '') modalAjaxTitle.textContent = title;
    else modalAjaxTitle.textContent = defaultUpdateAjaxTitle;
    modalAjaxBody.innerHTML = '';
    modalAjaxUpdateHiddenInput.value = id;
    modalAjaxBody.appendChild(modalAjaxUpdateHiddenInput);
    modalAjaxUpdateHeader.textContent = header === '' ? value : header;
    modalAjaxBody.appendChild(modalAjaxUpdateHeader);
    modalAjaxUpdateInput.rows = rows;
    modalAjaxUpdateInput.value = value;
    modalAjaxBody.appendChild(modalAjaxUpdateInput);

    if (modalLibrary == 'BV5') {
        modalInstance = new bootstrap.Modal(modalAjaxDiv);
        modalInstance.show();
    } else if (modalLibrary == 'BSN') {
        modalInstance = new BSN.Modal(modalAjaxDiv);
        modalInstance.show();
    }
}

const showResponseModal = (responseJSON) => {
    modalAjaxUpdateButton.classList.add('d-none');
    modalAjaxFooterCloseButton.innerText = 'Close';
    modalAjaxFooterCloseButton.classList.remove('btn-secondary');
    modalAjaxFooterCloseButton.classList.add('btn-primary');
    let dl, dt, dd, prop;
    modalAjaxBody.innerHTML = '';
    let status = '';
    if (responseJSON.Status !== undefined)
        status = responseJSON.Status;
    if (responseJSON.status !== undefined)
        status = responseJSON.status;
    if (status === 'Success') {
        dl = document.createElement('dl');
        for (prop in responseJSON) {
            if (responseJSON[prop] == null) responseJSON[prop] = 'NULL';
            dt = document.createElement('dt');
            dt.textContent = prop;
            dl.appendChild(dt);
            dd = document.createElement('dd');
            dd.classList.add('text-break');
            dd.textContent = responseJSON[prop].length === 0 ? 'Not Found' : responseJSON[prop];
            dl.appendChild(dd);
        }
        let successDiv = document.createElement('div');
        successDiv.classList.add('alert', 'alert-success');
        successDiv.appendChild(dl);
        modalAjaxBody.appendChild(successDiv);
    }
    else {
        dl = document.createElement('dl');
        for (prop in responseJSON) {
            if (responseJSON[prop] == null) responseJSON[prop] = 'NULL';
            dt = document.createElement('dt');
            dt.textContent = prop;
            dl.appendChild(dt);
            dd = document.createElement('dd');
            dd.classList.add('text-break');
            dd.textContent = responseJSON[prop].length === 0 ? 'Not Found' : responseJSON[prop];
            dl.appendChild(dd);
        }
        let alertDiv = document.createElement('div');
        alertDiv.classList.add('alert', 'alert-danger');
        alertDiv.appendChild(dl);
        modalAjaxBody.appendChild(alertDiv);
    }
}

const showErrorModal = (error) => {
    modalAjaxUpdateButton.classList.add('d-none');
    modalAjaxFooterCloseButton.innerText = 'Close';
    modalAjaxFooterCloseButton.classList.remove('btn-secondary');
    modalAjaxFooterCloseButton.classList.add('btn-primary');
    modalAjaxTitle.textContent = 'An Error Has Occurred';
    modalAjaxBody.innerHTML = '';
    let alertDiv = document.createElement('div');
    alertDiv.classList.add('alert', 'alert-danger');
    alertDiv.textContent = error;
    modalAjaxBody.appendChild(alertDiv);

    if (modalLibrary == 'BV5') {
        modalInstance = new bootstrap.Modal(modalAjaxDiv);
        modalInstance.show();
    } else if (modalLibrary == 'BSN') {
        modalInstance = new BSN.Modal(modalAjaxDiv);
        modalInstance.show();
    }
}

/*Bootstrap v5 Confirm Modal - Version 1.3.0 */
let confirmBodyDiv = document.createElement('div');
confirmBodyDiv.classList.add('modal-body', 'text-center');

let confirmElement = document.createElement('h4');
let confirmClassDiv = document.createElement('div');
confirmClassDiv.appendChild(confirmElement);
confirmBodyDiv.appendChild(confirmClassDiv);

let confirmButton = document.createElement('button', { type: 'button' });
confirmBodyDiv.appendChild(confirmButton);

let cancelButton = document.createElement('button', { type: 'button' });
cancelButton.dataset.bsDismiss = 'modal';
confirmBodyDiv.appendChild(cancelButton);

let confirmContentDiv = document.createElement('div');
confirmContentDiv.classList.add('modal-content');
confirmContentDiv.appendChild(confirmBodyDiv);

let confirmDialogDiv = document.createElement('div', { role: 'document' });
confirmDialogDiv.classList.add('modal-dialog', 'top-5');
confirmDialogDiv.appendChild(confirmContentDiv);

let modalConfirmDiv = document.createElement('div', { tabindex: '-1', role: 'dialog' });
modalConfirmDiv.classList.add('modal', 'fade', 'confirm-modal');
modalConfirmDiv.appendChild(confirmDialogDiv);

document.body.appendChild(modalConfirmDiv);

let defaultConfirmMessage = 'Are you sure?';
let defaultConfirmedCallback = () => { alert('Callback is not set.') };
let defaultConfirmMessageClass = 'alert-primary';
let defaultConfirmButtonText = 'Delete';
let defaultConfirmButtonClass = 'btn-danger';
let defaultCancelButtonText = 'Cancel';
let defaultCancelButtonClass = 'btn-primary';

const showConfirmModal = (
    confirmMessage = defaultConfirmMessage,
    confirmedCallback = defaultConfirmedCallback,
    confirmMessageClass = defaultConfirmMessageClass,
    confirmButtonText = defaultConfirmButtonText,
    confirmButtonClass = defaultConfirmButtonClass,
    cancelButtonText = defaultCancelButtonText,
    cancelButtonClass = defaultCancelButtonClass) => {

    if (modalLibrary.length == 0)
        return;

    confirmElement.innerText = confirmMessage;

    confirmClassDiv.classList.remove(...confirmClassDiv.classList);
    confirmClassDiv.classList.add('alert', confirmMessageClass, 'px-2', 'py-1', 'mb-3');

    confirmButton.textContent = confirmButtonText;
    confirmButton.classList.remove(...confirmButton.classList);
    confirmButton.classList.add('btn', 'me-2', confirmButtonClass);

    confirmButton.addEventListener('click', confirmedCallback);

    cancelButton.textContent = cancelButtonText;
    cancelButton.classList.remove(...cancelButton.classList);
    cancelButton.classList.add('btn', cancelButtonClass);

    if (modalLibrary == 'BV5') {
        modalInstance = new bootstrap.Modal(modalConfirmDiv);
        modalInstance.show();
    } else if (modalLibrary == 'BSN') {
        modalInstance = new BSN.Modal(modalConfirmDiv);
        modalInstance.show();
    }
}

document.addEventListener('DOMContentLoaded', () => {

    if (window.BSN) {
        if (window.BSN.Modal) modalLibrary = 'BSN';
        else alert('BootstrapNative Modal was not found.');
    } else if (window.bootstrap) {
        if (window.bootstrap.Modal) modalLibrary = 'BV5';
        else alert('Bootstrap v5 Modal was not found.');
    } else alert('Bootstrap v5 js, BootstrapNative, or jQuery is required by dynamic-modals.js.');

    // V2.2.0 added
    document.querySelectorAll('.modal').forEach(modal => {
      modal.addEventListener('hide.bs.modal', () => {
        document.activeElement.blur();
      });
    });
});
Created: 3/5/26