Skip to content

feat: [dasc] Form v2#436

Closed
kozmaadrian wants to merge 86 commits into
mainfrom
form-v2
Closed

feat: [dasc] Form v2#436
kozmaadrian wants to merge 86 commits into
mainfrom
form-v2

Conversation

@kozmaadrian
Copy link
Copy Markdown
Contributor

@kozmaadrian kozmaadrian commented May 11, 2026

Test url: https://da.live/form?nx=form-v2#/kozmaadrian/da-sc/forms/demo-v2

Form Block — v2 Review Guide

This guide covers the scope and motivation behind the change, and walks through the architecture and design decisions of the new implementation.

Part 1 — v1 vs v2

Scores

v1 v2
Code simplicity ★★☆☆☆ ★★★★☆
Cleanliness & consistency ★★☆☆☆ ★★★★★
A new engineer can understand it ★★☆☆☆ ★★★★☆
Follows codebase best practices ★★☆☆☆ ★★★★★
Testability (without a browser) ★☆☆☆☆ ★★★★★
Correctness under real usage ★★☆☆☆ ★★★★★
Ready for AI / MCP / agent integration ★☆☆☆☆ ★★★★★
Error visibility ★★☆☆☆ ★★★★☆

The problems in v1

1. Hard to follow. Schema resolution, state, mutations, validation, serialization, and network saves were all compressed into a single FormModel class with shared mutable state. There was no clear entry point, and the loading state relied on implicit null/undefined conventions that were invisible in the render logic. A new engineer had nowhere safe to start.

2. Mixed layers and responsibilities. There were no enforced boundaries between concerns. Data handling, UI logic, and network calls were coupled throughout — touching one area could silently break another, and any new feature had to navigate the entire codebase to understand where it belonged.

3. Not testable. The business logic had no clean seams. Testing any single concern required constructing the entire model with a full document, schema, and path — there was no way to verify schema compilation, validation, or a mutation in isolation. The only way to catch a regression was manually, in a browser, which meant bugs reached users before they were found.

4. Not reusable. With no headless layer, any external consumer — the DA import tool, an MCP server, an AI agent — had to re-implement the same logic the editor uses: validation, schema resolution, serialization. Copied logic drifts. There was no shared source of truth.

5. Active data loss bug. Every mutation immediately fired a network save with no coordination between concurrent calls. If two saves raced, the older response landing last would silently overwrite the user's latest change.

File structure

v1                                  v2
──────────────────────────────────  ──────────────────────────────────────────
data/                               core/          ← pure logic, no DOM
  model.js                            index.js     ← public API (createCore)
utils/                                schema.js    ← schema compiler
  html2json.js                        model.js     ← document model builder
  json2html.js                        mutate.js    ← pure mutation functions
  pointer.js                          validation.js
  schema.js                           pointer.js
  validator.js                        ids.js
  value-resolver.js                   clone.js
views/                              app/           ← orchestration
  editor.js                           context.js   ← loading state machine
  sidebar.js                          da-api.js    ← network layer
  preview.js                          serialize.js
  components/                         html2json.js
    array-item-menu/                  json2html.js
    reorder-dialog/                 ui/            ← rendering only
form.js                               editor.js
form.css                              sidebar.js
                                      preview.js
                                      array-menu.js
                                      reorder.js
                                    docs/
                                      architecture.md
                                      schema-spec.md
                                      request-flow.md
                                      performance-review.md
                                      headless-consumer.md
                                    form.js

The shift is not just a rename — the three folders enforce a strict dependency rule: ui/ can import from core/, but core/ has no knowledge of ui/, app/, or the browser. That constraint is what makes the core headless and testable.


Part 2 — Overview of v2

Why it is built the way it is

The core design principle of v2 is that each concern lives in exactly one place and cannot bleed into another. Instead of one class that does everything, v2 has three layers with explicit, documented boundaries:

┌──────────────────────────────────┐
│  ui/   (rendering only)          │  reads state, calls core mutations
├──────────────────────────────────┤
│  app/  (orchestration)           │  wires core to the network and DOM
├──────────────────────────────────┤
│  core/ (pure logic, no DOM)      │  schema, model, validation, mutations
└──────────────────────────────────┘

core/ is the most important layer. It has zero browser dependencies — no DOM, no fetch, no globals. The entire form's business logic can be loaded in Node.js, in an MCP server, in a CLI script, or in a test runner without a browser. The ui/ layer knows about the DOM but knows nothing about schemas or validation. The app/ layer is the thin glue between them. The boundary is enforced by convention and import structure, not by a build tool, so it is worth checking in review.

Why it is more future-proof

AI, MCP, and agent integration is already possible. Because core/ has no browser dependencies, an AI agent or MCP server can call createCore(), load a schema and document, validate it, mutate fields, and read the result — all without a browser. The headless-consumer.md file in the block documents exactly how to do this. This was impossible with v1.

It has a stable public API. createCore() returns a fixed, named set of functions. Any consumer — a UI, a test, a script, an agent — programs against that contract. The internals can change without breaking callers, as long as the contract holds.

Saves are correct by construction. The single-flight queue is built into the core. No caller can accidentally bypass it. No new mutation type will accidentally introduce the v1 overwrite bug.

Errors are visible by design. Schema issues (unresolvable refs, unsupported composition) and save failures are part of the state shape, not console logs. Any UI or consumer that reads state will see them.

It ships documentation as part of the block. architecture.md, schema-spec.md, and request-flow.md live alongside the code. A new engineer can read the architecture document before reading a single source file.


Part 3 — Main Components and Their Responsibilities

File Responsibility
form.js Shell — creates the core, routes to the right view based on context status, passes state down. Contains no business logic.
app/context.js Loading state machine — takes a URL and resolves to one of: ready, select-schema, no-schemas, or blocked (with a typed reason). Replaces v1's implicit null/undefined conventions.
app/da-api.js Network layer — thin fetch/save wrappers that return { html } or { error, status }.
app/serialize.js + html2json.js + json2html.js Round-trip serialization between DA's HTML storage format and internal JSON.
core/index.js Public API — createCore() returns load, setField, addItem, insertItem, removeItem, moveItem, getState. Everything else in core is internal.
core/schema.js Schema compiler — resolves $ref, detects cycles, flags unsupported composition keywords (allOf/oneOf/anyOf) as schemaIssues instead of dropping them silently.
core/model.js Model builder — merges the compiled definition with the live document into a tree indexed by JSON Pointer for O(1) lookup. Assigns stable IDs to array items so reorder does not destroy input elements.
core/mutate.js Pure mutations — each function takes a document and returns a new one. Nothing is mutated in place.
core/validation.js Field validators — walks the model and returns errorsByPointer with user-facing error strings, one entry per failing field.
ui/editor.js Field rendering — native inputs with proper label associations and aria-invalid. Stateless: reads core state, calls core mutations.
ui/sidebar.js Tree navigation — lets the author move between sections; shows schema issue indicators on affected nodes.
ui/preview.js Read-only JSON view of the current document, updated reactively.

Further reading

  • architecture.md — design decisions, the three-layer rule, and the full state shape
  • schema-spec.md — which JSON Schema keywords are supported, validated-only, or explicitly unsupported
  • request-flow.md — the full loading sequence from boot to first render
  • performance-review.md — known hot paths and their current budgets

v2 — same UX

da-sc-v2

@aem-code-sync
Copy link
Copy Markdown

aem-code-sync Bot commented May 11, 2026

Hello, I'm the AEM Code Sync Bot and I will run some actions to deploy your branch.
In case there are problems, just click the checkbox below to rerun the respective action.

  • Re-sync branch
Commits

@kozmaadrian kozmaadrian marked this pull request as ready for review May 12, 2026 20:01
@kozmaadrian kozmaadrian deleted the form-v2 branch May 13, 2026 18:12
@kozmaadrian
Copy link
Copy Markdown
Contributor Author

Closed in favor of #441

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant