In this project, we will be working with promises. Specifically, we are going to use with async
& await
to build a prompt interface. When someone clicks on something on the page, we want them to be prompted with a popup asking 3 questions in a row. This is a bit more detailed than the built in prompt()
which just allows a single input. Additionally the prompt()
input blocks the page - let's create something a little more custom.
I've changed the formatting a bit in this article - I'm not sure I love it so I will likely use the original formatting for future articles. Let me know if you love it or hate it in the comments section below!
I am creating this post as notes from Wes Bos Javascript course which you can sign up for and do with me here: https://beginnerjavascript.com/. Here is a link to Wes' GitHub readme.md.
Start by creating a new project folder and adding 3 files with HTML and CSS provided below:
HTML
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Async Prompt</title> <link rel="stylesheet" href="style.css"> </head> <body> <div class="wrapper"> <button class="askMe" data-question="What is your name?">Enter Name</button> <button class="askMe" data-cancel data-question="What is your age?">Enter Age</button> </div> <script src="scripts.js"></script> </body> </html>
CSS
/* normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */button,hr,input{overflow:visible}progress,sub,sup{vertical-align:baseline}[type=checkbox],[type=radio],legend{box-sizing:border-box;padding:0}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}details,main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:ButtonText dotted 1px}fieldset{padding:.35em .75em .625em}legend{color:inherit;display:table;max-width:100%;white-space:normal}textarea{overflow:auto}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}[hidden],template{display:none} /* Variables */ html { --grey: #e7e7e7; --gray: var(--grey); --blue: #0072B9; --pink: rgb(31, 161, 194); --yellow: #ffc600; --black: #2e2e2e; --red: #c73737; --green: #61e846; --text-shadow: 2px 2px 0 rgba(0,0,0,0.2); --box-shadow: 0 0 5px 5px rgba(0,0,0,0.2); font-size: 62.5%; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; box-sizing: border-box; } *, *:before, *:after { box-sizing: inherit; } body { font-size: 2rem; line-height: 1.5; background-color: var(--blue); background-image: url("data:image/svg+xml,%3Csvg width='20' height='100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 21.184c.13.357.264.72.402 1.088l.661 1.768C4.653 33.64 6 39.647 6 50c0 10.271-1.222 15.362-4.928 24.629-.383.955-.74 1.869-1.072 2.75v6.225c.73-2.51 1.691-5.139 2.928-8.233C6.722 65.888 8 60.562 8 50c0-10.626-1.397-16.855-5.063-26.66l-.662-1.767C1.352 19.098.601 16.913 0 14.85v6.335zm20 0C17.108 13.258 16 8.077 16 0h2c0 5.744.574 9.951 2 14.85v6.334zm0 56.195c-2.966 7.86-4 13.123-4 22.621h2c0-6.842.542-11.386 2-16.396v-6.225zM6 0c0 8.44 1.21 13.718 4.402 22.272l.661 1.768C14.653 33.64 16 39.647 16 50c0 10.271-1.222 15.362-4.928 24.629C7.278 84.112 6 89.438 6 100h2c0-10.271 1.222-15.362 4.928-24.629C16.722 65.888 18 60.562 18 50c0-10.626-1.397-16.855-5.063-26.66l-.662-1.767C9.16 13.223 8 8.163 8 0H6z' fill='%23fff' fill-rule='nonzero' fill-opacity='.1' opacity='.349'/%3E%3C/svg%3E%0A"); background-size: 15px; } /* Table Styles */ table { border-radius: 5px; overflow: hidden; margin-bottom: 2rem; border-collapse: collapse; } td, th { border: 1px solid var(--grey); padding: 0.5rem; } /* Helper Divs */ .wrapper { max-width: 1000px; margin: 4rem auto; padding: 2rem; background: white; } .box, .wrapper { box-shadow: 0 0 3px 5px rgba(0,0,0,0.08653); } a { color: var(--blue); text-decoration-color: var(--yellow); } a.button, button, input[type="button"] { color: white; background: var(--pink); padding: 1rem; border: 0; border: 2px solid transparent; text-decoration: none; font-weight: 600; font-size:2rem; } :focus { outline-color: var(--pink); } fieldset { border: 1px solid black; } input:not([type="checkbox"]):not([type="radio"]), textarea, select { display: block; padding: 1rem; border: 1px solid var(--grey); } .success { border: 1px solid red; } h1, h2, h3, h4, h5, h6 { color: white; margin-top: 0; line-height: 1; text-shadow: var(--text-shadow); } .wrapper h1, .wrapper h2, .wrapper h3, .wrapper h4, .wrapper h5, .wrapper h6 { color: var(--black); text-shadow: none; } .popup { background: hsla(0, 0%, 30%, 0.5); position: fixed; height: 100vh; width: 100vw; transition: all 0.25s; top: 0; display: grid; justify-content: center; align-items: center; pointer-events: none; --opacity: 0; opacity: var(--opacity); } .popup fieldset { background: var(--grey); padding: 2rem; border: 3px solid var(--pink); border-radius: 5px; box-shadow: var(--box-shadow); transition: all 0.2s; --scale: 0.3; transform: scale(var(--scale)); } .popup.open { --opacity: 1; pointer-events: all; } .popup.open fieldset { --scale: 1; }
Now create a file in that project folder called script.js
and open it up in your visual studio code editor. Let's start with some sudo code:
function ask(options){ return new Promise(function(resolve) { //FIRST CREATE A POPUP WITH ALL THE FIELDS IN IT //CHECK IF THEY WANT TO CANCEL //LISTEN FOR THE SUBMIT EVENT ON THE INPUTS //WHEN SOMEONE DOES SUBMIT, RESOLVE THE DATA THAT WAS IN THE INPUT BOX //INSERT THAT POPUP INTO THE DOM }); };
// FIRST CREATE A POPUP WITH ALL THE FIELDS IN IT
function ask(options){ return new Promise(function(resolve) { //FIRST CREATE A POPUP WITH ALL THE FIELDS IN IT const popup = document.createElement('form'); //CHECK IF THEY WANT TO CANCEL //LISTEN FOR THE SUBMIT EVENT ON THE INPUTS //WHEN SOMEONE DOES SUBMIT, RESOLVE THE DATA THAT WAS IN THE INPUT BOX //INSERT THAT POPUP INTO THE DOM }); };
Spoiler, we are using
createElement()
. When we usedocument.createElement
- this will immediately return to us the DOM node. This allows us to create eventListeners to the submit button. This way we don't have to wait for the element to be created to put an eventListener on it.
We created the popup above, now we want to add a classList to it of .popup
from our CSS file. Then we want to insert some HTML to the popup.
function ask(options){ return new Promise(function(resolve) { //FIRST CREATE A POPUP WITH ALL THE FIELDS IN IT const popup = document.createElement('form'); popup.classList.add('popup'); popup.insertAdjacentHTML( 'afterbegin', ` <fieldset> <label>${options.title}</label> </fieldset> ` ); //CHECK IF THEY WANT TO CANCEL //LISTEN FOR THE SUBMIT EVENT ON THE INPUTS //WHEN SOMEONE DOES SUBMIT, RESOLVE THE DATA THAT WAS IN THE INPUT BOX //INSERT THAT POPUP INTO THE DOM }); };
Let's add a cancel button!
function ask(options){ return new Promise(function(resolve) { //FIRST CREATE A POPUP WITH ALL THE FIELDS IN IT const popup = document.createElement('form'); popup.classList.add('popup'); popup.insertAdjacentHTML( 'afterbegin', ` <fieldset> <label>${options.title}</label> </fieldset> ` ); //CHECK IF THEY WANT TO CANCEL if (options.cancel){ const skipButton = document.createElement('button); skipButton.type = 'button'; // important because if we don't tell it a type it will assume it's a submit. skipButton.textContent = 'Cancel'; //TODO: LISTEN FOR A CLICK ON THAT CANCEL BUTTON }; //LISTEN FOR THE SUBMIT EVENT ON THE INPUTS //WHEN SOMEONE DOES SUBMIT, RESOLVE THE DATA THAT WAS IN THE INPUT BOX //INSERT THAT POPUP INTO THE DOM }); };
// INSERT THAT POPUP INTO THE DOM
function ask(options){ return new Promise(function(resolve) { //FIRST CREATE A POPUP WITH ALL THE FIELDS IN IT const popup = document.createElement('form'); popup.classList.add('popup'); popup.insertAdjacentHTML( 'afterbegin', ` <fieldset> <label>${options.title}</label> </fieldset> ` ); //CHECK IF THEY WANT TO CANCEL if (options.cancel){ const skipButton = document.createElement('button); skipButton.type = 'button'; // important because if we don't tell it a type it will assume it's a submit. skipButton.textContent = 'Cancel'; //TODO: LISTEN FOR A CLICK ON THAT CANCEL BUTTON }; //LISTEN FOR THE SUBMIT EVENT ON THE INPUTS //WHEN SOMEONE DOES SUBMIT, RESOLVE THE DATA THAT WAS IN THE INPUT BOX //INSERT THAT POPUP INTO THE DOM document.body.appendChild(popup); }); };
Now that it's on the DOM, let's make it show. In the CSS it's currently set to --opacity: 0;
but there is a popup.open
class we want to add that changes the opacity.
function ask(options){ return new Promise(function(resolve) { //FIRST CREATE A POPUP WITH ALL THE FIELDS IN IT const popup = document.createElement('form'); popup.classList.add('popup'); popup.insertAdjacentHTML( 'afterbegin', ` <fieldset> <label>${options.title}</label> </fieldset> ` ); //CHECK IF THEY WANT TO CANCEL if (options.cancel){ const skipButton = document.createElement('button); skipButton.type = 'button'; // important because if we don't tell it a type it will assume it's a submit. skipButton.textContent = 'Cancel'; //TODO: LISTEN FOR A CLICK ON THAT CANCEL BUTTON }; //LISTEN FOR THE SUBMIT EVENT ON THE INPUTS //WHEN SOMEONE DOES SUBMIT, RESOLVE THE DATA THAT WAS IN THE INPUT BOX //INSERT THAT POPUP INTO THE DOM document.body.appendChild(popup); // PUT A SMALL TIMEOUT BEFORE WE ADD THE OPEN CLASS popup.classList.add('open'); }); };
This will create the popup instantly and it's not transitioning in - this goes back to the event loop we spoke of in our last article. The reason why this is not animating itself in is because it's creating the popup and animating itself in at the same time. You can see in line 25 we added some sudo code to remind us that we need to transition from creation to open css class. This will stick the code that is beyond it at the end of the event loop, this will allow some time for the popup to show after we append the child.
function ask(options){ return new Promise(function(resolve) { //FIRST CREATE A POPUP WITH ALL THE FIELDS IN IT const popup = document.createElement('form'); popup.classList.add('popup'); popup.insertAdjacentHTML( 'afterbegin', ` <fieldset> <label>${options.title}</label> </fieldset> ` ); //CHECK IF THEY WANT TO CANCEL if (options.cancel){ const skipButton = document.createElement('button'); skipButton.type = 'button'; // important because if we don't tell it a type it will assume it's a submit. skipButton.textContent = 'Cancel'; //TODO: LISTEN FOR A CLICK ON THAT CANCEL BUTTON }; //LISTEN FOR THE SUBMIT EVENT ON THE INPUTS //WHEN SOMEONE DOES SUBMIT, RESOLVE THE DATA THAT WAS IN THE INPUT BOX //INSERT THAT POPUP INTO THE DOM document.body.appendChild(popup); // PUT A SMALL TIMEOUT BEFORE WE ADD THE OPEN CLASS setTimeout(function(){ popup.classList.add('open'); }, 50); }; popup.classList.add('open'); }); };
Instead of using a setTimeout
like we used above we can leverage async
and await
.
function wait (ms = 0) { return new Promise(resolve => setTimeout(resolve, ms)); }; function ask(options){ return new Promise( async function(resolve) { //FIRST CREATE A POPUP WITH ALL THE FIELDS IN IT const popup = document.createElement('form'); popup.classList.add('popup'); popup.insertAdjacentHTML( 'afterbegin', ` <fieldset> <label>${options.title}</label> <input type="text" name="input" required> <button type="submit">Submit</button> </fieldset> ` ); //CHECK IF THEY WANT TO CANCEL if (options.cancel){ const skipButton = document.createElement('button'); skipButton.type = 'button'; // important because if we don't tell it a type it will assume it's a submit. skipButton.textContent = 'Cancel'; popup.firstElementChild.appendChild(skipButton); // this needs to be firstElementChild because the space on line 11 otherwise you could use firstChild //TODO: LISTEN FOR A CLICK ON THAT CANCEL BUTTON }; //LISTEN FOR THE SUBMIT EVENT ON THE INPUTS //WHEN SOMEONE DOES SUBMIT, RESOLVE THE DATA THAT WAS IN THE INPUT BOX //INSERT THAT POPUP INTO THE DOM document.body.appendChild(popup); // PUT A SMALL TIMEOUT BEFORE WE ADD THE OPEN CLASS await wait(50); popup.classList.add('open'); }); };
We use popup.firstElementChild.appendChild(skipButton);
. This needs to be firstElementChild because the space on line 11 creates an empty node - otherwise you could use firstChild
if you removed the space on line 11 before the <fieldset>
//LISTEN FOR THE SUBMIT EVENT ON THE INPUTS
We want to prevent default responses when clicking on buttons on the popup form so that we can pull the value out and resolve it. We learned about this in our events article in the Prevent Default Section.
function wait (ms = 0) { return new Promise(resolve => setTimeout(resolve, ms)); }; function ask(options){ return new Promise( async function(resolve) { //FIRST CREATE A POPUP WITH ALL THE FIELDS IN IT const popup = document.createElement('form'); popup.classList.add('popup'); popup.insertAdjacentHTML( 'afterbegin', ` <fieldset> <label>${options.title}</label> <input type="text" name="input" required> <button type="submit">Submit</button> </fieldset> ` ); //CHECK IF THEY WANT TO CANCEL if (options.cancel){ const skipButton = document.createElement('button'); skipButton.type = 'button'; // important because if we don't tell it a type it will assume it's a submit. skipButton.textContent = 'Cancel'; popup.firstElementChild.appendChild(skipButton); // this needs to be firstElementChild because the space on line 11 otherwise you could use firstChild //TODO: LISTEN FOR A CLICK ON THAT CANCEL BUTTON }; //LISTEN FOR THE SUBMIT EVENT ON THE INPUTS popup.addEventListener('submit', function(event){ event.preventDefault(); console.log(event.target); // gives us the form console.log(event.target.input); // gives us the input box console.log(event.target.input.value); // gives us the input value console.log('Submitted'); resolve(event.target.input.value); destroyPopup(popup); }; //WHEN SOMEONE DOES SUBMIT, RESOLVE THE DATA THAT WAS IN THE INPUT BOX //INSERT THAT POPUP INTO THE DOM document.body.appendChild(popup); // PUT A SMALL TIMEOUT BEFORE WE ADD THE OPEN CLASS await wait(50); popup.classList.add('open'); }); };
Line 26-33 added
If we run ask({title: 'does this work', cancel: true});
in the console we will see we get a promise. If we want to get the value out of it, we have to make it say "await ask({title: 'does this work', cancel: true});
.
We can pass a third eventListener option to only allow it to run once.
function wait (ms = 0) { return new Promise(resolve => setTimeout(resolve, ms)); }; function ask(options){ return new Promise( async function(resolve) { //FIRST CREATE A POPUP WITH ALL THE FIELDS IN IT const popup = document.createElement('form'); popup.classList.add('popup'); popup.insertAdjacentHTML( 'afterbegin', ` <fieldset> <label>${options.title}</label> <input type="text" name="input" required> <button type="submit">Submit</button> </fieldset> ` ); //CHECK IF THEY WANT TO CANCEL if (options.cancel){ const skipButton = document.createElement('button'); skipButton.type = 'button'; // important because if we don't tell it a type it will assume it's a submit. skipButton.textContent = 'Cancel'; popup.firstElementChild.appendChild(skipButton); // this needs to be firstElementChild because the space on line 11 otherwise you could use firstChild //TODO: LISTEN FOR A CLICK ON THAT CANCEL BUTTON }; //LISTEN FOR THE SUBMIT EVENT ON THE INPUTS popup.addEventListener('submit', function(event){ event.preventDefault(); console.log(event.target); // gives us the form console.log(event.target.input); // gives us the input box console.log(event.target.input.value); // gives us the input value console.log('Submitted'); resolve(event.target.input.value); destroyPopup(popup); }, {once: true}); //WHEN SOMEONE DOES SUBMIT, RESOLVE THE DATA THAT WAS IN THE INPUT BOX //INSERT THAT POPUP INTO THE DOM document.body.appendChild(popup); // PUT A SMALL TIMEOUT BEFORE WE ADD THE OPEN CLASS await wait(50); popup.classList.add('open'); }); };
Added line 34. This is the 3rd argument that only allows the eventListener to be called one time.
Now we need to remove the submit event listener from the DOM entirely. We will need to create a destroyPopup function that will force the popup to fade out. We will need to leverage the async
await
function. We want to make popup = null;
because if not done, the popup still exists in Javascript memory. This is not good for performance. Let's create a variable that we pass the (popup)
argument to prevent reassigning a parameter. Take a look at lines 5-12 below.
function wait (ms = 0) { return new Promise(resolve => setTimeout(resolve, ms)); }; async function destroyPopup(popup){ let myPopup = popup; popup.classList.remove('open'); await wait(1000); //REMOVE THE POPUP ENTIRELY popup.remove(); myPopup = null; }; function ask(options){ return new Promise( async function(resolve) { //FIRST CREATE A POPUP WITH ALL THE FIELDS IN IT const popup = document.createElement('form'); popup.classList.add('popup'); popup.insertAdjacentHTML( 'afterbegin', ` <fieldset> <label>${options.title}</label> <input type="text" name="input" required> <button type="submit">Submit</button> </fieldset> ` ); //CHECK IF THEY WANT TO CANCEL if (options.cancel){ const skipButton = document.createElement('button'); skipButton.type = 'button'; // important because if we don't tell it a type it will assume it's a submit. skipButton.textContent = 'Cancel'; popup.firstElementChild.appendChild(skipButton); // this needs to be firstElementChild because the space on line 11 otherwise you could use firstChild //TODO: LISTEN FOR A CLICK ON THAT CANCEL BUTTON }; //LISTEN FOR THE SUBMIT EVENT ON THE INPUTS popup.addEventListener('submit', function(event){ event.preventDefault(); console.log(event.target); // gives us the form console.log(event.target.input); // gives us the input box console.log(event.target.input.value); // gives us the input value console.log('Submitted'); resolve(event.target.input.value); destroyPopup(popup); }, {once: true}; //WHEN SOMEONE DOES SUBMIT, RESOLVE THE DATA THAT WAS IN THE INPUT BOX //INSERT THAT POPUP INTO THE DOM document.body.appendChild(popup); // PUT A SMALL TIMEOUT BEFORE WE ADD THE OPEN CLASS await wait(50); popup.classList.add('open'); }); };
Note the
async
function we created.async
is required on the parent function anytime we leverage an await. This tells the browser "hey there is something in this function that will need to use a callback" and is handled like a reference point for the browser later.
// Select All Buttons That Have A Question
In the HTML you will see a data attribute of data-question
- we are going to select these and then run a loop over them that will allow us to use an eventListener.
function wait (ms = 0) { return new Promise(resolve => setTimeout(resolve, ms)); }; async function destroyPopup(popup){ let myPopup = popup; popup.classList.remove('open'); await wait(1000); //REMOVE THE POPUP ENTIRELY popup.remove(); myPopup = null; }; function ask(options){ return new Promise( async function(resolve) { //FIRST CREATE A POPUP WITH ALL THE FIELDS IN IT const popup = document.createElement('form'); popup.classList.add('popup'); popup.insertAdjacentHTML( 'afterbegin', ` <fieldset> <label>${options.title}</label> <input type="text" name="input" reuired> <button type="submit">Submit</button> </fieldset> ` ); //CHECK IF THEY WANT TO CANCEL if (options.cancel){ const skipButton = document.createElement('button'); skipButton.type = 'button'; // important because if we don't tell it a type it will assume it's a submit. skipButton.textContent = 'Cancel'; popup.firstElementChild.appendChild(skipButton); // this needs to be firstElementChild because the space on line 11 otherwise you could use firstChild //TODO: LISTEN FOR A CLICK ON THAT CANCEL BUTTON }; //LISTEN FOR THE SUBMIT EVENT ON THE INPUTS popup.addEventListener('submit', function(event){ event.preventDefault(); console.log(event.target); // gives us the form console.log(event.target.input); // gives us the input box console.log(event.target.input.value); // gives us the input value console.log('Submitted'); resolve(event.target.input.value); destroyPopup(popup); }, {once: true}; //WHEN SOMEONE DOES SUBMIT, RESOLVE THE DATA THAT WAS IN THE INPUT BOX //INSERT THAT POPUP INTO THE DOM document.body.appendChild(popup); // PUT A SMALL TIMEOUT BEFORE WE ADD THE OPEN CLASS await wait(50); popup.classList.add('open'); }); }; // Select All Buttons That Have A Question async function askQuestion (event) { const button = event.currentTarget; const shouldCancel = 'cancel;' in button.hasAttribute('data-cancel'); const answer = await ask({ title: button.dataset.question, cancel: shouldCancel}); console.log(answer); }; const buttons = document.querySelectorAll('[data-question]'); buttons.forEach(button => button.addEventListener('click', askQuestion));
In the HTML our dataset-cancel
is just present with no value, so if present - it's truthy. If it's not present it's falsey. const shouldCancel = 'cancel;' in button.hasAttribute('data-cancel');
on line 57 allows you to check if there is a dataset-cancel
present on the HTML and if so, the result will be "true".
//TODO: LISTEN FOR A CLICK ON THAT CANCEL BUTTON
We want to pass another eventListener into the cancel button that only allows hitting the cancel button once and closes the popup on line 34-40 below.
function wait (ms = 0) { return new Promise(resolve => setTimeout(resolve, ms)); }; async function destroyPopup(popup){ let myPopup = popup; popup.classList.remove('open'); await wait(1000); //REMOVE THE POPUP ENTIRELY popup.remove(); myPopup = null; }; function ask(options){ return new Promise( async function(resolve) { //FIRST CREATE A POPUP WITH ALL THE FIELDS IN IT const popup = document.createElement('form'); popup.classList.add('popup'); popup.insertAdjacentHTML( 'afterbegin', ` <fieldset> <label>${options.title}</label> <input type="text" name="input" required> <button type="submit">Submit</button> </fieldset> ` ); //CHECK IF THEY WANT TO CANCEL if (options.cancel){ const skipButton = document.createElement('button'); skipButton.type = 'button'; // important because if we don't tell it a type it will assume it's a submit. skipButton.textContent = 'Cancel'; popup.firstElementChild.appendChild(skipButton); // this needs to be firstElementChild because the space on line 11 otherwise you could use firstChild //TODO: LISTEN FOR A CLICK ON THAT CANCEL BUTTON skipButton.addEventListener( 'click', function() { resolve(null); destroyPopup(popup); }, {once: true}); }; //LISTEN FOR THE SUBMIT EVENT ON THE INPUTS popup.addEventListener('submit', function(event){ event.preventDefault(); console.log(event.target); // gives us the form console.log(event.target.input); // gives us the input box console.log(event.target.input.value); // gives us the input value console.log('Submitted'); resolve(event.target.input.value); destroyPopup(popup); }, {once: true}; //WHEN SOMEONE DOES SUBMIT, RESOLVE THE DATA THAT WAS IN THE INPUT BOX //INSERT THAT POPUP INTO THE DOM document.body.appendChild(popup); // PUT A SMALL TIMEOUT BEFORE WE ADD THE OPEN CLASS await wait(50); popup.classList.add('open'); }); }; // Select All Buttons That Have A Question async function askQuestion (event) { const button = event.currentTarget; const shouldCancel = 'cancel;' in button.hasAttribute('data-cancel'); const answer = await ask({ title: button.dataset.question, cancel: shouldCancel}); console.log(answer); }; const buttons = document.querySelectorAll('[data-question]'); buttons.forEach(button => button.addEventListener('click', askQuestion));
Prompt Questions In Series
const questions = [ {'title: what is your name?'}, {'title: what is your age?'}, {'title: what is your pet's name?'}, ];
Can we do the above to chain a series of questions in the prompt? Let's try that using something like this:
promise.all
wraps all the promises into a single promise and waits for the whole thing to complete. We could then chain answers using .then
right?
const answers = Promise.all([ ask(questions[0]), ask(questions[1]), ask(questions[2]), ]).then(answers => { console.log(answers); };
The odd thing is that it works but it asks out of order and pops up all 3 of the questions at the same time. This isn't a really good solution. In many cases, this is what we would like.
A better way to do this would be to do something like this:
Promise.all(questions.map(ask)).then(data => { console.log(data); });
This works but we still have the problem of the UI not working like we'd like.
We could loop over them perhaps.
questions.forEach(async function(questions) { const answer = await ask(questions; console.log(answer); });
That didn't wait for anything but it did work - it did resolve but they all ran at the same time. So how do we make a map or async function that doesn't populate them all at the same time?
We can use a for of
, let's take a look!
function wait(ms = 0) { return new Promise(resolve => setTimeout(resolve, ms)); }; async function destroyPopup(popup) { popup.classList.remove('open'); await wait(1000); //REMOVE THE POPUP ENTIRELY popup.remove(); popup = null; }; function ask(options) { return new Promise(async function (resolve) { //FIRST CREATE A POPUP WITH ALL THE FIELDS IN IT const popup = document.createElement('form'); popup.classList.add('popup'); popup.insertAdjacentHTML( 'afterbegin', ` <fieldset> <label>${options.title}</label> <input type="text" name="input" required> <button type="submit">Submit</button> </fieldset> ` ); //CHECK IF THEY WANT TO CANCEL if (options.cancel) { const skipButton = document.createElement('button'); skipButton.type = 'button'; // important because if we don't tell it a type it will assume it's a submit. skipButton.textContent = 'Cancel'; popup.firstElementChild.appendChild(skipButton); // this needs to be firstElementChild because the space on line 11 otherwise you could use firstChild //TODO: LISTEN FOR A CLICK ON THAT CANCEL BUTTON skipButton.addEventListener( 'click', function () { resolve(null); destroyPopup(popup); }, { once: true } ); }; //LISTEN FOR THE SUBMIT EVENT ON THE INPUTS popup.addEventListener( 'submit', function (event) { event.preventDefault(); console.log(event.target); // gives us the form console.log(event.target.input); // gives us the input box console.log(event.target.input.value); // gives us the input value console.log('Submitted'); resolve(event.target.input.value); destroyPopup(popup); }, { once: true }); //WHEN SOMEONE DOES SUBMIT, RESOLVE THE DATA THAT WAS IN THE INPUT BOX //INSERT THAT POPUP INTO THE DOM document.body.appendChild(popup); // PUT A SMALL TIMEOUT BEFORE WE ADD THE OPEN CLASS await wait(50); popup.classList.add('open'); }); }; // Select All Buttons That Have A Question async function askQuestion(event) { const button = event.currentTarget; const cancel = 'cancel;' in button.hasAttribute('data-cancel'); const answer = await ask({ title: button.dataset.question, cancel, }); console.log(answer); }; const buttons = document.querySelectorAll('[data-question]'); buttons.forEach(button => button.addEventListener('click', askQuestion)); const questions = [ {title: `What is your name?`}, {title: `What is your age?`, cancel: true}, {title: `What is your pet's name?`}, ]; async function askMany() { for (const question of questions) { const answer = await ask(question); console.log(answer); }; }; askMany();
You can see that these pause a loop by await
ing something inside of it. Let's map this so it outputs an array.
function wait(ms = 0) { return new Promise(resolve => setTimeout(resolve, ms)); }; async function destroyPopup(popup) { popup.classList.remove('open'); await wait(1000); //REMOVE THE POPUP ENTIRELY popup.remove(); popup = null; }; function ask(options) { return new Promise(async function (resolve) { //FIRST CREATE A POPUP WITH ALL THE FIELDS IN IT const popup = document.createElement('form'); popup.classList.add('popup'); popup.insertAdjacentHTML( 'afterbegin', ` <fieldset> <label>${options.title}</label> <input type="text" name="input" required> <button type="submit">Submit</button> </fieldset> ` ); //CHECK IF THEY WANT TO CANCEL if (options.cancel) { const skipButton = document.createElement('button'); skipButton.type = 'button'; // important because if we don't tell it a type it will assume it's a submit. skipButton.textContent = 'Cancel'; popup.firstElementChild.appendChild(skipButton); // this needs to be firstElementChild because the space on line 11 otherwise you could use firstChild //TODO: LISTEN FOR A CLICK ON THAT CANCEL BUTTON skipButton.addEventListener( 'click', function () { resolve(null); destroyPopup(popup); }, { once: true } ); }; //LISTEN FOR THE SUBMIT EVENT ON THE INPUTS popup.addEventListener( 'submit', function (event) { event.preventDefault(); console.log(event.target); // gives us the form console.log(event.target.input); // gives us the input box console.log(event.target.input.value); // gives us the input value console.log('Submitted'); resolve(event.target.input.value); destroyPopup(popup); }, { once: true }); //WHEN SOMEONE DOES SUBMIT, RESOLVE THE DATA THAT WAS IN THE INPUT BOX //INSERT THAT POPUP INTO THE DOM document.body.appendChild(popup); // PUT A SMALL TIMEOUT BEFORE WE ADD THE OPEN CLASS await wait(50); popup.classList.add('open'); }); }; // Select All Buttons That Have A Question async function askQuestion(event) { const button = event.currentTarget; const cancel = 'cancel;' in button.hasAttribute('data-cancel'); const answer = await ask({ title: button.dataset.question, cancel, }); console.log(answer); }; const buttons = document.querySelectorAll('[data-question]'); buttons.forEach(button => button.addEventListener('click', askQuestion)); const questions = [ {title: `What is your name?`}, {title: `What is your age?`, cancel: true}, {title: `What is your pet's name?`}, ]; async function asyncMap(array, callback) { const results = []; for(const item of array) { results.push(await callback(item)); }; return results; }; async function go() { const answers = await asyncMap(questions, ask); console.log(answers); }; go(); // async function askMany() { // for (const question of questions) { // const answer = await ask(question); // console.log(answer); // }; // }; // askMany();
Final Thoughts & My Ask
Async and Await are methods we can use to eliminate the use of static eventLoops/promises and callbacks. In this project, you were able to create a popup form that shows up once, allows you to chain prompts, and then pops the inputs into an array. I hope you enjoyed this project as much as I enjoyed writing about it.
If you found this article helpful, share/retweet it and follow me on twitter @codingwithdrewk! There is so much more in Wes' courses I think you will find valuable as I have. I'm learning so much and really enjoying the course, Wes has an amazingly simple way to explain the difficult bits of Javascript that other courses I've taken could only wish. You can view the course over at WesBos.com. (I am in no way getting any referrals or kickbacks for recommending this)
Drew is a seasoned DevOps Engineer with a rich background that spans multiple industries and technologies. With foundational training as a Nuclear Engineer in the US Navy, Drew brings a meticulous approach to operational efficiency and reliability. His expertise lies in cloud migration strategies, CI/CD automation, and Kubernetes orchestration. Known for a keen focus on facts and correctness, Drew is proficient in a range of programming languages including Bash and JavaScript. His diverse experiences, from serving in the military to working in the corporate world, have equipped him with a comprehensive worldview and a knack for creative problem-solving. Drew advocates for streamlined, fact-based approaches in both code and business, making him a reliable authority in the tech industry.