v1.0.0

πŸ“‘ AJAX Requests

OnigiriJS provides a simple AJAX API with automatic CSRF protection, promise-based responses, and convenient helper methods.

Basic AJAX Request

Simple GET Request

// Basic GET request
Onigiri.ajax({
    url: '/api/users'
}).then(data => {
    console.log('Users:', data);
}).catch(error => {
    console.error('Error:', error);
});

POST Request

// POST request with data
Onigiri.ajax({
    url: '/api/users',
    method: 'POST',
    data: {
        name: 'John Doe',
        email: 'john@example.com',
        role: 'chef'
    }
}).then(response => {
    console.log('User created:', response);
}).catch(error => {
    console.error('Failed:', error);
});

With Options

Onigiri.ajax({
    url: '/api/onigiri',
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'Accept': 'application/json'
    },
    data: {
        name: 'Salmon Onigiri',
        price: 3.50,
        ingredients: ['rice', 'salmon', 'nori']
    },
    csrf: true,      // Include CSRF token (default)
    timeout: 5000    // 5 second timeout
}).then(response => {
    console.log('πŸ™ Onigiri created!', response);
});

πŸ”’ Automatic CSRF Protection

CSRF is Automatic

// Initialize security module
Onigiri.security.init({
    autoInjectCSRF: true
});

// All requests now include CSRF token automatically!
await Onigiri.ajax({
    url: '/api/save',
    method: 'POST',
    data: { content: 'data' }
});
// X-CSRF-Token header added automatically

Disable CSRF for Specific Request

// Public API endpoint without CSRF
await Onigiri.ajax({
    url: '/public/api/news',
    method: 'GET',
    csrf: false  // Disable CSRF for this request
});

✨ Convenience Methods

GET

// Simple GET
const users = await Onigiri.get('/api/users');

// With query parameters
const url = '/api/onigiri?category=salmon&limit=10';
const onigiri = await Onigiri.get(url);

// With options
const data = await Onigiri.get('/api/data', {
    timeout: 10000
});

POST

// POST with data
const newUser = await Onigiri.post('/api/users', {
    name: 'Chef Tanaka',
    specialty: 'Onigiri'
});

// CSRF token included automatically
const order = await Onigiri.post('/api/orders', {
    items: [
        { id: 1, quantity: 2 },
        { id: 3, quantity: 1 }
    ],
    total: 10.50
});

PUT

// Update resource
const updated = await Onigiri.put('/api/users/123', {
    name: 'Updated Name',
    email: 'new@example.com'
});

DELETE

// Delete resource
await Onigiri.delete('/api/users/123');

// With confirmation
if (confirm('Delete this onigiri? πŸ™')) {
    await Onigiri.delete(\`/api/onigiri/\${id}\`);
    console.log('Deleted!');
}

🎯 Real-World Examples

Load and Display Data

async function loadOnigiriMenu() {
    const container = O('#menu');
    
    // Show loading
    container.html('<p>Loading menu... πŸ™</p>');
    
    try {
        // Fetch data
        const items = await Onigiri.get('/api/onigiri');
        
        // Clear loading
        container.empty();
        
        // Display items
        items.forEach(item => {
            container.append(\`
                <div class="menu-item" data-id="\${item.id}">
                    <h3>πŸ™ \${item.name}</h3>
                    <p>\${item.description}</p>
                    <span class="price">$\${item.price}</span>
                    <button class="order-btn">Order</button>
                </div>
            \`);
        });
        
    } catch (error) {
        container.html(\`
            <p class="error">Failed to load menu: \${error.message}</p>
        \`);
    }
}

loadOnigiriMenu();

Form Submission

O('#contact-form').on('submit', async function(e) {
    e.preventDefault();
    
    // Validate form
    const result = O(this).validate({
        name: { required: true },
        email: { required: true, email: true },
        message: { required: true, minLength: 10 }
    });
    
    if (!result.isValid) {
        showErrors(result.errors);
        return;
    }
    
    // Get form data
    const formData = {
        name: O('[name="name"]').val(),
        email: O('[name="email"]').val(),
        message: O('[name="message"]').val()
    };
    
    // Show loading
    const submitBtn = O('button[type="submit"]');
    submitBtn.attr('disabled', true).text('Sending...');
    
    try {
        // Submit with CSRF (automatic)
        const response = await Onigiri.post('/api/contact', formData);
        
        // Success
        alert('πŸ™ Message sent successfully!');
        O(this).elements[0].reset();
        
    } catch (error) {
        alert('Failed to send message: ' + error.message);
        
    } finally {
        submitBtn.attr('disabled', false).text('Send Message');
    }
});

Auto-Complete Search

const searchInput = O('#search');
const resultsContainer = O('#search-results');

// Debounce search
const debouncedSearch = Onigiri.debounce(async (query) => {
    if (query.length < 2) {
        resultsContainer.empty();
        return;
    }
    
    // Show loading
    resultsContainer.html('<p>Searching... πŸ™</p>');
    
    try {
        // Search API
        const results = await Onigiri.get(
            \`/api/search?q=\${encodeURIComponent(query)}\`
        );
        
        // Display results
        resultsContainer.empty();
        
        if (results.length === 0) {
            resultsContainer.html('<p>No results found</p>');
            return;
        }
        
        results.forEach(result => {
            resultsContainer.append(\`
                <div class="search-result">
                    <a href="\${result.url}">\${result.title}</a>
                </div>
            \`);
        });
        
    } catch (error) {
        resultsContainer.html(\`<p>Search failed</p>\`);
    }
}, 300);

searchInput.on('input', function() {
    debouncedSearch(this.value);
});

Pagination

const pagination = {
    currentPage: 1,
    perPage: 10,
    
    async loadPage(page) {
        this.currentPage = page;
        
        const container = O('#items-list');
        container.addClass('loading');
        
        try {
            const response = await Onigiri.get(
                \`/api/items?page=\${page}&per_page=\${this.perPage}\`
            );
            
            // Display items
            container.removeClass('loading').empty();
            
            response.items.forEach(item => {
                container.append(\`
                    <div class="item">\${item.name}</div>
                \`);
            });
            
            // Update pagination controls
            this.renderPagination(response.total_pages);
            
        } catch (error) {
            container.removeClass('loading');
            alert('Failed to load page');
        }
    },
    
    renderPagination(totalPages) {
        const container = O('#pagination');
        container.empty();
        
        for (let i = 1; i <= totalPages; i++) {
            const btn = \`
                <button 
                    class="page-btn \${i === this.currentPage ? 'active' : ''}"
                    data-page="\${i}"
                >
                    \${i}
                </button>
            \`;
            container.append(btn);
        }
    }
};

// Click handlers
O(document).on('click', '.page-btn', function() {
    const page = parseInt(O(this).data('page'));
    pagination.loadPage(page);
});

// Load first page
pagination.loadPage(1);

File Upload

O('#upload-form').on('submit', async function(e) {
    e.preventDefault();
    
    const fileInput = document.querySelector('#file');
    const file = fileInput.files[0];
    
    if (!file) {
        alert('Please select a file');
        return;
    }
    
    // Create FormData
    const formData = new FormData();
    formData.append('file', file);
    formData.append('description', O('[name="description"]').val());
    
    // Add CSRF token
    if (Onigiri.security.getToken()) {
        formData.append('_csrf', Onigiri.security.getToken());
    }
    
    // Show progress
    O('#upload-progress').show();
    
    try {
        // Upload (using fetch for FormData)
        const response = await fetch('/api/upload', {
            method: 'POST',
            body: formData,
            headers: {
                'X-CSRF-Token': Onigiri.security.getToken()
            }
        });
        
        if (!response.ok) throw new Error('Upload failed');
        
        const result = await response.json();
        alert('πŸ™ File uploaded successfully!');
        
    } catch (error) {
        alert('Upload failed: ' + error.message);
        
    } finally {
        O('#upload-progress').hide();
    }
});

Polling / Auto-Refresh

const statusMonitor = {
    interval: null,
    pollInterval: 5000, // 5 seconds
    
    start() {
        // Initial load
        this.checkStatus();
        
        // Poll every 5 seconds
        this.interval = setInterval(() => {
            this.checkStatus();
        }, this.pollInterval);
    },
    
    stop() {
        if (this.interval) {
            clearInterval(this.interval);
            this.interval = null;
        }
    },
    
    async checkStatus() {
        try {
            const status = await Onigiri.get('/api/status');
            
            // Update UI
            O('#status-indicator')
                .removeClass('online offline')
                .addClass(status.online ? 'online' : 'offline')
                .text(status.online ? 'πŸ™ Online' : '❌ Offline');
            
            O('#last-update').text(\`Updated: \${new Date().toLocaleTimeString()}\`);
            
        } catch (error) {
            console.error('Status check failed:', error);
            O('#status-indicator')
                .removeClass('online')
                .addClass('offline')
                .text('❌ Error');
        }
    }
};

// Start monitoring
statusMonitor.start();

// Stop on page unload
window.addEventListener('beforeunload', () => {
    statusMonitor.stop();
});

Optimistic UI Updates

async function toggleFavorite(onigiriId) {
    const button = O(\`[data-id="\${onigiriId}"] .favorite-btn\`);
    const isFavorite = button.hasClass('active');
    
    // Optimistic update (before server response)
    button.toggleClass('active');
    button.text(isFavorite ? 'β˜†' : 'β˜…');
    
    try {
        // Send to server
        await Onigiri.post('/api/favorites', {
            onigiri_id: onigiriId,
            action: isFavorite ? 'remove' : 'add'
        });
        
        // Success - optimistic update was correct
        console.log('πŸ™ Favorite updated');
        
    } catch (error) {
        // Revert optimistic update
        button.toggleClass('active');
        button.text(isFavorite ? 'β˜…' : 'β˜†');
        
        alert('Failed to update favorite');
    }
}

Retry Logic

async function fetchWithRetry(url, options = {}, maxRetries = 3) {
    let lastError;
    
    for (let i = 0; i < maxRetries; i++) {
        try {
            const response = await Onigiri.ajax({
                url: url,
                ...options
            });
            
            return response;
            
        } catch (error) {
            lastError = error;
            console.log(\`Attempt \${i + 1} failed, retrying...\`);
            
            // Wait before retry (exponential backoff)
            await new Promise(resolve => 
                setTimeout(resolve, Math.pow(2, i) * 1000)
            );
        }
    }
    
    throw lastError;
}

// Usage
try {
    const data = await fetchWithRetry('/api/important-data', {
        method: 'GET'
    }, 3);
} catch (error) {
    console.error('Failed after 3 retries:', error);
}

βš™οΈ Advanced Patterns

Request Queue

class RequestQueue {
    constructor(concurrency = 2) {
        this.concurrency = concurrency;
        this.running = 0;
        this.queue = [];
    }
    
    async add(fn) {
        while (this.running >= this.concurrency) {
            await new Promise(resolve => {
                this.queue.push(resolve);
            });
        }
        
        this.running++;
        
        try {
            return await fn();
        } finally {
            this.running--;
            const resolve = this.queue.shift();
            if (resolve) resolve();
        }
    }
}

const queue = new RequestQueue(2);

// Add multiple requests
const requests = ids.map(id => 
    queue.add(() => Onigiri.get(\`/api/items/\${id}\`))
);

const results = await Promise.all(requests);

Cache Layer

class APICache {
    constructor(ttl = 60000) {
        this.ttl = ttl;
        this.cache = new Map();
    }
    
    async get(url, options = {}) {
        const cached = this.cache.get(url);
        
        if (cached && Date.now() - cached.timestamp < this.ttl) {
            console.log('πŸ™ Cache hit:', url);
            return cached.data;
        }
        
        console.log('πŸ“‘ Fetching:', url);
        const data = await Onigiri.ajax({ url, ...options });
        
        this.cache.set(url, {
            data: data,
            timestamp: Date.now()
        });
        
        return data;
    }
    
    clear() {
        this.cache.clear();
    }
}

const apiCache = new APICache(60000); // 1 minute

// Use cached requests
const users = await apiCache.get('/api/users');
const onigiri = await apiCache.get('/api/onigiri');
πŸ™ Pro Tip: Always handle errors gracefully and provide feedback to users during async operations!

AJAX Best Practices

  • Always handle errors with try/catch
  • Show loading indicators for long requests
  • Use appropriate HTTP methods (GET, POST, PUT, DELETE)
  • Include CSRF tokens for state-changing requests
  • Debounce user-triggered requests (search, autocomplete)
  • Implement retry logic for critical requests
  • Cache responses when appropriate
  • Set reasonable timeouts
  • Validate responses before using data
  • Cancel pending requests on component unmount

βœ… 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.