Aquargin Way / Build Pages / Module P2

Your First Page

Enough theory — let's build a real screen. By the end of this module you'll have a working “New Contact” page: a clean form that collects a few fields and creates a Contact record when the user clicks Save.

5 units ~20 min hands-on Earn the Page Builder stamp
Unit 1 of 5

What we'll build

A “New Contact” page — a clean little form with a few fields and a Save button that creates a Contact record. Simple, but it exercises every move a data-entry page is made of.

Before placing a single element, say out loud what you're making. On this platform a page always starts with an intent:

intent
INTENT: This is a Data Entry page that creates a Contact.
Shows:   a form — name, email, status, account — and a Save button.
Does:    collects the fields into a contact object, then invokes
         a server function to create the record.

That tells us this is a navigable create screen — a page a user opens, fills in, and submits. It is not a button action. (A one-click “convert”/“approve” action would be a layout_button function — the Zero Rule from Trail 01.) A standalone create page like this one is exactly the kind of screen pages are for.

page
New Contact form
bind
fields → contact
invoke
createContact__fx
toast
“Saved!”

Bind the inputs → collect them into a contact object → invoke a create function → toast the result. Learn that shape and you can build most data-entry screens.

What you need

A Contact object to save into, and a server function — something like createContact__fx — that takes a single contact input and inserts the record. (Trail 01 taught you to build exactly that kind of function.) The API names here (contact__m, account__m, the field apis) may differ in your org — you'll pick the real ones, so you never have to guess.

Unit 2 of 5

The rtext shell

A custom page is one rtext with three parts — HTML, CSS and JS — each edited in its own field. Lay the skeleton first; wire it later.

The Aquargin Code Editor with HTML, CSS and JavaScript tabs, showing the New Contact form markup including a status placeholder.
Fig 1 The rtext editor — your one component's three fields as HTML, CSS and JavaScript tabs. (That's the very New Contact markup you're about to write.)

Start with plain markup: a heading, a couple of labelled inputs (each with an id so we can bind it), a Save button, and a {|status|} placeholder for a confirmation message:

config.html
<section class="new-contact">
  <h2>New Contact</h2>

  <label>Name</label>
  <input id="f-name" type="text">

  <label>Email</label>
  <input id="f-email" type="text">

  <button id="save">Save Contact</button>

  <!-- live status text, rendered by rtext -->
  <p class="status">{|status|}</p>
</section>

A little CSS gives it shape — this lives in the config.css field, never inside the HTML:

config.css
.new-contact { max-width: 420px; display: grid; gap: 10px; }
.new-contact label { font-weight: 600; }
.new-contact .status { color: #2fae74; min-height: 1.2em; }

And the brain — config.js — is empty for now. We fill it in the next three units:

config.js
// variables + binds go here — one line each. Empty to start.
Three fields, never one blob

Keep HTML, CSS and JS in their three separate fields. Do not paste one giant chunk of markup with <style> and <script> tags inside it — the rtext architecture expects them split, and that split is what lets the binding engine wire your elements up.

Unit 3 of 5

The object variable

A page declares what it works with up front. For a create form, that's one object variable to hold the record you're building.

In config.js, declare the page's variables before any bind. We need two: the record itself, and a small UI flag we'll use for the button's loading state:

config.js
ctx.vars([
  { name: 'contact',  type: 'object',  objectApi: 'contact__m' },
  { name: 'isSaving', type: 'boolean' }
]);

The contact variable is an object-pattern variable: it carries a whole record, and every field on the form binds into it — contact.name, contact.email__m, and so on. You group the fields inside the object (or a wrapper), rather than scattering a loose name, email, status as separate scalars. One object in, one object out.

The isSaving boolean is pure UI state — no field, no record, just a flag the button toggles while it waits. Declaring it now means the bind in Unit 5 can lean on it.

Why the object, not loose fields

Collecting fields into a single contact object means your Save function receives one tidy input — {name:'contact', value:'contact'} — instead of a fistful of separate arguments. It also mirrors how the record is shaped in the database. The full object/wrapper story — nested objects, lists of children, repeating rows — is Module P4; for now, one object for one record is all you need.

Unit 4 of 5

Binding the fields

Now the heart of the page: one ctx.bind per field. Each line connects an element to a path inside contact — that's the whole job.

Add these to config.js, right after the variables. Four fields, four binds:

config.js
// 1 — name: a plain text box. Type is inferred from the field.
ctx.bind('#f-name',  'contact.name', { required: true });

// 2 — email: another text input
ctx.bind('#f-email', 'contact.email__m');

// 3 — status: a picklist with fixed options
ctx.bind('#f-status', 'contact.status__m', {
  type: 'picklist',
  options: ['New', 'Active', 'Lapsed']
});

// 4 — account: a lookup to another object, shown by its name
ctx.bind('#f-account', 'contact.account__m', {
  type: 'lookup',
  object: 'account__m',
  displayField: 'name'
});

A few things worth noticing:

  • The path is where the value lives — contact.name, contact.email__m. Type the real field apis from your org; they may differ.
  • Type is auto-resolved from the bound variable, so most binds need no type at all. You only spell it out when you're shaping the control — a picklist's options, or a lookup's target object and display field.
  • Options like {required:true} (and friends such as {disabledWhen:'isSaving'}) ride along in that third argument.

Remember to add the matching <select id="f-status"> and <input id="f-account"> to your HTML so these binds have something to attach to.

bind does the boilerplate for you

One ctx.bind wires up everything: it reads the current value into the element, writes the user's input back into contact, listens on the right event for that control, and re-syncs when the data changes. No addEventListener, no ctx.set(...) plumbing, no manual DOM reads. That's why a whole form is just a handful of lines.

Unit 5 of 5

Save it & test

One last bind wires the button to your server function. Then open the page, fill it in, and watch a Contact appear.

The Save button isn't a link and isn't an onclick — it's a ctx.bind on the click event that invokes your function:

config.js
ctx.bind('#save', 'click', {
  invoke:      'createContact__fx',
  params:      [{ name: 'contact', value: 'contact' }],
  loadingWhen: 'isSaving',
  onSuccess:   () => ctx.toast('Saved!', 'success')
});

Read that left to right: on click, invoke the function createContact__fx, hand it one param named contact whose value is our contact variable, flip isSaving true while it runs (so a {disabledWhen:'isSaving'} control greys out), and on success pop a toast. That's the entire submit path — five lines.

Invocations are checked at save

When you save the page, every invoke is validated against the real function. The target — here createContact__fx — must exist, be compiled, and be active, and each params name must match one of the function's declared inputs. Get the name or a param wrong and the page won't save until you fix it. (The __fx suffix marks a function callable from a page.)

  1. Save the page

    Hit Save. The rtext compiles, the binds register, and the invoke is validated. If createContact__fx isn't there or isn't active, you'll hear about it now — not from a confused user later.

  2. Open it and fill it in

    Navigate to your new page. Type a name and email, pick a status, choose an account. Each keystroke flows straight into the contact object behind the scenes.

  3. Click Save & confirm

    Click the button. createContact__fx runs, the new Contact lands in the database, and the {|status|} placeholder updates while the “Saved!” toast confirms it. Open the Contact list — your record is there.

You just built a page 🎉

One rtext, one object variable, a few binds, and a save button — a complete create screen, saved and tested. Next, Module P3 — Page Archetypes shows the other shapes (lists, dashboards, detail screens) you'll reach for. And when your form needs nested objects and repeating rows, Trail 03 — Master rtext goes deep. Pass the checkpoint to claim your Page Builder stamp.

Checkpoint

Earn the Page Builder stamp

Five questions on what you just built.