Tables - Advanced
Master advanced table techniques including data binding, calculated columns, cell spanning, repeating headers, and page breaks for complex, data-driven tables.
Learning Objectives
By the end of this article, you’ll be able to:
- Generate table rows dynamically with data binding
- Create calculated columns with formulas
- Use colspan and rowspan for complex layouts
- Control table page breaks
- Repeat headers on each page
- Build running totals and aggregations
- Create professional data-driven tables
Dynamic Table Rows
Basic Data Binding
<!DOCTYPE html>
<html xmlns='http://www.w3.org/1999/xhtml'>
<head>
<title>Dynamic Table</title>
<style>
@page {
size: Letter;
margin: 1in;
}
body {
font-family: Helvetica, Arial, sans-serif;
margin: 0;
}
table {
width: 100%;
border-collapse: collapse;
}
th {
background-color: #2563eb;
color: white;
padding: 10pt;
text-align: left;
}
td {
padding: 8pt;
border-bottom: 1pt solid #e5e7eb;
}
tbody tr:nth-child(even) {
background-color: #f9fafb;
}
</style>
</head>
<body>
<h1>Employee Directory</h1>
<table>
<thead>
<tr>
<th>Name</th>
<th>Department</th>
<th>Email</th>
<th>Phone</th>
</tr>
</thead>
<tbody>
{{#each employees}}
<tr>
<td>{{this.name}}</td>
<td>{{this.department}}</td>
<td>{{this.email}}</td>
<td>{{this.phone}}</td>
</tr>
{{/each}}
</tbody>
</table>
</body>
</html>
Accessing Index
<tbody>
{{#each items}}
<tr>
<td>{{calc(@index, '+', 1)}}</td> <!-- Row number -->
<td>{{this.name}}</td>
<td>{{this.value}}</td>
</tr>
{{/each}}
</tbody>
Calculated Columns
Simple Calculations
<table>
<thead>
<tr>
<th>Item</th>
<th>Quantity</th>
<th>Price</th>
<th>Total</th>
</tr>
</thead>
<tbody>
{{#each items}}
<tr>
<td>{{this.name}}</td>
<td>{{this.quantity}}</td>
<td>${{this.price}}</td>
<td>${{calc(this.quantity, '*', this.price)}}</td>
</tr>
{{/each}}
</tbody>
</table>
Complex Calculations
<tbody>
{{#each orders}}
<tr>
<td>{{this.productName}}</td>
<td>{{this.quantity}}</td>
<td>${{this.unitPrice}}</td>
<!-- Subtotal -->
<td>${{calc(this.quantity, '*', this.unitPrice)}}</td>
<!-- Tax (8%) -->
<td>${{calc(this.quantity, '*', this.unitPrice, '*', 0.08)}}</td>
<!-- Total with tax -->
<td>${{calc(this.quantity, '*', this.unitPrice, '*', 1.08)}}</td>
</tr>
{{/each}}
</tbody>
Running Totals
Using Variables for Accumulation
<!-- Initialize running total -->
<var data-id="runningTotal" data-value="0" />
<table>
<thead>
<tr>
<th>Date</th>
<th>Transaction</th>
<th>Amount</th>
<th>Balance</th>
</tr>
</thead>
<tbody>
{{#each transactions}}
<tr>
<td>{{this.date}}</td>
<td>{{this.description}}</td>
<td>${{this.amount}}</td>
<!-- Update running total -->
<var data-id="runningTotal"
data-value="{{calc(Document.Params.runningTotal, '+', this.amount)}}" />
<!-- Display current total -->
<td>${{Document.Params.runningTotal}}</td>
</tr>
{{/each}}
</tbody>
</table>
Cell Spanning
Column Span (colspan)
<table>
<thead>
<tr>
<th colspan="3">Q1 Results</th>
</tr>
<tr>
<th>Product</th>
<th>Units</th>
<th>Revenue</th>
</tr>
</thead>
<tbody>
<tr>
<td>Product A</td>
<td>1,200</td>
<td>$48,000</td>
</tr>
<tr>
<td>Product B</td>
<td>850</td>
<td>$34,000</td>
</tr>
</tbody>
<tfoot>
<tr>
<td>Total</td>
<td>2,050</td>
<td>$82,000</td>
</tr>
</tfoot>
</table>
Row Span (rowspan)
<table>
<tbody>
<tr>
<td rowspan="3">Engineering</td>
<td>John Smith</td>
<td>Senior Developer</td>
</tr>
<tr>
<!-- Engineering cell spans into this row -->
<td>Jane Doe</td>
<td>Developer</td>
</tr>
<tr>
<!-- Engineering cell spans into this row -->
<td>Bob Johnson</td>
<td>Junior Developer</td>
</tr>
<tr>
<td rowspan="2">Marketing</td>
<td>Alice Brown</td>
<td>Manager</td>
</tr>
<tr>
<td>Charlie Davis</td>
<td>Coordinator</td>
</tr>
</tbody>
</table>
Page Breaks in Tables
Controlling Page Breaks
/* Avoid breaking table */
table {
page-break-inside: avoid;
}
/* Allow breaks in tbody */
tbody {
page-break-inside: auto;
}
/* Avoid breaking rows */
tr {
page-break-inside: avoid;
}
/* Keep header with first row */
thead {
display: table-header-group; /* Repeats on each page */
}
/* Keep footer with last row */
tfoot {
display: table-footer-group; /* Repeats on each page */
}
New Page Before Table
.new-page-table {
page-break-before: always;
}
<table class="new-page-table">
<!-- Table content -->
</table>
Repeating Headers and Footers
Repeating on Each Page
thead {
display: table-header-group;
}
tfoot {
display: table-footer-group;
}
<table>
<!-- This header repeats on each page -->
<thead>
<tr>
<th>Column 1</th>
<th>Column 2</th>
<th>Column 3</th>
</tr>
</thead>
<tbody>
<!-- Many rows that span multiple pages -->
<tr><td>...</td><td>...</td><td>...</td></tr>
<!-- ... hundreds of rows ... -->
</tbody>
<!-- This footer repeats on each page -->
<tfoot>
<tr>
<td colspan="3">Continued on next page...</td>
</tr>
</tfoot>
</table>
Practical Examples
Example 1: Invoice with Calculations
<!DOCTYPE html>
<html xmlns='http://www.w3.org/1999/xhtml'>
<head>
<title>Invoice</title>
<style>
@page {
size: Letter;
margin: 1in;
}
body {
font-family: Helvetica, Arial, sans-serif;
font-size: 11pt;
margin: 0;
}
h1 {
font-size: 28pt;
margin: 0 0 30pt 0;
color: #1e40af;
}
.invoice-info {
margin-bottom: 30pt;
}
.invoice-info p {
margin: 5pt 0;
}
/* ==============================================
INVOICE TABLE
============================================== */
.invoice-table {
width: 100%;
border-collapse: collapse;
margin: 20pt 0;
}
.invoice-table thead th {
background-color: #1e40af;
color: white;
padding: 12pt;
text-align: left;
font-weight: 600;
}
.invoice-table thead th.right {
text-align: right;
}
.invoice-table tbody td {
padding: 10pt;
border-bottom: 1pt solid #e5e7eb;
}
.invoice-table tbody td.right {
text-align: right;
font-family: 'Courier New', monospace;
}
.invoice-table tbody tr:nth-child(even) {
background-color: #f9fafb;
}
/* ==============================================
TOTALS SECTION
============================================== */
.totals {
width: 50%;
margin-left: auto;
margin-top: 20pt;
}
.totals table {
width: 100%;
border-collapse: collapse;
}
.totals td {
padding: 8pt;
border-bottom: 1pt solid #e5e7eb;
}
.totals td:first-child {
font-weight: 600;
text-align: right;
}
.totals td:last-child {
text-align: right;
font-family: 'Courier New', monospace;
}
.totals .grand-total {
font-size: 14pt;
font-weight: 700;
background-color: #eff6ff;
border-top: 2pt solid #2563eb;
border-bottom: 2pt solid #2563eb;
color: #1e40af;
}
</style>
</head>
<body>
<h1>Invoice #{{invoice.number}}</h1>
<div class="invoice-info">
<p><strong>Date:</strong> {{invoice.date}}</p>
<p><strong>Customer:</strong> {{customer.name}}</p>
<p><strong>Address:</strong> {{customer.address}}</p>
</div>
<!-- Initialize subtotal variable -->
<var data-id="subtotal" data-value="0" />
<!-- Line items table -->
<table class="invoice-table">
<thead>
<tr>
<th>#</th>
<th>Description</th>
<th class="right">Qty</th>
<th class="right">Unit Price</th>
<th class="right">Amount</th>
</tr>
</thead>
<tbody>
{{#each invoice.items}}
<tr>
<td>{{calc(@index, '+', 1)}}</td>
<td>{{this.description}}</td>
<td class="right">{{this.quantity}}</td>
<td class="right">${{this.unitPrice}}</td>
<td class="right">${{calc(this.quantity, '*', this.unitPrice)}}</td>
</tr>
<!-- Accumulate subtotal -->
<var data-id="subtotal"
data-value="{{calc(Document.Params.subtotal, '+', this.quantity, '*', this.unitPrice)}}" />
{{/each}}
</tbody>
</table>
<!-- Totals section -->
<div class="totals">
<table>
<tr>
<td>Subtotal:</td>
<td>${{Document.Params.subtotal}}</td>
</tr>
<tr>
<td>Tax (8%):</td>
<td>${{calc(Document.Params.subtotal, '*', 0.08)}}</td>
</tr>
<tr>
<td>Shipping:</td>
<td>$15.00</td>
</tr>
<tr class="grand-total">
<td>Total:</td>
<td>${{calc(Document.Params.subtotal, '*', 1.08, '+', 15)}}</td>
</tr>
</table>
</div>
</body>
</html>
Example 2: Multi-Page Report with Repeating Headers
<!DOCTYPE html>
<html xmlns='http://www.w3.org/1999/xhtml'>
<head>
<title>Sales Report</title>
<style>
@page {
size: Letter;
margin: 0.75in;
}
body {
font-family: Helvetica, Arial, sans-serif;
font-size: 10pt;
margin: 0;
}
h1 {
font-size: 20pt;
margin: 0 0 20pt 0;
color: #1e40af;
}
/* ==============================================
LARGE DATA TABLE
============================================== */
.data-table {
width: 100%;
border-collapse: collapse;
}
/* Header repeats on each page */
.data-table thead {
display: table-header-group;
}
.data-table thead th {
background-color: #2563eb;
color: white;
padding: 8pt;
text-align: left;
font-weight: 600;
font-size: 9pt;
}
.data-table thead th.right {
text-align: right;
}
.data-table tbody td {
padding: 6pt;
border-bottom: 0.5pt solid #e5e7eb;
font-size: 9pt;
}
.data-table tbody td.right {
text-align: right;
font-family: 'Courier New', monospace;
}
.data-table tbody tr:nth-child(even) {
background-color: #f9fafb;
}
/* Prevent row breaks */
.data-table tbody tr {
page-break-inside: avoid;
}
/* Footer repeats on each page */
.data-table tfoot {
display: table-footer-group;
}
.data-table tfoot td {
padding: 8pt;
font-weight: 700;
background-color: #eff6ff;
border-top: 2pt solid #2563eb;
}
</style>
</head>
<body>
<h1>Annual Sales Report - {{year}}</h1>
<table class="data-table">
<!-- This header will repeat on every page -->
<thead>
<tr>
<th>Order ID</th>
<th>Date</th>
<th>Customer</th>
<th>Product</th>
<th class="right">Quantity</th>
<th class="right">Unit Price</th>
<th class="right">Total</th>
</tr>
</thead>
<tbody>
{{#each orders}}
<tr>
<td>{{this.orderId}}</td>
<td>{{this.date}}</td>
<td>{{this.customerName}}</td>
<td>{{this.productName}}</td>
<td class="right">{{this.quantity}}</td>
<td class="right">${{this.unitPrice}}</td>
<td class="right">${{calc(this.quantity, '*', this.unitPrice)}}</td>
</tr>
{{/each}}
</tbody>
<!-- This footer will repeat on every page -->
<tfoot>
<tr>
<td colspan="6">Grand Total:</td>
<td class="right">${{totals.grandTotal}}</td>
</tr>
</tfoot>
</table>
</body>
</html>
Try It Yourself
Exercise 1: Order Summary
Create an order summary table with:
- Dynamic rows from data
- Quantity × Price calculations
- Running subtotal column
- Tax and total at bottom
Exercise 2: Grouped Data Table
Build a table with:
- Row spanning for group headers
- Multiple items per group
- Subtotals per group
- Grand total at end
Exercise 3: Multi-Page Inventory Report
Design an inventory report with:
- 100+ items (test pagination)
- Repeating headers on each page
- Calculated “Total Value” column (Qty × Price)
- Footer showing page totals
Common Pitfalls
❌ Not Using table-header-group
/* Headers won't repeat on pages */
thead {
display: block; /* Wrong! */
}
✅ Solution:
thead {
display: table-header-group; /* Repeats on pages */
}
❌ Breaking Rows Across Pages
/* Rows may split awkwardly */
tr {
page-break-inside: auto;
}
✅ Solution:
tr {
page-break-inside: avoid; /* Keep rows together */
}
❌ Incorrect Calculation Syntax
<!-- Wrong: Missing operators -->
<td>{{calc(this.a this.b)}}</td>
<!-- Wrong: Too many nested calcs -->
<td>{{calc(calc(calc(this.a, '+', this.b), '*', 2), '-', 10)}}</td>
✅ Solution:
<!-- Correct: All operators specified -->
<td>{{calc(this.a, '+', this.b)}}</td>
<!-- Better: Break into steps with variables -->
<var data-id="sum" data-value="{{calc(this.a, '+', this.b)}}" />
<var data-id="doubled" data-value="{{calc(Document.Params.sum, '*', 2)}}" />
<td>{{calc(Document.Params.doubled, '-', 10)}}</td>
Advanced Tables Checklist
- Data binding working correctly
- Calculations tested with various values
- Running totals accumulate properly
- Page breaks controlled (thead repeats)
- Rows don’t break across pages
- Column spanning correct
- Row spanning correct
- Large datasets tested (pagination)
Best Practices
- Test with Real Data - Varying dataset sizes
- Use table-header-group - For repeating headers
- Avoid Row Breaks - page-break-inside: avoid on tr
- Validate Calculations - Test with known values
- Initialize Variables - Set to 0 before loops
- Clear Spanning - Ensure colspan/rowspan makes sense
- Performance - Limit calculations in large tables
- Debugging - Test with small datasets first
Next Steps
- Attachments & Embedded Content - File embedding
- Content Best Practices - Optimization
- Practical Applications - Real-world examples
Continue learning → Attachments & Embedded Content