Aquargin Way / Build Pages / Module P4

Variables & the Object Pattern

A page is only as good as the data it holds. Here you'll learn how a custom page declares its data — typed variables, the one big grouping rule, and how the page's own values play against the record it sits on.

4 units ~16 min Earn the Data Binder stamp
Unit 1 of 4

Page variable types

A custom page declares the data it works with as typed variables. Each one has a name and a type, and that type is what makes everything downstream — bindings, placeholders, form fields — behave correctly.

You won't write much markup before you need a place to put data: the customer's name as they type it, a running total, a flag for whether a modal is open. Every one of those is a declared variable. Here are the kinds you'll reach for:

  • Object variables — hold a whole record, bound to an object's API. Shape: {name:'invoice', type:{name:'object'}, typeDefinition:{api:'invoice__m'}}.
  • Wrapper variables — a custom shape you define (a bag of named elements). This is the heart of Unit 2.
  • Scalars — a single value: text, number, boolean, date, currency.
  • List-type variables — a collection (rows for a table, options for a repeater).
  • UI-state flags — booleans that drive the screen, not the data: {name:'isSubmitting', type:{name:'Boolean'}, default:'false'}.

The reason types matter is one helper: ctx.bind. When you bind a form input to a variable, ctx.bind auto-resolves the input's type from the variable's declared type. Declare a field as currency and the input shows its symbol; declare it as a date and it parses dates. You almost never pass an explicit {type:...}.

object
a record
wrapper
custom shape
scalar
text / number…
list
a collection
flag
UI boolean
Bind only to declared data

Every ctx.bind path must point at a declared variable (or a wrapper element, or a record.* field). You can not bind to a value you only made in JavaScript — e.g. ctx.set('x', 0) with no matching variable. That's blocked at save. If you want to bind it, declare it first.

Unit 2 of 4

The object pattern

Here's the one rule that shapes every page you'll ever build: group related form fields into a single wrapper instead of scattering them across many loose scalars.

Say your page collects a customer's name, email, phone, amount and a note. The tempting way is five separate variables. Don't. Compare:

wrong — five loose scalars
// five unrelated variables, no shared shape
{name:'customerName',  type:{name:'text'}}
{name:'customerEmail', type:{name:'text'}}
{name:'customerPhone', type:{name:'text'}}
{name:'amount',        type:{name:'number'}}
{name:'note',          type:{name:'text'}}
right — one formData wrapper
// 1. define the wrapper shape "formData" with its elements
formData.elements = [
  {name:'customerName',  type:{name:'text'}},
  {name:'customerEmail', type:{name:'text'}},
  {name:'amount',        type:{name:'number'}}
]

// 2. declare ONE variable that references that wrapper
{name:'formData', type:{name:'Wrapper'}, typeDefinition:{name:'formData'}}

The payoff is everywhere. Your bindings become a clean dotted path — ctx.bind('#name', 'formData.customerName') — and when you hand the data to a function, you send one object param instead of N scalars:

function params — one object, not five
// before: five params to keep in sync
[{name:'customerName',  value:'customerName'},
 {name:'customerEmail', value:'customerEmail'}, /* …and on */]

// after: one tidy object
[{name:'formData', value:'formData'}]
It's enforced at save — design with it from the start

This isn't a style suggestion. At save, the platform auto-consolidates loose scalars into a formData wrapper and rewrites your binding paths to match. If you build the messy way, the platform fixes it for you — but you'll be debugging paths that suddenly read formData.customerName. Build with the wrapper from the first keystroke and there's nothing to untangle.

For developers — what stays standalone

Consolidation is smart, not blunt. It groups the loose form-input scalars and leaves everything else alone. These variables are never pulled into formData:

  • UI-state booleansisLoading, isSubmitting, showModal, canEdit, hasError.
  • Input-connected variables — anything already wired to a live input.
  • Iterator variables — a loop's current item.
  • Already-structured variables — object, wrapper and lookup types.
  • List-type variables — collections stay collections.

So your isSubmitting flag and your invoice object survive untouched; it's only the flat name/email/amount scalars that get folded together.

Unit 3 of 4

record vs pageData

A page sees two data sources at once: the current record it's attached to, and its own pageData — the variables you just declared.

Knowing which is which keeps you out of trouble:

  • record.* — the current record's fields. Read-only, resolved from the object's schema at runtime. Use it to display what's already saved: record.accountName, record.total.
  • pageData — the page's own declared variables. These override record fields of the same name. This is your editable working copy.

Two helpers move data in and out of pageData: ctx.get reads it and ctx.set writes it. Placeholders and bindings merge both sources — and when a name exists in both, pageData wins.

reading & writing pageData
// read a wrapper element out of pageData
const current = ctx.get('formData.amount');

// write a value back into pageData
ctx.set('formData.amount', 100);

// a placeholder in the page's HTML, resolved at render
<span>{|formData.amount|}</span>
Edit your copy, read the record

Bind editable fields to your own variables (formData.*) — those changes live in pageData and are yours to send to a function. When you only need to show a value that's already saved, read it straight off the record with record.*. Editing goes to pageData; display comes from the record.

Unit 4 of 4

Matching field types

A form field's binding must match the object field's type, or it simply won't behave — a date won't parse, a picklist won't list, a lookup won't save.

The good news from Unit 1 carries through: because ctx.bind infers the type from the declared variable, you rarely pass {type:...} by hand. You just declare the variable (or wrapper element) with the right type and let the binder do the matching:

object field → how it binds
lookup    field  →  binds as a lookup   // stores {_id, name}
picklist  field  →  binds as a picklist // carries its options
currency  field  →  binds as currency   // auto-shows the symbol
date      field  →  binds as a date     // parses Mongo date envelopes
text      field  →  binds as text       // the plain default

The mistake to avoid is binding everything as plain text because it's the default. A lookup is not text. A picklist is not text. Declare each wrapper element with the type that mirrors its object field, and the binding carries the right shape.

A lookup bound as text breaks saves

Bind a lookup field as text and it loses its {_id, name} shape — the page shows a string, but the save has no id to write, so it fails. The fix is never to patch the binding call; it's to declare the variable's type correctly (lookup, picklist, currency, date) and let ctx.bind resolve the rest.

That's the Build Pages trail 🎉

You can declare a page's data, group it the right way with the object pattern, tell record.* from pageData, and match every binding to its field type. That's the foundation every page stands on. Pass the checkpoint to claim your Data Binder stamp — then keep going with Trail 03, Master rtext, where the markup, components and scripts come together into a full page.

Checkpoint

Earn the Data Binder stamp

Four questions to complete the Build Pages trail.