- ClayJS
- Reference
- Plugins
- bind-text
bind-text
Bind a textarea in a freeze region to one record, so long text lives in the record and the editor does not.
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 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:
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.
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 covers the copy that listener keeps.