One bind, both directions
ctx.bind wires an element to a data path — read, write, event and
re-sync, all in one line.
When you call it, four things happen at once. It reads the current value out
of pageData and paints it into the element. It attaches the right
input / change listener so anything the user types flows back. It
writes that edited value into pageData. And it
re-syncs the element whenever that path changes from elsewhere (a function
response, another bind, code that calls ctx.set). One call, both directions, kept
in step.
The signature is small, and the third argument is optional:
ctx.bind(selector, path, options?)
// selector — a CSS selector for ONE element on the page
// path — a dotted path into pageData (record.* is allowed)
// options — optional; type is auto-resolved from the variableBecause the type is auto-resolved from the declared variable, you usually pass
no type at all. Two text fields, done:
ctx.bind('#name', 'record.name', { required: true });
ctx.bind('#email', 'record.email');Compare that to wiring the same field by hand — read it in, add a listener, push the value back, and you still wouldn't get external re-sync:
// three lines, and no re-sync — don't do this
const el = document.querySelector('#name');
el.value = ctx.get('record.name');
el.addEventListener('input', e => ctx.set('record.name', e.target.value));One ctx.bind replaces all of that — and adds the syncing the manual version forgot.
ctx.bind finds its element once, at bind time. Only use it on
elements that are static — outside any {|#each|} loop or
{|#if|} block. For elements that live inside a loop (and get re-rendered), you use a
delegated handler instead — that's Unit 5.
Every field type
Text, number, currency, phone, date, boolean, picklist, lookup — each is one
ctx.bind. The options just tell the binding what kind of input to manage.
-
Text & number
Plain text needs nothing; a number field declares
type:'number'so the value is coerced to a number, not a string.ctx.bind('#name', 'record.name', { required: true }); ctx.bind('#qty', 'record.quantity__m', { type: 'number' }); -
Currency
Pass
type:'currency'with the owningobjectand the binding renders the correct currency symbol automatically.ctx.bind('#total', 'invoice.total__m', { type: 'currency', object: 'invoice__m' // auto-resolves the symbol }); -
Phone
type:'phone'gives you an automatic country-code dropdown alongside the number input.ctx.bind('#phone', 'record.phone__m', { type: 'phone' }); -
Date
A date bind auto-parses the MongoDB date envelope stored on the record — you point it at the field and it handles the format.
ctx.bind('#due', 'record.due__m'); // auto-parses Mongo dates -
Boolean (checkbox)
Bind a checkbox straight to a boolean field; the checked state and the stored value track each other.
ctx.bind('#active', 'record.is_active__m'); -
Picklist
type:'picklist'with anoptionsarray renders the choices and stores the selected one.ctx.bind('#status', 'record.status__m', { type: 'picklist', options: ['Active', 'Inactive'] }); -
Lookup
type:'lookup'takes the targetobjectand adisplayField; the user searches records, and the binding stores a{_id, name}object — exactly the shape the platform expects for a relationship.ctx.bind('#customer', 'record.customer__m', { type: 'lookup', object: 'customer__m', displayField: 'name' // stores { _id, name } });
ctx.bind auto-resolves the field type from the declared variable.
If invoice.total__m is declared as a currency, or record.customer__m as a
lookup, you can drop {type:'currency'} / {type:'lookup'} entirely — the
binding already knows. The explicit forms above are shown so you recognise the option when you do
need to override it (for example, a picklist whose options are computed at runtime).
Buttons are binds too
A button is just ctx.bind on a 'click' with an
invoke spec — no async boilerplate, no manual loading flags, no error handling to
wire.
Instead of a data path, you pass 'click' and an options object that describes the
call. Here's the full shape, saving an invoice:
ctx.bind('#save', 'click', {
invoke: 'saveInvoice__fx',
params: [{ name: 'invoice', value: 'invoice' }],
loadingWhen: 'isSaving',
responseMap: { result: 'saveResult' },
onSuccess: (r) => ctx.toast('Saved', 'success')
});Reading that top to bottom:
invoke— the function to run. It carries the__fxsuffix because it's wired to the page.params— an array of{name, value}. Thenamemust match the function's declared input; thevalueis apageDatapath. Where the two are identical you can use the shorthand['invoice'], which uses the path as both name and value.loadingWhen— a boolean path the binding toggles for you while the call is in flight; the button auto-disables and shows a spinner, then clears when it returns.responseMap— maps fields off the function's response intopageData(here the response'sresultlands atsaveResult).onSuccess— a callback that runs after a clean response. Errors are auto-toasted, so you only write the happy path.
When you save the page, every invoke is checked: the function must
exist, be compiled and active, and the
params names must match its declared inputs. A typo in the function name or
a param that the function doesn't accept is a save-time error, not a silent runtime failure — you
find out before users do.
Dynamic options
Bindings react to state through their options. The trick is that the *When options take a path, not a fixed value — so the field responds as the data underneath it changes.
The reactive options:
disabledWhen— a path to a boolean. While that path is truthy the field is disabled; flip the boolean and it enables itself.requiredWhen— a path to a boolean that makes the field conditionally required.required— a hardtruethat simply sets the HTMLrequiredattribute.onChange— invokes a function when the field loses focus (on blur), handy for recalculating or validating as the user moves on.
Because disabledWhen and requiredWhen point at data, you never re-run
code to toggle them — set the flag in pageData (or let a function set it) and the
fields follow:
// 'isLocked' and 'needsReason' are boolean paths in pageData
ctx.bind('#amount', 'record.amount__m', {
type: 'currency',
object: 'invoice__m',
disabledWhen: 'isLocked'
});
ctx.bind('#reason', 'record.reason__m', {
requiredWhen: 'needsReason',
placeholder: 'Why was this changed?'
});Other options you'll reach for: placeholder for empty-state text, and
defaultCode to seed an initial value.
Encode rules in the interface with disabledWhen / requiredWhen /
required so the form guides the user — but the server function on save must
still enforce them. A disabled field can be bypassed; the function is the real boundary.
(That's the same lesson from the build trail: validate where it counts.)
Custom & loop handlers
For clicks inside {|#each|} loops — or any custom interaction — you
don't use ctx.bind. You use a delegated ctx.on with a
string selector.
The difference is where the listener lives. ctx.on('.row','click', …) attaches to the
container and matches its descendants by selector. Because it isn't bound to a specific
node, it survives innerHTML re-renders and matches rows that are
loaded later — exactly what a loop needs. Read the row's identity from a
data attribute via e.currentTarget.getAttribute('data-id'):
<!-- in the page HTML: a row per record, carrying its id -->
{|#each records|}
<tr class="row" data-id="{|._id|}">
<td>{|.name|}</td>
<td><button class="row-open">Open</button></td>
</tr>
{|/each|}
// in the page script: ONE delegated handler for every row
ctx.on('.row', 'click', (e) => {
const id = e.currentTarget.getAttribute('data-id');
ctx.set('selectedId', id);
});The rules that keep loop handlers working:
- Pass a string selector — never a cached node. The match is re-evaluated on every event, so re-rendered rows still fire.
- Never
addEventListeneron individual rows; they vanish on the next render and take your listener with them. - Never use inline
onclickattributes — Angular strips them, so they simply won't fire.
ctx.bind for static fields and buttons (resolved once). ctx.on
for anything inside a loop or conditional, and for custom interactions you wire by hand.
Get this split right and your page survives every re-render.
You've met bind, on, set, get and
toast in passing. Module R4 is the full tour of the ctx
object — every method a page script can call. Unsure of a term along the way? The
Glossary has it.
Earn the Binder stamp
Five questions to lock in Module R3.