From b294bd9177c31a356b3ebfb25226c49167ed51b7 Mon Sep 17 00:00:00 2001 From: Fabio Massaioli Date: Sat, 7 Feb 2026 11:24:33 +0100 Subject: [PATCH 1/9] Fix bad type in JS `Events.Emit` API --- v3/internal/runtime/desktop/@wailsio/runtime/src/events.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/v3/internal/runtime/desktop/@wailsio/runtime/src/events.ts b/v3/internal/runtime/desktop/@wailsio/runtime/src/events.ts index cf6a2581dd7..3ff3fcd68e1 100644 --- a/v3/internal/runtime/desktop/@wailsio/runtime/src/events.ts +++ b/v3/internal/runtime/desktop/@wailsio/runtime/src/events.ts @@ -109,7 +109,7 @@ function dispatchWailsEvent(event: any) { * @param maxCallbacks - The maximum number of times the callback can be called for the event. Once the maximum number is reached, the callback will no longer be called. * @returns A function that, when called, will unregister the callback from the event. */ -export function OnMultiple(eventName: E, callback: WailsEventCallback, maxCallbacks: number) { +export function OnMultiple(eventName: E, callback: WailsEventCallback, maxCallbacks: number): () => void { let listeners = eventListeners.get(eventName) || []; const thisListener = new Listener(eventName, callback, maxCallbacks); listeners.push(thisListener); @@ -164,7 +164,6 @@ export function OffAll(): void { */ export function Emit(name: E, data: WailsEventData): Promise export function Emit(name: WailsEventData extends null | void ? E : never): Promise -export function Emit(name: WailsEventData, data?: any): Promise { - return call(EmitMethod, new WailsEvent(name, data)) +export function Emit(name: E, data?: any): Promise { + return call(EmitMethod, new WailsEvent(name, data)) } - From 5b0b1272e55a3cf4f5ea11cf9fc51b33cd9c7ada Mon Sep 17 00:00:00 2001 From: Fabio Massaioli Date: Sat, 7 Feb 2026 11:31:54 +0100 Subject: [PATCH 2/9] Fix sidebar links for bindings, events, packaging --- docs/astro.config.mjs | 3 +-- docs/src/content/docs/features/bindings/advanced.mdx | 2 +- docs/src/content/docs/features/bindings/best-practices.mdx | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index 35298972727..e6a68419491 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -138,6 +138,7 @@ export default defineConfig({ items: [ { label: "Method Binding", link: "/features/bindings/methods" }, { label: "Services", link: "/features/bindings/services" }, + { label: "Data Models", link: "/features/bindings/models" }, { label: "Advanced Binding", link: "/features/bindings/advanced" }, { label: "Best Practices", link: "/features/bindings/best-practices" }, ], @@ -148,7 +149,6 @@ export default defineConfig({ items: [ { label: "Event System", link: "/features/events/system" }, { label: "Application Events", link: "/features/events/application" }, - { label: "Window Events", link: "/features/events/window" }, { label: "Custom Events", link: "/features/events/custom" }, ], }, @@ -230,7 +230,6 @@ export default defineConfig({ { label: "Windows Packaging", link: "/guides/build/windows" }, { label: "macOS Packaging", link: "/guides/build/macos" }, { label: "Linux Packaging", link: "/guides/build/linux" }, - { label: "MSIX Packaging", link: "/guides/build/msix" }, ], }, { diff --git a/docs/src/content/docs/features/bindings/advanced.mdx b/docs/src/content/docs/features/bindings/advanced.mdx index 11ca5291fa6..2a2a8b8c44f 100644 --- a/docs/src/content/docs/features/bindings/advanced.mdx +++ b/docs/src/content/docs/features/bindings/advanced.mdx @@ -2,7 +2,7 @@ title: Advanced Binding description: Advanced binding techniques including directives, code injection, and custom IDs sidebar: - order: 3 + order: 4 --- import { FileTree } from "@astrojs/starlight/components"; diff --git a/docs/src/content/docs/features/bindings/best-practices.mdx b/docs/src/content/docs/features/bindings/best-practices.mdx index 5b2d71d8d73..85e820117af 100644 --- a/docs/src/content/docs/features/bindings/best-practices.mdx +++ b/docs/src/content/docs/features/bindings/best-practices.mdx @@ -2,7 +2,7 @@ title: Bindings Best Practices description: Design patterns and best practices for Go-JavaScript bindings sidebar: - order: 4 + order: 5 --- ## Bindings Best Practices From 655748ca655e1822e72434be45613933134c350d Mon Sep 17 00:00:00 2001 From: Fabio Massaioli Date: Sat, 7 Feb 2026 11:42:06 +0100 Subject: [PATCH 3/9] Fix placement of file association and single instance guides --- docs/src/content/docs/guides/build/windows.mdx | 2 +- docs/src/content/docs/guides/distribution/auto-updates.mdx | 2 +- .../docs/guides/{ => distribution}/file-associations.mdx | 7 +++++-- .../docs/guides/{ => distribution}/single-instance.mdx | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) rename docs/src/content/docs/guides/{ => distribution}/file-associations.mdx (99%) rename docs/src/content/docs/guides/{ => distribution}/single-instance.mdx (99%) diff --git a/docs/src/content/docs/guides/build/windows.mdx b/docs/src/content/docs/guides/build/windows.mdx index fe4183cb45f..6589d5f8d36 100644 --- a/docs/src/content/docs/guides/build/windows.mdx +++ b/docs/src/content/docs/guides/build/windows.mdx @@ -22,7 +22,7 @@ This runs `wails3 task windows:package` which: Output: `build/windows/nsis/-installer.exe` -### MSIX Package +## MSIX Package For Microsoft Store distribution or modern Windows deployment: diff --git a/docs/src/content/docs/guides/distribution/auto-updates.mdx b/docs/src/content/docs/guides/distribution/auto-updates.mdx index 273915653ae..2adb9868e8c 100644 --- a/docs/src/content/docs/guides/distribution/auto-updates.mdx +++ b/docs/src/content/docs/guides/distribution/auto-updates.mdx @@ -2,7 +2,7 @@ title: Auto-Updates description: Implement automatic application updates with Wails v3 sidebar: - order: 5 + order: 1 --- import { Tabs, TabItem } from '@astrojs/starlight/components'; diff --git a/docs/src/content/docs/guides/file-associations.mdx b/docs/src/content/docs/guides/distribution/file-associations.mdx similarity index 99% rename from docs/src/content/docs/guides/file-associations.mdx rename to docs/src/content/docs/guides/distribution/file-associations.mdx index ba37d9f35a9..8d583014e6a 100644 --- a/docs/src/content/docs/guides/file-associations.mdx +++ b/docs/src/content/docs/guides/distribution/file-associations.mdx @@ -1,7 +1,7 @@ --- title: File Associations sidebar: - order: 20 + order: 2 --- import { Steps } from "@astrojs/starlight/components"; @@ -115,6 +115,8 @@ Let's walk through setting up file associations for a simple text editor: - For macOS add copy statement like `cp build/darwin/documenticon.icns {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources` in the `create:app:bundle:` task. +
+ 2. ### Configure File Associations Edit the `build/config.yml` file to add your file associations: @@ -149,6 +151,7 @@ Let's walk through setting up file associations for a simple text editor: }) ``` +
:::tip[Why are file extensions required in both the application config and config.yml?] On Windows, when a file is opened with a file association, the application is @@ -175,4 +178,4 @@ Let's walk through setting up file associations for a simple text editor: - Icons should be provided in PNG format in the build folder - Testing file associations requires installing the packaged application - \ No newline at end of file + diff --git a/docs/src/content/docs/guides/single-instance.mdx b/docs/src/content/docs/guides/distribution/single-instance.mdx similarity index 99% rename from docs/src/content/docs/guides/single-instance.mdx rename to docs/src/content/docs/guides/distribution/single-instance.mdx index 25b7fac47ce..d1a0a7a913c 100644 --- a/docs/src/content/docs/guides/single-instance.mdx +++ b/docs/src/content/docs/guides/distribution/single-instance.mdx @@ -2,7 +2,7 @@ title: Single Instance description: Limiting your app to a single running instance sidebar: - order: 40 + order: 4 --- import { Tabs, TabItem } from "@astrojs/starlight/components"; From c560920259c3e609e9e463fde33492bf05519795 Mon Sep 17 00:00:00 2001 From: Fabio Massaioli Date: Sat, 7 Feb 2026 11:43:16 +0100 Subject: [PATCH 4/9] Add stub feature pages for application events and custom events --- .../docs/features/events/application.mdx | 142 +++++ .../content/docs/features/events/custom.mdx | 601 ++++++++++++++++++ .../content/docs/features/events/system.mdx | 18 +- 3 files changed, 752 insertions(+), 9 deletions(-) create mode 100644 docs/src/content/docs/features/events/application.mdx create mode 100644 docs/src/content/docs/features/events/custom.mdx diff --git a/docs/src/content/docs/features/events/application.mdx b/docs/src/content/docs/features/events/application.mdx new file mode 100644 index 00000000000..f611d219d5b --- /dev/null +++ b/docs/src/content/docs/features/events/application.mdx @@ -0,0 +1,142 @@ +--- +title: Application Events +description: Handle application lifecycle events +sidebar: + order: 2 +--- + +import { Tabs, TabItem, Card, CardGrid } from "@astrojs/starlight/components"; + +Application events are triggered by application-level state changes such as application startup, theme changes, and power events. You can listen for these events using the `OnApplicationEvent` method: + + +```go +app.Event.OnApplicationEvent(events.Mac.ApplicationDidBecomeActive, func(event *application.ApplicationEvent) { + app.Logger.Info("Application started!") +}) + +app.Event.OnApplicationEvent(events.Windows.SystemThemeChanged, func(event *application.ApplicationEvent) { + app.Logger.Info("System theme changed!") + if event.Context().IsDarkMode() { + app.Logger.Info("System is now using dark mode!") + } else { + app.Logger.Info("System is now using light mode!") + } + }) +``` + +## Common Application Events + +Common application events are aliases for platform-specific application events. These events are triggered by application-level state +changes such as application startup, theme changes, and power events. + +Here is the same example as above, but using common application events to make it work across all platforms: + +```go +app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(event *application.ApplicationEvent) { + app.Logger.Info("Application started!") +}) + +app.Event.OnApplicationEvent(events.Common.ThemeChanged, func(event *application.ApplicationEvent) { + if event.Context().IsDarkMode() { + app.Logger.Info("System is now using dark mode!") + } else { + app.Logger.Info("System is now using light mode!") + } +}) +``` +### Common Application Event List + +| Event Name | Description | +|----------------------------|------------------------------------------------------------------------------------------------------------------------| +| ApplicationOpenedWithFile | Application opened with a file. See [File Associations](/guides/distribution/file-associations) for more information. | +| ApplicationStarted | Application has started | +| ApplicationLaunchedWithUrl | Application opened with an URL. See [Custom Protocols](/guides/distribution/custom-protocols) for more information. | +| ThemeChanged | System theme changed | + +## Platform-Specific Application Events + +Below is a list of all platform-specific application events. + + + + + | Event Name | Common Event | Description | + |------------|--------------|-------------| + | ApplicationDidBecomeActive | - | Application became active | + | ApplicationDidChangeBackingProperties | - | Application backing properties changed | + | ApplicationDidChangeEffectiveAppearance | ThemeChanged | Application appearance changed | + | ApplicationDidChangeIcon | - | Application icon changed | + | ApplicationDidChangeOcclusionState | - | Application occlusion state changed | + | ApplicationDidChangeScreenParameters | - | Screen parameters changed | + | ApplicationDidChangeStatusBarFrame | - | Status bar frame changed | + | ApplicationDidChangeStatusBarOrientation | - | Status bar orientation changed | + | ApplicationDidChangeTheme | ThemeChanged | System theme changed | + | ApplicationDidFinishLaunching | ApplicationStarted | Application finished launching | + | ApplicationDidHide | - | Application hidden | + | ApplicationDidResignActiveNotification | - | Application resigned active state | + | ApplicationDidUnhide | - | Application unhidden | + | ApplicationDidUpdate | - | Application updated | + | ApplicationShouldHandleReopen | - | Application should handle reopen | + | ApplicationWillBecomeActive | - | Application is about to become active | + | ApplicationWillFinishLaunching | - | Application is about to finish launching | + | ApplicationWillHide | - | Application is about to hide | + | ApplicationWillResignActiveNotification | - | Application is about to resign active state | + | ApplicationWillTerminate | - | Application is about to terminate | + | ApplicationWillUnhide | - | Application is about to unhide | + | ApplicationWillUpdate | - | Application is about to update | + + + + + + | Event Name | Common Event | Description | + |------------|--------------|-------------| + | APMPowerSettingChange | - | Power settings changed | + | APMPowerStatusChange | - | Power status changed | + | APMResumeAutomatic | - | System resuming automatically | + | APMResumeSuspend | - | System resuming from suspend | + | APMSuspend | - | System suspending | + | ApplicationStarted | ApplicationStarted | Application started | + | SystemThemeChanged | ThemeChanged | System theme changed | + + + + | Event Name | Common Event | Description | + |------------|--------------|-------------| + | ApplicationStartup | ApplicationStarted | Application started | + | SystemThemeChanged | ThemeChanged | System theme changed | + + + +## Next Steps + + + + Handle specific file types when users open them. + + [Learn More →](/guides/distribution/file-associations) + + + + Launch your application by clicking on a link. + + [Learn More →](/guides/distribution/custom-protocols) + + + + Create your own event types. + + [Learn More →](/features/events/custom) + + + + Handle window lifecycle events. + + [Learn More →](/features/windows/events) + + + +--- + +**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [event examples](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples/events). diff --git a/docs/src/content/docs/features/events/custom.mdx b/docs/src/content/docs/features/events/custom.mdx new file mode 100644 index 00000000000..1b99a6dd304 --- /dev/null +++ b/docs/src/content/docs/features/events/custom.mdx @@ -0,0 +1,601 @@ +--- +title: Custom Events +description: Notify windows about application-specific events with optional typing and autocompletion support +sidebar: + order: 3 +--- + +import { Tabs, TabItem, Card, CardGrid } from "@astrojs/starlight/components"; + +## Event System + +Wails provides a **unified event system** for pub/sub communication. Emit events from anywhere, listen from anywhere—Go to JavaScript, JavaScript to Go, window to window—enabling decoupled architecture with typed events and lifecycle hooks. + +## Quick Start + +**Go (emit):** + +```go +app.Event.Emit("user-logged-in", map[string]interface{}{ + "userId": 123, + "name": "Alice", +}) +``` + +**JavaScript (listen):** + +```javascript +import { Events } from '@wailsio/runtime' + +Events.On("user-logged-in", (event) => { + console.log(`User ${event.data.name} logged in`) +}) +``` + +**That's it!** Cross-language pub/sub. + +## Event Types + +### Custom Events + +Your application-specific events: + +```go +// Emit from Go +app.Event.Emit("order-created", order) +app.Event.Emit("payment-processed", payment) +app.Event.Emit("notification", message) +``` + +```javascript +// Listen in JavaScript +Events.On("order-created", handleOrder) +Events.On("payment-processed", handlePayment) +Events.On("notification", showNotification) +``` + +### System Events + +Built-in OS and application events: + +```go +import "github.com/wailsapp/wails/v3/pkg/events" + +// Theme changes +app.Event.OnApplicationEvent(events.Common.ThemeChanged, func(e *application.ApplicationEvent) { + if e.Context().IsDarkMode() { + app.Logger.Info("Dark mode enabled") + } +}) + +// Application lifecycle +app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(e *application.ApplicationEvent) { + app.Logger.Info("Application started") +}) +``` + +### Window Events + +Window-specific events: + +```go +window.OnWindowEvent(events.Common.WindowFocus, func(e *application.WindowEvent) { + app.Logger.Info("Window focused") +}) + +window.OnWindowEvent(events.Common.WindowClosing, func(e *application.WindowEvent) { + app.Logger.Info("Window closing") +}) +``` + +## Emitting Events + +### From Go + +**Basic emit:** + +```go +app.Event.Emit("event-name", data) +``` + +**With different data types:** + +```go +// String +app.Event.Emit("message", "Hello") + +// Number +app.Event.Emit("count", 42) + +// Struct +app.Event.Emit("user", User{ID: 1, Name: "Alice"}) + +// Map +app.Event.Emit("config", map[string]interface{}{ + "theme": "dark", + "fontSize": 14, +}) + +// Array +app.Event.Emit("items", []string{"a", "b", "c"}) +``` + +**To specific window:** + +```go +window.EmitEvent("window-specific-event", data) +``` + +### From JavaScript + +```javascript +import { Events } from '@wailsio/runtime' + +// Emit to Go +Events.Emit("button-clicked", { buttonId: "submit" }) + +// Emit to all windows +Events.Emit("broadcast-message", "Hello everyone") +``` + +## Listening to Events + +### In Go + +**Application events:** + +```go +app.Event.On("custom-event", func(e *application.CustomEvent) { + data := e.Data + // Handle event +}) +``` + +**With type assertion:** + +```go +app.Event.On("user-updated", func(e *application.CustomEvent) { + user := e.Data.(User) + app.Logger.Info("User updated", "name", user.Name) +}) +``` + +**Multiple handlers:** + +```go +// All handlers will be called +app.Event.On("order-created", logOrder) +app.Event.On("order-created", sendEmail) +app.Event.On("order-created", updateInventory) +``` + +### In JavaScript + +**Basic listener:** + +```javascript +import { Events } from '@wailsio/runtime' + +Events.On("event-name", (event) => { + console.log("Event received:", event.data) +}) +``` + +**With cleanup:** + +```javascript +const unsubscribe = Events.On("event-name", handleEvent) + +// Later, stop listening +unsubscribe() +``` + +**Multiple handlers:** + +```javascript +Events.On("data-updated", updateUI) +Events.On("data-updated", saveToCache) +Events.On("data-updated", logChange) +``` + +**One Time handlers:** + +```javascript +Events.Once("data-updated", updateVariable) +``` + +**Removing all handlers:** + +```javascript +Events.Off("data-updated") +``` + +## System Events + +### Application Events + +**Common events (cross-platform):** + +```go +import "github.com/wailsapp/wails/v3/pkg/events" + +// Application started +app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(e *application.ApplicationEvent) { + app.Logger.Info("App started") +}) + +// Theme changed +app.Event.OnApplicationEvent(events.Common.ThemeChanged, func(e *application.ApplicationEvent) { + isDark := e.Context().IsDarkMode() + app.Event.Emit("theme-changed", isDark) +}) + +// File opened +app.Event.OnApplicationEvent(events.Common.ApplicationOpenedWithFile, func(e *application.ApplicationEvent) { + filePath := e.Context().OpenedFile() + openFile(filePath) +}) +``` + +**Platform-specific events:** + + + + ```go + // Application became active + app.Event.OnApplicationEvent(events.Mac.ApplicationDidBecomeActive, func(e *application.ApplicationEvent) { + app.Logger.Info("App became active") + }) + + // Application will terminate + app.Event.OnApplicationEvent(events.Mac.ApplicationWillTerminate, func(e *application.ApplicationEvent) { + cleanup() + }) + ``` + + + + ```go + // Power status changed + app.Event.OnApplicationEvent(events.Windows.APMPowerStatusChange, func(e *application.ApplicationEvent) { + app.Logger.Info("Power status changed") + }) + + // System suspending + app.Event.OnApplicationEvent(events.Windows.APMSuspend, func(e *application.ApplicationEvent) { + saveState() + }) + ``` + + + + ```go + // Application startup + app.Event.OnApplicationEvent(events.Linux.ApplicationStartup, func(e *application.ApplicationEvent) { + app.Logger.Info("App starting") + }) + + // Theme changed + app.Event.OnApplicationEvent(events.Linux.SystemThemeChanged, func(e *application.ApplicationEvent) { + updateTheme() + }) + ``` + + + +### Window Events + +**Common window events:** + +```go +// Window focus +window.OnWindowEvent(events.Common.WindowFocus, func(e *application.WindowEvent) { + app.Logger.Info("Window focused") +}) + +// Window blur +window.OnWindowEvent(events.Common.WindowBlur, func(e *application.WindowEvent) { + app.Logger.Info("Window blurred") +}) + +// Window closing +window.OnWindowEvent(events.Common.WindowClosing, func(e *application.WindowEvent) { + if hasUnsavedChanges() { + e.Cancel() // Prevent close + } +}) + +// Window closed +window.OnWindowEvent(events.Common.WindowClosed, func(e *application.WindowEvent) { + cleanup() +}) +``` + +## Event Hooks + +Hooks run **before** standard listeners and can **cancel** events: + +```go +// Hook - runs first, can cancel +window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) { + if hasUnsavedChanges() { + result := showConfirmdialog("Unsaved changes. Close anyway?") + if result != "yes" { + e.Cancel() // Prevent window close + } + } +}) + +// Standard listener - runs after hooks +window.OnWindowEvent(events.Common.WindowClosing, func(e *application.WindowEvent) { + app.Logger.Info("Window closing") +}) +``` + +**Key differences:** + +| Feature | Hooks | Standard Listeners | +|---------|-------|-------------------| +| Execution order | First, in registration order | After hooks, no guaranteed order | +| Blocking | Synchronous, blocks next hook | Asynchronous, non-blocking | +| Can cancel | Yes | No (already propagated) | +| Use case | Control flow, validation | Logging, side effects | + +## Event Patterns + +### Pub/Sub Pattern + +```go +// Publisher (service) +type OrderService struct { + app *application.Application +} + +func (o *OrderService) CreateOrder(items []Item) (*Order, error) { + order := &Order{Items: items} + + if err := o.saveOrder(order); err != nil { + return nil, err + } + + // Publish event + o.app.Event.Emit("order-created", order) + + return order, nil +} + +// Subscribers +app.Event.On("order-created", func(e *application.CustomEvent) { + order := e.Data.(*Order) + sendConfirmationEmail(order) +}) + +app.Event.On("order-created", func(e *application.CustomEvent) { + order := e.Data.(*Order) + updateInventory(order) +}) + +app.Event.On("order-created", func(e *application.CustomEvent) { + order := e.Data.(*Order) + logOrder(order) +}) +``` + +### Request/Response Pattern + +```javascript +// Frontend requests data +Events.Emit("get-user-data", { userId: 123 }) +``` + +```go +// Backend responds +app.Event.On("get-user-data", func(e *application.CustomEvent) { + data := e.Data.(map[string]interface{}) + userId := int(data["userId"].(float64)) + + user := getUserFromDB(userId) + + // Send response + app.Event.Emit("user-data-response", user) +}) +``` + +```javascript +// Frontend receives response +Events.On("user-data-response", (event) => { + const user = event.data + displayUser(user) +}) +``` + +**Note:** For request/response, **bindings are better**. Use events for notifications. + +### Broadcast Pattern + +```go +// Broadcast to all windows +app.Event.Emit("global-notification", "System update available") +``` + +```javascript +// Each window handles it +Events.On("global-notification", (event) => { + const message = event.data + showNotification(message) +}) +``` + +### Event Aggregation + +```go +type EventAggregator struct { + events []Event + mu sync.Mutex +} + +func (ea *EventAggregator) Add(event Event) { + ea.mu.Lock() + defer ea.mu.Unlock() + + ea.events = append(ea.events, event) + + // Emit batch every 100 events + if len(ea.events) >= 100 { + app.Event.Emit("event-batch", ea.events) + ea.events = nil + } +} +``` + +## Complete Example + +**Go:** + +```go +package main + +import ( + "github.com/wailsapp/wails/v3/pkg/application" + "github.com/wailsapp/wails/v3/pkg/events" +) + +type NotificationService struct { + app *application.Application +} + +func (n *NotificationService) Notify(message string) { + // Emit to all windows + n.app.Event.Emit("notification", map[string]interface{}{ + "message": message, + "timestamp": time.Now(), + }) +} + +func main() { + app := application.New(application.Options{ + Name: "Event Demo", + }) + + notifService := &NotificationService{app: app} + + // System events + app.Event.OnApplicationEvent(events.Common.ThemeChanged, func(e *application.ApplicationEvent) { + isDark := e.Context().IsDarkMode() + app.Event.Emit("theme-changed", isDark) + }) + + // Custom events from frontend + app.Event.On("user-action", func(e *application.CustomEvent) { + data := e.Data.(map[string]interface{}) + action := data["action"].(string) + + app.Logger.Info("User action", "action", action) + + // Respond + notifService.Notify("Action completed: " + action) + }) + + // Window events + window := app.Window.New() + + window.OnWindowEvent(events.Common.WindowFocus, func(e *application.WindowEvent) { + app.Event.Emit("window-focused", window.Name()) + }) + + window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) { + // Confirm before close + app.Event.Emit("confirm-close", nil) + e.Cancel() // Wait for confirmation + }) + + app.Run() +} +``` + +**JavaScript:** + +```javascript +import { Events } from '@wailsio/runtime' + +// Listen for notifications +Events.On("notification", (event) => { + showNotification(event.data.message) +}) + +// Listen for theme changes +Events.On("theme-changed", (event) => { + const isDark = event.data + document.body.classList.toggle('dark', isDark) +}) + +// Listen for window focus +Events.On("window-focused", (event) => { + const windowName = event.data + console.log(`Window ${windowName} focused`) +}) + +// Handle close confirmation +Events.On("confirm-close", (event) => { + if (confirm("Close window?")) { + Events.Emit("close-confirmed", true) + } +}) + +// Emit user actions +document.getElementById('button').addEventListener('click', () => { + Events.Emit("user-action", { action: "button-clicked" }) +}) +``` + +## Best Practices + +### ✅ Do + +- **Use events for notifications** - One-way communication +- **Use bindings for requests** - Two-way communication +- **Keep event names consistent** - Use kebab-case +- **Document event data** - What fields are included? +- **Unsubscribe when done** - Prevent memory leaks +- **Use hooks for validation** - Control event flow + +### ❌ Don't + +- **Don't use events for RPC** - Use bindings instead +- **Don't emit too frequently** - Batch if needed +- **Don't block in handlers** - Keep them fast +- **Don't forget to unsubscribe** - Memory leaks +- **Don't use events for large data** - Use bindings +- **Don't create event loops** - A emits B, B emits A + +## Next Steps + + + + Common event patterns and best practices. + + [Learn More →](/features/events/system#event-patterns) + + + + Bind complex data structures. + + [Learn More →](/features/bindings/models) + + + + Handle application lifecycle events. + + [Learn More →](/features/events/application) + + + + Handle window lifecycle events. + + [Learn More →](/features/windows/events) + + + +--- + +**Questions?** Ask in [Discord](https://discord.gg/JDdSxwjhGf) or check the [event examples](https://github.com/wailsapp/wails/tree/v3-alpha/v3/examples/events). diff --git a/docs/src/content/docs/features/events/system.mdx b/docs/src/content/docs/features/events/system.mdx index 678abff2f8b..66bcea2551f 100644 --- a/docs/src/content/docs/features/events/system.mdx +++ b/docs/src/content/docs/features/events/system.mdx @@ -563,25 +563,25 @@ document.getElementById('button').addEventListener('click', () => { Create your own event types. - - [Learn More →](/features/events/custom) - - - Common event patterns and best practices. - - [Learn More →](/features/events/patterns) + [Learn More →](/features/events/custom) Use bindings for request/response. - + [Learn More →](/features/bindings/methods) + + Handle application lifecycle events. + + [Learn More →](/features/events/application) + + Handle window lifecycle events. - + [Learn More →](/features/windows/events) From 929d1ea585816a4903fbe4aef3d50686a57acb0c Mon Sep 17 00:00:00 2001 From: Fabio Massaioli Date: Sat, 7 Feb 2026 11:43:40 +0100 Subject: [PATCH 5/9] Fix JS API examples in event system feature page --- .../content/docs/features/events/system.mdx | 66 +++++++++++-------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/docs/src/content/docs/features/events/system.mdx b/docs/src/content/docs/features/events/system.mdx index 66bcea2551f..ed51fbcdf69 100644 --- a/docs/src/content/docs/features/events/system.mdx +++ b/docs/src/content/docs/features/events/system.mdx @@ -129,13 +129,13 @@ window.EmitEvent("window-specific-event", data) ### From JavaScript ```javascript -import { Emit } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' // Emit to Go -Emit("button-clicked", { buttonId: "submit" }) +Events.Emit("button-clicked", { buttonId: "submit" }) // Emit to all windows -Emit("broadcast-message", "Hello everyone") +Events.Emit("broadcast-message", "Hello everyone") ``` ## Listening to Events @@ -204,6 +204,12 @@ Events.On("data-updated", logChange) Events.Once("data-updated", updateVariable) ``` +**Removing all handlers:** + +```javascript +Events.Off("data-updated") +``` + ## System Events ### Application Events @@ -240,7 +246,7 @@ app.Event.OnApplicationEvent(events.Common.ApplicationOpenedWithFile, func(e *ap app.Event.OnApplicationEvent(events.Mac.ApplicationDidBecomeActive, func(e *application.ApplicationEvent) { app.Logger.Info("App became active") }) - + // Application will terminate app.Event.OnApplicationEvent(events.Mac.ApplicationWillTerminate, func(e *application.ApplicationEvent) { cleanup() @@ -254,7 +260,7 @@ app.Event.OnApplicationEvent(events.Common.ApplicationOpenedWithFile, func(e *ap app.Event.OnApplicationEvent(events.Windows.APMPowerStatusChange, func(e *application.ApplicationEvent) { app.Logger.Info("Power status changed") }) - + // System suspending app.Event.OnApplicationEvent(events.Windows.APMSuspend, func(e *application.ApplicationEvent) { saveState() @@ -268,7 +274,7 @@ app.Event.OnApplicationEvent(events.Common.ApplicationOpenedWithFile, func(e *ap app.Event.OnApplicationEvent(events.Linux.ApplicationStartup, func(e *application.ApplicationEvent) { app.Logger.Info("App starting") }) - + // Theme changed app.Event.OnApplicationEvent(events.Linux.SystemThemeChanged, func(e *application.ApplicationEvent) { updateTheme() @@ -307,7 +313,7 @@ window.OnWindowEvent(events.Common.WindowClosed, func(e *application.WindowEvent ## Event Hooks -Hooks run **before** standard listeners and can **cancel** events: +Hooks run **before** standard listeners and can **cancel** events, which also prevents triggering their default behaviour: ```go // Hook - runs first, can cancel @@ -347,14 +353,14 @@ type OrderService struct { func (o *OrderService) CreateOrder(items []Item) (*Order, error) { order := &Order{Items: items} - + if err := o.saveOrder(order); err != nil { return nil, err } - + // Publish event o.app.Event.Emit("order-created", order) - + return order, nil } @@ -377,17 +383,19 @@ app.Event.On("order-created", func(e *application.CustomEvent) { ### Request/Response Pattern -```go +```javascript // Frontend requests data -Emit("get-user-data", { userId: 123 }) +Events.Emit("get-user-data", { userId: 123 }) +``` +```go // Backend responds app.Event.On("get-user-data", func(e *application.CustomEvent) { data := e.Data.(map[string]interface{}) userId := int(data["userId"].(float64)) - + user := getUserFromDB(userId) - + // Send response app.Event.Emit("user-data-response", user) }) @@ -408,7 +416,9 @@ Events.On("user-data-response", (event) => { ```go // Broadcast to all windows app.Event.Emit("global-notification", "System update available") +``` +```javascript // Each window handles it Events.On("global-notification", (event) => { const message = event.data @@ -427,9 +437,9 @@ type EventAggregator struct { func (ea *EventAggregator) Add(event Event) { ea.mu.Lock() defer ea.mu.Unlock() - + ea.events = append(ea.events, event) - + // Emit batch every 100 events if len(ea.events) >= 100 { app.Event.Emit("event-batch", ea.events) @@ -466,39 +476,39 @@ func main() { app := application.New(application.Options{ Name: "Event Demo", }) - + notifService := &NotificationService{app: app} - + // System events app.Event.OnApplicationEvent(events.Common.ThemeChanged, func(e *application.ApplicationEvent) { isDark := e.Context().IsDarkMode() app.Event.Emit("theme-changed", isDark) }) - + // Custom events from frontend app.Event.On("user-action", func(e *application.CustomEvent) { data := e.Data.(map[string]interface{}) action := data["action"].(string) - + app.Logger.Info("User action", "action", action) - + // Respond notifService.Notify("Action completed: " + action) }) - + // Window events window := app.Window.New() - + window.OnWindowEvent(events.Common.WindowFocus, func(e *application.WindowEvent) { app.Event.Emit("window-focused", window.Name()) }) - + window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) { // Confirm before close app.Event.Emit("confirm-close", nil) e.Cancel() // Wait for confirmation }) - + app.Run() } ``` @@ -506,7 +516,7 @@ func main() { **JavaScript:** ```javascript -import { Events, Emit } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' // Listen for notifications Events.On("notification", (event) => { @@ -528,13 +538,13 @@ Events.On("window-focused", (event) => { // Handle close confirmation Events.On("confirm-close", (event) => { if (confirm("Close window?")) { - Emit("close-confirmed", true) + Events.Emit("close-confirmed", true) } }) // Emit user actions document.getElementById('button').addEventListener('click', () => { - Emit("user-action", { action: "button-clicked" }) + Events.Emit("user-action", { action: "button-clicked" }) }) ``` From 89319ff5116ed635cd3cf884dc9467e4487cf540 Mon Sep 17 00:00:00 2001 From: Fabio Massaioli Date: Sat, 7 Feb 2026 11:44:01 +0100 Subject: [PATCH 6/9] Fix errors in events reference --- docs/src/content/docs/reference/events.mdx | 314 +++++++++++++-------- 1 file changed, 197 insertions(+), 117 deletions(-) diff --git a/docs/src/content/docs/reference/events.mdx b/docs/src/content/docs/reference/events.mdx index d6286018eac..0c146a872bd 100644 --- a/docs/src/content/docs/reference/events.mdx +++ b/docs/src/content/docs/reference/events.mdx @@ -29,26 +29,37 @@ The Events API provides methods to emit and listen to events, enabling communica Emits a custom event to all windows. ```go -func (em *EventManager) Emit(name string, data ...interface{}) +func (em *EventManager) Emit(name string, data ...any) bool ``` **Parameters:** - `name` - Event name - `data` - Optional data to send with the event +**Returns:** `true` if the event was cancelled by a hook; `false` otherwise + **Example:** ```go // Emit simple event app.Event.Emit("user-logged-in") // Emit with data -app.Event.Emit("data-updated", map[string]interface{}{ +app.Event.Emit("data-updated", map[string]any{ "count": 42, "status": "success", }) // Emit multiple values app.Event.Emit("progress", 75, "Processing files...") + +// Detect cancellation +cancelled := app.Event.Emit("document-closing", documentInfo) +if cancelled { + return +} else { + closeDocument() + app.Event.Emit("document-closed", documentInfo) +} ``` ### app.Event.On() @@ -69,7 +80,7 @@ func (em *EventManager) On(name string, callback func(*CustomEvent)) func() ```go // Listen for events cleanup := app.Event.On("user-action", func(e *application.CustomEvent) { - data := e.Data.(map[string]interface{}) + data := e.Data.(map[string]any) action := data["action"].(string) app.Logger.Info("User action", "action", action) }) @@ -80,26 +91,89 @@ cleanup() ### Window-Specific Events -Emit events to a specific window: +### window.EmitEvent() + +Emit events to all windows with a specific window reported as sender. + +```go +func (w *WebviewWindow) Emit(name string, data ...any) bool +``` + +**Parameters:** +- `name` - Event name to emit +- `...data` - Zero, one or more data items of any type + +**Returns:** `true` if the event was cancelled by a hook; `false` otherwise +**Example:** ```go -// Emit to specific window +// Backend window.EmitEvent("notification", "Hello from Go!") +``` + +```javascript +// Frontend +import { Events, Window } from '@wailsio/runtime' -// Emit to all windows -app.Event.Emit("global-update", data) +Events.On('notification', async (ev) => { + // The following comparison will be true only if window.EmitEvent was called on the webview we are running in + if (ev.sender === await Window.Name()) { + return + } + + // Show global notifications or notifications from other windows. + showNotification(ev.data) +}) ``` ## Event Methods (Frontend) +### Emit() + +Emits a custom event with the current webview window as sender. + +```javascript +import { Events } from '@wailsio/runtime' + +Events.Emit(name) +Events.Emit(name, data) +``` + +**Parameters:** +- `name` - Event name +- `data` - Optional data to send with the event + +**Returns:** A promise that resolves to `true` if the event was cancelled by a hook; to `false` otherwise + +**Example:** +```javascript +// Emit simple event +Events.Emit("user-logged-in") + +// Emit with data +Events.Emit("data-updated", { + "count": 42, + "status": "success", +}) + +// Detect cancellation +const cancelled = await Events.Emit("document-closing", documentInfo) +if cancelled { + return +} else { + await closeDocument() + app.Event.Emit("document-closed", documentInfo) +} +``` + ### On() Listens for events from Go. ```javascript -import { On } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' -On(eventName, callback) +Events.On(eventName, callback) ``` **Parameters:** @@ -110,16 +184,16 @@ On(eventName, callback) **Example:** ```javascript -import { On } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' // Listen for events -const cleanup = On('data-updated', (data) => { - console.log('Count:', data.count) - console.log('Status:', data.status) - updateUI(data) +const cleanup = Events.On('data-updated', (ev) => { + console.log('Count:', ev.data.count) + console.log('Status:', ev.data.status) + updateUI(ev.data) }) -// Later, remove listener +// Later, stop listening cleanup() ``` @@ -128,61 +202,69 @@ cleanup() Listens for a single event occurrence. ```javascript -import { Once } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' -Once(eventName, callback) +Events.Once(eventName, callback) ``` +**Parameters:** +- `eventName` - Name of the event to listen for +- `callback` - Function called when event is received + +**Returns:** Cleanup function + **Example:** ```javascript -import { Once } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' // Listen for first occurrence only -Once('initialization-complete', (data) => { - console.log('App initialized!', data) +Events.Once('initialization-complete', (ev) => { + console.log('App initialized!', ev.data) // This will only fire once }) ``` ### Off() -Removes an event listener. +Removes _all event listeners_ for a given set of events. ```javascript -import { Off } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' -Off(eventName, callback) +Events.Off(eventName1, ..., eventNameN) ``` +**Parameters:** +- `...eventName` - One or more event names to stop listening to + **Example:** ```javascript -import { On, Off } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' -const handler = (data) => { - console.log('Event received:', data) +const handler1 = (ev) => { + console.log('Event received:', ev.name, ev.data) } +const handler2 = () => console.log('Additional handler triggered') + // Start listening -On('my-event', handler) +Events.On('my-event-1', handler1) +Events.On('my-event-1', handler2) + +Events.On('my-event-2', handler1) // Stop listening -Off('my-event', handler) +Events.Off('my-event-1', 'my-event-2') ``` ### OffAll() -Removes all listeners for an event. +Removes _all registered listeners_ for _all events_. ```javascript -import { OffAll } from '@wailsio/runtime' - -OffAll(eventName) -``` +import { Events } from '@wailsio/runtime' -**Example:** -```javascript -// Remove all listeners for this event -OffAll('data-updated') +Events.OffAll() ``` ## Application Events @@ -195,25 +277,17 @@ Listens for application lifecycle events. func (em *EventManager) OnApplicationEvent(eventType ApplicationEventType, callback func(*ApplicationEvent)) func() ``` -**Event Types:** -- `EventApplicationStarted` - Application has started -- `EventApplicationShutdown` - Application is shutting down -- `EventApplicationDebug` - Debug event (dev mode only) - **Example:** ```go -// Handle application startup -app.Event.OnApplicationEvent(application.EventApplicationStarted, func(e *application.ApplicationEvent) { - app.Logger.Info("Application started") - // Initialize resources +// Handle file opening event +app.Event.OnApplicationEvent(events.Common.ApplicationOpenedWithFile, func(e *application.ApplicationEvent) { + app.Logger.Info("File opened", "name", e.Context().Filename()) + loadDocument(e.Context().Filename()) }) -// Handle application shutdown -app.Event.OnApplicationEvent(application.EventApplicationShutdown, func(e *application.ApplicationEvent) { - app.Logger.Info("Application shutting down") - // Cleanup resources, save state - database.Close() - saveSettings() +// Handle theme change +app.Event.OnApplicationEvent(events.Common.ThemeChanged, func(e *application.ApplicationEvent) { + app.Logger.Info("Theme changed") }) ``` @@ -227,22 +301,15 @@ Listens for window-specific events. func (w *Window) OnWindowEvent(eventType WindowEventType, callback func(*WindowEvent)) func() ``` -**Event Types:** -- `EventWindowFocus` - Window gained focus -- `EventWindowBlur` - Window lost focus -- `EventWindowClose` - Window is closing -- `EventWindowResize` - Window was resized -- `EventWindowMove` - Window was moved - **Example:** ```go // Handle window focus -window.OnWindowEvent(application.EventWindowFocus, func(e *application.WindowEvent) { +window.OnWindowEvent(events.Common.WindowFocus, func(e *application.WindowEvent) { app.Logger.Info("Window focused") }) // Handle window resize -window.OnWindowEvent(application.EventWindowResize, func(e *application.WindowEvent) { +window.OnWindowEvent(events.Common.WindowDidResize, func(e *application.WindowEvent) { width, height := window.Size() app.Logger.Info("Window resized", "width", width, "height", height) }) @@ -268,7 +335,7 @@ func (s *DataService) FetchData(query string) ([]Item, error) { items := fetchFromDatabase(query) // Emit event when done - s.app.Event.Emit("data-fetched", map[string]interface{}{ + s.app.Event.Emit("data-fetched", map[string]any{ "query": query, "count": len(items), }) @@ -281,10 +348,10 @@ func (s *DataService) FetchData(query string) ([]Item, error) { ```javascript import { FetchData } from './bindings/DataService' -import { On } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' // Listen for completion event -On('data-fetched', (data) => { +Events.On('data-fetched', ({ data }) => { console.log(`Fetched ${data.count} items for query: ${data.query}`) showNotification(`Found ${data.count} results`) }) @@ -309,7 +376,7 @@ func (s *Service) ProcessFiles(files []string) error { processFile(file) // Emit progress event - s.app.Event.Emit("progress", map[string]interface{}{ + s.app.Event.Emit("progress", map[string]any{ "current": i + 1, "total": total, "percent": float64(i+1) / float64(total) * 100, @@ -325,16 +392,17 @@ func (s *Service) ProcessFiles(files []string) error { **JavaScript:** ```javascript -import { On, Once } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' // Update progress bar -On('progress', (data) => { +const cleanup = Events.On('progress', ({ data }) => { progressBar.style.width = `${data.percent}%` statusText.textContent = `Processing ${data.file}... (${data.current}/${data.total})` }) // Handle completion -Once('processing-complete', () => { +Events.Once('processing-complete', () => { + cleanup() progressBar.style.width = '100%' statusText.textContent = 'Complete!' setTimeout(() => hideProgressBar(), 2000) @@ -343,35 +411,43 @@ Once('processing-complete', () => { ### Multi-Window Communication -Perfect for applications with multiple windows like settings panels, dashboards, or document viewers. Broadcast events to synchronize state across all windows (theme changes, user preferences) or send targeted events to specific windows for window-specific updates. +Events can be sent from a specific window whose name will be reported in the `Sender` field of the event struct. Handlers will then be able to filter incoming events depending on the sending window. Perfect for applications with multiple windows like settings panels, dashboards, or document viewers. **Go:** ```go -// Broadcast to all windows -app.Event.Emit("theme-changed", "dark") - -// Send to specific window +// Send from specific window preferencesWindow.EmitEvent("settings-updated", settings) -// Window-specific listener -window1.OnEvent("request-data", func(e *application.CustomEvent) { - // Only this window will receive this event - window1.EmitEvent("data-response", data) +app.Event.On("settings-updated", func(e *application.CustomEvent) { + if (e.Sender === window1.EmitEvent()) { + // Process changes + } else { + app.Logger.Warn("settings-updated event from unexpected sender", "sender", e.Sender) + } }) ``` **JavaScript:** ```javascript -import { On } from '@wailsio/runtime' +import { Events, Window } from '@wailsio/runtime' -// Listen in any window -On('theme-changed', (theme) => { - document.body.className = theme +// Send from current window +Events.Emit('status-changed', localStatus) + +// Process only events emitted from the current window +Events.On('status-changed', async (ev) => { + if (ev.sender === await Window.Name()) { + console.log("Status changed:", ev.data) + } }) ``` +:::note +Events emitted from the frontend are always sent from the window the triggering code is running in. +::: + ### State Synchronization Use when you need to keep frontend and backend state in sync, such as user sessions, application configuration, or collaborative features. When state changes on the backend, emit events to update all connected frontends, ensuring consistency across your application. @@ -381,46 +457,52 @@ Use when you need to keep frontend and backend state in sync, such as user sessi ```go type StateService struct { app *application.Application - state map[string]interface{} + state map[string]any mu sync.RWMutex } -func (s *StateService) UpdateState(key string, value interface{}) { +func (s *StateService) UpdateState(key string, value any) { s.mu.Lock() s.state[key] = value s.mu.Unlock() // Notify all windows - s.app.Event.Emit("state-updated", map[string]interface{}{ + s.app.Event.Emit("state-updated", map[string]any{ "key": key, "value": value, }) } -func (s *StateService) GetState(key string) interface{} { +func (s *StateService) GetState(key string) any { s.mu.RLock() defer s.mu.RUnlock() return s.state[key] } + +func (s *StateService) GetFullState() map[string]any { + s.mu.RLock() + defer s.mu.RUnlock() + return maps.Clone(s.state) +} ``` **JavaScript:** ```javascript -import { On } from '@wailsio/runtime' -import { GetState } from './bindings/StateService' +import { Events } from '@wailsio/runtime' +import { GetFullState } from './bindings/StateService' // Keep local state in sync -let localState = {} +const localState = {} -On('state-updated', async (data) => { +Events.On('state-updated', async ({ data }) => { localState[data.key] = data.value updateUI(data.key, data.value) }) // Initialize state -const initialState = await GetState("all") -localState = initialState +const initialState = await GetFullState() +Object.assign(localState, initialState) ``` ### Event-Driven Notifications @@ -435,21 +517,21 @@ type NotificationService struct { } func (s *NotificationService) Success(message string) { - s.app.Event.Emit("notification", map[string]interface{}{ + s.app.Event.Emit("notification", map[string]any{ "type": "success", "message": message, }) } func (s *NotificationService) Error(message string) { - s.app.Event.Emit("notification", map[string]interface{}{ + s.app.Event.Emit("notification", map[string]any{ "type": "error", "message": message, }) } func (s *NotificationService) Info(message string) { - s.app.Event.Emit("notification", map[string]interface{}{ + s.app.Event.Emit("notification", map[string]any{ "type": "info", "message": message, }) @@ -459,10 +541,10 @@ func (s *NotificationService) Info(message string) { **JavaScript:** ```javascript -import { On } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' // Unified notification handler -On('notification', (data) => { +Events.On('notification', ({ data }) => { const toast = document.createElement('div') toast.className = `toast toast-${data.type}` toast.textContent = data.message @@ -486,7 +568,9 @@ package main import ( "sync" "time" + "github.com/wailsapp/wails/v3/pkg/application" + "github.com/wailsapp/wails/v3/pkg/events" ) type EventDemoService struct { @@ -499,7 +583,7 @@ func NewEventDemoService(app *application.Application) *EventDemoService { // Listen for custom events app.Event.On("user-action", func(e *application.CustomEvent) { - data := e.Data.(map[string]interface{}) + data := e.Data.(map[string]any) app.Logger.Info("User action received", "data", data) }) @@ -513,14 +597,14 @@ func (s *EventDemoService) StartLongTask() { for i := 1; i <= 10; i++ { time.Sleep(500 * time.Millisecond) - s.app.Event.Emit("task-progress", map[string]interface{}{ + s.app.Event.Emit("task-progress", map[string]any{ "step": i, "total": 10, "percent": i * 10, }) } - s.app.Event.Emit("task-completed", map[string]interface{}{ + s.app.Event.Emit("task-completed", map[string]any{ "message": "Task finished successfully!", }) }() @@ -536,14 +620,10 @@ func main() { }) // Handle application lifecycle - app.Event.OnApplicationEvent(application.EventApplicationStarted, func(e *application.ApplicationEvent) { + app.Event.OnApplicationEvent(events.Common.ApplicationStarted, func(e *application.ApplicationEvent) { app.Logger.Info("Application started!") }) - app.Event.OnApplicationEvent(application.EventApplicationShutdown, func(e *application.ApplicationEvent) { - app.Logger.Info("Application shutting down...") - }) - // Register service service := NewEventDemoService(app) app.RegisterService(application.NewService(service)) @@ -568,34 +648,34 @@ func main() { **JavaScript:** ```javascript -import { On, Once } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' import { StartLongTask, BroadcastMessage } from './bindings/EventDemoService' // Task events -On('task-started', () => { +Events.On('task-started', () => { console.log('Task started...') document.getElementById('status').textContent = 'Running...' }) -On('task-progress', (data) => { +Events.On('task-progress', ({ data }) => { const progressBar = document.getElementById('progress') progressBar.style.width = `${data.percent}%` console.log(`Step ${data.step} of ${data.total}`) }) -Once('task-completed', (data) => { +Events.Once('task-completed', ({ data }) => { console.log('Task completed!', data.message) document.getElementById('status').textContent = data.message }) // Broadcast events -On('broadcast', (message) => { +Events.On('broadcast', ({ data: message }) => { console.log('Broadcast:', message) alert(message) }) // Window state events -On('window-state', (state) => { +Events.On('window-state', ({ data: state }) => { console.log('Window is now:', state) document.body.dataset.windowState = state }) @@ -761,18 +841,18 @@ app.Event.Emit("e", value) ## Performance Considerations -### Debouncing High-Frequency Events +### Throttling High-Frequency Events ```go type Service struct { app *application.Application lastEmit time.Time - debounceWindow time.Duration + minInterval time.Duration } -func (s *Service) EmitWithDebounce(event string, data interface{}) { +func (s *Service) EmitThrottled(event string, data any) { now := time.Now() - if now.Sub(s.lastEmit) < s.debounceWindow { + if now.Sub(s.lastEmit) < s.minInterval { return // Skip this emission } @@ -781,15 +861,15 @@ func (s *Service) EmitWithDebounce(event string, data interface{}) { } ``` -### Throttling Events +### Throttling Event Processing ```javascript -import { On } from '@wailsio/runtime' +import { Events } from '@wailsio/runtime' let lastUpdate = 0 const throttleMs = 100 -On('high-frequency-event', (data) => { +Events.On('high-frequency-event', ({ data }) => { const now = Date.now() if (now - lastUpdate < throttleMs) { return // Skip this update From b9c91835f63e506500a63195e9d07a199d1a042e Mon Sep 17 00:00:00 2001 From: Fabio Massaioli Date: Sat, 7 Feb 2026 13:55:41 +0100 Subject: [PATCH 7/9] Complete and cleanup custom event method reference --- docs/src/content/docs/reference/events.mdx | 194 +++++++++++++++++---- 1 file changed, 163 insertions(+), 31 deletions(-) diff --git a/docs/src/content/docs/reference/events.mdx b/docs/src/content/docs/reference/events.mdx index 0c146a872bd..369bc75e627 100644 --- a/docs/src/content/docs/reference/events.mdx +++ b/docs/src/content/docs/reference/events.mdx @@ -12,9 +12,9 @@ import { Card, CardGrid } from "@astrojs/starlight/components"; The Events API provides methods to emit and listen to events, enabling communication between different parts of your application. **Event Types:** +- **Custom Events** - User-defined events for app-specific communication - **Application Events** - App lifecycle events (startup, shutdown) - **Window Events** - Window state changes (focus, blur, resize) -- **Custom Events** - User-defined events for app-specific communication **Communication Patterns:** - **Go to Frontend** - Emit events from Go, listen in JavaScript @@ -22,11 +22,11 @@ The Events API provides methods to emit and listen to events, enabling communica - **Frontend to Frontend** - Via Go or local runtime events - **Window to Window** - Target specific windows or broadcast to all -## Event Methods (Go) +## Custom Event Methods (Go) ### app.Event.Emit() -Emits a custom event to all windows. +Emits a custom event. ```go func (em *EventManager) Emit(name string, data ...any) bool @@ -62,6 +62,41 @@ if cancelled { } ``` +### window.EmitEvent() + +Emits a custom event reporting a specific window as the sender. + +```go +func (w *WebviewWindow) Emit(name string, data ...any) bool +``` + +**Parameters:** +- `name` - Event name to emit +- `...data` - Zero, one or more data items of any type + +**Returns:** `true` if the event was cancelled by a hook; `false` otherwise + +**Example:** +```go +// Backend +window.EmitEvent("notification", "Hello from Go!") +``` + +```javascript +// Frontend +import { Events, Window } from '@wailsio/runtime' + +Events.On('notification', async (ev) => { + // The following comparison will be true only if window.EmitEvent was called on the webview we are running in + if (ev.sender === await Window.Name()) { + return + } + + // Show global notifications or notifications from other windows. + showNotification(ev.data) +}) +``` + ### app.Event.On() Listens for custom events in Go. @@ -89,46 +124,46 @@ cleanup := app.Event.On("user-action", func(e *application.CustomEvent) { cleanup() ``` -### Window-Specific Events +### app.Event.Off() -### window.EmitEvent() - -Emit events to all windows with a specific window reported as sender. +Removes all listeners for a custom event in Go. ```go -func (w *WebviewWindow) Emit(name string, data ...any) bool +func (em *EventManager) Off(name string) ``` **Parameters:** -- `name` - Event name to emit -- `...data` - Zero, one or more data items of any type - -**Returns:** `true` if the event was cancelled by a hook; `false` otherwise +- `name` - Event name to stop listening for **Example:** ```go -// Backend -window.EmitEvent("notification", "Hello from Go!") +app.Event.On("user-action", listener1) +app.Event.On("user-action", listener2) + +// Remove both listener1 and listener2 +app.Event.Off("user-action") ``` -```javascript -// Frontend -import { Events, Window } from '@wailsio/runtime' +### app.Event.Reset() -Events.On('notification', async (ev) => { - // The following comparison will be true only if window.EmitEvent was called on the webview we are running in - if (ev.sender === await Window.Name()) { - return - } +Removes all listeners for all custom events in Go. - // Show global notifications or notifications from other windows. - showNotification(ev.data) -}) +```go +func (em *EventManager) Reset() ``` -## Event Methods (Frontend) +**Example:** +```go +app.Event.On("user-action", listener1) +app.Event.On("settings-update", listener2) -### Emit() +// Remove both listener1 and listener2 (as well as any other registered listener) +app.Event.Reset() +``` + +## Custom Event Methods (Frontend) + +### Events.Emit() Emits a custom event with the current webview window as sender. @@ -166,7 +201,7 @@ if cancelled { } ``` -### On() +### Events.On() Listens for events from Go. @@ -197,7 +232,7 @@ const cleanup = Events.On('data-updated', (ev) => { cleanup() ``` -### Once() +### Events.Once() Listens for a single event occurrence. @@ -224,7 +259,7 @@ Events.Once('initialization-complete', (ev) => { }) ``` -### Off() +### Events.Off() Removes _all event listeners_ for a given set of events. @@ -257,7 +292,7 @@ Events.On('my-event-2', handler1) Events.Off('my-event-1', 'my-event-2') ``` -### OffAll() +### Events.OffAll() Removes _all registered listeners_ for _all events_. @@ -267,6 +302,103 @@ import { Events } from '@wailsio/runtime' Events.OffAll() ``` +## Typed Event API + +Custom events may be registered at init time, enabling strict checking of correctness on names and data types, as well as generation and runtime translation of Typescript types on the frontend side. + +If a registered event is emitted with the wrong data type, an error will be reported through the application error handler (configurable through application options, the default handler logs errors to the configured logger) and the event will be dropped. + +By default, registering an event will only enable checking of data types. If the application is compiled with the `strictevents` tag, it will also log warnings when unregistered events are emitted (but will not drop them). + +### application.RegisterEvent() + +Registers an event with a specified data type. Duplicate calls for the same event name trigger a panic. + +```go +func RegisterEvent[Data any](name string) +``` + +**Type Parameters:** +- `Data` - Type of data associated to the registered event + +**Parameters:** +- `name` - Event name to register + +**Example:** +```go +package auth + +type User struct{ + Id number `json:"id"` + Name string `json:"name"` +} + +func init() { + application.RegisterEvent[User]("auth:login") +} + +// Valid! Emits event +app.Events.Emit("auth:login", User{ Id: 0, Name: "root" }) + +// Invalid! Logs error and drops event +app.Events.Emit("auth:login") +app.Events.Emit("auth:login", "username") +``` + +```typescript +import { Events } from '@wailsio/runtime' +import { User } from './bindings/auth' + +Events.On('auth:login', ({ data }) => { + // data has type User, data.id has type number and data.name has type string. + // Without registration, their type would be any. +}) + +// Valid! Emits event. Go listeners will receive an instance of struct `auth.User` +Events.Emit('auth:login', new User({ id: 0, name: 'root' })) + +// Invalid! Does not typecheck at compile time, and log an error at runtime +Events.Emit('auth:login') +Events.Emit('auth:login', "username") +``` + +### application.Void + +A placeholder type to register events that must have no associated data. + +```go +type Void interface{ + /* Private Methods */ +} +``` + +**Example:** +```go +func init() { + application.RegisterEvent[application.Void]("processing-complete") +} + +// Valid! Emits event +Events.Emit("processing-complete") + +// Invalid! Logs error and drops event +Events.Emit("processing-complete", "100%") +``` + +```javascript +import { Events } from '@wailsio/runtime' + +Events.On('processing-complete', ({ data }) => { + // data has type void +}) + +// Valid! Emits event +Events.Emit('processing-complete') + +// Invalid! Does not typecheck at compile time, and will log an error at runtime +Events.Emit('processing-complete', 100) +``` + ## Application Events ### app.Event.OnApplicationEvent() From 4b8343797915d5372aa06a5b461cfb515a3db582 Mon Sep 17 00:00:00 2001 From: Fabio Massaioli Date: Sat, 7 Feb 2026 14:42:40 +0100 Subject: [PATCH 8/9] Complete and cleanup application event method reference --- docs/src/content/docs/reference/events.mdx | 43 ++++++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/docs/src/content/docs/reference/events.mdx b/docs/src/content/docs/reference/events.mdx index 369bc75e627..c94911a9fff 100644 --- a/docs/src/content/docs/reference/events.mdx +++ b/docs/src/content/docs/reference/events.mdx @@ -399,7 +399,7 @@ Events.Emit('processing-complete') Events.Emit('processing-complete', 100) ``` -## Application Events +## Application Event Methods ### app.Event.OnApplicationEvent() @@ -409,6 +409,12 @@ Listens for application lifecycle events. func (em *EventManager) OnApplicationEvent(eventType ApplicationEventType, callback func(*ApplicationEvent)) func() ``` +**Parameters:** +- `eventType` - Type of the event to listen for (see [Application Events](#application-events)) +- `callback` - Function called when event is received + +**Returns:** Cleanup function + **Example:** ```go // Handle file opening event @@ -418,12 +424,43 @@ app.Event.OnApplicationEvent(events.Common.ApplicationOpenedWithFile, func(e *ap }) // Handle theme change -app.Event.OnApplicationEvent(events.Common.ThemeChanged, func(e *application.ApplicationEvent) { +cleanup := app.Event.OnApplicationEvent(events.Common.ThemeChanged, func(e *application.ApplicationEvent) { app.Logger.Info("Theme changed") }) + +// Stop listening to theme changes later +cleanup() +``` + +### app.Event.RegisterApplicationEventHook() + +Registers a hook to intercept and cancel application events. For cancelable events, canceling will prevent the default behaviour. For non-cancelable events, canceling will just stop propagation to later hooks and listeners. + +```go +func (em *EventManager) RegisterApplicationEventHook(eventType events.ApplicationEventType, callback func(event *ApplicationEvent)) func() +``` + +**Parameters:** +- `eventType` - Type of the event to listen for (see [Application Events](#application-events)) +- `callback` - Function called when event is received + +**Returns:** Cleanup function + +**Example:** +```go +// Suppress theme change notifications conditionally +cleanup := app.Event.RegisterApplicationEventHook(events.Common.ThemeChanged, func(e *application.ApplicationEvent) { + if condition { + // If condition is met, stop propagation + e.Cancel() + } +}) + +// Stop filtering theme changes later +cleanup() ``` -## Window Events +## Window Event Methods ### OnWindowEvent() From 717ea9e3a2a33b26092af4324f48c40c5b9734f3 Mon Sep 17 00:00:00 2001 From: Fabio Massaioli Date: Sat, 7 Feb 2026 14:42:50 +0100 Subject: [PATCH 9/9] Complete and cleanup window event method reference --- docs/src/content/docs/reference/events.mdx | 38 +++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/docs/src/content/docs/reference/events.mdx b/docs/src/content/docs/reference/events.mdx index c94911a9fff..991c51860df 100644 --- a/docs/src/content/docs/reference/events.mdx +++ b/docs/src/content/docs/reference/events.mdx @@ -470,6 +470,12 @@ Listens for window-specific events. func (w *Window) OnWindowEvent(eventType WindowEventType, callback func(*WindowEvent)) func() ``` +**Parameters:** +- `eventType` - Type of the event to listen for (see [Window Events](#window-events)) +- `callback` - Function called when event is received + +**Returns:** Cleanup function + **Example:** ```go // Handle window focus @@ -478,10 +484,40 @@ window.OnWindowEvent(events.Common.WindowFocus, func(e *application.WindowEvent) }) // Handle window resize -window.OnWindowEvent(events.Common.WindowDidResize, func(e *application.WindowEvent) { +cleanup := window.OnWindowEvent(events.Common.WindowDidResize, func(e *application.WindowEvent) { width, height := window.Size() app.Logger.Info("Window resized", "width", width, "height", height) }) + +// Stop listening to resize events later +cleanup() +``` + +### window.RegisterHook() + +Registers a hook to intercept and cancel window events. For cancelable events, canceling will prevent the default behaviour. For non-cancelable events, canceling will just stop propagation to later hooks and listeners. + +```go +func (w *WebviewWindow) RegisterHook(eventType events.WindowEventType, callback func(event *WindowEvent)) func() +``` + +**Parameters:** +- `eventType` - Type of the event to listen for (see [Window Events](#window-events)) +- `callback` - Function called when event is received + +**Returns:** Cleanup function + +**Example:** +```go +// Ask for confirmation before closing window +cleanup := window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) { + if !confirm("There is unsaved data! Do you want to quit?") { + e.Cancel() + } +}) + +// Stop asking for confirmation later +cleanup() ``` ## Common Patterns