Ways to incorporate radio buttons, checkboxes, and select fields into a multi-step form using JavaScript

In the snippet below, I have created a multi-step form where users can provide details. The issue is that currently only text input fields are being accepted. The array of questions specifies the type of input required for each question:

  1. Question no.1 requires a text input field for entering a name.
  2. Question no.2 should be answered using radio buttons to select gender.
  3. Question no.3 needs a text input field along with a datepicker for entering a date of birth.
  4. Question no.4 should be a select box to choose a country from a list.
  5. Question no.5 should have checkboxes to select preferences (male, female, other).

As a newcomer to JavaScript, I am facing challenges in implementing this functionality. Can anyone guide me on how to proceed?

The relevant section of the provided JavaScript code is as follows:

// load the next question
function putQuestion() {
  inputLabel.innerHTML = questions[position].question
  inputField.type = questions[position].type || 'text'
  inputField.value = questions[position].answer || ''
  inputField.focus()

  // set the progress of the background
  progress.style.width = position * 100 / questions.length + '%'
  previousButton.className = position ? 'ion-android-arrow-back' : 'ion-person'
  showCurrent()
}

A complete working example has been included for reference.

[JavaScript and CSS code not repeated for uniqueness]
<link rel="stylesheet" href="https://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css">
<div id="progress"></div>
<div class="center">
  <div id="register"> 
    
    <i id="previousButton" class="ion-android-arrow-back"></i> 
    <i id="forwardButton" class="ion-android-arrow-forward"></i>
    
    <div id="inputContainer">
      <input id="inputField" required multiple />
      <label id="inputLabel"></label>
      <div id="inputProgress"></div>
    </div>
    
  </div>
</div>

While I understand the importance of providing a minimal reproducible example, I believe showcasing a functional snippet will aid in comprehending this small project. Any assistance or guidance on this matter would be highly appreciated. Thank you in advance :)

Answer №1

In an effort to meet your requirements, I have included 3 additional divs as per your specifications.

I've also made adjustments to the validation condition as requested.

I hope these changes prove beneficial for you.

Answer №2

To begin, you must update the questions object with all the necessary properties to generate your elements.

Next, make some adjustments to your HTML structure by adding another wrapper within the input container for appending extra input fields like radio buttons, checkboxes, and select options.

If this is not what you're looking for, please take a look at this example:

var questions = [{
    question: "What's your full name?",
    type: 'text',
    name: 'fullname'
  }, // TEXT INPUT FIELD
  {
    question: "What's your gender?",
    type: 'radio',
    name: 'gender',
    values: ['male', 'female', 'other']
  }, // RADIO BUTTONS {male, female, other}
  {
    question: "What's your date of birth?",
    type: 'text',
    name: 'dob'
  }, // TEXT INPUT FIELD WITH id="datepicker"
  {
    question: "What's your country?",
    type: 'select',
    name: 'country',
    values: ['Canada', 'US', 'Other']
  }, // SELECT BOX WITH LIST OF COUNTRIES IN IT
  {
    question: "Interest in?",
    type: 'checkbox',
    name: 'interest',
    values: ['male', 'female', 'other']
  } // CHECKBOXES {male, female, other}
]

// perform actions once all questions have been answered
var onComplete = function() {
  var h1 = document.createElement('h1')
  h1.appendChild(document.createTextNode('Thanks ' + questions[0].answer + ' for checking out this example!'))
  setTimeout(function() {
    register.parentElement.appendChild(h1)
    setTimeout(function() {
      h1.style.opacity = 1
    }, 50)
  }, 1000)
}

;
(function(questions, onComplete) {
  var tTime = 100 // transition transform time from #register in ms
  var wTime = 200 // transition width time from #register in ms
  var eTime = 1000 // transition width time from inputLabel in ms

  // initialization
  if (questions.length == 0) return
  var position = 0
  putQuestion()

  forwardButton.addEventListener('click', validate)
  inputField.addEventListener('keyup', function(e) {
    transform(0, 0) // ie hack to redraw
    if (e.keyCode == 13) validate()
  })

  previousButton.addEventListener('click', function(e) {
    if (position === 0) return
    position -= 1
    hideCurrent(putQuestion)
  })

  // load the next question
  function putQuestion() {
    //hide the elements that you create so they don't show up when you proceed to next steps
    const hideExtraElems = document.querySelectorAll('#inputWrapper span, #inputWrapper select');
    hideExtraElems.forEach(elem => {
      elem.style.display = 'none';
    });

    inputLabel.innerHTML = questions[position].question
    inputField.type = questions[position].type || 'text'

    //add a name attribute so that radio buttons can work
    inputField.name = questions[position].name

    if (questions[position].type == 'radio' || questions[position].type == 'checkbox') {
      //validation for type radio and checkbox

      //hide the main input field
      inputField.style.visibility = 'hidden';
      //loop through each value for 
      questions[position].values.forEach(item => {
        //create a span that will contain your new input fields
        let innerWrapper = document.createElement("span");
        //style a bit
        innerWrapper.style.display = 'flex';
        innerWrapper.style.justifyContent = 'space-between';
        //add value name inside the span before adding the element itself
        let textNode = document.createTextNode(item.toUpperCase());
        //append the value name to the span element
        innerWrapper.appendChild(textNode);

        //create a new input either using createElement or clone your inputField
        let newInput = document.getElementById('inputField').cloneNode(true);
        //assign attributes accordingly
        newInput.type = questions[position].type;
        newInput.value = item;
        newInput.id = newInput.id + '-' + item;
        newInput.style.visibility = 'visible';
        newInput.style.width = 'auto';
        //append the input field inside the span
        innerWrapper.appendChild(newInput);
        //append the span inside the wrapper
        inputWrapper.appendChild(innerWrapper);
      });
    } else if (questions[position].type == 'select') {
      //validation for select field

      //hide the main input field
      inputField.style.visibility = 'hidden';

      //create the main select box outside the loop
      let newSelect = document.createElement("select");

      //loop through the options
      questions[position].values.forEach(item => {
        //create an option element and assign the attributes
        let newOption = document.createElement("option");
        let textNode = document.createTextNode(item.toUpperCase());
        newOption.value = item;
        newOption.appendChild(textNode);

        //append the final option inside the select
        newSelect.appendChild(newOption);

      });

      //append the select inside the inputWrapper
      inputWrapper.appendChild(newSelect);
    } else {
      //show the inputField
      inputField.style.visibility = 'visible';
      inputField.value = questions[position].values || ''
      inputField.focus();
    }

    // set the progress of the background
    progress.style.width = position * 100 / questions.length + '%'
    previousButton.className = position ? 'ion-android-arrow-back' : 'ion-person'
    showCurrent()
  }

  // when submitting the current question
  function validate() {

    var validateCore = function() {
      return inputField.value.match(questions[position].pattern || /.+/)
    }

    if (!questions[position].validate) questions[position].validate = validateCore
    // check if the pattern matches
    if (!questions[position].validate())
      wrong(inputField.focus.bind(inputField))
    else ok(function() {
      // execute the custom end function or the default value set
      if (questions[position].done) questions[position].done()
      else questions[position].answer = inputField.value
      ++position
      // if there is a new question, hide current and load next
      if (questions[position]) hideCurrent(putQuestion)
      else hideCurrent(function() {
        // remove the box if there is no next question
        register.className = 'close'
        progress.style.width = '100%'
        onComplete()
      })
    })
  }

  // helper functions
  function hideCurrent(callback) {
    inputContainer.style.opacity = 0
    inputLabel.style.marginLeft = 0
    inputProgress.style.width = 0
    inputProgress.style.transition = 'none'
    inputContainer.style.border = null
    setTimeout(callback, wTime)
  }

  function showCurrent(callback) {
    inputContainer.style.opacity = 1
    inputProgress.style.transition = ''
    inputProgress.style.width = '100%'
    setTimeout(callback, wTime)
  }

  function transform(x, y) {
    register.style.transform = 'translate(' + x + 'px ,  ' + y + 'px)'
  }

  function ok(callback) {
    register.className = ''
    setTimeout(transform, tTime * 0, 0, 10)
    setTimeout(transform, tTime * 1, 0, 0)
    setTimeout(callback, tTime * 2)
  }

  function wrong(callback) {
    register.className = 'wrong'
    for (var i = 0; i < 6; i++) // shaking motion
      setTimeout(transform, tTime * i, (i % 2 * 2 - 1) * 20, 0)
    setTimeout(transform, tTime * 6, 0, 0)
    setTimeout(callback, tTime * 7)
  }
}(questions, onComplete))
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@100&display=swap');
body {
  margin: 0;
  background: #fbc02d;
  font-family: 'Roboto', sans-serif;
  overflow-x: hidden;
}

h1 {
  position: relative;
  color: #fff;
  opacity: 0;
  transition: .8s ease-in-out;
}

#progress {
  position: absolute;
  background: #c49000;
  height: 100vh;
  width: 0;
  transition: width 0.2s ease-in-out;
}

select{
  width: 100%;
  border: 0;
  outline: none;
  padding: 1rem 0;
}

.center {
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
}


#register {
  background: #fff;
  position: relative;
  width: 550px;
  box-shadow: 0 16px 24px 2px rgba(0,0,0,0.14), 0 6px 30px 5px rgba(0,0,0,0.12), 0 8px 10px -5px rgba(0,0,0,0.3);
  transition: transform .1s ease-in-out;
}

#register.close {
  width: 0;
  padding: 0;
  overflow: hidden;
  transition: .8s ease-in-out;
  box-shadow: 0 16px 24px 2px rgba(0,0,0,0);
}

#forwardButton {
  position: absolute;
  right: 20px;
  bottom: 5px;
  font-size: 40px;
  color: #fbc02d;
  float: right;
  cursor: pointer;
  z-index: 20
}
#previousButton {
  position: absolute;
  font-size: 18px;
  left: 30px; /* same as padding on container */
  top: 12px;
  z-index: 20;
  color: #9e9e9e;
  float: right;
  cursor: pointer;
}
#previousButton:hover {color: #c49000}
#forwardButton:hover {color: #c49000}
.wrong #forwardButton {color: #ff2d26}
.close #forwardButton, .close #previousButton {color: #fff}

#inputContainer {
  position: relative;
  padding: 30px 20px 20px 20px;
  margin: 10px 60px 10px 10px;
  opacity: 0;
  transition: opacity .3s ease-in-out;
}

#inputContainer input {
  position: relative;
  width: 100%;
  border: none;
  font-size: 20px;
  outline: 0;
  background: transparent;
  box-shadow: none;
}

#inputLabel {
  position: absolute;
  pointer-events: none;
  top: 32px; /* same as container padding + margin */
  left: 20px; /* same as container padding */
  font-size: 20px;
  transition: .2s ease-in-out;
}

#inputContainer input:valid + #inputLabel {
  top: 6px;
  left: 42px; /* space for previous arrow */
  margin-left: 0!important;
  font-size: 11px;
  font-weight: normal;
  color: #9e9e9e;
}

#inputProgress {
  border-bottom: 3px solid #fbc02d;
  width: 0;
  transition: width .6s ease-in-out;
}

.wrong #inputProgress {
  border-color: #ff2d26;
}

@media (max-width: 420px) {
  #forwardButton {right: 10px}
  #previousButton {left: 10px}
  #inputLabel {left: 0}
  #inputContainer {padding-left: 0; margin-right:20px}
}
<link rel="stylesheet" href="https://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css">
<div id="progress"></div>
<div class="center">
  <div id="register"> 
    
    <i id="previousButton" class="ion-android-arrow-back"></i> 
    <i id="forwardButton" class="ion-android-arrow-forward"></i>
    
    <div id="inputContainer">
      <div id="inputWrapper">
        <input id="inputField" required multiple />
        <label id="inputLabel"></label>
      </div>
      <div id="inputProgress"></div>
    </div>
    
  </div>
</div>

Answer №3

It seems that you haven't specified the type in the questions array. This is causing every input field to default to text because the type couldn't be found.

inputField.type = questions[position].type || 'text'

var questions = [
  {question: "What's your full name?"}, // TEXT INPUT FIELD
  {question: "What's your gender?", type:"radio"}, // RADIO BUTTONS {male, female, other}
  {question: "What's your date of birth?"}, // TEXT INPUT FIELD WITH id="datepicker"
  {question: "What's your country?"}, // SELECT BOX WITH LIST OF COUNTRIES IN IT
  {question: "Interest in?"} // CHECKBOXES {male, female, other}
]

//do something after the questions have been answered
var onComplete = function() {
  var h1 = document.createElement('h1')
  h1.appendChild(document.createTextNode('Thanks ' + questions[0].answer + ' for checking this pen out!'))
  setTimeout(function() {
    register.parentElement.appendChild(h1)
    setTimeout(function() { h1.style.opacity = 1 }, 50)
  }, 1000)
}

;(function(questions, onComplete) {
  var tTime = 100 // transition transform time from #register in ms
  var wTime = 200 // transition width time from #register in ms
  var eTime = 1000 // transition width time from inputLabel in ms

  // init
  if (questions.length == 0) return
  var position = 0
  putQuestion()

  forwardButton.addEventListener('click', validate)
  inputField.addEventListener('keyup', function(e) {
    transform(0, 0) // ie hack to redraw
    if (e.keyCode == 13) validate()
  })

  previousButton.addEventListener('click', function(e) {
    if (position === 0) return
    position -= 1
    hideCurrent(putQuestion)
  })

  // load the next question
  function putQuestion() {
    inputLabel.innerHTML = questions[position].question
    inputField.type = questions[position].type || 'text'
    inputField.value = questions[position].answer || ''
    inputField.focus()

    // set the progress of the background
    progress.style.width = position * 100 / questions.length + '%'
    previousButton.className = position ? 'ion-android-arrow-back' : 'ion-person'
    showCurrent()
  }

  // when submitting the current question
  function validate() {
    var validateCore = function() {      
      return inputField.value.match(questions[position].pattern || /.+/)
    }

    if (!questions[position].validate) questions[position].validate = validateCore
    // check if the pattern matches
    if (!questions[position].validate()) 
      wrong(inputField.focus.bind(inputField))
    else ok(function() {
      // execute the custom end function or the default value set
      if (questions[position].done) questions[position].done()
      else questions[position].answer = inputField.value
        ++position
        // if there is a new question, hide current and load next
        if (questions[position]) hideCurrent(putQuestion)
      else hideCurrent(function() {
        // remove the box if there is no next question
        register.className = 'close'
        progress.style.width = '100%'
        onComplete()
      })
    })
  }
  
  // helper
  function hideCurrent(callback) {
    inputContainer.style.opacity = 0
    inputLabel.style.marginLeft = 0
    inputProgress.style.width = 0
    inputProgress.style.transition = 'none'
    inputContainer.style.border = null
    setTimeout(callback, wTime)
  }

  function showCurrent(callback) {
    inputContainer.style.opacity = 1
    inputProgress.style.transition = ''
    inputProgress.style.width = '100%'
    setTimeout(callback, wTime)
  }

  function transform(x, y) {
    register.style.transform = 'translate(' + x + 'px ,  ' + y + 'px)'
  }

  function ok(callback) {
    register.className = ''
    setTimeout(transform, tTime * 0, 0, 10)
    setTimeout(transform, tTime * 1, 0, 0)
    setTimeout(callback, tTime * 2)
  }
  function wrong(callback) {
    register.className = 'wrong'
    for (var i = 0; i < 6; i++) // shaking motion
      setTimeout(transform, tTime * i, (i % 2 * 2 - 1) * 20, 0)
    setTimeout(transform, tTime * 6, 0, 0)
    setTimeout(callback, tTime * 7)
  }
}(questions, onComplete))
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@100&display=swap');
body {
  margin: 0;
  background: #fbc02d;
  font-family: 'Roboto', sans-serif;
  overflow-x: hidden;
}

h1 {
  position: relative;
  color: #fff;
  opacity: 0;
  transition: .8s ease-in-out;
}

#progress {
  position: absolute;
  background: #c49000;
  height: 100vh;
  width: 0;
  transition: width 0.2s ease-in-out;
}

.center {
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
}


#register {
  background: #fff;
  position: relative;
  width: 550px;
  box-shadow: 0 16px 24px 2px rgba(0,0,0,0.14), 0 6px 30px 5px rgba(0,0,0,0.12), 0 8px 10px -5px rgba(0,0,0,0.3);
  transition: transform .1s ease-in-out;
}

#register.close {
  width: 0;
  padding: 0;
  overflow: hidden;
  transition: .8s ease-in-out;
  box-shadow: 0 16px 24px 2px rgba(0,0,0,0);
}

#forwardButton {
  position: absolute;
  right: 20px;
  bottom: 5px;
  font-size: 40px;
  color: #fbc02d;
  float: right;
  cursor: pointer;
  z-index: 20
}
#previousButton {
  position: absolute;
  font-size: 18px;
  left: 30px; /* same as padding on container */
  top: 12px;
  z-index: 20;
  color: #9e9e9e;
  float: right;
  cursor: pointer;
}
#previousButton:hover {color: #c49000}
#forwardButton:hover {color: #c49000}
.wrong #forwardButton {color: #ff2d26}
.close #forwardButton, .close #previousButton {color: #fff}

#inputContainer {
  position: relative;
  padding: 30px 20px 20px 20px;
  margin: 10px 60px 10px 10px;
  opacity: 0;
  transition: opacity .3s ease-in-out;
}

#inputContainer input {
  position: relative;
  width: 100%;
  border: none;
  font-size: 20px;
  outline: 0;
  background: transparent;
  box-shadow: none;
}

#inputLabel {
  position: absolute;
  pointer-events: none;
  top: 32px; /* same as container padding + margin */
  left: 20px; /* same as container padding */
  font-size: 20px;
  transition: .2s ease-in-out;
}

#inputContainer input:valid + #inputLabel {
  top: 6px;
  left: 42px; /* space for previous arrow */
  margin-left: 0!important;
  font-size: 11px;
  font-weight: normal;
  color: #9e9e9e;
}

#inputProgress {
  border-bottom: 3px solid #fbc02d;
  width: 0;
  transition: width .6s ease-in-out;
}

.wrong #inputProgress {
  border-color: #ff2d26;
}

@media (max-width: 420px) {
  #forwardButton {right: 10px}
  #previousButton {left: 10px}
  #inputLabel {left: 0}
  #inputContainer {padding-left: 0; margin-right:20px}
}
<link rel="stylesheet" href="https://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css">
<div id="progress"></div>
<div class="center">
  <div id="register"> 
    
    <i id="previousButton" class="ion-android-arrow-back"></i> 
    <i id="forwardButton" class="ion-android-arrow-forward"></i>
    
    <div id="inputContainer">
      <input id="inputField" required multiple />
      <label id="inputLabel"></label>
      <div id="inputProgress"></div>
    </div>
    
  </div>
</div>

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Troubleshooting Issue with Importing File into CSS Document

I'm currently working on building a website and I've been trying to import an image to use as the header. I want to add this .png picture to my CSS file, but despite extensive research and double checking everything, I still can't figure out ...

How to Invoke JavaScript Functions within jQuery Functions?

I'm working on a jQuery function that involves dynamically loading images and checking their width. I have two JavaScript functions, func1() and func2(), that I want to call from within the jQuery function in order to check the width of each image. T ...

Enhance your HTML5 video by incorporating strategically placed buttons (images) as an overlay

Recently, I embedded a video: <video preload muted autoplay loop id="sty"> <source src="content/1/sty.mp4" type="video/mp4"> </video> To make this video full-width, I made sure to set the CSS properti ...

Utilizing Ajax-jQuery in Yii framework for Dynamic Row Generation

Struggling to come up with a solution on my own, I am reaching out for ideas from others. I have successfully implemented cJUIcomplete, but now I am focusing on Inventory Management. For the sale form, I envision structuring it like this: Product Name - ...

Select elements from a PHP loop

As part of my school project, I am developing a basic webshop. Currently, I am using a while loop to display featured products on the homepage. However, I now need to implement a shopping cart functionality. After a user clicks the "add to cart" button, th ...

Tips for showing Japanese characters received through an ajax request on a jsp webpage

I'm facing an issue with my .jsp page where it makes an ajax request to another .jsp page. The query parameter contains Japanese characters and I've confirmed that they are passed correctly in the request. However, the string is not being receive ...

What method does the framework use to determine the specific API being accessed?

How can the framework determine which API is being accessed? app.get('/user/:userId/name/export', function (req, res) { var userId = req.params.userId; } app.get('/user/:userId/name/:name', function (req, res) { var userId = req ...

Tips for pausing a jQuery ajax request until the page finishes loading

Currently, I am facing an issue with jQuery Ajax where the callback function reads XML and creates dynamic HTML tags using the data from the XML file. Everything seems to be working correctly except for the jQuery Sliderkit plugin, which isn't functio ...

Connecting the input[date] and Moment.js in AngularJS

For the purpose of formulating a question, I have prepared a simplified example: ... <input type="date" ng-model="selectedMoment" /> ... <script> angular.module('dateInputExample', []) .controller('DateController', [& ...

Implementing a Vue feature where an object is utilized as the v-model within a select

I am currently working with a select tag that displays an array of objects as its options. Each option only shows the name property of the object. The v-model on the select tag is initially set to one of the object's name properties. My aim is to use ...

The persistent Bulma dropdown glitch that refuses to close

In the project I'm working on, I have implemented a Bulma dropdown. While the dropdown functions correctly, I am facing an issue when adding multiple dropdowns in different columns with backend integration. When one dropdown is open and another is cli ...

Leveraging the :checked state in CSS to trigger click actions

Is there a way to revert back to the unchecked or normal state when clicking elsewhere in the window, after using the :checked state to define the action for the clicked event? ...

How can I display and utilize the selected value from a Rails select form before submitting it?

Currently, I am in the process of developing a multi-step form for placing orders. This form includes two dropdown selectors - one for shipping countries and another for shipping services. Once a country is selected, all available shipping services for tha ...

Autoprefixer encountered a TypeError while executing the npm script: Patterns must be specified as a string or an array of strings

Upon executing npm run prefix:css, the following error message is displayed: Error: Patterns must be a string or an array of strings { "name": "natours", "version": "1.0.0", "description": "A project for natours", "main": "index.js", "script ...

Vue JS - Issue with data reactivity not being maintained

Currently, I have implemented a pagination indicator that displays the number of results on each page. For instance, page 1 shows '1-5' and page 2 shows '6-10 out of 50 results', and so on. The logic for updating the results seems to b ...

Is using JQuery recommended for implementing onclick and onchange events?

Hello there, I'm completely new to the world of jQuery and currently facing a bit of confusion. Here's my dilemma: should I be utilizing jQuery with the onclick event or the onchange event for an element using something like this: $(selector).som ...

Guide to efficiently utilizing flex to horizontally position two elements next to each other in a Material UI and ReactJS environment

I have successfully achieved side-by-side components of two cards from Material-UI. However, I am facing an issue when expanding one of the cards. The problem is that Card 1 automatically expands to match the size of Card 2 when Card 2 is expanded, even th ...

Helping React and MUI components become mobile responsive - Seeking guidance to make it happen

My React component uses Material-UI (MUI) and I'm working on making it mobile responsive. Here's how it looks currently: But this is the look I want to achieve: Below is the code snippet for the component: import React from 'react'; i ...

Struggle with implementing enums correctly in ngSwitch

Within my application, I have implemented three buttons that each display a different list. To control which list is presented using Angular's ngSwitch, I decided to incorporate enums. However, I encountered an error in the process. The TypeScript co ...

I encountered an issue while attempting to retrieve data using route parameters. It seems that I am unable to access the 'imagepath' property as it is undefined

Is there a way to access data when the page loads, even if it is initially undefined? I keep getting this error message: "ERROR TypeError: Cannot read properties of undefined (reading 'imagepath')". How can I resolve this issue? import { Compone ...