Overview

Preview is not a separate "mode" in the way drag-drop and code modes are; it is a spectrum of fidelity layered onto the editor. The canvas itself is the highest-fidelity in-editor surface — every mmm-* web component renders with its real SDK bindings via the imperative WebComponentElement wrapper, so what you see during authoring already runs production code. The Code-mode iframe layers on a sandboxed view that mirrors what the backend SSR pipeline produces. The Preview tab opens the same SSR output without any editor chrome, on the live backend URL.

Which surface you reach depends on which toolbar control you click. The Preview button in the right cluster of the top toolbar opens the external preview tab. Switching the floating bottom toolbar's editor mode from Design to Code swaps the canvas for the in-app preview iframe. And resizing the canvas via the device pill group reuses the same renderer with a different canvasWidth applied.

How it works

The editor mode toggle lives in useCommmerceEditorSetupStore's editorMode field ('drag-drop' | 'code') with the matching toggleEditorMode(mode) action. canvas/index.tsx reads that value and switches the rendered surface — <DragDropEditor /> when drag-drop, <DevEditor /> when code. The bottom toolbar's Code button in control/index.tsx is what flips the value.

DevEditor mounts a Monaco-driven three-pane code editor next to an <iframe>. The iframe's srcDoc is rebuilt from the current JSON via jsonToHtmlCss(...), so it always reflects the latest edit, and the editor injects the store's STORE_THEME_SETTINGS plus the backend SDK script so mmm-* components hydrate inside the preview just as they do on the live site. An additional getInspectScript() blob is written into the same srcDoc — it sits dormant until the parent posts { type: 'INSPECT_MODE', enabled: true } via postMessage, then renders hover overlays and reports clicks back as { type: 'INSPECT_CLICK', nodeId } for selection sync.

The external Preview tab is fired by handlePreview in control/index.tsx. In Dev Portal mode it composes ${api_base_url}/preview/${appSlug}/v/${app_version_code}/${block_id}/${block_version_code} so the backend's devportal.routes.ts renders the exact block + version the editor is editing; if any of those params are missing it falls back to the public ${api_base_url}/preview/${appSlug}. In Store Editor mode the button opens the live storefront URL directly. appSlug and appName are stored in useCommmerceEditorSetupStore so the preview button can be wired up regardless of which app/page the user is editing.

Inside the canvas, in-editor preview is handled per breakpoint. The device pill group calls toggleDeviceMode(id) on the setup store, which resizes the canvas to the breakpoint's canvasWidth (from useBreakpointStore) and toggles applyStylesToResponsive on for every non-xl breakpoint so edits land in the responsive slice. The Base / Resp switch on the bottom toolbar lets you override that default — useful when you want to preview a smaller device while tweaking the base style.

Data binding inside the code-mode iframe is realistic. DevEditor scans the preview tree for <mmm-data-source> nodes (and grid containers whose layoutConfig.gridDataConfig.mode === 'data-source'), fetches each one's API in parallel against the dev store (https://<storeCode>.commmerce.com/store/api/<path>), and injects the results as window.PRODUCT_RESPONSE_<KEY> globals inside the iframe — the same shape the backend SSR layer produces. Loops, filters, and pagination inside the preview therefore see real product shapes.

Key features

  • Always-on canvas WYSIWYG — drag-drop mode mounts production web components inline with real SDK bindings. USE CASE: editing the product gallery already shows the actual cart-button behavior, no separate preview needed.
  • Code-mode iframe re-renders on every keystroke — every change to HTML / CSS / JS panes recomputes srcDoc and reloads the iframe. USE CASE: tweaking a CSS color and seeing the updated SDK-driven component restyle in < 1 s.
  • Inspect-to-select bridge — toggle the inspect button, hover the iframe to see a blue overlay + tag/class label, click to send { type: 'INSPECT_CLICK', nodeId } back to the parent. The parent reveals the matching line in Monaco and posts INSPECT_SELECT back to keep a persistent green selection overlay on screen. USE CASE: jumping from a misaligned button in the preview directly to its HTML line in the code editor.
  • External preview URL with version context — in Dev Portal the URL embeds app_version_code, block_id, and block_version_code so the backend resolver renders the exact rows the editor is editing, not the last published bundle. USE CASE: shareable QA link that always shows the in-progress draft.
  • Public-app fallback URL — when context is missing, handlePreview falls back to /preview/<app_slug> which renders whatever is currently published. USE CASE: clicking Preview from a context where the version triple isn't known still gives a useful page.
  • Device-aware canvas sizing — the canvas reuses the same renderer at the breakpoint's canvasWidth. Picking a non-xl breakpoint auto-flips applyStylesToResponsive = true so the next edit lands in the responsive style slice. USE CASE: switching to tablet to add mobile-specific spacing without manually opening a switch.
  • Base / Resp override — the bottom-toolbar switch lets the user pin writes to the base style even while previewing a smaller breakpoint. USE CASE: previewing how the base style cascades to mobile without accidentally overriding it.
  • Custom breakpoints first-class — user-defined breakpoints share the same renderer and write into customResponsiveStyles[id]. Beyond four breakpoints the device pill collapses into a 3-dot overflow menu. USE CASE: a store with a 1440 desktop breakpoint and a 380 mini-phone breakpoint can preview both without modifying the renderer.
  • Real product data in the iframe — the code-mode preview fetches every <mmm-data-source> in parallel and injects window.PRODUCT_RESPONSE_* globals; payload kebab-attributes become camelCase keys. USE CASE: previewing a Featured Products grid with the real "featured" sort against the dev store.
  • Console pane with intercepted log/error/warn/info — the bottom panel runs jsCode through a custom console proxy so authors can debug their inline scripts without leaving the editor. USE CASE: validating a one-off tracking snippet outputs the right event payload.
  • Empty-token marker — the iframe's CSS marks {{scope.path}} tokens that resolved to empty with a dashed underline and color-coded reason (wrong-page / requires-auth / loop-only / no-data). USE CASE: spotting that a token won't render on the customer page before publishing.
  • Read-only editors still preview — opening a published version sets isReadOnly = true; the Preview button, device switcher, and zoom keep working. USE CASE: comparing a known-good production version against the in-progress draft.

Preview surfaces

Three surfaces, three fidelities. The canvas is highest-WYSIWYG and writable; the code-mode iframe is sandboxed but lets you drive the page from raw HTML/CSS/JS; the external preview tab is the closest to production but read-only.

SurfaceHow to openRenders viaEditing / use case
Canvas (Design)defaultDragDropEditor + WebComponentElementFull drag-drop authoring; mmm-* components run with live SDK. USE CASE: routine page editing.
Code-mode iframebottom toolbar → CodeDevEditor iframe with jsonToHtmlCss srcDocHTML/CSS/JS editing in Monaco; iframe re-renders on every change. USE CASE: precision tweaks the WYSIWYG can't express (custom selectors, inline scripts).
External previewtop toolbar → PreviewBackend SSR via /preview/<app_slug>Read-only; opens in a new browser tab. USE CASE: sharing a QA link with stakeholders.
BreakpointPreviewSettings → BreakpointsMobile / Tablet / Desktop frame mocksRead-only diagram showing canvas width & contained margin. USE CASE: validating a custom breakpoint's geometry before saving.

Device preview widths

Each breakpoint owns a canvasWidth that the device toolbar applies as the visible canvas width. The defaults below match DEFAULT_BREAKPOINT_SETTINGS in types/breakpoint.types.ts.

ModeCanvas widthIconEffect on writes / use case
xl1920pxTvMinimalWrites land in the base style by default (applyStylesToResponsive = false). USE CASE: authoring on a large monitor with full-width layouts.
desktop1280pxMonitorWrites land in responsiveStyles.desktop. USE CASE: tightening padding for medium screens without affecting the xl base.
tablet768pxTabletWrites land in responsiveStyles.tablet. USE CASE: stacking a 3-column grid into 2 columns for iPads.
mobile430pxSmartphoneWrites land in responsiveStyles.mobile. USE CASE: enlarging tap targets and adjusting hero font size for phones.
custom_Nuser-defineduser choiceWrites land in customResponsiveStyles[custom_N]. USE CASE: a 1440 "laptop" breakpoint with its own typography scale.

postMessage protocol

Inspect mode is a bidirectional postMessage channel between common/components/commmerce-editor/dev/index.tsx (parent) and common/components/commmerce-editor/dev/inspectScript.ts (iframe). All payloads share { type, ...data }.

Event typePayloadDirectionPurpose / use case
INSPECT_MODE{ enabled: boolean }Parent → iframeTurn hover-to-inspect on or off. Hides the hover overlay when off; hides the selection overlay when on so hover can take over. USE CASE: toggling the inspect button.
INSPECT_SELECT{ nodeId: string | null }Parent → iframeKeep a persistent green selection overlay on the named element. nodeId=null hides it. USE CASE: post-click, the parent re-asserts the selection so it survives inspect-mode toggling.
INSPECT_HOVER{ nodeId: string | null }Iframe → parentThe cursor is over an element with data-node-id. Parent uses it to peek-scroll the matching Monaco line. USE CASE: live highlight as the user pans across the preview.
INSPECT_CLICK{ nodeId: string }Iframe → parentThe user clicked an element. Parent reveals the corresponding line, exits inspect mode, then re-asserts the selection via INSPECT_SELECT. USE CASE: jumping from a button on the preview to its HTML in < 200 ms.

Workflows

1. Preview the page exactly as it ships

  1. Click Preview in the right cluster of the top toolbar (drag-drop mode) or the Preview pill in code mode.
  2. In Dev Portal, the backend resolves the URL to /preview/<app_slug>/v/<app_version_code>/<block_id>/<block_version_code> — rendering the exact block + version you are editing — and opens it in a new tab.
  3. If the URL is missing the version params, the fallback /preview/<app_slug> opens instead, rendering the published app at its public route.
  4. Make changes in the editor, hit Save, then refresh the preview tab to see the changes applied.

2. Resize the canvas to preview a different breakpoint

  1. Locate the device pill group in the top toolbar. Up to four breakpoints render inline; extras hide behind the three-dot overflow.
  2. Click the icon for the breakpoint you want to preview. The canvas resizes to that breakpoint's canvasWidth and the property panels begin reading from its style slice.
  3. If you only want to look at the smaller layout without changing the target of subsequent edits, toggle Base on the bottom toolbar's Base / Resp switch — that pins writes to the base style regardless of the active device.

3. Inspect a rendered element in code-mode preview

  1. Switch to Code mode using the Code button on the floating bottom toolbar (toggleEditorMode('code')).
  2. Click the inspect / pointer toggle in the DevEditor controls. The parent posts { type: 'INSPECT_MODE', enabled: true } to the iframe and the cursor becomes a crosshair.
  3. Hover any element in the preview iframe — a blue rectangle + tag label follows the cursor. Click to send { type: 'INSPECT_CLICK', nodeId } back to the parent, which reveals the matching line in Monaco and re-asserts the selection via INSPECT_SELECT.
  4. Switch back to Design mode and the selection persists, ready for property-panel edits.

4. Preview live changes inside the code iframe

  1. In Code mode, edit HTML, CSS, or JS in the Monaco panels.
  2. DevEditor re-runs the parser and rebuilds the iframe's srcDoc — the iframe reloads automatically, the SDK re-initialises and all mmm-* custom elements hydrate.
  3. Use the Console tab to inspect runtime logs and errors (intercepted via custom log/error/warn/info hooks).

5. Validate breakpoint layouts in the Settings preview

  1. Open Settings → Breakpoints from the device toolbar's (+) button.
  2. Below the breakpoint editor, the Live Preview picker lets you select any breakpoint and shows a mocked device frame (mobile / tablet / desktop) at the breakpoint's canvasWidth, with the contained margin drawn inside.
  3. Use this to sanity-check that a custom breakpoint's canvas width and margin make sense before saving.

6. Debug a data-source fetch in code-mode preview

  1. Place a <mmm-data-source api="product.getProductList" response-key="featured"> in the page.
  2. Switch to Code mode. DevEditor scans the tree, builds the payload from payload-* attributes, and POSTs to the dev store.
  3. The browser console logs the request and response: [editor preview] ▶ POST .../productList and [editor preview] ◀ 200 ....
  4. Iframe receives window.PRODUCT_RESPONSE_FEATURED via injected script tags — your loop / filter / pagination components hydrate with real shapes.

Examples

External preview URL (Dev Portal)

/*   With editor context — opens the block at a specific version   */
${api_base_url}/preview/${appSlug}/v/${app_version_code}/${block_id}/${block_version_code}

/*   Fallback when block / version context is missing — opens the public app   */
${api_base_url}/preview/${appSlug}

/*   Example resolved URL: */
https://api.commmerce.com/preview/acme-theme/v/3/108/2

Inspect-mode postMessage protocol

/*   Parent → iframe: enable / disable hover-to-inspect   */
iframe.contentWindow.postMessage({ type: 'INSPECT_MODE', enabled: true }, '*');

/*   Parent → iframe: keep an element highlighted after the user clicked it   */
iframe.contentWindow.postMessage({ type: 'INSPECT_SELECT', nodeId: 'el_42' }, '*');

/*   Iframe → parent: cursor moved over an element with data-node-id   */
window.parent.postMessage({ type: 'INSPECT_HOVER', nodeId: 'el_17' }, '*');

/*   Iframe → parent: user clicked an element, here's its data-node-id   */
window.parent.postMessage({ type: 'INSPECT_CLICK', nodeId: 'el_17' }, '*');

Switching editor mode and device mode in code

/*   Flip from drag-drop to code (mounts the DevEditor + preview iframe)   */
useCommmerceEditorSetupStore.getState().toggleEditorMode('code');

/*   Resize canvas to tablet — writes will start landing in responsiveStyles.tablet   */
useCommmerceEditorSetupStore.getState().toggleDeviceMode('tablet');

/*   Preview the base style at tablet width without changing the write target   */
useCommmerceEditor.getState().setApplyStylesToResponsive(false);

/*   Zoom — driven by Ctrl/Cmd + Scroll on the canvas, range 25%–200%   */
useCommmerceEditorSetupStore.getState().setZoom(0.75);

Programmatically open the external preview

/*   Equivalent to clicking the toolbar's Preview button in Dev Portal.   */
import { api_base_url } from '../../common/apis/RequestAPI';

const appSlug = 'acme-theme';
const versionCode = 3;
const blockId = 108;
const blockVersionCode = 2;

window.open(
  `${api_base_url}/preview/${appSlug}/v/${versionCode}/${blockId}/${blockVersionCode}`,
  '_blank'
);

Data-source globals injected into the code-mode iframe

/*   For a node like:                                                    */
/*   <mmm-data-source api="product.getProductList"                       */
/*                    response-key="featured"                            */
/*                    payload-sort="newest"                              */
/*                    payload-limit="8">                                  */
/*                                                                       */
/*   DevEditor POSTs to the dev store and injects (inside iframe):       */
<script>
  window.PRODUCT_RESPONSE_FEATURED = {
    products: [ { id: 1, name: 'Tee', price: '$29', ... }, ... ],
    pagination: { page: 1, limit: 8, total: 42 }
  };
</script>

Keyboard shortcuts

Preview itself is mouse-driven, but every shortcut bound by the canvas keeps working against whatever the iframe / device-resized canvas is showing. Shortcuts are wired up in control/index.tsx.

ShortcutActionNotes
Ctrl/Cmd + ZUndoReplays the last delta from createHistorySlice; blocked in read-only mode.
Ctrl/Cmd + Y / Ctrl/Cmd + Shift + ZRedoRe-applies the last undone delta.
Maximize buttonToggle fullscreenNative fullscreen via document.documentElement.requestFullscreen(); maximises the preview canvas.
EscExit overlays / fullscreen panelCloses the DevEditor per-panel fullscreen and clears placement mode.

Tips & gotchas

  • The canvas is already a live preview. Every mmm-* tag is mounted through drag-drop/WebComponentElement.tsx using imperative DOM calls that bypass React's vDOM, so the custom element's shadow DOM hydrates with real SDK data even while you are still authoring.
  • Code-mode iframe rebuilds on every keystroke. The srcDoc is recomputed from the JSON snapshot whenever HTML / CSS / JS changes; the iframe reloads, the SDK boots, and mmm-* tags re-register. Heavy pages can noticeably re-flash on each edit — debounce or pause between edits if needed.
  • Preview button context matters. In Dev Portal the URL embeds the current app_version_code, block_id, and block_version_code; in Store Editor it opens the public store URL. If the editor cannot resolve the version triple, it silently falls back to the published version of the app.
  • Device mode flips the write target automatically. Switching to tablet does not just resize the canvas — it sets applyStylesToResponsive = true, so the next property-panel edit lands in responsiveStyles.tablet. Use the Base / Resp switch on the bottom toolbar to override that.
  • Custom breakpoints participate in preview. They share the same renderer as the defaults; the canvas resizes to their canvasWidth and writes land in customResponsiveStyles[id]. Extras beyond four sit in the device-pill overflow menu.
  • Inspect-mode overlays survive mode flips. The selection overlay is kept on screen after you click an element so the parent editor can sync selection without the highlight disappearing — re-entering inspect mode hides it again so the hover overlay can take over.
  • Read-only editors still preview. When a published version is opened, isReadOnly is true; the Preview button, device switcher and zoom all still work, but Undo / Redo and write actions are short-circuited.
  • Browser fullscreen is not the same as the editor's "fullscreen panel" feature. The toolbar's Maximize icon enters native browser fullscreen; DevEditor's per-panel fullscreen is a separate state inside the code editor.
  • Data-source fetches use the dev store. The iframe POSTs to https://<devDefaultKeys.storeId>.commmerce.com/store/api/... regardless of which store the editor is editing for — this is intentional so previews don't require a fully-provisioned target store.
  • Empty-token marker is editor-only. The dashed-underline wrapper around unresolvable {{scope.path}} tokens is added by the editor preview's CSS and stripped before publishing — production HTML never carries these markers.