# bind-text

> Bind a textarea in a freeze region to one record, so long text lives in the record and the editor does not.

Source: https://clayjs.com/docs/plugins/bind-text/

Keeps long text in a record's text element and the editor outside the saved records. Ask for it by name: `?plugins=bind-text`.

## clay.bindText

`clay.bindText(textarea, { onWrite, onShow, onConflict, onGone })` binds a textarea in a `clay="freeze"` region to one record: every keystroke is written into the record, a change from elsewhere shows in the textarea with the caret kept, and an IME composition is held until it ends. Two people editing different paragraphs both keep their words. [OverType: a markdown editor](https://clayjs.com/docs/guide/components/#overtype-a-markdown-editor) shows it with OverType.

It returns the binding, `{ open, close, flush, dispose, current, attach, composing }`:

| Member | Returns | What it does |
|---|---|---|
| `open(textEl, { key, resolve })` | `{ ok: true }` or `{ ok: false, code }` | Opens the record whose text element is `textEl`, showing its text from the start. Opening the same `key` again rebinds through `resolve()` without touching the textarea; opening another `key` flushes the open record first |
| `close()` | `{ ok: true }` or `{ ok: false, code }` | Flushes the open record and releases it |
| `flush()` | `{ ok: true }` or `{ ok: false, code }` | Writes text the record has not taken yet and reports whether the record now holds the textarea's text |
| `dispose()` | `{ ok: true }` or `{ ok: false, code }` | Detaches the textarea's listeners and the observer, once the exit barrier has landed. It never flushes |
| `current()` | the record's text element, or `null` | The record the textarea is bound to |
| `attach(textarea)` | nothing | Moves the binding to a replacement textarea; ignored after `dispose()` |
| `composing()` | `true` or `false` | Whether an IME composition is running |

## Refusals

`flush()` is how the exit barrier asks whether the page holds text the record does not, so its answer is a result rather than a boolean: `{ ok: true }` when the record holds exactly the textarea's text, and `{ ok: false, code }` otherwise.

| `code` | Meaning |
|---|---|
| `composition` | An IME composition is running. It is left exactly as it was, flag included, so it finishes through its own operation |
| `conflict` | The record changed under the textarea and the two edits could not be merged. `onConflict` has the typed text, and the refusal stands until that text is in the record |
| `gone` | `resolve()` returned nothing, so `onGone` has the text and the binding has released the record |
| `busy` | The mode lifecycle is not admitting writes right now, because a mode transition is running. A standalone page has no lifecycle and always admits |

A conflict or a gone record is a refusal, not a success: nothing was represented, so a flush only reports `{ ok: true }` once a resolution or a later flush has put the text in the record. With nothing open, `flush()` is `{ ok: true }`.

`open()` and `close()` return a flush's refusal instead of going ahead: the text that has not landed, the record it belongs to, its textarea and its observer all stay as they were. A record that was gone has already handed its text to `onGone` and released itself, so there is nothing left to protect and the switch goes ahead. `open()` also answers `{ ok: false, code: "disposed" }` after `dispose()`, and `{ ok: false, code: "gone" }` when reopening the same key finds the record unreachable. `dispose()` is idempotent: `{ ok: false, code: "composition" }` while a composition runs, `{ ok: false, code: "pending-input" }` while the record has not taken the text, and `{ ok: true }` once detached, including on a second call.

A refused `open()` leaves the entry that was open still open, so show the selection that is actually in the textarea and retry when the refusal clears:

```js
function showEntry(entry) {
  const result = binding.open(entry.body, { key: entry.id, resolve: () => entry.body });
  markSelected(result.ok ? entry.body : binding.current());
}

// A composition is the refusal that clears itself.
textarea.addEventListener("compositionend", () => showEntry(selectedEntry()));
```

## Callbacks

| Callback | Called when |
|---|---|
| `onWrite(code)` | the binding wrote the textarea's value into the record; `code` is the record's text element |
| `onShow()` | the record's value was put into the textarea, with the caret kept |
| `onConflict(typed, { key, base, remote })` | the record changed while the textarea held text the record had not taken, and the two could not be merged |
| `onGone(typed, { key, base })` | the record the textarea is bound to is gone |

The second argument is additive, so a callback written for the string alone keeps working. `base` is the text both sides were editing, read before the textarea was shown the incoming value: it is what a clash was measured against, and what a caller staging the losing side needs. `remote` is the record's new value; `key` is the key the binding was opened with.

```js
clay.bindText(textarea, {
  onConflict(typed, { key, base }) {
    clay.recovery?.register({ channel: "journal.entries", version: 1 })
      .stage({ key, base, payload: typed });
  }
});
```

A composition is never forced to end. `flush()`, `close()` and `open()` refuse while one runs and leave it where it is, so nothing a switch does can clear the composing flag or interfere with the input. To journal IME text, listen for `input` on the textarea instead, which sees the composed text as it is typed. [Recovery](https://clayjs.com/docs/recovery/) covers the copy that listener keeps.
