Technical Article

XFA SOM Path Expressions in Delphi: Dynamic Form Data

losLab PDF Library reads and writes repeating data nodes in dynamic XFA forms through XFA 3.3 SOM path expressions: GetXFAFormFieldValue and SetXFAFormFieldValue accept the $data, $record and !data roots, [*] occurrence wildcards, descendant and child-wildcard selectors, the parent property, #dataGroup/#dataValue class selectors, and simple FormCalc-style predicates, so a Delphi program can update every invoice line in a government tax form with one call. This article is the addressing reference; for moving whole data sets between files, see the companion article on FDF, XFDF and XFA form data interchange

The problem shows up the moment a form stops being flat. A VAT return or customs declaration built as a dynamic XFA form does not have twelve fields named Total_Price_1 through Total_Price_12; it has one Detail subform declared once in the template DOM and instantiated as many times as the data demands. The values live in a second tree, the Data DOM inside the datasets packet, where each line item is a repeated <Detail> element under <Receipt>. Flat field names cannot address "the price on the third line" or "every line over 200"; that is exactly the job the Scripting Object Model chapter of the Adobe XFA 3.3 specification gives to SOM expressions, and it is the syntax the losLab PDF Library XFA field-value APIs speak

How do you address XFA data nodes from Delphi?

Every datasets path starts from a root, and losLab PDF Library accepts the standard spellings interchangeably: $data is the XFA 3.3 short form of xfa.datasets.data, !data is the short form rooted at xfa.datasets, and in packets that do not use record processing $record resolves to the outer data record, the first element under the data node. Template-side APIs such as SetXFAFormFieldAccess accept $template and xfa.template the same way. Segments may be dot-delimited or slash-delimited ($data.Receipt.Tax or $data/Receipt/Tax), which matters because GetXFAFormFieldNames enumerates fields as slash-delimited paths that feed straight back into the value and template APIs. Occurrence indexes are zero-based per the specification, so Detail[0] is the first row. Two escaping rules keep unusual names addressable: in a dot-delimited path \. denotes a literal period (Line\.Item), while slash-delimited paths treat periods as ordinary characters. Business data that carries its own XML namespace prefix is matched by local name, so <m:Receipt> still answers to Receipt

var
  Lib: TPDFlib;
  Tax: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('vat-return.pdf', '');
    // Equivalent addresses for the same data node
    Tax := Lib.GetXFAFormFieldValue('Receipt.Tax');
    Tax := Lib.GetXFAFormFieldValue('$data.Receipt.Tax');
    Tax := Lib.GetXFAFormFieldValue('xfa.datasets.data.Receipt.Tax');
    Tax := Lib.GetXFAFormFieldValue('$record.Tax');
    // Slash form with a zero-based occurrence index
    Lib.SetXFAFormFieldValue('$record/Detail[0]/Total_Price', '251.00');
    Lib.SaveToFile('vat-return-updated.pdf');
  finally
    Lib.Free;
  end;
end;

How do you fill repeating rows in an XFA form programmatically?

The [*] occurrence wildcard is the batch tool. Where a numeric index selects one sibling, [*] selects all same-name siblings, so $data.Receipt.Detail[*].Total_Price addresses the price field on every line item at once. On a read, GetXFAFormFieldValue returns the values of all matching nodes joined with a | separator; on a write, SetXFAFormFieldValue updates every matching node with the same value and returns 1 on success. A path that matches nothing reads back as an empty string, which is the cheap way to probe whether a branch exists before writing into it

var
  Lib: TPDFlib;
  Prices: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('invoice.pdf', '');
    // All Detail rows at once: '250.00|60.00'
    Prices := Lib.GetXFAFormFieldValue('$data.Receipt.Detail[*].Total_Price');
    // Reset one column across every repeated row in a single call
    Lib.SetXFAFormFieldValue('$data.Receipt.Detail[*].Surcharge', '0.00');
    Lib.SaveToFile('invoice-updated.pdf');
  finally
    Lib.Free;
  end;
end;

Descendant, child-wildcard and parent selectors

Three structural selectors cover the cases where you know the field name but not its exact depth. The descendant selector .. matches at any depth below the current node: $data..Total_Price finds the first Total_Price anywhere under the data root, and a write through a descendant path updates that first matching node. Note the boundary: an occurrence index after a descendant match does not step across sibling branches, so if $data..Total_Price[0] resolves inside the first Detail row, $data..Total_Price[1] returns empty rather than jumping to the next row; use Detail[*] when you want all of them. The child wildcard .* matches every direct child and lets the remaining segments filter: $data.Receipt.*.Total_Price reaches the Total_Price inside each child branch of Receipt without also selecting a same-name summary field sitting directly on Receipt itself. Finally, the parent property climbs one level, which XFA 3.3 deliberately separates from ..: $data.Receipt.Detail[1].parent.Tax starts at the second line item, steps up to Receipt, and lands on its sibling Tax value

Class selectors and predicates: #dataGroup, #dataValue, .[expression]

The #class syntax addresses Data DOM nodes by object class instead of by name, which is the practical route when XML tags contain characters that are awkward to spell as script names. losLab PDF Library maps #dataGroup to elements that have element children and #dataValue to leaf elements without element children, and both combine with numeric or [*] occurrences: $data.Receipt.#dataGroup[0].#dataValue[0] selects the first value inside the first group under Receipt. The predicate selector .[expression] filters same-name siblings by content. Supported predicates are simple comparisons of a child value against a literal, with relational operators such as >, < and <=; when a predicate matches several rows, reads come back |-joined and writes update every match

// Flag every line whose Total_Price exceeds 200
Lib.SetXFAFormFieldValue(
  '$data.Receipt.Detail.[Total_Price > 200].Review_Flag', '1');

// Descriptions of all low-value lines, '|'-joined when several match
Desc := Lib.GetXFAFormFieldValue(
  '$data.Receipt.Detail.[Total_Price <= 200].Description');

// Class-based addressing when tag names resist script spelling
Total := Lib.GetXFAFormFieldValue(
  '$data.Receipt.#dataGroup[0].#dataValue[0]');

Which XFA SOM features are not supported?

The SOM support in losLab PDF Library is scoped to datasets and template field paths, and the edges are explicit rather than best-effort. Knowing them up front saves debugging a path that silently returns an empty string

  • Predicates do not execute arbitrary FormCalc or JavaScript: no function calls (a contains(...) predicate returns empty), no compound boolean expressions, only a single child op literal comparison
  • $record works in non-record-processing packets only; dataWindow record paging and record-group rotation are not implemented
  • Resolution is against the Data DOM and template DOM directly; Form DOM relative resolution, the merged view a viewer builds at run time, is not performed
  • Data DOM attribute-loading semantics are not applied; the library reads the datasets packet as raw XML, so paths address elements, not attributes promoted to nodes

These boundaries rarely bite in batch-filling work, because a filler addresses data by structure rather than by scripted logic. When they do, the escape hatch is the packet level: read the whole datasets XML, transform it in Delphi, and write it back

Writing the whole packet with SetXFAFromString

SetXFAFromString is that packet-level entry point: it installs a complete XDP string, template and datasets together, creating the AcroForm container in a new document when none exists yet, and it accepts UTF-16 encoded packets with byte-order marks as well as UTF-8. A common production shape is to keep the agency-issued XDP as a fixture, load it with SetXFAFromString, then run SOM-addressed SetXFAFormFieldValue calls for the volatile per-invoice values before saving. Since XFA is one of two form models a PDF can carry, the classic AcroForm side has its own automation story, covered in the article on interactive form actions and JavaScript

The XFA field-value, enumeration and packet APIs shown here ship in losLab PDF Library for Delphi, C# and VB.NET; the product page carries the full form-handling reference