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);
}