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>andCol N — <User Label>. This logic usesrowNumberexplicitly pointing to generated positional indexes regardless of type. - Preserves raw string configurations as strictly
@Row.Nto 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:
isUnconfiguredFormulaFieldexplicitly unpacks standard nested arrays recursively natively checking withintype === 'table'.- Property interactions instantly ping
.validateItemFormulamaintaining real-time UI accuracy by leveraging synchronous validations blocking overarchingSavedirectives 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
- 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.1incorrectly throwing errors against@Row.10. - 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 inareAnyKeysReferenced().
Guard Integration Points
All protection layers exist within right-panel-static.component.ts.
| Action | Internal Guard Implementation | External Guard Implementation |
|---|---|---|
removeMatrixRow | Checks isRowUsedInMatrixFormulas → blocks + toasts | Pings getFormulaKeysForMatrixStructure({ row }) → blocks + toasts |
removeSimpleMatrixColumn | Checks isColUsedInMatrixFormulas(index + 1) | Pings getFormulaKeysForMatrixStructure({ simpleColumn }) |
removeMatrixColumn (grouped) | Iterates nested arrays, mapping flattened 1-based indexing testing isColUsedInMatrixFormulas per group entity | Pings getFormulaKeysForMatrixStructure({ groupedColumn }) |
removeMatrixColumnGroup | Validates every single simple-column mapped under group array | Pings getFormulaKeysForMatrixStructure({ group }) |
onRowTypeChange | Checks isRowUsedInMatrixFormulas actively checking dangling dependencies | Not Applicable (type-changes don't remove matrix cells) |
onToggleFormulaColumn | Not 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:
| Feature | Config Property | Formula Syntax | Direction |
|---|---|---|---|
| Formula Row | row.type === 'formula' | @Row.N references | Cross-row: combines values from different rows |
| Formula Column | config.formulaColumn | @Col.N references | Cross-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.
- Extracts
@Col.Nreferences using regex. - Iterates through all regular rows (including iterative rows if enabled).
- Maps positional column indexes to actual
FormControlvalues. - Constructs a batch measurement payload:
[{ id, expression, values }]. - Calls
evaluateFormulaBatch. - 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:
- Extracts
@Row.Nreferences. - Gathers values for all columns (simple or grouped) into a batch payload.
- Includes the Intersection Cell (Formula Row × Formula Column).
- The intersection cell reads the previously computed formula column values of the referenced rows.
- Sends the batch call for that specific row.
- 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
| Scenario | Legacy (Individual Calls) | Current (Batch API) | Improvement |
|---|---|---|---|
| 20 rows, 1 Formula Col | 20 API calls | 1 API call | 95% reduction |
| 10 rows, 1 Col, 2 Formula Rows | 30 API calls | 3 API calls | 90% reduction |
| 50 rows, 1 Formula Col | 50 API calls | 1 API call | 98% 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
| File | Role |
|---|---|
matrix.component.ts | Renderer: core logic, re-entrancy guard, batch orchestration |
formula-field.component.ts | External reactivity: snapshot comparison, null-guards |
form-engine.service.ts | engine: pre-registration of formula controls |
dynaform.service.ts | API Service: evaluateFormulaBatch implementation |
formula.controller.ts | Backend: batch evaluation and expression parsing cache |
right-panel-static.component.ts | Builder: formula persistence, suggestions, deletion guards |