|
-
Mar 10th, 2026, 03:10 PM
#1
[Javascript] [ES6] Bootstrap Modal Builder
Bootstrap Modal Builder
A simple JavaScript solution for building Bootstrap modals on the fly.
Demo
Fiddle (ESM build): https://jsfiddle.net/c1uLypkg/
Fiddle (UMD build): https://jsfiddle.net/u5ah2dxq/
Repository
https://github.com/dday9/bootstrap-modal-extension
Usage
Code:
alert('E lorem ipsum dolor, etc...', { title: 'Hello World' });
confirm('Are you sure?', { title: 'Confirm' });
prompt('What\'s my age again?', { title: 'Age', type: 'number' });
Source Code
Code:
/* Utilities */
export class DOMBuilder {
static create(tag, options = {}) {
const element = document.createElement(tag);
if (options.class) {
if (Array.isArray(options.class)) {
element.classList.add(...options.class);
} else {
element.className = options.class;
}
}
if (options.attrs) {
for (const [key, value] of Object.entries(options.attrs)) {
if (value !== undefined && value !== null) {
element.setAttribute(key, value);
}
}
}
if (options.dataset) {
for (const [key, value] of Object.entries(options.dataset)) {
if (value !== undefined && value !== null) {
element.dataset[key] = value;
}
}
}
if (options.text !== undefined) {
element.textContent = options.text;
}
if (Array.isArray(options.children)) {
for (const child of options.children) {
if (child) {
element.append(child);
}
}
}
return element;
}
}
export class IdGenerator {
static #counter = 0;
static next(prefix = 'id') {
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
return `${prefix}-${crypto.randomUUID()}`;
}
this.#counter++;
const base36Timestamp = Date.now().toString(36);
const base36Counter = this.#counter.toString(36);
return `${prefix}-${base36Timestamp}-${base36Counter}`;
}
}
export class Utilities {
static BOOTSTRAP_EVENT_SHOWN = 'shown.bs.modal';
static BOOTSTRAP_EVENT_HIDDEN = 'hidden.bs.modal';
static buildModal(options = {}) {
return {
dialog: {
content: {
header: options.header,
body: options.body,
footer: options.footer
}
},
static: options.static === true
};
}
static createModal(modalOptions) {
const element = Modal.createElement(modalOptions);
document.body.append(element);
const modalInstance = new bootstrap.Modal(element);
element.addEventListener(
Utilities.BOOTSTRAP_EVENT_HIDDEN,
() => document.body.removeChild(element),
{ once: true }
);
modalInstance.show();
return { element, modalInstance };
}
}
/* header */
export class ModalHeader {
static BOOTSTRAP_CLASS = 'modal-header';
static createElement(options) {
if (!options?.title && options?.close === false) {
return null;
}
const children = [];
if (options?.title) {
children.push(ModalTitle.createElement(options.title));
}
if (options?.close !== false) {
children.push(CloseButton.createElement(options.close));
}
return DOMBuilder.create('div', {
class: ModalHeader.BOOTSTRAP_CLASS,
children
});
}
}
export class CloseButton {
static BOOTSTRAP_CLASS = 'btn-close';
static DEFAULT_ARIA_LABEL = 'Close';
static createElement(options) {
let ariaLabel = CloseButton.DEFAULT_ARIA_LABEL;
if (typeof options === 'string') {
ariaLabel = options.toString();
}
return DOMBuilder.create('button', {
class: CloseButton.BOOTSTRAP_CLASS,
attrs: {
type: 'button',
'aria-label': ariaLabel
},
dataset: {
bsDismiss: 'modal'
}
});
}
}
export class ModalTitle {
static BOOTSTRAP_CLASS = 'modal-title';
static createElement(options) {
let title = '';
if (typeof options === 'string') {
title = options.toString();
} else if (options?.title) {
title = options.title;
}
return DOMBuilder.create('h5', {
class: ModalTitle.BOOTSTRAP_CLASS,
text: title
});
}
}
/* body */
export class ModalBody {
static BOOTSTRAP_CLASS = 'modal-body';
static createElement(options) {
let text = '';
if (typeof options === 'string') {
text = options.toString();
} else if (typeof options?.text === 'string') {
text = options.text.toString();
}
const children = [];
if (text.length > 0) {
children.push(
DOMBuilder.create('p', { text })
);
}
if (options?.prompt) {
children.push(
ModalForm.createElement(options.prompt)
);
}
return DOMBuilder.create('div', {
class: ModalBody.BOOTSTRAP_CLASS,
children
});
}
}
export class ModalForm {
static GLOBAL_ATTRIBUTES = Object.freeze(new Set([
'accept'
, 'accept-charset'
, 'action'
, 'autocapitalize'
, 'autocomplete'
, 'enctype'
, 'id'
, 'method'
, 'name'
, 'novalidate'
, 'rel'
, 'target'
, ]));
static createElement(options) {
const element = document.createElement('form');
if (options?.input) {
const input = ModalInput.createElement(options.input);
element.appendChild(input);
}
ModalForm.applyGlobalAttributes(element, options);
if (!element.id) {
element.id = IdGenerator.next('modal-form');
}
if (options?.submit && typeof options.submit === 'function') {
element.addEventListener('submit', options.submit);
}
return element;
}
static applyGlobalAttributes(element, options) {
if (!element || !options) {
return;
}
for (let globalAttribute of ModalForm.GLOBAL_ATTRIBUTES) {
const attributeValue = options[globalAttribute];
if (attributeValue !== undefined && attributeValue !== null) {
element.setAttribute(globalAttribute, attributeValue);
}
}
}
}
export class ModalInput {
static BOOTSTRAP_CLASS = 'form-control';
static DEFAULT_TYPE = 'text';
static INPUT_TYPES = Object.freeze(new Set(['color', 'date', 'datetime-local', 'email', 'file', 'month', 'number', 'password', 'range', 'search', 'tel', 'text', 'time', 'url', 'week']));
static VALIDATION_ENUMS = Object.freeze(new Set(['required', 'minlength', 'maxlength', 'min', 'max', 'step', 'pattern']));
static GLOBAL_ATTRIBUTES = Object.freeze([
'accesskey'
, 'autocapitalize'
, 'autocorrect'
, 'contenteditable'
, 'dir'
, 'disabled'
, 'draggable'
, 'enterkeyhint'
, 'exportparts'
, 'id'
, 'inert'
, 'inputmode'
, 'is'
, 'itemid'
, 'itemprop'
, 'itemref'
, 'itemscope'
, 'itemtype'
, 'lang'
, 'name'
, 'nonce'
, 'part'
, 'placeholder'
, 'popover'
, 'role'
, 'slot'
, 'spellcheck'
, 'style'
, 'tabindex'
, 'title'
, 'translate'
, 'virtualkeyboardpolicy'
, 'writingsuggestions'
]);
static createElement(options) {
if (options?.type === 'range') {
return ModalInputRange.createElement(options);
}
let elementType = ModalInput.DEFAULT_TYPE;
if (options?.type && ModalInput.INPUT_TYPES.has(options.type)) {
elementType = options.type;
} else if (options?.type) {
console.warn(`Defaulting the input type to ${ModalInput.DEFAULT_TYPE} because "${options.type}" is not valid.`);
}
const element = document.createElement('input');
element.setAttribute('type', elementType);
if (options?.class) {
element.className = options.class;
} else {
element.classList.add(ModalInput.BOOTSTRAP_CLASS);
}
if (options.validation) {
ModalInput.applyValidation(element, options.validation);
}
ModalInput.applyGlobalAttributes(element, options);
return element;
}
static applyGlobalAttributes(element, options) {
if (!element || !options) {
return;
}
for (let globalAttribute of ModalInput.GLOBAL_ATTRIBUTES) {
const attributeValue = options[globalAttribute];
if (attributeValue !== undefined && attributeValue !== null) {
element.setAttribute(globalAttribute, attributeValue);
}
}
}
static applyValidation(element, options) {
if (!element || !options) {
return;
}
for (let validationEnum of ModalInput.VALIDATION_ENUMS) {
const optionValue = options[validationEnum];
if (optionValue !== undefined && optionValue !== null) {
const optionValueType = typeof optionValue;
if (
(validationEnum === 'pattern' && optionValueType !== 'string')
|| (validationEnum === 'required' && optionValueType !== 'boolean')
|| (validationEnum !== 'pattern' && validationEnum !== 'required' && optionValueType !== 'number')
) {
console.warn(`You may see unexpected results because the ${validationEnum} validation is not the right type.`);
}
element.setAttribute(validationEnum, optionValue);
}
}
}
}
export class ModalInputRange {
static BOOTSTRAP_CLASS = 'form-range';
static createElement(options) {
const element = document.createElement('input');
element.setAttribute('type', 'range');
if (options?.class) {
element.className = options.class;
} else {
element.classList.add(ModalInputRange.BOOTSTRAP_CLASS);
}
if (options.validation) {
ModalInput.applyValidation(element, options.validation);
}
ModalInput.applyGlobalAttributes(element, options);
return element;
}
}
/* footer */
export class ModalFooter {
static BOOTSTRAP_CLASS = 'modal-footer';
static createElement(options) {
if (!options?.ok && !options?.cancel) {
return null;
}
const children = [];
if (options.ok) {
const button = OkButton.createElement(options.ok);
if (options.form && typeof options.form === 'string') {
ActionButton.applyForm(button, options.ok, options.form);
}
children.push(button);
}
if (options.cancel) {
const button = CancelButton.createElement(options.cancel);
if (options.form && typeof options.form === 'string') {
ActionButton.applyForm(button, options.cancel, options.form);
}
children.push(button);
}
return DOMBuilder.create('div', {
class: ModalFooter.BOOTSTRAP_CLASS,
children
});
}
}
export class ActionButton {
static BOOTSTRAP_CLASS = 'btn';
static BUTTON_TYPES = Object.freeze(new Set(['button', 'reset', 'submit']));
static createElement(options) {
const element = document.createElement('button');
if (options?.text) {
element.textContent = options.text;
}
return element;
}
static applyDataDismiss(element) {
if (!element) {
return;
}
element.dataset.bsDismiss = 'modal';
}
static applyForm(element, options, formSelector) {
if (!element || !formSelector) {
return;
}
let type = element.getAttribute('type');
if (typeof options === 'object' && options !== null) {
type = options.type;
}
if (type === 'submit' || type === 'reset') {
element.setAttribute('form', formSelector);
}
}
static applyClassName(element, options, bootstrapClass) {
if (!element) {
return;
}
if (options?.class && typeof options.class === 'string') {
element.className = options.class;
} else {
element.classList.add(ActionButton.BOOTSTRAP_CLASS, bootstrapClass);
}
}
static applyType(element, options, defaultType) {
if (!element) {
return;
}
if (options?.type && ActionButton.BUTTON_TYPES.has(options.type)) {
element.setAttribute('type', options.type);
} else {
element.setAttribute('type', defaultType);
}
}
static applyText(element, options) {
if (!element) {
return;
}
if ((options?.text && typeof options.text === 'string') || typeof options === 'string') {
element.textContent = options?.text ?? options;
}
}
}
export class OkButton {
static BOOTSTRAP_CLASS = 'btn-primary';
static DEFAULT_TYPE = 'submit';
static createElement(options) {
const button = ActionButton.createElement(options);
OkButton.applyStyles(button, options);
return button;
}
static applyStyles(element, options) {
if (!element) {
return;
}
ActionButton.applyClassName(element, options, OkButton.BOOTSTRAP_CLASS);
ActionButton.applyType(element, options, OkButton.DEFAULT_TYPE);
if (options?.dismissOnClick === true) {
ActionButton.applyDataDismiss(element);
}
ActionButton.applyText(element, options);
}
}
export class CancelButton {
static BOOTSTRAP_CLASS = 'btn-secondary';
static DEFAULT_TYPE = 'button';
static createElement(options) {
const button = ActionButton.createElement(options);
CancelButton.applyStyles(button, options);
return button;
}
static applyStyles(element, options) {
if (!element) {
return;
}
ActionButton.applyClassName(element, options, CancelButton.BOOTSTRAP_CLASS);
ActionButton.applyType(element, options, CancelButton.DEFAULT_TYPE);
if (options?.dismissOnClick !== false) {
ActionButton.applyDataDismiss(element);
}
ActionButton.applyText(element, options);
}
}
/* wrappers */
export class Modal {
static BOOTSTRAP_CLASS = 'modal';
static DEFAULT_FADE = 'fade';
static DEFAULT_TAB_INDEX = -1;
static createElement(options) {
const classes = [Modal.BOOTSTRAP_CLASS];
if (options?.fade !== false) {
classes.push(Modal.DEFAULT_FADE);
}
const dataset = {};
if (options?.static === true) {
dataset.bsBackdrop = 'static';
dataset.bsKeyboard = 'false';
}
const dialog = ModalDialog.createElement(options?.dialog ?? {});
return DOMBuilder.create('div', {
class: classes,
attrs: {
tabindex: options?.tabIndex ?? Modal.DEFAULT_TAB_INDEX,
id: options?.id ?? IdGenerator.next('bs-modal')
},
dataset,
children: [dialog]
});
}
}
export class ModalDialog {
static BOOTSTRAP_CLASS = 'modal-dialog';
static SIZE_ENUM = Object.freeze(new Set(['sm', 'lg', 'xl']));
static createElement(options) {
const classes = [ModalDialog.BOOTSTRAP_CLASS];
if (options?.scrollable !== false) {
classes.push('modal-dialog-scrollable');
}
if (options?.verticallyCentered !== false) {
classes.push('modal-dialog-centered');
}
if (options?.size && ModalDialog.SIZE_ENUM.has(options.size)) {
classes.push(`modal-${options.size}`);
}
if (options?.fullscreen === true) {
classes.push('modal-fullscreen');
}
const content = ModalContent.createElement(options?.content ?? {});
return DOMBuilder.create('div', {
class: classes,
children: [content]
});
}
}
export class ModalContent {
static BOOTSTRAP_CLASS = 'modal-content';
static createElement(options) {
const children = [];
const header = ModalHeader.createElement(options?.header);
if (header) {
children.push(header);
}
children.push(
ModalBody.createElement(options?.body)
);
const footer = ModalFooter.createElement(options?.footer);
if (footer) {
children.push(footer);
}
return DOMBuilder.create('div', {
class: ModalContent.BOOTSTRAP_CLASS,
children
});
}
}
/* prebuilt defaults */
export function alert(message, options = {}) {
const modalOptions = Utilities.buildModal({
header: {
title: options.title ?? window.location.hostname,
close: true
},
body: {
text: message ?? ''
},
footer: false,
static: true
});
Utilities.createModal(modalOptions);
}
export function confirm(message, options = {}) {
let resolveFn;
const promise = new Promise((resolve) => {
resolveFn = resolve;
});
let confirmed = false;
const formId = IdGenerator.next('confirm-form');
const modalOptions = Utilities.buildModal({
header: {
title: options.title ?? window.location.hostname,
close: true
},
body: {
text: message ?? '',
prompt: {
id: formId,
submit: e => {
e.preventDefault();
confirmed = true;
modalInstance.hide();
}
}
},
footer: {
ok: 'Yes',
cancel: 'No',
form: formId
},
static: true
});
const { element, modalInstance } = Utilities.createModal(modalOptions);
element.addEventListener(
Utilities.BOOTSTRAP_EVENT_HIDDEN,
() => {
resolveFn(confirmed);
},
{ once: true }
);
return promise;
}
export function prompt(message, options = {}) {
let resolveFn;
let rejectFn;
const promise = new Promise((resolve, reject) => {
resolveFn = resolve;
rejectFn = reject;
});
let submitted = false;
let valueEntered = null;
const formId = IdGenerator.next('prompt-form');
const modalOptions = Utilities.buildModal({
header: {
title: options.title ?? window.location.hostname,
close: true
},
body: {
text: message ?? '',
prompt: {
id: formId,
submit: e => {
e.preventDefault();
submitted = true;
const input = element.querySelector(`#${formId} input`);
valueEntered = input?.value;
modalInstance.hide();
},
input: {
type: options.type,
validation: { required: true }
}
}
},
footer: {
ok: 'Submit',
cancel: 'Cancel',
form: formId
},
static: true
});
const { element, modalInstance } = Utilities.createModal(modalOptions);
element.addEventListener(
Utilities.BOOTSTRAP_EVENT_SHOWN,
() => {
const input = element.querySelector(`#${formId} input`);
input?.focus();
},
{ once: true }
);
element.addEventListener(
Utilities.BOOTSTRAP_EVENT_HIDDEN,
() => {
if (submitted) {
resolveFn(valueEntered);
} else {
rejectFn(false);
}
},
{ once: true }
);
return promise;
}
Screenshots
Alert:

Confirm:

Prompt:
Posting Permissions
- You may not post new threads
- You may not post replies
- You may not post attachments
- You may not edit your posts
-
Forum Rules
|
Click Here to Expand Forum to Full Width
|