Skip to content

Binding nested lists

Greg Bowler edited this page Jul 19, 2023 · 22 revisions

In the previous section, we saw how to add data-list attributes to HTML elements to use them in bindList operations, but we only ever saw examples using one dimensional data.

Depending on your development style, or how your database supplies the data, you may want to provide the DocumentBinder with data structures that contain the entire dataset, including lists that contain other nested lists.

As an example of one possible data structure, let's use the idea of a user's music collection. The dataset consists of a list of Artists. Each Artist has a list of Albums. Each Album has a list of tracks. The data structure is three nested arrays:

/** @var array<string, array<string, array<string>>> $musicData */
$musicData = [
	"Artist1" => ["Album1" => ["Track1", "Track2", "Track3"], "Album2" => ["Track4", "Track5"]],
	"Artist2" => ["Album3" => ["Track6", "Track7", "Track8", "Track9"]],
	// ... and so on ...
];
View a full example dataset with lots of real data.

PHP:

/** @var array<string, array<string, array<string>>> $musicData */
$musicData = [
	"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",
		],
	]
];

In this example, we see each artist name as the key of the outer array, which contains an array of album tracks. The inner array's key represents the album name, and the value contains the list of tracks as an array of strings.

Because we are using either the array key or a plain string to represent the data, the HTML's bind attributes do not need to specify any bind keys.

HTML:

<!doctype html>
<h1>Music library</h1>

<ul>
	<li data-list>
		<h2 data-bind:text>Artist name</h2>
		
		<ul>
			<li data-list>
				<h3 data-bind:text>Album name</h3>
				
				<ol>
					<li data-list data-bind:text>Track name</li>
				</ol>
			</li>
		</ul>
	</li>
</ul>

To bind this data to the example HTML above, we can use bindList() and pass the array directly.

function example(DocumentBinder $binder, array $musicData):void {
	$binder->bindList($musicData);
}

Because the nesting of the data matches with the number of data-list elements, the lists will be bound to the appropriate HTML elements for each nested list.

Show the full HTML output.
<!doctype html>
<h1>Music library</h1>

<ul>
	<li>
		<h2>A Band From Your Childhood</h2>

		<ul>
			<li>
				<h3>This Album is Good</h3>

				<ol>
					<li>The Best Song You‘ve Ever Heard</li>
					<li>Another Cracking Tune</li>
					<li>Top Notch Music Here</li>
					<li>The Best Is Left ‘Til Last</li>
				</ol>
			</li>
			<li>
				<h3>Adequate Collection</h3>

				<ol>
					<li>Meh</li>
					<li>‘sok</li>
					<li>Sounds Like Every Other Song</li>
				</ol>
			</li>
		</ul>
	</li>
	<li>
		<h2>Bongo and The Bronks</h2>

		<ul>
			<li>
				<h3>Salad</h3>

				<ol>
					<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>
					<li>Steak</li>
					<li>Is Chicken Really a Meat?</li>
					<li>Don‘t Look in the Sausage Factory</li>
					<li>Stop Horsing Around</li>
				</ol>
			</li>
			<li>
				<h3>SnaxX</h3>

				<ol>
					<li>Crispy Potatoes With Salt</li>
					<li>Pretzel Song</li>
					<li>Pork Scratchings Are Skin</li>
					<li>The Peanut Is Not Actually A Nut</li>
				</ol>
			</li>
		</ul>
	</li>
	<li>
		<h2>Crayons</h2>

		<ul>
			<li>
				<h3>Pastel Colours</h3>

				<ol>
					<li>Egg Shell</li>
					<li>Cotton</li>
					<li>Frost</li>
					<li>Periwinkle</li>
				</ol>
			</li>
			<li>
				<h3>Different Shades of Blue</h3>

				<ol>
					<li>Cobalt</li>
					<li>Slate</li>
					<li>Indigo</li>
					<li>Teal</li>
				</ol>
			</li>
		</ul>
	</li>
</ul>

Using the data-bind:list attribute

Not all data will be a single nesting of lists. Model objects from real data sources will often have their own key-value data, along with multiple sub lists. Imagine an e-commerce website: a Customer will have its own details such as id, name, email, address, but may have sub lists like orderList (an array of Order objects) and savedCardList (an array of PaymentCard objects). Each Order object can have its own id, billingAddress, etc. along with its own sub-lists, like itemList (an array of ShopItem objects).

The data-bind:list attribute can be used to bind all of the sub-lists within a data structure in a single bind operation.

Whenever an object has a sub-list identified by a key on the object, such as $customer->orderList, we can mark an element in the HTML as that list's container using data-bind:list="orderList". When the orderList bind key is bound, the DocumentBinder will look for a child element with the data-list attribute and use that as the list element for the list.

As a useful helper, if a custom HTML element is used as the container of a sub-list, its tag name can be used as the name of the list by omitting the bind key. In the example below the <order-list> tag is used to demonstrate this.

Here's a complex HTML structure that shows a summary of the most active customers of an online store:

<!doctype html>
<h1>Customer overview</h1>

<customer-list>
	<ul>
		<li data-list>
			<customer-details>
				<dl>
					<dt>ID</dt>
					<dd data-bind:text="id">000</dd>
					
					<dt>Name</dt>
					<dd data-bind:text="name">Customer Name!</dd>
					
					<dt>Address</dt>
					<dd>
						<span data-bind:text="address.street">Address Line 1</span>
						<span data-bind:text="address.line2">Address Line 2</span>
						<span data-bind:text="address.cityState">Address City</span>
						<span data-bind:text="address.postcodeZip">Address Postcode</span>
						<span data-bind:text="address.country.name">Address Country</span>
					</dd>
					
					<dt>Latest orders</dt>
					
<!-- the use of a custom element allows data-bind:list to be left blank, so the
list name will be the same as the element name (camel-cased to orderList) -->
					<order-list data-bind:list>
						<ul>
							<li data-list>
								<dl>
									<dt>City / State</dt>
									<dd data-bind:text="shippingAddress.cityState"></dd>
									
									<dt>Subtotal</dt>
									<dd data-bind:text="subtotal">£0.--</dd>
									
									<dt>Shipping</dt>
									<dd data-bind:text="shippingCost">£0.--</dd>
									
									<dt>Total</dt>
									<dd data-bind:text="totalCost">£0.--</dd>
								</dl>
								<h3>Items in order</h3>
								<ul data-bind:list="itemList">
									<li data-list>
										<h4><a href="/item/{{id}}" data-bind:text="title">Item name</a></h4>
										<p data-bind:text="cost">£0.--</p>
									</li>								
								</ul>
							</li>						
						</ul>					
					</order-list>
				</dl>
			</customer-details>		
		</li>
	</ul>
</customer-list>

In the above example, custom HTML elements like <customer-details> and <order-list> are used only for stylistic reasons - using custom elements allows easy access to the element using CSS selectors, and easy future extraction into their own HTML components when needed.

There are three nested lists bound in the above example. The <customer-list> element has a data-list for each Customer; the <order-list> element has a data-list for each Order, and within the <order-list> element, a named list called itemList is bound for each item in the Order.


Next, learn how to automatically remove unbound elements.