Skip to content

Binding data to the DOM

Greg Bowler edited this page Dec 1, 2019 · 46 revisions

Document data binding is provided as part of the DOM template repository, which is separately maintained at https://github.com/PhpGt/DomTemplate

Thanks to the power of having the DOM to hand on the server, it's possible to set the content of HTML elements from PHP without having to echo strings of HTML directly. Using standard DOM manipulation, you can achieve this by setting Element properties such as innerText or value, for example.

However, manipulating the DOM directly can lead to tightly coupled code, meaning that your PHP code may break if the HTML is altered at a later date. A better way to bind data to an element is to use the data-bind attribute on the element in the HTML, as described in this section.

Binding data to placeholder elements

Any element that you wish to output dynamic data to can have the data-bind attribute added to it. The syntax of this attribute is the following:

data-bind:x=y, where x is the name of the element's attribute to bind to, and y is the key of the data to bind with.

For example, with the following HTML source:

<div id="output">
	<h1 data-bind:text="title">Page title</h1>
	<p data-bind:text="content">Page content</p>
</div>

and the following PHP logic:

public function outputTitleAndContent() {
	$outputTo = $this->document->getElementById("output");
	$outputTo->bindKeyValue("title", "How to bind data");
	$outputTo->bindKeyValue("content", "It's quite simple really");
}

will produce the following HTML output:

<div id="output">
	<h1>How to bind data</h1>
	<p>It's quite simple really</p>
</div>

Binding data can be performed on placeholder elements as above, but can also be used to output multiple template elements for each row of a dataset, as described in the next section.

Attributes and keys of data-bind

The use of text and html as the data-bind output property maps directly to the innerText and innerHTML property of the element, and are shortened due to HTML's specification limiting attribute names to be lowercase only.

The following properties are handled specially:

  • text - sets the element's innerText (synonym of textContent)
  • html - sets the element's innerHTML
  • value - sets the element's nodeValue, which has different behaviour for different types of element (setting a value on a <select> element will select the matching option)

All other properties provided to the data-bind attribute will be set as element attributes. For example, data-bind:src="imageSrc", data-bind:alt="altText", etc. Other data-* attributes can be set. For example: <div data-bind:data-example="keyname"></div> will add a data-example attribute with the value matching the keyname key.

Using another attribute value as the data's key

It's possible to use the value of another attribute on the element as the data key. This is achieved by prefixing the attribute' name within the data-bind attribute with the @ character. This is useful to prevent repetitive code when there is a direct link between one attribute's value and the data. A common usage for this is when the id attribute of an element is also the key of the data you wish to bind.

Example HTML:

<dl>
	<dt>Employee ID</dt>
	<dd id="empId" data-bind:text="@id">000</dd>
	<dt>Employee name</dt>
	<dd id="empName" data-bind:text="@id">Name</dd>
</dl>

The data-bind attribute of the dd elements in the example above will set their key to the value of the id attributes, indicated by the @ prefix. The text of the dd elements will be set to the value of the empId and empName keys in this case.

Example PHP:

$element->bindKeyValue("empId", 123);
$element->bindKeyValue("empName", "Mrs. Example");

Binding boolean values

When a boolean value is passed to a bind function, it behaves slightly differently. The boolean is used to define whether the bind value's key should be output to the element.

This is useful when a flag is required to be output to the DOM. For example, adding a class to an element when a navigation menu is "selected" or a to do item is "completed".

This technique can be used to bind a predefined value to an element only when the bind key is true. When a false value is bound, nothing will be bound to the DOM.

Example adding the "open" class to a <menu> element that already contains classes:

<menu class="main-menu positive" data-bind:class="open">
	...
</menu>
$document->querySelector("menu")->bindKeyValue("open", true);

Bind functions

Provided by DomTemplate, there are various bind functions made available on all DOM Elements. You can call these methods on any Element, including the Document itself.

  • bindKeyValue(string $key, $value) - injects $value anywhere in the DOM subtree where $key is used as the data-bind attribute. $value must be classed as "Bindable Value" (see below).
  • bindData($data) - Calls bindKeyValue for every key-value of $data. The $data variable must be classed as "Bindable Data" (see below).
  • bindValue($value) - Implicitly bind a value to an Element, so the key is not required in the HTML. See section on implicit binding below. $value must be classed as "Bindable Value" (see below).
  • bindList(iterable $list) - Clone a template element for every iteration in the $list, binding the sub-children with data from each iteration.
  • bindNestedList(iterable $list) - Works in the same way as bindList, but allows multiple child-lists per cloned Element.

Definitions

A "Bindable Value" is a variable that represents a single value, such as: a string, int, float or bool scalar, or any object with a __toString function (binding a bool is handled differently to other types).

"Bindable Data" is a variable that represents a set of key-value pairs: either an associative array, any object with public properties or an object that implements BindObject or BindDataMapper.

bindKeyValue function

This is the simplest of the bind functions. It is available on any element, including the document itself. Calling the function will bind the value to the DOM with a matching key as many times as it appears within the tree.

The simplest example is binding a single key-value pair to a single element in the DOM:

HTML:

<h1>Welcome, <span data-bind:text="name">user</span>!</h1>

PHP:

$document->querySelector("h1")->bindKeyValue("name", "Alice");

Rendered output:

<h1>Welcome, <span>Alice</span></h1>

In the above example, notice how the element that is referenced in PHP is the h1, but the actual binding is done on the child span element. If there are any matching data-bind attributes present on any children of the referenced Element, the binding will be made on those elements too. Nothing happens if bindKeyValue is called on an Element that doesn't contain any matching data-bind elements.

This functionality allows for default data to be added to the actual HTML. Without performing any data binding, the above example will still output "Welcome, user!".

bindData function

Binding per key-value pair can get repetitive. The bindData function allows binding a whole dataset at once. The dataset can be an associative array, an object with public properties exposed, or an object that implements the BindObject or BindDataMapper interfaces.

This is especially useful when dealing with data structures obtained from a source such as a relational database.

Binding an associative array

The ease of working with associative arrays is one of the main powers of getting stuff done in PHP. Pass an associative array to the bindData function, and every key-value-pair will be passed to the bindKeyValue function in one operation.

Example PHP:

$element = $document->querySelector("div.output");
$element->bindData([
	"name" => "Audrey",
	"nationality" => "British",
	"dob" => "1929-05-04",
]);

Note that any keys that exist in the associative array that do not have any elements to bind to will be skipped, just as they are ignored when using bindKeyValue.

Binding an object with public properties

Just like passing in an associative array of key-values, bindData accepts an object with public properties for its key-value pairs. The function accepts any type of object and by default will only look at public properties of the same name as the bind key that is referenced in the HTML.

Example PHP:

$object = new StdClass();
$object->name = "Audrey";
$object->nationality = "British";
$object->dob = "1929-05-04";

$element = $document->querySelector("div.output");
$element->bindData($object);

The above example has exactly the same effect as the example using an associative array, but the object in this case could be a "Model" or "Entity" class from within your application containing other functionality.

In PHP, it is possible to use magic methods to define custom behaviour when getting the property value of an object, but magic methods often lead to less readable code that is more difficult to maintain. If custom behaviour is required, the BindObject or BindDataMapper interfaces can help keep code clean and readable.

Binding an object that implements BindObject or BindDataMapper

Passing an object with public properties to bindData is only really useful when the object is a data object, A.K.A. plain old PHP object (POPO).

A common programming design pattern is to use "Model" or "Entity" objects to represent data from a database and provide custom business logic. These objects will typically expose functionality specific to the area of the system they represent, and by implementing either the BindObject or BindDataMapper interfaces, those same objects can also be used to bind data to the page.

The BindDataMapper interface specifies a single function to implement, bindDataMap, which must return the key-value-pairs required to bind the object's data to the document, such as an associative array, or any other type of Bindable Data. The return value of the bindDataMap function is passed directly back into to the bindData function of the bind element.

Example PHP showing BindDataMapper usage:

class Customer extends Entity implements BindDataMapper {
	private $id;
	private $forename;
	private $surname;

	public function __construct(int $id, string $forename, string $surname) {
		$this->id = $id;
		$this->forename = $forename;
		$this->surname = $surname;
	}

// The object can have any number of functions and properties, 
// without affecting the bind characteristics.

	public function getId():int {
		return $this->id;
	}

// The interface requires the implementation of bindDataMap function,
// which must return the bindable data as key-value-pairs:

	public function bindDataMap() {
		return [
			"id" => $this->getId(),
			"fullName" => $this->forename . " " . $this->surname,
			"customerCode" => $this->id . substr($this->surname, 0, 2),
		];
	}
}

Alternatively, the BindObject interface doesn't specify any particular functions to implement, but instead it tells the data binder to handle the object differently. When a bound element defines a bind key, such as <span data-bind:text="fullName">Person Name</span>, the data binder looks for a function called bindFullName() on the object, and calls it to get the data value to bind.

Example PHP showing BindDataGetter usage:

class Customer extends Entity implements BindObject {
	private $id;
	private $forename;
	private $surname;

	public function __construct(int $id, string $forename, string $surname) {
		$this->id = $id;
		$this->forename = $forename;
		$this->surname = $surname;
	}

// The object can have any number of functions and properties, 
// without affecting the bind characteristics.

	public function getId():int {
		return $this->id;
	}

// Any functions that begin with "bind" are used when binding to the page:

	public function bindFullName():string {
		return $this->forename . " " . $this->surname;
	}

	public function bindCustomerCode():string {
		return $this->id . substr($this->surname, 0, 2);
	}
}

A complete example of this can be seen in the Address book tutorial.

bindValue(string $value) (implicit binding on a single Element)

When a data-bind property is not provided, this is called implicit binding. Implicit binding can be used to bind a Bindable Value to any elements that have a data-bind attribute without a property, like this: <span data-bind:text>Example</span>.

This is only useful when binding single values to simple HTML structures, or when using the bindList function (see below).

bindList function

The bindList function is used to output a freshly cloned template element per row of data. The single parameter passed to the function should be an iterable object such as an array. This functionality relies on the DOM Templates functionality.

Each item within the iteratable should return Bindable Data - the same type of object that can be passed to bindData (see above for details).

Within the source HTML, to indicate that a particular element should be used to represent a single row of data, apply the data-template attribute on the element without any value.

Example (the li element represents each iteration of data):

<h1>Employee list:</h1>

<ul id="emp-list">
	<li data-template>
		<img data-bind:src="photo" data-bind:alt="name" />
		<h1 data-bind:text="name">Employee name</h1>
		<h2 data-bind:text="department">Department name</h2>
	</li>
</ul>
public function outputEmployees() {
	$allEmployees = [
		["name" => "Abigail Adams", "photo" => "abi.jpg", "department" => "Marketing"],
		["name" => "Barry Benson", "photo" => "baz.jpg", "department" => "HR"],
		["name" => "Charlie Cobsworth", "photo" => "char.jpg", "department" => "Telesales"],
		["name" => "Doris Day", "photo" => "doris.jpg", "department" => "Software development"],
	];

	$outputTo = $this->document->getElementById("emp-list");
	$outputTo->bindList($allEmployees);
}

The above PHP code will clone and insert four new li elements (one for every row in the $allEmployees dataset), automatically binding the data on each cloned element.

In a real-world example, the dataset will come from a data source such as a database, or through a Data Repository using the Repository-Entity pattern.

Lists of strings (implicit binding)

If the data that you are binding to the document is just a simple indexed array of values (or similar Iterator object), the bind key can be left blank in the HTML element's bind attribute, and the value of each item in the array will be implicitly bound.

Example PHP:

$fruit = ["Apple", "Banana", "Cherry"];
$document->bindList($fruit);

Example HTML:

<p>List of fruits:</p>

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

Output HTML after binding:

<p>List of fruits:</p>

<ul>
	<li>Apple</li>
	<li>Banana</li>
	<li>Cherry</li>
</ul>

bindNestedList function

When the data contains nested lists such as multidimensional arrays, this can be handled with the bindNestedList function. When a value within the iterable data is also an iterable object, the function will be called recursively, allowing multiple nested HTML elements within the same structure to have the data-template attribute.

When this function is called, if the key of the data is a string (such as in an associative array), the key will be implicitly bound to the current Element. This allows for associative array-like objects to be used to reduce the repetition of outputting fairly complex HTML structures.

See the example below. It shows a nested list of music, consisting of a list of artists with a nested list of albums, and a final nested list of tracks.

Source HTML:

<h1>Music list!</h1>
	
<ul class="artist-list">
	<li data-template>
		<h2 data-bind:text>Artist name</h2>
		
		<ul class="album-list">
			<li data-template>
				<h3 data-bind:text>Album name</h3>
				
				<ol class="track-list">
					<li data-template data-bind:text>Track name</li>
				</ol> 				
			</li>			
		</ul>		
	</li>
</ul>

Page logic:

$musicList = [
	"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",
		],
	],
	"Crayons" => [
		"Pastel Colours" => [
			"Egg Shell",
			"Cotton",
			"Frost",
			"Periwinkle",
		],
		"Different Shades of Blue" => [
			"Cobalt",
			"Slate",
			"Indigo",
			"Teal",
		],
	]
];

$document->bindNestedList($musicList);

An extract of the output HTML is as follows:

<h1>Music list!</h1>
	
<ul class="artist-list">
	<li>
		<h2>Bongo and The Bronks</h2>
		
		<ul class="album-list">
			<li>
				<h3>Salad</h3>
				
				<ol class="track-list">
					<li>Tomatoes</li>
					<li>Song about Cucumber</li>
					<li>Onions MAke Me Cry (but I love them)</li>
				</ol>
			</li>
			<li>
				<h3>Meat</h3>

				<ol class="track-list">
					...
				</ol>
			</li>
		</ul>
	</li>
	<li>
		...
	</li>
</ul>

Binding data within attributes using {curly braces}

Using the data-bind attribute is limited to setting the property value of an element with the data provided to one of the bind functions. Properties can't be concatenated or spliced using this method.

Using placeholders in attribute values makes it possible to bind data within the pre-written attributes. This is useful when a long or complicated value simply requires a single value replacing, such as a link containing an ID. To enable attribute binding, add the data-bind-attributes attribute to the element with the placeholder(s).

Example:

<a id="user-profile" href="/user/{id}/profile" data-bind-attributes>User profile</a>
public function setUserProfileId() {
	$profileLink = $this->document->getElementById("user-profile");
	$profileLink->bindKeyValue("id", 12345);
}

Will render:

<a id="user-profile" href="/user/12345/profile">User profile</a>

Note that because curly braces are discouraged as a template mechanism, this method is only possible to bind data within attribute values. To bind data to elements' text content, use the data-bind:text attribute, as described in the above sections.

Related documentation

For a background on the dynamic page mechanism used throughout WebEngine, see the DOM Manipulation section.

To learn more about the underlying templating functionality, see DOM Templates.

Clone this wiki locally