π‘ 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.