Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save version-control/59e616d0d8150e34bdacd32ebc49b128 to your computer and use it in GitHub Desktop.
Save version-control/59e616d0d8150e34bdacd32ebc49b128 to your computer and use it in GitHub Desktop.
Handling conditional form fields based on user input

Handling conditional form fields based on user input

Create conditional form fields using jQuery.

A Pen by Truong Sa on CodePen.

License.

<div id="branching-form">
<h1>Weee branching form</h1>
<!-- Level 0 form field: person_age -->
<div class="field">
<label>How old are you?</label>
<input type="text" name="person_age" placeholder="Age" />
<!-- Level 1 form fields: children of person_age -->
<div class="field hidden--no-hide" data-parent-branch="person_age" data-show-on-value="3..7">
<p class="info"><span class="level">Level 1</span> Person's age is <b>between 3 & 7</b></p>
<label>What's your favorite candy?</b></label>
<input type="text" name="favorite_candy" placeholder="Favorite candy" />
<!-- Level 2 form fields: children of favorite_candy -->
<div class="field hidden---" data-parent-branch="favorite_candy" data-show-on-value=" StarBurst,skittles">
<p class="info"><span class="level">Level 2</span> Person's favorite candy is <b>starburst or skittles</b></p>
<label>What flavor?</b></label>
<input type="text" name="what_flavor_starburst" placeholder="What flavor?" />
</div>
<div class="field hidden" data-parent-branch="favorite_candy" data-show-on-value="chocolate">
<p class="info"><span class="level">Level 2</span> Person's favorite candy is <b>chocolate</b></p>
<label>Which chocolate bar?</b></label>
<input type="text" name="which_chocolate_bar" placeholder="Which chocolate bar?" />
</div>
</div>
<!-- Level 1 form fields: children of person_age -->
<div class="field hidden" data-parent-branch="person_age" data-show-on-value="!5,!7">
<p class="info"><span class="level">Level 1</span> Person's age <b>does not equal 5 or 7</b></p>
<label>Did you start kindergarden this year?</label>
<input type="hidden" name="started_kindergarden_this_year" />
<span class="custom-radio"> YES</span>
<span class="custom-radio"> NO</span>
<!-- Level 2 form fields: children of started_kindergarden_this_year -->
<div class="field hidden" data-parent-branch="started_kindergarden_this_year" data-show-on-value="yes">
<p class="info"><span class="level">Level 2</span> Person <b>did</b> start kindergarden this year</p>
<label>What kindergarden do you go to?</b></label>
<input type="text" name="what_kindergarden" />
</div>
</div>
<!-- Level 1 form fields: children of person_age -->
<div class="field hidden" data-parent-branch="person_age" data-show-on-value="gte-21">
<p class="info"><span class="level">Level 1</span>Person's age is <b>at least 21</b></p>
<label>Did you graduate college?</label>
<input type="radio" name="graduated_college" value="1">Yes &nbsp;
<input type="radio" name="graduated_college" value="2">No &nbsp;
<input type="radio" name="graduated_college" value="3">Maybe
<!-- Level 2 form fields: children of started_kindergarden_this_year -->
<div class="field hidden" data-parent-branch="graduated_college" data-show-on-value="1">
<p class="info"><span class="level">Level 2</span> Person <b>did</b> graduate college</p>
<label>What college did you graduate from?</b></label>
<input type="text" name="graduated_from" />
</div>
<div class="field hidden" data-parent-branch="graduated_college" data-show-on-value="2,3">
<p class="info"><span class="level">Level 2</span> Person <b>did not</b> graduate college</p>
<label>What college would you like to attend?</b></label>
<input type="text" name="wants_to_graduate_from" />
</div>
</div>
</div>
<!-- Level 0 form field: person_income -->
<div class="field">
<label>What is your annual income?</label>
<select name="person_income">
<option value="under30">$0 - $30,000</option>
<option value="between30and60">$30,000 - $60,000</option>
<option value="between60and100">$60,000 - $100,000</option>
<option value="morethan100">More than $100,000</option>
</select>
<!-- Level 1 form fields: children of person_income -->
<div class="field hidden" data-parent-branch="person_income" data-show-on-value="morethan100">
<p class="info"><span class="level">Level 1</span> Person earns <b>more than $100,000</b></p>
<label>How does it feel to be rich?</label>
<input type="text" name="being_rich" />
</div>
</div>
</div>
<div id="fake-submit-button">Submit</div>
<div id="fake-post-data"></div>
<p>This demo using form forker plugin <a target="blank" href="https://github.com/brittanystoroz/form-forker">https://github.com/brittanystoroz/form-forker</a></p>
/*
* Form Forker - jQuery / Zepto forkable form plugin
*
* Copyright (c) 2014 Brittany Storoz
*
* Licensed under the MIT license:
* http://www.opensource.org/licenses/mit-license.php
*
* Project home:
* http://www.brittanystoroz.github.io/form-forking
*
* Version: 0.0.0
*
*/
;
(function($, window, document, undefined) {
"use strict";
$.fn.forkable = function(opts) {
var BranchingForm = function(form, options) {
var opts = options || {};
this.$domForm = form;
this.opts = opts;
this.init();
};
BranchingForm.prototype = {
testChangeEvent: function(e) {
return(e);
},
getForks: function() {
var self = this,
forkNames = [],
forkInputs = [],
childrenFormFields = self.$domForm.find('div[data-parent-branch]'); // TO DO: don't limit this to div elems
$.each(childrenFormFields, function(index, childFormField) {
var forkName = $(childFormField).data("parent-branch");
if (forkNames.indexOf(forkName) === -1) {
forkNames.push(forkName);
var fork = self.$domForm.find('input[name="' + forkName + '"], select[name="' + forkName + '"]');
forkInputs.push(fork);
}
});
self.forks = forkInputs;
},
init: function() {
var self = this;
self.getForks();
self.$domForm.addClass('forkable');
/* If custom evaluation methods have been defined, make them available in the branching form */
if (self.opts.branchingMethods) {
$.each(self.opts.branchingMethods, function(key, val) {
if (typeof self[key] !== "undefined") {
console.warn("Note: You are overriding one of Form Forker's default branching methods with \"" + key + "\". If this was not your intention, try renaming your custom branching method.");
}
self[key] = val;
});
}
/* Set up event handlers to monitor the values of our forks */
$.each(self.forks, function(index, fork) {
var branchMethod = $(fork).data('branching-fn'); // determine the type of evaluation we need to do on the input value
if ($(fork).prop('type') === 'text') {
$(fork).keyup(function() {
$(fork).trigger('change');
});
}
$(fork).change(function(e) {
self.testChangeEvent(e.currentTarget);
var childBranches = self.$domForm.find('div[data-parent-branch=' + $(fork).prop('name') + ']'),
userEnteredValue = $(fork).val();
if (!branchMethod && isNaN(parseInt(userEnteredValue))) {
self.stringEval(childBranches, userEnteredValue);
} else if (!branchMethod) {
self.integerEval(childBranches, parseInt(userEnteredValue));
} else {
self[branchMethod](childBranches, userEnteredValue);
}
});
});
},
/* Gather any values from inputs that are currently displayed to submit */
postData: function() {
var unhiddenFields = this.domForm.find('div:not(".hidden")'),
inputFieldsToSend = unhiddenFields.find('> input, > textarea, > select'),
postData = {};
$.each(inputFieldsToSend, function(index, input) {
var postVal;
($(input).prop('type') === 'radio') ? postVal = $('input:checked').val() : postVal = $(input).val(); // if input is a radio button, only send the value of the checked radio
if (postVal) { // if the input value isn't blank, add a key to the postData object
postData[$(input).prop('name')] = postVal;
}
});
return postData;
},
util: {
/* Fake radio buttons so we can give them custom styling; will probaby need to support custom select menus, checkboxes, etc. in the future */
handleCustomRadios: function(e) {
var $elem = $(e.currentTarget), // radios span that was clicked on
$associatedInput = $elem.parent().find('> input'), // the input whose value needs to be toggled based on our span selection
selectedRadioVal = $.trim($elem.html()); // the innerHTML of our selected span
$associatedInput.val(selectedRadioVal); // set the hidden input value equal to the selected span
$associatedInput.trigger('change'); // manually trigger the change even on our input so we can evaluate it's value
$elem.addClass('radio-selected').siblings().removeClass('radio-selected');
},
/* Remove leading/trailing whitespace & convert to lowercase to ensure no false negatives when comparing strings */
cleanString: function(dirtyString) {
return dirtyString.toLowerCase().replace(/^\s+|\s+$/g, '');
},
/*
* Map string-based operators to their arithmetic equivalent and return the result of the evaluated expression
* @param {string} comparisonOperator The string-based comparison to convert to an arithmetic operator // possible values: gte, gt, lte, lt, eq, noteq
* @param {integer} userEnteredValue The current integer value a user has entered
* @param {integer} integerToCompareAgainst The integer we will compare the userEnteredValue to as specified by the child branch's data-show-on-value attribute
*/
convertToArithmeticComparison: function(comparisonOperator, userEnteredValue, integerToCompareAgainst) {
switch (comparisonOperator) {
case 'gte':
return (userEnteredValue >= integerToCompareAgainst);
case 'gt':
return (userEnteredValue > integerToCompareAgainst);
case 'lte':
return (userEnteredValue <= integerToCompareAgainst);
case 'lt':
return (userEnteredValue < integerToCompareAgainst);
case 'eq':
return (userEnteredValue === integerToCompareAgainst);
case 'noteq':
return (userEnteredValue !== integerToCompareAgainst);
}
},
/*
* Parse the data-show-on-value attribute of child branches for integer evaluations.
* @param {integer} userEnteredValue The current integer value a user has entered into an input with dependent child branches
* @param {string} conditionsToBeMet The conditions to be met for showing a child branch, as specified by the child branch's
data-show-on-value attribute // possible values: gte-{int}, gt-{int}, lte-{int}, lt-{int}, eq-{int}, noteq-{int}
* @param {string} logicalOperator The logical operator to use when evaluating multiple conditions // possible values: _and_, _or_
*/
parseNumericConditions: function(userEnteredValue, conditionsToBeMet, logicalOperator) {
var self = this,
userInput = parseInt(userEnteredValue),
valuePassesConditions;
/* Loop through the conditions that must be met to show a particular child branch */
$.each(conditionsToBeMet, function(index, condition) {
var conditionToEvaluate = condition.split('-'),
comparisonOperator = conditionToEvaluate[0], // gte, gt, lte, lt, eq, noteq
integerToCompareAgainst = parseInt(conditionToEvaluate[1]), // integer we will compare the userEnteredValue to
conditionResult = self.convertToArithmeticComparison(comparisonOperator, userInput, integerToCompareAgainst);
/* If there are multiple conditions but the logical operator is an ||, we can return true after the first condition that passes. */
/* If there is no logical operator (only 1 condition is specified), or it is an &&, we must loop through all conditions and only return true if all pass. */
if (logicalOperator && logicalOperator === '_or_' && conditionResult === true) {
valuePassesConditions = true;
return false;
} else if (logicalOperator && logicalOperator === '_and_' && conditionResult === false) {
valuePassesConditions = false;
return false;
} else {
valuePassesConditions = conditionResult;
}
});
return valuePassesConditions;
},
},
/*
* Method for comparing integer values with arithmetic operators. This code is method is CRAZY.
* @param {object} opts Options containing references to the child branches and the user-entered value to be evaluated
*/
integerEval: function(childBranches, userEnteredValue) {
var self = this,
userInput = parseInt(userEnteredValue), // convert input value from string to integer
logicalOperator,
numericOperations,
conditionsForShowingChildBranch;
/* for each of the possible child branches, find the condition that must be met in order for them to be shown */
$.each(childBranches, function(index, childBranch) {
var $childBranch = $(childBranch),
showOnValueAttribute = $(childBranch).data('show-on-value'),
conditionType = typeof showOnValueAttribute,
childBranchShouldBeShown;
if (conditionType === 'string') {
if (showOnValueAttribute.indexOf(',') !== -1) {
conditionType = 'integerArray';
} else if (showOnValueAttribute.indexOf('..') !== -1) {
conditionType = 'inclusiveRange';
} else if (showOnValueAttribute.match(/\_and\_|\_or\_/g) !== null) {
conditionType = 'logicalExpression';
}
}
switch (conditionType) {
case 'number': // data-show-on-value="1"
numericOperations = ['eq-' + showOnValueAttribute];
break;
case 'string': // data-show-on-value="gte-8"
if (showOnValueAttribute.charAt(0) === "!") {
numericOperations = ["noteq-" + showOnValueAttribute.slice(1)]
}
else {
numericOperations = [showOnValueAttribute];
}
break;
case 'inclusiveRange': // data-show-on-value="3..7"
conditionsForShowingChildBranch = showOnValueAttribute.split('..');
logicalOperator = "_and_"; // _and_, _or_
numericOperations = ["gte-" + conditionsForShowingChildBranch[0], "lte-" + conditionsForShowingChildBranch[1]]; // gte-{int}, lte-{int}
break;
case 'integerArray': // data-show-on-value="1,2,3,4,5"
conditionsForShowingChildBranch = showOnValueAttribute.split(',');
numericOperations = [];
$.each(conditionsForShowingChildBranch, function(index, condition) {
if (condition.charAt(0) === "!") {
numericOperations.push('noteq-' + condition.slice(1));
logicalOperator = '_and_';
}
else {
numericOperations.push('eq-' + condition);
logicalOperator = '_or_';
}
});
break;
case 'logicalExpression': // data-show-on-value="gte-3_and_lte-7"
conditionsForShowingChildBranch = showOnValueAttribute.split(/(\_and\_|\_or\_)/g);
logicalOperator = conditionsForShowingChildBranch[1]; // _and_, _or_
numericOperations = [conditionsForShowingChildBranch[0], conditionsForShowingChildBranch[2]]; // gte-{int}, gt-{int}, lte-{int}, lt-{int}, eq-{int}, noteq-{int}
}
childBranchShouldBeShown = self.util.parseNumericConditions(userInput, numericOperations, logicalOperator);
(childBranchShouldBeShown) ? $childBranch.removeClass('hidden').find('> div.field').removeClass('hidden') : $childBranch.addClass('hidden');
$childBranch.find('div.field').addClass('hidden');
});
},
/*
* Compare string values. Will also work for boolean values by comparing their string equivalents, though this could get buggy.
* @param {object} opts Options containing references to the child branches and the user-entered value to be evaluated
*/
stringEval: function(childBranches, userEnteredValue) {
var self = this,
userInput = self.util.cleanString(userEnteredValue),
childBranchShouldBeShown;
$.each(childBranches, function(index, childBranch) {
var $childBranch = $(childBranch),
conditionsForShowingChildBranch = $childBranch.data('show-on-value').toString().split(','); // grab the conditions that must be true in order to show this child branch
conditionsForShowingChildBranch = $.map(conditionsForShowingChildBranch, function(condition) {
return self.util.cleanString(condition);
});
childBranchShouldBeShown = (conditionsForShowingChildBranch.indexOf(userInput) !== -1);
(childBranchShouldBeShown) ? $childBranch.removeClass('hidden').find('> div.field').removeClass('hidden') : $childBranch.addClass('hidden');
$childBranch.find('div.field').addClass('hidden');
});
}
}; // end BranchingForm.prototype
/* Initialize the new forkable form */
var userBranchingForm = new BranchingForm($(this), opts);
return userBranchingForm;
}; // end $.fn.forkable
})(window.jQuery || window.Zepto, window, document);
///TESTING
(function () {
$('#branching-form').forkable({
branchingMethods: {
"customEval": function(childBranches, userEnteredValue) {
console.log("CUSTOM This: ", this);
console.log("CB: ", childBranches);
console.log("User Val: ", userEnteredValue)
}
}
});
$('#branching-form').find('input, select, textarea').eq(0).trigger( 'change');
// /* Attach click event handler for custom radios */
// $('#branching-form').find('div span.custom-radio').click(function(e) {
// userFeedbackForm.util.handleCustomRadios(e);
// });
// /* Stub submit button to display postData on form submit */
// $('#fake-submit-button').click(function(e) {
// var stringifiedPostData = userFeedbackForm.postData();
// stringifiedPostData = JSON.stringify(stringifiedPostData);
// var radioVal = $(userFeedbackForm).find('input[type=radio').val();
// $('#fake-post-data').html('<h2>Post Data on Submit:</h2>' + stringifiedPostData);
// });
})();
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
html {
background: #e1e1e1;
}
body {
font: 200 16px/18px'helvetica', arial, sans-serif;
max-width: 600px;
margin: 50px auto;
color: #000;
padding: 50px 50px;
border: 1px solid #f2f2f2;
background: #f5f5f5;
border-radius: 5px;
}
h1 {
font: 100 26px/30px'helvetica', arial, sans-serif;
font-weight: 100;
margin: 0 0 20px 0;
color: #999;
padding: 0px;
}
#branching-form div.field p.info {
font-size: 11px;
background: rgba(0, 0, 0, 0.3);
padding: 2px 10px;
color: #333;
text-align: right;
margin: 0 0 5px 0;
}
#branching-form div.field > div.field > p.info {
background: rgba(0, 0, 0, 0.2);
}
#branching-form div.field > div.field > div.field > p.info {
background: rgba(0, 0, 0, 0.1);
}
#branching-form div.field p.info span.level {
float: left;
}
#test {
display: inline;
}
#branching-form div.field {
padding: 15px 0px 15px 15px;
height: auto;
transition: padding 0.2s ease 0.0s;
}
#branching-form div.field label {
display: block;
padding: 5px 0px;
}
#branching-form div.field input {
width: 99%;
height: 18px;
}
#branching-form div.field input[type="radio"] {
display: inline-block;
margin: 10px 5px;
width: auto;
vertical-align: middle;
}
#branching-form div.field.hidden {
overflow: hidden;
height: 0px;
padding: 0px 0px 0px 15px;
transition: padding 0.2s ease 0.0s;
}
#branching-form div.field span.custom-radio {
border: 1px solid #bfbfbf;
border-radius: 5px;
display: inline-block;
margin: 5px 0 10px 0px;
font: bold 10px sans-serif;
color: #717171;
background-color: #ffffff;
padding: 4px 9px;
cursor: pointer;
}
#branching-form div.field span.custom-radio.radio-selected {
background-color: #cbcbcb;
}
#fake-submit-button {
background-color: #6288a5;
border: 1px solid rgb(83, 118, 145);
border-radius: 3px;
font: 600 13px/20px'helvetica', arial, sans-serif;
text-transform: uppercase;
padding: 5px 15px;
max-width: 100px;
text-align: center;
margin: 0 auto;
color: #fff;
}
#fake-submit-button: hover {
background-color: #326891;
border-color: #325c8a;
cursor: pointer;
}
#fake-post-data {
margin: 40px auto;
max-width: 600px;
font: 400 12px/16px'courier', serif;
word-wrap: break-word;
}
#fake-post-data h2 {
font: 600 10px/16px'helvetica', arial, sans-serif;
color: #666;
text-transform: uppercase;
background-color: #eee;
padding: 5px 10px 3px 10px;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment