πͺ Event System
OnigiriJS includes a powerful event system with namespacing, delegation, and custom events for building reactive applications.
Component Events
Basic Event Handling
const emitter = new Onigiri.prototype.EventEmitter();
// Listen to events
emitter.on('onigiri:prepared', (data) => {
console.log('π Onigiri ready:', data.filling);
console.log('Temperature:', data.temperature);
});
// Emit events
emitter.emit('onigiri:prepared', {
filling: 'salmon',
temperature: 'warm',
time: new Date()
});
Multiple Listeners
const shop = new Onigiri.prototype.EventEmitter();
// Add multiple listeners to the same event
shop.on('sale', (item) => {
console.log('Recording sale:', item);
});
shop.on('sale', (item) => {
console.log('Updating inventory');
});
shop.on('sale', (item) => {
console.log('Notifying customer');
});
// All three listeners will be called
shop.emit('sale', { item: 'Salmon Onigiri', price: 3.50 });
π·οΈ Namespaced Events
Organize events with namespaces for better control:
const app = new Onigiri.prototype.EventEmitter();
// Add namespaced listeners
app.on('click', handleAnalytics, 'analytics');
app.on('click', handleNavigation, 'navigation');
app.on('click', handleUI, 'ui');
app.on('click', handleLogging, 'logging');
// Remove specific namespace
app.off('click', null, 'analytics');
// analytics handler removed, others still work
// Remove all click handlers in navigation namespace
app.off('click', null, 'navigation');
// Emit to all remaining namespaces
app.emit('click', { x: 100, y: 200 });
Namespace Benefits
const widget = new Onigiri.prototype.EventEmitter();
// Plugin A adds handlers with its namespace
widget.on('update', pluginAHandler, 'pluginA');
widget.on('change', pluginAHandler2, 'pluginA');
// Plugin B adds handlers with its namespace
widget.on('update', pluginBHandler, 'pluginB');
widget.on('change', pluginBHandler2, 'pluginB');
// Disable plugin A (remove all its handlers)
widget.off('update', null, 'pluginA');
widget.off('change', null, 'pluginA');
// Plugin B still works!
widget.emit('update', data);
1οΈβ£ Once Events
Listen to an event only once:
const app = new Onigiri.prototype.EventEmitter();
// Listen only once
app.once('init', () => {
console.log('π Application initialized!');
loadData();
});
app.emit('init'); // Logs message and loads data
app.emit('init'); // Nothing happens
app.emit('init'); // Still nothing
// Once with namespace
app.once('ready', handleReady, 'startup');
app.emit('ready'); // Fires once
app.emit('ready'); // Nothing
π― DOM Events
Simple Event Binding
// Bind to elements
O('.onigiri-button').on('click', function(e) {
console.log('π Button clicked!');
console.log('Element:', this);
O(this).addClass('clicked');
});
// Multiple events
O('.input').on('focus', function() {
O(this).addClass('focused');
});
O('.input').on('blur', function() {
O(this).removeClass('focused');
});
// Remove event listener
const handler = function() { console.log('Clicked'); };
O('.button').on('click', handler);
O('.button').off('click', handler);
Event Delegation
// Listen to events on dynamically added elements
O('#onigiri-list').on('click', '.item', function(e) {
console.log('Item clicked:', this.dataset.id);
O(this).toggleClass('selected');
});
// Works even for items added later
O('#onigiri-list').append('<div class="item" data-id="5">New Item</div>');
// Click on new item works automatically!
// Form delegation
O('#form-container').on('submit', 'form', function(e) {
e.preventDefault();
console.log('Form submitted:', this.id);
});
// Input delegation
O('#search-area').on('input', 'input[type="text"]', function(e) {
console.log('Search:', this.value);
});
Multiple Element Events
// Bind to all matching elements
O('.onigiri-card').on('mouseenter', function() {
O(this).addClass('hover');
});
O('.onigiri-card').on('mouseleave', function() {
O(this).removeClass('hover');
});
// Chain event bindings
O('.interactive')
.on('click', handleClick)
.on('dblclick', handleDoubleClick)
.on('contextmenu', handleRightClick);
β¨ Custom Events
Trigger Custom Events
// Create and trigger custom events
O('.widget').trigger('onigiri:ready', {
count: 5,
type: 'salmon',
timestamp: Date.now()
});
// Listen to custom events
document.addEventListener('onigiri:ready', (e) => {
console.log('Widget ready:', e.detail);
console.log('Count:', e.detail.count);
console.log('Type:', e.detail.type);
});
Custom Event Bubbling
// Events bubble up the DOM
O('.child-element').trigger('custom:action', { data: 'value' });
// Listen on parent
O('.parent-element').on('custom:action', function(e) {
console.log('Child triggered event:', e.detail);
});
// Listen on document
document.addEventListener('custom:action', (e) => {
console.log('Event bubbled to document:', e.detail);
});
π Reactive Component Events
Data Change Events
const shop = new Onigiri.prototype.Component({
data: {
stock: 10,
price: 3.50
}
});
// Listen to specific property changes
shop.on('change:stock', (newVal, oldVal) => {
console.log(\`Stock: \${oldVal} β \${newVal}\`);
if (newVal === 0) {
alert('π Out of stock!');
}
});
shop.on('change:price', (newVal, oldVal) => {
console.log(\`Price: $\${oldVal} β $\${newVal}\`);
});
// Listen to any property update
shop.on('update', (property, newVal, oldVal) => {
console.log(\`\${property} changed from \${oldVal} to \${newVal}\`);
// Save to storage
Onigiri.storage.set(\`shop_\${property}\`, newVal);
});
// Change data (triggers events automatically)
shop.stock = 5; // Fires change:stock and update
shop.price = 4.00; // Fires change:price and update
Component Lifecycle Events
const widget = new Onigiri.prototype.Component({
data: { message: 'Hello' },
created() {
// Component emits events during lifecycle
console.log('Created');
}
});
// You can't listen to lifecycle directly,
// but you can use hooks or custom events
const app = new Onigiri.prototype.Component({
created() {
this.emit('app:created');
},
mounted() {
this.emit('app:mounted');
}
});
app.on('app:created', () => {
console.log('App created event fired');
});
app.on('app:mounted', () => {
console.log('App mounted event fired');
});
π Event Communication Patterns
Parent-Child Communication
// Create event bus for component communication
const eventBus = new Onigiri.prototype.EventEmitter();
// Parent component
const parent = new Onigiri.prototype.Component({
data: { parentData: 'Hello' },
created() {
// Listen to child events
eventBus.on('child:update', (data) => {
console.log('Child says:', data);
this.parentData = data;
});
},
methods: {
sendToChild(message) {
eventBus.emit('parent:message', message);
}
}
});
// Child component
const child = new Onigiri.prototype.Component({
data: { childData: 'World' },
created() {
// Listen to parent events
eventBus.on('parent:message', (message) => {
console.log('Parent says:', message);
this.childData = message;
});
},
methods: {
sendToParent(message) {
eventBus.emit('child:update', message);
}
}
});
// Usage
parent.sendToChild('Data from parent');
child.sendToParent('Data from child');
Global Event Bus
// Create global event bus
window.OnigiriBus = new Onigiri.prototype.EventEmitter();
// Component A
const componentA = new Onigiri.prototype.Component({
methods: {
broadcast(message) {
OnigiriBus.emit('global:message', {
from: 'ComponentA',
message: message
});
}
}
});
// Component B
const componentB = new Onigiri.prototype.Component({
created() {
OnigiriBus.on('global:message', (data) => {
console.log(\`Message from \${data.from}: \${data.message}\`);
});
}
});
// Component C
const componentC = new Onigiri.prototype.Component({
created() {
OnigiriBus.on('global:message', (data) => {
console.log('Component C received:', data);
});
}
});
// Any component can broadcast
componentA.broadcast('Hello everyone!');
β‘ Event Performance
Removing Event Listeners
const widget = new Onigiri.prototype.EventEmitter();
// Store handler reference for removal
const updateHandler = (data) => {
console.log('Update:', data);
};
widget.on('update', updateHandler);
// Remove specific handler
widget.off('update', updateHandler);
// Remove all handlers for an event
widget.off('update');
// Remove all event handlers
widget.off();
Event Cleanup on Destroy
const component = new Onigiri.prototype.Component({
data: { count: 0 },
created() {
// Set up external listeners
this.externalHandler = (e) => {
console.log('Window resized');
};
window.addEventListener('resize', this.externalHandler);
// Set up component listeners
this.on('custom:event', this.handleCustom);
},
methods: {
handleCustom(data) {
console.log('Custom event:', data);
}
},
beforeDestroy() {
// Clean up external listeners
window.removeEventListener('resize', this.externalHandler);
// Component listeners are cleaned up automatically
// but you can also do it manually
this.off('custom:event', this.handleCustom);
}
});
// Destroy component (cleanup happens automatically)
component.destroy();
π Pro Tip: Use namespaces to organize events by feature or plugin, making it easy to enable/disable entire groups of handlers!
π‘ Real-World Examples
Shopping Cart Events
const cart = new Onigiri.prototype.Component({
data: {
items: [],
total: 0
},
methods: {
addItem(item) {
this.items.push(item);
this.calculateTotal();
this.emit('cart:item-added', item);
},
removeItem(id) {
const item = this.items.find(i => i.id === id);
this.items = this.items.filter(i => i.id !== id);
this.calculateTotal();
this.emit('cart:item-removed', item);
},
calculateTotal() {
this.total = this.items.reduce((sum, item) => sum + item.price, 0);
}
}
});
// Listen to cart events
cart.on('cart:item-added', (item) => {
console.log('π Added:', item.name);
showNotification(\`Added \${item.name} to cart\`);
});
cart.on('cart:item-removed', (item) => {
console.log('Removed:', item.name);
showNotification(\`Removed \${item.name} from cart\`);
});
cart.on('change:total', (newTotal) => {
O('#cart-total').text(\`$\${newTotal.toFixed(2)}\`);
});
Form Validation Events
const form = O('#registration-form');
// Listen to various form events
form.on('submit', async (e) => {
e.preventDefault();
// Emit validation start
O(form).trigger('form:validating');
const result = O(form).validate(rules);
if (result.isValid) {
O(form).trigger('form:valid', result);
await submitForm(formData);
O(form).trigger('form:submitted');
} else {
O(form).trigger('form:invalid', result.errors);
}
});
// Handle validation events
document.addEventListener('form:validating', () => {
O('.submit-button').attr('disabled', true).text('Validating...');
});
document.addEventListener('form:valid', () => {
O('.error').text('').hide();
});
document.addEventListener('form:invalid', (e) => {
const errors = e.detail;
Object.keys(errors).forEach(field => {
O(\`#\${field}-error\`).text(errors[field][0]).show();
});
O('.submit-button').attr('disabled', false).text('Submit');
});
document.addEventListener('form:submitted', () => {
O('.success-message').fadeIn(300);
});
Next Steps
β 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.