Node Library
To bind events from WUI (CPP) to your UI (HTML) you will need to install an npm package that adds helper functions and types. This project includes an npm package written in typescript that exposes function to directly communicate with WUI behind it.
If you are using this package in any browser that is not WUI (this includes opening the index.html
directly in your browser), the website will show an error.
A check if the backend exists is performed before most operations: as can be seen here.
If you want to use the package anyways then the replay tool might be what you need
Installation
To install the package, follow your desired method on JSR https://jsr.io/@wui/web-user-interface-lib/score.
Alternatively you can download the release archive directly from the registries most recent release and install it using npm install path/to/archive
. This means you have statically downloaded the version.
That's it. The package includes inline-typescript definitions needed for use and helper functions/hooks.
Quickstart
The recommended way to start is to use deno and the template provided in the vivaci-ui-deno project. It has 100% test coverage and is a very goo starting point. There is also a more "blank" version available in the deno template project.
There are many other templates that were created during the creation of the library, but most of them are not fully up to date.
There are a few starter templates available:
- https://gitlab.zweieuro.at/wui/html_ui/bouncing_objects (archived) older version with full ( at the time) test coverage
- https://gitlab.zweieuro.at/wui/projects/vivaci typescript based template, works well with tests (no full coverage)
- https://gitlab.zweieuro.at/wui/projects/vivaci-ui-deno/ deno based template (100% test coverage, full support)
- https://github.com/ZweiEuro/deno_wui_template blank project, test support
The two vivaci templates are meant to be used with the example game, both CPP and Godot based. The deno version is cleaner to work with, but has no test support. The typescript version runs jest and works fine.
The main reason to switch to deno was the cleaner installation and easier style and maintenance. During this i also figured out that vite runs MUCH faster than jest for my tests that feature react. BUT tests that feature WUI would break due to some incorrect config, there is an open issue for it.
Testing
See here.
Usage
Initialization and startup of the library are done automatically as soon as the library is loaded/importet. The library will check if the WUI backend is available.
Event binding
Bi-directional event binding is the star of the show! To see the other side of this then check here.
Event binding is the main way to communicate between the C++ and the Typescript side of WUI. Every event has a name and a payload. The payload is a json object that can be of any complexity.
Sending information from one side to the other is done via events as strings. Event names are unknown between the two projects/sides. This means that it is the developer's responsibility to keep the event names and the types of the payloads consistent between the two sides.
Should you send an event that the other side is not listening to, the library will throw the appropriate error. But not every case for events is handled in this asynchronous way. This means not everything throws an error.
As a solo developer it's relatively easy to keep the two sides synchronized. As a team (or just 2 people, one for each side) it is important to have some kind of API standard that is shared between the two sides. A simple table listing event names and their payloads is enough.
If you access types that don't exist on either side you will get undefined/null at best or a crash at worst if your code is not properly guarded.
For up-to-date type definitions see the source code.
Errors
When trying to send events that the other side is not listening to will produce debug logs (unless handled otherwise).
sendEvent
will throw an error:
- In typescript
WUI_ERR_BINDINGS_NO_LISTENER_IN_ENGINE
with an exception stringNo listener registered for event $eventName
. - In C++
WUI_ERR_BINDINGS_NO_LISTENER_IN_DOM
which will stop your program if wrapped inWUI_ERROR_CHECK(...)
.
Check that your application does not have any race conditions inside of it, this is easily the case on multi-threaded setups. Initialization order should always be:
- Initialize WUI
- Start the UI tab (calling only
createTab
is enough)- Check that the initial tab has been loaded successfully with
onLoadEndCallback
(withhttpCode
200)
- Check that the initial tab has been loaded successfully with
- Register all event listeners (in either side)
- start sending events.
Alternatively you can ignore the error on either side, as they should not affect the functionality of the application.
Sending events
Sending an event only occurses inside the tab that the current page is lodaded in. This means that if you have multiple tabs open, the event will only be sent from the tab that the event was sent from (aka. that is running this code).
The node module exposes helper functions for this.
import { sendEvent } from '@wui/web-user-interface-lib/hooks';
// send the event
sendEvent('exampleEvent', { value: 12345 });
Events can be of any complexity, but they must be serializable to JSON. This means that you can't send functions or DOM elements (without serializing them first). Any object is thrown into JSON.stringify
before being sent.
sendEvent is a generic function, so to avoid errors you can define a type for safety, similar to the listening example below.
Listening to events
To listen to events they must be subscribed to first. Events are internally 'multiplexed' so listening to the same event on multiple 'objects' or callback functions does not produce additional overhead.
import { registerEventListener, useEventListener } from '@wui/web-user-interface-lib/hooks';
// Subscribe to the event
// without a type it will be "unknown"
type ExampleType = {
value:number
};
const listener = (payload: ExampleType) {
console.log("received event payload",payload)
}
// we get a unique symbol that identifies the callback
const callbackSymbol = registerEventListener<ExampleType>("exampleEvent",listener);
// When done, cleanup with
unregisterEventListener("exampleEvent", callbackSymbol);
For convenience listeners can also be used directly inside React components. The hook will automatically clean up the listener when the component is unmounted.
// Alternatively/Additionally directly inside a react component
const Example = () => {
// Cleanup not necessary, the hook will do it for you
const data = useEventListener<ExampleType>('exampleEvent');
return <> {JSON.stringify(data)} </>;
};
Typically it's save to send as many events from the UI to the C++ side as desired.
Sending events from C++ to the UI on the other hand should be done with caution. Using the data of the event inside a hook will cause a re-render each time the data changes, which also happens if the 'same' data is pushed.
This is due to how react and WUI work internally using useEffect hooks.
Each time you send an event from C++ to the UI, useEventListener
will re-emit the new value. Unless you do your own de-bouncing this will cause a re-render.
The UI is rendered defaults to 30fps which is fine for most web-applications, large amounts of unneccessary renders will slow your UI to a halt or can even break it. I.g. try not to update the data that feeds your UI more than ~10 times a second (even this is a lot).
2 Simple solutions:
- Bulk your events together and send them in one go. Large events are preferred to many small events.
- Skip unneeded
sendEvent
on the C++ side.
API docs
For the currennt API docs see the repo docs.