---
name: evolution_evomodal_declarative
description: Gen-2 dynamic modals deploy declaratively via data-evo-* attributes; evoModal.js is global
metadata: 
  node_type: memory
  type: project
  originSessionId: 3b0ec29c-b418-4f31-bfd1-bb30042a8717
  modified: 2026-07-28T04:02:17.882Z
---

Evolution's Gen-2 dynamic-modal system (Ticket 1154, branch `quoteOptimizeV3Htmx`)
is now **declarative** — deploy a modal by markup alone, no per-page JS:

```html
<a data-evo-type="dynamicModal" data-evo-tgt="<registryKey>" data-evo-id="<entityId>">…</a>
```

- `assets/js/evoModal.js` is loaded **globally in index.php** (right after global.js), not per-page. It scans `[data-evo-type="dynamicModal"]` on DOMContentLoaded AND delegates clicks at document, so late-rendered triggers (ajax rows/tabs) work with no re-scan.
- Optional `data-evo-ready="fnName"` → global `fn(triggerEl, modalId)` after open (e.g. `editRetention` on a `.retentionRow` `<tr>`).
- `window.evoModalKeys` (emitted by index.php from `ModalRegistry::allowedKeys()`) lets the client console-warn at scan time when a `data-evo-tgt` isn't registered — before any click.
- Backend `modalload.php` gives dev-facing errors: unregistered key → 404 "not registered… add to library/ModalRegistry.php"; template not under `modaltemplates/` or missing → 500 with the path. Both surface as console + toast client-side.
- Legacy `evoModal.init('#sel','key',id,onReady)` still works but is deprecated; all job-edit triggers converted to declarative.

Add a new modal = 4 steps: registry row in `library/ModalRegistry.php`, memoised getter(s) in the context class (`library/JobContext.php`), pure-markup `modaltemplates/<key>.php` (NO inline `<script>` — evoModal doesn't execute injected scripts; behaviour goes in the page module via delegation), then the declarative trigger.

Docs: `docs-dev/dynamicmodals.inc` (index.php?page=docs-dev/dynamicmodals). Migration plan/inventory: `evolution/audits/DYNAMIC_MODAL_MIGRATION.md`. Inline Gen-1 render blocks stay gated behind `if(!$moveModalsToAjax)` (true on the branch, so dead there — but the flag itself is branch-only, see below). Related: [[evolution_ai_quote_notes]], [[docs_authoring]].

Gotcha writing docs-dev pages: literal `<?php ?>` inside a `<textarea>` STILL executes (PHP tokenizer ignores HTML context) — escape as `&lt;?php … ?&gt;`.

**Branch merge state (checked 2026-07-27) — the branch is UNMERGED and this is the next piece of work.**
`$moveModalsToAjax = true;` at `index.php:411` is **branch-only — zero occurrences in `staging`**. That
single flag is the whole jobedit load win, so nothing ships until the branch merges. Branch is 34 commits
ahead of staging, 13 non-merge commits behind. `git merge-tree` yields **0 conflict markers**, but 5 files
changed on both sides — `quotedit.inc` (branch rewrote 2,595 lines while staging added 61 for
[[evolution_ai_quote_notes]]), `jobedit.inc`, `jobeditquot.inc`, `quotedit.js`, `quoteditsave.php` — so
the clean auto-merge needs a **semantic** review, not a tick.

Agreed 5-step plan (proposed, not yet started): merge staging *into* the branch → hand-review the merged
regions of those 5 files → verify all 14 modals open on dev + measure TTFB → merge to staging (where the
load win lands) → resume Gen-1→Gen-2 migration per §9.4 of `DYNAMIC_MODAL_MIGRATION.md` → release notes +
docs per [[feature_release_docs_policy]]. Gen-2 currently has 5 job modals; **9 job + 7 quote remain on
Gen-1**, and no `QuoteContext.php` exists yet.

Scope boundary vs [[mariadb_oom_investigation]]: this branch owns jobedit/quotedit **modal** deferral
(8 jobedit + 7 quotedit inline bodies, modal bodies pulled out of tab renders, the
`quoteditmovemodalcommon.inc` ~15-queries-per-open cost). **Tab lazy-loading is the OOM doc's**, not the
branch's — tab includes verified byte-identical on both sides. Don't edit modal `.inc` bodies while the
branch is open.
