Decorators¶
A decorator wraps or prepares a variant: a theme provider around the view, a
signed-in session in its frame, a stub in place of an effect. A story or
variant lists the decorators it wants under :decorators, each as a vector of
the decorator's id and any arguments. A variant's decorators sit inside its
story's.
Register one with reg-decorator, in one of three kinds:
;; :hiccup wraps the rendered view. :wrap gets the body and the effective args.
(rf.story/reg-decorator :app/card-frame
{:kind :hiccup
:wrap (fn [body args] [:div.card {:class (name (:theme args :light))} body])})
;; :frame-setup prepares the variant's frame before it renders.
(rf.story/reg-decorator :app/signed-in
{:kind :frame-setup
:init [[:session/restore {:user "ada"}]]
:app-db-patch {:feature-flags {:beta true}}
:teardown [[:session/clear]]})
;; :fx-override stands a stub in for an effect.
(rf.story/reg-decorator :app/no-analytics
{:kind :fx-override
:fx-id :analytics/track
:response nil})
(rf.story/reg-variant :story.login-form/signed-in
{:decorators [[:app/card-frame] [:app/signed-in] [:app/no-analytics]]})
:init events are dispatched and :app-db-patch is merged into app-db before
the view renders; :teardown events run when the variant's frame is destroyed.
A :frame-setup decorator needs at least one of the three. An :fx-override
stub records each call instead of performing the effect, and
:rf.assert/effect-emitted still sees the effect as emitted.
Built-in decorators¶
Four decorators are built in, each named by a Var so a typo fails to compile:
| Var | What it does |
|---|---|
rf.story/force-fx-stub-id |
Stubs one effect for this reference: [rf.story/force-fx-stub-id :rf.http/managed {}]. |
rf.story/layout-debug-measure-id |
Overlays element sizes and spacing. |
rf.story/layout-debug-outline-id |
Outlines every element in its own colour. |
rf.story/layout-debug-pseudo-id |
Forces pseudo-states, #{:hover} by default, or any of :hover, :focus, :active and :visited: [rf.story/layout-debug-pseudo-id #{:focus}]. |
The Layout-debug panel in the right rail switches the three layout-debug overlays on and off for the selected variant, without touching its source.
A decorator for every story¶
Storybook keeps project-wide wrappers such as a theme provider in preview.ts.
In Story it is one registration, made once in your stories namespace:
(rf.story/reg-global-decorator :app/theme
{:kind :hiccup
:wrap (fn [body _args] [:div.app-theme body])})
Every variant now renders inside :app/theme. Globals are the outermost layer,
so the stack reads global, then story, then variant, with the earliest-registered
global outermost. Unlike preview.ts, the chain is data:
rf.story/variant-plan carries the resolved stack under [:world :decorators],
and Docs mode's Decorators table lists the global first. Neither
rf.story/explain nor the Explain panel lists the stack, so check the plan or
Docs mode when you want to know what wraps a variant. The
registration reference covers
clear-global-decorator and the configure! form.
A variant that :extends another takes its parent's decorators unless it
declares its own, which replace them (Composition).
Try the effect stub¶
The login error variant already uses
[rf.story/force-fx-stub-id :rf.http/managed {}]. Open its Tests tab:
the machine reaches the error state from setup without a request leaving
the browser. The submitting variant keeps the same stub but supplies no
failure reply, so it stays pending. Use a :network stub when the reply
path itself is part of the behaviour you want to test.
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
:rf.error/story-decorator-unresolved |
A decorator reference names no registration | Register it or use the built-in id Var. |
:rf.error/decorator-shape |
Required fields for the decorator kind are absent | Use the matching :wrap, frame setup or effect-override form. |
| A child loses its parent's stub | Its explicit :decorators replaces the inherited list |
Include the stub in the child's list, or keep the inherited list. |
| A frame setup dependency remains after closing | It has no teardown event | Declare its cleanup under :teardown. |