SSJS: Generate a dynamic form based on a Data Extension

In this tutorial, we explore how to dynamically generate forms in Salesforce Marketing Cloud using Server-Side JavaScript (SSJS). Learn how to retrieve Data Extension fields, build forms on the fly, and handle form submissions—all while leveraging the flexibility of SSJS for custom form handling.

SSJS: Generate a dynamic form based on a Data Extension

Introduction

In this SSJS example, we will create a dynamic form based on a Data Extension. This post is not meant as a replacement for Smart Capture, but rather an alternative solution for more customized use cases. Through this post, you will gain a better understanding of:

  • How to differentiate between POST and GET requests in CloudPages
  • How to handle form data in SSJS
  • How to dynamically create a form based on an existing Data Extension
  • How to use SSJS and AMPScript together in a CloudPage
Data Extension used for this example
Resulting CloudPage showing the form based on the data extension fields

Where This Could Be Useful

Salesforce Marketing Cloud’s Contact Builder allows you to manually add data to a Data Extension, and Smart Capture provides an easy way to generate dynamic forms that save data directly to a Data Extension. However, this example offers an alternative approach that could be useful in scenarios where:

  • You need more control over how form fields are generated and displayed.
  • You want to implement conditional logic or custom validation that Smart Capture might not fully support.
  • You aim to integrate with additional backend services or perform other tasks before saving data to a Data Extension.

By following this tutorial, you’ll learn how to leverage SSJS and AMPScript together for more flexible form handling in CloudPages, providing you with a deeper level of customization and control. Let's get started.

How The Page works

This solution is built within a single CloudPage that:

  • Accepts a Data Extension External Key via the URL: The page captures the external key of the Data Extension from the URL parameter (de). This key is used to retrieve the fields of the specific Data Extension.
  • Dynamically Renders a Form: Based on the retrieved Data Extension fields, the form is dynamically generated. Each form input corresponds to a field in the Data Extension and is rendered according to its field type (e.g., text, number, checkbox, etc.).
  • Handles Form Submission and Saves Data: Upon submission, the form data is processed and saved into the corresponding Data Extension. The server-side script handles the form’s POST request, ensuring data is stored accurately.
  • Provides Basic Error Handling: The script includes error handling for situations like missing Data Extension keys or failed data submissions, displaying relevant messages to the user when issues arise.

The code

You can access the full code example at the end of this post or on this github repository.

Differentiating between GET and POST requests

In this solution, one of the first tasks is to determine whether the request is a GET or a POST. This allows us to either display the form or handle the form submission.

GET Request:

When the page is loaded via a GET request (usually when the user first lands on the page), the primary goal is to:

  1. Retrieve the Data Extension Key from the URL.
  2. Generate the Form based on the Data Extension fields.

Here’s the basic structure for handling the GET request:

if (Platform.Request.Method == "GET") {
    // Check if the 'de' parameter is provided in the URL
    var de_key = Platform.Request.GetQueryStringParameter("de");

    if (de_key == null) {
        // If the Data Extension key is missing, display an error message
        Variable.SetValue("@ShowMessage", true);
        Variable.SetValue("@Message", "Please provide a Data Extension External Key in the URL.");
    } else {
        // If the key is present, retrieve the Data Extension fields and generate the form
        var de = DataExtension.Init(de_key);
        var fields = de.Fields.Retrieve();
        Variable.SetValue("@Form", generateForm(fields));
    }
}

POST Request:

When the user submits the form, a POST request is triggered. In this step, we:

  1. Capture the Form Data submitted by the user.
  2. Save the Data into the Data Extension.
  3. Handle Success or Error by displaying a message to the user.

Here’s how the POST request is handled:

if (Platform.Request.Method == "POST") {
    // Get form data from the POST request
    var form_data = Platform.Request.GetPostData();
    var de_key = Platform.Request.GetQueryStringParameter("de");

    if (de_key) {
        var de = DataExtension.Init(de_key);
        var data = GetFormData(form_data);

        // Insert data into the Data Extension
        var result = de.Rows.Add(data);

        // Check if the data insertion was successful
        if (result == "1") {
            Variable.SetValue("@Message", "Data added successfully");
        } else {
            Variable.SetValue("@Message", "Error adding data");
        }
    }
}

The GenerateForm() function

The generateForm() function is the core of this solution. It dynamically builds the form by looping through the fields of the Data Extension, generating the appropriate HTML input elements based on the field type. This eliminates the need to manually code forms for every Data Extension, making the form creation process highly flexible and efficient.

How it works

  1. Retrieve the Fields:

The function takes an array of fields from the Data Extension and loops through them. Each field has a type (e.g., text, number, date), and this type determines what kind of input element will be generated.

  1. Create the Appropriate Input Element:

Based on the field type, generateForm() creates the corresponding form element using simple HTML tags (<input>, <checkbox>, etc.). For example, text fields will generate a text input, while boolean fields generate a checkbox.

3. Apply HTML Structure and Styling:

In this example, basic HTML structure is used to wrap labels and input elements together. Styling can be handled separately (using Tailwind CSS, for instance), but the function focuses on ensuring that the form fields are mapped correctly to the Data Extension’s field types.

Here’s a breakdown of the code:

   // function to get a form element given the fields
        function generateForm(fields){
            form = ""
            for (var i = 0; i < fields.length; i++) {
                if (fields[i].FieldType == "Text" && fields[i].DefaultValue.indexOf(";") > -1) {
                    // Adding Tailwind classes to the label and text input
                    form += "<label for='" + fields[i].Name + "' class='block text-sm font-medium text-gray-700 mb-1'>" + fields[i].Name + "</label>";
                    form += "<select name='" + fields[i].Name + "' id='" + fields[i].Name + "' class='w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 mb-4'>";
                        var options = fields[i].DefaultValue.split(";");
                        for (var j = 0; j < options.length; j++) {
                            form += "<option value='" + options[j] + "'>" + options[j] + "</option>";
                        }
                    form += "</select><br>";
                }else if(fields[i].FieldType == "Text"){
                    form += "<label for='" + fields[i].Name + "' class='block text-sm font-medium text-gray-700 mb-1'>" + fields[i].Name + "</label>";
                    form += "<input type='text' name='" + fields[i].Name + "' id='" + fields[i].Name + "' class='w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 mb-4'><br/>";
                }else if (fields[i].FieldType == "Number") {
                    // Adding Tailwind classes to the label and number input
                    form += "<label for='" + fields[i].Name + "' class='block text-sm font-medium text-gray-700 mb-1'>" + fields[i].Name + "</label>";
                    form += "<input type='number' name='" + fields[i].Name + "' id='" + fields[i].Name + "' class='w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 mb-4'><br>";
                } else if (fields[i].FieldType == "Date" && fields[i].DefaultValue == "") {
                    // Adding Tailwind classes to the label and date input
                    form += "<label for='" + fields[i].Name + "' class='block text-sm font-medium text-gray-700 mb-1'>" + fields[i].Name + "</label>";
                    form += "<input type='date' name='" + fields[i].Name + "' value='' id='" + fields[i].Name + "' class='w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 mb-4'><br>";
                } else if (fields[i].FieldType == "Boolean") {
                    // Adding Tailwind classes to the label and checkbox
                    form += "<label for='" + fields[i].Name + "' class='block text-sm font-medium text-gray-700 mb-1'>" + fields[i].Name + "</label>";
                    form += "<input type='checkbox' name='" + fields[i].Name + "' id='" + fields[i].Name + "' value='True' class='h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500 mb-4'><br>";
                } else if (fields[i].FieldType == "EmailAddress") {
                    // Adding Tailwind classes to the label and email input
                    form += "<label for='" + fields[i].Name + "' class='block text-sm font-medium text-gray-700 mb-1'>" + fields[i].Name + "</label>";
                    form += "<input type='email' name='" + fields[i].Name + "' value='" + fields[i].DefaultValue + "' id='" + fields[i].Name + "' class='w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 mb-4'><br>";
                } else if (fields[i].FieldType == "Decimal") {
                    // Adding Tailwind classes to the label and decimal input
                    form += "<label for='" + fields[i].Name + "' class='block text-sm font-medium text-gray-700 mb-1'>" + fields[i].Name + "</label>";
                    form += "<input type='number' name='" + fields[i].Name + "' step='0.01' id='" + fields[i].Name + "' class='w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 mb-4'><br>";
                } else if (fields[i].FieldType == "Phone") {
                    // Adding Tailwind classes to the label and phone input
                    form += "<label for='" + fields[i].Name + "' class='block text-sm font-medium text-gray-700 mb-1'>" + fields[i].Name + "</label>";
                    form += "<input type='tel' name='" + fields[i].Name + "' id='" + fields[i].Name + "' class='w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 mb-4'><br>";
                }
            }
        return form;
        }

Key Parts of the Function:

  • Field Type Handling: Each field type (text, number, date, etc.) is handled separately to ensure the correct HTML input type is generated. This ensures that the form is appropriately structured for the data types in your Data Extension.
  • Label and Input Pairing: For every field, a corresponding label is created, followed by the input element. This ensures a user-friendly form where each input field is properly labeled.
  • Dynamic Form Generation: The function returns the entire form as a string, allowing it to be injected directly into the page. This keeps the process highly dynamic, adjusting to the structure of whatever Data Extension you are working with.

Putting it All Together

Now that we’ve walked through the core components—differentiating between GET and POST requests and dynamically generating form fields with the generateForm() function—let’s see how it all works as a complete solution.

How It Flows

  1. Accept the Data Extension Key:

The page first looks for the Data Extension key in the URL parameter (de). If it’s missing, an error message is shown. Otherwise, we use this key to fetch the Data Extension fields.

  1. Render the Form Dynamically:

Using the generateForm() function, the form is dynamically built based on the fields retrieved from the Data Extension. Each field type (e.g., text, number, date, checkbox) is mapped to the appropriate input element.

  1. Handle Form Submission:

When the form is submitted, the data is captured via a POST request. The values entered by the user are processed and added to the Data Extension, while providing feedback on the success or failure of the data submission.

  1. Display Messages and Handle Errors:

Throughout the process, error handling ensures that the user is informed if something goes wrong (e.g., missing Data Extension key, invalid fields). Success or error messages are dynamically displayed based on the result of the form submission.

Complete Code Example

Here's the complete CloudPages code:

<script runat="server">
    Platform.Load("Core", "1.1.1");
    
    try {
      
    
        // Handle GET request
        if (Platform.Request.Method == "GET") {
            // Try getting the data extension key from the URL using the parameter "de"
            // If it's not provided, show an error message
            de_key = Platform.Request.GetQueryStringParameter("de");
            Write(de_key);
            if (Platform.Request.GetQueryStringParameter("de") == null) {
                Variable.SetValue("@ShowMessage", true);
                Variable.SetValue("@Message", "Please provide a Data Extension External Key in the URL using the parameter 'de'. Such as '?de=MyDataExtension'");
            } else {
                // If the data extension key is provided, get the data extension fields
                var de_key = Platform.Request.GetQueryStringParameter("de");
                var de = DataExtension.Init(de_key);
                var fields = de.Fields.Retrieve();
    
                // Check if there are no fields or invalid data extension key
                if (fields.length == 0) {
                    Variable.SetValue("@ShowMessage", true);
                    Variable.SetValue("@Message", "No fields found in the Data Extension. Or Data Extension key is invalid.");
                } else {
                    // If fields are found, render the form
                    Variable.SetValue("@RenderForm", true);
                    Variable.SetValue("@Message", "Data Extension found");
    
                    var form = generateForm(fields);
                    // Loop through the fields and generate form inputs based on the field type
                    
    
                    // Set the form content
                    Variable.SetValue("@Form", form);
                }
            }
        }
    
        // Handle POST request
        if (Platform.Request.Method == "POST") {
            // Get the form data from the POST request
            var form_data = Platform.Request.GetPostData();
            // Write(Stringify(GetFormData(form_data)));
    
            var de_key = Platform.Request.GetQueryStringParameter("de");
            var de = DataExtension.Init(de_key);
            var fields = de.Fields.Retrieve();
            var data = GetFormData(form_data);
            var form = generateForm(fields);
            Variable.SetValue("@Form", form);
            // Add a new row to the Data Extension
            var result = de.Rows.Add(data);
            // Write(Stringify(result));
    
            // Display success or error message based on the result
            if (result == "1") {
                Variable.SetValue("@Message", "Data added successfully");
            } else {
                Variable.SetValue("@Message", "Error adding data");
            }
            //display the form again
            Variable.SetValue("@RenderForm", true);
        }
    
        // Function to parse form data from the URL-encoded string format
        function GetFormData(form_data_string) {
            var form_data = {};
            var form_data_array = form_data_string.split("&");
            for (var i = 0; i < form_data_array.length; i++) {
                var key_value = form_data_array[i].split("=");
                form_data[decodeURIComponent(key_value[0])] = decodeURIComponent(key_value[1]);
            }
            return form_data;
        }
    
        // function to get a form element given the fields
        function generateForm(fields){
            form = ""
            for (var i = 0; i < fields.length; i++) {
                if (fields[i].FieldType == "Text" && fields[i].DefaultValue.indexOf(";") > -1) {
                    // Adding Tailwind classes to the label and text input
                    form += "<label for='" + fields[i].Name + "' class='block text-sm font-medium text-gray-700 mb-1'>" + fields[i].Name + "</label>";
                    form += "<select name='" + fields[i].Name + "' id='" + fields[i].Name + "' class='w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 mb-4'>";
                        var options = fields[i].DefaultValue.split(";");
                        for (var j = 0; j < options.length; j++) {
                            form += "<option value='" + options[j] + "'>" + options[j] + "</option>";
                        }
                    form += "</select><br>";
                }else if(fields[i].FieldType == "Text"){
                    form += "<label for='" + fields[i].Name + "' class='block text-sm font-medium text-gray-700 mb-1'>" + fields[i].Name + "</label>";
                    form += "<input type='text' name='" + fields[i].Name + "' id='" + fields[i].Name + "' class='w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 mb-4'><br/>";
                }else if (fields[i].FieldType == "Number") {
                    // Adding Tailwind classes to the label and number input
                    form += "<label for='" + fields[i].Name + "' class='block text-sm font-medium text-gray-700 mb-1'>" + fields[i].Name + "</label>";
                    form += "<input type='number' name='" + fields[i].Name + "' id='" + fields[i].Name + "' class='w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 mb-4'><br>";
                } else if (fields[i].FieldType == "Date" && fields[i].DefaultValue == "") {
                    // Adding Tailwind classes to the label and date input
                    form += "<label for='" + fields[i].Name + "' class='block text-sm font-medium text-gray-700 mb-1'>" + fields[i].Name + "</label>";
                    form += "<input type='date' name='" + fields[i].Name + "' value='' id='" + fields[i].Name + "' class='w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 mb-4'><br>";
                } else if (fields[i].FieldType == "Boolean") {
                    // Adding Tailwind classes to the label and checkbox
                    form += "<label for='" + fields[i].Name + "' class='block text-sm font-medium text-gray-700 mb-1'>" + fields[i].Name + "</label>";
                    form += "<input type='checkbox' name='" + fields[i].Name + "' id='" + fields[i].Name + "' value='True' class='h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500 mb-4'><br>";
                } else if (fields[i].FieldType == "EmailAddress") {
                    // Adding Tailwind classes to the label and email input
                    form += "<label for='" + fields[i].Name + "' class='block text-sm font-medium text-gray-700 mb-1'>" + fields[i].Name + "</label>";
                    form += "<input type='email' name='" + fields[i].Name + "' value='" + fields[i].DefaultValue + "' id='" + fields[i].Name + "' class='w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 mb-4'><br>";
                } else if (fields[i].FieldType == "Decimal") {
                    // Adding Tailwind classes to the label and decimal input
                    form += "<label for='" + fields[i].Name + "' class='block text-sm font-medium text-gray-700 mb-1'>" + fields[i].Name + "</label>";
                    form += "<input type='number' name='" + fields[i].Name + "' step='0.01' id='" + fields[i].Name + "' class='w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 mb-4'><br>";
                } else if (fields[i].FieldType == "Phone") {
                    // Adding Tailwind classes to the label and phone input
                    form += "<label for='" + fields[i].Name + "' class='block text-sm font-medium text-gray-700 mb-1'>" + fields[i].Name + "</label>";
                    form += "<input type='tel' name='" + fields[i].Name + "' id='" + fields[i].Name + "' class='w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 mb-4'><br>";
                }
            }
        return form;
        }
    
    } catch (e) {
        // Catch and display any errors that occur during execution
        Write(Stringify("Error: " + e));
    }
    </script>
    
    <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Data Extension - Add Fields</title>
    
    <!-- Tailwind CSS CDN -->
    <script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100 text-gray-900">
    <div class="max-w-4xl mx-auto py-8">
        <div class="bg-white p-8 shadow-md rounded-lg">

            <!-- Server-side script -->
            <script runat="server">
                try {
            </script>

            <!-- Show fields or messages dynamically -->
            %%=v(@Fields)=%%

            %%[ IF @Message != ""  THEN ]%%
                <div class="mb-4">
                    <p class="text-green-500 font-semibold">%%=v(@Message)=%%</p>
                </div>
            %%[ENDIF]%%

            %%[
                IF @RenderForm THEN 
            ]%%

            <form method="POST" class="space-y-6">

                <!-- Dynamically inserted form fields -->
                %%=v(@Form)=%%

                <div>
                    <input type="submit" value="Submit" class="bg-blue-500 text-white px-4 py-2 rounded-lg shadow hover:bg-blue-600">
                </div>

            </form>

            %%[ENDIF]%%

            <script runat="server">
                } catch (e) {
                    Write(Stringify(e));
                }
            </script>
        </div>
    </div>
</body>
</html>

Paste the code into a CloudPage and publish it. To use the form, pass the de parameter in the URL, setting it to the External Key of the Data Extension you want to generate the form for. Example: https://your-cloud-page-url.com?de=MyDataExtension

Conclusion

In this advanced example, we explored how to dynamically generate forms in Salesforce Marketing Cloud using SSJS, based on Data Extension fields. This approach offers flexibility and control, making it a great alternative to Smart Capture for custom form handling. By combining GET and POST request handling with dynamic form generation, you can easily adapt this solution to fit various use cases in SFMC, streamlining data collection and management.

Happy coding!