MD.OFFICE
FAL

2026-03-10

Iterative Table Formula Integration — Column Sums Only

Achievements

  • Iterative Table Support: Extended the formula builder to discover and expose column sum aggregate cells from iterative (dynamic-row) tables. Previously, iterative tables were completely excluded from formula suggestions.
  • Configured Aggregate Labels: Formula suggestion labels for row sum and column sum cells now use the actual configured rowAggregateLabel and columnAggregateLabel from the table (defaulting to 'Sum'), instead of hardcoded strings like 'Total' or 'Sum'.

Technical Decisions

  • Why only column sums for iterative rows: Iterative tables let users add and remove rows at runtime. Row keys are sequential numeric indices (0, 1, 2...) that re-index when any row is deleted. For example, if a user has rows 0, 1, 2 and deletes row 1, row 2 silently becomes row 1. If a formula referenced matrix_abc_2_col_1 (Row 3, Column 1), after that deletion the key matrix_abc_2_col_1 no longer exists — the data that was in Row 3 is now at matrix_abc_1_col_1. The formula would either fail silently or compute against wrong data. This makes individual cell keys and row sum keys (matrixKey_N_sum) inherently unstable and unsafe to reference in formulas.
  • Why row aggregates are excluded too: Row aggregate keys follow the same pattern as cell keys — matrixKey_<rowIndex>_sum. The row index (0, 1, 2...) is a runtime value that shifts when rows are deleted. Additionally, at design time (when the formula builder runs), rows: [] is empty so there is literally nothing to generate suggestions from. Column aggregates work because their keys (matrixKey_colKey_sum) derive from the column config (a structural property), not from runtime row data. Put simply: column aggregates are a property of the table's structure (columns), while row aggregates are a property of the table's data (rows). For iterative tables, data is runtime-only, so only structural keys survive.
  • Column sums are stable: Column sum keys (matrixKey_colKey_sum, matrixKey_groupKey_colKey_sum) are derived from the table's column configuration, not from runtime row state. They exist regardless of how many rows the user adds or removes, making them the only safe iterative table keys to expose in formulas.
  • No new files or services needed: The change was a surgical modification to the existing updateFormulaFieldSuggestions method in right-panel-static.component.ts, replacing the blanket if (el.iterativeTable) return; early exit with targeted column sum extraction.

Matrix Formula Guards — Prevent Destructive Configuration Changes

Achievements

  • Centralized formula reference checking in DynaformService:

    • Updated the regex in isFieldReferencedInFormulas to use \b word boundaries for accurate exact matching (matrix keys like table_row_1_col_1 need full-key matching, not partial).
    • Added areAnyKeysReferenced(keys: string[]) — a multi-key, short-circuit checker that iterates all keys and returns references from the first matched key.
  • Implemented guards in RightPanelStaticComponent:

    • onColumnTypeChange(item, col, newType, groupKey?): Prevents changing a column's type from number to anything else if any of its cells are currently referenced in a formula. Reverts the ngModel value via setTimeout.
    • onToggleRowAggregate(item, newValue): Prevents disabling row aggregates if any row-sum cells (matrixKey_rowKey_sum) are referenced. Reverts the toggle on block.
    • onToggleColumnAggregate(item, newValue): Same guard for column aggregates.
    • onToggleIterativeTable(item): Prevents switching to iterative mode if any static row cells or row aggregates are referenced in formulas (those keys would become invalid/unstable in iterative mode).
    • removeMatrixRow, removeSimpleMatrixColumn, removeMatrixColumn, removeMatrixColumnGroup: All deletion methods now guard against deleting a row/column/group if any of the cells it contains are referenced in formulas.
  • Added getFormulaKeysForMatrixStructure(item, options) helper — generates the exact set of formula jsonKeys for any structural context (a specific row, a simple column, a grouped column, an entire group, or all aggregates), handling both simpleColumn and useColumnGroups matrix layouts.

  • Wired HTML template bindings: Changed [(ngModel)] to [ngModel] + (ngModelChange)="handler()" for column type dropdowns and aggregate toggle switches, so new guard methods are invoked correctly on each change.

Problems Encountered & Decisions

  1. Wrong matrix prefix field (item.matrixId vs item.matrixKey):
    • The guard's key-generation helper getFormulaKeysForMatrixStructure was computing the matrix prefix as item.matrixId || item.jsonKey || ''.
    • However, all formula cell values are stored using item.matrixKey (e.g., the cell key table_row_1_col_1 uses "table" which is item.matrixKey, not item.matrixId).
    • The guard was generating keys like someJsonKey_row_1_col_1 while the formulas stored table_row_1_col_1 — these never matched, so areAnyKeysReferenced always returned [].
    • Confirmed by cross-referencing with updateFormulaFieldSuggestions (line 1678) and the group/simple mode migration helpers (lines 1139, 1167) — all use item.matrixKey || item.jsonKey || ''.
    • Fix: Changed the one line in getFormulaKeysForMatrixStructure to const matrixKey = item.matrixKey || item.jsonKey || ''.
    • Debug approach used: Added temporary console.log in isFieldReferencedInFormulas (logging the regex and each formula being tested) and in onColumnTypeChange (logging the generated keys). User confirmed the keys looked correct (table_row_1_col_1) but references returned []. Cross-referencing updateFormulaFieldSuggestions revealed the field name mismatch.

Files Modified

  • src/ifile-teapot-web-dynaforms/services/dynaform.service.ts — Added areAnyKeysReferenced.
  • src/ifile-teapot-web-dynaforms/components/dynaform-builder/right-panel-static/right-panel-static.component.ts — Added onColumnTypeChange, onToggleRowAggregate, onToggleColumnAggregate, getFormulaKeysForMatrixStructure and guards in all deletion methods + onToggleIterativeTable.
  • src/ifile-teapot-web-dynaforms/components/dynaform-builder/right-panel-static/right-panel-static.component.html — Updated column type dropdowns and aggregate toggle bindings to use [ngModel] + (ngModelChange).

Refactoring Guards: ChangeDetectorRef vs setTimeout

Achievements

  • Removed setTimeout from Matrix Guards: Replaced all asynchronous setTimeout reverts (which were used to prevent visual UI glitches when guarded actions were blocked) with synchronous, idiomatic Angular forced ChangeDetectorRef triggering (this.cdr.detectChanges()).
  • Synchronous Model Enforcement: Applied this.cdr.detectChanges() to the iterativeTable, col.type, includeRowAggregate, and includeColumnAggregate guards to instantly sync the rejected model state back to PrimeNG components.\

Decisions

  • One-Way Intercept [ngModel] vs Two-Way Binding [(ngModel)]:
    • Standardized on the single-binding intercept pattern ([ngModel]="value" + (ngModelChange)="onChange($event)") for heavily guarded UI toggles (column/row aggregates, iterative table toggle).
    • Why single binding: Two-way bindings ([(ngModel)]) temporarily mutate the source-of-truth model to an illegal state before the guard can reject and revert it. This creates a tiny window where other components or watchers might read corrupted data. Single binding fixes this by acting as an uncorruptible gatekeeper—the model is never touched until the guard explicitly approves the $event value.
  • Why ONE detectChanges() is enough:
    • Established that since the model is never updated during a rejected change (using single-binding), forcing a quick false -> detectChanges() -> true cycle forces the visual PrimeNG component to sync to the model's true state and snap back. Angular handles the final push automatically after the event loop ends, removing the need for a second DOM refresh.

Files Modified

  • src/ifile-teapot-web-dynaforms/components/dynaform-builder/right-panel-static/right-panel-static.component.ts — Injected ChangeDetectorRef, refactored all setTimeout reverts to use this.cdr.detectChanges(), updated onToggleIterativeTable signature to accept newValue for one-way binding.
  • src/ifile-teapot-web-dynaforms/components/dynaform-builder/right-panel-static/right-panel-static.component.html — Updated iterativeTable toggle binding to pass $event to the intercept method.

Matrix Formula Guards — Review & Refactor

Achievements

  • Code Review Completed: Performed a senior-level code review of the Matrix Formula Guards implementation, focusing on simplicity, Angular-agnosticism, and redundancy.
  • Redundancy Validated: Confirmed that the key-generation patterns duplicated between getFormulaKeysForMatrixStructure and updateFormulaFieldSuggestions are justified and not harmful, since they serve different purposes (structural guard blocks vs building full hierarchy suggestions) and produce different data shapes.
  • Method Relocation (Refactoring): Extracted getFormulaKeysForMatrixStructure from the 2300-line RightPanelStaticComponent (where it didn't belong due to being pure, framework-agnostic domain logic) and moved it into DynaformService.
  • References Updated: Updated all guard usages inside right-panel-static.component.ts to correctly reference the service method via this.state.getFormulaKeysForMatrixStructure(...).
  • Documentation Enhanced: Added a comprehensive JSDoc comment for getFormulaKeysForMatrixStructure in DynaformService to clearly explain its purpose within the validation guards and document all the configuration parameters within the options object.
  • Version Control: Committed the refactoring changes and pushed to the remote feature/df-matrix-formula-guards branch.

Files Modified

  • src/ifile-teapot-web-dynaforms/components/dynaform-builder/right-panel-static/right-panel-static.component.ts — Moved getFormulaKeysForMatrixStructure out and updated usages.
  • src/ifile-teapot-web-dynaforms/services/dynaform.service.ts — Added the relocated getFormulaKeysForMatrixStructure method with detailed JSDoc.