The Loup Scripting Book — contents
  1. Chapter 1: Getting Started
  2. Chapter 2: Variables and Data Types
  3. Chapter 3: Functions and Closures
  4. Chapter 4: Control Flow
  5. Chapter 5: Collections
  6. Chapter 6: Structs
  7. Chapter 7: Enums and Pattern Matching
  8. Chapter 8: Classes and Objects
  9. Chapter 9: Interfaces and Generics
  10. Chapter 10: Error Handling
  11. Chapter 11: Modules and Packages
  12. Chapter 12: Strings, Interpolation, and Template Fences
  13. Chapter 13: Concurrency
  14. Chapter 14: Reflection and Dynamic Compilation
  15. Chapter 15: Reactive UI
  16. Chapter 16: Testing
  17. Chapter 17: JavaScript Host Interop
  18. Chapter 18: Building Script Tools
  19. Chapter 19: Memory Safety and Resource Management

Chapter 15: Reactive UI

⚠ Planned — not in Loup 1.25. This chapter describes the design in the Language Reference (§2-4 Reactive UI: §2-4.1 signals, §2-4.2 components, §2-4.3 mounting, §2-4.6 the ui fence, §2-4.8 the desktop backend). The toolchain does not implement it yet, so its listings are design sketches and are not verified.

What you'll learn: the planned design of Loup's reactive UI — signals, derived values and effects, components written as functions that return a View, the cross-platform ui fence, and mounting a view in a window.

Status in Loup 1.25

None of the ui modules ship. from ui import (signal); fails with 'ui' is not a standard-library module, and signal, derived, effect, View, ComponentRegistry and mount are undefined. A (ui) template fence does compile, but only as an ordinary text template: rendering it returns its markup as a string, not a View. No desktop or browser renderer exists.

What you can do today is generate HTML text with html template fences (Chapter 12) and serve it with the net.http server module; Chapter 18 builds command-line tools, which is where most scripts end up.

The Design in Brief

The UI system has three layers (§2-4): a universal core, ui, with signals and the component model; a web backend, ui.web, for browsers and server-side rendering; and a desktop backend, ui.desktop, for native windows. A host.scripted file uses the core and the desktop backend; ui.web is for the browser and service targets and is not available to scripts (§1-2.4.3).

Signals, Derived Values and Effects

A signal is a value that records who reads it and notifies them when it changes (§2-4.1). derived(fn) is a read-only signal computed from others; effect(fn) runs fn once immediately and again whenever a signal it read changes:

Fragment — not a complete program
@mode(host.scripted)

from ui import (signal, derived, effect);

let celsius = signal(20.0);
let fahrenheit = derived(func() => celsius.get() * 9.0 / 5.0 + 32.0);

let watcher = effect(func() {
    println(i"{$ celsius.get() $} C = {$ fahrenheit.get() $} F");
});
# prints "20 C = 68 F" immediately

celsius.set(30.0);
# prints "30 C = 86 F"

celsius.update(func(c: float) => c + 5.0);
# prints "35 C = 95 F"

watcher.dispose();
celsius.set(0.0);
# prints nothing: the effect was disposed

Listing 15-1: A signal, a derived signal and an effect (design sketch)

The API is small: Signal[T] has get(), set(value) and update(fn); effect returns an EffectHandle with dispose().

Components and the ui Fence

A component is a function that returns a View (§2-4.2). Its body runs once, when the component is created, so local signals hold its state. The markup is written in a ui template fence whose tags — Column, Row, Text, Button, TextField, Checkbox and the rest of §2-4.6 — map to native widgets or to HTML, depending on the backend. Uppercase tags are looked up in a ComponentRegistry, named in the fence's components= descriptor:

Fragment — not a complete program
@mode(host.scripted)

from ui import (signal, View, ComponentRegistry);
import ui.primitives;

let registry = ComponentRegistry.of(ui.primitives.components);

func Counter(start: int): View {
    let count = signal(start);
    return ```(ui):(components=registry)
        <Column spacing={$ 8 $}>
            <Text>Count: {$ count.get() $}</Text>
            <Row spacing={$ 8 $}>
                <Button onclick={$ func() { count.update(func(n: int) => n + 1); } $}>
                    <Text>+1</Text>
                </Button>
                <Button onclick={$ func() { count.set(start); } $}>
                    <Text>Reset</Text>
                </Button>
            </Row>
        </Column>
    ```;
}

Listing 15-2: A counter component (design sketch)

Reading count.get() inside the fence makes the text depend on count; clicking a button changes the signal and the runtime re-renders only what depends on it. Event handlers are Loup closures, never strings of JavaScript (E_INLINE_JS_EVENT_HANDLER). The fence directives of Chapter 12 — {% if %}, {% for %}, {% match %} — work inside ui fences, and {% suspend fallback={$ … $} %} shows a placeholder while an async component's Task[View] resolves (§2-4.5).

Input widgets bind to a signal directly: TextField(value: Signal[string]), Checkbox(checked: Signal[bool]).

Mounting

mount(view, target) attaches a view to a rendering root and returns a handle with unmount() (§2-4.3). runtime.ui.rootMount() gives the platform's default root, so one entry point works on every backend. A script would open a desktop window:

Fragment — not a complete program
@mode(host.scripted)

from ui import (signal, mount, View, ComponentRegistry);
import ui.primitives;

let registry = ComponentRegistry.of(ui.primitives.components);

func TodoApp(): View {
    let items = signal(list[string]());
    let draft = signal("");

    return ```(ui):(components=registry)
        <Column spacing={$ 12 $}>
            <Row spacing={$ 8 $}>
                <TextField value={$ draft $} placeholder="Add item..." />
                <Button onclick={$ func() {
                    items.update(func(xs: list[string]) => xs + [draft.get()]);
                    draft.set("");
                } $}>
                    <Text>Add</Text>
                </Button>
            </Row>
            {% for (item in items.get()) %}
                <Text>{$ item $}</Text>
            {% end %}
        </Column>
    ```;
}

mount(TodoApp(), runtime.ui.rootMount());

Listing 15-3: A to-do list mounted at the platform's default root (design sketch)

The list is updated by producing a new list (xs + [draft.get()]) rather than mutating the old one; treating the values held in signals as immutable is the intended style.

Custom elements (@webcomponent), server-side rendering and hydration belong to the web backend; see The Loup Web Book, which describes them as planned as well.

Summary

  • Reactive UI is specified in §2-4 and not implemented in Loup 1.25: the ui modules do not exist, and a (ui) fence renders only as text.
  • The design: signal, derived and effect for state; components are functions returning View, written with the ui fence and a ComponentRegistry; mount(view, target) attaches a view, with runtime.ui.rootMount() as the portable target.
  • Scripts are meant to use the core ui module and the desktop backend; ui.web is for browser and service targets.
  • Until it ships, generate HTML with html fences (Chapter 12).