How to build HTML forms without Smart Capture in Salesforce Marketing Cloud

How to build HTML forms without Smart Capture in Salesforce Marketing Cloud

This article showcases 3 different methods of how to build HTML forms without Smart Capture in Salesforce Marketing Cloud using AMPscript and server-side JavaScript.

The Problem

Don’t get me wrong, Smart Capture is a useful and powerful feature for building forms for the Cloud pages.

But when it comes to custom solutions where the HTML needs to be tweaked and attached to a different style sheet, Smart Capture doesn’t fit the bill.

Fortunately, there are at least 3 different methods we can use to replace this feature and extend it even further: to AJAX calls and custom data processing.

Prerequisites

Before we start, please create the following in your Business Unit:

  • 1 Data Extension named “Emails” with a single field called “EmailAddress”.
  • 2 Cloud pages, named “Form” and “Form Handler”.
  • 1 JavaScript file, named “form.js”.

Please make sure to adapt the following code with the paths and URLs that you chose for your Marketing Cloud implementation.

How it works

The flow we are creating will work as follows:

  1. A customer fills out the Form and clicks on the submit button.
  2. The Form send the data to the Form Handler page using the POST method.
  3. The Form Handler inserts a new record in the Data Extension and redirects the customer back to the Form page.
  4. The Form page display a success message.

Now, let’s explore the different methods of replicating the Smart Capture feature.


AMPscript method

This method works as follows: The form submits the data to the Form Handler and the latter redirects the customer back to the form page with a parameter in the URL named “ok“.

If the “ok” parameter in the URL is equal to something, AMPscript will no longer display the form but will show the success message instead.

This method is very simple but doesn’t account for any errors. If the AMPscript fails for one reason or another, the whole flow fails, and nobody will know why.

Form page

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AMP Form</title>
</head>
<body>
    <form action="https://mc-domain.com/form-handler" method="post">
        %%[ IF EMPTY(RequestParameter("ok")) THEN ]%%
        <fieldset class="%%=v(@form_class)=%%">
            <legend>Form</legend>
            <label for="EmailAddress">Email*:</label>
            <input type="email" name="EmailAddress" id="email" placeholder="ex: john.smith@mail.com"
                required>
            <br><br>
            <button>Subscribe</button>
        </fieldset>
        %%[ ELSE ]%%
        <fieldset>
            <legend>Message</legend>
            Thank you for subscribing :)
        </fieldset>
        %%[ ENDIF ]%%
    </form>
</body>
</html>

Form Handler page

%%[
  SET @EmailAddress = RequestParameter("EmailAddress")

  InsertData("Emails", "EmailAddress", @EmailAddress)

  SET @Referer = RegExMatch(HTTPRequestHeader("Referer"), "(.+?)(\#|\?|$)", 1)

  Redirect(Concat(@Referer,"?ok=1"))
]%%

In case you were wondering about the RegExMatch function: it removes all the arguments from the referrer URL to avoid an invalid URL to be passed to the Redirect function.

Referrer: https://mc-domain.com/form-page?utm_media=email
Redirect w/o RegExMatch: https://mc-domain.com/form-page?utm_media=tv?ok=1
Redirect with RegExMatch: https://mc-domain.com/form-page?ok=1

Hybrid method (AMPscript + SSJS)

This method works almost in the same manner as the AMPscript method, but the Form Handler page uses both AMPscript and server-side JavaScript, in order to create a proper error handling.

Form page

Note that the Form page will now accept 3 parameters: “ok“, “error” and “message“.

The new arguments are used for the error handling: “error” argument will show the error message to the customer and the “message” argument will print the error in the Console for the developer to examine.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hybrid Form</title>
</head>
<body>
    %%[
        SET @ok = RequestParameter("ok")
        SET @error = RequestParameter("error")
        SET @message = RequestParameter("message")
    ]%%
    <form action="https://mc-domain.com/form-handler" method="post">
        %%[ IF EMPTY(@ok) AND EMPTY(@error) THEN ]%%
        <fieldset>
            <legend>Form</legend>
            <label for="EmailAddress">Email*:</label>
            <input type="email" name="EmailAddress" id="email" placeholder="ex: john.smith@mail.com"
                required>
            <br><br>
            <button>Subscribe</button>
        </fieldset>
        %%[ ELSE ]%%
            %%[ IF NOT EMPTY(@ok) THEN ]%%
            <fieldset>
                <legend>Message</legend>
                Thank you for subscribing :)
            </fieldset>
            %%[ ENDIF ]%%
            %%[ IF NOT EMPTY(@error) THEN ]%%
            <fieldset>
                <legend>Message</legend>
                Something went wrong :(
            </fieldset>
            %%[ ENDIF ]%%
        %%[ ENDIF ]%%
    </form>
    %%[ IF NOT EMPTY(@message) AND NOT EMPTY(@error) THEN ]%%
    <script>
        console.error(`%%=v(@message)=%%`);
    </script>
    %%[ ENDIF ]%%
</body>
</html>

Form Handler page

The error handling is possible thanks to the Try Catch function. Mixing AMPscript and JavaScript is a bit hacky, but it works like a charm.

<script runat="server">

    Platform.Load("core", "1.1.1");

    var referer = String(Platform.Request.ReferrerURL).split("?")[0];

    try {
      
      </script>%%[
      
        SET @EmailAddress = RequestParameter("EmailAddress")
        InsertData("Emails", "EmailAddress", @EmailAddress)
          
      ]%%<script runat="server">
        
    } catch (err) {

      referer += "?error=1&message=" + Stringify(err.message) + Stringify(err.description);
      Platform.Response.Redirect(referer);

    } finally {

      Platform.Response.Redirect(referer + "?ok=1");
      
    }
</script>

Server-side JavaScript method

This method is rather complicated but it allows using AJAX instead of POST submissions and redirects.

The form is no longer submitted but rather compiled in a JSON object with client-side JavaScript and sent to the Form Handler page asynchronously.

When the Form Handler page responds, the JavaScript will hide and display the error or show a success message.

Form page

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SSJS Form</title>
    <style>.hidden { display: none; }</style>
</head>
<body>
    <form action="https://mc-domain.com/form-handler" method="post">
        <fieldset>
            <legend>Form</legend>
            <label for="EmailAddress">Email*:</label>
            <input type="email" name="EmailAddress" id="email" placeholder="ex: john.smith@mail.com"
                required>
            <br><br>
            <button>Subscribe</button>
        </fieldset>
        <fieldset class="ok hidden">
            <legend>Message</legend>
            Thank you for subscribing :)
            <br><br>
            <button onclick="document.location.reload()">Retry</button>
        </fieldset>
        <fieldset class="error hidden">
            <legend>Message</legend>
            Something went wrong :(
            <br><br>
            <button onclick="document.location.reload()">Retry</button>
        </fieldset>
    </form>
    <script src="form.js"></script>
</body>
</html>

form.js

Note that the code below uses Fetch API and FormData object, which are only supported by older browsers.

var form = document.querySelector('form');

form.addEventListener("submit", postFormData);

function postFormData(e) {

    e.preventDefault();

    var payload = {};

    var formData = new FormData(form);

    for (var pair of formData.entries()) {
        payload[pair[0]] = pair[1];
    }

    fetch(form.getAttribute("action"), {
        method: "POST",
        headers: { 'Content-Type': 'application/json; charset=utf-8' },
        body: JSON.stringify(payload)
    })
    .then(function (result) {
        return result.json();
    })
    .then(function (data) {
        if(data.Status != 'OK') {
            console.error(data.Message);
        } else {
            console.info(data.Message);
        }
        toggleMessage(data.Status);
    })
    .catch(function (error) {
        console.error(error);
        toggleMessage('Error');
    });

}

function toggleMessage(status) {

    var fieldsets = form.querySelectorAll('fieldset');

    fieldsets.forEach(function(fieldset) {
        if(fieldset.classList.contains(status.toLowerCase())) {
            fieldset.classList.remove("hidden");
        } else {
            fieldset.classList.add("hidden");
        } 
    });

}

Form Handler page

This is a full server-side JavaScript solution. It allows for a better error handling thanks to WSProxy and it won’t fail if we try to push unknown field names into the Data Extension.

<script runat='server'>

    Platform.Load('core', '1.1.1');

    var proxy = new Script.Util.WSProxy();

    try {

        var data = Platform.Request.GetPostData();

        var parsed = Platform.Function.ParseJSON(data);

        var req = proxy.createItem('DataExtensionObject', {
            Name: "Emails",
            Properties: formatProps(parsed)
        });

        var res = req.Results[0],
            message = "";

        if(req.Status != 'OK') {
            message = res.ErrorMessage;
        } else {
            message = res.StatusMessage;
        }

        Write(Stringify({ Status: req.Status, Message: message }));

    } catch(err) {

        Write(Stringify({ Status: 'Error', Message: err }));

    }

    function formatProps(data) {
        var arr = [];
        for (k in data) {
            arr.push({
                Name: k,
                Value: data[k]
            });
        }
        return arr;
    }

</script>

Conclusion

Replacing Smart Capture is a hassle. But keeping it means depriving yourself from a world of possibilities.

Possible improvements

This article is a good starting point and there are many improvements that can be implemented. To name a few:

Pay me a coffee

Want to say thanks? Pay me a coffee! Remember, I turn coffee into code.

Have I missed anything?

Please poke me with a sharp comment below or use the contact form.

  1. Really great article. Like the way you present and then explain parts of the code. I had one question though. I was able to get the SSJS method working with extra fields that are being inserted in the data extension. The record is being inserted although I always get a Syntax error in the console and the “Something went wrong :(” message on the page.

    SyntaxError: Unexpected token < in JSON at position 63
    (anonymous) @ form.js:36
    Promise.catch (async)
    postFormData @ form.js:35
    form.js:19

  2. That’s because there is some HTML in your response. Did you create your Cloud page using Content Builder option? If not, that’s probably the tracking HTML that MC injects in the page.

  3. Thank you very much Ivan. You are right. Only the Form Handler page needed to be made by Content Builder in order to avoid the tracking HTML being inserted. I still got the error because I tried to secure the Form Handler with security headers following your other tutorial: https://ampscript.xyz/how-tos/protect-your-cloud-pages/
    When I removed this code it all works. Isn’t it possible to protect this page somehow?

  4. Yes there is. I have a SSJS solution for this, I’ll update the article asap.

Leave a Reply

Your email address will not be published. Required fields are marked *

server-side Javascript
Up Next:

How to use WSProxy to work with Data Extensions in server-side JavaScript

How to use WSProxy to work with Data Extensions in server-side JavaScript