Creating modal dialogs is so hard

Custom dialogs can (by definition) not be copy and paste, unfortunately – you’ll need to understand the concepts to build dialogs – it is nothing that can be done by copying and pasting some code (dialogs are too diverse for that). The only thing that can be done is to show an example of how it can be achieved – this is nothing to be copied and pasted, however.

I showed an example of how to use my storage helper to achieve this in its Readme.

Where does your code “give you” null or undefined (no offense, but this isn’t precise enough for anyone to really give you a good answer without having to make a guess what you mean)? I’d love to help, but I honestly don’t know what you’re saying here…

Edit: Looking at @kerrishotts answer below, I stand corrected in assuming there isn’t a good answer anyone can provide :wink:

1 Like

Dialog.close()

dialog.close() shouldn’t be used to return input from fields, it’s intended as a signaling mechanism only. This is because if the user presses ESC, dialog.close('reasonCanceled') is called for you automatically, which means you’ll have a harder time differentiating between system-generated data and user-generated data. Instead, use dialog.close('ok') (or similar) to indicate that the user is finished with the dialog.

Listening for form submission

Assume you’ve got something like this:

ourDialog = document.createElement("dialog");
ourDialog.innerHTML=`
<form>
    <h1>Title</h1>
    <hr />
    <label>
        <span>Button Width</span>
        <input type="text" id="buttonwidth" />
    </label>
    <footer>
        <button uxp-variant="primary" id="cancelbutton">Cancel</button>
        <button uxp-variant="cta" id="okbutton">Set width</button>
    </footer>
</form>
`;
document.body.appendChild(ourDialog); // <-- this is super important; without it the dialog will fail to show

Note: Just now realizing the docs don’t have document.body.appendChild(dialog); for the non-react example. D’oh! We’ll fix that…

Now, to get a reference to the elements within, you can use dialog.querySelector:

const form = ourDialog.querySelector("form");
const okButton = ourDialog.querySelector("#okbutton");
const cancelButton = ourDialog.querySelector("#cancelbutton");

There are several things at play when it comes to submitting forms:

  • User presses ENTER
  • User clicks the “ok” button (here, it’s “Set Width”)
  • User clicks the “Cancel” button
  • User presses ESC

Unless the user is already focused on a button, ENTER will raise form.onsubmit. Pressing ENTER or clicking a button will raise that button’s onclick event. And pressing ESC will call dialog.close('reasonCanceled').

There’s two things you need to be concerned with when responding to user input:

  1. closing the dialog and indicating user intent
  2. reading the dialog’s data to act on the user’s intent

The two should be kept fairly distinct – the result of dialog.showModal() for example, should only ever deal with #1 – closing the dialog and indicating whether the user wants to proceed or cancel.

The second option can be handled after that point by reading the values of form elements in the dialogs.

(There’s a third step here – disposing of/reusing the dialog which I’ll skip for now.)

To handle #1, we need to wire up some event handlers:

form.onsubmit = function() {
    evt.preventDefault(); // <-- otherwise, dialog.close() is called with nothing; will fix docs
    ourDialog.close("ok");
}
okButton.onclick = function(evt) {
    evt.preventDefault();
    ourDialog.close("ok");
}
cancelButton.onclick = function() {
    ourDialog.close("reasonCanceled");
}

This will now properly dismiss the dialog whatever the user clicks or presses.

Reading the user’s data

Dismissal is not the final goal, of course, so you need to get at the user’s data. We can do this pretty easily back where we call showModal – the trick is to remember that the dialog itself and all its elements are still in the DOM:

return ourDialog.showModal()
.then(response => {
    if (response === "ok") {
        // user wants to set the button width
        const buttonWidthElement = ourDialog.querySelector("#buttonwidth");
        const buttonWidth = Number(buttonWidthElement.value);
        // now we can do something with it
        // for example:
        selection.items[0].resize(buttonWidth, selection.items[0].height);
   }
})
.catch(err => {
    console.error(err.message);
});

Putting it all together

Here’s the final snippet, which should work for you (assuming you’ve got a manifest that has a menu pointing at showButtonWidthDialog):

let ourDialog;
function showButtonWidthDialog(selection) {
    if (!ourDialog) {
        // the dialog has never been created before; create it now
        ourDialog = document.createElement("dialog");
        ourDialog.innerHTML = `
<form>
    <h1>Title</h1>
    <hr />
    <label>
        <span>Button Width</span>
        <input type="text" id="buttonwidth" />
    </label>
    <footer>
        <button uxp-variant="primary" id="cancelbutton">Cancel</button>
        <button uxp-variant="cta" id="okbutton">Set width</button>
    </footer>
</form>`;
        document.body.appendChild(ourDialog);
    }

    // get references to the dialog
    const form = ourDialog.querySelector("form");
    const okButton = ourDialog.querySelector("#okbutton");
    const cancelButton = ourDialog.querySelector("#cancelbutton");

    // wire up our events
    form.onsubmit = function(evt) {
        evt.preventDefault();
        ourDialog.close("ok");
    }
    okButton.onclick = function(evt) {
        evt.preventDefault();
        ourDialog.close("ok");
    }
    cancelButton.onclick = function() {
        ourDialog.close("reasonCanceled");
    }

    // show the dialog (returning a promise), and handle the response
    return ourDialog.showModal()
    .then(response => {
        if (response === "ok") {
            // user wants to set the button width
            const buttonWidthElement = ourDialog.querySelector("#buttonwidth");
            const buttonWidth = Number(buttonWidthElement.value);
            // now we can do something with it
            // for example:
            selection.items[0].resize(buttonWidth, selection.items[0].height);
       }
    })
    .catch(err => {
        console.error(err.message);
    });
}

module.exports = {
    commands: {
        showButtonWidthDialog
    }
}
4 Likes

Also – what kind of dialog are you trying to build? What kinds of inputs and such? That might help us provide a sample that is closer to what you need.

2 Likes

thanks @kerrishotts for this thoroughly explanation but the onsubmit when pressing enter seems to never fire up.

This is my problem, not the dialog.close() per se. This and returning previous form input value from previous utilisation of the plugin, which I don’t know how it’s made yet.

Other than these 2 points I pretty much I understand how the system of dialogs work :slight_smile:

Like in the code below, I never get console.log(“FORM SUBMITTED”) in the console if the two button do nothing (just preventDefault in this example):

let dialog;
//  lazy load the dialog
function getDialog() {
if (dialog == null) {
    //  create the dialog
    dialog = document.createElement("dialog");

     //  create the form element
    //  the form element has default styling and spacing
    let form = document.createElement("form");
    dialog.appendChild(form);
    //  don't forget to set your desired width
    form.style.width = 200;
    form.id= "myForm";

    //  add your content
    let hello = document.createElement("h1");
    hello.textContent = "Let's see if onsubmit works!";
    form.appendChild(hello);

    //  create a footer to hold your form submit and cancel buttons
    let footer = document.createElement("footer");
    form.appendChild(footer)
    // include at least one way to close the dialog

    let okButton = document.createElement("button");
    okButton.uxpVariant = "warning";
    okButton.textContent = "OK";
    // okButton.onclick = (e) => {
    //     dialog.close("i have hit ok button");
    // }    
    okButton.onclick = (e) => e.preventDefault();
    footer.appendChild(okButton);

    let closeButton = document.createElement("button");
    closeButton.uxpVariant = "primary";
    closeButton.textContent = "Close";
    // closeButton.onclick = (e) => dialog.close("I hit the close button.");
    closeButton.onclick = (e) => e.preventDefault();
    footer.appendChild(closeButton);

    form.onsubmit = function(e) {
        e.preventDefault();
        // dialog.close("ok");
        console.log("FORM SUBMITTED!") // < - - - - - - IN NEVER GET THIS MESSAGE!
    }
    
}
return dialog;
}


function menuCommand(selection) {
    // console.log(selection.items[0].name);
    //  attach the dialog to the body and show it
    document.body.appendChild(getDialog()).showModal()
    .then(result => {
        // handle dialog result
        // if canceled by ESC, will be "reasonCanceled"
        console.log("The form is " + document.getElementById("myForm").value);
        console.log('Promise received: ' + result);
    });    
    
}

//  this file is deliberately written in low level document.appendChild format.
module.exports = {
    commands: {
        menuCommand
    }
};

In other words, if I take your code (but slightly modified) you will see that we never get the “ok SUBMITTED” in the console.log when pressing Enter:

form.onsubmit = function(evt) {
    evt.preventDefault();
    ourDialog.close("ok SUBMITTED");
    console.log("ok SUBMITTED");
}
// okButton.onclick = function(evt) {
//     evt.preventDefault();
//     ourDialog.close("ok button");
//     console.log("ok button");
// }
// cancelButton.onclick = function() {
//     ourDialog.close("reasonCanceled");
//     console.log("reasonCanceled");
// }

How can we trigger what happens when we hit the Enter key?

Oooooooooh, I think you may be running into a bug based on your tabbing preferences. If you open up your keyboard preferences > keyboard shortcuts, do you have TAB set to focus all controls or just text boxes and lists?

If the latter, we have a bug where ENTER doesn’t submit correctly.

Also — just realized I hadn’t asked — are you running macos or Windows?

1 Like

I only have windows 10 :slight_smile:

Ok — maybe we have a bug on win10 then. I’ll try in my VM when I get back to my laptop.

1 Like
 maybe we have a bug on win10 then

it could be, because no matter how many times I may press TAB and then ENTER or just enter, I will never reach for the form.onsubmit to be triggered.

In my example that I have presented above: Creating modal dialogs is so hard - #25 by aldobsom, hitting enter as soon as the ui appears, will trigger the OK button, if it is the first one appended as a child to the form footer:
image

let okButton = document.createElement("button");
    okButton.uxpVariant = "warning";
    okButton.textContent = "OK";
    // okButton.onclick = (e) => {
    //     dialog.close("i have hit ok button");
    // }    
    okButton.onclick = (e) => e.preventDefault();
    footer.appendChild(okButton);

    let closeButton = document.createElement("button");
    closeButton.uxpVariant = "primary";
    closeButton.textContent = "Close";
    // closeButton.onclick = (e) => dialog.close("I hit the close button.");
    closeButton.onclick = (e) => e.preventDefault();
    footer.appendChild(closeButton);

if the Close button is the first one appended then it will be triggered by default if Enter key is pressed.

Yep – definitely a bug in Windows. Sorry about all that.

Here’s a way to check for the ENTER button that works on my VM (and doesn’t break macOS either):

form.onkeydown = function(evt) {
  if (evt.key === "Enter") {
    if (evt.target.tagName !== "BUTTON") {
      evt.preventDefault();
      ourDialog.close("ok");
    }
  }
}

I’ll file the appropriate bugs tomorrow.

1 Like

I thought it was me :slight_smile: thanks!

1 Like