v1.0.0

βœ… Form Validation

OnigiriJS includes a comprehensive validation system with built-in rules and support for custom validators.

Basic Usage

const form = O('#registration-form');

const result = form.validate({
    username: {
        required: true,
        minLength: 3,
        maxLength: 20,
        alphanumeric: true
    },
    email: {
        required: true,
        email: true
    },
    password: {
        required: true,
        minLength: 8
    },
    age: {
        required: true,
        numeric: true,
        min: 18,
        max: 120
    },
    website: {
        url: true  // Optional field
    }
});

if (result.isValid) {
    // Form is valid, submit
    submitForm();
} else {
    // Show errors
    Object.keys(result.errors).forEach(field => {
        const errors = result.errors[field];
        console.log(\`\${field}: \${errors.join(', ')}\`);
    });
}

Built-in Validation Rules

Required

username: {
    required: true
}

// Error message: "This field is required"

Email

email: {
    required: true,
    email: true
}

// Validates: user@example.com
// Invalid: user@, @example.com, user

URL

website: {
    url: true
}

// Validates: https://example.com
// Invalid: example.com, htp://example

Numeric

quantity: {
    numeric: true
}

// Validates: 123, 0, 999
// Invalid: 12.5, abc, 12a

Alpha (Letters Only)

firstName: {
    alpha: true
}

// Validates: John, Mary
// Invalid: John123, John-Paul

Alphanumeric

username: {
    alphanumeric: true
}

// Validates: user123, JohnDoe
// Invalid: user-123, john_doe

Min/Max Value

age: {
    required: true,
    numeric: true,
    min: 18,
    max: 120
}

price: {
    min: 0.01,
    max: 9999.99
}

// Error messages:
// "Value must be at least 18"
// "Value must be at most 120"

Min/Max Length

password: {
    required: true,
    minLength: 8,
    maxLength: 64
}

bio: {
    maxLength: 500
}

// Error messages:
// "Must be at least 8 characters"
// "Must be at most 64 characters"

Pattern (Regex)

password: {
    required: true,
    minLength: 8,
    pattern: '^(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])'
}

phoneNumber: {
    pattern: '^\\d{3}-\\d{3}-\\d{4}$'
}

// Error message: "Invalid format"

Custom Validation Rules

Adding Custom Rules

// Add a custom rule
Onigiri.validation.addRule(
    'strongPassword',
    function(value) {
        const hasUpperCase = /[A-Z]/.test(value);
        const hasLowerCase = /[a-z]/.test(value);
        const hasNumber = /[0-9]/.test(value);
        const hasSpecial = /[!@#$%^&*]/.test(value);
        
        return hasUpperCase && hasLowerCase && hasNumber && hasSpecial;
    },
    'Password must contain uppercase, lowercase, number, and special character'
);

// Use the custom rule
const result = form.validate({
    password: {
        required: true,
        minLength: 8,
        strongPassword: true
    }
});

Rule with Parameters

// Custom rule that accepts parameters
Onigiri.validation.addRule(
    'matchesField',
    function(value, fieldName) {
        const otherField = document.querySelector(\`[name="\${fieldName}"]\`);
        return otherField && value === otherField.value;
    },
    'Fields do not match'
);

// Usage
const result = form.validate({
    password: {
        required: true,
        minLength: 8
    },
    confirmPassword: {
        required: true,
        matchesField: 'password'
    }
});

Async Validation

// Async rule for username availability
Onigiri.validation.addRule(
    'usernameAvailable',
    async function(value) {
        try {
            const response = await Onigiri.get(\`/api/check-username/\${value}\`);
            return response.available;
        } catch (error) {
            return false;
        }
    },
    'Username is already taken'
);

// Use async validation
const result = await form.validate({
    username: {
        required: true,
        minLength: 3,
        alphanumeric: true,
        usernameAvailable: true
    }
});

Real-time Validation

Validate on Input

const form = O('#signup-form');

// Validate each field on input
form.on('input', 'input', function() {
    const fieldName = this.name;
    const value = this.value;
    
    // Define rules for this field
    const rules = {
        [fieldName]: getFieldRules(fieldName)
    };
    
    // Validate
    const result = Onigiri.validation.validate(
        document.querySelector('#signup-form'),
        rules
    );
    
    // Show/hide error
    const errorEl = O(\`#\${fieldName}-error\`);
    if (result.errors[fieldName]) {
        errorEl.text(result.errors[fieldName][0]).show();
        O(this).addClass('input-error');
    } else {
        errorEl.text('').hide();
        O(this).removeClass('input-error');
    }
});

function getFieldRules(fieldName) {
    const allRules = {
        username: {
            required: true,
            minLength: 3,
            alphanumeric: true
        },
        email: {
            required: true,
            email: true
        },
        password: {
            required: true,
            minLength: 8
        }
    };
    
    return allRules[fieldName] || {};
}

Validate on Blur

// Validate when user leaves field
O('#registration-form').on('blur', 'input', function() {
    const fieldName = this.name;
    const rules = { [fieldName]: getFieldRules(fieldName) };
    
    const result = Onigiri.validation.validate(
        document.querySelector('#registration-form'),
        rules
    );
    
    if (result.errors[fieldName]) {
        showFieldError(fieldName, result.errors[fieldName][0]);
    } else {
        clearFieldError(fieldName);
    }
});

Complete Form Example

HTML

<form id="registration-form">
    <div class="form-group">
        <label for="username">Username</label>
        <input type="text" id="username" name="username">
        <span class="error" id="username-error"></span>
    </div>
    
    <div class="form-group">
        <label for="email">Email</label>
        <input type="email" id="email" name="email">
        <span class="error" id="email-error"></span>
    </div>
    
    <div class="form-group">
        <label for="password">Password</label>
        <input type="password" id="password" name="password">
        <span class="error" id="password-error"></span>
    </div>
    
    <div class="form-group">
        <label for="confirmPassword">Confirm Password</label>
        <input type="password" id="confirmPassword" name="confirmPassword">
        <span class="error" id="confirmPassword-error"></span>
    </div>
    
    <div class="form-group">
        <label for="age">Age</label>
        <input type="number" id="age" name="age">
        <span class="error" id="age-error"></span>
    </div>
    
    <button type="submit">Register πŸ™</button>
</form>

JavaScript

// Custom password matching rule
Onigiri.validation.addRule(
    'matchesPassword',
    function(value) {
        const password = document.querySelector('[name="password"]').value;
        return value === password;
    },
    'Passwords do not match'
);

const form = O('#registration-form');

// Real-time validation
form.on('input', 'input', function() {
    const fieldName = this.name;
    validateField(fieldName);
});

// Submit validation
form.on('submit', async function(e) {
    e.preventDefault();
    
    // Clear all errors
    O('.error').text('').hide();
    O('.input-error').removeClass('input-error');
    
    // Validate entire form
    const result = O(this).validate({
        username: {
            required: true,
            minLength: 3,
            maxLength: 20,
            alphanumeric: true
        },
        email: {
            required: true,
            email: true
        },
        password: {
            required: true,
            minLength: 8,
            pattern: '^(?=.*[A-Z])(?=.*[0-9])'
        },
        confirmPassword: {
            required: true,
            matchesPassword: true
        },
        age: {
            required: true,
            numeric: true,
            min: 18,
            max: 120
        }
    });
    
    if (result.isValid) {
        // Collect form data
        const formData = {
            username: O('[name="username"]').val(),
            email: O('[name="email"]').val(),
            password: O('[name="password"]').val(),
            age: parseInt(O('[name="age"]').val())
        };
        
        // Submit with CSRF
        try {
            await Onigiri.post('/api/register', formData);
            alert('πŸ™ Registration successful!');
            window.location = '/dashboard';
        } catch (error) {
            alert('Registration failed: ' + error.message);
        }
    } else {
        // Show errors
        Object.keys(result.errors).forEach(field => {
            const errors = result.errors[field];
            showFieldError(field, errors[0]);
        });
        
        // Focus first error
        const firstError = Object.keys(result.errors)[0];
        O(\`[name="\${firstError}"]\`).elements[0].focus();
    }
});

function validateField(fieldName) {
    const rules = getFieldRules(fieldName);
    if (!rules) return;
    
    const result = Onigiri.validation.validate(
        document.querySelector('#registration-form'),
        { [fieldName]: rules }
    );
    
    if (result.errors[fieldName]) {
        showFieldError(fieldName, result.errors[fieldName][0]);
    } else {
        clearFieldError(fieldName);
    }
}

function showFieldError(fieldName, message) {
    O(\`[name="\${fieldName}"]\`).addClass('input-error');
    O(\`#\${fieldName}-error\`)
        .text(message)
        .show()
        .css({ color: 'red', fontSize: '14px' });
}

function clearFieldError(fieldName) {
    O(\`[name="\${fieldName}"]\`).removeClass('input-error');
    O(\`#\${fieldName}-error\`).text('').hide();
}

function getFieldRules(fieldName) {
    const allRules = {
        username: {
            required: true,
            minLength: 3,
            maxLength: 20,
            alphanumeric: true
        },
        email: {
            required: true,
            email: true
        },
        password: {
            required: true,
            minLength: 8,
            pattern: '^(?=.*[A-Z])(?=.*[0-9])'
        },
        confirmPassword: {
            required: true,
            matchesPassword: true
        },
        age: {
            required: true,
            numeric: true,
            min: 18,
            max: 120
        }
    };
    
    return allRules[fieldName];
}

Advanced Patterns

Multi-Step Form Validation

const steps = ['step1', 'step2', 'step3'];
let currentStep = 0;

const stepRules = {
    step1: {
        firstName: { required: true, alpha: true },
        lastName: { required: true, alpha: true }
    },
    step2: {
        email: { required: true, email: true },
        phone: { required: true, pattern: '^\\d{3}-\\d{3}-\\d{4}$' }
    },
    step3: {
        password: { required: true, minLength: 8 },
        confirmPassword: { required: true, matchesPassword: true }
    }
};

O('.next-step').on('click', async () => {
    const step = steps[currentStep];
    const form = O(\`#\${step}\`);
    
    const result = form.validate(stepRules[step]);
    
    if (result.isValid) {
        currentStep++;
        showStep(currentStep);
    } else {
        showStepErrors(result.errors);
    }
});

O('.prev-step').on('click', () => {
    currentStep--;
    showStep(currentStep);
});

Conditional Validation

// Only validate if checkbox is checked
O('#shipping-different').on('change', function() {
    const isDifferent = this.checked;
    
    if (isDifferent) {
        O('#shipping-fields').show();
    } else {
        O('#shipping-fields').hide();
    }
});

O('#checkout-form').on('submit', function(e) {
    e.preventDefault();
    
    const rules = {
        billingAddress: { required: true },
        billingCity: { required: true }
    };
    
    // Add shipping rules only if needed
    if (O('#shipping-different').elements[0].checked) {
        rules.shippingAddress = { required: true };
        rules.shippingCity = { required: true };
    }
    
    const result = O(this).validate(rules);
    
    if (result.isValid) {
        submitOrder();
    }
});
πŸ™ Pro Tip: Combine validation with the security module for complete form protection - validation checks input integrity while CSRF protection verifies request authenticity!

Validation Best Practices

  • Validate on both client and server side
  • Provide clear, specific error messages
  • Validate in real-time for better UX
  • Focus the first field with an error
  • Use visual indicators (colors, icons)
  • Group related validation rules
  • Sanitize input even after validation
  • Test edge cases (empty, very long, special characters)

βœ… Development Roadmap

Track the progress of OnigiriJS modules. Tasks are marked complete by the development team.

OnigiriJS Module Roadmap

Implementation progress of planned modules

6 / 21 completed (29%)
onigiri-state
Shared global & scoped state management
onigiri-directives
Declarative DOM bindings (o-show, o-model, etc.)
onigiri-resource
REST-style data models over AJAX
onigiri-observe
Intersection & Mutation observer helpers
onigiri-humhub-ui
Standard HumHub UI abstractions (modal, notify, confirm)
onigiri-lifecycle
Component lifecycle hooks
onigiri-guard
Debounce, throttle, single-run guards
onigiri-scroll
Scroll save/restore & helpers (PJAX-friendly)
onigiri-permission
Client-side permission awareness
onigiri-portal
DOM teleport / overlay mounting
onigiri-router
Micro router (non-SPA, PJAX-first)
onigiri-sanitize
HTML & input sanitization
onigiri-shortcut
Keyboard shortcut manager
onigiri-queue
Sequential async task runner
onigiri-gesture
Touch & swipe helpers
onigiri-devtools
Debugging & inspection helpers
onigiri-plugin
Plugin registration system
onigiri-time
Relative time & timezone utilities
onigiri-emojis
Emoji Picker and Manager
onigiri-tasks
Task Management
onigiri-polls
Polls creation and management
Note: Task completion is managed by the OnigiriJS development team.