6: Views

Views live directly in .phlo files. A view transpiles 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 attribute

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

Both {{ ... }} and {( ... )} output their value raw. For untrusted or user-supplied text, use {[ ... ]}, which HTML-escapes its value (the same as writing {{ esc(...) }}):

view($comment):
<p>{[ $comment ]}</p>
<p>{[ $this->user->name ]}</p>

Reach for {[ ... ]} whenever the value could contain characters a browser would interpret as markup; keep {{ ... }} for values you have already made safe.

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: avoid

6.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(...) builds and renders the response, but it does not stop PHP execution. Return it from a route guard or let the routine end immediately afterwards. 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
sync DOM/store (a mirrored value: applied, never sent on again)
setvar DOM/CSS.var
template DOM/template

No build-time check on apply keys. A typo (innerinnr) is silently ignored. Keep this table at hand, or consult /opt/phlo/docs/apply-protocol.md for 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',
	)
}

Reference. The DOM and view resources are documented per node in the Manual, generated from the resource files so it never drifts from the code.

Last updated on 23 August 2026

We use essential cookies to make this site work. With your permission we also use analytics to improve the site.