6: Views
Views live directly in .phlo files. A view compiles to a PHP method that returns HTML. You render a view with view(...).
One rule before everything else: a blank line closes the view. The first empty line after view ...: ends the block; HTML after it becomes controller code and the build stops with HTML outside a view. Never insert a blank line inside a view for visual spacing.
6.1: Declaration
Anonymous view:
view:
<p>Test</p>
Named view:
view home:
<h1>Welcome</h1>
View with arguments:
view greeting($name):
<p>Hello $name</p>
Calling it:
method show => view($this->greeting('Jordi'))6.2: Multiline blocks
A multiline view runs until an empty line. So don't put empty lines in the middle of view HTML.
view:
<section>
<h1>Welcome</h1>
<p>Intro</p>
</section>
view footer:
<footer>Phlo</footer>6.3: HTML shorthand
Phlo supports compact id/class shorthand:
view:
<p#intro.lead/>
This becomes:
<p id="intro" class="lead"></p>
A trailing slash makes a tag self-closing in the source, and Phlo turns it into a normal open/close tag.
One hard rule: a tag uses EITHER the shorthand OR an explicit class/id attribute for that property, never both. Combining them can emit a duplicate attribute and the browser keeps only the first, silently dropping the dynamic one. When part of a class list is dynamic, write the whole thing as one attribute; keep shorthands for fully static tags:
<a.site-logo href="/"> valid: fully static
<a class="card {( $active ? 'is-active' : void )}"> valid: dynamic, one attribute
<a.card class="$extra"> INVALID: duplicate class attribute6.4: Text and variables
You can use plain variables and simple properties directly in text:
view($name):
<p>Hello $name</p>
<p>$this->title</p>
For method calls, chained access, or expressions, use {{ ... }}:
view:
<p>{{ $this->label('start') }}</p>
<p>{{ $this->record->title }}</p>
<p>{{ $this->count > 1 ? 'Multiple' : 'One' }}</p>
{( ... )} exists as a short expression form and is translated internally to {{ (...) }}, but don't use it as the default example. In documentation and app code, {{ ... }} is usually clearer.
6.5: Translatable view text
For static translatable text, use the language shorthand:
view:
<h1>{nl: Welkom}</h1>
<p>{nl: Hallo wereld}</p>
With arguments:
view($name):
<p>{nl: Hallo %s ($name)}</p>
Use the shorthand for this; it is shorter and shows at a glance which source language the text has.
6.6: Attributes
Attribute values without spaces or variables can go unquoted:
view:
<a href=/contact>Contact</a>
With variables or expressions, use quotes:
view:
<a href="$this->url">Link</a>
<a href="{{ $this->url('contact') }}">Contact</a>
Attribute values interpolate $var, $this->prop and %instance->prop directly, including with a literal suffix. Wrapping plain property access in {{ }} is redundant; reserve {{ }} for calls and {( )} for expressions:
<a href="%base->view/install"> valid: direct interpolation plus suffix
<a href="{{ %base->view }}/install"> works, but redundant and ugly: avoid6.7: Control flow
Use control-flow tags on their own lines:
view:
<ul>
<foreach $this->items AS $item>
<li>$item->title</li>
</foreach>
</ul>
With if:
view:
<if $this->active>
<p>Active</p>
<else>
<p>Inactive</p>
</if>6.8: Rendering
view(...) renders the response and stops further request handling. So build composite pages in a single view, or let a view include other view methods inline with {{ ... }}.
route both GET home => view($this)
view:
<main>
{{ $this->hero }}
{{ $this->content }}
</main>
view hero:
<header>
<h1>$this->title</h1>
</header>
view content:
<section>
<p>{nl: Welkom op de site}</p>
</section>
All view() parameters are optional and named:
| Parameter | Does |
|---|---|
title |
Page title, combined with the app title via title() |
css / js / defer |
Extra assets next to the namespace bundles |
options |
Body class list |
settings |
Body data-* attributes |
ns |
Bundle namespace (default app; see chapter 2) |
path |
Browser URL; false keeps the current URL |
inline |
Embed local css/js into the HTML instead of linking |
bodyAttrs / htmlAttrs |
Extra attributes on <body> / <html> |
lang |
Page language |
| trailing named args | Any apply command, e.g. scroll: 0, trans: 'fade' |
App-level defaults come from %app props with the same names. The <head> is further fed by %app->description, %app->viewport, %app->themeColor, %app->nonce, %app->head, %app->link and %app->version (the asset cache-buster).
6.9: Apply commands
apply() accepts named arguments where each key is a DOM mutation or UI action. The runtime core provides the basics; resources can register extra commands via app.mod.<name> (such as DOM/toasts for toast: or DOM/dialog for alert:).
DOM mutations
| Cmd | Argument | Effect |
|---|---|---|
inner |
{selector: html} |
el.innerHTML = html |
outer |
{selector: html} |
el.outerHTML = html |
before / after |
{selector: html} |
Insert adjacent |
prepend / append |
{selector: html} |
Insert inside, first/last |
remove |
selector or array | Remove elements |
attr |
{selector: {attr: value}} |
Set/remove (null = remove) |
class |
{selector: 'a b -c !d'} |
Add / remove (-) / toggle (!) |
value |
{selector: value} |
Form value |
data |
{selector: {key: value}} |
el.dataset[key] |
App state
| Cmd | Effect |
|---|---|
title |
document.title |
lang |
html.lang |
options |
Body classes (replaces) |
settings |
Body data attributes |
path |
history.pushState (URL changes without reload) |
trans |
View-transition class (forward/backward/...) |
scroll |
int (pixels) or #anchor |
Assets (once per href/src)
css, js, defer, add a link or script; already-loaded URLs are ignored.
Navigation and callbacks
| Cmd | Effect |
|---|---|
location |
Path or true (reload current path); an external URL does location.assign() |
call |
Call app[name]() after the apply |
Meta
| Cmd | Effect |
|---|---|
log / error |
console.log / console.error on the client |
phlo |
Server-side debug trace, logged in the browser console (debug mode) |
Resource mods, available once the corresponding resource is loaded:
| Cmd | Resource |
|---|---|
toast |
DOM/toasts |
alert / confirm / prompt |
DOM/dialog |
store |
DOM/store |
setvar |
DOM/CSS.var |
template |
DOM/template |
No build-time check on apply keys. A typo (
inner→innr) is silently ignored. Keep this table at hand, or consult/srv/phlo/docs/apply-protocol.mdfor the complete, up-to-date reference including edge cases and stream semantics.
Example combining multiple commands:
route async POST item save {
if (!$item = item::save(%payload)) return apply(
error: 'Save failed',
class: ['[name=title]' => '!error'],
)
apply(
outer: ['#item-'.$item->id => $this->itemView($item)],
toast: 'Saved',
scroll: '#item-'.$item->id,
trans: 'fade',
)
}