How to create universal auto-generated forms with server-side Javascript in Marketing Cloud

How to create universal auto-generated forms with server-side Javascript in Marketing Cloud

This article explains how to create a universal Cloud page form in Marketing Cloud using server-side Javascript .

Smart Capture is just not good enough

– Elephant in the room

Why use it

Let’s address the elephant in the room: Smart Capture is just not good enough if your goal is to create complex forms to capture your customer’s data.

The best solution so far is to implement an HTML form with tailor-made AMPscript code that would manage the data and create records in a Data Extension.

But maybe there is a better solution.

What if the form was completely generic? What if the only thing we needed was to provide the script with the name of the Data Extension and it would do all the rest?

In that way, we can re-use the same code on all of the Cloud pages and forget about tailor-made solutions.

How it works

The idea is simple.

The code fetches the names of the fields from a Data Extension and uses a JSON object as a reference for building the input fields.

After the form is built and then submitted, the code generates a primary key and uses the same field names to create a record in the Data Extension.

In the end, a success message is displayed to communicate to the customer that his data has been registered.

Data Extension

For the purpose of this example, we’ll use only 5 fields. There will be one record per submission and no record updates.

The Id field is the primary key that will receive a timestamp as a record value.

IdText50Primary Key
FirstNameText50Nullable
LastNameText80Nullable
EmailAddressEmailAddress254Nullable
CompanyText50Nullable

JSON object

Since the only thing we are going to retrieve from the Data Extension are the field names, we need to cover the gaps with some additional parameters from a JSON object.

This decision comes from the fact that we are only able to retrieve the name and the field type from the DE (not even the Nullable status).

This object can be remote or on the same page.

{
	"FirstName" : {
		"Type": "Text",
		"Pattern": "",
		"Label": "First name",
		"Required": true,
		"Placeholder": "Enter your first name"
	},
	"LastName": {
		"Type": "Text",
		"Pattern": "",
		"Label": "Last name",
		"Required": true,
		"Placeholder": "Enter your last name"
	},
	"EmailAddress": {
		"Type": "Email",
		"Pattern": "^([a-zA-Z0-9_\\-\\.]+)@([a-zA-Z0-9_\\-\\.]+)\\.([a-zA-Z]{2,})$",
		"Label": "Email",
		"Required": true,
		"Placeholder": "Enter your email"
	},
	"Company": {
		"Type": "Text",
		"Pattern": "",
		"Label": "Company",
		"Required": false,
		"Placeholder": "Where do you work?"
	}
}

Javascript functions

In order to make our code readable and generic, the usage of multiple functions is necessary.

Each function described below corresponds to a different action. Every other function is just a helper for compensating the lack of modern Javascript methods in Marketing Cloud server-side Javascript.

getFieldNames

This function retrieves the field names from a Data Extension based on the provided name.

Unfortunately, Fields.Retrieve() function doesn’t work if we initiate the DataExtension object with a name. A Customer Key is needed, otherwise the result will be an empty array.

For some reason, the field names will be retrieved in a seemingly random order. We need to sort the result by the Ordinal attribute.

The strStartsWith() function is a polyfill for the startsWith() function. It’s used to filter out all the invisible fields from the Data Extension that start with an underscore.

function getFieldNames(de_name) {
	var out = [];
	var attr = DataExtension.Retrieve({ Property: "Name", SimpleOperator: "equals", Value: de_name });
	var de = DataExtension.Init(attr[0].CustomerKey);
	var fields = de.Fields.Retrieve();
	if(fields.length > 0) {
		fields.sort(function (a, b) { return (a.Ordinal > b.Ordinal) ? 1 : -1 });
		for (k in fields) {
			if (!strStartsWith(fields[k].Name, "_")) out = out.concat(fields[k].Name);
		}
	}    
	return out;
} 

buildFields

This function renders the HTML of the input fields based on the JSON object discussed previously.

If the field name from the Data Extension is not found in the JSON object, a basic input field will be rendered, unless the field name is an exception, like the Id field name.

The inArray() function is a polyfill for the arr.indexOf().

function buildFields(fields, attributes) {

	var html = "";
	var exceptions = ["Id"];

	for (n in fields) {

		var field = attributes[fields[n]];
		var name = fields[n];

		if (field != null) {

			var required = (field.Required != false) ? "required" : "";
			var pattern = (!!isNotEmpty(field.Pattern)) ? "pattern='" + field.Pattern + "'" : "";
			var label = (!!isNotEmpty(field.Label)) ? "<label for='" + name + "' >" + field.Label + ((required.length > 0) ? " *" : "") + "</label>" : "";
			var placeholder = field.Placeholder;
			var type = field.Type.toLowerCase();

			html += label + "<br>";
			html += "<input name='" + name + "' type='" + type + "' " + pattern + " placeholder='" + placeholder + "' " + required + ">" + "<br>";

		} else if(!inArray(exceptions,name)) {

			html += "<label for='" + name + "' >" + name + "</label>" + "<br>";
			html += "<input name='" + name + "' type='text'>" + "<br>";

		}

	}  

	return html;
}

insertInDataExtension

This function generates timestamp to be inserted in the Id field and creates a new record in the Data Extension.

First, the values are collected from the Request and inserted in a simple key/value object.

{ key : value, key : value, key : value}

Then splitKeysValues() function formats the object in order to be used in the InsertData function.

function insertInDataExtension(options) {

	var fields = {};
	var list = getFieldNames(options.DataExtension);
	var de_name = options.DataExtension;

	for (n in list) {
		var name = list[n];
		var val = (Request.GetFormField(list[n]) != null) ? Request.GetFormField(list[n]) : "";
		fields[name] = val;
	}

	fields["Id"] = timestamp();

	var insert = splitKeysValues(fields);

	Platform.Function.InsertData(de_name, insert.Keys, insert.Values);

	return insert;
}

splitKeysValues

This function formats the given object to be used with the InsertData function, as it expects 2 different arrays (keys and values).

function splitKeysValues(fields) {
	var out = {
		"Keys": [],
		"Values": []
	}
	for (k in fields) {
		var v = fields[k];
		out.Keys.push(k);
		out.Values.push(v);
	}
	return out;
}

Full code

Copy/paste this code and see it in action! But please make sure to have correctly created the Data Extension.

<script runat="server">

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

    var options = {
        "DataExtension" : "MyAwesomeDataExtension"
    }

    var attributes = {
        "FirstName" : {
            "Type": "Text",
            "Pattern": "",
            "Label": "First name",
            "Required": true,
            "Placeholder": "Enter your first name"
        },
        "LastName": {
            "Type": "Text",
            "Pattern": "",
            "Label": "Last name",
            "Required": true,
            "Placeholder": "Enter your last name"
        },
        "EmailAddress": {
            "Type": "Email",
            "Pattern": "^([a-zA-Z0-9_\\-\\.]+)@([a-zA-Z0-9_\\-\\.]+)\\.([a-zA-Z]{2,})$",
            "Label": "Email",
            "Required": true,
            "Placeholder": "Enter your email"
        },
        "Company": {
            "Type": "Text",
            "Pattern": "",
            "Label": "Company",
            "Required": false,
            "Placeholder": "Where do you work?"
        }
    }

    var submit = (Request.Method() == "POST") ? true : false ;
    var success = (!!isNotEmpty(Request.GetQueryStringParameter("success"))) ? true : false ;

    if(!!submit) {

        var insert = insertInDataExtension(options);

        Redirect(getPageURL() + "?success=1", true);

    } else if(!!success) {

        Write("<strong>Thank you for submitting<strong><br><a href='" + getPageURL() + "'>Restart</a>");

    } else {

        buildForm(options, attributes);

    }

    function insertInDataExtension(options) {

        var fields = {};
        var list = getFieldNames(options.DataExtension);
        var de_name = options.DataExtension;

        for (n in list) {
            var name = list[n];
            var val = (Request.GetFormField(list[n]) != null) ? Request.GetFormField(list[n]) : "";
            fields[name] = val;
        }

        fields["Id"] = timestamp();

        var insert = splitKeysValues(fields);

        Platform.Function.InsertData(de_name, insert.Keys, insert.Values);

        return insert;

    }

    function buildForm(options, attributes) {

        var html = "";
        var fields = getFieldNames(options.DataExtension);

        html += "<form action='" + getPageURL() + "' method='post'>";
        html += buildFields(fields, attributes);
        html += "<br>";
        html += "<button>Send</button>";
        html += "</form>"; 

        Write(html);

    }

    function buildFields(fields, attributes) {

        var html = "";
        var exceptions = ["Id"];

        for (n in fields) {

            var field = attributes[fields[n]];
            var name = fields[n];

            if (field != null) {

                var required = (field.Required != false) ? "required" : "";
                var pattern = (!!isNotEmpty(field.Pattern)) ? "pattern='" + field.Pattern + "'" : "";
                var label = (!!isNotEmpty(field.Label)) ? "<label for='" + name + "' >" + field.Label + ((required.length > 0) ? " *" : "") + "</label>" : "";
                var placeholder = field.Placeholder;
                var type = field.Type.toLowerCase();

                html += label + "<br>";
                html += "<input name='" + name + "' type='" + type + "' " + pattern + " placeholder='" + placeholder + "' " + required + ">" + "<br>";

            } else if(!inArray(exceptions,name)) {

                html += "<label for='" + name + "' >" + name + "</label>" + "<br>";
                html += "<input name='" + name + "' type='text'>" + "<br>";

            }
            
        }  

        return html;
    }

    function getFieldNames(de_name) {

        var out = [];
        var attr = DataExtension.Retrieve({ Property: "Name", SimpleOperator: "equals", Value: de_name });
        var de = DataExtension.Init(attr[0].CustomerKey);
        var fields = de.Fields.Retrieve();

        if(fields.length > 0) {
            fields.sort(function (a, b) { return (a.Ordinal > b.Ordinal) ? 1 : -1 });
            for (k in fields) {
                if (!strStartsWith(fields[k].Name, "_")) out = out.concat(fields[k].Name);
            }
        }    

        return out;

    } 

    function splitKeysValues(fields) {

        var out = {
            "Keys": [],
            "Values": []
        }
        for (k in fields) {
            var v = fields[k];
            out.Keys.push(k);
            out.Values.push(v);
        }
        return out;
    }

    function getPageURL() {

        var page = Platform.Request.RequestURL;

        if (page.indexOf("?") > 0) {
            var out = page.substring(0, page.indexOf("?"));
        } else {
            var out = page;
        }

        return out;
    }

    function strStartsWith(str, search, pos) {
        pos = !pos || pos < 0 ? 0 : +pos;
        return str.substring(pos, pos + search.length) === search;
    }   

    function inArray(arr, k) {
        var out = -1;
        for (var i in arr) {
            if (arr[i] == k) out = i;
        }
        return out;
    }

    function isNotEmpty(val) {
        return (val != null && val.length > 0);
    }

    function timestamp() {

        var now = new Date();
        var out = now.getFullYear()
            + addZero(now.getMonth() + 1)
            + addZero(now.getDate())
            + addZero(now.getHours())
            + addZero(now.getMinutes())
            + addZero(now.getSeconds())
            + random(100, 999);
        return out;

        function addZero(n) {
            return n < 10 ? '0' + n : n
        }

        function random(min, max) {
            min = Math.ceil(min);
            max = Math.floor(max);
            return Math.floor(Math.random() * (max - min)) + min;
        }

    }

</script>

Considerations

Every Marketing Cloud project is different.

In this example, we assume that every Data Extension has Id as a primary key and it is equal to a timestamp when a record is created.

But, you may have a SubscriberKey or something completely different.

Maybe, when a form is submitted a second time by the same user, a record is updated and not inserted.

Please adapt your code to fit your needs.

What’s next?

Prefill the form for a known customer.

Put translations in your JSON object.

Update the JSON object to describe every possible field on every of your Cloud pages and make the form truly universal.


Have I missed anything?

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

Pay me a coffee

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

  1. Hi, a really nice post. Bud, I’m confused, where exactly should we copy/paste the entire code? I’ve tried different methods in CloudPages but non seem to display the form…

  2. This is fantastic! I was actually on my way to build something similar to this for a project I’m working on. Have various DEs that could have a non-specific number of fields. They are content DEs so they are all text fields. I modified the attributes var to

    var attributes = {
    name : {
    “Type”: “Text”,
    “Pattern”: “”,
    “Label”: name,
    “Required”: false,
    “Placeholder”: “Enter ” + name
    }
    }

    and removed the reference to the ID field (will be replacing this with other PK fields that will be needed).

    Long story short; with these mods you can enter in any DE name, populate the fields, and update the DE from the page.

    Thanks for sharing this and saving me literal hours of trial and error!

  3. Hey Ivan,

    Is there any way (I haven’t had a real chance to troubleshoot this) to allow the form to allow HTML along with the plain text? I’ve tested it with plain text and the form inserts as expected; but as soon as I add any HTML such as an inline link or in-line styling it errors out.

  4. Hey Tony, please give me more details. Are you trying to put the HTML in the JSON?

  5. Ivan,
    Yeah my use case involves being able to add some basic HTML into the variable form; href tags, and inline style tags mainly.

    I think I’ll need to use RegEx to achieve what I need to do.

  6. Tony,

    When trying to insert HTML in a JS variable, errors can be made.

    Please verify the validity of your JSON by using an online JSON validator.

Leave a Reply

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

ampscript
Up Next:

How to create a password protected Marketing Cloud page using AMPscript and SSJS

How to create a password protected Marketing Cloud page using AMPscript and SSJS