/* * Copyright 2000-2024 Vaadin Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * 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, useReducer} from "react"; type FlowStateKeyChangedAction = Readonly<{ 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; return { ...state, key: value } as S; default: return state; } } type DispatchEvent = T extends undefined ? () => boolean : (value: T) => boolean; const emptyAction: Dispatch = () => {}; /** * An object with APIs exposed for using in the {@link ReactAdapterElement#render} * implementation. */ export type RenderHooks = { /** * A hook API for using stateful JS properties of the Web Component from * the React `render()`. * * @typeParam T - Type of the state value * * @param key - Web Component property name, which is used for two-way * value propagation from the server and back. * @param initialValue - Fallback initial value (optional). Only applies if * the Java component constructor does not invoke `setState`. * @returns A tuple with two values: * 1. The current state. * 2. The `set` function for changing the state and triggering render * @protected */ readonly useState: ReactAdapterElement["useState"] /** * A hook helper to simplify dispatching a `CustomEvent` on the Web * Component from React. * * @typeParam T - The type for `event.detail` value (optional). * * @param type - The `CustomEvent` type string. * @param options - The settings for the `CustomEvent`. * @returns The `dispatch` function. The function parameters change * depending on the `T` generic type: * - For `undefined` type (default), has no parameters. * - For other types, has one parameter for the `event.detail` value of that type. * @protected */ readonly useCustomEvent: ReactAdapterElement["useCustomEvent"] }; /** * A base class for Web Components that render using React. Enables creating * adapters for integrating React components with Flow. Intended for use with * `ReactAdapterComponent` Flow Java class. */ export abstract class ReactAdapterElement extends HTMLElement { #root: Root | undefined = undefined; #rootRendered: boolean = false; #state: Record = Object.create(null); #stateSetters = new Map>(); #customEvents = new Map>(); #dispatchFlowState: Dispatch = emptyAction; readonly #renderHooks: RenderHooks; readonly #Wrapper: () => ReactElement | null; #unmountComplete = Promise.resolve(); constructor() { super(); this.#renderHooks = { useState: this.useState.bind(this), useCustomEvent: this.useCustomEvent.bind(this) }; this.#Wrapper = this.#renderWrapper.bind(this); this.#markAsUsed(); } public async connectedCallback() { await this.#unmountComplete; this.#root = createRoot(this); this.#maybeRenderRoot(); } public async disconnectedCallback() { this.#unmountComplete = Promise.resolve(); await this.#unmountComplete; this.#root?.unmount(); this.#root = undefined; this.#rootRendered = false; } /** * A hook API for using stateful JS properties of the Web Component from * the React `render()`. * * @typeParam T - Type of the state value * * @param key - Web Component property name, which is used for two-way * value propagation from the server and back. * @param initialValue - Fallback initial value (optional). Only applies if * the Java component constructor does not invoke `setState`. * @returns A tuple with two values: * 1. The current state. * 2. The `set` function for changing the state and triggering render * @protected */ protected useState(key: string, initialValue?: T): [value: T, setValue: Dispatch] { if (this.#stateSetters.has(key)) { return [this.#state[key] as T, this.#stateSetters.get(key)!]; } const value = ((this as Record)[key] as T) ?? initialValue!; this.#state[key] = value; Object.defineProperty(this, key, { enumerable: true, get(): T { return this.#state[key]; }, set(nextValue: T) { this.#state[key] = nextValue; this.#dispatchFlowState({type: 'stateKeyChanged', key, 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}); }; this.#stateSetters.set(key, setValue as Dispatch); return [value, setValue]; } /** * A hook helper to simplify dispatching a `CustomEvent` on the Web * Component from React. * * @typeParam T - The type for `event.detail` value (optional). * * @param type - The `CustomEvent` type string. * @param options - The settings for the `CustomEvent`. * @returns The `dispatch` function. The function parameters change * depending on the `T` generic type: * - For `undefined` type (default), has no parameters. * - For other types, has one parameter for the `event.detail` value of that type. * @protected */ protected useCustomEvent(type: string, options: CustomEventInit = {}): DispatchEvent { if (!this.#customEvents.has(type)) { const dispatch = ((detail?: T) => { const eventInitDict = detail === undefined ? options : { ...options, detail }; const event = new CustomEvent(type, eventInitDict); return this.dispatchEvent(event); }) as DispatchEvent; this.#customEvents.set(type, dispatch as DispatchEvent); return dispatch; } return this.#customEvents.get(type)! as DispatchEvent; } /** * The Web Component render function. To be implemented by users with React. * * @param hooks - the adapter APIs exposed for the implementation. * @protected */ protected abstract render(hooks: RenderHooks): ReactElement | null; #maybeRenderRoot() { if (this.#rootRendered || !this.#root) { return; } this.#root.render(createElement(this.#Wrapper)); this.#rootRendered = true; } #renderWrapper(): ReactElement | null { const [state, dispatchFlowState] = useReducer(stateReducer, this.#state); this.#state = state; this.#dispatchFlowState = dispatchFlowState; return this.render(this.#renderHooks); } #markAsUsed() : void { // @ts-ignore let vaadinObject = window.Vaadin || {}; // @ts-ignore if (vaadinObject.developmentMode) { vaadinObject.registrations = vaadinObject.registrations || []; vaadinObject.registrations.push({ is: 'ReactAdapterElement', version: '24.4.17' }); } } }