From ff2e91b6ff65a6bd9831f3bcabf6852aa7ee0280 Mon Sep 17 00:00:00 2001 From: Jan Potoms <2109932+Janpot@users.noreply.github.com> Date: Fri, 26 Aug 2022 11:45:18 +0200 Subject: [PATCH 1/8] move parts of the bridge into canvas --- .../getPageViewState.ts} | 6 +-- packages/toolpad-app/src/canvas/index.tsx | 26 +++++++++++ .../toolpad-app/src/runtime/ToolpadApp.tsx | 4 +- .../AppEditor/PageEditor/EditorCanvasHost.tsx | 43 +++++++++++-------- .../PageEditor/RenderPanel/RenderPanel.tsx | 12 ++---- packages/toolpad-app/src/utils/useEvent.ts | 38 ++++++++++++++++ 6 files changed, 100 insertions(+), 29 deletions(-) rename packages/toolpad-app/src/{pageViewState.ts => canvas/getPageViewState.ts} (97%) create mode 100644 packages/toolpad-app/src/utils/useEvent.ts diff --git a/packages/toolpad-app/src/pageViewState.ts b/packages/toolpad-app/src/canvas/getPageViewState.ts similarity index 97% rename from packages/toolpad-app/src/pageViewState.ts rename to packages/toolpad-app/src/canvas/getPageViewState.ts index 2f8174fed43..c667faf4ddb 100644 --- a/packages/toolpad-app/src/pageViewState.ts +++ b/packages/toolpad-app/src/canvas/getPageViewState.ts @@ -1,8 +1,8 @@ import { FiberNode, Hook } from 'react-devtools-inline'; import { NodeId, RUNTIME_PROP_NODE_ID, RUNTIME_PROP_SLOTS, SlotType } from '@mui/toolpad-core'; import { NodeFiberHostProps } from '@mui/toolpad-core/runtime'; -import { PageViewState, NodesInfo, NodeInfo, FlowDirection } from './types'; -import { getRelativeBoundingRect, getRelativeOuterRect } from './utils/geometry'; +import { PageViewState, NodesInfo, NodeInfo, FlowDirection } from '../types'; +import { getRelativeBoundingRect, getRelativeOuterRect } from '../utils/geometry'; declare global { interface Window { @@ -130,6 +130,6 @@ export function getNodesViewInfo(rootElm: HTMLElement): { return { nodes }; } -export function getPageViewState(rootElm: HTMLElement): PageViewState { +export default function getPageViewState(rootElm: HTMLElement): PageViewState { return getNodesViewInfo(rootElm); } diff --git a/packages/toolpad-app/src/canvas/index.tsx b/packages/toolpad-app/src/canvas/index.tsx index 7bf3bec0f86..d7c839e8652 100644 --- a/packages/toolpad-app/src/canvas/index.tsx +++ b/packages/toolpad-app/src/canvas/index.tsx @@ -1,7 +1,11 @@ import * as React from 'react'; import { fireEvent } from '@mui/toolpad-core/runtime'; +import invariant from 'invariant'; import ToolpadApp, { CanvasHooks, CanvasHooksContext } from '../runtime'; import * as appDom from '../appDom'; +import { PageViewState } from '../types'; +import getPageViewState from './getPageViewState'; +import { rectContainsPoint } from '../utils/geometry'; export interface AppCanvasState { appId: string; @@ -10,6 +14,8 @@ export interface AppCanvasState { export interface ToolpadBridge { update(updates: AppCanvasState): void; + getViewCoordinates(clientX: number, clientY: number): { x: number; y: number } | null; + getPageViewState(): PageViewState; } declare global { @@ -26,6 +32,8 @@ export interface AppCanvasProps { export default function AppCanvas({ basename }: AppCanvasProps) { const [state, setState] = React.useState(null); + const rootRef = React.useRef(null); + React.useEffect(() => { // eslint-disable-next-line no-underscore-dangle window.__TOOLPAD_BRIDGE__ = { @@ -34,7 +42,24 @@ export default function AppCanvas({ basename }: AppCanvasProps) { setState(newState); }); }, + getPageViewState: () => { + const appRoot = rootRef.current; + invariant(appRoot, 'App ref not attached'); + return getPageViewState(appRoot); + }, + getViewCoordinates(clientX, clientY) { + const appRoot = rootRef.current; + if (!appRoot) { + return null; + } + const rect = appRoot.getBoundingClientRect(); + if (rectContainsPoint(rect, clientX, clientY)) { + return { x: clientX - rect.x, y: clientY - rect.y }; + } + return null; + }, }; + // eslint-disable-next-line no-underscore-dangle if (typeof window.__TOOLPAD_READY__ === 'function') { // eslint-disable-next-line no-underscore-dangle @@ -65,6 +90,7 @@ export default function AppCanvas({ basename }: AppCanvasProps) { return state ? ( ; hidePreviewBanner?: boolean; basename: string; appId: string; @@ -787,6 +788,7 @@ export interface ToolpadAppProps { } export default function ToolpadApp({ + rootRef, basename, appId, version, @@ -800,7 +802,7 @@ export default function ToolpadApp({ React.useEffect(() => setResetNodeErrorsKey((key) => key + 1), [dom]); return ( - + diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx index f0a3f0230d6..1c965fcb022 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx @@ -6,9 +6,11 @@ import { CacheProvider } from '@emotion/react'; import ReactDOM from 'react-dom'; import { setEventHandler } from '@mui/toolpad-core/runtime'; import { throttle } from 'lodash-es'; +import invariant from 'invariant'; import * as appDom from '../../../appDom'; import { HTML_ID_APP_ROOT, HTML_ID_EDITOR_OVERLAY } from '../../../constants'; -import { rectContainsPoint } from '../../../utils/geometry'; +import { PageViewState } from '../../../types'; +import { ToolpadBridge } from '../../../canvas'; interface OverlayProps { children?: React.ReactNode; @@ -32,8 +34,8 @@ function Overlay(props: OverlayProps) { } export interface EditorCanvasHostHandle { - getRootElm(): HTMLElement | null; getViewCoordinates(clientX: number, clientY: number): { x: number; y: number } | null; + getPageViewState(): PageViewState; } export interface EditorCanvasHostProps { @@ -75,11 +77,15 @@ export default React.forwardRef( }, [appId, dom]); React.useEffect(() => update(), [update]); - const onReadyRef = React.useRef(update); - React.useEffect(() => { - onReadyRef.current = update; + const onReady = React.useCallback(() => { + update(); }, [update]); + const onReadyRef = React.useRef(onReady); + React.useEffect(() => { + onReadyRef.current = onReady; + }, [onReady]); + React.useEffect(() => { const frameWindow = frameRef.current?.contentWindow; if (!frameWindow) { @@ -100,26 +106,29 @@ export default React.forwardRef( const [editorOverlayRoot, setEditorOverlayRoot] = React.useState(null); const [appRoot, setAppRoot] = React.useState(null); + const getBridge = React.useCallback((): ToolpadBridge => { + invariant( + // eslint-disable-next-line no-underscore-dangle + frameRef.current?.contentWindow?.__TOOLPAD_BRIDGE__, + 'bridge not initialized yet', + ); + // eslint-disable-next-line no-underscore-dangle + return frameRef.current?.contentWindow?.__TOOLPAD_BRIDGE__; + }, []); + React.useImperativeHandle( forwardedRef, () => { return { - getRootElm() { - return appRoot; - }, getViewCoordinates(clientX, clientY) { - if (!appRoot) { - return null; - } - const rect = appRoot.getBoundingClientRect(); - if (rectContainsPoint(rect, clientX, clientY)) { - return { x: clientX - rect.x, y: clientY - rect.y }; - } - return null; + return getBridge().getViewCoordinates(clientX, clientY); + }, + getPageViewState() { + return getBridge().getPageViewState(); }, }; }, - [appRoot], + [getBridge], ); const handleRuntimeEventRef = React.useRef(onRuntimeEvent); diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderPanel.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderPanel.tsx index 1ccd687e9d4..39264fdf62e 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderPanel.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/RenderPanel/RenderPanel.tsx @@ -2,9 +2,9 @@ import * as React from 'react'; import { styled } from '@mui/material'; import { RuntimeEvent, NodeId } from '@mui/toolpad-core'; import { useNavigate } from 'react-router-dom'; +import invariant from 'invariant'; import * as appDom from '../../../../appDom'; import EditorCanvasHost, { EditorCanvasHostHandle } from '../EditorCanvasHost'; -import { getPageViewState } from '../../../../pageViewState'; import { useDom, useDomApi } from '../../../DomLoader'; import { usePageEditorApi, usePageEditorState } from '../PageEditorProvider'; import RenderOverlay from './RenderOverlay'; @@ -35,13 +35,9 @@ export default function RenderPanel({ className }: RenderPanelProps) { const canvasHostRef = React.useRef(null); const handlePageViewStateUpdate = React.useCallback(() => { - const rootElm = canvasHostRef.current?.getRootElm(); - - if (!rootElm) { - return; - } - - api.pageViewStateUpdate(getPageViewState(rootElm)); + invariant(canvasHostRef.current, 'canvas ref not attached'); + const pageViewState = canvasHostRef.current?.getPageViewState(); + api.pageViewStateUpdate(pageViewState); }, [api]); const navigate = useNavigate(); diff --git a/packages/toolpad-app/src/utils/useEvent.ts b/packages/toolpad-app/src/utils/useEvent.ts new file mode 100644 index 00000000000..475d7507f74 --- /dev/null +++ b/packages/toolpad-app/src/utils/useEvent.ts @@ -0,0 +1,38 @@ +import * as React from 'react'; + +let dispatcher: any = null; + +function getCurrentDispatcher() { + // eslint-disable-next-line no-underscore-dangle + return (React as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher + .current; +} + +function useRenderTracker() { + if (dispatcher === null) { + dispatcher = getCurrentDispatcher(); + } +} + +function isInRender() { + return dispatcher !== null && dispatcher === getCurrentDispatcher(); +} + +/** + * See https://github.com/reactjs/rfcs/pull/220 + */ +export function useEvent(handler: F): F { + useRenderTracker(); + const ref = React.useRef(handler); + React.useInsertionEffect(() => { + ref.current = handler; + }); + // @ts-expect-error + return React.useCallback((...args) => { + if (isInRender()) { + throw new Error('Cannot call event in Render!'); + } + const fn = ref.current; + return fn(...args); + }, []); +} From d6db2d65151d84c5d87084c549e25115e8cc1d69 Mon Sep 17 00:00:00 2001 From: Jan Potoms <2109932+Janpot@users.noreply.github.com> Date: Fri, 26 Aug 2022 13:06:12 +0200 Subject: [PATCH 2/8] Move screenupdate --- packages/toolpad-app/src/canvas/index.tsx | 58 ++++++++++++++----- .../toolpad-app/src/runtime/ToolpadApp.tsx | 2 +- .../AppEditor/PageEditor/EditorCanvasHost.tsx | 39 +------------ .../PageEditor/RenderPanel/RenderPanel.tsx | 12 ++-- packages/toolpad-core/src/types.ts | 2 +- 5 files changed, 53 insertions(+), 60 deletions(-) diff --git a/packages/toolpad-app/src/canvas/index.tsx b/packages/toolpad-app/src/canvas/index.tsx index d7c839e8652..1b27bcab8a3 100644 --- a/packages/toolpad-app/src/canvas/index.tsx +++ b/packages/toolpad-app/src/canvas/index.tsx @@ -1,6 +1,7 @@ import * as React from 'react'; import { fireEvent } from '@mui/toolpad-core/runtime'; import invariant from 'invariant'; +import { throttle } from 'lodash-es'; import ToolpadApp, { CanvasHooks, CanvasHooksContext } from '../runtime'; import * as appDom from '../appDom'; import { PageViewState } from '../types'; @@ -32,7 +33,45 @@ export interface AppCanvasProps { export default function AppCanvas({ basename }: AppCanvasProps) { const [state, setState] = React.useState(null); - const rootRef = React.useRef(null); + const rootRef = React.useRef(); + + const [appRoot, setAppRoot] = React.useState(null); + + React.useEffect(() => { + if (!appRoot) { + return () => {}; + } + + rootRef.current = appRoot; + + const onScreenUpdate = () => fireEvent({ type: 'screenUpdate' }); + + const handleScreenUpdateThrottled = throttle(onScreenUpdate, 50, { + trailing: true, + }); + + const mutationObserver = new MutationObserver(handleScreenUpdateThrottled); + + mutationObserver.observe(appRoot, { + attributes: true, + childList: true, + subtree: true, + characterData: true, + }); + + const resizeObserver = new ResizeObserver(handleScreenUpdateThrottled); + + resizeObserver.observe(appRoot); + appRoot.querySelectorAll('*').forEach((elm) => resizeObserver.observe(elm)); + + onScreenUpdate(); + + return () => { + handleScreenUpdateThrottled.cancel(); + mutationObserver.disconnect(); + resizeObserver.disconnect(); + }; + }, [appRoot]); React.useEffect(() => { // eslint-disable-next-line no-underscore-dangle @@ -43,16 +82,14 @@ export default function AppCanvas({ basename }: AppCanvasProps) { }); }, getPageViewState: () => { - const appRoot = rootRef.current; - invariant(appRoot, 'App ref not attached'); - return getPageViewState(appRoot); + invariant(rootRef.current, 'App ref not attached'); + return getPageViewState(rootRef.current); }, getViewCoordinates(clientX, clientY) { - const appRoot = rootRef.current; - if (!appRoot) { + if (!rootRef.current) { return null; } - const rect = appRoot.getBoundingClientRect(); + const rect = rootRef.current.getBoundingClientRect(); if (rectContainsPoint(rect, clientX, clientY)) { return { x: clientX - rect.x, y: clientY - rect.y }; } @@ -74,11 +111,6 @@ export default function AppCanvas({ basename }: AppCanvasProps) { }; }, []); - React.useEffect(() => { - // Run after every render - fireEvent({ type: 'afterRender' }); - }); - const editorHooks: CanvasHooks = React.useMemo(() => { return { navigateToPage(pageNodeId) { @@ -90,7 +122,7 @@ export default function AppCanvas({ basename }: AppCanvasProps) { return state ? ( ; + rootRef?: React.Ref; hidePreviewBanner?: boolean; basename: string; appId: string; diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx index 1c965fcb022..87a95f741fc 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx @@ -5,10 +5,9 @@ import createCache from '@emotion/cache'; import { CacheProvider } from '@emotion/react'; import ReactDOM from 'react-dom'; import { setEventHandler } from '@mui/toolpad-core/runtime'; -import { throttle } from 'lodash-es'; import invariant from 'invariant'; import * as appDom from '../../../appDom'; -import { HTML_ID_APP_ROOT, HTML_ID_EDITOR_OVERLAY } from '../../../constants'; +import { HTML_ID_EDITOR_OVERLAY } from '../../../constants'; import { PageViewState } from '../../../types'; import { ToolpadBridge } from '../../../canvas'; @@ -44,7 +43,6 @@ export interface EditorCanvasHostProps { pageNodeId: NodeId; dom: appDom.AppDom; onRuntimeEvent?: (event: RuntimeEvent) => void; - onScreenUpdate?: () => void; overlay?: React.ReactNode; } @@ -62,7 +60,7 @@ const CanvasFrame = styled('iframe')({ export default React.forwardRef( function EditorCanvasHost( - { appId, className, pageNodeId, dom, overlay, onRuntimeEvent, onScreenUpdate }, + { appId, className, pageNodeId, dom, overlay, onRuntimeEvent }, forwardedRef, ) { const frameRef = React.useRef(null); @@ -104,7 +102,6 @@ export default React.forwardRef( const [contentWindow, setContentWindow] = React.useState(null); const [editorOverlayRoot, setEditorOverlayRoot] = React.useState(null); - const [appRoot, setAppRoot] = React.useState(null); const getBridge = React.useCallback((): ToolpadBridge => { invariant( @@ -146,7 +143,6 @@ export default React.forwardRef( } const observer = new MutationObserver(() => { - setAppRoot(contentWindow.document.getElementById(HTML_ID_APP_ROOT)); setEditorOverlayRoot(contentWindow.document.getElementById(HTML_ID_EDITOR_OVERLAY)); }); @@ -165,37 +161,6 @@ export default React.forwardRef( }; }, [contentWindow]); - React.useEffect(() => { - if (!appRoot || !onScreenUpdate) { - return () => {}; - } - - onScreenUpdate(); - const handleScreenUpdateThrottled = throttle(onScreenUpdate, 50, { - trailing: true, - }); - - const mutationObserver = new MutationObserver(handleScreenUpdateThrottled); - - mutationObserver.observe(appRoot, { - attributes: true, - childList: true, - subtree: true, - characterData: true, - }); - - const resizeObserver = new ResizeObserver(handleScreenUpdateThrottled); - - resizeObserver.observe(appRoot); - appRoot.querySelectorAll('*').forEach((elm) => resizeObserver.observe(elm)); - - return () => { - handleScreenUpdateThrottled.cancel(); - mutationObserver.disconnect(); - resizeObserver.disconnect(); - }; - }, [appRoot, onScreenUpdate]); - return ( (null); - const handlePageViewStateUpdate = React.useCallback(() => { - invariant(canvasHostRef.current, 'canvas ref not attached'); - const pageViewState = canvasHostRef.current?.getPageViewState(); - api.pageViewStateUpdate(pageViewState); - }, [api]); - const navigate = useNavigate(); const handleRuntimeEvent = React.useCallback( @@ -70,7 +64,10 @@ export default function RenderPanel({ className }: RenderPanelProps) { api.pageBindingsUpdate(event.bindings); return; } - case 'afterRender': { + case 'screenUpdate': { + invariant(canvasHostRef.current, 'canvas ref not attached'); + const pageViewState = canvasHostRef.current?.getPageViewState(); + api.pageViewStateUpdate(pageViewState); return; } case 'pageNavigationRequest': { @@ -95,7 +92,6 @@ export default function RenderPanel({ className }: RenderPanelProps) { dom={dom} pageNodeId={pageNodeId} onRuntimeEvent={handleRuntimeEvent} - onScreenUpdate={handlePageViewStateUpdate} overlay={} /> diff --git a/packages/toolpad-core/src/types.ts b/packages/toolpad-core/src/types.ts index 2f3ac06bf12..829ea861d80 100644 --- a/packages/toolpad-core/src/types.ts +++ b/packages/toolpad-core/src/types.ts @@ -210,7 +210,7 @@ export type RuntimeEvent = type: 'pageBindingsUpdated'; bindings: LiveBindings; } - | { type: 'afterRender' } + | { type: 'screenUpdate' } | { type: 'pageNavigationRequest'; pageNodeId: NodeId }; export interface ComponentConfig

{ From e894aecba5a20b5c70c1a2d4b18b9bf350460864 Mon Sep 17 00:00:00 2001 From: Jan Potoms <2109932+Janpot@users.noreply.github.com> Date: Fri, 26 Aug 2022 13:09:07 +0200 Subject: [PATCH 3/8] didn't mean to commit this --- packages/toolpad-app/src/utils/useEvent.ts | 38 ---------------------- 1 file changed, 38 deletions(-) delete mode 100644 packages/toolpad-app/src/utils/useEvent.ts diff --git a/packages/toolpad-app/src/utils/useEvent.ts b/packages/toolpad-app/src/utils/useEvent.ts deleted file mode 100644 index 475d7507f74..00000000000 --- a/packages/toolpad-app/src/utils/useEvent.ts +++ /dev/null @@ -1,38 +0,0 @@ -import * as React from 'react'; - -let dispatcher: any = null; - -function getCurrentDispatcher() { - // eslint-disable-next-line no-underscore-dangle - return (React as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher - .current; -} - -function useRenderTracker() { - if (dispatcher === null) { - dispatcher = getCurrentDispatcher(); - } -} - -function isInRender() { - return dispatcher !== null && dispatcher === getCurrentDispatcher(); -} - -/** - * See https://github.com/reactjs/rfcs/pull/220 - */ -export function useEvent(handler: F): F { - useRenderTracker(); - const ref = React.useRef(handler); - React.useInsertionEffect(() => { - ref.current = handler; - }); - // @ts-expect-error - return React.useCallback((...args) => { - if (isInRender()) { - throw new Error('Cannot call event in Render!'); - } - const fn = ref.current; - return fn(...args); - }, []); -} From 44fdbda914a88483835ef99279097da7a4f76768 Mon Sep 17 00:00:00 2001 From: Jan Potoms <2109932+Janpot@users.noreply.github.com> Date: Fri, 26 Aug 2022 15:54:08 +0200 Subject: [PATCH 4/8] simplify bridge initialization --- packages/toolpad-app/src/canvas/index.tsx | 13 +++--- .../AppEditor/PageEditor/EditorCanvasHost.tsx | 43 ++++++++----------- 2 files changed, 24 insertions(+), 32 deletions(-) diff --git a/packages/toolpad-app/src/canvas/index.tsx b/packages/toolpad-app/src/canvas/index.tsx index 1b27bcab8a3..1599cda92e8 100644 --- a/packages/toolpad-app/src/canvas/index.tsx +++ b/packages/toolpad-app/src/canvas/index.tsx @@ -21,8 +21,7 @@ export interface ToolpadBridge { declare global { interface Window { - __TOOLPAD_READY__?: boolean | (() => void); - __TOOLPAD_BRIDGE__?: ToolpadBridge; + __TOOLPAD_BRIDGE__?: ToolpadBridge | ((bridge: ToolpadBridge) => void); } } @@ -74,8 +73,7 @@ export default function AppCanvas({ basename }: AppCanvasProps) { }, [appRoot]); React.useEffect(() => { - // eslint-disable-next-line no-underscore-dangle - window.__TOOLPAD_BRIDGE__ = { + const bridge: ToolpadBridge = { update: (newState) => { React.startTransition(() => { setState(newState); @@ -98,13 +96,14 @@ export default function AppCanvas({ basename }: AppCanvasProps) { }; // eslint-disable-next-line no-underscore-dangle - if (typeof window.__TOOLPAD_READY__ === 'function') { + if (typeof window.__TOOLPAD_BRIDGE__ === 'function') { // eslint-disable-next-line no-underscore-dangle - window.__TOOLPAD_READY__(); + window.__TOOLPAD_BRIDGE__(bridge); } else { // eslint-disable-next-line no-underscore-dangle - window.__TOOLPAD_READY__ = true; + window.__TOOLPAD_BRIDGE__ = bridge; } + return () => { // eslint-disable-next-line no-underscore-dangle delete window.__TOOLPAD_BRIDGE__; diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx index 87a95f741fc..d3c170cadc8 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx @@ -65,14 +65,12 @@ export default React.forwardRef( ) { const frameRef = React.useRef(null); + const [bridge, setBridge] = React.useState(null); + const update = React.useCallback(() => { const renderDom = appDom.createRenderTree(dom); - // eslint-disable-next-line no-underscore-dangle - frameRef.current?.contentWindow?.__TOOLPAD_BRIDGE__?.update({ - appId, - dom: renderDom, - }); - }, [appId, dom]); + bridge?.update({ appId, dom: renderDom }); + }, [appId, dom, bridge]); React.useEffect(() => update(), [update]); const onReady = React.useCallback(() => { @@ -86,46 +84,41 @@ export default React.forwardRef( React.useEffect(() => { const frameWindow = frameRef.current?.contentWindow; - if (!frameWindow) { - throw new Error('Iframe ref not attached'); - } + invariant(frameWindow, 'frameRef not atached'); // eslint-disable-next-line no-underscore-dangle - if (frameWindow.__TOOLPAD_READY__ === true) { + if (typeof frameWindow.__TOOLPAD_BRIDGE__ === 'object') { + // eslint-disable-next-line no-underscore-dangle + setBridge(frameWindow.__TOOLPAD_BRIDGE__); onReadyRef.current?.(); // eslint-disable-next-line no-underscore-dangle - } else if (typeof frameWindow.__TOOLPAD_READY__ !== 'function') { + } else if (typeof frameWindow.__TOOLPAD_BRIDGE__ === 'undefined') { // eslint-disable-next-line no-underscore-dangle - frameWindow.__TOOLPAD_READY__ = () => onReadyRef.current?.(); + frameWindow.__TOOLPAD_BRIDGE__ = (newBridge: ToolpadBridge) => { + setBridge(newBridge); + onReadyRef.current?.(); + }; } }, []); const [contentWindow, setContentWindow] = React.useState(null); const [editorOverlayRoot, setEditorOverlayRoot] = React.useState(null); - const getBridge = React.useCallback((): ToolpadBridge => { - invariant( - // eslint-disable-next-line no-underscore-dangle - frameRef.current?.contentWindow?.__TOOLPAD_BRIDGE__, - 'bridge not initialized yet', - ); - // eslint-disable-next-line no-underscore-dangle - return frameRef.current?.contentWindow?.__TOOLPAD_BRIDGE__; - }, []); - React.useImperativeHandle( forwardedRef, () => { return { getViewCoordinates(clientX, clientY) { - return getBridge().getViewCoordinates(clientX, clientY); + invariant(bridge, 'bridge not initialized'); + return bridge.getViewCoordinates(clientX, clientY); }, getPageViewState() { - return getBridge().getPageViewState(); + invariant(bridge, 'bridge not initialized'); + return bridge.getPageViewState(); }, }; }, - [getBridge], + [bridge], ); const handleRuntimeEventRef = React.useRef(onRuntimeEvent); From 5895a72b8ac4a9f6fa2f85fac22059899cb65902 Mon Sep 17 00:00:00 2001 From: Jan Potoms <2109932+Janpot@users.noreply.github.com> Date: Fri, 26 Aug 2022 17:40:01 +0200 Subject: [PATCH 5/8] simplify more --- packages/toolpad-app/src/canvas/index.tsx | 5 +-- .../AppEditor/PageEditor/EditorCanvasHost.tsx | 29 +++++++------- packages/toolpad-app/src/utils/useEvent.ts | 38 +++++++++++++++++++ 3 files changed, 53 insertions(+), 19 deletions(-) create mode 100644 packages/toolpad-app/src/utils/useEvent.ts diff --git a/packages/toolpad-app/src/canvas/index.tsx b/packages/toolpad-app/src/canvas/index.tsx index 1599cda92e8..23b9aa12601 100644 --- a/packages/toolpad-app/src/canvas/index.tsx +++ b/packages/toolpad-app/src/canvas/index.tsx @@ -104,10 +104,7 @@ export default function AppCanvas({ basename }: AppCanvasProps) { window.__TOOLPAD_BRIDGE__ = bridge; } - return () => { - // eslint-disable-next-line no-underscore-dangle - delete window.__TOOLPAD_BRIDGE__; - }; + return () => {}; }, []); const editorHooks: CanvasHooks = React.useMemo(() => { diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx index d3c170cadc8..4c251d45f3c 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx @@ -10,6 +10,7 @@ import * as appDom from '../../../appDom'; import { HTML_ID_EDITOR_OVERLAY } from '../../../constants'; import { PageViewState } from '../../../types'; import { ToolpadBridge } from '../../../canvas'; +import useEvent from '../../../utils/useEvent'; interface OverlayProps { children?: React.ReactNode; @@ -68,19 +69,19 @@ export default React.forwardRef( const [bridge, setBridge] = React.useState(null); const update = React.useCallback(() => { - const renderDom = appDom.createRenderTree(dom); - bridge?.update({ appId, dom: renderDom }); + if (bridge) { + const renderDom = appDom.createRenderTree(dom); + bridge.update({ appId, dom: renderDom }); + } }, [appId, dom, bridge]); - React.useEffect(() => update(), [update]); - const onReady = React.useCallback(() => { - update(); - }, [update]); + React.useEffect(() => update(), [update]); - const onReadyRef = React.useRef(onReady); - React.useEffect(() => { - onReadyRef.current = onReady; - }, [onReady]); + const handleInit = useEvent((newBridge: ToolpadBridge) => { + setBridge(newBridge); + const renderDom = appDom.createRenderTree(dom); + newBridge.update({ appId, dom: renderDom }); + }); React.useEffect(() => { const frameWindow = frameRef.current?.contentWindow; @@ -89,17 +90,15 @@ export default React.forwardRef( // eslint-disable-next-line no-underscore-dangle if (typeof frameWindow.__TOOLPAD_BRIDGE__ === 'object') { // eslint-disable-next-line no-underscore-dangle - setBridge(frameWindow.__TOOLPAD_BRIDGE__); - onReadyRef.current?.(); + handleInit(frameWindow.__TOOLPAD_BRIDGE__); // eslint-disable-next-line no-underscore-dangle } else if (typeof frameWindow.__TOOLPAD_BRIDGE__ === 'undefined') { // eslint-disable-next-line no-underscore-dangle frameWindow.__TOOLPAD_BRIDGE__ = (newBridge: ToolpadBridge) => { - setBridge(newBridge); - onReadyRef.current?.(); + handleInit(newBridge); }; } - }, []); + }, [handleInit]); const [contentWindow, setContentWindow] = React.useState(null); const [editorOverlayRoot, setEditorOverlayRoot] = React.useState(null); diff --git a/packages/toolpad-app/src/utils/useEvent.ts b/packages/toolpad-app/src/utils/useEvent.ts new file mode 100644 index 00000000000..6c2fba52afa --- /dev/null +++ b/packages/toolpad-app/src/utils/useEvent.ts @@ -0,0 +1,38 @@ +import * as React from 'react'; + +let dispatcher: any = null; + +function getCurrentDispatcher() { + // eslint-disable-next-line no-underscore-dangle + return (React as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentDispatcher + .current; +} + +function useRenderTracker() { + if (dispatcher === null) { + dispatcher = getCurrentDispatcher(); + } +} + +function isInRender() { + return dispatcher !== null && dispatcher === getCurrentDispatcher(); +} + +/** + * See https://github.com/reactjs/rfcs/pull/220 + */ +export default function useEvent(handler: F): F { + useRenderTracker(); + const ref = React.useRef(handler); + React.useInsertionEffect(() => { + ref.current = handler; + }); + // @ts-expect-error + return React.useCallback((...args) => { + if (isInRender()) { + throw new Error('Cannot call event in Render!'); + } + const fn = ref.current; + return fn(...args); + }, []); +} From 7137dfcb2c34683702346dc2911bdac7eda5d62a Mon Sep 17 00:00:00 2001 From: Jan Potoms <2109932+Janpot@users.noreply.github.com> Date: Fri, 26 Aug 2022 17:43:50 +0200 Subject: [PATCH 6/8] remove unnecessary --- packages/toolpad-app/src/constants.ts | 1 - packages/toolpad-app/src/runtime/ToolpadApp.tsx | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/toolpad-app/src/constants.ts b/packages/toolpad-app/src/constants.ts index 2ece17b3b3f..639173c9021 100644 --- a/packages/toolpad-app/src/constants.ts +++ b/packages/toolpad-app/src/constants.ts @@ -1,6 +1,5 @@ export const MUI_X_PRO_LICENSE = '82281641f885336d8c6b61faa73edecdT1JERVI6c3R1ZGlvXzEyMyxFWFBJUlk9MTY3NzY2NTQ3MjM4MSxLRVlWRVJTSU9OPTE='; -export const HTML_ID_APP_ROOT = 'root'; export const HTML_ID_EDITOR_OVERLAY = 'editor-overlay'; export const WINDOW_PROP_TOOLPAD_APP_RENDER_PARAMS = '__TOOLPAD_APP_RENDER_PARAMS__'; export const RUNTIME_CONFIG_WINDOW_PROPERTY = '__TOOLPAD_RUNTIME_CONFIG__'; diff --git a/packages/toolpad-app/src/runtime/ToolpadApp.tsx b/packages/toolpad-app/src/runtime/ToolpadApp.tsx index 0a636ff8b7c..0ceec83ba7a 100644 --- a/packages/toolpad-app/src/runtime/ToolpadApp.tsx +++ b/packages/toolpad-app/src/runtime/ToolpadApp.tsx @@ -58,7 +58,7 @@ import evalJsBindings, { evaluateExpression, ParsedBinding, } from './evalJsBindings'; -import { HTML_ID_APP_ROOT, HTML_ID_EDITOR_OVERLAY } from '../constants'; +import { HTML_ID_EDITOR_OVERLAY } from '../constants'; import { mapProperties, mapValues } from '../utils/collections'; import usePageTitle from '../utils/usePageTitle'; import ComponentsContext, { useComponents, useComponent } from './ComponentsContext'; @@ -802,7 +802,7 @@ export default function ToolpadApp({ React.useEffect(() => setResetNodeErrorsKey((key) => key + 1), [dom]); return ( - + From 2020882cfa1277a5c6cdd1a9097c8f0a7bd93ded Mon Sep 17 00:00:00 2001 From: Jan Potoms <2109932+Janpot@users.noreply.github.com> Date: Fri, 26 Aug 2022 18:00:44 +0200 Subject: [PATCH 7/8] contentWindow --- .../src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx index 8be50046383..ef9e577fc48 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx @@ -12,6 +12,9 @@ import { PageViewState } from '../../../types'; import { ToolpadBridge } from '../../../canvas'; import useEvent from '../../../utils/useEvent'; import { LogEntry } from '../../../components/Console'; +import { Maybe } from '../../../utils/types'; + +type IframeContentWindow = Window & typeof globalThis; interface OverlayProps { children?: React.ReactNode; @@ -119,10 +122,7 @@ export default React.forwardRef( }); React.useEffect(() => { - const frameWindow = frameRef.current?.contentWindow as - | (Window & typeof globalThis) - | undefined - | null; + const frameWindow = frameRef.current?.contentWindow as Maybe; invariant(frameWindow, 'Iframe ref not attached'); const originalConsole: Console = frameWindow.console; From 7bf7254ea4bdc8006e66d34cf0f34407c4c189f2 Mon Sep 17 00:00:00 2001 From: Jan Potoms <2109932+Janpot@users.noreply.github.com> Date: Fri, 26 Aug 2022 18:21:56 +0200 Subject: [PATCH 8/8] updates --- .../AppEditor/PageEditor/EditorCanvasHost.tsx | 46 +++++++++++-------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx index ef9e577fc48..3aa56c6b7d0 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/EditorCanvasHost.tsx @@ -94,26 +94,31 @@ function wrapConsole(targetConsole: Console, onEntry: (entry: LogEntry) => void) export default React.forwardRef( function EditorCanvasHost( - { appId, className, pageNodeId, dom, overlay, onRuntimeEvent, onConsoleEntry }, + { appId, className, pageNodeId, dom, overlay, onRuntimeEvent = () => {}, onConsoleEntry }, forwardedRef, ) { const frameRef = React.useRef(null); const [bridge, setBridge] = React.useState(null); - const update = React.useCallback(() => { - if (bridge) { + const updateOnBridge = React.useCallback( + (bridgeInstance: ToolpadBridge) => { const renderDom = appDom.createRenderTree(dom); - bridge.update({ appId, dom: renderDom }); - } - }, [appId, dom, bridge]); + bridgeInstance.update({ appId, dom: renderDom }); + }, + [appId, dom], + ); - React.useEffect(() => update(), [update]); + React.useEffect(() => { + if (bridge) { + // Update every time dom prop updates + updateOnBridge(bridge); + } + }, [updateOnBridge, bridge]); const handleInit = useEvent((newBridge: ToolpadBridge) => { setBridge(newBridge); - const renderDom = appDom.createRenderTree(dom); - newBridge.update({ appId, dom: renderDom }); + updateOnBridge(newBridge); }); const onConsoleEntryRef = React.useRef(onConsoleEntry); @@ -167,18 +172,16 @@ export default React.forwardRef( [bridge], ); - const handleRuntimeEventRef = React.useRef(onRuntimeEvent); - React.useEffect(() => { - handleRuntimeEventRef.current = onRuntimeEvent; - }, [onRuntimeEvent]); + const handleRuntimeEvent = useEvent(onRuntimeEvent); const handleFrameLoad = React.useCallback(() => { - setContentWindow(frameRef.current?.contentWindow || null); + invariant(frameRef.current, 'Iframe ref not attached'); + setContentWindow(frameRef.current.contentWindow); }, []); React.useEffect(() => { if (!contentWindow) { - return () => {}; + return undefined; } const observer = new MutationObserver(() => { @@ -190,16 +193,19 @@ export default React.forwardRef( childList: true, }); - const cleanupHandler = setEventHandler(contentWindow, (event) => - handleRuntimeEventRef.current?.(event), - ); - return () => { observer.disconnect(); - cleanupHandler(); }; }, [contentWindow]); + React.useEffect(() => { + if (!contentWindow) { + return undefined; + } + + return setEventHandler(contentWindow, handleRuntimeEvent); + }, [handleRuntimeEvent, contentWindow]); + return (