<?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>
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
-
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/settingsContainerselectors. 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, neverwindow.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); -
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" — storegetData()output to your DB, hand it back toload()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); } -
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 callsload(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) orwindow.demoBuilders[1](B) in the live demo above; in your own page, keep the references the constructor returned (builderA/builderB).JavaScriptdocument.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); });
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.
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.