MD.OFFICE
FAL

Feature: Matrix Formula Row and Column Integration

Project: ifile-teapot-web-collect / md-office Date: 2026-04-06


Builder Component UI

Accordion Nested UX

Because formula rows and columns possess their respective formula builders, exposing them concurrently clutters the Property Panel drastically. The right-panel leverages p-accordion panels controlled symmetrically by synchronous index states (expandedMatrixRowFormulaIndex, expandedMatrixColFormulaIndex) to ensure only one active formula builder is expanded at once. Successful configuration immediately collapses the panel and sets a visual confirmation tick. Also, disabling the formula column completely inherently collapses the builder UI by resetting expandedMatrixColFormulaIndex = 0.

Contextual @ Mentions Transformation

The helpers getMatrixRowReferenceFields and getMatrixColReferenceFields cleanly intercept standard @Field suggestions, transforming them based upon the matrixFormulaMode:

  • Replaces backend keys with structurally mapped 1-based positional metrics: Row N — <User Label> and Col N — <User Label>. This logic uses rowNumber explicitly pointing to generated positional indexes regardless of type.
  • Preserves raw string configurations as strictly @Row.N to retain structural independence, ensuring evaluations remain valid even if users rename headers dynamically afterwards.

Real-time Validation Integration

Standard Builder validators (BuilderValidationOrchestratorService) historically bypassed matrix rows entirely. To ensure no unconfigured formulas are submitted downstream:

  1. isUnconfiguredFormulaField explicitly unpacks standard nested arrays recursively natively checking within type === 'table'.
  2. Property interactions instantly ping .validateItemFormula maintaining real-time UI accuracy by leveraging synchronous validations blocking overarching Save directives securely until missing parameters resolve.

Deletion Safe Guards

A persistent issue regarding computed form layouts resides inside maintaining variable dependencies when altering core schema columns/rows visually. Deleting columns/rows carelessly silently cascades breakages affecting the evaluation engine directly. The robust multi-layer guard approach protects matrix schemas safely.

Guard Detection Logic

  1. Internal Dependency Scanning (isRowUsedInMatrixFormulas & isColUsedInMatrixFormulas): These guards scan embedded indexing syntaxes directly. By computing 1-based positional data of referenced elements globally, it loops active formula nodes verifying matched string outputs using strict mapping (@Row.<N>(?!\d) / @Col.<N>(?!\d) regex patterns). The negative lookahead explicitly prevents @Row.1 incorrectly throwing errors against @Row.10.
  2. External Structure Mapping: Resolves prospective nested layout mappings capturing exact matrix properties directly from recursive scopes getFormulaKeysForMatrixStructure, then testing those mappings against external variables via boolean responses in areAnyKeysReferenced().

Guard Integration Points

All protection layers exist within right-panel-static.component.ts.

ActionInternal Guard ImplementationExternal Guard Implementation
removeMatrixRowChecks isRowUsedInMatrixFormulas → blocks + toastsPings getFormulaKeysForMatrixStructure({ row }) → blocks + toasts
removeSimpleMatrixColumnChecks isColUsedInMatrixFormulas(index + 1)Pings getFormulaKeysForMatrixStructure({ simpleColumn })
removeMatrixColumn (grouped)Iterates nested arrays, mapping flattened 1-based indexing testing isColUsedInMatrixFormulas per group entityPings getFormulaKeysForMatrixStructure({ groupedColumn })
removeMatrixColumnGroupValidates every single simple-column mapped under group arrayPings getFormulaKeysForMatrixStructure({ group })
onRowTypeChangeChecks isRowUsedInMatrixFormulas actively checking dangling dependenciesNot Applicable (type-changes don't remove matrix cells)
onToggleFormulaColumnNot Applicable (No internal column references permitted outwardly yet)Maps all inline rows dynamically calling areAnyKeysReferenced

Toast Messages: Blocked events output 'error' severity PrimeNG actions notifying users specifically which element stopped the deletion safely:

"Cannot delete row 'Day 1'. It is referenced in a formula row or formula column within this table." "Cannot delete column 'Food'. Its cells are currently used in formulas: formulaUsingFormulaColumn"

UI Visual State Revert for Models

Because Right-Panel controls (dropdown, inputSwitch) bind state two-ways by leveraging [(ngModel)], functions returning early due to blocked deletion conditions break visual-layer synchronization leaving switches toggled permanently incorrectly.

To fix stuck UI elements securely natively:

// Visual Reset leveraging ChangeDetectorRef
item.useFormulaColumn = false; // Bind to the incorrectly blocked visual state natively
this.cdr.detectChanges();      // Force Angular layout renders accepting visual differences 
item.useFormulaColumn = true;  // Instantly restore to precise protected schema state securely

Matrix Formula Evaluation — Renderer Technical Documentation

This document describes the complete mechanics of how formula rows and formula columns are computed in the Matrix widget renderer, how re-entrancy is prevented, and how external formula fields react to matrix changes.

Files involved: matrix.component.ts, formula-field.component.ts, form-engine.service.ts, dynaform.service.ts, formula.controller.ts (backend).


Overview

The Matrix widget supports two types of computed cells:

FeatureConfig PropertyFormula SyntaxDirection
Formula Rowrow.type === 'formula'@Row.N referencesCross-row: combines values from different rows
Formula Columnconfig.formulaColumn@Col.N referencesCross-column: combines values from different columns

A matrix can have multiple formula rows and one optional formula column. When both exist, their intersection cell (formula row × formula column) is also computed.

Example Matrix

          Col 1  Col 2  Formula Col (@Col.1 + @Col.2)
Day 1       1      2         3       ← regular row
Day 2       3      4         7       ← regular row
FOR1        4      6        10       ← formula row: @Row.1 + @Row.2
Row 4       5      5        10       ← formula row: @Row.1 + @Row.2 + @Row.3 (cascades)

The Debounce Pattern

To prevent firing an API call for every single keystroke, we use an RxJS Subject with a 300ms debounce.

// matrix.component.ts
this.formulaSub = this.formulaSubject
  .pipe(debounceTime(300))
  .subscribe(() => this.evaluateFormulas());

this.valueChangeSub = this.form.valueChanges.subscribe(() => {
  this.calculateAggregates(); // Synchronous sums + triggering formulaSubject
});

Full Trigger Flow


The Re-Entrancy Guard (_computingFormulas)

The Problem

When evaluateFormulas() completes, it updates the cells and triggers a final patchValue to notify external fields. However, this notification bubbles up and triggers the matrix's own valueChanges subscription, which calls calculateAggregates(). Without a guard, this would create an infinite loop.

The implementation

The _computingFormulas boolean acts as a lock.

1. The guard in calculateAggregates():

private calculateAggregates() {
  // ... structural sums ...
  
  if (needsFormulaEvaluation && !this._computingFormulas) {
    this.formulaSubject.next(); // Only schedule evaluation if not already running
  }
}

2. The execution guard in evaluateFormulas():

private evaluateFormulas(): void {
  this._computingFormulas = true; // Set the lock

  const stream$ = this.config.useFormulaColumn 
    ? this.evaluateFormulaColumn().pipe(concatMap(() => this.evaluateFormulaRows()))
    : this.evaluateFormulaRows();

  this.formulaExecutionSub = stream$
    .pipe(
      finalize(() => {
        this.form.patchValue({}, { emitEvent: true }); // Notify external fields
        this._computingFormulas = false; // Release the lock
      })
    )
    .subscribe();
}

Phase 1: Formula Column Evaluation

Method: evaluateFormulaColumn()

Computes the formula column for all regular rows in a single batch.

  1. Extracts @Col.N references using regex.
  2. Iterates through all regular rows (including iterative rows if enabled).
  3. Maps positional column indexes to actual FormControl values.
  4. Constructs a batch measurement payload: [{ id, expression, values }].
  5. Calls evaluateFormulaBatch.
  6. Updates results using ctrl.setValue(val, { emitEvent: false }) (silent update).

Phase 2: Formula Row Evaluation

Method: evaluateFormulaRows()

Formula rows are processed sequentially using concatMap. This is critical because a formula row might reference a previous formula row (cascading).

Single Row Evaluation (evaluateSingleFormulaRow$)

For each formula row:

  1. Extracts @Row.N references.
  2. Gathers values for all columns (simple or grouped) into a batch payload.
  3. Includes the Intersection Cell (Formula Row × Formula Column).
    • The intersection cell reads the previously computed formula column values of the referenced rows.
  4. Sends the batch call for that specific row.
  5. Updates all cell controls in that row silently.

External Formula Field Integration

External formula fields (outside the matrix) often reference matrix formula cells. Because matrix evaluations are async and debounced, the root form might fire many valueChanges before the matrix is truly "ready".

1. Control Discovery (form-engine.service.ts)

The addMatrixControls function ensures that formula column cells are registered with the engine even if they haven't been evaluated yet. This allows the root FormulaFieldComponent to find the control and subscribe to its parent.

2. Snapshot Comparison (formula-field.component.ts)

The external component subscribes to form.valueChanges at the root. To avoid infinite loops and redundant API calls:

  • It maintains a _lastValueSnapshot.
  • It only triggers an evaluation if the relevant matrix cell value has actually changed.
  • It uses a null-dependency guard: if a dependency is null (not yet evaluated), it skips the API call.

API Analysis: N+M vs. Batch API

ScenarioLegacy (Individual Calls)Current (Batch API)Improvement
20 rows, 1 Formula Col20 API calls1 API call95% reduction
10 rows, 1 Col, 2 Formula Rows30 API calls3 API calls90% reduction
50 rows, 1 Formula Col50 API calls1 API call98% reduction

Backend: Expression Deduplication

In formula.controller.ts, the evaluateBatch endpoint parses unique expressions once per request using a local cache. Since every row in a formula column evaluation usually shares the same expression, this reduces CPU overhead significantly.


File References

FileRole
matrix.component.tsRenderer: core logic, re-entrancy guard, batch orchestration
formula-field.component.tsExternal reactivity: snapshot comparison, null-guards
form-engine.service.tsengine: pre-registration of formula controls
dynaform.service.tsAPI Service: evaluateFormulaBatch implementation
formula.controller.tsBackend: batch evaluation and expression parsing cache
right-panel-static.component.tsBuilder: formula persistence, suggestions, deletion guards