Examples

Multi-page Report

A complete recipe for rendering large nested datasets across multiple pages - sales orders with line items and bin locations - using Hierarchical Table, hierarchyRules, Section, and Spacer together.

What You'll Build

A paginated report that includes:

  • A document header with report title, period, and generated timestamp
  • A three-level hierarchical table: orders at the top level, line items per order, and bin locations per line item
  • Per-level styling (full grid for orders, minimal borders for line items, compact key-value for bin locations)
  • Computed totals (total quantity, line total) rolled up from nested arrays onto parent rows
  • Automatic page breaks between order groups

This example is the right pattern whenever your data has more than one level of nesting and you need the renderer to handle depth automatically rather than building manual nested tables.


Controls Used

ControlRole in This Template
templateSettingsSets locale, date format, and currency for consistent column formatting
sectionWraps the report header with a boxed header row
hierarchical_tableRenders the nested orders/lines/bins structure with per-level styling
spacer_separatorCreates visual breathing room between the header section and the table

Payload Shape

{
  "templateId": "sales-order-report",
  "templateSettings": { },
  "theme":            { },
  "hierarchyRules":   { },
  "layoutSchema":     { },

  "reportTitle":    "Sales Order Report",
  "reportPeriod":   "Q1 2026",
  "generatedAt":    "2026-03-18T09:00:00Z",
  "generatedBy":    "Finance Team",

  "salesOrders": [
    {
      "orderNumber": "SO-2026-001",
      "customer":    "Acme Corp",
      "orderDate":   "2026-01-15",
      "status":      "Fulfilled",
      "lineItems": [
        {
          "sku":       "WIDGET-A",
          "description": "Widget Alpha",
          "qty":       100,
          "unitPrice": 49.99,
          "bins": [
            { "binCode": "BIN-A01", "quantity": 60 },
            { "binCode": "BIN-A02", "quantity": 40 }
          ]
        }
      ]
    }
  ]
}

The hierarchical_table node binds to salesOrders via dataSource: "{{salesOrders}}". The renderer discovers lineItems nested inside each order row, and bins nested inside each line item, automatically - no extra binding config is needed for deeper levels.


Step 1 - Template Settings

"templateSettings": {
  "locale":     "en-US",
  "dateFormat": "MMM D, YYYY",
  "timezone":   "UTC"
}

Step 2 - Report Header Section

A boxed section with a light header row provides a consistent document title block. Setting pagination.breakAfter: "never" keeps the header attached to the first data rows.

{
  "type": "section",
  "props": {
    "title":           "Sales Order Report",
    "titleIcon":       "list",
    "showHeader":      true,
    "headerFillColor": "#1a56db",
    "layout":          "2"
  }
}

With layout: "2", the left child shows reportTitle and reportPeriod, the right child shows generatedAt (format: datetime) and generatedBy. No layout_group is needed here because both halves belong under the same header.


Step 3 - Spacer

A spacer_separator with lineStyle: "none" creates a clean gap between the header block and the table without drawing a visible line.

{
  "type": "spacer_separator",
  "props": {
    "lineStyle":  "none",
    "spaceAbove": 0,
    "spaceBelow": 12
  }
}

When to use a spacer vs marginTop

spacer_separator with lineStyle: "none" is useful when you want unconditional whitespace between two sibling nodes. For spacing that should appear before a section only when it is visible, prefer marginTop on the section itself - orphan protection handles the conditional case automatically.


Step 4 - Hierarchical Table Node

The hierarchical_table node is placed in the layout once and bound to the root array. The hierarchyRules object (at the top level of the payload) controls per-level rendering.

{
  "type": "hierarchical_table",
  "props": {
    "title":      "Orders",
    "titleIcon":  "package",
    "dataSource": "{{salesOrders}}",
    "maxCols":    8
  }
}

dataSource syntax

Use double-brace syntax: "{{salesOrders}}". This is not a field token - it is a binding expression that the adaptive renderer resolves at runtime to the root array in the payload.


Step 5 - hierarchyRules

hierarchyRules lives at the top level of the payload alongside your data keys - not inside the hierarchical_table node itself.

Level guide for this dataset:

  • Level 1 - scalar header fields (orderNumber, customer, orderDate, status)
  • Level 2 - lineItems array (sku, description, qty, unitPrice)
  • Level 3 - bins array (binCode, quantity)
"hierarchyRules": {
  "fieldVisibility": {
    "internalCode": false
  },
  "levelStyling": {
    "2": {
      "fontSize":     10,
      "borderStyle":  "full",
      "stripeOpacity": 0.4,
      "columnFormats": {
        "unitPrice": "currency",
        "qty":       "number",
        "orderDate": "date",
        "lineTotal": "currency",
        "totalQty":  "number"
      }
    },
    "3": {
      "fontSize":    9,
      "borderStyle": "minimal",
      "format":      "compact-kv",
      "columnFormats": {
        "quantity": "number"
      }
    }
  },
  "computations": [
    {
      "level":          2,
      "field":          "totalQty",
      "label":          "Total Qty",
      "type":           "sum",
      "source":         "bins",
      "aggregateField": "quantity",
      "format":         "number"
    },
    {
      "level":   2,
      "field":   "lineTotal",
      "type":    "formula",
      "formula": "qty * unitPrice",
      "format":  "currency"
    }
  ]
}

Computations run before rendering

The computations block writes derived fields (totalQty, lineTotal) onto each level-2 row before the table is rendered. Add these field names to columnFormats so the renderer applies the right format string to the computed values.

Level numbering starts at 1

Level 1 is always the scalar header block (non-array fields). The first array you see in the data is level 2. lineItems nested inside each order is therefore level 2, and bins nested inside each line item is level 3.


Complete Payload

{
  "templateId": "sales-order-report",
  "templateSettings": {
    "locale":     "en-US",
    "dateFormat": "MMM D, YYYY",
    "timezone":   "UTC"
  },
  "theme": {
    "primary": "#1a56db"
  },
  "hierarchyRules": {
    "fieldVisibility": {
      "internalCode": false
    },
    "levelStyling": {
      "2": {
        "fontSize":      10,
        "borderStyle":   "full",
        "stripeOpacity":  0.4,
        "columnFormats": {
          "unitPrice": "currency",
          "qty":       "number",
          "orderDate": "date",
          "lineTotal": "currency",
          "totalQty":  "number"
        }
      },
      "3": {
        "fontSize":      9,
        "borderStyle":   "minimal",
        "format":        "compact-kv",
        "columnFormats": {
          "quantity": "number"
        }
      }
    },
    "computations": [
      {
        "level":          2,
        "field":          "totalQty",
        "label":          "Total Qty",
        "type":           "sum",
        "source":         "bins",
        "aggregateField": "quantity",
        "format":         "number"
      },
      {
        "level":   2,
        "field":   "lineTotal",
        "type":    "formula",
        "formula": "qty * unitPrice",
        "format":  "currency"
      }
    ]
  },
  "reportTitle":  "Sales Order Report",
  "reportPeriod": "Q1 2026",
  "generatedAt":  "2026-03-18T09:00:00Z",
  "generatedBy":  "Finance Team",
  "salesOrders": [
    {
      "orderNumber": "SO-2026-001",
      "customer":    "Acme Corp",
      "orderDate":   "2026-01-15",
      "status":      "Fulfilled",
      "lineItems": [
        {
          "sku":         "WIDGET-A",
          "description": "Widget Alpha",
          "qty":         100,
          "unitPrice":   49.99,
          "bins": [
            { "binCode": "BIN-A01", "quantity": 60 },
            { "binCode": "BIN-A02", "quantity": 40 }
          ]
        },
        {
          "sku":         "WIDGET-B",
          "description": "Widget Beta",
          "qty":         50,
          "unitPrice":   89.99,
          "bins": [
            { "binCode": "BIN-B01", "quantity": 50 }
          ]
        }
      ]
    },
    {
      "orderNumber": "SO-2026-002",
      "customer":    "Globex Inc",
      "orderDate":   "2026-02-03",
      "status":      "In Progress",
      "lineItems": [
        {
          "sku":         "GADGET-X",
          "description": "Gadget X Pro",
          "qty":         25,
          "unitPrice":   199.00,
          "bins": [
            { "binCode": "BIN-C05", "quantity": 25 }
          ]
        }
      ]
    }
  ]
}

Design Decisions Summary

DecisionWhy
hierarchical_table not nested table nodesData has three levels of nesting - a single adaptive node handles all depths automatically
section layout: "2" for headerSingle titled header block with two content areas - no independent section titles needed
spacer_separator with lineStyle: "none"Explicit whitespace gap between header and table without a visible divider line
computations for totalQty and lineTotalDerived values computed from nested arrays before rendering - keeps business logic out of the payload transformation layer
format: "compact-kv" for level 3Bin data has few fields and high row count - compact key-value pairs consume far less vertical space than a full grid
fieldVisibility.internalCode: falseSuppress internal fields from the rendered output without removing them from the payload

Next Steps

  • Add a columnManifest to level 2 for explicit column ordering and overflow sub-rows - see Hierarchical Table: Column Manifest
  • Add pagination.breakBefore: "always" to the hierarchical_table node to force the table onto its own page
  • Use grouping.pageBreakPerGroup on a flat table when you have one level of data with group breaks - see Table: Grouping

On this page