API · 2 of 5

getData() + load() — copy state between two Builders

Two side-by-side Builders A and B share theme + templates but render independent JSON state. One button calls A.getData() → B.load(...) and the page tree teleports. Symmetrical contract: whatever getData returns, load consumes.

Walkthrough

  1. Mount two Builder instances on the same page

    Two Builders coexist as long as their containers don't overlap. Pass each instance its own mainContainer / widgetsContainer / settingsContainer selectors. Both instances read from the same set of four globals (THEME_JSON, THEME_TEMPLATES, THEME_CONFIG_DATA, MEDIA_URL) — the bundle is resolved ONCE on the server and consumed N times in the browser.

    Multi-instance is first-class as of the host-injection refactor (BUILDER.md RULE G + D13.A.A.5). Every internal Element / Overlay / Control reads this.host, never window.builder. There's no "main" instance — the page can carry as many side-by-side Builders as the layout has room for.

    JavaScript
    // Both instances share the four named globals — the bundle is
    // resolved server-side once, JS references the same arrays.
    const builderA = new Builder({
      mainContainer:     '#CanvasA',
      widgetsContainer:  '#WidgetsA',
      settingsContainer: '#SettingsA',
    });
    builderA.load(THEME_JSON, THEME_TEMPLATES, THEME_CONFIG_DATA, MEDIA_URL);
    
    const builderB = new Builder({
      mainContainer:     '#CanvasB',
      widgetsContainer:  '#WidgetsB',
      settingsContainer: '#SettingsB',
    });
    builderB.load(THEME_JSON, THEME_TEMPLATES, THEME_CONFIG_DATA, MEDIA_URL);
  2. getData() returns what load() consumes — byte-identical contract

    builder.getData() walks the live page tree and serialises every element + format + custom attribute into a JSON object. builder.load(json, templates, configData, mediaUrl) reverses the operation — parses the JSON, hydrates element classes, paints the canvas. Same shape, both directions.

    This means the cross-instance copy is one line: B.load(A.getData(), THEME_TEMPLATES, THEME_CONFIG_DATA, MEDIA_URL). Templates, config, and media URL stay constant (both instances share the same theme); only the JSON page tree differs. Same shape works for "save current canvas → reload after refresh" — store getData() output to your DB, hand it back to load() on next mount.

    JavaScript
    // Cross-instance copy — one line.
    function copyAtoB() {
      const aData = builderA.getData();
      builderB.load(aData, THEME_TEMPLATES, THEME_CONFIG_DATA, MEDIA_URL);
    }
    
    // Save → reload symmetry.
    async function saveAndReload() {
      // Save: capture state.
      const data = builderA.getData();
      await fetch('/api/save', { method: 'POST', body: JSON.stringify(data) });
    
      // Reload: hand it back.
      const fresh = await fetch('/api/load').then(r => r.json());
      builderA.load(fresh, THEME_TEMPLATES, THEME_CONFIG_DATA, MEDIA_URL);
    }
  3. Wire three buttons: A→B, B→A, Reset

    The live demo above carries three buttons in the host toolbar above the two side-by-side Builders. Copy A → B calls A.getData()B.load(...). Copy B → A reverses. Reset both calls load(THEME_JSON, ...) on each — handing them back the seed JSON the page started with so they diverge again from a known baseline.

    The two Builders are isolated by default — selection, history, and event subscriptions are per-instance. Resolve a specific instance via window.demoBuilders[0] (A) or window.demoBuilders[1] (B) in the live demo above; in your own page, keep the references the constructor returned (builderA / builderB).

    JavaScript
    document.getElementById('c7CopyAtoBBtn').addEventListener('click', () => {
      builderB.load(builderA.getData(), THEME_TEMPLATES, THEME_CONFIG_DATA, MEDIA_URL);
    });
    
    document.getElementById('c7CopyBtoABtn').addEventListener('click', () => {
      builderA.load(builderB.getData(), THEME_TEMPLATES, THEME_CONFIG_DATA, MEDIA_URL);
    });
    
    document.getElementById('c7ResetBtn').addEventListener('click', () => {
      // Hand both instances back the seed JSON they originally loaded.
      builderA.load(THEME_JSON, THEME_TEMPLATES, THEME_CONFIG_DATA, MEDIA_URL);
      builderB.load(THEME_JSON, THEME_TEMPLATES, THEME_CONFIG_DATA, MEDIA_URL);
    });

Live demo

demo-mini-builder--compact-3col

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 — `getData()` + `load()` round-trip across two
 * side-by-side Builder instances.
 *
 * Drop alongside `dist/` + `themes/default/`, run a PHP server.
 * Edit either canvas → click "Copy A → B" or "Copy B → A" → the
 * page tree teleports between instances. Click "Reset both" to
 * snap both back to the seed sample.
 *
 * IDs `c7CopyAtoBBtn` / `c7CopyBtoABtn` / `c7ResetBtn` /
 * `c7Status` match the live demo on /examples/api/2-get-data-load/
 * 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/Minimal');
?>
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>getData + load — copy state between two Builders</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 { padding: 6px 12px; background: #fff; border: 1px solid #d1d5db; border-radius: 6px; cursor: pointer; font: inherit; font-size: 12px; }
        .my-btn:hover { background: #f3f4f6; }
        #c7Status { min-width: 110px; font-size: 12px; color: #6b7280; padding-left: 8px; }
        #c7Status[data-state="ok"] { color: #047857; }

        .my-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; padding: 16px; min-height: 0; }
        .my-pane { display: flex; flex-direction: column; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; }
        .my-pane__label { padding: 8px 12px; background: #f8f9fa; border-bottom: 1px solid #e5e7eb; font-size: 11px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: #4b5563; }
        .my-builder-host { display: grid; grid-template-columns: 160px 1fr 200px; flex: 1; 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">getData / load — A | B side-by-side</span>
    <button type="button" id="c7CopyAtoBBtn" class="my-btn">Copy A → B</button>
    <button type="button" id="c7CopyBtoABtn" class="my-btn">Copy B → A</button>
    <button type="button" id="c7ResetBtn"    class="my-btn">Reset both</button>
    <span id="c7Status" aria-live="polite"></span>
</header>

<div class="my-pair">
    <section class="my-pane" aria-labelledby="pane-a-label">
        <span id="pane-a-label" class="my-pane__label">Builder A</span>
        <div class="my-builder-host">
            <div id="WidgetsA"  class="my-host-slot"></div>
            <div id="CanvasA"   class="my-canvas"></div>
            <div id="SettingsA" class="my-host-slot"></div>
        </div>
    </section>
    <section class="my-pane" aria-labelledby="pane-b-label">
        <span id="pane-b-label" class="my-pane__label">Builder B</span>
        <div class="my-builder-host">
            <div id="WidgetsB"  class="my-host-slot"></div>
            <div id="CanvasB"   class="my-canvas"></div>
            <div id="SettingsB" class="my-host-slot"></div>
        </div>
    </section>
</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>
    // Two instances on the same page — multi-instance is canonical
    // post-host-injection-refactor (BUILDER.md RULE G + D13.A.A.5).
    const builderA = new Builder({
        mainContainer:     '#CanvasA',
        widgetsContainer:  '#WidgetsA',
        settingsContainer: '#SettingsA',
    });
    const builderB = new Builder({
        mainContainer:     '#CanvasB',
        widgetsContainer:  '#WidgetsB',
        settingsContainer: '#SettingsB',
    });

    builderA.load(THEME_JSON, THEME_TEMPLATES, THEME_CONFIG_DATA, MEDIA_URL);
    builderB.load(THEME_JSON, THEME_TEMPLATES, THEME_CONFIG_DATA, MEDIA_URL);

    const status = document.getElementById('c7Status');
    let statusFadeTimer = null;
    function flashStatus(msg) {
        status.textContent = msg + ' ✓';
        status.dataset.state = 'ok';
        if (statusFadeTimer) clearTimeout(statusFadeTimer);
        statusFadeTimer = setTimeout(() => {
            status.textContent = '';
            delete status.dataset.state;
        }, 2000);
    }

    document.getElementById('c7CopyAtoBBtn').addEventListener('click', () => {
        // One line — A.getData() returns what B.load() consumes.
        builderB.load(builderA.getData(), THEME_TEMPLATES, THEME_CONFIG_DATA, MEDIA_URL);
        flashStatus('Copied A → B');
    });

    document.getElementById('c7CopyBtoABtn').addEventListener('click', () => {
        builderA.load(builderB.getData(), THEME_TEMPLATES, THEME_CONFIG_DATA, MEDIA_URL);
        flashStatus('Copied B → A');
    });

    document.getElementById('c7ResetBtn').addEventListener('click', () => {
        // Hand both instances back the seed JSON they originally loaded.
        builderA.load(THEME_JSON, THEME_TEMPLATES, THEME_CONFIG_DATA, MEDIA_URL);
        builderB.load(THEME_JSON, THEME_TEMPLATES, THEME_CONFIG_DATA, MEDIA_URL);
        flashStatus('Reset both');
    });
</script>

</body>
</html>

Notes

Why two instances and not one? Cross-instance copy is the cleanest demonstration of the symmetrical contract: A's getData() output is byte-identical to B's load() input. With one instance you'd have to serialise to a string and re-load — same proof, more ceremony. Two side-by-side Builders make the "page tree teleports" experience visceral.

Multi-instance is canonical. Pre-host-injection-refactor (D13.A.A.5), Builder relied on a shared window.builder global — multi-instance was a workaround. Post-refactor, every internal class reads this.host; var b1 = new Builder({...}); var b2 = new Builder({...}); works first-class. See basic/5-multi-page for the 3-instance variant. BUILDER.md RULE G locks the contract.

Templates + config + media URL stay constant. The 4-arg load() signature is load(themeJson, themeTemplates, themeConfigData, mediaUrl). Across the cross-instance copy, only the first arg changes — both instances share theme. If you ever wanted to TRANSPLANT a page from theme X to theme Y, you'd swap all four args and call load() with the new bundle's globals. The save round-trip stays single-theme.

What about diverging asset URLs? If A and B were loaded with different MEDIA_URLs (e.g. A from /templates/abc/, B from /templates/xyz/), the absolute media URLs in A's getData() output would carry A's prefix. B.load(A_data, ...) with B's MEDIA_URL would NOT rewrite those absolute URLs — they'd stay pointing at A's assets. For cross-tenant copy where assets must rewrite, use A.getHtmlWithRelativeLinks() + a parse-and-rehydrate pass, or strip absolute URLs before save (see api/1 getHtml export).

Performance note. getData() walks the page tree once (typical: < 5ms for a full email template). load() rebuilds the canvas from JSON (typical: 30-150ms depending on theme template count). For a real-time "live mirror" between two Builders, debounce the copy on document:changed with a 200-500ms window — see events/1-document-changed.