diff --git a/flow-react/src/main/resources/com/vaadin/flow/server/frontend/ReactAdapter.template b/flow-react/src/main/resources/com/vaadin/flow/server/frontend/ReactAdapter.template index f1ce4af0874..6c22e1ad74f 100644 --- a/flow-react/src/main/resources/com/vaadin/flow/server/frontend/ReactAdapter.template +++ b/flow-react/src/main/resources/com/vaadin/flow/server/frontend/ReactAdapter.template @@ -13,30 +13,21 @@ * License for the specific language governing permissions and limitations under * the License. */ -import {createRoot, Root} from "react-dom/client"; -import { - createElement, - type Dispatch, - type ReactElement, - type ReactNode, - Ref, - useEffect, - useReducer, - useRef -} from "react"; +import { createRoot, Root } from 'react-dom/client'; +import { createElement, type Dispatch, type ReactElement, type ReactNode, useEffect, useReducer } from 'react'; type FlowStateKeyChangedAction = Readonly<{ - type: 'stateKeyChanged', - key: K, - value: V, + type: 'stateKeyChanged'; + key: K; + value: V; }>; type FlowStateReducerAction = FlowStateKeyChangedAction; function stateReducer>>(state: S, action: FlowStateReducerAction): S { switch (action.type) { - case "stateKeyChanged": - const {key, value} = action; + case 'stateKeyChanged': + const { value } = action; return { ...state, key: value @@ -46,9 +37,7 @@ function stateReducer>>(state: S, act } } -type DispatchEvent = T extends undefined - ? () => boolean - : (value: T) => boolean; +type DispatchEvent = T extends undefined ? () => boolean : (value: T) => boolean; const emptyAction: Dispatch = () => {}; @@ -72,7 +61,7 @@ export type RenderHooks = { * 2. The `set` function for changing the state and triggering render * @protected */ - readonly useState: ReactAdapterElement["useState"] + readonly useState: ReactAdapterElement['useState']; /** * A hook helper to simplify dispatching a `CustomEvent` on the Web @@ -88,7 +77,7 @@ export type RenderHooks = { * - For other types, has one parameter for the `event.detail` value of that type. * @protected */ - readonly useCustomEvent: ReactAdapterElement["useCustomEvent"] + readonly useCustomEvent: ReactAdapterElement['useCustomEvent']; /** * A hook helper to generate the content element with name attribute to bind @@ -109,7 +98,7 @@ export type RenderHooks = { * * @param name - The name attribute of the element */ - readonly useContent: ReactAdapterElement["useContent"] + readonly useContent: ReactAdapterElement['useContent']; }; interface ReadyCallbackFunction { @@ -137,7 +126,7 @@ export abstract class ReactAdapterElement extends HTMLElement { readonly #Wrapper: () => ReactElement | null; - #unmountComplete = Promise.resolve(); + #unmounting?: Promise; constructor() { super(); @@ -151,22 +140,25 @@ export abstract class ReactAdapterElement extends HTMLElement { } public async connectedCallback() { - await this.#unmountComplete; this.#rendering = createElement(this.#Wrapper); - const createNewRoot = this.dispatchEvent(new CustomEvent('flow-portal-add', { - bubbles: true, - cancelable: true, - composed: true, - detail: { - children: this.#rendering, - domNode: this, - } - })); + const createNewRoot = this.dispatchEvent( + new CustomEvent('flow-portal-add', { + bubbles: true, + cancelable: true, + composed: true, + detail: { + children: this.#rendering, + domNode: this + } + }) + ); if (!createNewRoot || this.#root) { return; } + await this.#unmounting; + this.#root = createRoot(this); this.#maybeRenderRoot(); this.#root.render(this.#rendering); @@ -187,19 +179,24 @@ export abstract class ReactAdapterElement extends HTMLElement { } public async disconnectedCallback() { - this.dispatchEvent(new CustomEvent('flow-portal-remove', { - bubbles: true, - cancelable: true, - composed: true, - detail: { - children: this.#rendering, - domNode: this, - } - })); - this.#unmountComplete = Promise.resolve(); - await this.#unmountComplete; - this.#root?.unmount(); - this.#root = undefined; + if (!this.#root) { + this.dispatchEvent( + new CustomEvent('flow-portal-remove', { + bubbles: true, + cancelable: true, + composed: true, + detail: { + children: this.#rendering, + domNode: this + } + }) + ); + } else { + this.#unmounting = Promise.resolve(); + await this.#unmounting; + this.#root.unmount(); + this.#root = undefined; + } this.#rootRendered = false; this.#rendering = undefined; } @@ -233,15 +230,15 @@ export abstract class ReactAdapterElement extends HTMLElement { }, set(nextValue: T) { this.#state[key] = nextValue; - this.#dispatchFlowState({type: 'stateKeyChanged', key, value}); + this.#dispatchFlowState({ type: 'stateKeyChanged', key, value }); } }); - const dispatchChangedEvent = this.useCustomEvent<{value: T}>(`${key}-changed`, {detail: {value}}); + const dispatchChangedEvent = this.useCustomEvent<{ value: T }>(`${key}-changed`, { detail: { value } }); const setValue = (value: T) => { this.#state[key] = value; - dispatchChangedEvent({value}); - this.#dispatchFlowState({type: 'stateKeyChanged', key, value}); + dispatchChangedEvent({ value }); + this.#dispatchFlowState({ type: 'stateKeyChanged', key, value }); }; this.#stateSetters.set(key, setValue as Dispatch); return [value, setValue]; @@ -264,10 +261,13 @@ export abstract class ReactAdapterElement extends HTMLElement { protected useCustomEvent(type: string, options: CustomEventInit = {}): DispatchEvent { if (!this.#customEvents.has(type)) { const dispatch = ((detail?: T) => { - const eventInitDict = detail === undefined ? options : { - ...options, - detail - }; + const eventInitDict = + detail === undefined + ? options + : { + ...options, + detail + }; const event = new CustomEvent(type, eventInitDict); return this.dispatchEvent(event); }) as DispatchEvent; @@ -295,7 +295,7 @@ export abstract class ReactAdapterElement extends HTMLElement { useEffect(() => { this.#readyCallback.get(name)?.(); }, []); - return createElement('flow-content-container', {name, style: {display: 'contents'}}); + return createElement('flow-content-container', { name, style: { display: 'contents' } }); } #maybeRenderRoot() { @@ -314,7 +314,7 @@ export abstract class ReactAdapterElement extends HTMLElement { return this.render(this.#renderHooks); } - #markAsUsed() : void { + #markAsUsed(): void { // @ts-ignore let vaadinObject = window.Vaadin || {}; // @ts-ignore diff --git a/flow-server/src/main/resources/com/vaadin/flow/server/frontend/Flow.tsx b/flow-server/src/main/resources/com/vaadin/flow/server/frontend/Flow.tsx index eb72d07c389..a96949bfd32 100644 --- a/flow-server/src/main/resources/com/vaadin/flow/server/frontend/Flow.tsx +++ b/flow-server/src/main/resources/com/vaadin/flow/server/frontend/Flow.tsx @@ -14,27 +14,14 @@ * the License. */ /// -import { Flow as _Flow } from "Frontend/generated/jar-resources/Flow.js"; -import React, { - useCallback, - useEffect, - useReducer, - useRef, - useState, - type ReactNode -} from "react"; -import { - matchRoutes, - useBlocker, - useLocation, - useNavigate, - type NavigateOptions, useHref, -} from "react-router-dom"; -import type { AgnosticRouteObject } from '@remix-run/router'; -import { createPortal } from "react-dom"; +import { nanoid } from 'nanoid'; +import { Flow as _Flow } from 'Frontend/generated/jar-resources/Flow.js'; +import React, { useCallback, useEffect, useReducer, useRef, useState, type ReactNode } from 'react'; +import { matchRoutes, useBlocker, useLocation, useNavigate, type NavigateOptions, useHref } from 'react-router'; +import { createPortal } from 'react-dom'; const flow = new _Flow({ - imports: () => import("Frontend/generated/flow/generated-flow-imports.js") + imports: () => import('Frontend/generated/flow/generated-flow-imports.js') }); const router = { @@ -52,9 +39,10 @@ function getAnchorOrigin(anchor) { const protocol = anchor.protocol; const defaultHttp = protocol === 'http:' && port === '80'; const defaultHttps = protocol === 'https:' && port === '443'; - const host = (defaultHttp || defaultHttps) - ? anchor.hostname // does not include the port number (e.g. www.example.org) - : anchor.host; // does include the port number (e.g. www.example.org:80) + const host = + defaultHttp || defaultHttps + ? anchor.hostname // does not include the port number (e.g. www.example.org) + : anchor.host; // does include the port number (e.g. www.example.org:80) return `${protocol}//${host}`; } @@ -88,8 +76,8 @@ function extractPath(event: MouseEvent): void | string { let maybeAnchor = event.target; const path = event.composedPath ? event.composedPath() - // @ts-ignore - : (event.path || []); + : // @ts-ignore + event.path || []; // example to check: `for...of` loop here throws the "Not yet implemented" error for (let i = 0; i < path.length; i++) { @@ -152,61 +140,100 @@ function extractPath(event: MouseEvent): void | string { * @param pathname pathname of navigation * @param search search of navigation */ -function fireNavigated(pathname:string, search: string) { +function fireNavigated(pathname: string, search: string) { setTimeout(() => { - window.dispatchEvent(new CustomEvent('vaadin-navigated', { + window.dispatchEvent( + new CustomEvent('vaadin-navigated', { detail: { pathname, search } - })); - // @ts-ignore - delete window.Vaadin.Flow.navigation; - } - ) + }) + ); + // @ts-ignore + delete window.Vaadin.Flow.navigation; + }); } -function postpone() { -} +function postpone() {} const prevent = () => postpone; -type RouterContainer = Awaited>; +type RouterContainer = Awaited>; type PortalEntry = { - readonly children: ReactNode, - readonly domNode: Element | DocumentFragment, + readonly children: ReactNode; + readonly domNode: HTMLElement; }; -const enum PortalActionType { - Add = 'add', - Remove = 'remove', +type FlowPortalProps = React.PropsWithChildren< + Readonly<{ + domNode: HTMLElement; + onRemove(): void; + }> +>; + +function FlowPortal({ children, domNode, onRemove }: FlowPortalProps) { + useEffect(() => { + domNode.addEventListener( + 'flow-portal-remove', + (event: Event) => { + event.preventDefault(); + onRemove(); + }, + { once: true } + ); + }, []); + + return createPortal(children, domNode); } -type PortalAction = { - readonly type: PortalActionType, - readonly entry: PortalEntry, -}; +const ADD_FLOW_PORTAL = 'ADD_FLOW_PORTAL'; + +type AddFlowPortalAction = Readonly<{ + type: typeof ADD_FLOW_PORTAL; + portal: React.ReactElement; +}>; + +function addFlowPortal(portal: React.ReactElement): AddFlowPortalAction { + return { + type: ADD_FLOW_PORTAL, + portal + }; +} + +const REMOVE_FLOW_PORTAL = 'REMOVE_FLOW_PORTAL'; -function portalsReducer(portals: readonly PortalEntry[], action: PortalAction) { +type RemoveFlowPortalAction = Readonly<{ + type: typeof REMOVE_FLOW_PORTAL; + key: string; +}>; + +function removeFlowPortal(key: string): RemoveFlowPortalAction { + return { + type: REMOVE_FLOW_PORTAL, + key + }; +} + +function flowPortalsReducer( + portals: readonly React.ReactElement[], + action: AddFlowPortalAction | RemoveFlowPortalAction +) { switch (action.type) { - case PortalActionType.Add: - return [ - ...portals, - action.entry - ]; - case PortalActionType.Remove: - return portals.filter(({domNode}) => domNode !== action.entry.domNode); + case ADD_FLOW_PORTAL: + return [...portals, action.portal]; + case REMOVE_FLOW_PORTAL: + return portals.filter(({ key }) => key !== action.key); default: return portals; } } - -type NavigateOpts = { - to: string, - callback: boolean, - opts?: NavigateOptions +type NavigateOpts = { + to: string; + callback: boolean; + opts?: NavigateOptions; }; type NavigateFn = (to: string, callback: boolean, opts?: NavigateOptions) => void; @@ -216,7 +243,10 @@ type NavigateFn = (to: string, callback: boolean, opts?: NavigateOptions) => voi * with React Router API that has more consistent history updates. Uses internal * queue for processing navigate calls. */ -function useQueuedNavigate(waitReference: React.MutableRefObject | undefined>, navigated: React.MutableRefObject): NavigateFn { +function useQueuedNavigate( + waitReference: React.MutableRefObject | undefined>, + navigated: React.MutableRefObject +): NavigateFn { const navigate = useNavigate(); const navigateQueue = useRef([]).current; const [navigateQueueLength, setNavigateQueueLength] = useState(0); @@ -236,7 +266,7 @@ function useQueuedNavigate(waitReference: React.MutableRefObject | navigated.current = !navigateArgs.callback; navigate(navigateArgs.to, navigateArgs.opts); setNavigateQueueLength(navigateQueue.length); - } + }; blockingNavigate(); }, [navigate, setNavigateQueueLength]); @@ -244,22 +274,28 @@ function useQueuedNavigate(waitReference: React.MutableRefObject | queueMicrotask(dequeueNavigation); }, [dequeueNavigation]); - const enqueueNavigation = useCallback((to: string, callback: boolean, opts?: NavigateOptions) => { - navigateQueue.push({to: to, callback: callback, opts: opts}); - setNavigateQueueLength(navigateQueue.length); - if (navigateQueue.length === 1) { - // The first navigation can be started right after any pending sync - // jobs, which could add more navigations to the queue. + const enqueueNavigation = useCallback( + (to: string, callback: boolean, opts?: NavigateOptions) => { + navigateQueue.push({ to: to, callback: callback, opts: opts }); + setNavigateQueueLength(navigateQueue.length); + if (navigateQueue.length === 1) { + // The first navigation can be started right after any pending sync + // jobs, which could add more navigations to the queue. + dequeueNavigationAfterCurrentTask(); + } + }, + [setNavigateQueueLength, dequeueNavigationAfterCurrentTask] + ); + + useEffect( + () => () => { + // The Flow component has rendered, but history might not be + // updated yet, as React Router does it asynchronously. + // Use microtask callback for history consistency. dequeueNavigationAfterCurrentTask(); - } - }, [setNavigateQueueLength, dequeueNavigationAfterCurrentTask]); - - useEffect(() => () => { - // The Flow component has rendered, but history might not be - // updated yet, as React Router does it asynchronously. - // Use microtask callback for history consistency. - dequeueNavigationAfterCurrentTask(); - }, [navigateQueueLength, dequeueNavigationAfterCurrentTask]); + }, + [navigateQueueLength, dequeueNavigationAfterCurrentTask] + ); return enqueueNavigation; } @@ -268,7 +304,11 @@ function Flow() { const ref = useRef(null); const navigate = useNavigate(); const blocker = useBlocker(({ currentLocation, nextLocation }) => { - navigated.current = navigated.current || (nextLocation.pathname === currentLocation.pathname && nextLocation.search === currentLocation.search && nextLocation.hash === currentLocation.hash); + navigated.current = + navigated.current || + (nextLocation.pathname === currentLocation.pathname && + nextLocation.search === currentLocation.search && + nextLocation.hash === currentLocation.hash); return true; }); const location = useLocation(); @@ -281,63 +321,83 @@ function Flow() { const basename = useHref('/'); // portalsReducer function is used as state outside the Flow component. - const [portals, dispatchPortalAction] = useReducer(portalsReducer, []); - - const removePortalEventHandler = useCallback((event: CustomEvent) => { - event.preventDefault(); - dispatchPortalAction({type: PortalActionType.Remove, entry: event.detail}); - }, [dispatchPortalAction]) - - const addPortalEventHandler = useCallback((event: CustomEvent) => { - event.preventDefault(); - // Add remove event listener to the portal node - event.detail.domNode.addEventListener('flow-portal-remove', removePortalEventHandler as EventListener, {once: true}); - dispatchPortalAction({type: PortalActionType.Add, entry: event.detail}); - }, [dispatchPortalAction, removePortalEventHandler]); - - const navigateEventHandler = useCallback((event: MouseEvent) => { - const path = extractPath(event); - if (!path) { - return; - } + const [portals, dispatchPortalAction] = useReducer(flowPortalsReducer, []); - if (event && event.preventDefault) { + const addPortalEventHandler = useCallback( + (event: CustomEvent) => { event.preventDefault(); - } - navigated.current = false; - // When navigation is triggered by click on a link, fromAnchor is set to true - // in order to get a server round-trip even when navigating to the same URL again - fromAnchor.current = true; - navigate(path); - // Dispatch close event for overlay drawer on click navigation. - window.dispatchEvent(new CustomEvent('close-overlay-drawer')); - }, [navigate]); - - const vaadinRouterGoEventHandler = useCallback((event: CustomEvent) => { - const url = event.detail; - const path = normalizeURL(url); - if (!path) { - return; - } + const key = nanoid(); + dispatchPortalAction( + addFlowPortal( + dispatchPortalAction(removeFlowPortal(key))} + > + {event.detail.children} + + ) + ); + }, + [dispatchPortalAction] + ); + + const navigateEventHandler = useCallback( + (event: MouseEvent) => { + const path = extractPath(event); + if (!path) { + return; + } - event.preventDefault(); - navigate(path); - }, [navigate]); + if (event && event.preventDefault) { + event.preventDefault(); + } - const vaadinNavigateEventHandler = useCallback((event: CustomEvent<{state: unknown, url: string, replace?: boolean, callback: boolean}>) => { - // @ts-ignore - window.Vaadin.Flow.navigation = true; - const path = '/' + event.detail.url; - fromAnchor.current = false; - queuedNavigate(path, event.detail.callback, { state: event.detail.state, replace: event.detail.replace }); - }, [navigate]); - - const redirect = useCallback((path: string) => { - return (() => { - navigate(path, {replace: true}); - }); - }, [navigate]); + navigated.current = false; + // When navigation is triggered by click on a link, fromAnchor is set to true + // in order to get a server round-trip even when navigating to the same URL again + fromAnchor.current = true; + navigate(path); + // Dispatch close event for overlay drawer on click navigation. + window.dispatchEvent(new CustomEvent('close-overlay-drawer')); + }, + [navigate] + ); + + const vaadinRouterGoEventHandler = useCallback( + (event: CustomEvent) => { + const url = event.detail; + const path = normalizeURL(url); + if (!path) { + return; + } + + event.preventDefault(); + navigate(path); + }, + [navigate] + ); + + const vaadinNavigateEventHandler = useCallback( + (event: CustomEvent<{ state: unknown; url: string; replace?: boolean; callback: boolean }>) => { + // @ts-ignore + window.Vaadin.Flow.navigation = true; + const path = '/' + event.detail.url; + fromAnchor.current = false; + queuedNavigate(path, event.detail.callback, { state: event.detail.state, replace: event.detail.replace }); + }, + [navigate] + ); + + const redirect = useCallback( + (path: string) => { + return () => { + navigate(path, { replace: true }); + }; + }, + [navigate] + ); useEffect(() => { // @ts-ignore @@ -363,21 +423,29 @@ function Flow() { useEffect(() => { if (blocker.state === 'blocked') { - if(blockerHandled.current) { + if (blockerHandled.current) { // Blocker is handled and the new navigation // gets queued to be executed after the current handling ends. - const {pathname, state} = blocker.location; + const { pathname, state } = blocker.location; // Clear base name to not get /baseName/basename/path const pathNoBase = pathname.substring(basename.length); // path should always start with / else react-router will append to current url - queuedNavigate(pathNoBase.startsWith('/') ? pathNoBase : '/'+pathNoBase, true, { state: state, replace: true }); + queuedNavigate(pathNoBase.startsWith('/') ? pathNoBase : '/' + pathNoBase, true, { + state: state, + replace: true + }); return; } blockerHandled.current = true; let blockingPromise: any; - roundTrip.current = new Promise((resolve,reject) => blockingPromise = {resolve:resolve,reject:reject}); + roundTrip.current = new Promise( + (resolve, reject) => (blockingPromise = { resolve: resolve, reject: reject }) + ); // Release blocker handling after promise is fulfilled - roundTrip.current.then(() => blockerHandled.current = false, () => blockerHandled.current = false); + roundTrip.current.then( + () => (blockerHandled.current = false), + () => (blockerHandled.current = false) + ); // Proceed to the blocked location, unless the navigation originates from a click on a link. // In that case continue with function execution and perform a server round-trip @@ -387,15 +455,17 @@ function Flow() { return; } fromAnchor.current = false; - const {pathname, search} = blocker.location; - const routes = ((window as any)?.Vaadin?.routesConfig || []) as AgnosticRouteObject[]; + const { pathname, search } = blocker.location; + const routes = ((window as any)?.Vaadin?.routesConfig || []) as any[]; let matched = matchRoutes(Array.from(routes), pathname); // Navigation between server routes // @ts-ignore - if (matched && matched.filter(path => path.route?.element?.type?.name === Flow.name).length != 0) { - containerRef.current?.onBeforeEnter?.call(containerRef?.current, - {pathname, search}, { + if (matched && matched.filter((path) => path.route?.element?.type?.name === Flow.name).length != 0) { + containerRef.current?.onBeforeEnter?.call( + containerRef?.current, + { pathname, search }, + { prevent() { blocker.reset(); blockingPromise.resolve(); @@ -406,32 +476,40 @@ function Flow() { blocker.proceed(); blockingPromise.resolve(); } - }, router); + }, + router + ); navigated.current = true; } else { // For covering the 'server -> client' use case - Promise.resolve(containerRef.current?.onBeforeLeave?.call(containerRef?.current, { - pathname, - search - }, {prevent}, router)) - .then((cmd: unknown) => { - if (cmd === postpone && containerRef.current) { - // postponed navigation: expose existing blocker to Flow - containerRef.current.serverConnected = (cancel) => { - if (cancel) { - blocker.reset(); - blockingPromise.resolve(); - } else { - blocker.proceed(); - blockingPromise.resolve(); - } + Promise.resolve( + containerRef.current?.onBeforeLeave?.call( + containerRef?.current, + { + pathname, + search + }, + { prevent }, + router + ) + ).then((cmd: unknown) => { + if (cmd === postpone && containerRef.current) { + // postponed navigation: expose existing blocker to Flow + containerRef.current.serverConnected = (cancel) => { + if (cancel) { + blocker.reset(); + blockingPromise.resolve(); + } else { + blocker.proceed(); + blockingPromise.resolve(); } - } else { - // permitted navigation: proceed with the blocker - blocker.proceed(); - blockingPromise.resolve(); - } - }); + }; + } else { + // permitted navigation: proceed with the blocker + blocker.proceed(); + blockingPromise.resolve(); + } + }); } } }, [blocker.state, blocker.location]); @@ -442,38 +520,49 @@ function Flow() { } if (navigated.current) { navigated.current = false; - fireNavigated(location.pathname,location.search); + fireNavigated(location.pathname, location.search); return; } - flow.serverSideRoutes[0].action({pathname: location.pathname, search: location.search}) + flow.serverSideRoutes[0] + .action({ pathname: location.pathname, search: location.search }) .then((container) => { const outlet = ref.current?.parentNode; if (outlet && outlet !== container.parentNode) { outlet.append(container); container.addEventListener('flow-portal-add', addPortalEventHandler as EventListener); - window.addEventListener('click', navigateEventHandler); - containerRef.current = container + window.addEventListener('click', navigateEventHandler); + containerRef.current = container; } - return container.onBeforeEnter?.call(container, {pathname: location.pathname, search: location.search}, {prevent, redirect, continue() { - fireNavigated(location.pathname,location.search);}}, router); + return container.onBeforeEnter?.call( + container, + { pathname: location.pathname, search: location.search }, + { + prevent, + redirect, + continue() { + fireNavigated(location.pathname, location.search); + } + }, + router + ); }) .then((result: unknown) => { - if (typeof result === "function") { + if (typeof result === 'function') { result(); } }); }, [location]); - return <> - - {portals.map(({children, domNode}) => createPortal(children, domNode))} - ; + return ( + <> + + {portals} + + ); } Flow.type = 'FlowContainer'; // This is for copilot to recognize this -export const serverSideRoutes = [ - { path: '/*', element: }, -]; +export const serverSideRoutes = [{ path: '/*', element: }]; /** * Load the script for an exported WebComponent with the given tag @@ -487,17 +576,17 @@ export const loadComponentScript = (tag: String): Promise => { useEffect(() => { const script = document.createElement('script'); script.src = `/web-component/${tag}.js`; - script.onload = function() { + script.onload = function () { resolve(); }; - script.onerror = function(err) { + script.onerror = function (err) { reject(err); }; document.head.appendChild(script); return () => { document.head.removeChild(script); - } + }; }, []); }); }; @@ -514,16 +603,19 @@ interface Properties { * @param onload optional callback to be called for script onload * @param onerror optional callback for error loading the script */ -export const reactElement = (tag: string, props?: Properties, onload?: () => void, onerror?: (err:any) => void) => { - loadComponentScript(tag).then(() => onload?.(), (err) => { - if(onerror) { - onerror(err); - } else { - console.error(`Failed to load script for ${tag}.`, err); +export const reactElement = (tag: string, props?: Properties, onload?: () => void, onerror?: (err: any) => void) => { + loadComponentScript(tag).then( + () => onload?.(), + (err) => { + if (onerror) { + onerror(err); + } else { + console.error(`Failed to load script for ${tag}.`, err); + } } - }); + ); - if(props) { + if (props) { return React.createElement(tag, props); } return React.createElement(tag); @@ -533,14 +625,14 @@ export default Flow; // @ts-ignore if (import.meta.hot) { - // @ts-ignore - import.meta.hot.accept((newModule) => { - // A hot module replace for Flow.tsx happens when any JS/TS imported through @JsModule - // or similar is updated because this updates generated-flow-imports.js and that in turn - // is imported by this file. We have no means of hot replacing those files, e.g. some - // custom lit element so we need to reload the page. */ - if (newModule) { - window.location.reload(); - } - }); + // @ts-ignore + import.meta.hot.accept((newModule) => { + // A hot module replace for Flow.tsx happens when any JS/TS imported through @JsModule + // or similar is updated because this updates generated-flow-imports.js and that in turn + // is imported by this file. We have no means of hot replacing those files, e.g. some + // custom lit element so we need to reload the page. */ + if (newModule) { + window.location.reload(); + } + }); }