From 984618901d5f7adf84bc58a5f638b6300e18a3b4 Mon Sep 17 00:00:00 2001
From: Ricardo Rito
Date: Thu, 22 Jan 2026 15:30:21 -0600
Subject: [PATCH 1/4] :sparkles: [PB-284] EventsV2: Add examples to events v2
---
examples/README.md | 12 +
examples/basic-usage/BasicUsage.jsx | 4 +-
examples/complete-widget/CompleteWidget.jsx | 2 +
examples/dev/index.html | 127 ++++++++
examples/dev/main.jsx | 194 ++++++++++++
examples/device-selector/DeviceSelector.jsx | 2 +
.../EventVersioningExample.jsx | 204 +++++++++++++
.../real-time-dashboard/RealTimeDashboard.jsx | 2 +
examples/shared/EventEmitterPanel.css | 159 ++++++++++
examples/shared/EventEmitterPanel.jsx | 272 +++++++++++++++++
examples/widget-events-basic/README.md | 137 +++++++++
.../widget-events-basic/WidgetEventsBasic.jsx | 195 ++++++++++++
examples/widget-events-basic/styles.css | 254 ++++++++++++++++
examples/widget-events-multi/README.md | 199 +++++++++++++
.../widget-events-multi/WidgetEventsMulti.jsx | 223 ++++++++++++++
examples/widget-events-multi/styles.css | 277 ++++++++++++++++++
examples/with-hocs/WithHocsExample.jsx | 2 +
17 files changed, 2264 insertions(+), 1 deletion(-)
create mode 100644 examples/dev/index.html
create mode 100644 examples/dev/main.jsx
create mode 100644 examples/event-versioning/EventVersioningExample.jsx
create mode 100644 examples/shared/EventEmitterPanel.css
create mode 100644 examples/shared/EventEmitterPanel.jsx
create mode 100644 examples/widget-events-basic/README.md
create mode 100644 examples/widget-events-basic/WidgetEventsBasic.jsx
create mode 100644 examples/widget-events-basic/styles.css
create mode 100644 examples/widget-events-multi/README.md
create mode 100644 examples/widget-events-multi/WidgetEventsMulti.jsx
create mode 100644 examples/widget-events-multi/styles.css
diff --git a/examples/README.md b/examples/README.md
index f11eea9..c938292 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -24,6 +24,18 @@ Complete example demonstrating all available functionalities.
Examples using Higher-Order Components to inject props.
+### 6. **event-versioning** - Event Versioning
+
+Demonstrates V1 and V2 event compatibility and automatic event emission.
+
+### 7. **widget-events-basic** - Widget Events (Basic)
+
+Shows how to use widget-specific events with the `v2:widget::` pattern. Demonstrates event emission, monitoring, and lifecycle events.
+
+### 8. **widget-events-multi** - Widget Events (Multi-Widget)
+
+Advanced example showing multiple widgets on the same page with isolated event namespaces and inter-widget communication.
+
## How to Use Examples
Each example is an independent React component that you can copy and adapt to your project.
diff --git a/examples/basic-usage/BasicUsage.jsx b/examples/basic-usage/BasicUsage.jsx
index b4babe0..afdc677 100644
--- a/examples/basic-usage/BasicUsage.jsx
+++ b/examples/basic-usage/BasicUsage.jsx
@@ -5,13 +5,13 @@ import {
useUbidotsSelectedDevice,
useUbidotsActions,
} from '@ubidots/react-html-canvas';
+import { EventEmitterPanel } from '../shared/EventEmitterPanel';
import './styles.css';
function DeviceInfo() {
const ready = useUbidotsReady();
const device = useUbidotsSelectedDevice();
const { setDashboardDevice, refreshDashboard } = useUbidotsActions();
-
if (!ready) {
return (
@@ -76,6 +76,8 @@ export function BasicUsage() {
+
+
);
diff --git a/examples/complete-widget/CompleteWidget.jsx b/examples/complete-widget/CompleteWidget.jsx
index ccfb3de..5284584 100644
--- a/examples/complete-widget/CompleteWidget.jsx
+++ b/examples/complete-widget/CompleteWidget.jsx
@@ -14,6 +14,7 @@ import {
useUbidotsActions,
useUbidotsAPI,
} from '@ubidots/react-html-canvas';
+import { EventEmitterPanel } from '../shared/EventEmitterPanel';
import './styles.css';
function DataDisplay() {
@@ -369,6 +370,7 @@ export function CompleteWidget() {
@ubidots/react-html-canvas
+
);
diff --git a/examples/dev/index.html b/examples/dev/index.html
new file mode 100644
index 0000000..708d12c
--- /dev/null
+++ b/examples/dev/index.html
@@ -0,0 +1,127 @@
+
+
+
+
+
+ Ubidots React HTML Canvas - Examples
+
+
+
+
+
+
+
diff --git a/examples/dev/main.jsx b/examples/dev/main.jsx
new file mode 100644
index 0000000..fc220a6
--- /dev/null
+++ b/examples/dev/main.jsx
@@ -0,0 +1,194 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+
+// Import examples
+import BasicUsage from '../basic-usage/BasicUsage.jsx';
+import DeviceSelector from '../device-selector/DeviceSelector.jsx';
+import RealTimeDashboard from '../real-time-dashboard/RealTimeDashboard.jsx';
+import CompleteWidget from '../complete-widget/CompleteWidget.jsx';
+import EventVersioningExample from '../event-versioning/EventVersioningExample.jsx';
+import WidgetEventsBasic from '../widget-events-basic/WidgetEventsBasic.jsx';
+import WidgetEventsMulti from '../widget-events-multi/WidgetEventsMulti.jsx';
+import WithHocsExample from '../with-hocs/WithHocsExample.jsx';
+
+// Get example from URL query parameter
+// eslint-disable-next-line no-undef
+const urlParams = new URLSearchParams(window.location.search);
+const exampleName = urlParams.get('example');
+
+// Map example names to components
+const examples = {
+ 'basic-usage': BasicUsage,
+ 'device-selector': DeviceSelector,
+ 'real-time-dashboard': RealTimeDashboard,
+ 'complete-widget': CompleteWidget,
+ 'event-versioning': EventVersioningExample,
+ 'widget-events-basic': WidgetEventsBasic,
+ 'widget-events-multi': WidgetEventsMulti,
+ 'with-hocs': WithHocsExample,
+};
+
+// Get the component to render
+const ExampleComponent = examples[exampleName];
+
+// Gallery component
+function ExamplesGallery() {
+ return (
+
+ );
+}
+
+// Render the example or show error
+// eslint-disable-next-line no-undef
+const root = ReactDOM.createRoot(document.getElementById('root'));
+
+if (ExampleComponent) {
+ root.render(
+
+
+
+ );
+} else if (!exampleName) {
+ // Show the gallery
+ root.render(
+
+
+
+ );
+} else {
+ root.render(
+
+ );
+}
diff --git a/examples/device-selector/DeviceSelector.jsx b/examples/device-selector/DeviceSelector.jsx
index 3be30e3..c040b29 100644
--- a/examples/device-selector/DeviceSelector.jsx
+++ b/examples/device-selector/DeviceSelector.jsx
@@ -6,6 +6,7 @@ import {
useUbidotsSelectedDevices,
useUbidotsActions,
} from '@ubidots/react-html-canvas';
+import { EventEmitterPanel } from '../shared/EventEmitterPanel';
import './styles.css';
function DeviceCard({ device, isSelected, onSelect }) {
@@ -233,6 +234,7 @@ export function DeviceSelectorExample() {
Interactive device selection with single and multi-select modes
+
);
diff --git a/examples/event-versioning/EventVersioningExample.jsx b/examples/event-versioning/EventVersioningExample.jsx
new file mode 100644
index 0000000..5d0a7fc
--- /dev/null
+++ b/examples/event-versioning/EventVersioningExample.jsx
@@ -0,0 +1,204 @@
+import React, { useEffect, useState } from 'react';
+import {
+ UbidotsProvider,
+ useUbidotsReady,
+ useUbidotsToken,
+ useUbidotsSelectedDevices,
+ useUbidotsActions,
+} from '@ubidots/react-html-canvas';
+import { EventEmitterPanel } from '../shared/EventEmitterPanel';
+
+/**
+ * Example demonstrating V1 and V2 event compatibility
+ *
+ * This example shows:
+ * 1. How V1 events automatically trigger V2 events
+ * 2. How to listen for both V1 and V2 events
+ * 3. How actions send both V1 and V2 events
+ */
+
+function EventLogger() {
+ const [events, setEvents] = useState([]);
+
+ useEffect(() => {
+ function handleMessage(ev) {
+ const { event, payload } = ev.data || {};
+ if (event) {
+ const timestamp = new Date().toLocaleTimeString();
+ setEvents(prev =>
+ [
+ ...prev,
+ { timestamp, event, payload: JSON.stringify(payload) },
+ ].slice(-10)
+ ); // Keep last 10 events
+ }
+ }
+
+ // eslint-disable-next-line no-undef
+ window.addEventListener('message', handleMessage);
+ // eslint-disable-next-line no-undef
+ return () => window.removeEventListener('message', handleMessage);
+ }, []);
+
+ return (
+
+
Event Log (Last 10 events)
+
+
+
+ | Time |
+ Event |
+ Payload |
+
+
+
+ {events.map((e, i) => (
+
+ | {e.timestamp} |
+ {e.event} |
+ {e.payload} |
+
+ ))}
+
+
+
+
+ V1 Events
+ {' '}
+
+ V2 Events
+
+
+
+ );
+}
+
+function WidgetContent() {
+ const ready = useUbidotsReady();
+ const token = useUbidotsToken();
+ const devices = useUbidotsSelectedDevices();
+ const { setDashboardDevice, setDashboardMultipleDevices, setRealTime } =
+ useUbidotsActions();
+
+ if (!ready) {
+ return Waiting for ready state...
;
+ }
+
+ return (
+
+
Event Versioning Demo
+
+
+
Current State
+
+ Token: {token ? '✓ Received' : '✗ Not received'}
+
+
+ Selected Devices: {devices ? devices.length : 0}
+
+
+
+
+
Actions (Send V1 + V2 Events)
+
+ Each button sends BOTH V1 and V2 events. Check the event log below.
+
+
+
+
+
+
+
+
+
+
+
+
+
How it works:
+
+ -
+ When you click a button, the library sends{' '}
+ both V1 and V2 events
+
+ - V1 events (orange) maintain backward compatibility
+ - V2 events (blue) use the new naming convention
+ - External listeners can subscribe to either version
+
+
+
+ );
+}
+
+export default function App() {
+ const handleReady = () => {
+ // eslint-disable-next-line no-console, no-undef
+ console.log('Widget ready!');
+ };
+
+ return (
+
+
+
+
+ );
+}
diff --git a/examples/real-time-dashboard/RealTimeDashboard.jsx b/examples/real-time-dashboard/RealTimeDashboard.jsx
index 7a19d2e..1da62fa 100644
--- a/examples/real-time-dashboard/RealTimeDashboard.jsx
+++ b/examples/real-time-dashboard/RealTimeDashboard.jsx
@@ -7,6 +7,7 @@ import {
useUbidotsDashboardDateRange,
useUbidotsActions,
} from '@ubidots/react-html-canvas';
+import { EventEmitterPanel } from '../shared/EventEmitterPanel';
import './styles.css';
function RealTimeIndicator() {
@@ -323,6 +324,7 @@ export function RealTimeDashboardExample() {
✅ Automatic data updates when real-time is active
+
);
diff --git a/examples/shared/EventEmitterPanel.css b/examples/shared/EventEmitterPanel.css
new file mode 100644
index 0000000..95e9261
--- /dev/null
+++ b/examples/shared/EventEmitterPanel.css
@@ -0,0 +1,159 @@
+/* Event Emitter Panel - Floating */
+.event-panel {
+ position: fixed;
+ bottom: 20px;
+ right: 20px;
+ width: 400px;
+ max-height: 80vh;
+ background: white;
+ border-radius: 12px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
+ z-index: 1000;
+ overflow: hidden;
+ transition: all 0.3s ease;
+}
+
+.event-panel.closed {
+ width: 200px;
+ max-height: 60px;
+}
+
+.event-panel-header {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ padding: 15px 20px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ cursor: pointer;
+ user-select: none;
+}
+
+.event-panel-header h3 {
+ margin: 0;
+ font-size: 16px;
+ font-weight: 600;
+}
+
+.toggle-btn {
+ background: rgba(255, 255, 255, 0.2);
+ border: none;
+ color: white;
+ width: 30px;
+ height: 30px;
+ border-radius: 50%;
+ cursor: pointer;
+ font-size: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: background 0.2s;
+}
+
+.toggle-btn:hover {
+ background: rgba(255, 255, 255, 0.3);
+}
+
+.event-panel-content {
+ max-height: calc(80vh - 60px);
+ overflow-y: auto;
+ padding: 15px;
+}
+
+.last-emitted {
+ background: #e7f3ff;
+ padding: 8px 12px;
+ border-radius: 6px;
+ margin-bottom: 15px;
+ font-family: monospace;
+ font-size: 11px;
+ color: #0066cc;
+ border-left: 3px solid #0066cc;
+}
+
+.event-categories {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+}
+
+.event-category {
+ border-bottom: 1px solid #e0e0e0;
+ padding-bottom: 15px;
+}
+
+.event-category:last-child {
+ border-bottom: none;
+ padding-bottom: 0;
+}
+
+.event-category h4 {
+ margin: 0 0 10px 0;
+ font-size: 13px;
+ color: #555;
+ font-weight: 600;
+}
+
+.event-buttons {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+}
+
+.event-btn {
+ padding: 6px 12px;
+ background: #f0f0f0;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ cursor: pointer;
+ font-size: 11px;
+ transition: all 0.2s;
+ color: #333;
+ font-weight: 500;
+}
+
+.event-btn:hover {
+ background: #667eea;
+ color: white;
+ border-color: #667eea;
+ transform: translateY(-1px);
+ box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
+}
+
+.event-btn:active {
+ transform: translateY(0);
+ box-shadow: 0 1px 4px rgba(102, 126, 234, 0.3);
+}
+
+/* Scrollbar styling */
+.event-panel-content::-webkit-scrollbar {
+ width: 6px;
+}
+
+.event-panel-content::-webkit-scrollbar-track {
+ background: #f1f1f1;
+ border-radius: 10px;
+}
+
+.event-panel-content::-webkit-scrollbar-thumb {
+ background: #888;
+ border-radius: 10px;
+}
+
+.event-panel-content::-webkit-scrollbar-thumb:hover {
+ background: #555;
+}
+
+/* Responsive */
+@media (max-width: 768px) {
+ .event-panel {
+ width: calc(100% - 40px);
+ right: 20px;
+ left: 20px;
+ }
+
+ .event-panel.closed {
+ width: 200px;
+ left: auto;
+ }
+}
diff --git a/examples/shared/EventEmitterPanel.jsx b/examples/shared/EventEmitterPanel.jsx
new file mode 100644
index 0000000..0325b70
--- /dev/null
+++ b/examples/shared/EventEmitterPanel.jsx
@@ -0,0 +1,272 @@
+import React, { useState } from 'react';
+import './EventEmitterPanel.css';
+
+export function EventEmitterPanel() {
+ const [isOpen, setIsOpen] = useState(true);
+ const [lastEmitted, setLastEmitted] = useState('');
+
+ // Get widgetId from window if available, otherwise use a demo ID
+ const widgetId =
+ // eslint-disable-next-line no-undef
+ (typeof window !== 'undefined' && window.widgetId) || 'demo-widget-001';
+
+ const emitEvent = (eventName, payload) => {
+ const message = { event: eventName, payload };
+ // eslint-disable-next-line no-undef
+ window.parent.postMessage(message, '*');
+ setLastEmitted(`${eventName} - ${new Date().toLocaleTimeString()}`);
+ };
+
+ const events = [
+ {
+ category: '⚡ System',
+ items: [
+ {
+ name: 'Ready',
+ event: 'ready',
+ payload: { timestamp: Date.now() },
+ },
+ ],
+ },
+ {
+ category: '🔐 Authentication',
+ items: [
+ {
+ name: 'Token',
+ event: 'receivedToken',
+ payload: 'demo-token-12345',
+ },
+ {
+ name: 'JWT Token',
+ event: 'receivedJWTToken',
+ payload: 'demo-jwt-token-67890',
+ },
+ {
+ name: 'V2 Token',
+ event: 'v2:auth:token',
+ payload: 'demo-v2-token-abc',
+ },
+ {
+ name: 'V2 JWT',
+ event: 'v2:auth:jwt',
+ payload: 'demo-v2-jwt-xyz',
+ },
+ ],
+ },
+ {
+ category: '📱 Device Selection',
+ items: [
+ {
+ name: 'Single Device',
+ event: 'selectedDevice',
+ payload: 'device-001',
+ },
+ {
+ name: 'Multiple Devices',
+ event: 'selectedDevices',
+ payload: [
+ { id: 'device-001', name: 'Sensor 1', label: 'S1' },
+ { id: 'device-002', name: 'Sensor 2', label: 'S2' },
+ ],
+ },
+ {
+ name: 'V2 Self Device',
+ event: 'v2:dashboard:devices:self',
+ payload: { id: 'device-self', name: 'Self Device' },
+ },
+ {
+ name: 'V2 Selected Devices',
+ event: 'v2:dashboard:devices:selected',
+ payload: [{ id: 'device-v2', name: 'V2 Device' }],
+ },
+ ],
+ },
+ {
+ category: '📅 Date & Time',
+ items: [
+ {
+ name: 'Date Range',
+ event: 'selectedDashboardDateRange',
+ payload: {
+ startTime: Date.now() - 86400000,
+ endTime: Date.now(),
+ },
+ },
+ {
+ name: 'V2 Date Range',
+ event: 'v2:dashboard:settings:daterange',
+ payload: {
+ startTime: Date.now() - 3600000,
+ endTime: Date.now(),
+ },
+ },
+ {
+ name: 'Real-time ON',
+ event: 'isRealTimeActive',
+ payload: true,
+ },
+ {
+ name: 'Real-time OFF',
+ event: 'isRealTimeActive',
+ payload: false,
+ },
+ {
+ name: 'V2 Real-time',
+ event: 'v2:dashboard:settings:rt',
+ payload: true,
+ },
+ ],
+ },
+ {
+ category: '🎛️ Dashboard Objects',
+ items: [
+ {
+ name: 'Dashboard Object',
+ event: 'selectedDashboardObject',
+ payload: { id: 'dash-001', name: 'Main Dashboard' },
+ },
+ {
+ name: 'Device Object',
+ event: 'selectedDeviceObject',
+ payload: { id: 'dev-obj-001', name: 'Device Config' },
+ },
+ {
+ name: 'V2 Dashboard Self',
+ event: 'v2:dashboard:self',
+ payload: { id: 'dash-v2', name: 'V2 Dashboard' },
+ },
+ ],
+ },
+ {
+ category: '🔄 Dashboard Actions',
+ items: [
+ {
+ name: 'Refresh Dashboard',
+ event: 'v2:dashboard:settings:refreshed',
+ payload: { timestamp: Date.now() },
+ },
+ ],
+ },
+ {
+ category: '🎨 Filters',
+ items: [
+ {
+ name: 'Selected Filters',
+ event: 'selectedFilters',
+ payload: [
+ { key: 'status', value: 'active' },
+ { key: 'type', value: 'sensor' },
+ ],
+ },
+ {
+ name: 'V2 Filters',
+ event: 'v2:dashboard:settings:filters',
+ payload: [{ filter: 'location', value: 'warehouse-1' }],
+ },
+ ],
+ },
+ {
+ category: '🔧 Widget Events',
+ items: [
+ {
+ name: 'Widget Ready',
+ event: `v2:widget:ready:${widgetId}`,
+ payload: {
+ status: 'ok',
+ features: ['charts', 'realtime', 'export'],
+ timestamp: Date.now(),
+ },
+ },
+ {
+ name: 'Widget Loaded',
+ event: `v2:widget:loaded:${widgetId}`,
+ payload: {
+ timestamp: Date.now(),
+ version: '1.0.0',
+ },
+ },
+ {
+ name: 'Widget Error',
+ event: `v2:widget:error:${widgetId}`,
+ payload: {
+ code: 'DEMO_ERROR',
+ message: 'This is a simulated error',
+ timestamp: Date.now(),
+ },
+ },
+ {
+ name: 'Custom Action',
+ event: `v2:widget:customAction:${widgetId}`,
+ payload: {
+ action: 'export',
+ format: 'csv',
+ timestamp: Date.now(),
+ },
+ },
+ ],
+ },
+ ];
+
+ const handleHeaderClick = () => setIsOpen(!isOpen);
+ const handleHeaderKeyDown = e => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ setIsOpen(!isOpen);
+ }
+ };
+
+ return (
+
+
+
📡 Event Emitter
+
+
+
+ {isOpen && (
+
+ {lastEmitted && (
+
+ Last: {lastEmitted}
+
+ )}
+
+
+ {events.map((category, idx) => (
+
+
{category.category}
+
+ {category.items.map((item, itemIdx) => (
+
+ ))}
+
+
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/examples/widget-events-basic/README.md b/examples/widget-events-basic/README.md
new file mode 100644
index 0000000..e4d338f
--- /dev/null
+++ b/examples/widget-events-basic/README.md
@@ -0,0 +1,137 @@
+# Widget Events - Basic Example
+
+This example demonstrates how to use **widget-specific V2 events** with the format `v2:widget::`.
+
+## Features
+
+- ✅ Emit widget lifecycle events (ready, loaded, error)
+- 📡 Real-time event monitoring
+- 🎯 Widget-specific event isolation
+- 🔍 Event payload inspection
+
+## Event Format
+
+All widget events follow this pattern:
+
+```
+v2:widget::
+```
+
+### Examples:
+
+- `v2:widget:ready:demo-widget-001`
+- `v2:widget:loaded:demo-widget-001`
+- `v2:widget:error:demo-widget-001`
+- `v2:widget:settingsChanged:demo-widget-001`
+
+## Usage
+
+```jsx
+import { UbidotsProvider, useWidgetEvents } from '@ubidots/react-html-canvas';
+
+function MyWidget() {
+ const { emitWidgetEvent } = useWidgetEvents();
+
+ useEffect(() => {
+ // Emit widget ready event
+ emitWidgetEvent('ready', { status: 'ok' });
+ }, []);
+
+ return Widget Content
;
+}
+
+export default function App() {
+ return (
+
+
+
+ );
+}
+```
+
+## Available Events
+
+### Standard Widget Events
+
+- **`ready`** - Widget is fully initialized and ready to use
+- **`loaded`** - Widget has loaded its initial data
+- **`error`** - Widget encountered an error
+- **`settingsChanged`** - Widget settings were modified
+
+### Custom Events
+
+You can emit any custom event type:
+
+```jsx
+emitWidgetEvent('customAction', {
+ action: 'export',
+ format: 'csv',
+});
+// Emits: v2:widget:customAction:my-widget-123
+```
+
+## Listening for Widget Events
+
+External applications can listen for widget events:
+
+```javascript
+window.addEventListener('message', event => {
+ const { event: eventName, payload } = event.data;
+
+ // Check if it's a widget event for a specific widget
+ if (eventName === 'v2:widget:ready:my-widget-123') {
+ console.log('Widget is ready!', payload);
+ }
+
+ // Or check for any widget event
+ if (eventName.startsWith('v2:widget:')) {
+ const [, , eventType, widgetId] = eventName.split(':');
+ console.log(`Widget ${widgetId} emitted ${eventType}`, payload);
+ }
+});
+```
+
+## Key Concepts
+
+### 1. Widget ID
+
+Each widget must have a unique ID passed to the `UbidotsProvider`:
+
+```jsx
+{/* ... */}
+```
+
+### 2. Event Isolation
+
+Events are scoped to specific widgets using the widget ID. This allows:
+
+- Multiple widgets on the same page without event conflicts
+- Targeted event handling for specific widgets
+- Better debugging and monitoring
+
+### 3. Payload
+
+Events can include any JSON-serializable payload:
+
+```jsx
+emitWidgetEvent('dataUpdated', {
+ recordCount: 150,
+ lastUpdate: new Date().toISOString(),
+ source: 'api',
+});
+```
+
+## Running the Example
+
+1. Import the component in your application
+2. Ensure the widget is running inside a Ubidots dashboard or has proper postMessage setup
+3. Open browser DevTools to see events in the console
+4. Click buttons to emit different widget events
+5. Watch the event monitor update in real-time
+
+## Notes
+
+- Widget events are **V2 only** (no V1 equivalent)
+- Events are sent via `window.postMessage` to the parent window
+- The widget ID must be unique across all widgets in the dashboard
+- Event payloads should be kept reasonably small for performance
diff --git a/examples/widget-events-basic/WidgetEventsBasic.jsx b/examples/widget-events-basic/WidgetEventsBasic.jsx
new file mode 100644
index 0000000..ce2ad21
--- /dev/null
+++ b/examples/widget-events-basic/WidgetEventsBasic.jsx
@@ -0,0 +1,195 @@
+import React, { useEffect, useState } from 'react';
+import {
+ UbidotsProvider,
+ useUbidotsReady,
+ useWidgetEvents,
+} from '@ubidots/react-html-canvas';
+import { EventEmitterPanel } from '../shared/EventEmitterPanel';
+import './styles.css';
+
+/**
+ * Example demonstrating Widget-specific V2 events
+ *
+ * This example shows:
+ * 1. How to emit widget events with format: v2:widget::
+ * 2. How to listen for widget-specific events
+ * 3. Widget lifecycle events (ready, loaded, error)
+ */
+
+function EventMonitor({ widgetId }) {
+ const [receivedEvents, setReceivedEvents] = useState([]);
+
+ useEffect(() => {
+ function handleMessage(ev) {
+ const { event, payload } = ev.data || {};
+
+ // Only log widget events for this specific widget
+ if (
+ event &&
+ event.startsWith(`v2:widget:`) &&
+ event.endsWith(`:${widgetId}`)
+ ) {
+ const timestamp = new Date().toLocaleTimeString();
+ const eventType = event.split(':')[2]; // Extract event type
+
+ setReceivedEvents(prev =>
+ [{ timestamp, event, eventType, payload }, ...prev].slice(0, 15)
+ ); // Keep last 15 events
+ }
+ }
+
+ // eslint-disable-next-line no-undef
+ window.addEventListener('message', handleMessage);
+ // eslint-disable-next-line no-undef
+ return () => window.removeEventListener('message', handleMessage);
+ }, [widgetId]);
+
+ return (
+
+
📡 Widget Event Monitor
+
+ Listening for: v2:widget:*:{widgetId}
+
+
+
+ {receivedEvents.length === 0 ? (
+
No events received yet
+ ) : (
+ receivedEvents.map((e, i) => (
+
+ {e.timestamp}
+
+ {e.eventType}
+
+ {JSON.stringify(e.payload)}
+
+ ))
+ )}
+
+
+ );
+}
+
+function WidgetContent({ widgetId }) {
+ const ready = useUbidotsReady();
+ const { emitWidgetEvent } = useWidgetEvents();
+ const [status, setStatus] = useState('initializing');
+
+ useEffect(() => {
+ if (ready) {
+ // Emit 'loaded' event when widget is ready
+ emitWidgetEvent('loaded', {
+ timestamp: Date.now(),
+ version: '1.0.0',
+ });
+ setStatus('ready');
+ }
+ }, [ready, emitWidgetEvent]);
+
+ const handleReady = () => {
+ emitWidgetEvent('ready', {
+ status: 'ok',
+ features: ['charts', 'realtime', 'export'],
+ });
+ setStatus('active');
+ };
+
+ const handleError = () => {
+ emitWidgetEvent('error', {
+ code: 'DEMO_ERROR',
+ message: 'This is a simulated error for demonstration',
+ });
+ setStatus('error');
+ };
+
+ const handleSettingsChange = () => {
+ emitWidgetEvent('settingsChanged', {
+ theme: 'dark',
+ refreshInterval: 5000,
+ showLegend: true,
+ });
+ };
+
+ const handleCustomEvent = () => {
+ emitWidgetEvent('customAction', {
+ action: 'export',
+ format: 'csv',
+ timestamp: Date.now(),
+ });
+ };
+
+ if (!ready) {
+ return (
+
+
🔄 Initializing widget...
+
+ );
+ }
+
+ return (
+
+
+
+ Widget Status: {status}
+
+
+ Widget ID: {widgetId}
+
+
+
+
+
🎯 Emit Widget Events
+
+ Click buttons to emit events with format:{' '}
+ v2:widget:<event>:{widgetId}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export function WidgetEventsBasic() {
+ const widgetId = 'widget-demo-001';
+
+ return (
+
+
+
+ );
+}
+
+export default WidgetEventsBasic;
diff --git a/examples/widget-events-basic/styles.css b/examples/widget-events-basic/styles.css
new file mode 100644
index 0000000..c93b829
--- /dev/null
+++ b/examples/widget-events-basic/styles.css
@@ -0,0 +1,254 @@
+.container {
+ font-family:
+ -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
+ Cantarell, sans-serif;
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 20px;
+}
+
+.header {
+ text-align: center;
+ margin-bottom: 30px;
+ padding: 20px;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ border-radius: 10px;
+}
+
+.header h1 {
+ margin: 0 0 10px 0;
+ font-size: 2em;
+}
+
+.header p {
+ margin: 0;
+ opacity: 0.9;
+}
+
+.main-layout {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 20px;
+ margin-top: 20px;
+}
+
+@media (max-width: 768px) {
+ .main-layout {
+ grid-template-columns: 1fr;
+ }
+}
+
+/* Widget Panel */
+.widget-panel {
+ background: white;
+ border-radius: 8px;
+ padding: 20px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.loading {
+ text-align: center;
+ padding: 40px;
+ color: #666;
+}
+
+.status-bar {
+ background: #f5f5f5;
+ padding: 15px;
+ border-radius: 6px;
+ margin-bottom: 20px;
+}
+
+.status-bar h3 {
+ margin: 0 0 10px 0;
+ font-size: 1.2em;
+}
+
+.status-initializing {
+ color: #ff9800;
+}
+.status-ready {
+ color: #2196f3;
+}
+.status-active {
+ color: #4caf50;
+}
+.status-error {
+ color: #f44336;
+}
+
+.widget-id {
+ margin: 5px 0 0 0;
+ font-size: 0.9em;
+ color: #666;
+}
+
+.widget-id code {
+ background: #e0e0e0;
+ padding: 2px 6px;
+ border-radius: 3px;
+ font-family: 'Courier New', monospace;
+}
+
+.actions-section {
+ margin-top: 20px;
+}
+
+.actions-section h3 {
+ margin: 0 0 10px 0;
+}
+
+.actions-info {
+ font-size: 0.9em;
+ color: #666;
+ margin-bottom: 15px;
+}
+
+.actions-info code {
+ background: #fff3cd;
+ padding: 2px 6px;
+ border-radius: 3px;
+ font-family: 'Courier New', monospace;
+ color: #856404;
+}
+
+.button-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 10px;
+}
+
+.btn {
+ padding: 12px 20px;
+ border: none;
+ border-radius: 6px;
+ font-size: 1em;
+ cursor: pointer;
+ transition: all 0.2s;
+ font-weight: 500;
+}
+
+.btn:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
+}
+
+.btn-success {
+ background: #4caf50;
+ color: white;
+}
+
+.btn-danger {
+ background: #f44336;
+ color: white;
+}
+
+.btn-primary {
+ background: #2196f3;
+ color: white;
+}
+
+.btn-info {
+ background: #00bcd4;
+ color: white;
+}
+
+/* Event Monitor */
+.monitor-panel {
+ background: white;
+ border-radius: 8px;
+ padding: 20px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+}
+
+.event-monitor h3 {
+ margin: 0 0 10px 0;
+}
+
+.monitor-info {
+ font-size: 0.9em;
+ color: #666;
+ margin-bottom: 15px;
+}
+
+.monitor-info code {
+ background: #e3f2fd;
+ padding: 2px 6px;
+ border-radius: 3px;
+ font-family: 'Courier New', monospace;
+ color: #1976d2;
+}
+
+.event-list {
+ max-height: 400px;
+ overflow-y: auto;
+ border: 1px solid #e0e0e0;
+ border-radius: 6px;
+ padding: 10px;
+}
+
+.no-events {
+ text-align: center;
+ padding: 20px;
+ color: #999;
+ font-style: italic;
+}
+
+.event-item {
+ display: grid;
+ grid-template-columns: 80px 120px 1fr;
+ gap: 10px;
+ padding: 8px;
+ margin-bottom: 8px;
+ background: #f9f9f9;
+ border-radius: 4px;
+ font-size: 0.85em;
+ align-items: center;
+}
+
+.event-time {
+ color: #666;
+ font-family: 'Courier New', monospace;
+}
+
+.event-type {
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-weight: 600;
+ text-align: center;
+ font-size: 0.9em;
+}
+
+.event-type-ready {
+ background: #c8e6c9;
+ color: #2e7d32;
+}
+
+.event-type-loaded {
+ background: #bbdefb;
+ color: #1565c0;
+}
+
+.event-type-error {
+ background: #ffcdd2;
+ color: #c62828;
+}
+
+.event-type-settingsChanged {
+ background: #fff9c4;
+ color: #f57f17;
+}
+
+.event-type-customAction {
+ background: #e1bee7;
+ color: #6a1b9a;
+}
+
+.event-payload {
+ font-family: 'Courier New', monospace;
+ color: #333;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
diff --git a/examples/widget-events-multi/README.md b/examples/widget-events-multi/README.md
new file mode 100644
index 0000000..1bcce35
--- /dev/null
+++ b/examples/widget-events-multi/README.md
@@ -0,0 +1,199 @@
+# Widget Events - Multi Widget Example
+
+This example demonstrates multiple widgets on the same page, each with isolated event namespaces using the `v2:widget::` pattern.
+
+## Features
+
+- 🎭 Multiple independent widgets on the same page
+- 🔒 Event isolation per widget using unique IDs
+- 📡 Inter-widget communication via broadcast events
+- 🌐 Global event monitoring across all widgets
+- 🎯 Selective event listening (widgets ignore their own events)
+
+## Event Isolation
+
+Each widget has its own event namespace:
+
+```
+Widget Alpha: v2:widget::widget-alpha
+Widget Beta: v2:widget::widget-beta
+Widget Gamma: v2:widget::widget-gamma
+```
+
+This prevents event conflicts and allows widgets to:
+
+- Emit events without affecting other widgets
+- Listen for events from specific widgets
+- Broadcast messages to all other widgets
+
+## Architecture
+
+```
+┌─────────────────────────────────────────┐
+│ Global Event Monitor │
+│ (Listens to all v2:widget:* events) │
+└─────────────────────────────────────────┘
+ ▲
+ │
+ ┌───────────┼───────────┐
+ │ │ │
+ ┌────▼────┐ ┌───▼─────┐ ┌──▼──────┐
+ │ Widget │ │ Widget │ │ Widget │
+ │ Alpha │ │ Beta │ │ Gamma │
+ └─────────┘ └─────────┘ └─────────┘
+```
+
+## Usage
+
+### Basic Setup
+
+```jsx
+import { UbidotsProvider, useWidgetEvents } from '@ubidots/react-html-canvas';
+
+function Widget({ widgetId }) {
+ const { emitWidgetEvent } = useWidgetEvents();
+
+ const sendMessage = () => {
+ emitWidgetEvent('message', {
+ text: `Hello from ${widgetId}`,
+ });
+ };
+
+ return ;
+}
+
+// Each widget needs its own Provider with unique widgetId
+function App() {
+ return (
+ <>
+
+
+
+
+
+
+
+ >
+ );
+}
+```
+
+### Inter-Widget Communication
+
+Widgets can listen for events from other widgets:
+
+```jsx
+function Widget({ widgetId }) {
+ const [messages, setMessages] = useState([]);
+
+ useEffect(() => {
+ function handleMessage(ev) {
+ const { event, payload } = ev.data || {};
+
+ // Listen for 'message' events from OTHER widgets
+ if (
+ event &&
+ event.startsWith('v2:widget:message:') &&
+ !event.endsWith(`:${widgetId}`)
+ ) {
+ const senderWidgetId = event.split(':')[3];
+ setMessages(prev => [
+ ...prev,
+ {
+ from: senderWidgetId,
+ text: payload.text,
+ },
+ ]);
+ }
+ }
+
+ window.addEventListener('message', handleMessage);
+ return () => window.removeEventListener('message', handleMessage);
+ }, [widgetId]);
+
+ return (
+
+ {messages.map((msg, i) => (
+
+ From {msg.from}: {msg.text}
+
+ ))}
+
+ );
+}
+```
+
+## Event Flow Example
+
+When Widget Alpha sends a message:
+
+```
+1. Widget Alpha calls: emitWidgetEvent('message', { text: 'Hello' })
+2. Event emitted: v2:widget:message:widget-alpha
+3. Global Monitor receives and displays the event
+4. Widget Beta receives the event (different widgetId)
+5. Widget Gamma receives the event (different widgetId)
+6. Widget Alpha ignores it (same widgetId)
+```
+
+## Key Concepts
+
+### 1. Unique Widget IDs
+
+Each widget MUST have a unique `widgetId`:
+
+```jsx
+
+
+
+```
+
+### 2. Event Filtering
+
+Widgets can filter events by:
+
+- **Event type**: `v2:widget:message:*`
+- **Widget ID**: `v2:widget:*:widget-alpha`
+- **Both**: `v2:widget:message:widget-alpha`
+
+### 3. Broadcast Pattern
+
+To broadcast to all widgets:
+
+```jsx
+// Sender
+emitWidgetEvent('broadcast', { data: 'for everyone' });
+
+// Receivers (all other widgets)
+if (
+ event.startsWith('v2:widget:broadcast:') &&
+ !event.endsWith(`:${myWidgetId}`)
+) {
+ // Handle broadcast
+}
+```
+
+## Use Cases
+
+- **Dashboard with multiple charts**: Each chart is a widget that can update others
+- **Collaborative tools**: Widgets share state changes
+- **Event logging**: Central monitor tracks all widget activities
+- **Widget orchestration**: One widget controls others
+
+## Running the Example
+
+```bash
+pnpm install
+pnpm dev
+```
+
+Then interact with the widgets to see:
+
+- Events appearing in the global monitor
+- Messages being received by other widgets
+- Event isolation in action
+
+## See Also
+
+- [Widget Events - Basic Example](../widget-events-basic/) - Single widget events
+- [Event Versioning Example](../event-versioning/) - V1/V2 compatibility
diff --git a/examples/widget-events-multi/WidgetEventsMulti.jsx b/examples/widget-events-multi/WidgetEventsMulti.jsx
new file mode 100644
index 0000000..6ec21cf
--- /dev/null
+++ b/examples/widget-events-multi/WidgetEventsMulti.jsx
@@ -0,0 +1,223 @@
+import React, { useEffect, useState } from 'react';
+import {
+ UbidotsProvider,
+ useUbidotsReady,
+ useWidgetEvents,
+} from '@ubidots/react-html-canvas';
+import { EventEmitterPanel } from '../shared/EventEmitterPanel';
+import './styles.css';
+
+/**
+ * Example demonstrating multiple widgets with isolated events
+ *
+ * This example shows:
+ * 1. Multiple widgets on the same page
+ * 2. Each widget has its own event namespace
+ * 3. Widgets can communicate with each other via events
+ * 4. Event isolation prevents cross-widget interference
+ */
+
+function GlobalEventMonitor() {
+ const [allEvents, setAllEvents] = useState([]);
+
+ useEffect(() => {
+ function handleMessage(ev) {
+ const { event, payload } = ev.data || {};
+
+ // Log all widget events
+ if (event && event.startsWith('v2:widget:')) {
+ const timestamp = new Date().toLocaleTimeString();
+ const parts = event.split(':');
+ const eventType = parts[2];
+ const widgetId = parts[3];
+
+ setAllEvents(prev =>
+ [{ timestamp, event, eventType, widgetId, payload }, ...prev].slice(
+ 0,
+ 20
+ )
+ ); // Keep last 20 events
+ }
+ }
+
+ // eslint-disable-next-line no-undef
+ window.addEventListener('message', handleMessage);
+ // eslint-disable-next-line no-undef
+ return () => window.removeEventListener('message', handleMessage);
+ }, []);
+
+ return (
+
+
🌐 Global Event Monitor
+
All widget events across the page
+
+
+ {allEvents.length === 0 ? (
+
+ No events yet. Interact with widgets below.
+
+ ) : (
+ allEvents.map((e, i) => (
+
+ {e.timestamp}
+
+ {e.widgetId}
+
+ {e.eventType}
+ {JSON.stringify(e.payload)}
+
+ ))
+ )}
+
+
+ );
+}
+
+function Widget({ widgetId, color }) {
+ const ready = useUbidotsReady();
+ const { emitWidgetEvent } = useWidgetEvents();
+ const [messageCount, setMessageCount] = useState(0);
+ const [receivedMessages, setReceivedMessages] = useState([]);
+
+ useEffect(() => {
+ if (ready) {
+ emitWidgetEvent('ready', { color, timestamp: Date.now() });
+ }
+ }, [ready, emitWidgetEvent, color]);
+
+ // Listen for messages from OTHER widgets
+ useEffect(() => {
+ function handleMessage(ev) {
+ const { event, payload } = ev.data || {};
+
+ // Only listen to 'message' events from OTHER widgets
+ if (
+ event &&
+ event.startsWith('v2:widget:message:') &&
+ !event.endsWith(`:${widgetId}`)
+ ) {
+ const senderWidgetId = event.split(':')[3];
+ setReceivedMessages(prev =>
+ [
+ {
+ from: senderWidgetId,
+ text: payload.text,
+ time: new Date().toLocaleTimeString(),
+ },
+ ...prev,
+ ].slice(0, 5)
+ );
+ }
+ }
+
+ // eslint-disable-next-line no-undef
+ window.addEventListener('message', handleMessage);
+ // eslint-disable-next-line no-undef
+ return () => window.removeEventListener('message', handleMessage);
+ }, [widgetId]);
+
+ const sendMessage = () => {
+ const count = messageCount + 1;
+ setMessageCount(count);
+
+ emitWidgetEvent('message', {
+ text: `Hello from ${widgetId}! (Message #${count})`,
+ timestamp: Date.now(),
+ });
+ };
+
+ const sendAction = () => {
+ emitWidgetEvent('action', {
+ type: 'buttonClick',
+ widgetId,
+ timestamp: Date.now(),
+ });
+ };
+
+ if (!ready) {
+ return Loading...
;
+ }
+
+ return (
+
+
+
{widgetId}
+ ● Ready
+
+
+
+
+
+
+
+
+
+ Messages sent: {messageCount}
+
+
+ {receivedMessages.length > 0 && (
+
+
📨 Received Messages
+ {receivedMessages.map((msg, i) => (
+
+ {msg.time}
+ from {msg.from}:
+ {msg.text}
+
+ ))}
+
+ )}
+
+
+ );
+}
+
+function WidgetWrapper({ widgetId, color }) {
+ return (
+
+
+
+ );
+}
+
+export function WidgetEventsMulti() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
💡 How it works
+
+ -
+ Each widget has its own
widgetId
+
+ -
+ Events are namespaced:{' '}
+
v2:widget:<event>:<widgetId>
+
+ - Widgets can broadcast messages to all other widgets
+ - The global monitor shows all events from all widgets
+ - Each widget only receives messages from OTHER widgets
+
+
+
+
+ );
+}
+
+export default WidgetEventsMulti;
diff --git a/examples/widget-events-multi/styles.css b/examples/widget-events-multi/styles.css
new file mode 100644
index 0000000..2dcf66c
--- /dev/null
+++ b/examples/widget-events-multi/styles.css
@@ -0,0 +1,277 @@
+.app-container {
+ font-family:
+ -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
+ Cantarell, sans-serif;
+ max-width: 1400px;
+ margin: 0 auto;
+ padding: 20px;
+ background: #f5f5f5;
+}
+
+.app-header {
+ text-align: center;
+ padding: 30px;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ border-radius: 12px;
+ margin-bottom: 30px;
+}
+
+.app-header h1 {
+ margin: 0 0 10px 0;
+ font-size: 2.5em;
+}
+
+.app-header p {
+ margin: 0;
+ opacity: 0.9;
+ font-size: 1.1em;
+}
+
+/* Global Monitor */
+.global-monitor {
+ background: white;
+ border-radius: 10px;
+ padding: 25px;
+ margin-bottom: 30px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+.global-monitor h2 {
+ margin: 0 0 5px 0;
+ color: #333;
+}
+
+.monitor-subtitle {
+ margin: 0 0 20px 0;
+ color: #666;
+ font-size: 0.9em;
+}
+
+.event-stream {
+ max-height: 300px;
+ overflow-y: auto;
+ background: #f8f9fa;
+ border-radius: 6px;
+ padding: 10px;
+}
+
+.no-events {
+ text-align: center;
+ padding: 40px;
+ color: #999;
+ font-style: italic;
+}
+
+.event-row {
+ display: grid;
+ grid-template-columns: 80px 120px 120px 1fr;
+ gap: 10px;
+ padding: 10px;
+ margin-bottom: 5px;
+ background: white;
+ border-radius: 4px;
+ font-size: 0.85em;
+ align-items: center;
+}
+
+.event-time {
+ color: #666;
+ font-family: 'Courier New', monospace;
+}
+
+.widget-badge {
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-weight: 600;
+ font-size: 0.85em;
+ text-align: center;
+ color: white;
+}
+
+.widget-widget-alpha {
+ background: #4caf50;
+}
+.widget-widget-beta {
+ background: #2196f3;
+}
+.widget-widget-gamma {
+ background: #ff9800;
+}
+
+.event-name {
+ font-family: 'Courier New', monospace;
+ color: #495057;
+ font-weight: 500;
+}
+
+.event-data {
+ font-family: 'Courier New', monospace;
+ color: #6c757d;
+ font-size: 0.9em;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* Widgets Grid */
+.widgets-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 20px;
+ margin-bottom: 30px;
+}
+
+/* Widget Card */
+.widget-card {
+ background: white;
+ border-radius: 10px;
+ overflow: hidden;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+ border: 3px solid;
+ transition: transform 0.2s;
+}
+
+.widget-card:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.15);
+}
+
+.widget-header {
+ padding: 15px 20px;
+ color: white;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.widget-header h3 {
+ margin: 0;
+ font-size: 1.2em;
+}
+
+.widget-status {
+ font-size: 0.9em;
+ opacity: 0.9;
+}
+
+.widget-body {
+ padding: 20px;
+}
+
+.widget-loading {
+ padding: 40px;
+ text-align: center;
+ color: #999;
+}
+
+.widget-actions {
+ display: flex;
+ gap: 10px;
+ margin-bottom: 15px;
+}
+
+.widget-actions button {
+ flex: 1;
+ padding: 10px 15px;
+ border: none;
+ border-radius: 6px;
+ font-size: 0.9em;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s;
+}
+
+.btn-send {
+ background: #28a745;
+ color: white;
+}
+
+.btn-send:hover {
+ background: #218838;
+ transform: scale(1.05);
+}
+
+.btn-action {
+ background: #6c757d;
+ color: white;
+}
+
+.btn-action:hover {
+ background: #5a6268;
+ transform: scale(1.05);
+}
+
+.message-counter {
+ padding: 10px;
+ background: #f8f9fa;
+ border-radius: 4px;
+ text-align: center;
+ margin-bottom: 15px;
+ color: #495057;
+}
+
+.received-messages {
+ border-top: 2px solid #e9ecef;
+ padding-top: 15px;
+}
+
+.received-messages h4 {
+ margin: 0 0 10px 0;
+ font-size: 1em;
+ color: #495057;
+}
+
+.message-item {
+ padding: 8px;
+ background: #e7f3ff;
+ border-left: 3px solid #2196f3;
+ border-radius: 4px;
+ margin-bottom: 8px;
+ font-size: 0.85em;
+}
+
+.message-time {
+ color: #666;
+ font-family: 'Courier New', monospace;
+ margin-right: 8px;
+}
+
+.message-from {
+ color: #2196f3;
+ font-weight: 600;
+ margin-right: 8px;
+}
+
+.message-text {
+ color: #333;
+}
+
+/* Info Panel */
+.info-panel {
+ background: white;
+ border-radius: 10px;
+ padding: 25px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+.info-panel h3 {
+ margin: 0 0 15px 0;
+ color: #333;
+}
+
+.info-panel ul {
+ margin: 0;
+ padding-left: 20px;
+ color: #495057;
+ line-height: 1.8;
+}
+
+.info-panel code {
+ background: #f8f9fa;
+ padding: 2px 6px;
+ border-radius: 3px;
+ font-family: 'Courier New', monospace;
+ color: #e83e8c;
+ font-size: 0.9em;
+}
diff --git a/examples/with-hocs/WithHocsExample.jsx b/examples/with-hocs/WithHocsExample.jsx
index c2ef7f0..dc4ea28 100644
--- a/examples/with-hocs/WithHocsExample.jsx
+++ b/examples/with-hocs/WithHocsExample.jsx
@@ -5,6 +5,7 @@ import {
withUbidotsActions,
compose,
} from '@ubidots/react-html-canvas';
+import { EventEmitterPanel } from '../shared/EventEmitterPanel';
import './styles.css';
function DeviceDisplay({ selectedDevice, title = 'Device Information' }) {
@@ -286,6 +287,7 @@ export function WithHocsExample() {
+
);
From bada000c559b5a03efdd8928c70a7e7e46b75687 Mon Sep 17 00:00:00 2001
From: Ricardo Rito
Date: Thu, 22 Jan 2026 15:31:07 -0600
Subject: [PATCH 2/4] :sparkles: [PB-284] EventsV2: Add suport to events v2
---
README.md | 19 +++-
docs/EventVersioning.md | 154 ++++++++++++++++++++++++++++++
package.json | 1 +
src/context/UbidotsReducer.ts | 8 +-
src/context/actions.ts | 65 ++++++++++---
src/context/constants.ts | 31 ++++++
src/context/messageHandlers.ts | 109 ++++++++++++++++++++-
src/context/ubidots.tsx | 12 ++-
src/hooks/index.ts | 1 +
src/hooks/useUbidotsSelections.ts | 5 +
src/hooks/useWidgetEvents.ts | 135 ++++++++++++++++++++++++++
src/types/index.ts | 22 ++++-
vite.examples.config.ts | 24 +++++
13 files changed, 568 insertions(+), 18 deletions(-)
create mode 100644 docs/EventVersioning.md
create mode 100644 src/hooks/useWidgetEvents.ts
create mode 100644 vite.examples.config.ts
diff --git a/README.md b/README.md
index 425fc35..fbf3e6b 100644
--- a/README.md
+++ b/README.md
@@ -98,14 +98,31 @@ See the [docs/](./docs/) folder for comprehensive API documentation:
### 🎯 Working Examples
-See the [examples/](./examples/) folder for complete working examples:
+#### Running Examples Locally
+
+```bash
+# Install dependencies
+pnpm install
+
+# Start the examples server
+pnpm run dev:examples
+```
+
+This will open `http://localhost:3000` with an interactive gallery of all examples.
+
+#### Available Examples
- **[Basic Usage](./examples/basic-usage/)** - Simple setup and device display
- **[Device Selector](./examples/device-selector/)** - Interactive device selection with single/multi-select
- **[Real-time Dashboard](./examples/real-time-dashboard/)** - Live data streaming and controls
- **[Complete Widget](./examples/complete-widget/)** - Comprehensive example testing all features
+- **[Event Versioning](./examples/event-versioning/)** - V1 and V2 event compatibility
+- **[Widget Events - Basic](./examples/widget-events-basic/)** ⭐ NEW - Widget-specific events with `v2:widget::` pattern
+- **[Widget Events - Multi](./examples/widget-events-multi/)** ⭐ NEW - Multiple widgets with isolated event namespaces
- **[With HOCs](./examples/with-hocs/)** - Higher-Order Components usage
+📖 **[Full Guide: How to Run Examples](./examples/RUNNING_EXAMPLES.md)**
+
## Configurable Ready State
You can control which events must occur before considering the system "ready":
diff --git a/docs/EventVersioning.md b/docs/EventVersioning.md
new file mode 100644
index 0000000..c21c341
--- /dev/null
+++ b/docs/EventVersioning.md
@@ -0,0 +1,154 @@
+# Event Versioning (V1 & V2)
+
+This library supports both V1 (legacy) and V2 event naming conventions for Ubidots dashboard communication.
+
+## Overview
+
+When V1 events are received or sent, the library **automatically emits the corresponding V2 events** as well. This ensures backward compatibility while supporting the new V2 event structure.
+
+## Event Mapping
+
+### Auth Events
+
+| V1 Event | V2 Event | Description |
+| ------------------ | --------------- | ----------------------------- |
+| `receivedToken` | `v2:auth:token` | Authentication token received |
+| `receivedJWTToken` | `v2:auth:jwt` | JWT token received |
+
+### Dashboard Events
+
+| V1 Event | V2 Event | Description |
+| ---------------------------- | --------------------------------- | ------------------------------------------------- |
+| `selectedDevice` | `v2:dashboard:devices:selected` | Single device selected (converted to array in V2) |
+| `selectedDevices` | `v2:dashboard:devices:selected` | Multiple devices selected |
+| `selectedDashboardDateRange` | `v2:dashboard:settings:daterange` | Date range selected |
+| `selectedDashboardObject` | `v2:dashboard:self` | Dashboard object received |
+| `selectedFilters` | `v2:dashboard:settings:filters` | Filters selected |
+| `isRealTimeActive` | `v2:dashboard:settings:rt` | Real-time status |
+
+### Outbound Events
+
+| V1 Event | V2 Event | Description |
+| ----------------------------- | ---------------------------------- | -------------------- |
+| `setDashboardDevice` | `v2:dashboard:devices:selected` | Set single device |
+| `setDashboardMultipleDevices` | `v2:dashboard:devices:selected` | Set multiple devices |
+| `setDashboardDateRange` | `v2:dashboard:settings:daterange` | Set date range |
+| `setRealTime` | `v2:dashboard:settings:rt` | Set real-time mode |
+| `refreshDashboard` | `v2:dashboard:settings:refreshed` | Refresh dashboard |
+| `setFullScreen` | `v2:dashboard:settings:fullscreen` | Set fullscreen mode |
+| `openDrawer` | `v2:dashboard:drawer:open` | Open drawer |
+
+## How It Works
+
+### Inbound Events (Receiving)
+
+When the library receives a V1 event, it:
+
+1. Processes the event normally (updates state, triggers callbacks)
+2. **Automatically emits the corresponding V2 event** to the parent window
+
+Example:
+
+```typescript
+// When 'receivedToken' arrives:
+// 1. Updates state.token
+// 2. Emits 'v2:auth:token' to parent window
+```
+
+The library can also receive V2 events directly and process them the same way.
+
+### Outbound Events (Sending)
+
+When you call an action (e.g., `setDashboardDevice`), the library:
+
+1. Sends the V1 event to the parent window
+2. **Automatically sends the corresponding V2 event** as well
+
+Example:
+
+```typescript
+const { setDashboardDevice } = useUbidotsActions();
+
+// This sends BOTH:
+// - 'setDashboardDevice' with deviceId
+// - 'v2:dashboard:devices:selected' with [{ id: deviceId }]
+setDashboardDevice('device-123');
+```
+
+## Data Format Differences
+
+### Single Device vs Array
+
+V1 uses a single device or device ID string, while V2 always uses an array:
+
+```typescript
+// V1
+setDashboardDevice('device-123');
+// Sends: { event: 'setDashboardDevice', payload: 'device-123' }
+
+// V2 (automatically sent)
+// Sends: { event: 'v2:dashboard:devices:selected', payload: [{ id: 'device-123' }] }
+```
+
+## Usage
+
+No changes are required in your code! The library handles V2 events automatically:
+
+```tsx
+import { UbidotsProvider, useUbidotsActions } from '@ubidots/react-html-canvas';
+
+function MyWidget() {
+ const { setDashboardDevice } = useUbidotsActions();
+
+ // This automatically sends both V1 and V2 events
+ const handleClick = () => {
+ setDashboardDevice('device-123');
+ };
+
+ return ;
+}
+
+export default function App() {
+ return (
+
+
+
+ );
+}
+```
+
+## Ready Events
+
+You can use either V1 or V2 event names in the `readyEvents` prop:
+
+```tsx
+// Using V1 events (legacy)
+
+
+
+
+// Using V2 events
+
+
+
+
+// Mixing both (not recommended, but supported)
+
+
+
+```
+
+## Migration Guide
+
+If you're migrating from V1 to V2:
+
+1. **No immediate action required** - V1 events continue to work
+2. **Gradual migration** - Update your event listeners to use V2 event names
+3. **Update readyEvents** - Switch to V2 event names in your `UbidotsProvider`
+4. **Test thoroughly** - Ensure both V1 and V2 events are being received correctly
+
+## Notes
+
+- Widget events (`v2:widget:*`) are **not** automatically emitted by this library
+- The library maintains full backward compatibility with V1 events
+- Both V1 and V2 events can be received and processed simultaneously
diff --git a/package.json b/package.json
index a23a6cc..d8a8934 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"build": "vite build",
"build:watch": "vite build --watch",
"dev": "vite",
+ "dev:examples": "vite --config vite.examples.config.ts",
"serve": "python3 -m http.server 8080 --directory dist",
"serve:tunnel": "pnpm run serve & cloudflared tunnel --url http://localhost:8080",
"dev:cdn": "concurrently \"pnpm run build:watch\" \"pnpm run serve\"",
diff --git a/src/context/UbidotsReducer.ts b/src/context/UbidotsReducer.ts
index e16e8e4..5dff7f4 100644
--- a/src/context/UbidotsReducer.ts
+++ b/src/context/UbidotsReducer.ts
@@ -23,6 +23,7 @@ export const initialState: UbidotsState = {
selectedFilters: null,
realTime: null,
widget: null,
+ widgetId: null,
};
/**
@@ -80,6 +81,10 @@ const reducerHandlers: Record<
...state,
widget: action.payload as WidgetInfo | null,
}),
+ [ACTION_TYPES.SET_WIDGET_ID]: (state, action) => ({
+ ...state,
+ widgetId: action.payload as string | null,
+ }),
};
export function ubidotsReducer(
@@ -87,5 +92,6 @@ export function ubidotsReducer(
action: UbidotsAction
): UbidotsState {
const handler = reducerHandlers[action.type];
- return handler ? handler(state, action) : state;
+ const r = handler ? handler(state, action) : state;
+ return r;
}
diff --git a/src/context/actions.ts b/src/context/actions.ts
index 8627cd5..c7727c0 100644
--- a/src/context/actions.ts
+++ b/src/context/actions.ts
@@ -1,5 +1,5 @@
import type { DateRange, OutboundActions } from '@/types';
-import { OUTBOUND_EVENTS } from './constants';
+import { OUTBOUND_EVENTS, OUTBOUND_EVENTS_V2 } from './constants';
/**
* Post message to parent window
@@ -10,6 +10,18 @@ function postMessage(event: string, payload?: unknown): void {
}
}
+/**
+ * Post both V1 and V2 events to parent window
+ */
+function postMessageWithV2(
+ eventV1: string,
+ eventV2: string,
+ payload?: unknown
+): void {
+ postMessage(eventV1, payload);
+ postMessage(eventV2, payload);
+}
+
/**
* Generate authentication headers based on available tokens
*/
@@ -64,36 +76,65 @@ function validateDateRange(range: DateRange): boolean {
/**
* Action creators using object mapping
+ * Each action emits both V1 and V2 events for backward compatibility
*/
const actionCreators = {
- setDashboardDevice: (deviceId: string) =>
- postMessage(OUTBOUND_EVENTS.SET_DASHBOARD_DEVICE, deviceId),
+ setDashboardDevice: (deviceId: string) => {
+ // Convert single device to array for V2
+ postMessage(OUTBOUND_EVENTS.SET_DASHBOARD_DEVICE, deviceId);
+ postMessage(OUTBOUND_EVENTS_V2.SET_DASHBOARD_DEVICE, [{ id: deviceId }]);
+ },
- setDashboardMultipleDevices: (deviceIds: string[]) =>
- postMessage(OUTBOUND_EVENTS.SET_DASHBOARD_MULTIPLE_DEVICES, deviceIds),
+ setDashboardMultipleDevices: (deviceIds: string[]) => {
+ // Convert string array to Device array for V2
+ const devices = deviceIds.map(id => ({ id }));
+ postMessage(OUTBOUND_EVENTS.SET_DASHBOARD_MULTIPLE_DEVICES, deviceIds);
+ postMessage(OUTBOUND_EVENTS_V2.SET_DASHBOARD_MULTIPLE_DEVICES, devices);
+ },
setDashboardDateRange: (range: DateRange) => {
if (!validateDateRange(range)) return;
- postMessage(OUTBOUND_EVENTS.SET_DASHBOARD_DATE_RANGE, range);
+ postMessageWithV2(
+ OUTBOUND_EVENTS.SET_DASHBOARD_DATE_RANGE,
+ OUTBOUND_EVENTS_V2.SET_DASHBOARD_DATE_RANGE,
+ range
+ );
},
- setDashboardLayer: (layerId: string) =>
- postMessage(OUTBOUND_EVENTS.SET_DASHBOARD_LAYER, layerId),
+ setDashboardLayer: (layerId: string) => {
+ // V2 doesn't have a direct equivalent for layer
+ postMessage(OUTBOUND_EVENTS.SET_DASHBOARD_LAYER, layerId);
+ },
- setRealTime: (rt: boolean) => postMessage(OUTBOUND_EVENTS.SET_REAL_TIME, rt),
+ setRealTime: (rt: boolean) =>
+ postMessageWithV2(
+ OUTBOUND_EVENTS.SET_REAL_TIME,
+ OUTBOUND_EVENTS_V2.SET_REAL_TIME,
+ rt
+ ),
- refreshDashboard: () => postMessage(OUTBOUND_EVENTS.REFRESH_DASHBOARD),
+ refreshDashboard: () =>
+ postMessageWithV2(
+ OUTBOUND_EVENTS.REFRESH_DASHBOARD,
+ OUTBOUND_EVENTS_V2.REFRESH_DASHBOARD
+ ),
openDrawer: (opts: { url: string; width: number }) => {
const id =
typeof window !== 'undefined'
? (window as unknown as Record).widgetId
: 'react-widget';
- postMessage(OUTBOUND_EVENTS.OPEN_DRAWER, { drawerInfo: opts, id });
+ const payload = { drawerInfo: opts, id };
+ postMessage(OUTBOUND_EVENTS.OPEN_DRAWER, payload);
+ postMessage(OUTBOUND_EVENTS_V2.OPEN_DRAWER, payload);
},
setFullScreen: (setting: 'toggle' | 'enable' | 'disable') =>
- postMessage(OUTBOUND_EVENTS.SET_FULL_SCREEN, setting),
+ postMessageWithV2(
+ OUTBOUND_EVENTS.SET_FULL_SCREEN,
+ OUTBOUND_EVENTS_V2.SET_FULL_SCREEN,
+ setting
+ ),
};
/**
diff --git a/src/context/constants.ts b/src/context/constants.ts
index e666a85..21d5056 100644
--- a/src/context/constants.ts
+++ b/src/context/constants.ts
@@ -4,6 +4,8 @@
import type { ReadyEvent } from '@/types';
+// ==================== V1 Events (Legacy) ====================
+
export const INBOUND_EVENTS = {
RECEIVED_TOKEN: 'receivedToken',
RECEIVED_JWT_TOKEN: 'receivedJWTToken',
@@ -28,6 +30,34 @@ export const OUTBOUND_EVENTS = {
SET_FULL_SCREEN: 'setFullScreen',
} as const;
+// ==================== V2 Events ====================
+
+export const INBOUND_EVENTS_V2 = {
+ // Auth Events
+ TOKEN: 'v2:auth:token',
+ JWT: 'v2:auth:jwt',
+
+ // Dashboard Events
+ DEVICES_ALL: 'v2:dashboard:devices:self',
+ SELECTED_DEVICES: 'v2:dashboard:devices:selected',
+ SELECTED_DATE_RANGE: 'v2:dashboard:settings:daterange',
+ REFRESH_DASHBOARD: 'v2:dashboard:settings:refreshed',
+ REALTIME_ACTIVE: 'v2:dashboard:settings:rt',
+ SELECTED_DASHBOARD_OBJECT: 'v2:dashboard:self',
+ SELECTED_FILTERS: 'v2:dashboard:settings:filters',
+} as const;
+
+export const OUTBOUND_EVENTS_V2 = {
+ // Dashboard Events
+ SET_DASHBOARD_DEVICE: 'v2:dashboard:devices:selected',
+ SET_DASHBOARD_MULTIPLE_DEVICES: 'v2:dashboard:devices:selected',
+ SET_DASHBOARD_DATE_RANGE: 'v2:dashboard:settings:daterange',
+ SET_REAL_TIME: 'v2:dashboard:settings:rt',
+ REFRESH_DASHBOARD: 'v2:dashboard:settings:refreshed',
+ SET_FULL_SCREEN: 'v2:dashboard:settings:fullscreen',
+ OPEN_DRAWER: 'v2:dashboard:drawer:open',
+} as const;
+
export const ACTION_TYPES = {
RECEIVED_TOKEN: 'RECEIVED_TOKEN',
RECEIVED_JWT_TOKEN: 'RECEIVED_JWT_TOKEN',
@@ -41,6 +71,7 @@ export const ACTION_TYPES = {
REAL_TIME_STATUS: 'REAL_TIME_STATUS',
SET_READY: 'SET_READY',
SET_WIDGET: 'SET_WIDGET',
+ SET_WIDGET_ID: 'SET_WIDGET_ID',
} as const;
export const DEFAULT_READY_EVENTS: ReadyEvent[] = ['receivedToken'];
diff --git a/src/context/messageHandlers.ts b/src/context/messageHandlers.ts
index af6ddec..dd69ce9 100644
--- a/src/context/messageHandlers.ts
+++ b/src/context/messageHandlers.ts
@@ -9,7 +9,17 @@ import type {
FilterValue,
ReadyEvent,
} from '@/types';
-import { INBOUND_EVENTS, ACTION_TYPES } from './constants';
+import { INBOUND_EVENTS, INBOUND_EVENTS_V2, ACTION_TYPES } from './constants';
+
+/**
+ * Post V2 event to parent window
+ * This is used to emit V2 events when V1 events are processed
+ */
+function emitV2Event(event: string, payload?: unknown): void {
+ if (typeof window !== 'undefined' && window.parent) {
+ window.parent.postMessage({ event, payload }, '*');
+ }
+}
/**
* Message handler function type
@@ -24,9 +34,12 @@ type MessageHandler = (
* Individual message handlers
*/
const messageHandlers: Record = {
+ // ==================== V1 Auth Events ====================
[INBOUND_EVENTS.RECEIVED_TOKEN]: (payload, dispatch, satisfiedEventsRef) => {
dispatch({ type: ACTION_TYPES.RECEIVED_TOKEN, payload: payload as string });
satisfiedEventsRef.current.add('receivedToken');
+ // Emit V2 event
+ emitV2Event(INBOUND_EVENTS_V2.TOKEN, payload);
},
[INBOUND_EVENTS.RECEIVED_JWT_TOKEN]: (
@@ -39,8 +52,11 @@ const messageHandlers: Record = {
payload: payload as string,
});
satisfiedEventsRef.current.add('receivedJWTToken');
+ // Emit V2 event
+ emitV2Event(INBOUND_EVENTS_V2.JWT, payload);
},
+ // ==================== V1 Dashboard Events ====================
[INBOUND_EVENTS.SELECTED_DEVICE]: (payload, dispatch, satisfiedEventsRef) => {
let validatedPayload: Device | null = null;
if (typeof payload === 'string') {
@@ -50,6 +66,10 @@ const messageHandlers: Record = {
dispatch({ type: ACTION_TYPES.SELECTED_DEVICE, payload: validatedPayload });
satisfiedEventsRef.current.add('selectedDevice');
+ // Emit V2 event - convert single device to array format
+ if (validatedPayload) {
+ emitV2Event(INBOUND_EVENTS_V2.SELECTED_DEVICES, [validatedPayload]);
+ }
},
[INBOUND_EVENTS.SELECTED_DEVICES]: (
@@ -62,6 +82,8 @@ const messageHandlers: Record = {
payload: payload as Device[] | null,
});
satisfiedEventsRef.current.add('selectedDevices');
+ // Emit V2 event
+ emitV2Event(INBOUND_EVENTS_V2.SELECTED_DEVICES, payload);
},
[INBOUND_EVENTS.SELECTED_DASHBOARD_DATE_RANGE]: (
@@ -74,6 +96,8 @@ const messageHandlers: Record = {
payload: payload as DateRange | null,
});
satisfiedEventsRef.current.add('selectedDashboardDateRange');
+ // Emit V2 event
+ emitV2Event(INBOUND_EVENTS_V2.SELECTED_DATE_RANGE, payload);
},
[INBOUND_EVENTS.SELECTED_DASHBOARD_OBJECT]: (
@@ -86,6 +110,8 @@ const messageHandlers: Record = {
payload: payload as DashboardObject | null,
});
satisfiedEventsRef.current.add('selectedDashboardObject');
+ // Emit V2 event
+ emitV2Event(INBOUND_EVENTS_V2.SELECTED_DASHBOARD_OBJECT, payload);
},
[INBOUND_EVENTS.SELECTED_DEVICE_OBJECT]: (
@@ -98,6 +124,7 @@ const messageHandlers: Record = {
payload: payload as DeviceObject | null,
});
satisfiedEventsRef.current.add('selectedDeviceObject');
+ // Note: No V2 equivalent for SELECTED_DEVICE_OBJECT
},
[INBOUND_EVENTS.SELECTED_DEVICE_OBJECTS]: (
@@ -110,6 +137,7 @@ const messageHandlers: Record = {
payload: payload as DeviceObject[] | null,
});
satisfiedEventsRef.current.add('selectedDeviceObjects');
+ // Note: No V2 equivalent for SELECTED_DEVICE_OBJECTS
},
[INBOUND_EVENTS.SELECTED_FILTERS]: (
@@ -122,12 +150,91 @@ const messageHandlers: Record = {
payload: payload as FilterValue[] | null,
});
satisfiedEventsRef.current.add('selectedFilters');
+ // Emit V2 event
+ emitV2Event(INBOUND_EVENTS_V2.SELECTED_FILTERS, payload);
},
[INBOUND_EVENTS.IS_REAL_TIME_ACTIVE]: (
payload,
dispatch,
satisfiedEventsRef
+ ) => {
+ dispatch({
+ type: ACTION_TYPES.REAL_TIME_STATUS,
+ payload: payload as boolean | null,
+ });
+ satisfiedEventsRef.current.add('isRealTimeActive');
+ // Emit V2 event
+ emitV2Event(INBOUND_EVENTS_V2.REALTIME_ACTIVE, payload);
+ },
+
+ // ==================== V2 Events (direct handlers) ====================
+ // These allow the system to also receive V2 events directly
+ [INBOUND_EVENTS_V2.TOKEN]: (payload, dispatch, satisfiedEventsRef) => {
+ dispatch({ type: ACTION_TYPES.RECEIVED_TOKEN, payload: payload as string });
+ satisfiedEventsRef.current.add('receivedToken');
+ },
+
+ [INBOUND_EVENTS_V2.JWT]: (payload, dispatch, satisfiedEventsRef) => {
+ dispatch({
+ type: ACTION_TYPES.RECEIVED_JWT_TOKEN,
+ payload: payload as string,
+ });
+ satisfiedEventsRef.current.add('receivedJWTToken');
+ },
+
+ [INBOUND_EVENTS_V2.SELECTED_DEVICES]: (
+ payload,
+ dispatch,
+ satisfiedEventsRef
+ ) => {
+ dispatch({
+ type: ACTION_TYPES.SELECTED_DEVICES,
+ payload: payload as Device[] | null,
+ });
+ satisfiedEventsRef.current.add('selectedDevices');
+ },
+
+ [INBOUND_EVENTS_V2.SELECTED_DATE_RANGE]: (
+ payload,
+ dispatch,
+ satisfiedEventsRef
+ ) => {
+ dispatch({
+ type: ACTION_TYPES.SELECTED_DASHBOARD_DATE_RANGE,
+ payload: payload as DateRange | null,
+ });
+ satisfiedEventsRef.current.add('selectedDashboardDateRange');
+ },
+
+ [INBOUND_EVENTS_V2.SELECTED_DASHBOARD_OBJECT]: (
+ payload,
+ dispatch,
+ satisfiedEventsRef
+ ) => {
+ dispatch({
+ type: ACTION_TYPES.SELECTED_DASHBOARD_OBJECT,
+ payload: payload as DashboardObject | null,
+ });
+ satisfiedEventsRef.current.add('selectedDashboardObject');
+ },
+
+ [INBOUND_EVENTS_V2.SELECTED_FILTERS]: (
+ payload,
+ dispatch,
+ satisfiedEventsRef
+ ) => {
+ dispatch({
+ type: ACTION_TYPES.SELECTED_FILTERS,
+ payload: payload as FilterValue[] | null,
+ });
+ satisfiedEventsRef.current.add('selectedFilters');
+ },
+
+ [INBOUND_EVENTS_V2.REALTIME_ACTIVE]: (
+ payload,
+ dispatch,
+ satisfiedEventsRef
) => {
dispatch({
type: ACTION_TYPES.REAL_TIME_STATUS,
diff --git a/src/context/ubidots.tsx b/src/context/ubidots.tsx
index 9d39d35..471625f 100644
--- a/src/context/ubidots.tsx
+++ b/src/context/ubidots.tsx
@@ -18,7 +18,7 @@ import type { UbidotsState, UbidotsActions, ReadyEvent } from '@/types';
import { initialState, ubidotsReducer } from './UbidotsReducer';
import { handleInboundMessage, checkReadyState } from './messageHandlers';
import { createActions } from './actions';
-import { DEFAULT_READY_EVENTS } from './constants';
+import { ACTION_TYPES, DEFAULT_READY_EVENTS } from './constants';
export interface UbidotsContextValue {
state: UbidotsState;
@@ -50,6 +50,7 @@ export interface UbidotsProviderProps {
readyEvents?: ReadyEvent[];
validateOrigin?: (origin: string) => boolean;
initialStateOverride?: Partial;
+ widgetId?: string;
}
export function UbidotsProvider({
@@ -58,6 +59,7 @@ export function UbidotsProvider({
readyEvents = DEFAULT_READY_EVENTS,
validateOrigin,
initialStateOverride,
+ widgetId,
}: UbidotsProviderProps) {
const [state, dispatch] = useReducer(ubidotsReducer, {
...initialState,
@@ -66,6 +68,14 @@ export function UbidotsProvider({
const readyRef = useRef(false);
const satisfiedEventsRef = useRef(new Set());
+ // Set widgetId on window and in state
+ useEffect(() => {
+ if (widgetId) {
+ (window as unknown as Record).widgetId = widgetId;
+ dispatch({ type: ACTION_TYPES.SET_WIDGET_ID, payload: widgetId });
+ }
+ }, [widgetId]);
+
const isOriginValid = useCallback(
(origin: string) => (validateOrigin ? validateOrigin(origin) : true),
[validateOrigin]
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
index 86b6c44..cb17230 100644
--- a/src/hooks/index.ts
+++ b/src/hooks/index.ts
@@ -2,3 +2,4 @@ export * from './useUbidotsReady';
export * from './useUbidotsSelections';
export * from './useUbidotsActions';
export * from './useUbidotsAPI';
+export * from './useWidgetEvents';
diff --git a/src/hooks/useUbidotsSelections.ts b/src/hooks/useUbidotsSelections.ts
index d7501dc..da361ca 100644
--- a/src/hooks/useUbidotsSelections.ts
+++ b/src/hooks/useUbidotsSelections.ts
@@ -54,3 +54,8 @@ export function useUbidotsSelectedFilters() {
const { state } = useUbidots();
return state.selectedFilters;
}
+
+export function useUbidotsWidgetId() {
+ const { state } = useUbidots();
+ return state.widgetId;
+}
diff --git a/src/hooks/useWidgetEvents.ts b/src/hooks/useWidgetEvents.ts
new file mode 100644
index 0000000..2da2a5b
--- /dev/null
+++ b/src/hooks/useWidgetEvents.ts
@@ -0,0 +1,135 @@
+import { useCallback, useEffect, useRef } from 'react';
+import { useUbidots } from '../context/ubidots';
+import { useUbidotsWidgetId } from './useUbidotsSelections';
+
+/**
+ * Hook for emitting and listening to widget-specific events
+ *
+ * Widget events follow the pattern: v2:widget::
+ * This allows multiple widgets to coexist with isolated event namespaces
+ */
+export function useWidgetEvents(widgetIdParam?: string) {
+ const { state } = useUbidots();
+ const contextWidgetId = useUbidotsWidgetId();
+ const widgetId = widgetIdParam || contextWidgetId;
+ const listenersRef = useRef