Tibi

Shared state and props

Every state() in a <script tibi> is a signal, and one effect tracker serves the whole page: an effect in any component re-runs when any signal it read changes, wherever that signal was created. On top of that, Tibi.shared gives named signals every component can reach, and .name="expr" passes real values (objects, arrays, functions) to child components.

Named shared signals

<!-- components/CartBadge.tibi --> <template> <span>{{ label }}</span> </template> <script tibi> const cart = Tibi.shared('cart', [], { persist: true }); const label = computed(() => `${cart.value.length} in cart`); </script>
  • Tibi.shared(name, initial, opts) returns the same signal for the same name everywhere on the page. The first call decides the initial value.
  • persist: true keeps the value in Tibi.store under shared:<name> (or pass a key such as 'prefs:theme'), so it survives reloads; ttl sets its lifetime in seconds (default one year). The stored value loads asynchronously and replaces the initial value unless something already changed it.
  • Assign a new value to trigger updates (cart.value = [...cart.value, id]); mutating an array in place doesn't notify.

Signals outside components

  • Tibi.signal(initial): a standalone signal. .value reads (and subscribes the running effect) and writes; .peek() reads without subscribing; .subscribe(fn) calls fn(value) on every change and returns an unsubscribe function.
  • Tibi.effect(fn): runs now and again whenever a signal it read changes. Returns the runner; call .dispose() to stop it.
  • Tibi.computed(fn): a read-only derived signal.

Component effects are tied to their host element. While the host is out of the document (a t-if branch that's hidden, a page swapped out by client navigation) shared signals drop its effects, so detached DOM is never updated and nothing leaks; they re-run when the host comes back. If it was out long enough for its onUnmount hooks to run, its on(...) and @event listeners come back with it; anything you tore down yourself in onUnmount doesn't.

Passing objects, arrays and functions

Attributes are strings. To hand a child the value itself, prefix the attribute with a dot:

<ProductList .products="products" .onadd="addToCart" title="Shop"/> <script tibi> const products = state([{ id: 1, name: 'Mug' }]); const addToCart = (p) => { cart.value = [...cart.value, p.id]; }; </script> <!-- components/ProductList.tibi --> <template> <ul> <li t-for="p of items">{{ p.name }} <button @click="add(p)">Add</button></li> </ul> </template> <script tibi> const items = computed(() => props.products || []); function add(p) { props.onadd?.(p); } </script>
  • The binding is reactive: when products changes, the child's effects reading props.products re-run.
  • Value props win over a string attribute of the same name; props.has('x') sees both. props.json('x') returns a value prop as-is.
  • Value props exist only in the browser: the server render sees the child without them, so render something sensible for undefined.
  • From plain JS, set them with Tibi.setProps(hostElement, { products, onadd }).
  • .name bindings work on component hosts and plain elements in a <script tibi> page or component, including inside t-for rows (nested loops too), where they can pass the row's item: <Card t-for="p of items" .product="p"/>.