The Fields & bindings section in
the README introduces the primitives. This is the practical
follow-up: an end-to-end walkthrough that builds a small but
complete interactive app in the Inspector, followed by a
quick-reference for looking up individual tasks later. By the end
of the walkthrough you will have produced something equivalent to
test/fixtures/export/demo-store-with-bindings.json
— a filterable product feed with an add-to-cart flow and a live
cart popover — and you can open that file in a second Bareforge
tab to diff your build against the reference.
Note — live commit, no Save button. Every keystroke and click in the Inspector commits to the document immediately. There is no "Save" step; Cmd-Z undoes, the history depth is 100 steps, and changes autosave to IndexedDB as you work. Project files are produced explicitly via File → Save / Open.
- Open the editor:
npx shadow-cljs watch app, navigate to http://localhost:8765. - The Inspector lives in the right-hand rail. It only shows sections that are meaningful for the current selection — the panels appear and disappear as you give a container a name, add a field, etc.
- Three fixtures in
test/fixtures/export/make useful reference checkpoints:demo-store-blank.jsonis the empty starting state,demo-store.jsonis the demo after the layout and fields exist but before bindings/triggers, anddemo-store-with-bindings.jsonis the fully-wired target. Open any of them via File → Open. - You will need a second browser tab open on the same editor URL if you want to diff your build against the reference fixture at the end.
Eleven steps. Each produces a visible change; if a step looks wrong,
stop and compare against the matching fragment of the reference
fixture before moving on. Tags that appear in the steps
(x-card, x-grid, x-popover, …) all come from BareDOM's
palette on the left.
Drag an x-card from the palette onto the canvas. Click it to
select. In the Inspector header, type product into the name
field — the card is now a named group with its own reactive state.
With the group named, a Fields section appears lower in the Inspector. Use the "Add field" form at the bottom three times:
| Name | Type | Default |
|---|---|---|
id |
number | 0 |
title |
string | "" |
price |
number | 0 |
Note. Bareforge inserts a locked
::idfield automatically the first time you name a group; theidrow above just sets its default. You cannot remove the locked field, and its row renders read-only.
Drag an x-grid onto the canvas, select it, and name it
product-feed. Add one field:
- name
product-feed-items, type vector,of-group: product.
When the type is vector a second dropdown appears labelled
"of group" — pick product from it. This has two effects: it marks
product as a template group, and it unlocks a seed-records table
right under the field row. Click "+ Add record" three times and
fill in:
{:id 1 :title "Widget" :price 9.99}
{:id 2 :title "Gadget" :price 8.75}
{:id 3 :title "Gizmo" :price 2.53}Each cell parses according to its field's declared type — number cells coerce on blur, string cells accept anything.
Still on product-feed, add another field:
- name
product-feed-item-count, type number. - Toggle computed on.
- Operation:
count-of. - Source field:
product-feed-items.
Note — computed fields have no "default" input. When you tick "computed", the default-value row is replaced by the operation pickers. If you expected a default box, it's intentional — a derived field has no stored value to seed.
Two more fields on product-feed:
product-search-term, type string, default"".visible-products, type vector,of-group: product, then tick computed and pick:- Operation:
filter-by. - Source field:
product-feed-items. - Search field:
product-search-term. - Match field:
title. - Match kind stays at its one v1 option, "case-insensitive contains".
- Operation:
The resulting field-def shape, lifted from the reference fixture:
{:name :visible-products
:type :vector
:of-group "product"
:computed {:operation :filter-by
:source-field :product-feed-items
:filter-spec {:search-field :product-search-term
:match-field :title
:match-kind :contains-ci}}}At this checkpoint your doc is roughly equivalent to
demo-store.json: three products seeded, the computed count +
filter wired, nothing user-facing yet.
Drop an x-search-field next to the product feed. In the Inspector's
Attributes section, find the value row and click the 🔗 icon next
to it. A grouped picker opens showing every field in every named
group — search for product-search-term and click it. The binding
shows as ↔ product-feed.product-search-term with an × to unbind.
Do the same for a count display: drop an x-typography or an
x-badge, bind its text attribute to
product-feed.product-feed-item-count (read-only, since the target
is a computed field).
Note — binding direction is auto-picked. Bareforge infers
:read,:write, or:read-writefrom the target property's kind. Input-ish widgets (value on search field, checked on switch, text-area value) get:read-write; display props liketexton a badge get:read. The binding row shows the chosen direction; you can't force it manually in v1.
Select the product group's root (the x-card you named in step 1).
Because product-feed's visible-products is :of-group "product",
a new section called Rendered from appears in the Inspector for
this group. In its source-field dropdown, pick
product-feed / visible-products.
Note — "Rendered from" lives on the template, not its host. This is the most common miss. The dropdown lives on the group whose records are being rendered (here:
product), not on the group that owns the collection field (here:product-feed). Select the right side of the relation before you hunt for the section.
At this point, previewing the canvas (or just moving focus off the Inspector) shows the card rendered three times — once per seeded product. Design-time template expansion kicks in as soon as the source field is set.
Drag another x-grid and name it cart-item. Set columns to
1fr auto auto and gap to sm via the Inspector's Attributes
section — the three columns will hold title / price / remove.
Add three fields:
| Name | Type | Default |
|---|---|---|
id |
number | 0 |
title |
string | "" |
price |
number | 0 |
Drop three children into the grid:
- An
x-typographyfor the title — bind itstexttocart-item.title. - An
x-typographyfor the price — bind itstexttocart-item.price. - An
x-button(variant ghost, size sm) containing an×glyph — this is the remove button you'll wire in step 9.
Drag an x-container into your page's header area (or into an
x-navbar's actions slot if you dropped a navbar earlier). Name
it cart. Inside it, drop an x-popover — this is the BareDOM
component that shows the floating panel when the cart icon is
clicked. Set its heading to Cart and its placement to
bottom-end.
Important — set
portaltotrueon the x-popover so the panel z-orders correctly above page content. Bareforge's exported renderer handles portaled children correctly; see CLAUDE.md rule 19 for the story.
Inside the popover (default slot), drop the cart-item group you
built in step 7. Inside the popover's trigger slot, drop an
x-icon (the shopping-cart SVG) and an x-badge next to it.
Back on the cart group itself, add two fields:
cart-items, type vector,of-group: cart-item, default[].cart-items-count, type number, computed,count-of, sourcecart-items.
Now the Actions section appears beneath the Fields section (it only materialises once a group has at least one field). Use its "Add action" form twice:
| Name | Operation | Target field |
|---|---|---|
add-to-cart |
add |
cart-items |
remove-from-cart |
remove |
cart-items |
Note — actions gate on "named + has at least one field." If you add an action before either condition is met the form accepts the input silently but nothing persists. Add a field first.
Two event wirings make the cart go.
Add to cart. Select the Add-to-cart button you dropped inside the
product template (step 6). The Inspector's Events section now
lists press (because the button dispatches a BareDOM press
event). Click its action-picker (🔗). A grouped picker opens,
listing every action across every group; pick
:app.cart.events/add-to-cart. A hint line under the row shows
"receives: product record" — the implicit payload is the enclosing
template group's record. No payload configuration is needed for v1
recipes.
Remove from cart. Select the × button you dropped inside the
cart-item template (step 7). Same flow: Events → press →
:app.cart.events/remove-from-cart. The hint shows "receives:
cart-item record".
Select the x-badge in the popover's trigger slot (step 8). Bind its
text attribute to cart.cart-items-count — a read-binding, since
the target is a computed field. The badge now shows 0 (the cart
starts empty) and will update live as add/remove fire in the
exported project.
File menu → Export ClojureScript (interactive). You get a .zip
named after the current project. Unzip, then inside the exported
directory:
npm install
npx shadow-cljs watch appOpen the served URL. The app behaves as you'd expect — three products visible, typing in the search field narrows the list in real time, clicking Add to cart increments the badge and inserts a row into the popover panel, clicking × on a row removes that row and decrements the badge.
Note — two other export modes are deliberately static. File → "Export HTML (static snapshot)" and "Export bundle (static snapshot)" emit markup only; they don't wire
:events/:bindings/:computedfields. They are useful for PDF-like previews or sharing a visual-only copy. Use the ClojureScript export when the artefact needs to be interactive.
Open test/fixtures/export/demo-store-with-bindings.json via File →
Open in a second tab. Compare the two Inspector trees node by node —
same named groups, same field lists, same actions, same bindings,
same triggers. If you see drift, the walkthrough step that produced
that part of the tree is the one to revisit. For the exported
output, the examples/demo-app/ directory in this repo is a
reference of what the emitted ClojureScript project should look
like.
Ten one-liner recipes pointing back into the walkthrough. Use this as a return-visit index after the first read-through.
- Name a group. Select the container, type a name in the
Inspector header. The locked
::idfield is auto-inserted. See walkthrough step 1. - Add a scalar field. In the Fields section's "Add field" form, pick a type (string/number/boolean/keyword), set a default, click Add. See step 1.
- Add a computed field. Add a field, tick "computed", pick an
operation from
count-of / sum-of / empty-of / negation / any-of / join-on / filter-by, pick source field(s). No default input appears. See steps 3 and 4. - Add a collection field. Add a field of type
vector, pick an existing named group in theof-groupdropdown. That group becomes a template group; seed records appear in the inline table. See step 2. - Edit seed records. Click cells in the seed-records table —
values parse per field type.
+ Add recordappends a new row with auto-incremented::id;×removes a row. See step 2. - Declare an action. In the Actions section (visible only once
the group has a field), use "Add action": name + operation (
set / toggle / increment / decrement / clear / add / remove) + target field. See step 8. - Add a binding. Click the 🔗 next to any attribute widget, search the grouped picker, click the target field. Direction is auto-picked from the property kind. See steps 5 and 10.
- Add a trigger. Select an interactive element (button, switch, search field, …), open Events, click the event's action-picker, pick an action-ref. Implicit payload is the enclosing template record; explicit payloads are doc-only in v1. See step 9.
- Set a template's source. Select the template group's root, scroll to "Rendered from", pick the owning collection field. See step 6.
- Export to ClojureScript. File menu → "Export ClojureScript
(interactive)". Unzip,
npm install,npx shadow-cljs watch app. See step 11.