A better JS prompt

Fredo Corleone
4 min readOct 10, 2021

Everybody knows how ugly the built-in browser prompt is.

The state of the art of browser prompt

We need to do better. We need multiple inputs, more type of them, we need validation, we need the prompt to be awaitable and we need a way to style the look and feel of it!

With this in mind I decided to create a better JS prompt that provides the flexibility to do much much more and works with native input elements and HTML validation.

I won’t bother you with all the lil details now. Just look the result!

My interpretation of how a prompt should look like
A GIF showing how it works

The API of my prompt is very simple:

let prompt = await new SmartPrompt({
figureColor: "#bada55", // The green-ish color
groundColor: "#fafae9", // A shade of white
title: "This is a title",
prescription: "This is the text under the title",
postscription: "This is the text at the bottom befor the buttons",
template: `<div style="display: grid; grid-gap: 1rem;">
<input name="myNumber" type="number" min="0" max="10" placeholder="Age">
<input name="myText" type="text" minlength="3" maxlength="10" placeholder="Username" required="true">
<div>
<input name="myCheck" type="checkbox" required="true"
<label>Check</label>
</div>
<!-- ...other HTML inputs... -->
</div>`
});

I started by imagining what I wanted and wrote that down:

I want to be able to provide the developer with simplest API possible to gather inputs from the user, with beatiful prompts.The promp is able to:
- Be awaited (async/await style)
- Be discarded (with outside click or cancel button click)
- Provide any complex template containing inputs
- Spit an error if the template is not valid HTML
- Have validation for inputs (with a lil animation)
- Have overflow scroll if there is too much content
- Two buttons, one to confirm and on to dismiss

With the above points constantly in mind I started thinking about how I could provide the most flexibility, and the answer was to accept any kind of markup from the consumer.

How does it work internally?

I won’t spend too much time into details, I just want to provide you with an idea on how I did the essential things.

The constructor takes the option object provided to the it and extracts it’s properties and provide some fallbacks, also checks if the template provided is valid HTML:

this.figureColor = opts.figureColor || "#111";
this.groundColor = opts.groundColor || "#fffff1";
this.textColor = opts.textColor || "#111";
this.title = opts.title || "";
this.prescription = opts.prescription || "";
this.postscription = opts.postscription || "";
this.template = opts.template;
if (!this.template) {
throw new Error("You must provide a valid template.");
}
if (!this.isValidTemplate(this.template)) {
throw new Error(`{{ error message here }}`);
}

To check the validity of a template you create it by injecting HTML with .innerHTML = and then read it back and check the result against your original template:

let doc = document.createElement("DIV");
doc.innerHTML = template;
if (doc.innerHTML !== template) { // Look strange, huh? Hehe
return false;
}

This works because the browser is adapting the HTML to make it fit if there are missing closing tags or malformed attributes.

Last the constructor returns a promise, because if you remember I wanted the prompt to be awaited:

// Return a promise and extract resolve and reject
return new Promise((res, rej) => {
// Open the modal
let el = document.createElement("DIV");
el.innerHTML = this.getBoilerPlate();
// Setup so that clicking outside dismisses the dialog
el.firstElementChild.addEventListener("click", (e) => {
if (e.target.classList[0] === ("modal-wrapper" + this.uuid)) {
this.cancel();
}
});
document.body.appendChild(el.firstElementChild);
this.fade(1);
window["prompt" + this.uuid] = this; // "this" is SmartPrompt this.resolve = res;
this.reject = rej;
});

I needed to extract the resolve() and reject() from the promise because I want to trigger them outside the promise.
When the user clicks “Submit” he/she triggers this.submit() (same goes for “Cancel”):

SmartPrompt.prototype.submit = function () {
if (this.getForm().checkValidity()) {
this.resolve(this.parseResult());
this.removeModal();
} else {
// Do a shake animation to signal invalidity
}
};
SmartPrompt.prototype.cancel = function () {
this.removeModal();
this.reject("Aborted by the user.");
};

As you can see from the above snippet the magic of validation happens when I call the .checkValidity() on the form element.

To get the form values and make them into an object I use the FormData constructor:

SmartPrompt.prototype.parseResult = function () {
let formData = new FormData(this.getForm());
return [...formData.entries()]
.reduce((result, tuple) =>
(result[tuple[0]] = tuple[1], result)
, {}
);
};
// <input type="text" name="myName">
// will become
// { myName: "whatever was inputted" }

In order to encapsulate the prompt markup and style I use a UUID:

this.uuid = Math.random().toString(36).slice(2);

The shell of the prompt and its styles are marked with the above UUID. I defined all the boilerplate associated with the prompt in a function that uses interpolation to generate the markup the way it has to be:

<div class="modal-wrapper${this.uuid}">...</div>
...
<style>
.modal-wrapper${this.uuid} {
position: fixed;
top: 0;
z-index: 999999;
...
}
...
</style>

Conclusion

You can play with this prompt, to be precise the first draft of it, in Codepen:

https://codepen.io/eternalsunshineofspotlessmind/full/WNOVdyQ

I made it into a GitHub repository:

https://github.com/4skinSkywalker/SmartPrompt

And a NPM package:

https://www.npmjs.com/package/smartprompt

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Fredo Corleone
Fredo Corleone

Written by Fredo Corleone

Ex web-app developer. Now just a free man!

Responses (1)

Write a response

I'm wondering why you'd have prompt in the first place since you have the required attribute. This all reeks of using JavaScript to do things that are NONE of Javascript's business.
But that stands to reason with the static style in the scripting…

--