Cross-page events
Pages talk to each other through events — not through shared globals. One page raises an event; another listens for it. That's the whole model.
You reach for events whenever one page needs to react to something that happened on another. There are two directions, and you pick by who needs to hear it:
- Child → parent — an embedded sub-page tells the page that hosts it that something happened. One level up, no further.
- Global broadcast — any page announces something to any other page that cares, no matter how they're related.
Child to parent
A sub-page raises the event with ctx.emitToParent; the host subscribes with
ctx.onChildEvent, naming the child's data-uid so it knows which child
to listen to:
// inside the CHILD sub-page — bubble one level up
ctx.emitToParent('orderSubmitted', { orderId: '123' });
// inside the HOST page — listen to that specific child by its data-uid
ctx.onChildEvent('child', 'orderSubmitted', payload => {
ctx.toast('Order ' + payload.orderId + ' came in');
});Global broadcast
When the listener isn't your child — it's a sibling, or a page in another tab entirely — use
ctx.emitGlobal to broadcast and ctx.onGlobal to subscribe. Any page
to any page:
// any page can announce…
ctx.emitGlobal('filterChanged', { status: 'Active' });
// …and any other page can listen
ctx.onGlobal('filterChanged', payload => {
ctx.toast('Now showing: ' + payload.status);
});Where the children come from
You embed a sub-page with a single marker — the same one-marker discipline as everything else in rtext. It gets its own isolated namespace, which is exactly why events exist as the way to coordinate it:
<!-- one marker: which page, what it gets in, its uid for events -->
<div data-aqua="page" data-page-id="<id>"
data-input="customerId:customer._id"
data-uid="child" contenteditable="false"></div>An embedded sub-page runs in an isolated namespace: its pageData
is its own and never spills into the parent (and the parent's never spills in). So events aren't
just a way for a host to coordinate its children — they're the sanctioned
channel. If you find yourself wishing for a shared global, you want an event.
Navigating the app
Move around the app the right way — open things in in-app subtabs for drill-down, or in a new browser tab when that's what the user wants.
The ctx object gives you a small, deliberate set of navigation calls. Reach for the
one that matches the intent:
ctx.openRecord(id)— opens a record in an app subtab. Pass(id, false)to load it without switching to it (open in the background).ctx.openPage(pageId)— opens a custom page in a subtab.ctx.openUrl(url)— opens a new browser tab. Use the internal URL formats below to point it at app content.ctx.navigate(target)— the convenience wrapper: iftargetstarts with/orhttpit behaves likeopenUrl, otherwise it treats the value as a record id and callsopenRecord.ctx.refresh()/ctx.refreshRecord()— reload the current record in place.
Internal URL formats
When you hand a URL to openUrl (or build a shareable link), use these shapes:
- A record —
/object/{api}/record/{id} - A custom page —
/page/{pageApi} - A list —
/object/{api}
// in-app drill-down: open a record in a subtab
ctx.openRecord(customer._id);
// open it in the background without switching tabs
ctx.openRecord(customer._id, false);
// open a custom page in a subtab
ctx.openPage('order_dashboard');
// new browser tab, pointed at app content via an internal URL
ctx.openUrl('/object/order__m/record/' + order._id); // a record
ctx.openUrl('/page/order_dashboard'); // a page
ctx.openUrl('/object/order__m'); // a listUse ctx.openRecord / ctx.openPage for in-app drill-down
— the user stays inside the workspace and can flip between subtabs. Use ctx.openUrl
when you genuinely want a fresh browser tab, or to produce a link the user can
copy and share.
Never <a href> for internal links
There's exactly one navigation mistake that breaks everything: using
<a href> for an app route. Make this the rule you never break.
A raw <a href="/object/..."> tells the browser to load that URL.
That triggers a full page reload, tears down the Angular SPA, and throws away every bit of page
state you'd carefully built up. The link "works" — and the whole app blinks and restarts.
The fix is a tiny, repeatable pattern: render the link or cell with a data-id, then
wire a single delegated handler with ctx.on that calls preventDefault()
and hands the id to ctx.openRecord:
// ✗ WRONG — full page reload, SPA routing dies
<a href="/object/order__m/record/{|._id|}">{|.name|}</a>
// ✓ RIGHT — a data-id link + one delegated handler
<a class="row" data-id="{|._id|}">{|.name|}</a>
ctx.on('.row', 'click', e => {
e.preventDefault();
ctx.openRecord(e.currentTarget.getAttribute('data-id'));
});One ctx.on('.row', …) covers every .row on the page — present or added
later by a re-render — because it's delegated. You don't bind per row.
Never use <a href> for internal links. Always
data-id + ctx.on + ctx.openRecord (or
ctx.openUrl when you truly want a new browser tab). The same goes for buttons, table
rows, and breadcrumbs — anything that points inside the app.
Charts with ctx.chart
A dashboard chart is one call. ctx.chart wires
Chart.js straight into a <canvas> for you — no library setup, no boilerplate.
You give it a selector and a config; it renders and hands back the live Chart.js instance:
A few rules to keep in mind:
- The target element must be a
<canvas>— not a<div>. - Supported
typevalues:bar,line,pie,doughnut,radar,polarArea,bubble,scatter. - It auto-destroys on teardown — when the page goes away, so does the chart.
- It returns the Chart.js instance. Change its
.dataand call.update()to re-render without rebuilding.
Feed the labels and data from a ctx.query — the chart is just a view of your records:
<!-- the target MUST be a canvas -->
<canvas id="salesChart"></canvas>
// pull rows, then shape them into labels + data
const rows = await ctx.query('order__m', { groupBy: 'region' });
const labels = rows.map(r => r.region);
const totals = rows.map(r => r.total);
const chart = ctx.chart('#salesChart', {
type: 'bar',
data: {
labels: labels,
datasets: [{ label: 'Sales', data: totals,
backgroundColor: '#2bb8cb' }]
},
options: { responsive: true }
});
// later — new data? swap .data and update, no rebuild
chart.data.datasets[0].data = nextTotals;
chart.update();ctx.chart throws if the target isn't a <canvas>.
And if you call it again on the same canvas, it destroys the old chart first
before drawing the new one — so re-running it on a data change is safe, and you never destroy a
chart by hand.
You've mastered the Way
That's it — you've completed all three trails of Aquargin Way. Take a breath: you can now build real software on this platform, end to end. 🎉
The rtext trail, from start to finish
You walked the whole language, in order:
The model taught you that a page is one rtext component. Templates
and binding taught you to render data with markers and {|.field|}.
The ctx API gave you the runtime — query, events, DOM. And here you tied pages
together with events, moved around with navigation, and
visualised data with charts.
The whole journey
Step back further and look at everything you built across the three trails:
- Build Functions — server-side logic: receive, read, decide, loop, write.
- Build Pages — custom screens your users actually look at and work in.
- Master rtext — the language that lives inside those pages and makes them move.
Functions do the work, pages present it, and rtext is the connective tissue. You have the full set now.
Before saving a page, run a quality scan. It catches the small issues early —
a marker without a partner, an <a href> that should be a data-id,
a ctx.chart pointed at a non-canvas — long before a user ever sees them.
Your lifelong companions
You don't have to remember it all. Two pages will be open in a tab for the rest of your building life:
- The Function Reference — every standard function and formula, searchable, whenever you need the exact signature.
- The Glossary — every term on the platform, defined in one place.
And any time you want to revisit a module — or walk a trail again from the top — head back to all trails.
Three trails, every checkpoint passed. You came in knowing nothing about Aquargin and you're leaving able to ship functions, pages, and the rtext that brings them alive. Pass the final checkpoint below to claim your rtext Master stamp — and go build something real. Welcome to the Aquargin Way. 🎉
Earn the rtext Master stamp
Five questions to complete Aquargin Way.