Data Validation, JavaScript & Looping
Forsta HX Platform - API Scripting Guide
Data Validation, JavaScript & Looping
Overview
This section covers comprehensive data validation patterns, JavaScript array/object manipulation, and efficient looping techniques for survey scripting.
Array Iteration Methods:
// forEach - Execute function for each element
var symptoms = ['headache', 'fever', 'cough'];
symptoms.forEach(function(symptom, index) {
console.log(index + ': ' + symptom);
});
// map - Transform array elements
var questionIds = ['AQ1', 'AQ2', 'AQ3'];
var values = questionIds.map(function(qid) {
return f(qid).val();
});
// filter - Select elements matching condition
var answered = questionIds.filter(function(qid) {
return f(qid).val() !== '';
});
// some - Check if any element matches
var hasError = questionIds.some(function(qid) {
return f(qid).val() === 'invalid';
});
// every - Check if all elements match
var allAnswered = questionIds.every(function(qid) {
return f(qid).val() !== '';
});
// reduce - Accumulate values
var total = [1, 2, 3, 4, 5].reduce(function(sum, val) {
return sum + val;
}, 0);
Object Iteration:
// Object.keys() - Get all keys
var responses = {AQ1: '5', AQ2: '3', AQ3: '4'};
Object.keys(responses).forEach(function(qid) {
console.log(qid + ' = ' + responses[qid]);
});
// Object.values() - Get all values
var scores = Object.values(responses).map(function(val) {
return parseInt(val);
});
// Object.entries() - Get key-value pairs
Object.entries(responses).forEach(function(entry) {
var qid = entry[0];
var value = entry[1];
console.log(qid + ': ' + value);
});
// for...in loop for objects
for(var qid in responses) {
if(responses.hasOwnProperty(qid)) {
f(qid).val(responses[qid]);
}
}
Advanced Validation Patterns:
/**
* validationRules - Configuration object defining validation rules per field
* @type {Object.}
* @description Each key is a field name, value is an object with:
* - required {boolean}: Whether field must have a value
* - type {string}: Expected data type ('number', 'string', etc.)
* - min/max {number}: Numeric range constraints
* - pattern {RegExp}: Regex pattern the value must match
* - message {string}: Error message to display on failure
*/
var validationRules = {
// Age field: required number between 18-100
age: {
required: true,
type: 'number',
min: 18,
max: 100,
message: 'Age must be between 18-100'
},
// Email field: required, must match email pattern
email: {
required: true,
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, // Standard email regex
message: 'Invalid email format'
},
// Phone field: optional, but if provided must be 10 digits
phone: {
required: false,
pattern: /^\d{10}$/, // Exactly 10 digits
message: 'Phone must be 10 digits'
}
};
/**
* validateField - Validates a single field against its rules
* @param {string} qid - The question ID to validate
* @param {Object} rules - Validation rules object for this field
* @returns {Object} - {valid: boolean, message: string} result object
* @description Runs through validation checks in order:
* 1. Required check - field must have value if required
* 2. Type check - value must be correct data type
* 3. Range check - numeric value within min/max bounds
* 4. Pattern check - value matches regex pattern
*/
function validateField(qid, rules) {
var value = f(qid).val(); // Get current field value
// Required check
if(rules.required && !value) {
return {valid: false, message: 'This field is required'};
}
// Type check
if(rules.type === 'number' && isNaN(Number(value))) {
return {valid: false, message: 'Must be a number'};
}
// Range check
var numVal = Number(value);
if(rules.min && numVal < rules.min) {
return {valid: false, message: rules.message};
}
if(rules.max && numVal > rules.max) {
return {valid: false, message: rules.message};
}
// Pattern check
if(rules.pattern && !rules.pattern.test(value)) {
return {valid: false, message: rules.message};
}
return {valid: true};
}
/**
* validateAllFields - Runs validation on all configured fields
* @returns {Array} - Array of error objects: [{field, message}, ...]
* @description Iterates through all fields in validationRules,
* validates each one, and collects any errors.
* Returns empty array if all fields are valid.
*/
function validateAllFields() {
var errors = []; // Collect validation errors
Object.keys(validationRules).forEach(function(field) {
var result = validateField(field, validationRules[field]);
if(!result.valid) {
errors.push({field: field, message: result.message});
}
});
return errors;
}
Complex Data Validation:
/**
* validateDateRange - Validates that end date is after start date
* @returns {Object} - {valid: boolean, message: string, fields: Array}
* @description Cross-field validation that compares two date inputs.
* Returns which fields are involved in the validation for
* targeted error highlighting.
*/
function validateDateRange() {
// Parse date strings into Date objects for comparison
var startDate = new Date(f('startDate').val());
var endDate = new Date(f('endDate').val());
if(endDate <= startDate) {
return {
valid: false,
message: 'End date must be after start date',
fields: ['startDate', 'endDate']
};
}
return {valid: true};
}
/**
* validateConditionally - Validates fields that are conditionally required
* @returns {Object} - {valid: boolean, message: string, fields: Array}
* @description When a trigger condition is met (hasSymptoms = Yes),
* additional fields become required. Checks if those
* conditional fields have been completed.
*/
function validateConditionally() {
// Check the trigger condition - is hasSymptoms set to 'Yes' (value '1')?
var hasSymptoms = f('hasSymptoms').val() === '1';
if(hasSymptoms) {
// These fields become required
var required = ['symptomType', 'symptomDuration', 'symptomSeverity'];
var missing = required.filter(function(qid) {
return !f(qid).val();
});
if(missing.length > 0) {
return {
valid: false,
message: 'Please complete all symptom questions',
fields: missing
};
}
}
return {valid: true};
}
/**
* validateMedicalHistory - Custom business rule validation for medical surveys
* @returns {Object} - {valid: boolean, message: string}
* @description Enforces complex business rules:
* If patient is over 65 AND has a medical condition,
* then medications field must be completed.
* Demonstrates domain-specific validation logic.
*/
function validateMedicalHistory() {
// Get patient age as integer
var age = parseInt(f('age').val());
// Check if patient indicated they have a condition (Yes = '1')
var hasCondition = f('hasCondition').val() === '1';
// Get medications array (default to empty array if null)
var medications = f('medications').val() || [];
// Logic: If over 65 AND has condition, medications required
if(age > 65 && hasCondition && medications.length === 0) {
return {
valid: false,
message: 'Medications required for patients over 65 with conditions'
};
}
return {valid: true};
}
Dynamic Question Looping:
// Loop through question sections
function processQuestionSections() {
var sections = ['A', 'B', 'C', 'D'];
sections.forEach(function(section, index) {
// Process all questions in section
for(var i = 1; i <= 10; i++) {
var qid = section + 'Q' + i;
if(f(qid).length > 0) {
// Apply section-specific logic
if(section === 'A') {
applyDemographicValidation(qid);
} else if(section === 'B') {
applySymptomValidation(qid);
}
}
}
});
}
// Loop with nested conditions
function applyConditionalLogic() {
var mainQuestions = ['AQ1', 'BQ1', 'CQ1'];
mainQuestions.forEach(function(mainQid) {
f(mainQid).on('change', function() {
var value = $(this).val();
// Find related follow-up questions
var followups = findFollowups(mainQid);
followups.forEach(function(followupQid) {
if(value === '1') {
f(followupQid).show();
} else {
f(followupQid).hide().val('');
}
});
});
});
}
function findFollowups(mainQid) {
// Return array of follow-up question IDs
var base = mainQid.substring(0, 2); // e.g., "AQ"
var num = parseInt(mainQid.substring(2)); // e.g., 1
// Follow-ups are mainQ + 'a', 'b', 'c'
return ['a', 'b', 'c'].map(function(suffix) {
return base + num + suffix;
}).filter(function(qid) {
return f(qid).length > 0;
});
}
Batch Operations with Validation:
/**
* batchValidateQuestions - Validates multiple questions at once
* @param {Array} questionIds - Array of question IDs to validate
* @param {Function} validator - Validation function(value, qid) returning boolean
* @returns {Object} - Summary with total, valid count, invalid array, and allValid boolean
* @description Applies a custom validator function to multiple questions.
* Adds visual feedback (error/valid classes) and returns
* a summary for further processing.
*/
function batchValidateQuestions(questionIds, validator) {
// Map each question ID to a result object
var results = questionIds.map(function(qid) {
var value = f(qid).val(); // Get current value
var isValid = validator(value, qid); // Run custom validator
return {
qid: qid,
value: value,
valid: isValid,
element: f(qid)
};
});
// Apply visual feedback
results.forEach(function(result) {
if(result.valid) {
result.element.removeClass('error').addClass('valid');
} else {
result.element.removeClass('valid').addClass('error');
}
});
// Return summary
return {
total: results.length,
valid: results.filter(function(r) { return r.valid; }).length,
invalid: results.filter(function(r) { return !r.valid; }),
allValid: results.every(function(r) { return r.valid; })
};
}
// Usage example
var scaleQuestions = ['BQ1', 'BQ2', 'BQ3', 'BQ4', 'BQ5'];
var summary = batchValidateQuestions(scaleQuestions, function(value) {
var num = parseInt(value);
return !isNaN(num) && num >= 1 && num <= 5;
});
if(!summary.allValid) {
alert(summary.invalid.length + ' questions need attention');
// Focus first invalid question
summary.invalid[0].element.focus();
}
Real-time Validation with Debouncing:
/**
* createDebouncedValidator - Factory function for debounced field validators
* @param {Function} validationFn - Validation function(value) returning {valid, message}
* @param {number} delay - Milliseconds to wait before validating
* @returns {Function} - Debounced validator function that takes qid parameter
* @description Creates a reusable debounced validator. Validates field
* after user stops typing for 'delay' ms. Automatically creates
* and updates visual feedback elements with success/error states.
*/
function createDebouncedValidator(validationFn, delay) {
var timer; // Stores setTimeout reference for debouncing
return function(qid) {
clearTimeout(timer);
timer = setTimeout(function() {
var result = validationFn(f(qid).val());
// Display result
var feedback = f(qid).siblings('.validation-feedback');
if(feedback.length === 0) {
feedback = $('<div class="validation-feedback"></div>');
f(qid).after(feedback);
}
if(result.valid) {
feedback.removeClass('error').addClass('success')
.html('<i class="fas fa-check"></i> Valid');
} else {
feedback.removeClass('success').addClass('error')
.html('<i class="fas fa-times"></i> ' + result.message);
}
}, delay);
};
}
// Email validator with debounce
var validateEmailDebounced = createDebouncedValidator(function(value) {
if(!value) return {valid: false, message: 'Email required'};
if(!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
return {valid: false, message: 'Invalid email format'};
}
return {valid: true};
}, 500);
// Apply to email field
f('emailQ').on('input', function() {
validateEmailDebounced('emailQ');
});
Multi-level Nested Validation:
/**
* validateNestedQuestions - Validates complex nested question dependencies
* @description Handles questions where different answers trigger different
* follow-up questions. Uses a configuration object to define:
* - Which answers are valid for main questions
* - Which follow-up questions are required for each answer
* Automatically hides/clears irrelevant follow-ups.
*/
function validateNestedQuestions() {
/**
* structure - Configuration defining question dependencies
* @description For each main question:
* - values: array of valid answer codes
* - followups: object mapping answer code to required follow-up IDs
*/
var structure = {
'AQ1': {
values: ['1', '2', '3'], // Valid answers for AQ1
followups: {
'1': ['AQ1a', 'AQ1b'], // If answer is '1', require these
'2': ['AQ1c'], // If answer is '2', require this
'3': ['AQ1d', 'AQ1e', 'AQ1f'] // If answer is '3', require these
}
}
};
Object.keys(structure).forEach(function(mainQid) {
var mainValue = f(mainQid).val();
var config = structure[mainQid];
// Validate main question
if(!config.values.includes(mainValue)) {
showError(mainQid, 'Invalid selection');
return;
}
// Validate relevant follow-ups
var followupIds = config.followups[mainValue] || [];
followupIds.forEach(function(followupQid) {
if(!f(followupQid).val()) {
showError(followupQid, 'This follow-up is required');
}
});
// Hide/clear irrelevant follow-ups
Object.keys(config.followups).forEach(function(key) {
if(key !== mainValue) {
config.followups[key].forEach(function(qid) {
f(qid).hide().val('');
});
}
});
});
}
/**
* showError - Displays an error message next to a question
* @param {string} qid - The question ID to show error for
* @param {string} message - The error message text to display
* @description Adds 'error' CSS class to the question element and
* creates a new div with the error message, inserting
* it immediately after the question element.
*/
function showError(qid, message) {
// Add error styling to the question
f(qid).addClass('error');
// Create error message div and insert after question
var errorDiv = $('<div class="error-message">' + message + '</div>');
f(qid).after(errorDiv);
}