Skip to content

Binding lists

Greg Bowler edited this page Apr 12, 2026 · 19 revisions

Binding lists to the document allows us to take any iterative data such as an array of objects from the database, and clone an element in the document for each item in the list. Each cloned element will also have any data-bind:* attributes bound at the point of cloning.

Any element can be marked as a list element with data-list, then DomTemplate can clone it once for each item in an iterable data source.

A simple scalar list

HTML:

<ul>
	<li data-list data-bind:text>Item</li>
</ul>

PHP:

$binder->bindList([
	"Tea",
	"Milk",
	"Biscuits",
]);

Output HTML:

<ul>
	<li>Tea</li>
	<li>Milk</li>
	<li>Biscuits</li>
</ul>

Because the list items are simple strings, we do not need any named bind keys here.

A list of associative arrays or objects

HTML:

<ul>
	<li data-list>
		<a href="/product/{{id}}">
			<strong data-bind:text="name">Product name</strong>
			<span data-bind:text="price">0.00</span>
		</a>
	</li>
</ul>

PHP:

$binder->bindList([
		["id" => 67, "name" => "Pot plant", "price" => 12.99],
		["id" => 20, "name" => "Umbrella", "price" => 9.50],
	]);
]);

Here we can see the two list systems working together: the <li> repeats, and each clone then receives its own key/value data.

An array of associative arrays is used in this example, but wherever possible it is preferred to have a class representation of the data structure - the benefits will become clear as you read on.

Associative scalar lists and the reserved key {{}}

When the iterable keys matter, DomTemplate exposes the current list key through the reserved bind key {{}}.

HTML:

<section>
	<div class="currency" data-list>
		<p data-bind:text="{{}}">CODE</p>
		<p data-bind:text>Name</p>
	</div>
</section>

PHP:

$binder->bindList([
	"GBP" => "Pound sterling",
	"EUR" => "Euro",
]);

Output HTML:

<section>
	<div class="currency">
		<p>GBP</p>
		<p>Pound sterling</p>
	</div>
	<div class="currency">
		<p>EUR</p>
		<p>Euro</p>
	</div>
</section>

Named list templates

If a page contains more than one list template, it is often clearer to name them.

HTML:

<section>
	<ul>
		<li data-list="languages" data-bind:text>Language</li>
	</ul>

	<ul>
		<li data-list="games" data-bind:text>Game</li>
	</ul>
</section>

PHP:

$binder->bindList(["PHP", "TypeScript", "SCSS"], templateName: "languages");
$binder->bindList(["Portal", "Celeste", "Terraria"], templateName: "games");

This is a little easier to reason about than relying on context alone.

Using a context instead of a template name

An alternative approach is to pass the document context of where the list should be bound:

$binder->bindList(["PHP", "TypeScript"], "#language-panel");
$binder->bindList(["Portal", "Celeste"], "#game-panel");

In this example, we would have un-named data-list elements within the individual elements with ID language-panel and game-panel. If more than 1 unnamed list element exists in the same context, a ListElementNotFoundInContextException will be thrown.

Nested lists

DomTemplate can recurse through nested iterable data.

HTML:

<ul>
	<li data-list>
		<h2 data-bind:text>Artist name</h2>

		<ul>
			<li data-list>
				<h3 data-bind:text>Album title</h3>

				<ol>
					<li data-list data-bind:text>Track</li>
				</ol>
			</li>
		</ul>
	</li>
</ul>

PHP:

$binder->bindList([
	"A Band From Your Childhood" => [
		"This Album is Good" => [
			"The Best Song You‘ve Ever Heard",
			"Another Cracking Tune",
			"Top Notch Music Here",
			"The Best Is Left ‘Til Last",
		],
		"Adequate Collection" => [
			"Meh",
			"‘sok",
			"Sounds Like Every Other Song",
		],
	],
	"Bongo and The Bronks" => [
		"Salad" => [
			"Tomatoes",
			"Song About Cucumber",
			"Onions Make Me Cry (but I love them)",
		],
		"Meat" => [
			"Steak",
			"Is Chicken Really a Meat?",
			"Don‘t Look in the Sausage Factory",
			"Stop Horsing Around",
		],
		"SnaxX" => [
			"Crispy Potatoes With Salt",
			"Pretzel Song",
			"Pork Scratchings Are Skin",
			"The Peanut Is Not Actually A Nut",
		],
	],
]);

If we do this, the outer list binds artists, the next list binds albums, and the innermost list binds tracks.

data-bind:list for nested object properties

For richer data, data-bind:list lets us point at a nested list property.

HTML:

<article>
	<h2 data-bind:text="name">Customer</h2>

	<section data-bind:list="orderList">
		<ul>
			<li data-list>
				<strong data-bind:text="id">Order ID</strong>
				for customer <span data-bind:text="customer.email">you@example.com</span>
			</li>
		</ul>
	</section>
</article>

If the bound customer object contains an orderList property, DomTemplate binds that sub-list into the nested template automatically.

Within the bind operations, nested properties can be addressed with dot notation - in the example above customer.email will set the text of the HTML element to the email property of the customer object from the current bound object.

When a custom element is used as the list container, the list name can be inferred from the tag name:

<order-list data-bind:list>
	<ul>
		<li data-list data-bind:text="id">Order ID</li>
	</ul>
</order-list>

order-list maps to the key orderList on the bound object. // TODO: Link to unit test to show this in action.

bindListCallback

bindListCallback works like bindList, but gives us a hook for each iteration.

$binder->bindListCallback(
	$productList,
	function(\GT\Dom\Element $template, array $row, int|string $key):array {
		if(($row["stock"] ?? 0) === 0) {
			$template->classList->add("out-of-stock");
		}

		$row["price"] = number_format((float)$row["price"], 2);
		return $row;
	}
);

This is useful when we want to tweak the cloned element, adjust the data shape, or skip pre-processing elsewhere.

<template data-list>

List templates do not have to be ordinary visible elements. We can also use real HTML <template> elements, which will be unwrapped and removed after binding.

HTML:

<dl>
	<template data-list>
		<dt data-bind:text="{{}}">Key</dt>
		<dd data-bind:text>Value</dd>
	</template>
</dl>

PHP:

$binder->bindList([
	"GBP" => "Pound sterling",
	"EUR" => "Euro",
]);

By default, DomTemplate unwraps the template contents and inserts the child nodes into the document.

Output HTML:

<dl>
	<dt>GBP</dt>
	<dd>Pound sterling</dd>

	<dt>EUR</dt>
	<dd>Euro</dd>
</dl>

data-list-keep-template

If we want the <template> wrapper itself to remain in the output, add data-list-keep-template. This is useful if you are planning to register the template on the client side.

HTML:

<template data-list data-list-keep-template>
	<dt data-bind:text="{{}}">Key</dt>
	<dd data-bind:text>Value</dd>
</template>

Supported iterable types

DomTemplate is happy with:

  • arrays
  • Iterator
  • IteratorAggregate
  • ArrayIterator
  • iterable objects that also expose bindable properties

That means we can often pass domain objects directly instead of flattening everything into arrays first.


When binding lists, we could use associative arrays, but binding objects opens up many more possibilities.

Clone this wiki locally