Skip to content

Binding

Greg Bowler edited this page Feb 22, 2023 · 98 revisions

One of the most common usages of DomTemplate is to bind data to the document. DomTemplate uses data-bind and data-template attributes on HTML elements to indicate what, where and how to bind data.

All examples in this section assume an HTMLDocument object is construced and passed to the DocumentBinder constructor. Learn more about constructing DomTemplate objects.

Run all examples in this section locally: https://github.com/PhpGt/DomTemplate/tree/master/examples/binding

Binding single values

An HTML element can have its content or one of its attributes bound from PHP by introducing a data-bind:* attribute. The word after the colon is called the bind property, and indicates what property of the HTMLElement to edit. See below for all possibile bind properties.

When binding a single value, there is no need to specify a matching bind key.

Simplest possible usage

Source HTML:

<h1 data-bind:text>Name</h1>

PHP:

function example(DocumentBinder $binder):void {
	$binder->bindValue("Cody");
}

Output HTML:

<h1>Cody</h1>

What's happening in this example is the <h1> element is having its textContent set to Cody. You may choose to only bind the value under certain circumstances, like if the user has clicked a button for example. If the bindValue() function isn't called, the textContent of the element remains as its default value (in this case, "Name"). This allows default values to be set in the HTML source, requiring no PHP execution.

Jump straight into a more complex example.

Non-trivial usage

In this example, our example function is being passed a DriverRepository object that represents our application's data source. We don't need to know anything about the data source's implementation, only that its getDrivers function can be called to retrieve an array of Driver objects. The implementation of a Driver object will provide the bindable values where appropriate.

Source HTML:

<h1>Top three drivers of <span data-bind:text="year">0000</span></h1>

<ul>
	<li data-template>
		<h2 data-bind:text="name">Name of driver</h2>
		<h3 data-bind:text="team">Team Name</h3>

		<p>Points: <span data-bind:text="points">0</span>
		<div>
			<img src="/flag/{{countryCode}}.png" alt="Flag of {{countryName}}" />
			<p data-bind:text="countryName">Country</p>
		</div>
	</li>
</ul>

PHP:

function example(DocumentBinder $binder, DriverRepository $driverRepo):void {
	$now = new DateTime();
	$currentYear = $now->format("Y");

	// Here we're calling an example data source to get an array of Driver objects.
	$drivers = $driverRepo->getDrivers(
		year: $currentYear,
		orderBy: "points", 
		limit: 3,
	);

	$binder->bindKeyValue("year", $currentYear);
	$binder->bindList($drivers);
}

Output HTML:

<h1>Top three drivers of <span>2020</span></h1>

<ul>
	<li>
		<h2>Lewis Hamilton</h2>
		<h3>Mercedes</h3>

		<p>Points: <span>347</span>
		<div>
			<img src="/flag/GBR.png" alt="Flag of United Kingdom" />
			<p>United Kingdom</p>
		</div>
	</li>
	<li>
		<h2>Valtteri Bottas</h2>
		<h3>Mercedes</h3>

		<p>Points: <span>223</span>
		<div>
			<img src="/flag/FIN.png" alt="Flag of Finland" />
			<p>Finland</p>
		</div>
	</li>
	<li>
		<h2>Max Verstappen</h2>
		<h3>Red Bull Racing Honda</h3>

		<p>Points: <span>214</span>
		<div>
			<img src="/flag/NED.png" alt="Flag of Netherlands" />
			<p>Netherlands</p>
		</div>
	</li>
</ul>

See more advanced examples at the end of this section.

Null values and empty strings

Binding a null has different behaviour to binding an empty string "". Binding an empty string will set the bind property to an empty string, but binding null will leave the HTML unaffected. With this knowledge, you can provide default values within the HTML and further reduce the logic required in PHP to manipulate the data to the desired shape.

Rebinding with data-rebind

Once an element's property has been bound once, its value will not change again with subsequent binds unless the element has a data-rebind attribute. This feature allows for a style of programming where specific areas of the document can be bound first, before applying a document-wide bind. For example, on a "Your orders" page of an e-commerce site, a list of orders can be bound to a specific element. Each Order might have common bind keys such as id, name, etc. After binding this list, the User can be bound to the entire document and any unbound id, name keys will take that of the User.

// TODO: I think the above paragraph is worded confusingly. I need to re-word it to be easier to follow.

The data-bind attribute

On any element that you wish to bind data to, a bind attribute can be set that is made up of the following:

  • the attribute name starting with data-bind
  • followed by a colon
  • followed by the bind property - what property to bind to
  • optionally, a bind key can be supplied as the attribute's value

Some examples:

  • <span data-bind:text="name">Your name</span> - the text bind property will be bound with the value of the name bind key.
  • <img src="/default.svg" alt="Profile image" data-bind:src="profileImageUrl" data-bind:alt="fullName" /> - the src bind property will be bound with the value of the profileImageUrl bind key, and the alt bind property will be bound with the value of the fullName bind key.
  • <button name="do" value="delete" data-bind:class="selected">Delete</button> - the class bind property will add selected to the element's class attribute if the value of the selected bind key is true or truthy.

Bind properties

In a data-bind attribute, the "bind property" is what appears after the colon in the element's attribute name. So in the example data-bind:href="exampleUrl", the bind property is "href".

The bind property is used to indicate what property of the HTMLElement should have dynamic data bound to it. Any property can be used, such as href, src, alt, title, etc. and even other data attributes (data-bind:data-id="id"). There are also some bind properties that have special behaviour, like when you want to toggle the class attribute.

Here is a list of the bind properties that have special behaviour:

  • text, inner-text, innertext, text-content and textcontent are synonyms that will set the textContent property of the HTMLElement
  • html, inner-html and innerhtml are synonyms that will set the innerHTML property of the HTMLElement (what's the difference between innerText and innerHTML?)
  • class will either add a value to the classList of the element, or using modifier characters can toggle the presence of a class in the list
  • table will bind data to rows and cells, maintaining integrity with any headers specified on the table. Binding tables is explained in more depth in its own section
  • list will bind a matching iterable to a contained template element - for more information, read about binding nested lists.

Additionally to the above list, any bind property can be used to set the corresponding element attribute to the bound value with optional usage of modifier characters.

Quick examples

  • <h1 data-bind:text>Name</h1> - the bind property is text, and there is no bind key specified. A value can be passed to the bindValue function, which will set the h1's innerText. The value passed must be a string or Stringable object, or a callable that returns a string/Stringable.
  • <input name="user" data-bind:value="username" required /> - the bind property is value, and the bind key is specified as username.
  • <img src="/blank.png" alt="Profile image" data-bind:src="profileImage" /> - the bind property is src, and the bind key is specified as profileImage. Note that if we do not bind anything to this element, the default src is already set in the HTML.
  • <li class="menu-option" data-bind:class=":selected"><a href="/contact">Contact us</a></li> - the bind property is class and the bind key is selected, with a boolean bind modifier. More about bind modifiers later in this section.

Bind keys

Bind keys are the optional value of the data-bind attribute on an element. For example, <a data-bind:href="url">Click me</a> has a bind key of url, and <h1 data-bind:text>Product Name</h1> has no bind key.

Adding a bind key to a data-bind attribute is optional. When $binder->bindValue($value) is called, it will bind the provided value to all elements in the Document that have a data-bind attribute without a bind key. It's possible to pass a context element as the second parameter like this: $binder->bindValue($value, $element), which can be used to reduce the scope of what elements are bound.

When elements do have a bind key, they can be bound by calling $binder->bindKeyValue($key, $value);, where $key is the name of the bind key to use. This technique allows a single element to set multiple bind properties, but also enables us to use more advanced DOM Template functionality.

Rather than setting each key and value with individual calls to the bindKeyValue function, it's possible to pass a key-value-pair data structure to the bindData function, such as an associative array, or an object with public properties or functions that represent bind keys. This allows you to pass any model of your application directly into the document binder without having to process the data first. Learn more about binding objects.

Bind functions

On any instance of the DocumentBinder, the following bind functions are available to you:

  • bindValue($value, [$context])
  • bindKeyValue($key, $value, [$context])
  • bindData($kvp, [$context])
  • bindList($list, [$context])
  • bindListCallback($list, $callback, [$context])
  • bindTable($tableData, [$context])

All functions take appropriate arguments for binding the type of data they deal with, along with an optional $context argument, which allows you to pass an Element to restrict the scope of the binding within the document.

To keep this documentation easy to read, the following type aliases are used:

  • BindableValue can be any value that can be cast to a string - the actual value that will be bound to the Document. You may pass a scalar value such as an int, or any Stringable object, and PHP will cast the value to a string prior to binding. If a callable is passed, it will be called once at the time of binding, and the string value of what is returned will be used for binding.
  • BindableKVP can be an associative array, ArrayAccess, object with public parameters, or an object with one or more Bind-Attributed functions. Values of the array, object properties or return types of the object's methods must be BindableValue. See binding objects for more information on how to use instances of your application's classes to bind to the Document.
  • BindableTable can be an iterator<int, array<int, BindableValue>>, iterator<int, array<int|string, BindableValue>> or iterator<array<int, BindableValue>>>. See binding tables for more information.
  • BindableList can be an iterator<BindableKVP>. See binding lists for more information.

With all bind functions, the last parameter is an optional Element called $context, which defines the scope of where the binding will occur. Passing an Element object will only bind elements within the tree of that Element. Leave the parameter unset to have the scope set to the whole Document.

bindValue

DocumentBinder::bindValue(BindableValue $value, ?Element $context = null):void

Calling bindValue will set the string representation of $value anywhere within the $context that there is a data-bind attribute that has no bind key (no value to the data-bind attribute).

Example for bindValue Source HTML:
<p data-bind:text>This is a quick example</p>

PHP:

function example(DocumentBinder $binder):void {
	$binder->bindValue("This is an updated example");
}

Output HTML:

<p>This is an updated example</p>

bindKeyValue

DocumentBinder::bindKeyValue(string $key, BindableValue $value, ?Element $context = null):void

Calling bindKeyValue will set the string representation of $value anywhere within the $context that there is a data-bind attribute that has a bind key matching $key.

Example for bindKeyValue

Source HTML:

<h1>Hello, <span data-bind:text="name">you</span>!</h1>

PHP:

function example(DocumentBinder $binder):void {
	$binder->bindKeyValue("name", "Cody");
}

Output HTML:

<h1>Hello, <span>Cody</span>!</h1>

bindData

DocumentBinder::bindData(BindableKVP $data, ?Element $context = null):void

When you have a data stricture that contains multiple key-value-pairs, you can call the bindData function to set each key-value-pair in one operation.

The $data parameter can be an associative array, ArrayAccess, object with public parameters, or an object with one or more Bind-Attributed functions. Values of the array, object properties or return types of the object's methods must be BindableValue. See binding objects for more information on how to use instances of your application's classes to bind to the Document.

Example for bindData

Source HTML:

<h1>User profile</h1>

<div>
	<h2 data-bind:text="username">Username</h2>
	<p>Full name: <span data-bind:text="fullName">Full Name</p>
	<p>Bio: <span data-bind:text="bio">Bio goes here</span></p>
</div>

PHP:

function example(DocumentBinder $binder):void {
	// In a real application, $data might be supplied from the database 
	// and could contain model objects rather than associative arrays.
	$data = [
		"username" => "PhpNut",
		"fullName" => "Larry E. Masters",
		"bio" => "Christian - Dad - 4x Grandad - Co-Founder of @CakePHP - Developer - Open Source Advocate",
	];

	$binder->bindData($data);
}

Output HTML:

<h1>User profile</h1>

<div>
	<h2>PhpNut</h2>
	<p>Full name: <span>Larry E. Masters</p>
	<p>Bio: <span>Christian - Dad - 4x Grandad - Co-Founder of @CakePHP - Developer - Open Source Advocate</span></p>
</div>

bindList

DocumentBinder::bindList(BindableList $listData, ?Element $context = null):int

An HTML Element can be marked as a template element so that it's repeated for every item in the BindableList. This is done by adding the data-template attribute to the element you wish to repeat. By doing this, the original element will be removed from the Document but its original position will be remembered. That way, when a BindableList is bound, the original template element will be cloned for every item in the list, data from each BindableKVP will be bound on each new cloned element, and finally each cloned element will be placed back into the Document in the position of the original template element.

When only one list is represented in a given context, there is no need to add a value to the data-template attribute, but it is possible to bind multiple lists, or even nested lists, using named template elements or providing a context. See binding lists for more information.

Example for bindList

Source HTML:

<h1>Shopping list</h1>

<ul>
	<li data-template data-bind:text>Item name</li>
</ul>

PHP:

function example(DocumentBinder $binder):void {
	$listData = [
		"Eggs",
		"Potatoes",
		"Butter",
		"Plain flour",
	];
	$binder->bindList($listData);
}

Output HTML:

<h1>Shopping list</h1>

<ul>
	<li>Eggs</li>
	<li>Potatoes</li>
	<li>Butter</li>
	<li>Plain flour</li>
</ul>

bindListCallback

DocumentBinder::bindListCallback(BindableList $listData, callable $callback, ?Element $context = null):int

The functionality of bindListCallback is identical to bindList, apart from you can supply a callable as the second parameter, which will be called for every iteration of the BindableList.

The callback will be called with the following parameters:

  • Element $element the newly-inserted element for the current iteration - data will not have been bound yet
  • array $listItem the current iterable value, converted to an associative array
  • int|string $listKey the current iterable key

The callback must return the value of $listItem, allowing you to manipulate it in the callback.

Within the callback, you may wish to modify the template element. The provided $element is a clone of the original template element. It has not had its data bound yet, but it has been attached to the document at the correct location.

Being able to modify the template element and manipulate the value of $listItem is useful for when data is being provided from a source that is difficult/inefficient to manipulate beforehand.

Example 1 for bindListCallback

Source HTML:

<h1>Shopping list</h1>

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

PHP:

function example(DocumentBinder $binder):void {
	$listData = [
		"Eggs",
		"Potatoes",
		"Butter",
		"Plain flour",
	];
	$binder->bindListCallback($listData, function(Element $element, $listItem, $listKey) {
		$element->classList->add("item-$listKey");
		return "$listItem (item $listKey)";
	});
}

Output HTML:

<ul>
	<li class="item-0">Eggs (item 0)</li>
	<li class="item-1">Potatoes (item 1)</li>
	<li class="item-2">Butter (item 2)</li>
	<li class="item-3">Plain flour (item 3)</li>
</ul>
Example 2 for bindListCallback - two nested lists

Source HTML:

<h1>Menu</h1>
<ul>
	<li data-template>
		<h2 data-bind:text="title">Menu item title</h2>
		<p>Ingredients:</p>
		<ul>
			<li data-template data-bind:text>Ingredient goes here</li>
		</ul>	
	</li>
</ul>

PHP:

function example(DocumentBinder $binder):void {
	$listData = [
		[
			"title" => "Roast king oyster mushroom",
			"ingredients" => ["hazelnut", "summer truffle", "black garlic"],
		],
		[
			"title" => "Cornish skate wing",
			"ingredients" => ["borlotti cassoulet", "lilliput caper", "baby gem", "orange oil"],
		],
		[
			"title" => "Aged Derbyshire beef",
			"ingredients" => ["turnip", "pickled mustard", "bone marrow mash", "rainbow chard"],
		],
	];
	$binder->bindListCallback($listData, function(Element $element, $listItem, $listKey) use($binder) {
		$binder->bindKeyValue("title", $listItem["title"]);
		$binder->bindList($listItem["ingredients"], $element);
	});
}

Output HTML:

<!DOCTYPE html>
<html>
<body>
    <h1>Menu</h1>
    <ul id="template-parent-62fe445401332">
        <li>
            <h2>Roast king oyster mushroom</h2>
            <p>Ingredients:</p>
            <ul id="template-parent-62fe44540144e">
                <li>hazelnut</li>
                <li>summer truffle</li>
                <li>black garlic</li>
            </ul>
        </li>
        <li>
            <h2>Cornish skate wing</h2>
            <p>Ingredients:</p>
            <ul id="template-parent-62fe44540144e">
                <li>borlotti cassoulet</li>
                <li>lilliput caper</li>
                <li>baby gem</li>
                <li>orange oil</li>
            </ul>
        </li>
        <li>
            <h2>Aged Derbyshire beef</h2>
            <p>Ingredients:</p>
            <ul id="template-parent-62fe44540144e">
                <li>turnip</li>
                <li>pickled mustard</li>
                <li>bone marrow mash</li>
                <li>rainbow chard</li>
            </ul>
        </li>
    </ul>
</body>
</html>

bindTable

DocumentBinder::bindTable(BindableTable $tableData, ?Element $context = null):void

Outputting a data structure to an HTML Table could easily be achieved by adding the data-template attribute to a <tr> element, then using bindList an described in the previous example. However, because HTML tables can define their data structure by defining a <thead>, the bindTable function allows mapping different data structures to a pre-defined output.

The content of BindableTable $tableData can be:

  • An iterable with integer keys, whose value is an iterator of BindableValues (where the first value represents the headers) - this is the data structure provided by fgetcsv
  • An iterable with string keys that match the table headers, whose value is an array of BindableValues
  • An iterable with integer keys, where the first value is an iterator of BindableValues representing the headers, and the second value is an iterator with string keys where the key represents the first field of the row and the value is an iterator of BindableValues representing subsequent field values

The different types of data structure BindableTable can represent, and more information on usage, see the binding tables section.

Example for bindTable

Source HTML:

<table>
	<thead>
		<tr>
			<th>Day</th>
			<th>Weather</th>
		</tr>
	</thead>
</table>

PHP:

function example(DocumentBinder $binder):void {
	$tableData = [
		"Day" => ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
		"Weather" => ["Rain", "Cloud", "Cloud", "Sun", "Sun", "Cloud", "Cloud"],
	];

	$binder->bindTable($tableData);
}

Output HTML:

<table>
	<thead>
		<tr>
			<th>Day</th>
			<th>Weather</th>
		</tr>
	</thead>
	<tbody>
		<tr>
			<td>Mon</td>
			<td>Rain</td>
		</tr>
		<tr>
			<td>Tue</td>
			<td>Cloud</td>
		</tr>
		<tr>
			<td>Wed</td>
			<td>Cloud</td>
		</tr>
		<tr>
			<td>Thu</td>
			<td>Sun</td>
		</tr>
		<tr>
			<td>Fri</td>
			<td>Sun</td>
		</tr>
		<tr>
			<td>Sat</td>
			<td>Cloud</td>
		</tr>
		<tr>
			<td>Sun</td>
			<td>Cloud</td>
		</tr>
	</tbody>
</table>

Next up, learn how to use bind key modifier characters.