corrad-bp/docs/form-builder/JAVASCRIPT_API.md
Afiq 33dc901107 Add Form and Process History Modals with Versioning Support
- Introduced FormHistoryModal and ProcessHistoryModal components for viewing and restoring previous versions of forms and processes.
- Implemented version tracking in the backend with new API endpoints for fetching and restoring historical data.
- Added database migrations for form and process history tables to support versioning functionality.
- Enhanced form and process update logic to save previous versions before modifications.
- Updated documentation to include details on the new history system and its benefits for data management.
- Improved user experience with intuitive modals for accessing historical data and restoring previous versions.
2025-05-30 19:10:43 +08:00

20 KiB
Raw Blame History

Form Builder JavaScript API Reference

Overview

The Form Builder JavaScript API enables developers to create dynamic, interactive forms with real-time calculations, conditional logic, and automated field updates. This API is powered by the FormScriptEngine component that executes JavaScript code safely within form contexts.

Quick Start

Basic Form Script Structure

// Form initialization - runs once when form loads
onLoad: function() {
    console.log('Form is loading...');
    
    // Set default values
    setField('status', 'new');
    setField('date_created', new Date().toISOString().split('T')[0]);
}

// Field change handler - runs when any field value changes
onFieldChange: function(fieldName, value) {
    console.log('Field changed:', fieldName, '=', value);
    
    // Handle specific field changes
    if (fieldName === 'quantity' || fieldName === 'price') {
        updateTotal();
    }
}

// Helper functions (optional)
function updateTotal() {
    const quantity = getField('quantity') || 0;
    const price = getField('price') || 0;
    const total = quantity * price;
    
    setField('total', total.toFixed(2));
}

Core Functions

setField(fieldName, value)

Updates a form field value and triggers UI refresh.

Parameters:

  • fieldName (string): The name/ID of the form field to update
  • value (any): The new value to set for the field

Returns: void

Examples:

// Set text field
setField('user_name', 'John Doe');

// Set number field
setField('age', 25);

// Set date field
setField('birth_date', '1999-01-15');

// Set boolean field
setField('is_active', true);

// Set calculated field
setField('total_amount', 149.99);

// Clear field
setField('notes', '');

Notes:

  • Field names are case-sensitive and must match the form field's name attribute exactly
  • Setting a field value will trigger the onFieldChange handler for that field
  • Values are automatically converted to appropriate types based on field type

getField(fieldName)

Retrieves the current value of a form field.

Parameters:

  • fieldName (string): The name/ID of the form field to retrieve

Returns: The current field value (type depends on field type)

Examples:

// Get text field value
const userName = getField('user_name'); // Returns string

// Get number field value with default
const quantity = getField('quantity') || 0; // Returns number or 0

// Get date field value
const birthDate = getField('birth_date'); // Returns string in YYYY-MM-DD format

// Get boolean field value
const isActive = getField('is_active'); // Returns boolean

// Check if field exists and has value
const email = getField('email');
if (email) {
    console.log('Email provided:', email);
}

Notes:

  • Returns undefined if field doesn't exist
  • Returns empty string '' for empty text fields
  • Returns false for unchecked boolean fields
  • Use logical OR || to provide default values for empty fields

Event Handlers

onLoad

Executes once when the form initially loads. Use for initialization, setting default values, and performing initial calculations.

Signature:

onLoad: function() {
    // Initialization code here
}

Common Use Cases:

onLoad: function() {
    // Set default values
    setField('country', 'United States');
    setField('currency', 'USD');
    setField('created_date', new Date().toISOString().split('T')[0]);
    
    // Initialize calculated fields
    setField('subtotal', 0);
    setField('tax_amount', 0);
    setField('total', 0);
    
    // Set up initial state
    const userRole = getField('user_role');
    if (userRole === 'admin') {
        setField('permissions', 'full_access');
    }
    
    // Perform initial calculations
    calculateTotals();
}

onFieldChange

Executes whenever a user changes any field value. Receives the field name and new value as parameters.

Signature:

onFieldChange: function(fieldName, value) {
    // Handle field changes here
}

Parameters:

  • fieldName (string): Name of the field that changed
  • value (any): New value of the field

Common Use Cases:

onFieldChange: function(fieldName, value) {
    console.log('Field updated:', fieldName, '=', value);
    
    // Handle specific fields
    switch(fieldName) {
        case 'quantity':
        case 'unit_price':
            calculateLineTotal();
            break;
            
        case 'country':
            updateTaxRate(value);
            updateShippingOptions(value);
            break;
            
        case 'subscription_type':
            updateFeatures(value);
            break;
            
        case 'birth_date':
            calculateAge(value);
            break;
    }
    
    // Handle multiple related fields
    if (['first_name', 'last_name'].includes(fieldName)) {
        updateFullName();
    }
    
    // Conditional logic
    if (fieldName === 'has_insurance' && value === true) {
        setField('insurance_provider', '');
        setField('policy_number', '');
    }
}

Practical Examples

Invoice Calculator

onLoad: function() {
    // Initialize invoice
    setField('invoice_number', 'INV-' + Date.now());
    setField('invoice_date', new Date().toISOString().split('T')[0]);
    setField('tax_rate', 8.5); // 8.5% default tax
    
    // Initialize line items
    setField('quantity', 1);
    setField('unit_price', 0);
    setField('subtotal', 0);
    setField('tax_amount', 0);
    setField('total', 0);
}

onFieldChange: function(fieldName, value) {
    // Recalculate when quantity, price, or tax rate changes
    if (['quantity', 'unit_price', 'tax_rate'].includes(fieldName)) {
        const quantity = parseFloat(getField('quantity')) || 0;
        const unitPrice = parseFloat(getField('unit_price')) || 0;
        const taxRate = parseFloat(getField('tax_rate')) || 0;
        
        const subtotal = quantity * unitPrice;
        const taxAmount = (subtotal * taxRate) / 100;
        const total = subtotal + taxAmount;
        
        setField('subtotal', subtotal.toFixed(2));
        setField('tax_amount', taxAmount.toFixed(2));
        setField('total', total.toFixed(2));
    }
}

User Registration with Validation

onLoad: function() {
    setField('registration_date', new Date().toISOString().split('T')[0]);
    setField('account_status', 'pending');
}

onFieldChange: function(fieldName, value) {
    // Email validation
    if (fieldName === 'email') {
        const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
        const isValid = emailPattern.test(value);
        
        setField('email_valid', isValid);
        setField('email_status', isValid ? 'Valid' : 'Invalid email format');
    }
    
    // Password strength
    if (fieldName === 'password') {
        let strength = 'Weak';
        
        if (value.length >= 8) {
            if (/[A-Z]/.test(value) && /[a-z]/.test(value) && /\d/.test(value)) {
                strength = 'Strong';
            } else {
                strength = 'Medium';
            }
        }
        
        setField('password_strength', strength);
    }
    
    // Full name combination
    if (['first_name', 'last_name'].includes(fieldName)) {
        const firstName = getField('first_name') || '';
        const lastName = getField('last_name') || '';
        const fullName = (firstName + ' ' + lastName).trim();
        
        setField('full_name', fullName);
    }
}

E-commerce Order Form

onLoad: function() {
    setField('order_date', new Date().toISOString().split('T')[0]);
    setField('shipping_method', 'standard');
    setField('gift_wrap', false);
    
    // Initialize pricing
    updateShipping();
}

onFieldChange: function(fieldName, value) {
    // Product selection
    if (fieldName === 'product') {
        const prices = {
            'laptop': 999.99,
            'mouse': 29.99,
            'keyboard': 79.99,
            'monitor': 299.99
        };
        
        const price = prices[value] || 0;
        setField('unit_price', price);
    }
    
    // Quantity and pricing updates
    if (['quantity', 'unit_price'].includes(fieldName)) {
        const quantity = parseFloat(getField('quantity')) || 0;
        const unitPrice = parseFloat(getField('unit_price')) || 0;
        const itemTotal = quantity * unitPrice;
        
        setField('item_total', itemTotal.toFixed(2));
        updateOrderTotal();
    }
    
    // Shipping calculation
    if (['shipping_method', 'country'].includes(fieldName)) {
        updateShipping();
    }
    
    // Gift wrap fee
    if (fieldName === 'gift_wrap') {
        const giftWrapFee = value ? 9.99 : 0;
        setField('gift_wrap_fee', giftWrapFee.toFixed(2));
        updateOrderTotal();
    }
}

function updateShipping() {
    const method = getField('shipping_method');
    const country = getField('country');
    
    let shippingCost = 0;
    
    if (country === 'US') {
        switch(method) {
            case 'standard': shippingCost = 5.99; break;
            case 'express': shippingCost = 14.99; break;
            case 'overnight': shippingCost = 24.99; break;
        }
    } else {
        switch(method) {
            case 'standard': shippingCost = 19.99; break;
            case 'express': shippingCost = 39.99; break;
        }
    }
    
    setField('shipping_cost', shippingCost.toFixed(2));
    updateOrderTotal();
}

function updateOrderTotal() {
    const itemTotal = parseFloat(getField('item_total')) || 0;
    const shippingCost = parseFloat(getField('shipping_cost')) || 0;
    const giftWrapFee = parseFloat(getField('gift_wrap_fee')) || 0;
    
    const orderTotal = itemTotal + shippingCost + giftWrapFee;
    setField('order_total', orderTotal.toFixed(2));
}

Loan Calculator

onLoad: function() {
    // Set default loan parameters
    setField('loan_amount', 100000);
    setField('interest_rate', 4.5);
    setField('loan_term_years', 30);
    
    // Calculate initial payment
    calculateLoanPayment();
}

onFieldChange: function(fieldName, value) {
    if (['loan_amount', 'interest_rate', 'loan_term_years'].includes(fieldName)) {
        calculateLoanPayment();
    }
}

function calculateLoanPayment() {
    const principal = parseFloat(getField('loan_amount')) || 0;
    const annualRate = parseFloat(getField('interest_rate')) || 0;
    const years = parseFloat(getField('loan_term_years')) || 0;
    
    if (principal > 0 && annualRate > 0 && years > 0) {
        const monthlyRate = (annualRate / 100) / 12;
        const numberOfPayments = years * 12;
        
        const monthlyPayment = principal * 
            (monthlyRate * Math.pow(1 + monthlyRate, numberOfPayments)) /
            (Math.pow(1 + monthlyRate, numberOfPayments) - 1);
        
        const totalPayments = monthlyPayment * numberOfPayments;
        const totalInterest = totalPayments - principal;
        
        setField('monthly_payment', monthlyPayment.toFixed(2));
        setField('total_payments', totalPayments.toFixed(2));
        setField('total_interest', totalInterest.toFixed(2));
    } else {
        setField('monthly_payment', '0.00');
        setField('total_payments', '0.00');
        setField('total_interest', '0.00');
    }
}

Available Global Objects

Math Object

Full JavaScript Math object with all methods:

// Common math operations
const rounded = Math.round(getField('price') * 1.1);
const randomId = Math.floor(Math.random() * 1000000);
const maxValue = Math.max(getField('value1'), getField('value2'));

// Advanced math
const power = Math.pow(getField('base'), getField('exponent'));
const squareRoot = Math.sqrt(getField('area'));

Date Object

Full JavaScript Date object for date/time operations:

// Current date/time
const now = new Date();
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD format

// Date calculations
const birthDate = new Date(getField('birth_date'));
const age = Math.floor((Date.now() - birthDate.getTime()) / (365.25 * 24 * 60 * 60 * 1000));

// Date formatting
const formatted = new Date().toLocaleDateString('en-US', {
    year: 'numeric',
    month: 'long',
    day: 'numeric'
});

String Methods

Standard JavaScript string methods:

// String manipulation
const name = getField('user_name').toUpperCase();
const initials = getField('first_name').charAt(0) + getField('last_name').charAt(0);
const slug = getField('title').toLowerCase().replace(/\s+/g, '-');

Number Parsing

Standard parsing functions:

// Parse numbers
const quantity = parseInt(getField('quantity_str')) || 0;
const price = parseFloat(getField('price_str')) || 0.0;

// Validation
const isValidNumber = !isNaN(parseFloat(getField('amount')));

Console Logging

Console object for debugging:

// Debug logging
console.log('Processing field:', fieldName, 'with value:', value);
console.warn('Invalid input detected:', value);
console.error('Calculation failed:', error);

// Grouped logging
console.group('Order Calculation');
console.log('Subtotal:', subtotal);
console.log('Tax:', tax);
console.log('Total:', total);
console.groupEnd();

Best Practices

Error Handling

onFieldChange: function(fieldName, value) {
    try {
        if (fieldName === 'price') {
            const price = parseFloat(value);
            
            if (isNaN(price)) {
                console.warn('Invalid price value:', value);
                setField('price_error', 'Please enter a valid number');
                return;
            }
            
            if (price < 0) {
                console.warn('Negative price not allowed:', price);
                setField('price_error', 'Price cannot be negative');
                return;
            }
            
            // Clear error and proceed
            setField('price_error', '');
            calculateTotal(price);
        }
    } catch (error) {
        console.error('Error processing field change:', error);
    }
}

Performance Optimization

// Cache calculations
let cachedTaxRate = null;

function getTaxRate(country) {
    if (cachedTaxRate === null) {
        const rates = {
            'US': 8.5,
            'CA': 12.0,
            'UK': 20.0
        };
        cachedTaxRate = rates[country] || 0;
    }
    return cachedTaxRate;
}

// Minimize calculations
onFieldChange: function(fieldName, value) {
    // Only calculate when relevant fields change
    if (['quantity', 'price', 'tax_rate'].includes(fieldName)) {
        // Batch related calculations
        updateAllTotals();
    }
    
    // Avoid recalculating if value hasn't actually changed
    const currentTotal = getField('total');
    const newTotal = calculateNewTotal();
    
    if (currentTotal !== newTotal) {
        setField('total', newTotal);
    }
}

Data Validation

// Validation helper functions
function isValidEmail(email) {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

function isValidPhone(phone) {
    return /^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$/.test(phone);
}

function validateCreditCard(number) {
    // Luhn algorithm
    const digits = number.replace(/\D/g, '');
    let sum = 0;
    let isEven = false;
    
    for (let i = digits.length - 1; i >= 0; i--) {
        let digit = parseInt(digits[i]);
        
        if (isEven) {
            digit *= 2;
            if (digit > 9) digit -= 9;
        }
        
        sum += digit;
        isEven = !isEven;
    }
    
    return sum % 10 === 0;
}

// Use in field change handler
onFieldChange: function(fieldName, value) {
    if (fieldName === 'email') {
        const valid = isValidEmail(value);
        setField('email_valid', valid);
        setField('email_message', valid ? 'Valid email' : 'Invalid email format');
    }
}

Common Patterns

Conditional Field Display

// Simulate showing/hiding fields by setting values
onFieldChange: function(fieldName, value) {
    if (fieldName === 'account_type') {
        if (value === 'business') {
            setField('company_name', '');
            setField('tax_id', '');
            setField('show_business_fields', 'true');
        } else {
            setField('show_business_fields', 'false');
        }
    }
}

Multi-step Calculations

// Break complex calculations into steps
function calculateShippingCost() {
    const weight = parseFloat(getField('package_weight')) || 0;
    const distance = parseFloat(getField('shipping_distance')) || 0;
    const method = getField('shipping_method');
    
    // Step 1: Base cost by weight
    let baseCost = 0;
    if (weight <= 1) baseCost = 5.99;
    else if (weight <= 5) baseCost = 12.99;
    else baseCost = 5.99 + (weight - 1) * 2.50;
    
    // Step 2: Distance multiplier
    let distanceMultiplier = 1;
    if (distance > 500) distanceMultiplier = 1.5;
    else if (distance > 100) distanceMultiplier = 1.2;
    
    // Step 3: Method adjustment
    let methodMultiplier = 1;
    switch(method) {
        case 'express': methodMultiplier = 2; break;
        case 'overnight': methodMultiplier = 3; break;
    }
    
    // Final calculation
    const finalCost = baseCost * distanceMultiplier * methodMultiplier;
    
    setField('shipping_cost', finalCost.toFixed(2));
    setField('shipping_breakdown', `Base: $${baseCost} × Distance: ${distanceMultiplier} × Method: ${methodMultiplier}`);
}

Form State Management

onLoad: function() {
    // Initialize form state
    setField('form_state', 'initialized');
    setField('validation_errors', '');
    setField('completion_percentage', 0);
}

onFieldChange: function(fieldName, value) {
    // Update completion percentage
    updateCompletionPercentage();
    
    // Track form state
    setField('form_state', 'editing');
    setField('last_modified', new Date().toISOString());
}

function updateCompletionPercentage() {
    const requiredFields = ['first_name', 'last_name', 'email', 'phone'];
    let completedFields = 0;
    
    requiredFields.forEach(field => {
        if (getField(field) && getField(field).trim() !== '') {
            completedFields++;
        }
    });
    
    const percentage = Math.round((completedFields / requiredFields.length) * 100);
    setField('completion_percentage', percentage);
}

Debugging and Troubleshooting

Common Issues

1. Field names don't match

// ❌ Wrong - case sensitive
setField('User_Name', 'John'); // Field is actually 'user_name'

// ✅ Correct
setField('user_name', 'John');

2. Missing error handling

// ❌ Without error handling
const price = parseFloat(getField('price'));
const total = price * quantity; // Will fail if price is invalid

// ✅ With error handling
const price = parseFloat(getField('price')) || 0;
const quantity = parseFloat(getField('quantity')) || 0;
const total = price * quantity;

3. Infinite loops in onFieldChange

// ❌ Can cause infinite loop
onFieldChange: function(fieldName, value) {
    if (fieldName === 'total') {
        setField('total', value * 1.1); // This will trigger onFieldChange again!
    }
}

// ✅ Avoid triggering changes for calculated fields
onFieldChange: function(fieldName, value) {
    if (fieldName === 'quantity' || fieldName === 'price') {
        const total = getField('quantity') * getField('price');
        setField('total', total); // Only update when input fields change
    }
}

Debug Techniques

// Enable detailed logging
onFieldChange: function(fieldName, value) {
    console.group(`Field Change: ${fieldName}`);
    console.log('New Value:', value);
    console.log('Current Form Data:', {
        quantity: getField('quantity'),
        price: getField('price'),
        total: getField('total')
    });
    
    // Your calculation logic here
    
    console.groupEnd();
}

// Add validation checkpoints
function calculateTotal() {
    const quantity = getField('quantity');
    const price = getField('price');
    
    console.log('Calculation inputs:', { quantity, price });
    
    if (isNaN(quantity) || isNaN(price)) {
        console.error('Invalid inputs for calculation');
        return;
    }
    
    const total = quantity * price;
    console.log('Calculated total:', total);
    
    setField('total', total.toFixed(2));
}

This API reference covers all available JavaScript functionality in the Form Builder. For implementation examples and integration details, see the Technical Guide and User Guide.

Last updated: December 2024