Skip to content

View and binding reference

Greg Bowler edited this page Sep 14, 2026 · 4 revisions

// TODO: Read and improve this doc

This page is a compact reference for WebEngine view and binding behaviour. It covers the features we use through WebEngine, plus the main DomTemplate functionality available through the injected Binder.

For walkthroughs, read page views, binding data to the DOM, custom HTML components, and page partials.

View models

WebEngine selects a view model from the matched route:

  • HTML page routes use GT\Dom\HTMLDocument
  • API routes use GT\Json\Schema\JSONDocument
  • empty responses use a null view internally

For normal pages, the HTML document is assembled before page logic runs. That assembled document may include route headers and footers, expanded components, and expanded partials.

HTML response assembly

The view assembly is the ordered list of view files selected by routing. For HTML responses, this usually contains:

  • the page view, such as page/account/settings.html
  • shared _header.html and _footer.html files

Components and partials are expanded later while the HTML document is being prepared. Headers and footers are route-level files. Partials are template composition files. Components are reusable custom elements. They can all contribute to the final document, but headers and footers must not be mixed with partial inheritance in the same response.

View processing order

For HTML responses, WebEngine processes the document in this broad order:

  1. Route matching builds the view and logic assemblies.
  2. The HTML view files are loaded into the document.
  3. Dynamic route classes are added to the <body>.
  4. Components are expanded from the configured component directory.
  5. Partials are expanded from the configured partial directory.
  6. Component logic runs with a component-scoped Binder.
  7. Page logic runs with a document-scoped Binder.
  8. WebEngine applies headers, CSRF output handling, and document cleanup.
  9. The final document is streamed into the response body.

In WebEngine applications, we do not call cleanupDocument() ourselves during ordinary page handling. WebEngine calls it after logic has finished.

Binder injection

Page and component logic can request GT\DomTemplate\Binder from the service container:

use GT\DomTemplate\Binder;

function go(Binder $binder):void {
	$binder->bindKeyValue("title", "Account settings");
}

In page logic, Binder is a document-scoped DocumentBinder. In component logic, Binder is a ComponentBinder scoped to the current component element.

Component logic can also request GT\Dom\Element; this is the component's outer element.

Binding methods

The main binding methods are:

  • bindValue(mixed $value, null|string|Element $context = null): void
  • bindKeyValue(string $key, mixed $value, null|string|Element $context = null): void
  • bindData(mixed $data, null|string|Element $context = null): void
  • bindList(iterable $listData, null|string|Element $context = null, ?string $templateName = null): int
  • bindListCallback(iterable $listData, callable $callback, null|string|Element $context = null, ?string $templateName = null): int
  • bindTable(mixed $tableData, null|string|Element $context = null, ?string $bindKey = null): void

The optional context can be:

  • null, meaning the whole document or whole component
  • an Element
  • a selector string

If a selector context does not match, DomTemplate throws ContextElementNotFoundException.

Binding syntax

The usual HTML syntax is:

<span data-bind:text="name">Guest</span>
<a data-bind:href="profileUrl" href="/login">View profile</a>

The part after data-bind: is the bind property. The attribute value is the bind key.

Common bind properties:

  • text sets text content
  • html sets inner HTML
  • value sets form values
  • class adds or toggles class names
  • list binds a nested iterable
  • table binds table data
  • remove removes the element conditionally
  • any other bind property is treated as a normal attribute name

text, innertext, inner-text, textcontent, and text-content are aliases. html, innerhtml, and inner-html are aliases.

Use html only for trusted HTML. For user-supplied or ordinary text, use text.

Placeholders

Placeholders use {{curly braces}} inside text or attribute values:

<a href="/user/{{id}}">View {{name ?? this user}}</a>

The fallback after ?? is used when the value is null or an empty string.

Use placeholders when only part of a string changes. Use data-bind:* when the whole property should be replaced.

Bind modifiers

DomTemplate supports modifier characters inside bind expressions.

? boolean attributes

<button data-bind:disabled="?isArchived">Archive</button>

Truthy values add the attribute. Falsey values remove it.

Use ?! or !? for the inverse:

<button data-bind:disabled="?!isEditable">Save</button>

Boolean equality

<input type="radio" name="size" value="m" data-bind:checked="?size=m" />

The attribute is added when the bound value equals the comparison value.

: token lists

<li data-bind:class=":isSelected selected"></li>

Truthy values add the listed token; falsey values remove it. This is most often used with class.

Use :! or !: for the inverse:

<li data-bind:class=":!isVisible hidden"></li>

@ attribute reference

<input name="email" data-bind:value="@name" />

This uses the current element's name attribute as the bind key. The shorthand @ means the same as @name.

Modifier expressions can be combined and separated with semicolons:

<div data-bind:class=":isSelected selected; :isAdmin admin"></div>

Rebinding

After a bind property is used, DomTemplate usually removes it so later bind calls do not keep changing the same element.

Use data-rebind when an element should remain bindable during the same request:

<button data-bind:disabled="?isBusy" data-rebind>Save</button>

Optional elements

data-element marks an element as optional. If it is not bound by the time cleanup runs, it is removed from the output.

<p data-element data-bind:text="error">Something went wrong.</p>

data-element="key" keeps or removes an element based on a bound key:

<button data-element="isAdmin" name="do" value="delete">Delete</button>

In WebEngine, this cleanup happens automatically after page logic.

Data shapes

bindValue() and bindKeyValue() accept scalar values, booleans, Stringable objects, DateTimeInterface values, and callables that return a bindable value.

bindData() accepts:

  • associative arrays
  • objects with public properties
  • objects with #[Bind] methods
  • objects with #[BindGetter] methods
  • objects with asArray()
  • iterable objects that also expose bindable properties

Indexed arrays are not valid for bindData(). Use bindList() for repeated indexed data.

Object binding

Public properties are bindable by default:

readonly class Customer {
	public function __construct(
		public string $id,
		public string $name,
	) {}
}

Computed values can be exposed with attributes:

use GT\DomTemplate\Bind;
use GT\DomTemplate\BindGetter;

#[Bind("displayName")]
public function buildDisplayName():string {}

#[BindGetter]
public function getTotalCost():string {}

#[BindGetter] converts getter names into bind keys:

  • getName() becomes name
  • getTotalCost() becomes totalCost
  • getFormattedDate() becomes formattedDate

Nested values can be addressed with dot notation:

<span data-bind:text="address.country.name">Country</span>

If an object defines asArray(), DomTemplate uses that representation for key/value binding.

Lists

A repeated element is marked with data-list:

<ul>
	<li data-list>
		<span data-bind:text="name">Product</span>
	</li>
</ul>

Bind it with:

$binder->bindList($productList);

Named list templates are useful when more than one list exists:

<li data-list="products">
	<span data-bind:text="name">Product</span>
</li>
$binder->bindList($productList, templateName: "products");

The reserved bind key {{}} exposes the current iterable key.

Nested lists can be bound recursively. data-bind:list="propertyName" binds a nested iterable property from the current item.

bindListCallback() works like bindList(), but lets us inspect or adjust each cloned template and row before binding.

<template data-list> can be used for list markup that should not appear directly before binding. Add data-list-keep-template when the <template> wrapper should remain in the final output.

Tables

Tables are bound with bindTable() and data-bind:table:

<table data-bind:table="sales"></table>
$binder->bindTable($salesTable, bindKey: "sales");

Supported table data shapes include:

  • row-oriented arrays where the first row contains headings
  • associative row lists
  • heading-to-value-list maps
  • double-header tables
  • traversable equivalents

Existing table headings can define output columns. Use data-table-key when the visible heading text differs from the data key:

<th data-table-key="firstName">First name</th>

For complex row markup, place data-list on the row that should be repeated.

Components

Component files live in the configured component directory, usually page/_component.

<profile-summary />

maps to:

page/_component/profile-summary.html
page/_component/profile-summary.php

Component names should contain a hyphen. The optional src attribute loads the component from a subdirectory:

<date-picker src="calendar" />

Component HTML is expanded before component logic runs. Component logic receives a scoped Binder, so binding operations stay inside the current component element.

When a component contains a POST form, DomTemplate can add a hidden __component field so server-side logic can identify the submitting component.

Partials

Partial files live in the configured partial directory, usually page/_partial.

A page can extend a partial with an opening INI comment:

<!--
extends=base-page

[vars]
title=Orders
-->

The partial must contain exactly one data-partial injection point:

<main data-partial></main>

Partials can extend other partials. WebEngine expands the chain before page logic runs. DomTemplate also supports [vars] values for partial-driven binding during expansion.

Headers and footers

_header.html and _footer.html files are discovered through routing rather than DomTemplate partial expansion.

They can exist at the root of page/ or in nested route directories. Outer headers are applied before inner headers; inner footers are applied before outer footers.

Use headers and footers for route-wide page framing. Use partials when a page explicitly extends a layout. Do not mix the two layout approaches in one response.

Dynamic body classes

For HTML routes, WebEngine adds classes to the document body based on the matched dynamic URI. This gives CSS a route-aware hook without page logic.

For example, dynamic route parts are normalised so slashes become -- and @ markers become _.

Debugging binds

Add data-bind-debug to an element or scope to record where bind operations came from:

<section data-bind-debug>
	<h1 data-bind:text="name">Guest</h1>
</section>

After binding, populated debug attributes can show source file and line information. Empty debug markers are removed during cleanup.

X-Logic-Execution is a separate WebEngine response header that shows which logic functions ran during the request.

Cleanup

DomTemplate cleanup removes binding helper attributes and optional unbound elements from the final output.

It removes attributes such as:

  • data-bind:*
  • data-list
  • data-template
  • data-table-key
  • data-element
  • internal data-bound markers

In plain DomTemplate usage, call cleanupDocument() after binding. In WebEngine, cleanup is part of the response lifecycle.

Common exceptions

Common DomTemplate exceptions include:

  • ContextElementNotFoundException
  • IncompatibleBindDataException
  • InvalidBindPropertyException
  • ListElementNotFoundInContextException
  • IncorrectTableDataFormat
  • TableElementNotFoundInContextException
  • ComponentDoesNotContainContextException
  • PartialContentDirectoryNotFoundException
  • PartialContentFileNotFoundException
  • PartialInjectionPointNotFoundException
  • PartialInjectionMultiplePointException
  • CommentIniInvalidDocumentLocationException
  • CyclicRecursionException

Next, move on to Errors and logging.

Clone this wiki locally