Signals#
A Signal is a reactive value. Bind one to a widget and the two stay in
sync automatically: when the user edits the widget the signal updates, and when
you call signal.set(...) the widget redraws. Signals are how you connect
application state to the interface without wiring up change callbacks by hand.
Creating a signal#
Construct a Signal with its initial value. The value’s type is fixed at
creation — passing a string makes a text signal, an int makes a numeric
one, a bool makes a boolean one:
import bootstack as bs
with bs.App() as app:
name = bs.Signal("World") # text signal
count = bs.Signal(0) # integer signal
enabled = bs.Signal(True) # boolean signal
app.run()
Note
A signal must be created inside a running App. Creating one at module
level — before bs.App() exists — raises an error, because a signal is
backed by the application’s variable system.
Reading and writing#
Call the signal to read its current value. Use set() to update it:
name() # "World" — call to read
name.set("Universe")
set() enforces the signal’s type. Assigning a value of a different type
raises TypeError — a Signal(0) accepts set(5) but rejects
set(1.5). The one exception is numeric widening: a float signal also
accepts an int (bs.Signal(0.0).set(5) stores 5.0). Setting the same
value the signal already holds is a no-op and does not notify subscribers.
Binding to widgets#
Pass a signal to a widget to create a two-way binding. Text-bearing widgets use
textsignal=; boolean and numeric widgets use signal=:
with bs.App(gap=8) as app:
name = bs.Signal("World")
bs.TextField(textsignal=name)
bs.Label(textsignal=name) # mirrors the field as you type
bs.Button("Greet", on_click=lambda: print(f"Hello, {name()}!"))
app.run()
Typing in the field updates name; calling name.set(...) updates the
field. The same signal can drive several widgets at once, keeping them all
consistent.
Reacting to changes#
Subscribe a callback to run whenever the value changes. The callback receives the new value:
sub = count.subscribe(lambda value: print(f"count is now {value}"))
count.set(1) # prints "count is now 1"
count.unsubscribe(sub) # stop listening
count.unsubscribe_all() # drop every subscriber
Pass immediate=True to fire the callback once with the current value at
subscription time, in addition to future changes:
count.subscribe(update_total, immediate=True)
Derived signals#
map() returns a new, read-only signal whose value is computed from the
source. It recomputes automatically whenever the source changes:
name = bs.Signal("world")
shout = name.map(str.upper)
shout() # "WORLD"
name.set("hello")
shout() # "HELLO"
Note
A derived signal is held weakly by its source. Keep a reference to it — assign it to a variable or bind it to a widget. If it is garbage-collected it silently stops updating.
See also#
API reference#
The complete reference — every method on Signal —
lives on the Reactivity API page (Signal is
part of the top-level compose surface). At a glance:
A reactive value that widgets can bind to. |