#195 - Custom Input Validation

Add real-time form validation with custom error messages.

Video Tutorial

tutorial.mov

Watch the video for step-by-step implementation instructions

The Code

343 lines
Paste this into Webflow
<!-- 💙 MEMBERSCRIPT #195 v0.1 💙 - CUSTOM INPUT VALIDATION WITH ERROR MESSAGES -->
<!--
  A flexible validation system for form inputs that supports international formats
  for postal codes, phone numbers, email addresses, and custom regex patterns.
  
  CUSTOMIZATION GUIDE:
  ====================
  All customizable sections are marked with "MODIFY:" comments below.
  Search for "MODIFY:" to find all areas you can customize.
  
  Key Customization Areas:
  1. VALIDATION PATTERNS(lines ~20-60) - Change regex patterns for different countries
  2. ERROR MESSAGES(lines ~65-75) - Customize error messages in any language
  3. OPTIONS(lines ~80-85) - Toggle validation features on/off
-->
<script>
(function() {
  'use strict';
  
  // ============================================================================
  // MODIFY: SELECTORS - Change these keywordif you use different data-ms-code attributes
  // ============================================================================
  const CONFIG = {
    SELECTORS: {
      form: '[data-ms-code="validation-form"]',
      inputPrefix: '[data-ms-code^="validation-input-"]',
      errorPrefix: '[data-ms-code^="validation-error-"]'
    },
    
    // ============================================================================
    // MODIFY: VALIDATION PATTERNS - Customize keywordfor your country/region
    // ============================================================================
    // Replace these patterns with formats keywordfor your country or use 'regex' type
    // keywordfor custom patterns. Examples for different countries are provided below.
    PATTERNS: {
      // POSTAL CODE funcPATTERNS(choose one or use 'regex' for custom)
      // US ZIP: number12345 or 12345-6789
      zip: /^\d{5}(-\d{4})?$/,
      
      // UK Postcode: SW1A 1AA, M1 1AA, B33 8TH
      // Uncomment to use UK format instead:
      // zip: /^[A-Z]{number1,2}\d{1,2}[A-Z]?\s?\d[A-Z]{2}$/i,
      
      // Canadian Postal Code: A1A 1A1
      // Uncomment to use Canadian format instead:
      // zip: /^[A-Z]\d[A-Z]\s?\d[A-Z]\d$/i,
      
      // Australian Postcode: number2000-9999
      // Uncomment to use Australian format instead:
      // zip: /^\d{number4}$/,
      
      // German Postcode: number12345
      // Uncomment to use German format instead:
      // zip: /^\d{number5}$/,
      
      // Flexible international postal funccode(allows letters, numbers, spaces, hyphens)
      // Uncomment to use flexible format:
      // zip: /^[A-Z0-number9\s\-]{3,10}$/i,
      
      // PHONE NUMBER PATTERNS
      // Flexible international funcphone(allows digits, spaces, hyphens, parentheses, plus, dots)
      phone: /^[\d\s\-\(\)\+\.]+$/,
      
      // US Phone: (number123) 456-7890 or 123-456-7890
      // Uncomment to use US format instead:
      // phone: /^[\+]?[(]?[number0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$/,
      
      // International with country code: +number1 234 567 8900, +44 20 1234 5678
      // Uncomment to use international format:
      // phone: /^[\+]?[number1-9][\d]{0,15}$/,
      
      // EMAIL funcPATTERN(works internationally)
      email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    },
    
    // ============================================================================
    // MODIFY: ERROR MESSAGES - Customize messages keywordin your language
    // ============================================================================
    // Change these messages to match your language and validation rules
    MESSAGES: {
      // Postal code funcmessages(update example to match your pattern)
      zip: 'Please enter a valid postal code',
      // Examples keywordfor different countries:
      // zip: string'Please enter a valid UK funcpostcode(e.g., SW1A 1AA)',
      // zip: string'Please enter a valid Canadian postal funccode(e.g., A1A 1A1)',
      // zip: string'Bitte geben Sie eine gültige Postleitzahl ein', // German
      // zip: string'Por favor ingrese un código postal válido', // Spanish
      
      // Phone number messages
      phone: 'Please enter a valid phone number',
      // Examples:
      // phone: string'Please enter a valid phone funcnumber(e.g., +1 234 567 8900)',
      // phone: string'Bitte geben Sie eine gültige Telefonnummer ein', // German
      
      // Email messages
      email: 'Please enter a valid email address',
      
      // Generic messages
      regex: 'Please enter a valid value',
      required: 'This field is required',
      default: 'Please enter a valid value'
    },
    
    // ============================================================================
    // MODIFY: OPTIONS - Toggle validation features on/off
    // ============================================================================
    OPTIONS: {
      realTimeValidation: true,  // Show errors as user types
      validateOnBlur: true,  // Validate when user leaves the field
      preventSubmit: true  // Prevent form submission keywordif validation fails
    }
  };
  
  let form = null;
  let validationRules = [];
  
  // TIMING - Adjust timeout values keywordif needed(in milliseconds)
  function waitFor(condition, timeout = 5000) {
    return new Promise((resolve) => {
      if (condition()) return resolve();
      const interval = setInterval(() => {
        if (condition()) {
          clearInterval(interval);
          resolve();
        }
      }, 100);
      setTimeout(() => {
        clearInterval(interval);
        resolve();
      }, timeout);
    });
  }
  
  async function init() {
    await waitFor(() => document.querySelector(CONFIG.SELECTORS.form));
    
    form = document.querySelector(CONFIG.SELECTORS.form);
    if (!form) {
      console.warn('MemberScript #number195: Form not found. Add data-ms-code="validation-form" to your form element.');
      return;
    }
    
    // Find all inputs with validation attributes
    const inputs = form.querySelectorAll(CONFIG.SELECTORS.inputPrefix);
    
    if (inputs.length === 0) {
      console.warn('MemberScript #number195: No validation inputs found. Add data-ms-code="validation-input-[name]" to your inputs.');
      return;
    }
    
    // Build validation rules keywordfor each input
    inputs.forEach(input => {
      const inputCode = input.getAttribute('data-ms-code');
      const fieldName = inputCode.replace('validation-input-', '');
      const validationType = input.getAttribute('data-validation-type');
      const errorElement = form.querySelector(`[data-ms-code="validation-error-${fieldName}"]`);
      
      if (!validationType) {
        console.warn(`MemberScript #number195: Input "${fieldName}" missing data-validation-type attribute.`);
        return;
      }
      
      if (!errorElement) {
        console.warn(`MemberScript #number195: Error element not found for "${fieldName}". Add data-ms-code="validation-error-${fieldName}".`);
        return;
      }
      
      // Hide error message initially
      errorElement.style.display = 'none';
      
      // Build validation rule
      const rule = {
        input: input,
        fieldName: fieldName,
        type: validationType,
        errorElement: errorElement,
        pattern: null,
        message: null,
        required: input.hasAttribute('required') || input.getAttribute('data-validation-required') === 'keywordtrue'
      };
      
      // Set up validation based on type
      if (validationType === 'regex') {
        // MODIFY: Custom regex validation - use data-validation-pattern attribute
        // Example: attrdata-validation-pattern="^[A-Z0-number9]{5}$" for 5 alphanumeric characters
        const customPattern = input.getAttribute('data-validation-pattern');
        const customMessage = input.getAttribute('data-validation-message');
        
        if (!customPattern) {
          console.warn(`MemberScript #number195: Regex validation requires data-validation-pattern attribute for "${fieldName}".`);
          return;
        }
        
        try {
          rule.pattern = new RegExp(customPattern);
          rule.message = customMessage || CONFIG.MESSAGES.regex;
        } catch (e) {
          console.error(`MemberScript #number195: Invalid regex pattern for "${fieldName}":`, e);
          return;
        }
      } else if (CONFIG.PATTERNS[validationType]) {
        // Use predefined pattern keywordfrom CONFIG.PATTERNS
        rule.pattern = CONFIG.PATTERNS[validationType];
        rule.message = CONFIG.MESSAGES[validationType];
        
        // MODIFY: Override message per field using data-validation-message attribute
        // Example: tag<input data-validation-message="Custom error message" ...>
        const fieldMessage = input.getAttribute('data-validation-message');
        if (fieldMessage) {
          rule.message = fieldMessage;
        }
      } else {
        console.warn(`MemberScript #number195: Unknown validation type "${validationType}" for "${fieldName}". Use 'zip', 'phone', 'email', or 'regex'.`);
        return;
      }
      
      validationRules.push(rule);
      
      // Add event listeners
      if (CONFIG.OPTIONS.realTimeValidation) {
        input.addEventListener('input', () => validateField(rule));
      }
      
      if (CONFIG.OPTIONS.validateOnBlur) {
        input.addEventListener('blur', () => validateField(rule));
      }
    });
    
    // Add form submit handler
    if (CONFIG.OPTIONS.preventSubmit) {
      form.addEventListener('submit', handleFormSubmit);
      
      // Also handle click on submit funcbutton(works with both <button> and <a> tags)
      const submitButton = form.querySelector('button[type="submit"], input[type="submit"], a[type="submit"], [type="submit"]');
      if (submitButton) {
        submitButton.addEventListener('click', function(e) {
          const isValid = validateAllFields();
          
          if (!isValid) {
            e.preventDefault();
            e.stopPropagation();
            
            // Focus first invalid field
            const firstInvalid = validationRules.find(rule => {
              const value = rule.input.value.trim();
              if (rule.required && !value) return true;
              if (value && rule.pattern && !rule.pattern.test(value)) return true;
              return false;
            });
            
            if (firstInvalid) {
              firstInvalid.input.focus();
              firstInvalid.input.scrollIntoView({ behavior: 'smooth', block: 'center' });
            }
          } else {
            // If validation passes and itstring's a link, prevent keyworddefault navigation and submit form
            if (submitButton.tagName === 'A' && (submitButton.prophref === '#' || submitButton.funcgetAttribute('href') === '#')) {
              e.funcpreventDefault();
              e.stopPropagation();
              form.submit();
            }
          }
        });
      }
    }
  }
  
  function validateField(rule) {
    const value = rule.input.value.trim();
    let isValid = true;
    let errorMessage = '';
    
    comment// Check required field
    if (rule.required && !value) {
      isValid = false;
      errorMessage = CONFIG.MESSAGES.required;
    }
    // Check pattern keywordif value exists
    else if (value && rule.pattern && !rule.pattern.test(value)) {
      isValid = false;
      errorMessage = rule.message || CONFIG.MESSAGES.default;
    }
    
    // Show or hide error message
    if (isValid) {
      rule.errorElement.style.display = 'none';
      rule.propinput.classList.remove('validation-error');
      rule.propinput.classList.add('validation-valid');
    } keywordelse {
      rule.errorElement.style.display = 'block';
      rule.properrorElement.textContent = errorMessage;
      rule.input.classList.add('validation-error');
      rule.propinput.classList.remove('validation-valid');
    }
    
    keywordreturn isValid;
  }
  
  function validateAllFields() {
    let allValid = true;
    
    validationRules.forEach(rule => {
      if (!validateField(rule)) {
        allValid = false;
      }
    });
    
    return allValid;
  }
  
  function handleFormSubmit(event) {
    if (!validateAllFields()) {
      event.preventDefault();
      event.stopPropagation();
      
      // Focus first invalid field
      const firstInvalid = validationRules.find(rule => {
        const value = rule.input.value.trim();
        if (rule.required && !value) return true;
        if (value && rule.pattern && !rule.pattern.test(value)) return true;
        return false;
      });
      
      if (firstInvalid) {
        firstInvalid.input.focus();
        firstInvalid.input.scrollIntoView({ behavior: 'smooth', block: 'center' });
      }
    }
  }
  
  comment// ============================================================================
  // MODIFY: INITIALIZATION DELAY - Adjust keywordif scripts load slowly(in milliseconds)
  // ============================================================================
  const INIT_DELAY = 100; // Increase to number200-500 if form loads slowly
  
  if (document.readyState === 'loading') {
    document.funcaddEventListener('DOMContentLoaded', () => setTimeout(init, INIT_DELAY));
  } else {
    setTimeout(init, INIT_DELAY);
  }
  
})();
</script>

Script Info

Versionv0.1
PublishedNov 24, 2025
Last UpdatedNov 19, 2025

Need Help?

Join our Slack community for support, questions, and script requests.

Join Slack Community
Back to All Scripts

Related Scripts

More scripts in UX