First app
Start with a little state and a place to use it. This counter needs just two pieces: JavaScript to mount the app, and HTML to describe the interaction.
Give the counter some state
Section titled “Give the counter some state”Run this JavaScript after the counter’s HTML exists:
import { createApp } from '@jnreis/briojs';
const app = createApp({ state: () => ({ count: 0 }),});
app.mount('#counter');Connect it to your HTML
Section titled “Connect it to your HTML”<section id="counter"> <button b-on-click="$data.count++"> Count: <span b-text="$data.count"></span> </button> <p b-text="`Twice: ${$data.count * 2}`"></p></section>Click the button and both numbers update. Here’s all that’s happening:
statecreates the app’s starting data:{ count: 0 }.mountconnects Brio to#counter, leaving the rest of the page alone.b-on-clickchanges the count;b-textkeeps the displayed text in sync.$data.countreads the count from the current state.
That’s enough to start. The sections below explain the expression helpers and scope rules you’ll encounter as your app grows.
Reading state in expressions
Section titled “Reading state in expressions”Brio uses explicit paths, so it’s clear where a value comes from:
| To read… | Use |
|---|---|
The nearest state (app state, component setup state, or b-data) | $data |
| Parent state | $data.$parent |
| Loop values | $for |
| JavaScript globals | Math, JSON, setTimeout, and other globals as usual |
Use $data.count, rather than bare count. Expressions run in strict mode,
without with or implicit state-name resolution. Unresolved identifiers throw
ReferenceError; when a bare name matches a loop variable, state key, or prop,
the error suggests the path to use. For example:
count is not defined. Use $data.count.item is not defined. Loop variables are read through $for: use $for.item.Helpers available to your HTML
Section titled “Helpers available to your HTML”The expression context is also available as $context.
| Where | Available bindings |
|---|---|
| Every expression | $data, $el, $id, $ref, $for, $props, $emit, $insert, $stores, $provided, trustedHTML, flush |
| Event handlers | $event |
| Observers | $mutations, $size, or $entry |
| Contextual scripts | onCleanup |
Bindings are read-only references. Change writable state properties, not the reference itself:
$data.count++; // Update a state property.// Don't reassign the binding: $data = {};$ref names are also read-only. Event statements can declare ordinary local
variables.
When you add nested scopes
Section titled “When you add nested scopes”Each nested context inherits from its parent through the prototype chain and stores only what it adds (for example its own $el). Reads such as $context.$data see the whole chain, but Object.keys($context) and spreads list only the element’s own keys.
Data and loop ancestry are independent:
| Scope | Grandparent access | What adds a level? |
|---|---|---|
| Data | $data.$parent.$parent | b-data with an expression, or component setup state |
| Loop | $for.$parent.$parent | A nested loop |
Ordinary elements and loops do not add data ancestry levels; data scopes do
not add loop ancestry levels. App state and outermost loops have
$parent === undefined. An empty b-data attribute inherits data without
adding a scope; b-data="{}" creates one.
$parent is reserved, read-only metadata on scope-specific views, not a field added to user state. It is excluded from keys, spreads, and JSON serialization. The same underlying state object may have different parents in different scopes. Normal reads, writes, and methods remain reactive. Access ancestry through $data.$parent and $for.$parent, and use $context for other helpers.
A note on authored HTML
Section titled “A note on authored HTML”trustedHTML remains public for JavaScript-authored values, for example
app.state.markup = brio.trustedHTML('<em>Authored content</em>') with
b-html.hydrate="$data.markup". Expressions also receive trustedHTML directly.
It marks trust intent, not sanitization; managed insertion uses it internally.
Default b-html also accepts unmarked strings but never activates Brio.