This article covers the approach used to write user interface programs using Alusus language and the WebPlatform library, in terms of how the code is written and how UI elements are handled during user interaction. It also explains how WebPlatform works behind the scenes to update the interface.
Accessing UI Widgets
Those familiar with the Alusus language, the WebPlatform library, and our previous articles and videos on the subject already know that the approach to building UI interfaces relies on the .{} operator to construct the element tree that forms the UI. For example, if we want to create a box containing a text label, an input field, and a button, the code would look like this:
Window.instance.setView(
Box().{
addChildren({
Text(String("Enter data")),
TextInput(),
Button(String("Send")).{
onClick.connect(closure (widget: ref[Widget], payload: ref[Int]) {
});
}
});
}
);
runEventLoop();
But how do we then programmatically interact with the input field (TextInput) to retrieve the value the user entered? It’s straightforward: we keep a reference to it so we can interact with it later, as follows:
def input: SrdRef[TextInput];
Window.instance.setView(
Box().{
addChildren({
Text(String("Enter data")),
TextInput().{
input = this;
},
Button(String("Send")).{
onClick.connect(closure (widget: ref[Widget], payload: ref[Int]) {
useUserData(input.text);
});
}
});
}
);
runEventLoop();
In this example, we keep a reference pointing to the element. Its value is set when the element is created, and then inside the button’s onClick handler, we use that variable to access the value entered by the user.
This approach works, but it has a problem: the variable input is only valid as long as execution hasn’t left the current function. In the case of the main function this isn’t an issue, since it’s an infinite function — execution blocks at the runEventLoop() call indefinitely, so we never exit the function and the input variable remains valid for the entire lifetime of the program. However, if this was a different function — one that builds the tree and returns it to another function to display — we couldn’t use local variables, because they are temporary and removed from memory when the function exits, even though the UI itself is still valid and displayed in the browser. In such cases, we have two options: use global variables (which we rarely recommend), or use the proper approach: relying on components.
Using Components
Components allow you to bundle a user interface together with its associated data and operations into a single reusable unit, much like components in frameworks such as React, Angular, and others. To create a component, you define a new class derived from the Component class, then build the user interface inside that class’s constructor. In this case, you can define variables that reference UI elements as class members rather than as temporary local variables, which ensures they remain in memory as long as the component is in memory. The following example shows how to convert the code above into a component and how to use it later in the main function:
class MyComponent {
@injection def component: Component;
def input: SrdRef[TextInput];
handler this~init() {
def self: ref[this_type](this);
this.view = Box().{
addChildren({
Text(String("Enter data")),
TextInput().{
self.input = this;
},
Button(String("Send")).{
onClick.connect(closure (widget: ref[Widget], payload: ref[Int]) {
self.useUserData(self.input.text);
});
}
});
};
}
handler this_type(): SrdRef[MyComponent] {
return SrdRef[MyComponent].construct();
}
}
@uiEndpoint["/"]
@title["Example"]
func main {
Window.instance.setView(MyComponent());
runEventLoop();
}
Notice that the class begins by declaring a variable of type Component annotated with @injection, and this is how inheritance is done in Alusus language. The @injection modifier tells the compiler to inject the members of the contained object into the scope of the containing object. This is essentially what inheritance is at its core — containment plus exposure of the inner class’s members in the outer class, and the extends keyword that some languages provide is just a syntactic sugar for this.
Also notice that the initializer (constructor) declares a variable named self of the exact same type as this. We copy the value of this here so we can use it later inside the UI tree. The reason for this copy is to distinguish between the this referring to the component and the this referring to a UI element. This need arises because the .{} operator automatically defines a variable named this pointing to the element that precedes the operator. In other words, the keyword this inside the curly braces of TextInput().{ this } refers to the TextInput being created there, shadowing the outer this that refers to the component. So inside those curly braces, self refers to the component while this refers to the TextInput.
At the end of the class, we define a this_type() handler. This handler redefines what happens when the compiler sees parentheses used with the class name — as in Window.instance.setView(MyComponent()) inside the main function. By default, using parentheses with a class name creates a temporary (stack-allocated) instance of that class. For example, Int() creates a temporary Int value. But what we want for UI components is heap allocation using a SrdRef (shared reference), which keeps the object alive even after the function exits, as long as something holds a copy of that reference.
If the component accepts arguments in its initializer, the this_type() handler would look like this:
handler this_type(arg: String): SrdRef[MyComponent] {
return SrdRef[MyComponent]().{ alloc()~init(arg) };
}
We could then instantiate the component like this: MyComponent(String("...")). In this definition, we declare a variable of type SrdRef[MyComponent] without allocating the object itself, then call alloc() to allocate memory, and ~init() to initialize it with the required arguments. When no arguments need to be passed, we can simply call the construct() function on SrdRef, which is a shortcut for calling alloc() followed by ~init().
Properties, Methods, and Callbacks
You can enable access to different widgets of your components from outside the component using regular Alusus class properties. For example, to make the the text of the input field accessible from outside the component you can add a text property to your component, like this:
handler this.text: String {
return this.input.text;
}
handler this.text = String {
this.input.text = value;
return value;
}
Likewise, you can define methods and callbacks like any regular class. For example:
class MyComponent {
@injection def component: Component;
def input: SrdRef[TextInput];
def onButtonClicked: closure();
handler this.text: String {
return this.input.text;
}
handler this.text = String {
this.input.text = value;
return value;
}
handler this~init() {
def self: ref[this_type](this);
this.view = Box().{
addChildren({
Text(String("Enter data")),
TextInput().{
self.input = this;
},
Button(String("Send")).{
onClick.connect(closure (widget: ref[Widget], payload: ref[Int]) {
self.useUserData(self.input.text);
if not self.onButtonClicked.isNull() {
self.onButtonClicked();
}
});
}
});
};
}
handler this_type(): SrdRef[MyComponent] {
return SrdRef[MyComponent].construct();
}
}
With the upper version of the component, you can now interact with these elements from outside the component, like this:
MyComponent().{
text = String("some default value");
onButtonClick = closure() {
// Do something when the button is clicked
doSomething(this.text);
};
}
Cleanup and Resource Release
Unlike languages that rely on garbage collection — such as JavaScript, TypeScript, and others — Alusus uses reference counting for memory management. What distinguishes this approach is that the timing of object construction and destruction is well-defined, deterministic, and under full control, making it reliable for releasing resources created within a component. All we need to do is define a ~terminate handler (destructor) in the MyComponent class and perform any necessary cleanup inside it.
handler this~terminate() {
// Free resources allocated by this component
}
In Alusus, we are guaranteed that ~terminate will be called immediately when the component is no longer needed — it won’t linger in memory waiting for a garbage collector. This makes it reliable for resource cleanup. For example, if the useUserData function in our component sends a request to the server, we can cancel that request in the terminate handler, guaranteeing that the request is cancelled immediately when the component is removed from the UI (assuming no other references to the component are held elsewhere).
Behind the Scenes: How UI Updates Work
A question that may come to mind at this point is: when exactly are UI elements created in the browser, when are they removed, and how are they updated? Unlike React and many other libraries, the mechanism for creating and updating the UI in Alusus and WebPlatform is very simple and direct. There is no lifecycle (like that in React or Angular); instead, UI elements are created immediately when they are added to the window, or when they are connected to a tree that is currently attached to the window — and this happens within the same call that performs that attachment.
For example, when calling:
Window.instance.setView(MyComponent());
the UI elements (the text, input field, and button) will be created in the browser immediately, before setView returns. And if setView is called again with a different value, those elements will be removed and replaced with the new ones immediately, before setView returns. The same applies to calling addChildren on a Box widget or similar functions in other widget types.
When setView is called with a new value, the current view is removed and any terminate handlers within that view are called immediately, before setView returns. The same applies to Box.removeChildren, which will destroy the child and release all its resources before returning.
The same principle applies to UI updates: executing an operation like text.text = String("...") updates the interface immediately, before that operation completes.
As for data entered by the user, it is read directly from the browser when the corresponding property is accessed. For example, reading the input.text property reads directly from the input field in the browser — there is no need to synchronize data between the browser and Alusus objects.
This simplicity of design makes applications written with Alusus and WebPlatform more stable, better performing, and predictable. There is no complex process running behind the scenes — as there is in other libraries — that causes confusion and surprises for the developer.