Solved

Multi-Field, Multi-Step Form 🔥


Userlevel 7
Badge +1
  • Former Community Manager @ Unbounce
  • 831 replies

Every so often, the Unbounce Community is blessed by some absolute freakin’ ROCKSTARS like @Caroline. A few months back, she took it upon herself to share this boss workaround for a multi-step form and it blew our minds. This deserved to be a standalone post within our Tips and Scripts.

Won’t you join me in celebrating the awesomeness that is Caroline?! It’s community members like her that make this such a badass online community! 🙌


33%20PM

Here is what we have been using for multi fields on multi steps.

This as has worked well for us across many pages & clients.
Here is an example of it in action: http://unbouncepages.com/multi-step-test/

  1. Create form in one column the way you normally would, in the order you want the fields to appear.
  2. Add the following script (before body end tag), name it “Multi Step 1”
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js" type="text/javascript"></script>
<script>
  function UnbounceMultiStep(form, options) {
    // Validate arguments
    if (!form.is('.lp-pom-form form')) {
      return console.error('jQuery.unbounceMultiStep must be called on an Unbounce form.');
    }

    if (typeof options !== 'object') {
      return console.error('No options were passed to jQuery.unbounceMultiStep');
    }

    this.options = options;

    // Store DOM elements
    this.form = form;
    this.formContainer = this.form.closest('.lp-pom-form');
    this.fields = this.form.find('div.lp-pom-form-field');
    this.fieldsByStep = [];
    this.currentStep = 0;
    this.forwardButton = this.form.find('button[type=submit]').eq(0);

    // Verbiage
    this.text = {};
    this.text.next = this.options.nextButton;
    this.text.showSteps = this.options.showSteps;
    this.text.back = this.options.backButton;
    this.text.submit = this.forwardButton.text();

    this.init();
  }

  UnbounceMultiStep.prototype.init = function() {
    this.formContainer.addClass('multistep initial');
    this.form.attr('id', 'fields');

    // Add progress bar
    this.formContainer.prepend('<div id="progress-bar"></div>');
    this.progressBar = jQuery('#progress-bar');

    // Replicate Unbounce's field spacing
    var height = parseInt(this.fields.eq(0).css('height'), 10);
    var top = parseInt(this.fields.eq(1).css('top'), 10);
    this.fields.css('margin-bottom', top - height + 'px');
    this.progressBar.css('margin-bottom', top - height + 'px');

    // Set up fieldset elements for each step
    for (var i = 0; i < this.options.steps.length; i++) {
      console.log('Adding new fieldset.');
      this.form.find('.fields').append('<fieldset></fieldset>');
    }
    this.steps = this.form.find('fieldset');
    this.steps.addClass('step');

    // Sort fields into new steps
    var currentField = 0;
    for (currentStep = 0; currentStep < this.options.steps.length; currentStep++) {
      this.progressBar.append(
        '<div class="step">' +
          '<span class="num">' +
          (currentStep + 1) +
          '</span>' +
          '<span class="title">' +
          this.options.steps[currentStep].title +
          '</span>' +
          '</div>'
      );
      this.fieldsByStep[currentStep] = [];
      for (i = 0; i < this.options.steps[currentStep].fields; i++) {
        console.log('Field ' + currentField + ' -> step ' + currentStep);
        this.fields.eq(currentField).appendTo(this.steps.eq(currentStep));
        this.fieldsByStep[currentStep].push(this.fields.eq(currentField));
        currentField++;
      }
    }

    // Add any remaining fields to last step
    if (currentField < this.fields.length) {
      currentStep--;
      for (i = currentField; i < this.fields.length; i++) {
        console.log('Field ' + currentField + ' -> step ' + currentStep);
        this.fields.eq(currentField).appendTo(this.steps.last());
        this.fieldsByStep[currentStep].push(this.fields.eq(currentField));
        currentField++;
      }
    }

    this.progressBarItems = jQuery('#progress-bar .step');

    // Add a back button
    this.backButton = this.forwardButton.clone().insertBefore(this.forwardButton);
    this.backButton.addClass('back-button');
    this.backButton.children('span').html(this.text.back);

    // Add event listeners
    this.backButton.bind('click.unbounceMultiStep', this.backHandler.bind(this));

    this.forwardButton.bind('click.unbounceMultiStep', this.forwardHandler.bind(this));

    this.fields.find(':input').bind('invalid', function(event) {
      // Prevent browser from trying to focus invalid inputs on non-visible steps
      if (jQuery(event.currentTarget).closest('fieldset.active').length === 0) {
        event.preventDefault();
      }
    });

    // Show first step
    this.goToStep(0);
  };

  UnbounceMultiStep.prototype.goToStep = function(newStep) {
    // Make sure we aren't going to a step that doesn't exist
    if (newStep < 0 || newStep >= this.steps.length) {
      return false;
    }

    this.steps
      .eq(this.currentStep)
      .removeClass('active')
      .hide();

    this.steps
      .eq(newStep)
      .addClass('active')
      .fadeIn();

    this.progressBarItems.eq(this.currentStep).removeClass('active');
    this.progressBarItems.eq(newStep).addClass('active');

    this.formContainer.toggleClass('initial', newStep === 0);

    // Update the label of the forward button
    var current = parseInt(newStep) + 2;
    var total = this.steps.length;
    var nextText = this.text.showSteps
      ? this.text.next + ' (Step ' + current + ' of ' + total + ')'
      : this.text.next;
    var submitText = this.text.submit;

    var forwardButtonLabel = newStep === this.steps.length - 1 ? submitText : nextText;

    this.forwardButton.children('span').html(forwardButtonLabel);

    this.currentStep = newStep;
  };

  UnbounceMultiStep.prototype.isValid = function() {
    return this.steps.eq(this.currentStep)[0].querySelectorAll(':invalid').length === 0;
  };

  UnbounceMultiStep.prototype.forwardHandler = function(event) {
    if (!this.isValid()) {
      // If one or more fields on the current step is invalid, prevent going to next step. Allow the
      // default click action, which will display the validation errors.
      return;
    }

    if (this.currentStep === this.steps.length - 1) {
      // If we are on the last step, go back to the first step, and allow the default click action,
      // which will submit the form.
      this.goToStep(0);
    } else {
      event.preventDefault();
      this.goToStep(this.currentStep + 1);
    }
  };

  UnbounceMultiStep.prototype.backHandler = function(event) {
    event.preventDefault();
    this.goToStep(this.currentStep - 1);
  };

  jQuery.fn.unbounceMultiStep = function(options) {
    this.map(function(index, element) {
      new UnbounceMultiStep(jQuery(element), options);
    });
    return this;
  };
</script>
  1. Add another script (before body end tag), name it “Multi Step 2”
<script>
  jQuery('.lp-pom-form form').unbounceMultiStep({
    steps: [
      { title: '', fields: 3 },
      { title: '', fields: 3 },
      { title: '', fields: 6 },
    ],
    nextButton: 'Continue »',
    backButton: 'Back',
  });

  jQuery('form#fields').css('margin-top', '20px');

  jQuery('#progress-bar').hide();

  jQuery('fieldset.step:last-of-type div.lp-pom-form-field').css('float', 'left');

  jQuery('fieldset.step:last-of-type div.lp-pom-form-field').css('width', '150px');

  jQuery('fieldset.step:last-of-type div.lp-pom-form-field input').css('width', '140px');

  jQuery('fieldset.step:last-of-type div.lp-pom-form-field select').css('width', '140px');

  jQuery('fieldset.step:last-of-type div.lp-pom-form-field:last-of-type select').css('width', '140px');

  jQuery('#Country').val('United States');

  var disclaimer =
    '<p style="margin-top: 55px; font-family:arial,helvetica,sans-serif;font-size: 10px; color: #636363; line-height: 12px; padding: 40px 0 20px 0;"> I like cats because they are fat and fluffy always ensure to lay down in such a manner that tail can lightly brush humans nose , for run outside as soon as door open. Chase ball of string lounge in doorway or give me some of your food give me some of your food give me some of your food meh, i dont want it yet plan steps for world domination so touch water with paw then recoil in horror for chase dog then run away i shredded your linens for you.</p>';

  jQuery('fieldset.step:last-of-type').append(disclaimer);

  jQuery(window).keydown(function(event) {
    if (event.keyCode === 13) {
      event.preventDefault();
      return false;
    }
  });
</script>
  1. Add the CSS (add stylesheet) - This is copied straight from the page I built for the demo.
<style>
  html,
  * {
    -webkit-font-smoothing: antialiased !important;
  }

  /* Style Form */

  .multistep #fields {
    height: auto !important;
  }

  .multistep #fields .step {
    height: auto !important;
    position: absolute;
    top: 0;
    display: none;
  }

  .multistep #fields .step.active {
    position: relative;
  }

  .multistep #fields .step .lp-pom-form-field {
    position: relative !important;
    top: auto !important;
  }

  .multistep .lp-pom-button {
    position: relative !important;
    top: 0 !important;
    width: 60% !important;
    display: inline-block !important;
    float: right;
    letter-spacing: 2px !important;
    font-weight: 700 !important;
    text-transform: uppercase !important;
  }

  .multistep .lp-pom-button.back-button {
    margin-right: 0;
    float: left;
    opacity: 0.7 !important;
    background-color: #f1f1f1 !important;
    color: #636363 !important;
    width: 35% !important;
    letter-spacing: 2px !important;
    font-weight: 700 !important;
    text-transform: uppercase !important;
  }

  .multistep.initial .lp-pom-button.back-button {
    display: none !important;
  }

  .lp-pom-form .lp-pom-form-field .text {
    font-family: 'Montserrat' !important;
    font-weight: 700 !important;
    text-transform: uppercase !important;
    color: #3f4144 !important;
    font-size: 12px !important;
    letter-spacing: 2px !important;
  }

  .lp-pom-form .lp-pom-form-field select {
    background-color: #ffffff !important;
    border-radius: 20px;
    font-family: 'Montserrat' !important;
    font-weight: 700 !important;
    text-transform: uppercase !important;
    color: #3f4144 !important;
    font-size: 11px !important;
    letter-spacing: 1px !important;
  }
</style>
  1. Make sure anything in the CSS that has an ID for the form is change to your ID (this one only has two places to replace)

  2. In step #3, in the script called, ‘Multi Step 2’ - you can change the number of fields on each step and they will appear in the order you have in them. There you can also rename the ‘Back’ and ‘Next’ buttons and well as add or remove a disclaimer.

  3. SAVE!

This is a bit more on the advanced side, so I will be happy to help anyone with this as much as I can!
Disclaimer - I am not the original creator of this script

Please feel free to add on to this, edit it, remove something, etc.

One issue I have ran into is that the page will still go as long as the form is in the editor UI. If anyone has any ideas of that, please add!

Please make sure to test, test, test!

Enjoy


I think I can speak on behalf of a lot of people…

notworthy

icon

Best answer by TimothyDO 7 August 2018, 15:27

View original

100 replies

@TimothyDO

Thanks that makes sense and it worked.

One issue I’m trying to figure out is how to stop the form submitting when filling out the form on mobile and tapping “go” on an android keyboard. yet to test on an iphone but i’m afraid the same issue will remain - that a form can be submitted without completing all fields by tapping “go” on the mobile keyboard after filling in a field.

EDIT:

The following code will change the behavior of the GO button on mobile devices, so instead of submitting the form pressing GO will just close the keyboard

jQuery(document).ready(function() {
    jQuery('input').keypress(function(e) {
        var code = (e.keyCode ? e.keyCode : e.which);
        if ( (code==13) || (code==10))
            {
            jQuery(this).blur();
            return false;
            }
    });
});

Remember to add script tags at the start and at the end

@raget in your style sheets set #lp-pom-button-11 left attribute to 0px; or turn it off…

@Jess

I noticed there is some script that adds a progress bar in lines 39-41

  // Add progress bar
  this.formContainer.prepend('<div id="progress-bar"></div>');
  this.progressBar = lp.jQuery('#progress-bar');

And in the second script there is code that hides it

lp.jQuery('#progress-bar').hide();

Showing it instead of hiding it makes numbers 1,2 and 3 show but no progress bar.

How do I get this progress bar working?

That basically is the progress bar 😉

It’s providing a good framework to create your own through JS/CSS tweaks…

For example on one of the landing pages I am using I’ve tweaked it as so…

Mutlipage.js
    this.progressBar.append('<div class="step">' +
                            '<span class="num">'+Math.floor(((currentStep)/this.options.steps.length)*100) + '% complete </span>' +
                            '<span class="title">'+ this.options.steps[currentStep].title +'</span>' +
                            '<span class="description">'+ this.options.steps[currentStep].description +'</span>' +
                            '</div>');

and multiPageOptions.js

      lp.jQuery('.lp-pom-form form').unbounceMultiStep({
  	steps: [
    	{ title: '', description: 'Select your gender:', fields: 1 },
    	{ title: '', description: 'Select your age:',  fields: 1 },
    	{ title: '', description: 'Are you a smoker?',  fields: 1 },
    	{ title: '', description: 'Enter your name:',  fields: 2 },
    	{ title: '', description: 'Enter your phone number:',  fields: 1 }
   	],

This leads to a title/description/percent complete info that I can then format using CSS…

((excuse the formating for the first javascript, the wysiwyg is refusing to format it correctly…))

Hey @Caroline ,

Sometimes I get a form completion where most of the fields are empty even though they are mandatory if you want to progress to the next step. Even the name, email fields (mandatory) in the last steps are empty.

Within these leads the fields in the first step are filled in, however alle of the fields from step 2 and onwards are empty. Anyone had this situation before?

Hi @Caroline @TimothyDO

Where do I edit the code to change the number of columns on each step? It currently does two columns on the final step and one during all the others. Ideally, I would be able to reverse the order of that and have two columns on my first step and one in my second step. Can you please help?

Thanks!
Riley

In this section in the second script you can set the number of pages and number of options to each page. The script above does 3 on the first and second pages, 6 on the last (Any remainder is added to the last page).
Make sense?

Hi Timothy,

This script is working for me when I have an email on the second part of the form but it is also removing the built in Unbounce email validation. The email field now validates with any value entered into the field even though the field has the Unbouce email validation checked.

I tested and when I removed the script the correct Unbounce email validation returned.

Have you had this issue also? If so, how were you able to resolve so the field validates as an email instead of any text?

Hi @Caroline -thank you very much for the code!

I’m having a problem where the back button is submitting the form.

Anyone else experiencing this?

I’m trying to isolate the issue and will report back.

Thanks!

Userlevel 5
Badge +2

With every update to the builder there is almost always an update that needs to be made to the script… which has been happening a lot lately. With this instance, there isn’t a great way around this (and it isn’t the easiest if you are not familiar with custom scripts or CSS) - but you have to change the button’s ID attributes. This will remove default styles

First, go into Multi Step 1 and add the following line at the bottom of the Store DOM Elements section:

this.forwardButton.attr('id', 'next-button');

The section will look like this:

  // Store DOM elements
  this.form = form;
  this.formContainer = this.form.closest('.lp-pom-form');
  this.fields = this.form.find('div.lp-pom-form-field');
  this.fieldsByStep = [];
  this.currentStep = 0;
  this.forwardButton = this.formContainer.find('.lp-pom-button').eq(0);
  this.forwardButton.attr('id', 'next-button');

Second, go down to the Add a Back Button section and change it to the following:

  // Add a back button
  this.backButton = this.forwardButton.clone();
  this.backButton.attr('id','back-button');
  this.backButton.insertBefore(this.forwardButton);
  this.backButton.addClass('back-button');
  this.backButton.children('span').html(this.text.back);

Now you will need to add a new stylesheet to edit the buttons. It will look something like the following:
Please note: this is just an example & will need to be completely custom for your design

<style>
#next-button, #back-button {
display: block;
border-radius: 20px;
width: 190px;
position: absolute;
background-color: #522398;
box-shadow: none;
text-shadow: none;
font-family: Roboto;
}
</style>

I hope this helps!

@Caroline

Thank you so much for your fast and very well explained solution!

This did the trick and fortunately I can fix up the CSS 🙂

Apologies Abraham, I only have this script on one variant at the moment as it goes through my companies QA process.
As Caroline mentions below, updates play havoc with certain scripts and I’ve spent the past week trying to debug issues on our custom inline errors script (which features on all our landing pages :*( ) So the new version of multipage is a little on the back burner for me at the moment… does however look like I am writing the new version of the multipage compatible inline errors script I’ve been pushing for…

Ok, I understand, thank you for the reply. Good luck with everything and I look forward to seeing any new tips/scripts that may be coming! 🙂

Hi again!
One more question, is there a way to make the “enter” android button to go to the next field?
The experience now is I need to hide the keyword each time to select the next field and is defenetly not the best.

Hi everybody! Thank you for the script!

Is there, by any chance, anyone that could share the script including a progress bar? That would be awesome!

Thanks! 🙂

Thanks for this. I ALMOST have it working. I can’t seem to figure out the css formatting, though, for the buttons. How do I get the form fields centered on mobile? On steps with multiple fields visible, they appear shortened and left justified instead of centered/full width. How do I fix this?

Having the same problem with email validation. Has anyone cracked this yet? Thanks.

Hi all,

Thanks again for all the great tips so far!

I’m having an issue where eager users are managing to submit the form multiple times before the follow up page loads- this is causing some havoc for our backend system that is receiving the data via webhook.

I initially used @TimothyDO 's code that prevents the form flicking back to “page one” on submit. I initially thought having the “submit” button sitting idly there on the page while the follow up page loaded was causing people to click it again thinking it wasnt working, so I reverted to the original code.

It seems though that the “continue” button becomes the “submit” button after meeting a set of criteria in the JS (is this correct?), so the problem is still occuring- people are clicking “continue” causing additional submits.

My question is, is there a way to disable the buttons on submit? Or better yet, adding to @TimothyDO 's fix, disable the submit button and change the text to “please wait”?

I’ve searched for a solution and tried my hand at fixing it but unfortunately I havent managed to get anything working.

Apologies if this is starting to fall outside the scope of this post, even a point in the right direction would be most appreciated 🙂

Try something along the lines of https://stackoverflow.com/questions/3366828/how-to-disable-submit-button-once-it-has-been-clicked

and add that in after the line
if ( this.currentStep === this.steps.length - 1 ) {

That ways it should cut in after the final button press if the form is valid… else your not blocking the persons ability to modify errors and try a submission again…

((would have a better go at this but trying to sort out database issues… ))

Hi @TimothyDO,

Thanks for the suggestion! It appears however that disabling the submit button is a bit trickier than you’d think, with certain browsers treating the disable JS differently. Someone in the linked thread mentioned they ended up moving the button instead, so for anyone else struggling with this I came up with the following:

In Multi Step 1 JS
After
this.form.submit();

Add
this.forwardButton.addClass('move-on-submit');

My code looks like this (note I removed the line that returns the form to the initial step)

  if ( this.currentStep === this.steps.length - 1 ) {    
    this.form.submit();
    this.forwardButton.addClass('move-on-submit');
    
  }else{
    //$('#reg_nurse_disclaimer').hide();	
    this.goToStep( this.currentStep + 1 );
  }
}; 

Then in one of your CSS files
Add
.move-on-submit { margin-top: 100px; visibility: hidden!important; }

The above will simply move the button down 100px and make it invisible. To the user the button will disapper while the followup page loads.

If anyone sees a potential problem with the above let me know!

Thanks again all.

Awesome!

Is there by any chance a way to automatically jump to next question without clicking “next” button?
sthing like this: https://audimagnet.com/comparador/?v=1&adsid=_19630420300699590

That’d be an extra-awe

@TimothyDO or anyone else, I have an issue with custom validation. I’ve added custom validation to one of my fields (postcode).

 lp.jQuery(function($) {
    // Config
    var ruleID = 'PostcodeValidation';
    var field = 'postcode';
    var message = 'Make sure poscode entered is correct. Include spaces.';
  
    var rules = module.lp.form.data.validationRules[field];
  
    $.validator.addMethod(ruleID, function(value, field) {
  
      // Replace this with any expression you like. It should evaluate to true
      // if the field is valid and false if invalid. This example adds a 
      // rule that the user must enter 'I accept' exactly into the field.
      var valid = /^([Gg][Ii][Rr] 0[Aa]{2})|((([A-Za-z][0-9]{1,2})|(([A-Za-z][A-Ha-hJ-Yj-y][0-9]{1,2})|(([AZa-z][0-9][A-Za-z])|([A-Za-z][A-Ha-hJ-Yj-y][0-9]?[A-Za-z])))) [0-9][A-Za-z]{2})$/.test(value);
  
      return valid || (!rules.required && !value);
    }, message);
  
    rules[ruleID] = true;
  
  });

Now I get a validation error right in the beginning and cant continue with the form (postcode field is at the end of the form).

Could you please help me out. I saw the fix for email validation field but dont really understand how to apply it to this.

Thank you 🙂

I’ve tried this, but it shows me all the numbers on all the pages instead of each step… any ideas?

Apologies but my original page where I was testing this is lost in discarded pages…
I would say (complete guess work) that the append is appending to something outside the page structure… therefore you can see all numbers on all pages. | can’t for the life of me remember where this.progressBar was defined as… but that is where I’d start looking…
Sorry again…

Has anyone had a chance to post this? It would be super useful.

Reply