Saturday, June 22, 2024

Return to Sender: POST a Suitelet without redirecting

    We all know the pain of submitting a form in NetSuite, only to have it error and lose the input. This happens because NetSuite forces a redirect to the new error page. It turns out we don't have to redirect to do something with our data (or display an error).

    We can POST our data back to NetSuite from a Client Script. This means we don't have to redirect, and we don't have to lose our data. While this can be done on any scriptable page, it's easiest on Suitelets. Let's make a simple example with a couple fields. Normally we'd add a submit button, but this would bring along the redirect logic; instead we'll add a regular button and define its functionality in a Client Script.

/**
 * @NApiVersion 2.1
 * @NScriptType Suitelet
 */
define(['N/ui/serverWidget'], (ui) => {
    function onRequest(context) {
        const { response, request } = context;

        if (request.method == 'GET') {
            let form = ui.createForm('Test Suitelet');
            //include out client script for our button to call
            form.clientScriptModulePath = './our_client_script_cs.js';

            form.addField({ id: 'custpage_field1', type: 'text', label: 'Test 1' });
            form.addField({ id: 'custpage_field2', type: 'text', label: 'Test 2' });
            form.addButton({ id: 'buttonid', label: 'Button', 
                functionName: 'cs_function_name' });

            response.writePage(form);
        }
    }

    return { onRequest };
});

   Depending on what we need to do, the CS might be able to handle it. Very often we need the governance, permissions, or modules of a server script though. Whatever server-side functionality we need, can be added to the same Suitelet that's rending our page; We'll just add a parameter so we can call it instead of the page. We can pass the rest of our data via the body of the POST.

/**
 * @NApiVersion 2.1
 * @NScriptType ClientScript
*/
define(['N/url', 'N/currentRecord', 'N/https'], (url, currentRec, https) => {
    function pageInit(context) { }

    function cs_function_name() {
const currentRecord = currentRec.get(); //no context obj, so use currentRec //Build POST body from a couple field values const [fieldValue1, fieldValue2] = ['custpage_field1', 'custpage_field2'].map(fld => currentRecord.getValue(fld)); const body = JSON.stringify({ fieldValue1, fieldValue2 }); const newURL = url.resolveScript({ scriptId: 'customscript_our_suitelet',
            deploymentId: 1, params: { server_flag: 1 } });
        const response = https.post({ url: newURL, body });
        if (response?.body) alert(JSON.parse(response.body)); //show Suitelet response 
    }

    return { pageInit, cs_function_name };
});

Reaction time

    Now we run into an issue with the user experience. They can't tell if their click has registered, if anything is happening, or if there was an error. This is where NetSuite would redirect you in order to provide feedback. That's a very outdated, static approach to web design though. NetSuite does actually give us some methods to dynamically provide feedback to the user. The Dialog and Message modules are two examples of this.

    I usually opt for the message module because it's unobtrusive, somewhat flexible, and can even be called from a User Event. So, let's display a message while we POST to our SL. Well, it turns out that doesn't work; the message doesn't display until after we get a response from our POST. While it's tempting to blame this on NetSuite, it's more down to how the browser calls JavaScript event handlers and when it updates the DOM.

Getting out of our own way

    Without getting too philosophical, JavaScript can only really do one thing at a time. If it's actively waiting on a POST response, it can't also be drawing a message. Unfortunately, the results of the message drawing will always happen last, even if called first. This is partly because the POST is "blocking" the message. We can avoid this by using promises because they're "non-blocking". I think of it as allowing us to passively wait for the POST. We can still react to the response from the POST very quickly, but we're free to do other things while we wait.

And then?

    A promise by itself isn't too helpful. We usually need to run some code after we're done waiting on the promise. One way to do this is to chain a "then" function onto the promise. It takes a callback function that will run after the promise is done. This lets us display a confirmation message to tell the user that the function completed successfully. But what if it wasn't successful?

const submittingMessage = message.create({ type: 2.0,
    title: 'Submitting', message: 'Please wait' });
submittingMessage.show(); //tell user to wait

https.post.promise({ url: newURL, body }).then((response) => {
    submittingMessage.hide(); //done waiting
    if (response?.body) //show confirmation
        message.create({ type: 1.0, title: 'Success', message: 'yay' }).show();
});


What's the catch?

    Previously, the way we handled errors didn't matter that much; the user's data was gone regardless. Now we have a chance to do something with any errors we encounter.

    Promises can also have a "catch" chained onto them. This works very much like a try/catch, but the try is implicit and wraps the promise. Here we can display an error message, or maybe prompt the user to store their data for later.

https.post.promise({ url: newURL, body }).then((response) => {
    ...
}).catch((reason) => {
    submittingMessage.hide();
    //show error to user
    message.create({ type: 3.0, title: 'Error', message: reason }).show();
});


Dominate Submitting

    Those are all the parts needed to roll a better form submitting solution. While I think this is the best application of these ideas, they can be used for other actions and other pages/records. Next time you add a button, consider if it could benefit!

Return to Sender: POST a Suitelet without redirecting

     We all know the pain of submitting a form in NetSuite, only to have it error and lose the input. This happens because NetSuite forces a...