Binding data to the DOM - PhpGt/WebEngine GitHub Wiki
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.
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.
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'sinnerText
(synonym oftextContent
) -
html
- sets the element'sinnerHTML
-
value
- sets the element'snodeValue
, 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.
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");
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);
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 thedata-bind
attribute.$value
must be classed as "Bindable Value" (see below). -
bindData($data)
- CallsbindKeyValue
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 asbindList
, but allows multiple child-lists per cloned Element.
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
.
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!".
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.
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
.
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.
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.
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).
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.
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>
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>
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.
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.