API · 5 of 5

clear() + load() + getData round-trip — wipe, reset, insert

Three structural API operations. `builder.clear()` empties the page (undoable in one step). `builder.load(SEED, ...)` replaces the tree with a known-good snapshot. The `getData() → mutate → load()` round-trip is the canonical pattern for programmatic insertion / reorder / splice / AI-replace.

Walkthrough

  1. Wipe the canvas — builder.clear() is history-aware

    builder.clear() mass-tears down every block on the page. Internally it routes through pageElement.removeAllBlocks() (NOT per-block BaseElement.remove()) which avoids the N-per-block history-leak + reflow storm. Then it commits ONE history.page_clear summary entry so the user can Cmd+Z to restore everything in one step (without that summary, the wipe was effectively irreversible after engine F1 silenced the per-block leak).

    Use clear() when you want a destructive wipe the user can undo. Use load(SEED, ...) (Step 2) when you want a clean reset that becomes the new baseline. The two share the "page is now empty / page is now reset" outcome; they differ on whether undo can recover the prior state.

    JavaScript
    document.getElementById('c10WipeBtn').addEventListener('click', () => {
      builder.clear();
    });
    
    // History entry emitted: 'history.page_clear' summary.
    // User can press Cmd+Z (or call builder.undo()) to restore everything
    // in ONE undo step.
  2. Reset to a template — builder.load(SEED, ...) replaces wholesale

    builder.load(SEED_JSON, THEME_TEMPLATES, THEME_CONFIG_DATA, MEDIA_URL) swaps the page tree for the JSON you hand it. Internally it walks the same path as the initial load() on page boot: pageElement.parse(data)removeAllBlocks() → re-parse new blocks → render()_adopt() walks the new tree → host-injection assigns this.host on every element + overlay. Idempotent setup (iframe auto-resize, focus-dim, inspect-mode, text-tracker) re-runs in-place; no double-binding.

    After load(SEED, ...), the SEED becomes the new baseline. The history stack does NOT carry an undo path back to whatever was on the canvas before the reset. If you need that, call clear() instead — or wrap your reset in an explicit history.silent + history.commit pair to author a custom undoable summary entry.

    JavaScript
    // SEED_JSON is the page snapshot you want to revert to. Capture it
    // at boot (before any user edits) and stash on window for later use.
    window.SEED_JSON = THEME_JSON;  // pristine bundle from the server
    
    document.getElementById('c10ResetBtn').addEventListener('click', () => {
      builder.load(SEED_JSON, THEME_TEMPLATES, THEME_CONFIG_DATA, MEDIA_URL);
      // After load() returns, builder.pageElement.getChildren() reflects
      // the SEED tree byte-identically.
    });
  3. Append a block via getData() → mutate → load() round-trip

    The engine doesn't expose a stable public appendBlock() on the builder API today. The buyer-canonical pattern is the round-trip: read the JSON via builder.getData(), mutate the blocks array (push / unshift / splice), hand it back via builder.load(modifiedData, ...). Same path as Reset, with a one-line transform between read and write.

    This pattern is GENERAL — works for insert, reorder, splice, swap, AI-replace, undo-edit-redo. Any structural page mutation reduces to "JSON in, JSON out, load() between." The reactive block-count pill in the header (Step bonus) listens for DOCUMENT_CHANGED and reads pageElement.getChildren().length after every mutation, so the buyer sees the count climb / drop in real time.

    The HERO_BLOCK JSON below is a minimal valid block — template: 'Block' + a single HeadingElement. Real apps would shape it from a template library, an AI generator, or a paste-from-clipboard handler. ElementFactory.createElement() (engine-internal, called by parse()) handles every block / element type the engine ships, including custom ones registered via the Phase C extensions tier.

    JavaScript
    // Engine JSON shape: `name` is the ElementFactory registry key
    // (e.g. 'BlockElement', 'HeadingElement'); `template` is the theme
    // template file ('Block.template.html', 'Heading.template.html').
    // HeadingElement carries `text` (raw markup allowed) + `type` (h1-h6).
    const HERO_BLOCK = {
      name:     'BlockElement',
      template: 'Block',
      formats:  { background_color: '#1b0c2b', padding_top: 28, padding_bottom: 28 },
      elements: [
        {
          name:     'HeadingElement',
          template: 'Heading',
          type:     'h1',
          text:     'Programmatically inserted ✓',
          formats:  { font_size: 32, text_color: '#ffffff', text_align: 'center' },
        },
      ],
    };
    
    document.getElementById('c10InsertBtn').addEventListener('click', () => {
      // 1) Read current state.
      const data = builder.getData();
      // 2) Mutate. Append at the top of the page (`unshift`) for visibility.
      data.blocks.unshift(HERO_BLOCK);
      // 3) Hand it back. Engine wipes + re-parses + re-renders.
      builder.load(data, THEME_TEMPLATES, THEME_CONFIG_DATA, MEDIA_URL);
    });

Live demo

demo-mini-builder--rich-3col-header
clear / load / round-trip insertBlocks: —

The whole snippet

Click Copy on any file to grab it byte-identical, or Expand to read inline (capped at ~520 px so the page stays scannable). Multi-file snippets surface a side-by-side grid — copy only the files you need.

<?php
/**
 * snippet.php — `builder.clear()` + `builder.load(SEED, ...)` +
 * `getData() → mutate → load()` round-trip insert.
 *
 * Drop alongside `dist/` + `themes/default/`, run a PHP server.
 * Three buttons in the header drive the three structural API
 * operations + a reactive block-count pill driven by the
 * `DOCUMENT_CHANGED` event.
 *
 * IDs `c10WipeBtn` / `c10ResetBtn` / `c10InsertBtn` / `c10CountPill`
 * match the live demo on /examples/api/5-clear-insert-elements/ AND
 * Steps 1-3 of the walkthrough (BUILDER.md RULE H).
 */
declare(strict_types=1);

require_once __DIR__ . '/../../../backend/_lib/ThemeRegistry.php';

$bundle = (new \DemoBuilder\ThemeRegistry(__DIR__ . '/../../../themes'))
    ->resolveBundle('default', 'master/sample/email/AboutOurServices');
?>
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>clear + load + round-trip insert — structural API operations</title>
    <link rel="stylesheet" href="/dist/builder.css">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&display=block">
    <style>
        body { margin: 0; font-family: system-ui, sans-serif; display: grid; grid-template-rows: 56px 1fr; height: 100vh; }
        .my-header { display: flex; align-items: center; gap: 8px; padding: 0 16px; background: #f8f9fa; border-bottom: 1px solid #e5e7eb; }
        .my-header__title { font-weight: 600; font-size: 14px; margin-right: auto; }
        .my-btn { font: inherit; font-size: 12px; padding: 6px 12px; background: #fff; border: 1px solid #d1d5db; border-radius: 6px; cursor: pointer; }
        .my-btn:hover { background: #f3f4f6; }
        .my-btn:focus-visible { outline: 2px solid #4338ca; outline-offset: 2px; }
        #c10WipeBtn { color: #b45309; border-color: #b45309; }
        #c10WipeBtn:hover { background: #fef3c7; }
        .my-divider { width: 1px; height: 16px; background: #e5e7eb; margin: 0 4px; }
        #c10CountPill { font-size: 11px; padding: 3px 10px; border-radius: 999px; background: #e0e7ff; color: #4338ca; font-weight: 600; min-width: 92px; text-align: center; font-variant-numeric: tabular-nums; }

        .my-builder-host { display: grid; grid-template-columns: 200px 1fr 240px; min-height: 0; }
        .my-host-slot { background: #f8f9fa; padding: 8px; overflow: auto; font-size: 12px; }
        .my-canvas { overflow: auto; background: #fff; }
    </style>
</head>
<body>

<header class="my-header">
    <span class="my-header__title">clear / load / round-trip insert</span>
    <button type="button" id="c10WipeBtn"   class="my-btn">Wipe canvas</button>
    <button type="button" id="c10ResetBtn"  class="my-btn">Reset to template</button>
    <button type="button" id="c10InsertBtn" class="my-btn">Append hero block</button>
    <span class="my-divider" aria-hidden="true"></span>
    <span id="c10CountPill" aria-live="polite">Blocks: —</span>
</header>

<div class="my-builder-host">
    <div id="MyWidgets"  class="my-host-slot"></div>
    <div id="MyCanvas"   class="my-canvas"></div>
    <div id="MySettings" class="my-host-slot"></div>
</div>

<script>
    window.THEME_JSON        = <?= json_encode($bundle->themeJson,      JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>;
    window.THEME_TEMPLATES   = <?= json_encode($bundle->themeTemplates, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>;
    window.THEME_CONFIG_DATA = <?= json_encode($bundle->configData,     JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>;
    window.MEDIA_URL         = <?= json_encode($bundle->mediaUrl,       JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?>;
</script>

<script src="/dist/builder.js"></script>

<script>
    const builder = new Builder({
        mainContainer:     '#MyCanvas',
        widgetsContainer:  '#MyWidgets',
        settingsContainer: '#MySettings',
    });

    // Capture pristine bundle BEFORE any user edits — this is the
    // "Reset to template" snapshot. In your own app, fetch from the
    // server again on Reset if memory tampering is a concern.
    const SEED_JSON = THEME_JSON;

    builder.load(SEED_JSON, THEME_TEMPLATES, THEME_CONFIG_DATA, MEDIA_URL);

    // Reactive count pill — listener owns repaint, single source of
    // truth (D13.C.8 pattern). DOCUMENT_CHANGED fires on every
    // structural mutation: clear, load, round-trip insert.
    const pill = document.getElementById('c10CountPill');
    function renderCount() {
        const n = builder.pageElement
            ? builder.pageElement.getChildren().length
            : 0;
        pill.textContent = `Blocks: ${n}`;
    }
    builder.events.on(Builder.EVENTS.DOCUMENT_CHANGED, renderCount);
    setTimeout(renderCount, 80);  // boot render — DOCUMENT_CHANGED skips first paint

    // 1) Wipe canvas — history-aware, undoable in one step.
    document.getElementById('c10WipeBtn').addEventListener('click', () => {
        builder.clear();
        renderCount();
    });

    // 2) Reset to template — replaces tree with seed snapshot.
    //    NOT history-aware; new state is the new baseline.
    document.getElementById('c10ResetBtn').addEventListener('click', () => {
        builder.load(SEED_JSON, THEME_TEMPLATES, THEME_CONFIG_DATA, MEDIA_URL);
        setTimeout(renderCount, 80);
    });

    // 3) Append hero block — getData → mutate → load round-trip.
    //    Same pattern works for splice / reorder / AI-replace / restore.
    // Engine JSON shape: `name` is the ElementFactory registry key
    // (BlockElement / HeadingElement / etc.); `template` maps to the
    // theme's <Template>.template.html file. HeadingElement carries
    // `text` (raw markup allowed) and `type` (h1-h6). See
    // docs/core/STRUCTURE.md + docs/core/ELEMENT_DEFINITION.md.
    const HERO_BLOCK = {
        name:     'BlockElement',
        template: 'Block',
        formats:  { background_color: '#1b0c2b', padding_top: 28, padding_bottom: 28 },
        elements: [
            {
                name:     'HeadingElement',
                template: 'Heading',
                type:     'h1',
                text:     'Programmatically inserted ✓',
                formats:  { font_size: 32, text_color: '#ffffff', text_align: 'center' },
            },
        ],
    };
    document.getElementById('c10InsertBtn').addEventListener('click', () => {
        const data = builder.getData();
        data.blocks = data.blocks || [];
        data.blocks.unshift(HERO_BLOCK);
        builder.load(data, THEME_TEMPLATES, THEME_CONFIG_DATA, MEDIA_URL);
        setTimeout(renderCount, 80);
    });
</script>

</body>
</html>

Notes

Why no public appendElement()? The engine has internal pageElement.appendBlock(block) and BlockElement.append(element), but they require the buyer to construct live Element instances via ElementFactory.createElement(json), manage container back-references, fire host-injection adopt walks, and call render() to paint. The getData() → mutate → load() round-trip handles all of that internally — one public API, every structural mutation. builder.insertElements() exists on the builder surface but its backing PageElement.appendElements isn't implemented today; treat it as undocumented + use the round-trip instead.

History semantics — one entry, not N. builder.clear() wraps the teardown in history.silent() + commits ONE history.page_clear summary. The user gets a single undo step that restores all blocks. Without the silent wrap, every removeAllBlocks() internal step would emit a per-block structure_remove commit, spamming the history dropdown with N "Delete Block" rows on every clear / template-swap / AI-replace. load() does NOT participate in history. After a Reset, the history stack starts fresh from the seed — no undo path back to pre-reset state. If you need that, wrap the reset in your own history.silent + history.commit('your.summary') pair.

SEED_JSON capture timing. Stash the seed JSON BEFORE any user edit. The example above uses cfg.bundle.themeJson — that's the pristine bundle the live-demo harness hands the example. In your own app, capture THEME_JSON (or fetch it again from your server) at the moment the page mounts. Re-fetching from the server on Reset is also fine if the seed is small — slightly slower (one network round-trip) but resistant to in-memory tampering.

HERO_BLOCK shape. Minimal valid block: template: 'Block' + an elements: [] array of valid element JSON. ElementFactory (engine-internal, called by pageElement.parse()) recognises every element template: the engine ships ('Heading', 'Paragraph', 'Image', 'Button', 'Grid', etc.) plus any custom elements registered via the Phase C extensions tier. Validate your shape against docs/core/STRUCTURE.md before shipping production-grade insertion code — the engine is forgiving (skips unknown templates) but silently dropping a block due to a typo is a debugging trap.

DOCUMENT_CHANGED fires on every mutation. The reactive block-count pill in the header subscribes to Builder.EVENTS.DOCUMENT_CHANGED. Engine emits on user edits AND on programmatic clear() / load() / round-trip insert. Single-source-of-truth: the pill never reads from the click handler — the listener walks pageElement.getChildren() on every fire. Same pattern locked in C.8 (mode-toggle-pair) and C.7 (status-pill flash).

Round-trip pattern is general. "Read JSON → mutate → load" handles insert (push/unshift/splice), reorder (swap indices), splice-replace (mutate one block in place), AI-rewrite (replace data.blocks wholesale with AI output), and template-restore (reuse the seed). Mastering this one pattern + docs/core/STRUCTURE.md JSON shape locks every structural mutation a buyer would ever build — no hidden API surface, no per-mutation handler.