Skip to main content

Component Isolation & Host Render Policy

By default, a Myop HTML component renders either as a same-origin iframe or, when it opts in, as a web component on the host document. Both run with access to the host page. That's ideal for trusted, first-party UI β€” but not for code you don't control.

The render policy gives the host app the final say over how (or whether) each component renders, and adds a new isolated rendering mode: a sandboxed, cross-origin iframe with no access to the host document β€” the safe way to render untrusted or marketplace components.


Render modes​

Every HTML component resolves to one of these modes, in ascending order of isolation:

ModeWhat it isIsolation
'webComponent'Custom element on the host document (Shadow DOM)none β€” full host privileges
'iframe'Same-origin inline-HTML iframe (today's default)none β€” same origin as host
'isolatedIframe'Sandboxed, cross-origin iframefull β€” no host DOM / cookies / storage / window.parent
falseRendering is blockedβ€”

The render policy hook​

A host supplies a predicate that receives context about the component and returns the mode to use (or false to block).

import { hostSDK, RenderPolicy } from '@myop/sdk/host';

const policy: RenderPolicy = (ctx) => {
// ctx = { componentDefinition, skin, requestedMode, isWebComponent }
return isTrusted(ctx.componentDefinition)
? ctx.requestedMode // trusted: render as authored
: 'isolatedIframe'; // untrusted: sandbox it
};

hostSDK.setRenderPolicy(policy); // global, app-wide

RenderPolicyContext:

FieldMeaning
componentDefinitionThe component being loaded (id, name, …) β€” key your trust decision on this.
skinThe selected skin / variant.
requestedModeWhat the component asked for (marker β‡’ 'webComponent', isolation config β‡’ 'isolatedIframe', else 'iframe').
isWebComponentWhether the HTML carries the <!-- myop-web-component --> marker.

The policy may be async (return a Promise) β€” useful if trust requires a lookup.

Global vs. per-call precedence​

  • Global: hostSDK.setRenderPolicy(policy) applies to every loadComponent.
  • Per-call: loadComponent(config, container, { renderPolicy }) β€” a per-call policy fully replaces the global one for that call.

This lets an app restrict globally and allow specific exceptions:

// Deny everything by default…
hostSDK.setRenderPolicy(() => false);

// …except this one trusted component.
await hostSDK.loadComponent(trustedConfig, container, {
renderPolicy: () => 'iframe',
});

With no policy set, behavior is unchanged from before: the marker renders a web component, everything else an iframe.


Forcing isolation directly​

You don't need a full policy just to isolate. Set the isolation flag:

// Per load
await hostSDK.loadComponent(config, container, { data, isolation: true });
// Or on the loader/skin config
{ "type": "HTMLLoader", "HTML": "…", "shadowRootMode": "localFrame", "isolation": true }

A policy returning 'isolatedIframe' has the same effect and always wins over the config value.


What an isolated iframe actually does​

When a component renders isolated, the SDK:

  1. Creates the iframe with sandbox="allow-scripts allow-forms allow-popups" β€” note the deliberate absence of allow-same-origin. This gives the frame an opaque origin: the host cannot read its contentDocument, and inside the frame window.parent is cross-origin, with no access to the host's cookies, localStorage, or DOM.
  2. Writes the component via srcdoc (you cannot document.write an opaque-origin frame).
  3. Injects a small postMessage bridge that translates the normal contract across the boundary:
    • data-in β€” the host posts myop:init; the bridge calls your window.myop_init_interface(data) (plus a baked initial value for the first render),
    • data-out β€” your window.myop_cta_handler(action, payload) is forwarded to the host as a myop:cta message,
    • resize β€” a ResizeObserver reports content height to the host as myop:size.

The author-facing contract is unchanged. Inside the frame you still define window.myop_init_interface and call window.myop_cta_handler exactly as in a normal HTML component β€” the SDK bridges it over postMessage. On the host side, component.props.myop_init_interface(...), getData(), and component.props.myop_cta_handler = … all work the same; the SDK routes them through the bridge instead of same-origin access.


The marketplace pattern​

The motivating case: a host renders components authored by contributors outside its org. Isolate the untrusted ones; keep first-party UI fast.

hostSDK.setRenderPolicy((ctx) => {
const trusted = isFirstParty(ctx.componentDefinition);
if (trusted) return ctx.requestedMode; // web component / iframe, as authored
if (ctx.isWebComponent) return 'isolatedIframe'; // deny inline web component β†’ sandbox
return 'isolatedIframe'; // untrusted iframe β†’ sandbox too
});

Net effect: outside-org code never runs on the host document.


Important: what does NOT work isolated​

Isolation intentionally severs same-origin access. So a component that depends on the host being same-origin will break under isolation. This includes any component that:

  • reads or writes window.parent / window.top directly (rather than via postMessage),
  • is itself a host of nested iframes it drives with contentDocument / contentWindow (authoring tools, live-preview editors, WYSIWYG inspectors),
  • relies on the full iframe SDK ref/props/ExecuteScript protocol.
Don't blanket-isolate first-party tooling

Applying 'isolatedIframe' to everything also isolates first-party tools (for example, an in-app component editor). A sandboxed frame's own child iframes inherit the sandbox and become cross-origin to it β€” so nested-host tooling and same-origin handshakes stop working.

Isolation is a per-component decision: sandbox untrusted code, render first-party UI normally. That's exactly why this is a policy, not a global switch.


Blocking​

Returning false from the policy blocks rendering entirely (loadComponent throws). Note that a web component denied by policy is blocked rather than downgraded β€” its HTML has no iframe-contract fallback, so it cannot be silently re-rendered as an iframe. Untrusted contributors who need to be sandboxed should ship iframe components (which the SDK can isolate), not web components.


Per-component in React / Vue / Angular​

The framework components forward SDK loader options, so you can isolate a single instance without a policy β€” pass isolation through loaderOptions:

// React
<MyopComponent componentId={id} data={data} loaderOptions={{ isolation: true }} on={onCta} />
<!-- Vue -->
<MyopComponent :componentId="id" :data="data" :loaderOptions="{ isolation: true }" :on="onCta" />
<!-- Angular -->
<myop-component [componentId]="id" [data]="data" [loaderOptions]="{ isolation: true }" [on]="onCta" />

Full framework guides: React Β· Vue Β· Angular. A global setRenderPolicy still takes precedence over a per-instance loaderOptions.isolation.

See also​