👉 Important Update About Horizontal Forms 👈

forms
feature-updates

#1

Big news from Unbounce HQ! :spinbounce:

We’re excited to announce that in the coming weeks, you’ll be able to set your landing pages and overlay forms horizontally natively within the Unbounce app! No more workarounds or custom scripts, just drag, drop and convert!

:warning: However, there’s some important information that goes along with this. Please read on…

Although native horizontal forms will be starting to roll out November 14, and should be available to everyone by November 30th, we wanted to give you a heads-up as we’ve had to make a few changes to how our forms are built.

If you’ve added custom code that targets the forms on your pages or overlays, there is a possibility that your forms may be adversely affected by this release (i.e. your forms may no longer look or function as before).

PLEASE NOTE: Our new form functionality will only take effect, possibly compromising your custom code, if you re-publish your landing page or overlay. If your page or overlay is currently published and remains that way, it will continue to function as normal. If you’d like further clarification on whether your scripts will be effected, comment on this thread and we’ll sort you out.

If you haven’t created any custom code targeting forms, feel free to ignore this warning and just bask in the exciting glow of new features coming to our page builder! :tada:

For those of you who have created custom scripts, there’s more information here into the changes we’ve made on the forms. We hope this will help you in making any applicable changes needed to your scripts.

If you have any questions, hit reply on this post and we’ll be happy to provide more information! :point_down:


[How-to] Create Horizontal Forms in Unbounce
#2

#3

We added a custom date picker to this page. Do you think it’ll be affected?
https://www.canuckplace.org/team-canuck-place-birthday-page/


#4

Hi there!

If you’re referring to the date picker found in our documentation here, your script won’t be affected. All good! If it’s a different script, let me know and I’ll make sure you have all the information you need!
(PS - Go Canucks go!)


#5

This is SO exciting @Jess! I never got around to testing out the horizontal form script on my pages, but will definitely do this once it rolls out in the builder. :raised_hands:t4: :heart_eyes:

I have a few scripts/css in place on most of my pages – will it affect these?

I had a scan through the Google doc, but not exactly clear …

  1. Form placeholders

  2. Form inline error messages/CSS

  3. Form CSS styling –

    <style>
    
    #lp-pom-form-962 .lp-pom-form-field input[type=text] {
     color: #333;  
     }
    
    #lp-pom-form-962 .lp-pom-form-field textarea {
    color: #333;
    } 
    
    #lp-pom-form-962 
    .form_elem_email, 
    .form_elem_first_name, 
    .form_elem_last_name, 
    .form_elem_phone_number, 
    .form_elem_how_did_you_hear_about_the_program, 
    .form_elem_where_do_you_live {  
       font-family: 'Open Sans', sans-serif;
       font-weight: 300;
       color: #333;
    }
    </style>

#6

Hey @Canuck_Place @Zoe_Tattersall

Took a quick peek of those scripts. From quick glance those should work exactly as they did before. We tried to make our form code as backwards compatible as possible. If it’s a popular script found on this community it will likely still work.

A possible caveat is if you do use the new horizontal form feature to layout your fields into multiple columns. We can’t guarantee some of these scripts will work with this new dimension.

That said, before you hit that ‘publish’ button be sure to check the results in ‘preview’. Preview is a great way to validate any potential conflicts before you take those changes live.

Johnny


#7

Hi @Jess, thanks for the update!
We use a javascript to autofill the field names in our forms. Just wanted to see if you thought the new changes would impact our form fields. (script is below)

<script type="text/javascript">
  lp.jQuery(function($) {
  
    // Define your placeholder texts here, corresponding to Unbounce's field names
    var placeholders = {
      "FirstName": "First Name",
      "LastName": "Last Name",
      "Email": "Email",
      "Phone": "Phone"
    };
    // Sets the HTML5 placeholders
    for(var id in placeholders){$("#"+id).attr("placeholder",placeholders[id])}  
    // Polyfill to add support for browsers like IE<=9
    if(document.createElement("input").placeholder===undefined){$("html").attr("data-placeholder-focus","false");$.getScript("//jamesallardice.github.io/Placeholders.js/assets/js/placeholders.jquery.min.js",function(){$(function(){var e=window.module.lp.form.data.validationRules;var t=window.module.lp.form.data.validationMessages;lp.jQuery.validator.addMethod("notEqual",function(e,t,n){return this.optional(t)||$(t).attr("data-placeholder-active")!=="true"||e!==n},function(e,n){return t[$(n).attr("id")].required});for(var n in placeholders){if($("#"+n).length){if(typeof t[n].required!=="undefined"){e[n].notEqual=placeholders[n]}else{e[n]={}}}}})})}
  });
</script>

#8

Hey @Samantha_Blaser!

Are you using this workaround for local storage to autofill function? If that’s the case, you’re all good! :slight_smile:


#9

You know for us novices, a short video of what you are talking would really be nice.

We appreciate your business, if there is anything further we can do please contact us.

Warmest regards,

Tony Bonifacic


#10

Awesome, I’ll be sure to test it out first :slight_smile:


#11

Hi @Jess, thank you for the update.

We are using custom scripts heavily inside our landing pages, in order not to just collect emails, but to let people signup to our website right inside Unbounce (so server-side data validation is necessary).

Some examples are:
https://benvenuto.cortilia.it/spesa-online/ —> cortilia_1_1.js
https://benvenuto.cortilia.it/spesa-online-classico/ —> cortilia_1_0.js

The scripts are identical for the most part, but version _1_1 also inject a second button to the form to redirect visitors to a dedicated page in our website to let them signup with Facebook/Google.

Do you think our scripts will be affected by the update?

Thank you,
Umberto


#12

This is awesome!!! Can’t wait to try it out, thanks guys!

We have a form label script to our pages, do you think it’ll be affected? From: Form titles in fields


#13

Hi Tony! That’s really great feedback. I’ll see what I can come up with to help make this a bit more clear :slightly_smiling_face:


#14

Hi @iumbs! I’m one of the developers who worked on this feature. I looked into your scripts and I believe they should be okay, but I recommend duplicating the page to test it out when the feature rolls out. Specifically, I have concerns on the inline errors that are attached to the form, presumably used for the server-side validation you mentioned. There’s a possibility they may not appear where you’re expecting them to.

Horizontal forms changes our element wrappers (the divs that contain the label, input, and error field) to flex box elements to ensure the inputs span the entire width of the form. If your inline errors rely on absolute positioning they may not appear in the correct spot. If they’re simply inserted into our element wrapper, as it looks like they are, I think you should be fine.

If you’d like more details or have any other questions please reach out!


#15

Hi @mae!

The label placeholder script won’t be affected. And we’ll have native placeholder text available after a bit more testing so you won’t need to use the custom script anymore :slight_smile:


#16

Hi,

1/ I use mask_plugin.js and add_mask.js in order to force people to indicate their phone number in an international format.

2/ And I use a script called “CTM” in order to pass data to CallTrackingMetrics’s FormReactor.

mask_plugin.js

<script>
var $j = jQuery.noConflict();

!function(factory) {
    "function" == typeof define && define.amd ? define([ "jquery" ], factory) : factory("object" == typeof exports ? require("jquery") : jQuery);
}(function($j) {
    var caretTimeoutId, ua = navigator.userAgent, iPhone = /iphone/i.test(ua), chrome = /chrome/i.test(ua), android = /android/i.test(ua);
    $j.mask = {
        definitions: {
            "1": "[1-9]",
            "9": "[0-9]",
            a: "[A-Za-z]",
            "*": "[A-Za-z0-9]"
        },
        autoclear: !0,
        dataName: "rawMaskFn",
        placeholder: "_"
    }, $j.fn.extend({
        caret: function(begin, end) {
            var range;
            if (0 !== this.length && !this.is(":hidden")) return "number" == typeof begin ? (end = "number" == typeof end ? end : begin, 
            this.each(function() {
                this.setSelectionRange ? this.setSelectionRange(begin, end) : this.createTextRange && (range = this.createTextRange(), 
                range.collapse(!0), range.moveEnd("character", end), range.moveStart("character", begin), 
                range.select());
            })) : (this[0].setSelectionRange ? (begin = this[0].selectionStart, end = this[0].selectionEnd) : document.selection && document.selection.createRange && (range = document.selection.createRange(), 
            begin = 0 - range.duplicate().moveStart("character", -1e5), end = begin + range.text.length), 
            {
                begin: begin,
                end: end
            });
        },
        unmask: function() {
            return this.trigger("unmask");
        },
        mask: function(mask, settings) {
            var input, defs, tests, partialPosition, firstNonMaskPos, lastRequiredNonMaskPos, len, oldVal;
            if (!mask && this.length > 0) {
                input = $j(this[0]);
                var fn = input.data($j.mask.dataName);
                return fn ? fn() : void 0;
            }
            return settings = $j.extend({
                autoclear: $j.mask.autoclear,
                placeholder: $j.mask.placeholder,
                completed: null
            }, settings), defs = $j.mask.definitions, tests = [], partialPosition = len = mask.length, 
            firstNonMaskPos = null, $j.each(mask.split(""), function(i, c) {
                "?" == c ? (len--, partialPosition = i) : defs[c] ? (tests.push(new RegExp(defs[c])), 
                null === firstNonMaskPos && (firstNonMaskPos = tests.length - 1), partialPosition > i && (lastRequiredNonMaskPos = tests.length - 1)) : tests.push(null);
            }), this.trigger("unmask").each(function() {
                function tryFireCompleted() {
                    if (settings.completed) {
                        for (var i = firstNonMaskPos; lastRequiredNonMaskPos >= i; i++) if (tests[i] && buffer[i] === getPlaceholder(i)) return;
                        settings.completed.call(input);
                    }
                }
                function getPlaceholder(i) {
                    return settings.placeholder.charAt(i < settings.placeholder.length ? i : 0);
                }
                function seekNext(pos) {
                    for (;++pos < len && !tests[pos]; ) ;
                    return pos;
                }
                function seekPrev(pos) {
                    for (;--pos >= 0 && !tests[pos]; ) ;
                    return pos;
                }
                function shiftL(begin, end) {
                    var i, j;
                    if (!(0 > begin)) {
                        for (i = begin, j = seekNext(end); len > i; i++) if (tests[i]) {
                            if (!(len > j && tests[i].test(buffer[j]))) break;
                            buffer[i] = buffer[j], buffer[j] = getPlaceholder(j), j = seekNext(j);
                        }
                        writeBuffer(), input.caret(Math.max(firstNonMaskPos, begin));
                    }
                }
                function shiftR(pos) {
                    var i, c, j, t;
                    for (i = pos, c = getPlaceholder(pos); len > i; i++) if (tests[i]) {
                        if (j = seekNext(i), t = buffer[i], buffer[i] = c, !(len > j && tests[j].test(t))) break;
                        c = t;
                    }
                }
                function androidInputEvent() {
                    var curVal = input.val(), pos = input.caret();
                    if (oldVal && oldVal.length && oldVal.length > curVal.length) {
                        for (checkVal(!0); pos.begin > 0 && !tests[pos.begin - 1]; ) pos.begin--;
                        if (0 === pos.begin) for (;pos.begin < firstNonMaskPos && !tests[pos.begin]; ) pos.begin++;
                        input.caret(pos.begin, pos.begin);
                    } else {
                        for (checkVal(!0); pos.begin < len && !tests[pos.begin]; ) pos.begin++;
                        input.caret(pos.begin, pos.begin);
                    }
                    tryFireCompleted();
                }
                function blurEvent() {
                    checkVal(), input.val() != focusText && input.change();
                }
                function keydownEvent(e) {
                    if (!input.prop("readonly")) {
                        var pos, begin, end, k = e.which || e.keyCode;
                        oldVal = input.val(), 8 === k || 46 === k || iPhone && 127 === k ? (pos = input.caret(), 
                        begin = pos.begin, end = pos.end, end - begin === 0 && (begin = 46 !== k ? seekPrev(begin) : end = seekNext(begin - 1), 
                        end = 46 === k ? seekNext(end) : end), clearBuffer(begin, end), shiftL(begin, end - 1), 
                        e.preventDefault()) : 13 === k ? blurEvent.call(this, e) : 27 === k && (input.val(focusText), 
                        input.caret(0, checkVal()), e.preventDefault());
                    }
                }
                function keypressEvent(e) {
                    if (!input.prop("readonly")) {
                        var p, c, next, k = e.which || e.keyCode, pos = input.caret();
                        if (!(e.ctrlKey || e.altKey || e.metaKey || 32 > k) && k && 13 !== k) {
                            if (pos.end - pos.begin !== 0 && (clearBuffer(pos.begin, pos.end), shiftL(pos.begin, pos.end - 1)), 
                            p = seekNext(pos.begin - 1), len > p && (c = String.fromCharCode(k), tests[p].test(c))) {
                                if (shiftR(p), buffer[p] = c, writeBuffer(), next = seekNext(p), android) {
                                    var proxy = function() {
                                        $j.proxy($j.fn.caret, input, next)();
                                    };
                                    setTimeout(proxy, 0);
                                } else input.caret(next);
                                pos.begin <= lastRequiredNonMaskPos && tryFireCompleted();
                            }
                            e.preventDefault();
                        }
                    }
                }
                function clearBuffer(start, end) {
                    var i;
                    for (i = start; end > i && len > i; i++) tests[i] && (buffer[i] = getPlaceholder(i));
                }
                function writeBuffer() {
                    input.val(buffer.join(""));
                }
                function checkVal(allow) {
                    var i, c, pos, test = input.val(), lastMatch = -1;
                    for (i = 0, pos = 0; len > i; i++) if (tests[i]) {
                        for (buffer[i] = getPlaceholder(i); pos++ < test.length; ) if (c = test.charAt(pos - 1), 
                        tests[i].test(c)) {
                            buffer[i] = c, lastMatch = i;
                            break;
                        }
                        if (pos > test.length) {
                            clearBuffer(i + 1, len);
                            break;
                        }
                    } else buffer[i] === test.charAt(pos) && pos++, partialPosition > i && (lastMatch = i);
                    return allow ? writeBuffer() : partialPosition > lastMatch + 1 ? settings.autoclear || buffer.join("") === defaultBuffer ? (input.val() && input.val(""), 
                    clearBuffer(0, len)) : writeBuffer() : (writeBuffer(), input.val(input.val().substring(0, lastMatch + 1))), 
                    partialPosition ? i : firstNonMaskPos;
                }
                var input = $j(this), buffer = $j.map(mask.split(""), function(c, i) {
                    return "?" != c ? defs[c] ? getPlaceholder(i) : c : void 0;
                }), defaultBuffer = buffer.join(""), focusText = input.val();
                input.data($j.mask.dataName, function() {
                    return $j.map(buffer, function(c, i) {
                        return tests[i] && c != getPlaceholder(i) ? c : null;
                    }).join("");
                }), input.one("unmask", function() {
                    input.off(".mask").removeData($j.mask.dataName);
                }).on("focus.mask", function() {
                    if (!input.prop("readonly")) {
                        clearTimeout(caretTimeoutId);
                        var pos;
                        focusText = input.val(), pos = checkVal(), caretTimeoutId = setTimeout(function() {
                            input.get(0) === document.activeElement && (writeBuffer(), pos == mask.replace("?", "").length ? input.caret(0, pos) : input.caret(pos));
                        }, 10);
                    }
                }).on("blur.mask", blurEvent).on("keydown.mask", keydownEvent).on("keypress.mask", keypressEvent).on("input.mask paste.mask", function() {
                    input.prop("readonly") || setTimeout(function() {
                        var pos = checkVal(!0);
                        input.caret(pos), tryFireCompleted();
                    }, 0);
                }), chrome && android && input.off("input.mask").on("input.mask", androidInputEvent), 
                checkVal();
            });
        }
    });
});
  
  window.jQuery = $ ;
/**
    * Do not remove this section; it allows our team to troubleshoot and track feature adoption. 
    * TS:0002-03-048
*/
</script>

add_mask.js

<script>
$j(function($j){
    //Below are few examples. Replace the IDs with your own form field IDs. 
   	$j("#phone").mask("+33 1 99 99 99 99");
  	$j("#phone_number_2_north_america").mask("1-999-999-9999");
  	$j("#postal_code").mask("a9a 9a9");
  	$j("#zipcode").mask("99999");
  });
</script>

CTM:

<script>
(function() {
  	function getFormValue(objArr, property) {
      	var obj = objArr.filter(function(item) { return item.name === property})
  		return obj.length > 0 ? obj[0].value : "";
    }
  
  	document.getElementsByTagName("form")[0].onsubmit = function(event) {
      	event.preventDefault()
      	var formData = $(this).serializeArray();
      	
      	// trigger this method once you have the form data captured within you web form.  This means you have to capture the form submission events and data.
		__ctm.form.track('app.calltrackingmetrics.com', // the capture host
                 '----', // this FormReactor
                 '----',
                 {
                    country_code: "33", // the expected country code e.g. +1, +44, +55, +61, etc... the plus is excluded
                    name: getFormValue(formData, "nom_de_famille"),
                    phone: getFormValue(formData, "phone").substr(3),
                    email: getFormValue(formData, "email"),
                    custom: {
                        civilite: getFormValue(formData, "civilite"),
                        message:  getFormValue(formData, "message")
                    }
                 });
      
      	return false;
    }
})()
</script>

#17

Hi,

I used a script with the help of someone at Unbounce on this page https://go.adrianflux.co.uk/learner-driver-insurance-1/ will this be affected?


#18

Hey Dez, can you tell me which script you used exactly? That’ll help us to give you all the info you need. :slight_smile:


#19

Hi @Etienne! It looks like both your scripts, Mask and CTM, will be fine. These scripts deal with the input values and the change to forms is strictly visual.

You’re probably already aware, but if you add or remove form fields that are using Mask, you may need to reconnect them. For example, if you’re using Mask on a field named “#phone_a” like:

<script>
$j(function($j){
  $j("#phone_a").mask("+33 1 99 99 99 99");
});
</script>

and you replace that field with a new one named “#phone_b”, you’ll need to update your function like:

<script>
$j(function($j){
  $j("#phone_b").mask("+33 1 99 99 99 99");
});
</script>

Feel free to reach out if you need a hand!


#20

Tx a ton Brian.