---
title: "7. Reactivity"
image: "https://adonia.pages.dev/og.png"
version: "next"
---

> Documentation Index
> Fetch the complete documentation index at: https://adonia.pages.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# 7. Reactivity

Three behaviours turn the form from a list of inputs into something that reacts:
the slug writes itself, `Published at` only exists for published posts, and a
subtitle appears for the one post kind that uses it. All three are declared, all
three are enforced twice.

<!-- @sample-preamble
import { F } from '@adonia/core'
-->

## `live()` and `sets`

```ts
F.text('title').label('Title').required().maxLength(200).live().sets('slug', 'slugify')
```

<small>`examples/blog-admin/app/adonia/resources/post_resource.ts`</small>

`live(debounceMs = 300)` marks the field as one whose changes re-run the
reactive graph as you type. `sets(target, transform)` declares a derivation:
each keystroke in `title` rewrites `slug`. The built-in transforms are
`slugify`, `lowercase`, `uppercase` and `copy`; `fn:<name>` reaches a transform
you registered, and an unresolvable name **fails compilation** rather than
silently doing nothing.

On the wire the rule rides beside `props`, never inside it:

```jsonc
{
  "type": "text-input",
  "key": "title",
  "props": { "label": "Title", "required": true, "maxLength": 200 },
  "reactive": {
"live": { "debounceMs": 300 },
"sets": [{ "target": "slug", "transform": "slugify" }]
  }
}
```

The server re-runs every derivation on submit and **overwrites** whatever the
client sent for the target. Post `title=Reactivity, Chapter Eight!` with
`slug=client-tampered-slug` and the row stores `reactivity-chapter-eight`: the
client's copy of the rule is a convenience, not the authority.

Re-derivation happens *after* validation, so `sets` is not a defaulting
mechanism — a required target still has to arrive. And because it runs on every
submit, editing a title would move a published URL. The fix is the fill set,
not a condition:

```ts
F.text('slug').label('Slug').required().maxLength(220).unique().readonlyOn('edit')
```

`readonlyOn('edit')` renders and validates the field but leaves it out of the
fill set for that mode: the derivation still runs, and the column does not
move.

## `visibleWhen` / `requiredWhen`

```ts
F.datetime('publishedAt')
  .label('Published at')
  .visibleWhen('status', 'published')
  .requiredWhen('status', 'published')
```

with `F.select('status')…live()` driving it. The matcher compiles to JsonLogic:

```jsonc
{ "visibleWhen": { "===": [{ "var": "status" }, "published"] } }
```

A bare value becomes `===`, an array becomes `in`, and a `when(…)` expression
passes through — `when.all`, `when.any`, `when.not`, `gte`, `contains`,
`isTruthy` and the rest compose without a server round trip. A closure matcher
is allowed but leaves the declarative path: it forces a descriptor refetch.

**The mirror is what matters.** While `status` is not `published`:

- the client hides the field, and
- the server prunes the key before validation, omits it from the validator, and
  omits it from the fill set.

So a client that submits `publishedAt` anyway changes nothing:

```sh
curl -X POST /admin/posts -d 'status=draft&publishedAt=2020-01-01T00:00:00.000Z&…'
```

stores `published_at = NULL`. Flip `status` to `published` and the same key
becomes **required** — enforced by the compiled validator, not by the widget:

```jsonc
{ "publishedAt": "The publishedAt field must be defined" }
```

`subtitle` in the tutorial app is the same rule with a deliberately silly
trigger, so you can watch it happen: type `special` into Title and the field
appears; save with anything else in Title and a submitted subtitle is discarded.

## The rest of the catalogue

The twenty patterns Adonia supports — and the exact JsonLogic each compiles to
— are in the [reactivity catalogue](/reactivity/catalogue), with a JSON
fixture per pattern that the server and client test suites both run. The four
mechanisms are worth knowing by name:

| Mechanism | Example | Cost |
|---|---|---|
| JsonLogic conditions | `visibleWhen`, `requiredWhen`, `disabledWhen` | none — evaluated in the browser |
| `sets` derivations | slugify, uppercase, copy | none |
| Capability URLs | dependent options, searchable relations | one JSON request |
| Server closures | `rules(v => …)` cross-field checks | submit-time only |

Prefer them in that order. Everything above the line is mirrored automatically;
everything below it is server-side by definition.

Next: [theming and tests](/guide/tutorial/theming).

Source: https://adonia.pages.dev/guide/tutorial/reactivity/index.mdx
