Skip to content

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.

Run this JavaScript after the counter’s HTML exists:

import { createApp } from '@jnreis/briojs';
const app = createApp({
state: () => ({ count: 0 }),
});
app.mount('#counter');
<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:

  • state creates the app’s starting data: { count: 0 }.
  • mount connects Brio to #counter, leaving the rest of the page alone.
  • b-on-click changes the count; b-text keeps the displayed text in sync.
  • $data.count reads 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.

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 globalsMath, 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.

The expression context is also available as $context.

WhereAvailable bindings
Every expression$data, $el, $id, $ref, $for, $props, $emit, $insert, $stores, $provided, trustedHTML, flush
Event handlers$event
Observers$mutations, $size, or $entry
Contextual scriptsonCleanup

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.

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:

ScopeGrandparent accessWhat adds a level?
Data$data.$parent.$parentb-data with an expression, or component setup state
Loop$for.$parent.$parentA 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.

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.