Portal Community

WidgetRenderer Component

// packages/flow-studio-api/src/clients/widgetApiClient.ts
// WidgetRenderer mounts the widget bundle into a sandbox iframe

interface WidgetRendererProps {
  widgetId: string;
  props: Record<string, unknown>;
  executionId: string;
  taskId: string;
  onInteraction: (event: string, data: Record<string, unknown>) => void;
}

export function WidgetRenderer({ widgetId, props, executionId, onInteraction }: WidgetRendererProps) {
  const { definition, loading } = useWidgetDefinition(widgetId);
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!definition || !containerRef.current) return;
    loadWidgetBundle(definition.bundleUrl).then(() => {
      const widget = window.BizFirstWidgets[widgetId];
      widget.mount(containerRef.current!, props, {
        onInteraction: (event, data) => onInteraction(event, data)
      });
      return () => widget.unmount(containerRef.current!);
    });
  }, [definition, widgetId]);

  if (loading) return <WidgetSkeleton />;
  return <div ref={containerRef} className="widget-container" />;
}

Bundle Loading Strategy

Widget bundles are loaded dynamically using a script tag injection strategy. Bundles are cached by the browser after first load — subsequent widget tasks for the same widget type load instantly.

const bundleCache = new Map<string, Promise<void>>();

function loadWidgetBundle(bundleUrl: string): Promise<void> {
  if (bundleCache.has(bundleUrl)) return bundleCache.get(bundleUrl)!;

  const promise = new Promise<void>((resolve, reject) => {
    const script = document.createElement('script');
    script.src = bundleUrl;
    script.onload = () => resolve();
    script.onerror = () => reject(new Error(`Failed to load widget bundle: ${bundleUrl}`));
    document.head.appendChild(script);
  });

  bundleCache.set(bundleUrl, promise);
  return promise;
}

Sandbox Isolation

Widgets run in a sandboxed context to prevent them from accessing the parent page's DOM, cookies, or local storage. The sandbox policy is configured per widget definition and enforced via iframe sandbox attributes and Content Security Policy headers.