v1.0.0

πŸŽͺ 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.