QuantityFormatter Lifecycle & Integration

This page covers the QuantityFormatter lifecycle APIs — readiness signals, spec registration, multi-system access, and auto-refreshing handles. These complement the core Parsing and Formatting workflows and Provider setup.

Who should read this

Role Question you're asking Jump to
Application Developer "How do I know when formatting is ready after app init?" Readiness & Initialization
Tool Provider "How do I register my domain's formatting specs and keep them fresh across reloads?" Spec Provider Integration
Tool Developer "How do I get auto-refreshing specs for my measure tool?" FormatSpecHandle
Tool Consumer "How do I display a formatted value in my UI component and keep it current?" FormatSpecHandle, Multi-System KoQ Access

Readiness & Initialization

The QuantityFormatter loads formatting and parsing specs asynchronously. Specs are not available immediately after IModelApp.startup() — you need to synchronize with the readiness lifecycle.

whenInitialized

QuantityFormatter.whenInitialized is a one-shot promise that resolves after the first successful initialization. It resolves once and stays resolved — safe to await at any point in app startup.

Example Code
/** Wait for the QuantityFormatter to complete its first initialization */ export async function waitForFormatterReady() { // Resolves after the first successful initialization; safe to call multiple times await IModelApp.quantityFormatter.whenInitialized; // Formatting APIs are now safe to use const formatterSpec = IModelApp.quantityFormatter.findFormatterSpecByQuantityType(QuantityType.Length); if (formatterSpec) { const formatted = IModelApp.quantityFormatter.formatQuantity(1.5, formatterSpec); assert(formatted.length > 0); } }

isReady

QuantityFormatter.isReady is a synchronous boolean. Returns false until the first reload completes, then true. Use it as a guard before synchronous spec lookups.

Example Code
/** Check if the QuantityFormatter is ready before formatting */ export function formatIfReady(meters: number): string | undefined { if (!IModelApp.quantityFormatter.isReady) return undefined; // Formatter not yet initialized const formatterSpec = IModelApp.quantityFormatter.findFormatterSpecByQuantityType(QuantityType.Length); if (!formatterSpec) return undefined; return IModelApp.quantityFormatter.formatQuantity(meters, formatterSpec); }

onFormattingReady

QuantityFormatter.onFormattingReady fires after every reload completes — initialization, unit system changes, and provider changes. This is the primary signal for keeping UI and caches in sync.

Example Code
/** Subscribe to onFormattingReady to refresh UI when formatting changes */ export function subscribeToFormattingReady(updateUI: (formatted: string) => void) { const removeListener = IModelApp.quantityFormatter.onFormattingReady.addListener(() => { const spec = IModelApp.quantityFormatter.findFormatterSpecByQuantityType(QuantityType.Length); if (spec) { const formatted = IModelApp.quantityFormatter.formatQuantity(1.5, spec); updateUI(formatted); } }); // Call removeListener() when the component unmounts return removeListener; }

Set-backed event: QuantityFormatter.onFormattingReady uses BeUnorderedUiEvent — a Set-backed event where listeners can safely add or remove themselves during emission and unsubscribe in O(1) via the closure returned by addListener().

Spec Provider Integration

This section is for teams that supply domain-specific formatting specs to the QuantityFormatter registry — for example, Civil's DisplayUnitFormatter or any package that provides KindOfQuantity definitions beyond the built-in defaults.

The problem

When the formatter reloads (unit system change, provider change, app init), the internal spec registry is rebuilt from IModelApp.formatsProvider. Any specs your domain registered via QuantityFormatter.addFormattingSpecsToRegistry are lost and need to be re-registered.

The pattern

  1. Subscribe to QuantityFormatter.onBeforeFormattingReady to register async work before the formatter is considered ready
  2. In your listener, call collector.addPendingWork(promise) with a promise that re-registers your domain's KoQ specs via QuantityFormatter.addFormattingSpecsToRegistry
  3. The formatter awaits all pending work (with a 10-second timeout) before emitting QuantityFormatter.onFormattingReady
  4. Downstream tool consumers using FormatSpecHandle or getSpecsByNameAndUnit will see your domain specs immediately when onFormattingReady fires

Event ordering note: The formatter follows a two-phase ready flow:

  1. onBeforeFormattingReady — Fires first. Providers register async work via the FormattingReadyCollector passed to listeners. Call collector.addPendingWork(promise) to register each async task.
  2. The formatter awaits all pending work (with a 10-second timeout). Rejections are logged as warnings but do not block readiness.
  3. onFormattingReady — Fires after all provider work has settled. Consumers can now safely read specs knowing all providers have finished registering.

Pattern: Providers use onBeforeFormattingReady, consumers use onFormattingReady.

Example: Registering async provider work before formatting is ready
/** Register async provider work before formatting is ready */ export function registerAsyncProvider() { const removeListener = IModelApp.quantityFormatter.onBeforeFormattingReady.addListener((collector) => { collector.addPendingWork(loadAndRegisterDomainFormats()); }); // Call removeListener() on teardown to unsubscribe return removeListener; } async function loadAndRegisterDomainFormats(): Promise<void> { // Async loading work here... await IModelApp.quantityFormatter.addFormattingSpecsToRegistry({ name: "MyDomain.MyKoQ", persistenceUnitName: "Units.M" }); }
Example: Domain spec provider that re-registers on reload
/** * Example: A domain spec provider that registers its KindOfQuantity specs * and re-registers them whenever the QuantityFormatter reloads. * * This is the pattern used by Civil's DisplayUnitFormatter and similar * domain-level providers that supply custom formatting specs. */ export class MyDomainFormatProvider { private _removeListener?: () => void; /** KoQ entries this domain provides: [koqName, persistenceUnit] */ private readonly _domainEntries: Array<[string, string]> = [ ["MyDomain.PRESSURE", "Units.PA"], ["MyDomain.FLOW_RATE", "Units.CUB_M_PER_SEC"], ]; /** Call once during app setup to start listening for reloads */ public register() { // Re-register domain specs before every ready cycle via the collector. // The formatter awaits all pending work before emitting onFormattingReady, // so downstream consumers see these specs immediately. this._removeListener = IModelApp.quantityFormatter.onBeforeFormattingReady.addListener((collector) => { collector.addPendingWork(this._registerSpecs()); }); } /** Call on teardown to stop listening */ public unregister() { this._removeListener?.(); this._removeListener = undefined; } private async _registerSpecs() { for (const [koqName, persistenceUnit] of this._domainEntries) { try { await IModelApp.quantityFormatter.addFormattingSpecsToRegistry({ name: koqName, persistenceUnitName: persistenceUnit }); } catch { // KoQ not found in formatsProvider — may not be available in this iModel } } } }

Composite-keyed registry

The spec registry is keyed by KindOfQuantity name, persistence unit, and unit system ([koqName][persistenceUnit][unitSystem]). This means the same KoQ with different persistence units or different unit systems can coexist.

Use QuantityFormatter.getSpecsByNameAndUnit to retrieve a specific entry by its composite key. Pass an optional system parameter to retrieve specs for a non-active unit system:

Example Code
/** Look up formatting specs by KindOfQuantity name and persistence unit */ export function lookupSpecsByNameAndUnit() { const entry = IModelApp.quantityFormatter.getSpecsByNameAndUnit({ name: "DefaultToolsUnits.LENGTH", persistenceUnitName: "Units.M", }); if (entry) { // Format a value const formatted = IModelApp.quantityFormatter.formatQuantity(3.14, entry.formatterSpec); assert(formatted.length > 0); // Parse a user-entered string const result = entry.parserSpec.parseToQuantityValue("3.14 m"); assert(result.ok && typeof result.value === "number"); } }

Multi-System KoQ Access

The spec registry supports storing and retrieving specs for multiple unit systems simultaneously. This is useful when you need to display the same measurement in different unit systems — for example, showing both metric and imperial values side-by-side.

Example: Format a KoQ in multiple unit systems
/** Access KoQ format specs for non-active unit systems */ export function formatKoqInMultipleSystems(meters: number) { const metricSpec = IModelApp.quantityFormatter.getSpecsByNameAndUnit({ name: "DefaultToolsUnits.LENGTH", persistenceUnitName: "Units.M", system: "metric" }); const imperialSpec = IModelApp.quantityFormatter.getSpecsByNameAndUnit({ name: "DefaultToolsUnits.LENGTH", persistenceUnitName: "Units.M", system: "imperial" }); if (metricSpec && imperialSpec) { const metricStr = IModelApp.quantityFormatter.formatQuantity(meters, metricSpec.formatterSpec); const imperialStr = IModelApp.quantityFormatter.formatQuantity(meters, imperialSpec.formatterSpec); assert(metricStr.length > 0 && imperialStr.length > 0); } }

FormatSpecHandle

FormatSpecHandle is a cacheable, auto-refreshing handle to formatting specs. It's the recommended way for tool developers and UI components to hold a reference to a formatting spec without manually subscribing to reload events.

Key behaviors:

  • Fallback formattingformat(value) returns value.toString() if specs aren't loaded yet, so your tool always produces output
  • Auto-refresh — The handle subscribes to QuantityFormatter.onFormattingReady and updates its internal specs on every reload
  • Disposable — Call dispose() or use a using declaration to unsubscribe from events and avoid leaks

Basic Usage

Create a handle via QuantityFormatter.getFormatSpecHandle, use it to format values, and dispose when done:

Example Code
/** Create, use, and explicitly dispose a FormatSpecHandle */ export function useFormatSpecHandle() { const handle = IModelApp.quantityFormatter.getFormatSpecHandle( "DefaultToolsUnits.LENGTH", // KindOfQuantity name "Units.M", // Persistence unit ); // format() returns a fallback string if specs aren't loaded yet const formatted = handle.format(1.5); assert(formatted.length > 0); // Explicitly dispose when done to unsubscribe from reload events handle[Symbol.dispose](); }

Using Declaration

FormatSpecHandle implements Symbol.dispose, so you can use a using declaration for automatic cleanup when the handle goes out of scope:

Example Code
/** Use a FormatSpecHandle with `using` for automatic disposal */ export function useFormatSpecHandleWithUsing() { using handle = IModelApp.quantityFormatter.getFormatSpecHandle( "DefaultToolsUnits.LENGTH", "Units.M", ); // The handle auto-refreshes when the formatter reloads const formatted = handle.format(2.75); assert(formatted.length > 0); // handle is automatically disposed at end of scope }

When to use FormatSpecHandle vs events

Pattern Best for Why
FormatSpecHandle Tool developers, UI components that format a specific KoQ Zero boilerplate — just create, format, dispose. Auto-refreshes.
onBeforeFormattingReady Spec providers that register domain specs, async loading Async work is awaited before the formatter is considered ready. Specs are available to all onFormattingReady consumers.
onFormattingReady Consumers that refresh UI or read specs after each reload Fires after all provider work has settled — safe to read any registered specs.
isReady / whenInitialized App startup gates, lazy initialization One-time checks before first use.

Migrating from Multiple Event Subscriptions

If your code subscribes to multiple QuantityFormatter events to stay in sync with formatting changes, you can simplify by migrating to QuantityFormatter.onFormattingReady or FormatSpecHandle.

Before: Multiple event subscriptions

A common legacy pattern involves subscribing to several events to cover all the ways formatting can change:

// ❌ Old pattern — subscribing to multiple events const unsubscribers = [ IModelApp.quantityFormatter.onActiveFormattingUnitSystemChanged.addListener(() => { refreshDisplay(); }), IModelApp.quantityFormatter.onQuantityFormatsChanged.addListener(() => { refreshDisplay(); }), IModelApp.formatsProvider.onFormatsChanged.addListener(async () => { await rebuildCaches(); refreshDisplay(); }), ]; // Must remember to unsubscribe all on teardown

Each of these events covers a different reload trigger, but they all mean the same thing: "formatting specs have changed."

After: Single event or auto-refreshing handle

Option A — For spec providers (packages that register domain KoQs):

Replace all subscriptions with QuantityFormatter.onBeforeFormattingReady. Register your async loading work via the FormattingReadyCollector — the formatter awaits all pending work before emitting QuantityFormatter.onFormattingReady.

// ✅ Provider work is awaited before formatting is considered ready const removeListener = IModelApp.quantityFormatter.onBeforeFormattingReady.addListener((collector) => { collector.addPendingWork( IModelApp.quantityFormatter.addFormattingSpecsToRegistry("MyDomain.PRESSURE", "Units.PA") ); }); // Single unsubscribe on teardown removeListener();

Option B — For tool developers and UI components (recommended):

Replace event subscriptions entirely with FormatSpecHandle. Each handle auto-refreshes when formatting changes and provides a format() method with a built-in fallback:

// ✅ No event subscriptions needed const handle = IModelApp.quantityFormatter.getFormatSpecHandle( "DefaultToolsUnits.LENGTH", "Units.M", ); // Always produces output — auto-refreshes on formatting changes label.textContent = handle.format(distanceInMeters); // Clean up on teardown handle[Symbol.dispose]();

Migration summary

Old pattern New pattern When to use
2-4 event subscriptions + async spec registration onBeforeFormattingReady You re-register domain specs or perform async loading before ready
Event subscription + manual spec re-fetch onFormattingReady You refresh UI or read specs after each reload
Event subscription + findFormatterSpecByQuantityType() FormatSpecHandle You format values for display in a tool or UI component
Guard pattern against double-subscription Neither needed Events fire exactly once per reload; FormatSpecHandle manages its own subscription

See Also

Last Updated: 15 April, 2026