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

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(...) 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 (innerinnr) is silently ignored. Keep this table at hand, or consult /srv/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',
    )
}

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