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>
            <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:

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.

  5. Hello Ivan, what exactly “Write” function do here ?
    Write(Stringify({ Status: req.Status, Message: message }));
    where it is writing that status and message?

  6. Well, this writes a JSON text on the page. In this case, the page is used as an AJAX endpoint.

  7. Are there any videos on this? Confused on where this is built. Are you creating the form and form handler as landing pages and then form.js as code resource? Not sure if something has maybe changed since this article, but it seems to skip the basic initial steps for where this is built in cloudpages.
    Thanks for all the articles and all your work you put into it!

  8. Hi Ryan. No videos. And your assumption is correct, but the JS file doesn’t have to be in SFMC. All of this is mentioned in Prerequisites section 😉

  9. Those examples, is amazing.
    I tried to simulated all cenarios, the last didn’t work for me.
    This message is returned:
    “”Status”:”Error”,”Message”:{“message”:”Invalid cast from ‘Char’ to ‘Double’.”,”description”:”System.InvalidCastException: Invalid cast from ‘Char’ to ‘Double’. – from mscorlib\r\n\r\n”}}”

    I tracked and the error is in this step:

    var req = proxy.createItem(‘DataExtensionObject’, {
    Name: “Emails”,
    Properties: formatProps(parsed)
    });
    Sincerely, I don’t know if I am missing something.
    Could anyone help to find the solution?

  10. Luana,

    I just re-tested my own code and everything works like a charm.

    My guess is: you are trying to write a text value in a Number or Decimal field in the Data Extension.

  11. Hi… Great post THANK YOU… question.. I am new to doing forms outside Smart Capture. Here my issue I am able to publish fine the form but for some reason SFMC doesn’t want to publish the form handler page … Always gives me an error on the preview and when I click in the url I get a 500 error. Any work around this?

  12. Hello there! First : don’t trust the preview, publish your page, click on the URL and see what happens. Second: error 500 means some code creates a fatal error, you need to debug it.

Comments are closed.

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