<?php
/**
* snippet.php — three Builder instances on one host page (header / body
* / footer) sharing one server-resolved bundle, plus a #save-all button
* that POSTs all three sections together.
*
* Drop this file alongside `dist/`, `themes/default/`, and the W2-shipped
* `backend/` dir, run `php -S localhost:8000`, open
* http://localhost:8000/snippet.php — three independent canvases stack
* vertically; click Save All Sections to round-trip the composed payload.
*
* Triple-surface parity (D13.B.parity):
* The live demo on /examples/basic/5-multi-page/ proves multi-instance
* isolation; this snippet adds the #save-all button described in Step 3
* so the same multi-instance pattern is end-to-end runnable.
*
* Why one bundle, three mounts:
* `ThemeRegistry::resolveBundle()` reads the sample + every referenced
* template ONCE server-side. Three Builders share the same bundle —
* THEME_TEMPLATES / THEME_CONFIG_DATA / MEDIA_URL are identical across
* sections; only the canvas mount selector differs.
*/
declare(strict_types=1);
require_once __DIR__ . '/../../../backend/_lib/ThemeRegistry.php';
$bundle = (new \DemoBuilder\ThemeRegistry(__DIR__ . '/../../../themes'))
->resolveBundle('default', 'master/sample/email/SimpleText');
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My Page Builder — Multi-page</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; padding: 24px; font-family: system-ui, sans-serif; max-width: 1200px; margin: 0 auto; }
.my-toolbar { display: flex; align-items: center; gap: 8px; padding: 12px 0; border-bottom: 1px solid #e5e7eb; margin-bottom: 24px; }
.my-toolbar__btn { padding: 8px 16px; background: #2563eb; color: #fff; border: 0; border-radius: 4px; font: inherit; cursor: pointer; }
.my-toolbar__btn:hover { background: #1d4ed8; }
.my-toolbar__status { margin-left: 8px; font-size: 12px; color: #6b7280; }
.editor { margin-bottom: 32px; }
.editor h2 { margin: 0 0 8px; font: 600 16px system-ui; color: #374151; }
.editor .frame { height: 320px; border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; }
</style>
</head>
<body>
<div class="my-toolbar">
<h1 style="margin: 0; font-size: 18px;">My multi-section page</h1>
<button id="save-all" type="button" class="my-toolbar__btn" style="margin-left: auto;">Save all sections</button>
<span id="save-all-status" class="my-toolbar__status"></span>
</div>
<section class="editor">
<h2>Header</h2>
<div id="header-builder" class="frame"></div>
</section>
<section class="editor">
<h2>Body</h2>
<div id="body-builder" class="frame"></div>
</section>
<section class="editor">
<h2>Footer</h2>
<div id="footer-builder" class="frame"></div>
</section>
<script>
// PHP → JS handoff: four named ingredients for builder.load(), in order.
// Same convention as 1-quickstart / 2-hello-world / 4-dark-mode (see
// BUILDER.md RULE I). One bundle, three Builder instances below.
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>
// Three Builder instances, one shared set of ingredients. Each is
// fully isolated — events / pinSet / recent-colors / active-controls
// are owned per-Builder (see BUILDER.md RULE G). `widgetsContainer:
// null` + `settingsContainer: null` opt-out cleanly via W6.A.A.1
// null-tolerance.
const builders = {};
for (const slug of ['header', 'body', 'footer']) {
const b = new Builder({
mainContainer: '#' + slug + '-builder',
widgetsContainer: null,
settingsContainer: null,
});
b.load(THEME_JSON, THEME_TEMPLATES, THEME_CONFIG_DATA, MEDIA_URL);
builders[slug] = b;
}
// Save-all: walk the 3 instances, compose one payload, POST atomically.
document.getElementById('save-all').addEventListener('click', async () => {
const status = document.getElementById('save-all-status');
status.textContent = 'Saving...';
const sections = Object.fromEntries(
Object.entries(builders).map(([slug, b]) => [
slug,
{ html: b.getHtml(), data: b.getData() },
])
);
try {
// The default /backend/save.php takes a single (dir, sample,
// html, data) tuple — for the multi-section shape, your buyer
// wires their own handler that walks `sections` and writes
// each. Below shows the simplest version: stringify the whole
// payload, write it as one JSON file. Adapt to your storage.
const fd = new FormData();
fd.append('dir', '_b5_demo');
fd.append('sample', 'sample/MultiPage');
fd.append('html', Object.values(sections).map(s => s.html).join('\n'));
fd.append('data', JSON.stringify({ sections }));
const res = await fetch('/backend/save.php', { method: 'POST', body: fd });
const j = await res.json();
status.textContent = j.status === 'success' ? 'Saved ✓ all 3 sections' : ('Error: ' + (j.message || 'unknown'));
} catch (err) {
status.textContent = 'Error: ' + err.message;
}
});
</script>
</body>
</html>
Multi-page
Drop multiple Builder instances on one host page — header, body, footer side-by-side. Each carries its own JSON state; events stay scoped to the instance that emitted them.
Walkthrough
-
Unique mount IDs per instance
Every
new Builder({ mainContainer })needs a CSS-selector-unique mount point. The convention shown below:#header-builder·#body-builder·#footer-builderwith matching widget + settings containers per builder.HTML<section class="editor"> <h2>Header</h2> <div id="header-builder" class="frame"></div> <div id="header-widgets" style="display:none"></div> <div id="header-settings" style="display:none"></div> </section> <section class="editor"> <h2>Body</h2> <div id="body-builder" class="frame"></div> <div id="body-widgets" style="display:none"></div> <div id="body-settings" style="display:none"></div> </section> <section class="editor"> <h2>Footer</h2> <div id="footer-builder" class="frame"></div> <div id="footer-widgets" style="display:none"></div> <div id="footer-settings" style="display:none"></div> </section> -
Three Builder instances, one shared set of ingredients
Resolve the theme bundle ONCE server-side (same
ThemeRegistry::resolveBundle()as Quick Start) and emit the four named globals (THEME_JSON,THEME_TEMPLATES,THEME_CONFIG_DATA,MEDIA_URL). Then mount N Builders, each with its own canvas / widgets / settings IDs but sharing the same templates + config. The instances are isolated — selecting in the header doesn't deselect in the body.JavaScript// Server already emitted THEME_JSON / THEME_TEMPLATES / THEME_CONFIG_DATA / MEDIA_URL // for the SimpleText sample. const builders = {}; for (const slug of ['header', 'body', 'footer']) { const builder = new Builder({ mainContainer: '#' + slug + '-builder', widgetsContainer: '#' + slug + '-widgets', settingsContainer: '#' + slug + '-settings', }); builder.load(THEME_JSON, THEME_TEMPLATES, THEME_CONFIG_DATA, MEDIA_URL); builders[slug] = builder; } // Different sample per instance? Resolve N bundles server-side // (one per sample) and pass distinct globals like // `window.HEADER_THEME_JSON` / `window.BODY_THEME_JSON` / etc. -
Save all three together
Compose a single payload by reading
getHtml()+getData()from every instance.getData()returns the JSON shape (everything needed for round-tripload());getHtml()returns the rendered HTML string. Send as one POST so your backend persists the whole page atomically.JavaScriptdocument.querySelector('#save-all').addEventListener('click', async () => { const sections = Object.fromEntries( Object.entries(builders).map(([slug, b]) => [ slug, { html: b.getHtml(), data: b.getData() }, ]) ); await fetch('/backend/save.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ slug: 'my-page', sections }), }); });
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
Memory cost: each Builder instance carries its own JSON state, history stack, and DOM tree. Three builders is fine; thirty on one page would be wasteful — at that scale, switch to a single builder + a sidebar of "section" tabs.
Cross-instance interactions. The instances don't share state by default. If you need them to (e.g. a colour applied in the header should propagate to the body), wire it through the public events bus: builders.header.events.on('format:change', (data) => builders.body.applyFormat(data)).
Different themes per builder. Each new Builder({...}) can load a different theme JSON. The widgets palette is per-instance; the canvas paints whichever templates the loaded theme exposes.
Save shape. The snippet's POST shape is illustrative — your buyer might prefer a flat array, or one POST per builder. Save to MySQL shows how to denormalise into a relational schema.
The live demo above proves multi-instance isolation — three Builders, three independent canvases, three independent histories. The snippet.php below adds the #save-all button that walks the three instances together; drop it into your own host page to see the full pattern (header / body / footer) in production shape.