Am looking to use a promise to handle a modal window, such that when the modal window is called via the await
syntax, the synchronous execution of the calling function is suspended until the user responds to the modal. The code snippet below extracts the essential elements of the problem. Although it functions, am not sure whether this is a promise antipattern, or whether I'm introducing hidden complexities should errors be thrown in the onclick
handlers. The closest Q&A I could find ( Resolve promise at a later time ) doesn't quite answer my issue, as the answers don't appear to apply to a promise held in reserve waiting on a user event to occur...
My stripped down Modal
class and sample execution includes the following key elements...
- class
Modal
constructs the modal DOM elements, and appends them to the HTML document. - class
Modal
has a method calledshow
which shows the modal (in this simplified example, three buttons) and sets up a promise. Theresolve
andreject
functions of the promise are then held asModal
instance attributes, specificallyresolveFunction
andrejectFunction
. - Only when the user hits Okay, Cancel, or CancelThrow is the promise resolved or rejected.
- function
openModal
is the function that sets up and shows the modal, and then suspends waiting on the resolution of the promise created by the modalshow()
method.
<html><head>
<style>
#ModalArea {
display: none;
}
#ModalArea.show {
display: block;
}
</style>
<script>
class Modal {
constructor() {
this.parentNode = document.getElementById( 'ModalArea' );
let okay = document.createElement( 'BUTTON' );
okay.innerText = 'Okay';
okay.onclick = ( event ) => {
this.resolveFunction( 'Okay button clicked!' )
};
this.parentNode.appendChild( okay );
let cancel = document.createElement( 'BUTTON' );
cancel.innerText = 'Cancel';
cancel.onclick = ( event ) => {
this.rejectFunction( 'Cancel button clicked!' )
};
this.parentNode.appendChild( cancel );
let cancelThrow = document.createElement( 'BUTTON' );
cancelThrow.innerText = 'Cancel w/Throw';
cancelThrow.onclick = ( event ) => {
try {
throw 'Thrown error!';
} catch( err ) {
this.rejectFunction( err );
}
this.rejectFunction( 'CancelThrow button clicked!' );
};
this.parentNode.appendChild( cancelThrow );
}
async show() {
this.parentNode.classList.add( 'show' );
// Critical code:
//
// Is this appropriate to stash away the resolve and reject functions
// as attributes to a class object, to be used later?!
//
return new Promise( (resolve, reject) => {
this.resolveFunction = resolve;
this.rejectFunction = reject;
});
}
}
async function openModal() {
// Create new modal buttons...
let modal = new Modal();
// Show the buttons, but wait for the promise to resolve...
try {
document.getElementById( 'Result' ).innerText += await modal.show();
} catch( err ) {
document.getElementById( 'Result' ).innerText += err;
}
// Now that the promise resolved, append more text to the result.
document.getElementById( 'Result' ).innerText += ' Done!';
}
</script>
</head><body>
<button onclick='openModal()'>Open Modal</button>
<div id='ModalArea'></div>
<div id='Result'>Result: </div>
</body></html>
Are there pitfalls to the way I'm handling the resolve
and reject
functions, and if so, is there a better design pattern to handle this use case?
EDIT
Based on Roamer-1888's guidance, I've arrived at the following cleaner implementation of the deferred promise... (Note that the test of Cancel w/Throw
results in the console showing an Uncaught (in Promise)
error, but processing continues as defined...)
<html><head>
<style>
#ModalArea {
display: none;
}
#ModalArea.show {
display: block;
}
</style>
<script>
class Modal {
constructor() {
this.parentNode = document.getElementById( 'ModalArea' );
this.okay = document.createElement( 'BUTTON' );
this.okay.innerText = 'Okay';
this.parentNode.appendChild( this.okay );
this.cancel = document.createElement( 'BUTTON' );
this.cancel.innerText = 'Cancel';
this.parentNode.appendChild( this.cancel );
this.cancelThrow = document.createElement( 'BUTTON' );
this.cancelThrow.innerText = 'Cancel w/Throw';
this.parentNode.appendChild( this.cancelThrow );
}
async show() {
this.parentNode.classList.add( 'show' );
let modalPromise = new Promise( (resolve, reject) => {
this.okay.onclick = (event) => {
resolve( 'Okay' );
};
this.cancel.onclick = ( event ) => {
reject( 'Cancel' );
};
this.cancelThrow.onclick = ( event ) => {
try {
throw new Error( 'Test of throwing an error!' );
} catch ( err ) {
console.log( 'Event caught error' );
reject( err );
}
};
});
modalPromise.catch( e => {
console.log( 'Promise catch fired!' );
} );
// Clear out the 'modal' buttons after the promise completes.
modalPromise.finally( () => {
this.parentNode.innerHTML = '';
});
return modalPromise;
}
}
async function openModal() {
// Create new modal buttons...
let modal = new Modal();
document.getElementById( 'Result' ).innerHTML = 'Result: ';
// Show the buttons, but wait for the promise to resolve...
try {
document.getElementById( 'Result' ).innerText += await modal.show();
} catch( err ) {
document.getElementById( 'Result' ).innerText += err;
}
// Now that the promise resolved, append more text to the result.
document.getElementById( 'Result' ).innerText += ' Done!';
}
</script>
</head><body>
<button onclick='openModal()'>Open Modal</button>
<div id='ModalArea'></div>
<div id='Result'></div>
</body></html>
Something still seems off though. Having added a promise catch
, when selecting Cancel w/Throw
, the error propagates through modalPromise.catch
, but the console still logs the following error:
Uncaught (in promise) Error: Test of throwing an error!
at HTMLButtonElement.cancelThrow.onclick