Aquargin Way / Master rtext / Module R5

Events, Navigation & Charts

The last mile. You can already read a record and bind it to the page — now make pages talk to each other, move around the app without breaking it, and turn a query into a chart with a single call. Finish this and you've walked all three trails.

5 units ~20 min Earn the rtext Master stamp
Unit 1 of 5

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:

child → parent
// 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:

global broadcast
// 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:

embedded sub-page
<!-- 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>
Children don't leak their data

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.

Unit 2 of 5

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: if target starts with / or http it behaves like openUrl, otherwise it treats the value as a record id and calls openRecord.
  • 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}
navigating
// 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 list
Subtab or new tab?

Use 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.

Unit 3 of 5

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 vs right
// ✗ 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.

The rule

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.

Unit 4 of 5

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:

ctx.query
rows of data
map
labels + data
ctx.chart
<canvas> renders

A few rules to keep in mind:

  • The target element must be a <canvas> — not a <div>.
  • Supported type values: 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 .data and 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:

a chart from a query
<!-- 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();
Canvas only — and no manual cleanup

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.

Unit 5 of 5

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:

model
one rtext
templates
data-aqua markers
binding
{|.field|}
ctx API
query, on, set…
this module
events · nav · charts

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.

One habit before you ship

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.

Trail complete — you've earned it ★

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. 🎉

Checkpoint

Earn the rtext Master stamp

Five questions to complete Aquargin Way.