A few years ago, I wrote about Creating Acumatica DAC Extension Tables — a clean alternative to dumping custom fields directly into standard Acumatica tables. If you haven't read that post, the short version is: a DAC Extension Table is a separate SQL table that Acumatica automatically joins to a standard table like SOShipment. Your custom fields live in that extension table, keeping the standard table clean and your customization nicely isolated.
That post covered the backend C# side: the DAC definition, the PXTable attribute, and the SQL table structure. What I didn't cover — because it wasn't relevant at the time — is what you need to do on the frontend when your Acumatica instance uses the Modern UI.
This article picks up where that one left off and walks through the TypeScript and HTML changes needed to surface extension table fields on a Modern UI page.
A Quick Refresher on Modern UI Page Structure
Modern UI forms are defined in TypeScript and HTML files rather than ASPX. For a given screen (say, SO302000 — Shipments), the files live in:
FrontendSources/screen/src/screens/SO/SO302000/
SO302000.ts ← screen class + view class definitions
SO302000.html ← layout template
extensions/
SO302000_MyExtension.ts
SO302000_MyExtension.html
The TypeScript file defines two things:
- A screen class (decorated with
@graphInfo) that declares which graph backs the form and creates a property for each data view. - View classes (extending
PXView) that list the fields available from each view.
The HTML file defines the layout — which tabs exist, which fieldsets appear in them, and which fields appear in each fieldset.
When you're adding new fields from a DAC Extension Table, you only need to touch the files in the extensions folder. You never modify the base screen files.
The Backend Relationship
Before getting into code, it's worth understanding why extension table fields are accessible directly on a view without any special joined-field syntax.
In C#, when you decorate your DAC extension table with [PXTable(IsOptional = true)] and it extends a base DAC, Acumatica automatically joins that extension table whenever the base DAC appears in a view. The graph doesn't need to know anything about the extension — the ORM handles it. By the time the data reaches the frontend, the extension fields look exactly like native fields of the base DAC.
That means in the Modern UI TypeScript code, you declare extension table fields the same way you declare any other field — no double-underscore joined-field syntax, no period notation. They're just fields on the view.
The Extension Files
Here's what a real example looks like. We have:
ISPSSOShipmentExtension— our DAC extension table that extendsSOShipment- Fields like
ISPSBillofLadingNo,ISPSTrailerNumber,ISPSProNumber, etc. - We want these fields to appear on a new SPS EDI tab on the Shipments (SO302000) screen
TypeScript Extension (SO302000_ISPSEDI.ts)
import {
SO302000, SOShipmentHeader, Packages
} from "src/screens/SO/SO302000/SO302000";
import {
PXFieldState
} from "client-controls";
// Extend the screen class itself (required boilerplate for any extension)
export interface SO302000_ISPSEDI extends SO302000 {}
export class SO302000_ISPSEDI {
}
// Extend the SOShipmentHeader view class to add our extension table fields
export interface ISPSEDICurrentDocument extends SOShipmentHeader { }
export class ISPSEDICurrentDocument {
ISPSBillofLadingNo: PXFieldState;
ISPSTrailerNumber: PXFieldState;
ISPSProNumber: PXFieldState;
ISPSLoadID: PXFieldState;
ISPSDeliveryDate: PXFieldState;
ISPSDelTime: PXFieldState;
ISPSTotalNumPallets: PXFieldState;
ISPSMarkForDelete: PXFieldState;
ISPSShipmentUDF1: PXFieldState;
ISPSShipmentUDF2: PXFieldState;
ISPSShipmentUDF3: PXFieldState;
ISPSShipmentUDF4: PXFieldState;
ISPSShipmentUDF5: PXFieldState;
}
A few things worth noting here:
The interface + class pattern. Each extension uses a TypeScript interface that extends the base class combined with a class of the same name. The interface declaration tells the TypeScript type system that ISPSEDICurrentDocument has all the members of SOShipmentHeader. The class declaration then adds the new fields. This is the standard Acumatica pattern for view class extensions.
The import. You import the base screen class (SO302000) and any view classes you're extending (SOShipmentHeader, Packages) from the base screen's TypeScript file. The import path uses src/screens/... — the source tree path, not the extension folder's relative path.
The field declarations. Every field from ISPSSOShipmentExtension that you want to use in the UI gets declared here as PXFieldState. You only need to list the fields you actually intend to display — you don't have to declare every field in the DAC extension table.
No joined-field syntax needed. Notice that ISPSBillofLadingNo is declared directly as a property, not as ISPSSOShipmentExtension__ISPSBillofLadingNo or using the period notation. Because the backend graph already joins the extension table, the frontend treats these fields as native members of the view.
If you're also adding fields to a grid view (like Packages), the same pattern applies — create an interface/class pair that extends the grid's view class and declare the new fields there.
HTML Extension (SO302000_ISPSEDI.html)
<template>
<qp-tab
after="#tab-Packages"
id="tab-ISPSEDI_Header"
caption="SPS EDI"
>
<qp-template name="1-1" id="form-ISPSEDI_Header">
<div id="divColumnA-SPS" slot="A">
<qp-fieldset id="ISPS-UDFs" view.bind="Document">
<field name="ISPSBillofLadingNo"></field>
<field name="ISPSTrailerNumber"></field>
<field name="ISPSProNumber"></field>
<field name="ISPSLoadID"></field>
<field name="ISPSDeliveryDate"></field>
<field name="ISPSDelTime" config-time-mode.bind="true"></field>
<field name="ISPSTotalNumPallets" config-enabled.bind="false" config-allow-edit.bind="true"></field>
</qp-fieldset>
</div>
<div id="divColumnB-SPS" slot="B">
<qp-fieldset id="ISPS-UDFs1" view.bind="Document">
<field name="ISPSShipmentUDF1"></field>
<field name="ISPSShipmentUDF2"></field>
<field name="ISPSShipmentUDF3"></field>
<field name="ISPSShipmentUDF4"></field>
<field name="ISPSShipmentUDF5"></field>
<field name="ISPSMarkForDelete"></field>
</qp-fieldset>
</div>
</qp-template>
</qp-tab>
</template>
A few things to note here:
after="#tab-Packages" — This positions the new tab after the existing Packages tab in the tab strip. The # prefix references an element by its id. If you want the tab first, use before instead. This is how extension HTML files insert themselves into an existing form's layout without touching the base files.
view.bind="Document" — The Document view on SO302000 is backed by SOShipmentHeader (which we extended in TypeScript). Binding the fieldset to Document is what connects it to the view class we extended. The Acumatica runtime resolves the field names against the merged view class — native fields and extension table fields alike.
<field name="ISPSBillofLadingNo"> — Field names in HTML match exactly the property names declared in the TypeScript view class extension. No prefix, no dot notation.
Field-level configuration. Notice config-time-mode.bind="true" on the time field and config-enabled.bind="false" config-allow-edit.bind="true" on ISPSTotalNumPallets. These are standard Modern UI field configuration bindings. The config-allow-edit flag lets a disabled field still be editable via a separate trigger — useful for fields that should only be updated programmatically but whose value you want users to override when needed.
Putting It Together
The full picture of what happens at runtime:
- Acumatica loads
SO302000.tsandSO302000.htmlfor the base form. - It scans the
extensionsfolder and findsSO302000_ISPSEDI.tsandSO302000_ISPSEDI.html. - The TypeScript extension merges
ISPSEDICurrentDocumentinto theSOShipmentHeaderview class, making the ISPS fields visible to the runtime's type system. - The HTML extension inserts the new
qp-tabafter#tab-Packages. - When the form renders, the fieldsets bound to
DocumentresolveISPSBillofLadingNo(and the rest) through the merged view class. - On the backend, the ORM's automatic join of
ISPSSOShipmentExtensiontoSOShipmentensures those fields are populated from the database.
Things to Watch Out For
Build step required. Unlike Classic UI customizations, Modern UI TypeScript changes require a build. Run npm run build (or your project's equivalent) from the FrontendSources directory before deploying. The build compiles the TypeScript and generates the JavaScript bundle that Acumatica actually serves.
Field names are case-sensitive. The name attribute in the HTML <field> tag must match exactly — including casing — the property name declared in your TypeScript view class extension. A mismatch will result in the field not rendering.
Extension file naming. The convention is [ScreenID]_[PostfixDescribingTheExtension].ts/.html. Both files must exist together — a .ts without a corresponding .html (or vice versa) is fine if you have nothing to declare in one of them, but an empty file with just <template></template> or an empty class is the typical placeholder.
DAC Extension Table fields vs. joined DAC fields. This article covers fields from a DAC Extension Table — a separate table that Acumatica automatically joins to the base table at the ORM level. This is different from fields from a joined DAC that you've explicitly joined in a BQL statement in the graph. For those, you do need the double-underscore or period notation described in the Acumatica Modern UI documentation.
Conclusion
Adding DAC Extension Table fields to a Modern UI form comes down to two files in the extensions folder: a TypeScript file that extends the view class with your new field declarations, and an HTML file that adds those fields to the layout. Because Acumatica's ORM handles the table join automatically on the backend, the frontend treats extension table fields just like native fields — no special syntax required.
If you haven't read the original DAC Extension Tables article yet, start there for the C# and SQL setup, then come back here for the frontend half. Happy coding!