Skip to content

Binding

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

One of the most common usages of DomTemplate is to bind data to the document - to inject information into elements to make pages dynamic. DomTemplate uses data-bind and data-list 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

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 HTMLElement property to bind to
  • optionally, a bind key can be supplied after an equals sign (=)

Some examples, to help visualise the different parts of a data bind attribute:

  • <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. The class bind property has special

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" will set the data-id attribute). There are also some bind properties that have special behaviour, like when you want to toggle a value within 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 - because it is so common to set the text content, the shorter text property is preferred
  • html, inner-html and innerhtml are synonyms that will set the innerHTML property of the HTMLElement (important: 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 of an HTML <table> element, 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 list element. Binding lists is explained in more depth in its own section

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.

Common properties include href, src, title, value, etc.

Examples to help clarify terminologies (property vs. key vs. value)

  • <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. Read more about modifier characters.

Bind keys

Bind keys are the optional value of the data-bind attribute on an element that corresponds to the key of a key-value-pair data structure. As an 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 HTMLElement 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.

Nested keys

When binding the document using objects to represent the data, it's possible that objects will have nested properties of other objects. These nested properties can be addressed in the bind key by using the dot character (.) as a separator. Usage within HTML looks like this: <span data-bind:text="customer.address.country.name">Country name</span> - learn more about nested bind keys on the Binding objects page.

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 list element so that it's repeated for every item in the BindableList. This is done by adding the data-list 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 list 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 list element.

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

Example for bindList

Source HTML:

<h1>Shopping list</h1>

<ul>
	<li data-list 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 list element. The provided $element is a clone of the original list 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 list 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-list 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-list>
		<h2 data-bind:text="title">Menu item title</h2>
		<p>Ingredients:</p>
		<ul>
			<li data-list 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="list-parent-62fe445401332">
        <li>
            <h2>Roast king oyster mushroom</h2>
            <p>Ingredients:</p>
            <ul id="list-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="list-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="list-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-list 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>

Special cases

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 an "Order summary" page of an e-commerce site, a list of orders can be bound to a specific element of the page. Each Order might have common bind keys such as id, name, etc. After binding this list to a specific element, a User object can be bound to the entire document, and any unbound id, name keys on the page will take that of the User. This is just an example of one style of programming that can help save a lot of time implementing common page layouts.


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