6: Async
Now make voting instant. Phlo's frontend runtime intercepts forms and links marked async, posts them in the background, and applies the server's DOM commands to the page. No reload, no client-side state, no API layer: the same route answers both worlds.
6.1: Switch on the frontend
Add two resources to data/app.json:
{
"resources": [
"DB/DB",
"DB/model",
"DB/JSONDB",
"DB/JSON.result",
"payload",
"phlo.async",
"DOM/form"
]
}
phlo.async is the async request engine in www/app.js (which view() already includes on every page), and DOM/form teaches it to submit forms. Rebuild:
php www/app.php build::run
The output lists *app.js: the frontend bundle was regenerated.
6.2: Routes have a sync side and an async side
Every route is sync-only unless you say otherwise:
- no modifier: regular HTTP requests only
async: requests from the Phlo frontend onlyboth: both
Change the vote route to serve both, and branch on the request type:
route both POST poll vote $id {
if (!$option = type_poll::record(id: (int)$id)) return false
type_poll::change('id=?', (int)$id, votes: $option->votes + 1)
if (%req->async) return apply(
outer: ['#results' => $this->results],
)
location('/poll')
}
%req->async is true when the Phlo frontend made the call. Async requests get an apply(...) response; plain browsers still get the redirect. One route, two transports, zero duplication.
Note the trailing comma inside apply(...): in Phlo, every line of a multiline argument list ends with a comma, including the last one. That comma is what tells the parser the statement continues.
6.3: Mark the form as async
The frontend only intercepts elements with the async class. One character in the choices view:
view choices:
<section.card>
<foreach type_poll::records() AS $option>
<form.async method=post action="/poll/vote/$option->id">
<button>$option->option</button>
</form>
</foreach>
</section>
<form.async ...> is the dot shorthand for class="async". The same works for navigation: <a.async href="/poll"> loads a page through the async pipeline with a view transition instead of a full reload.
Reload http://localhost/poll once (to pick up the new markup), then vote. The bar animates to its new width, the counts update, and the page never reloads. The transition: width .4s from chapter 4 is doing the easing.
6.4: What apply() actually sends
apply() returns JSON commands that the frontend executes against the DOM. Watch it happen:
curl -s -X POST -H "X-Requested-With: phlo" http://localhost/poll/vote/1
{"outer": {"#results": "<section id=\"results\" class=\"card\">..."}}
outer replaces the element's outerHTML; the server rendered the results view and shipped it as a string. Other commands work the same way: inner, append, remove, class, value, title, path, scroll, and more, all named arguments to one apply() call. There is no build-time check on command names, so a typo like innr: is silently ignored; keep the command table from the guide at hand.
Vote from two different browser tabs and you will spot the last gap: the other tab does not move until you reload it. Hold that thought for chapter 8. First: more languages.