re-frame.fresco.forms¶
Use this module for a text field that edits a draft and commits it on Enter or
blur. buffered-field keeps the draft in app-db in front of the committed value,
and at commit time decides whether the commit still belongs to the edit the user
made. For a field whose every keystroke should update the model, a plain controlled
input is enough: [:input {:value v :on-input [:todo/title-edited id ::h/value]}].
It is an optional namespace: an application that never requires it carries none of its code. It is ClojureScript only; the namespace does not exist on the JVM.
It is built from ordinary Fresco parts: the draft is an h/reg-state concern, the
reset is ::h/revision, the protocol is three ordinary events, and the field is
one h/defview, so it adds no hooks beyond a view's own.
Forms teaches the interaction protocol, the rejection
rule and the recipes the module leaves to the application.
The field¶
buffered-field¶
- Kind: component (Fresco view)
- Signature:
- Description: Renders a controlled
<input>whose edits go to anapp-dbdraft in front of the committed value.- The first edit starts a draft; focus alone does not. Enter and blur both
dispatch
:on-commitwith the draft text appended:[:todo/title-committed id "new text"]. Escape discards the draft, shows:valueagain and dispatches:on-cancelif given. Unmount neither commits nor cancels, so a virtualized row keeps its draft. - The
:on-commithandler accepts by writing the text as the new value. To normalise or reject it, write another value or none, and advance the revision as well: the field shows the draft while the revision matches, and:valueonce it moves. A committed or cancelled draft is removed, so Enter followed by blur commits once and a blur after Escape commits nothing. After a revision change, the next render's callbacks cannot commit the old draft. A saved protocol event with the old revision can still match that draft: the fence is the rendered revision, not a lookup of the domain value. - Every keystroke writes the draft to
app-db, which keeps the edit visible to tests and Xray. For a dense grid where that is too much, use an uncontrolled input or a React component mounted throughh/defhost. - The protocol is three events in the module's own keyword namespace:
[::edit control revision text]on:on-inputwrites the draft;[::commit control revision on-commit]on Enter and on blur ends a live draft and dispatcheson-commitwith the text appended; and[::cancel control revision on-cancel]on Escape ends a live draft and dispatcheson-cancel. A commit or cancel carrying a revision other than the draft's does nothing. They are written into the field rather than exported as names; a test that drives the field by hand spells them throughre-frame.fresco.test.forms.
- The first edit starts a draft; focus alone does not. Enter and blur both
dispatch
- Options:
:control(required): a keyword, string, number or vector, followingreg-state's address rules. It is an opaque identity for the draft, not anapp-dbpath. Make it identify the form instance and the field; two fields with one address share one draft.:value: the committed value.::h/revision: a counter you keep beside the value and advance to throw a draft away. A field that is never rejected, rewritten or reset may pass a constant such as0.:on-commit: an event vector, dispatched with the draft text appended on Enter and on blur.:on-cancel(optional): an event vector, dispatched on Escape.- Any other prop reaches the
<input>unchanged, with:typedefaulting to"text". The field writes:on-input,:on-blurand:on-key-downitself, so a value you pass at one of those is replaced.
- Errors: the module has no error ids of its own. Two faults surface through
other ids:
- A
nilor otherwise invalid:controlfails the draft read: every render emits:rf.error/sub-exception, whose exception carriesreg-state's:rf.error/fresco-state-bad-argument, and the field shows:value, never a draft. - A
:typewhose value is not its text, such as"checkbox", raises:rf.error/fresco-revision-not-controlledwhile:valueisnil.
- A
- Example:
[forms/buffered-field {:control [:todo id :title] :value (h/sub [:todo/title id]) ::h/revision (h/sub [:todo/title-revision id]) :on-commit [:todo/title-committed id] :aria-label "Todo title" :placeholder "What needs doing?"}] ;; Accept a non-empty title and reject an empty one; advance the revision ;; either way, so a rejected draft is replaced by the committed title. (rf/reg-event :todo/title-committed (fn [{:keys [db]} [_ id text]] {:db (cond-> (update-in db [:todo id :title-revision] (fnil inc 0)) (seq text) (assoc-in [:todo id :title] text))}))
The draft's home¶
drafts¶
- Kind: var
- Signature:
- Description: The
h/reg-stateconcern every buffered draft lives under, and the address an application clears to end a draft: on route entry, an explicit cancel, or a successful save reply.- The value under one control is
{:revision r :draft text}. Absence means no editing session, and is the only way to say so, which is what makes a repeated commit idempotent. - A draft survives re-render, remount, virtualization and navigation, so ending one is the application's call.
- The value under one control is
- Example:
See also¶
- Fresco API reference — every Fresco name, with the chapter that teaches it.
re-frame.fresco—h/reg-stateand::h/revision.