239 lines
8.3 KiB
TypeScript
239 lines
8.3 KiB
TypeScript
/*
|
|
* 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<K extends string, V> = Readonly<{
|
|
type: 'stateKeyChanged',
|
|
key: K,
|
|
value: V,
|
|
}>;
|
|
|
|
type FlowStateReducerAction = FlowStateKeyChangedAction<string, unknown>;
|
|
|
|
function stateReducer<S extends Readonly<Record<string, unknown>>>(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> = T extends undefined
|
|
? () => boolean
|
|
: (value: T) => boolean;
|
|
|
|
const emptyAction: Dispatch<unknown> = () => {};
|
|
|
|
/**
|
|
* 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<string, unknown> = Object.create(null);
|
|
#stateSetters = new Map<string, Dispatch<unknown>>();
|
|
#customEvents = new Map<string, DispatchEvent<unknown>>();
|
|
#dispatchFlowState: Dispatch<FlowStateReducerAction> = 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<T>(key: string, initialValue?: T): [value: T, setValue: Dispatch<T>] {
|
|
if (this.#stateSetters.has(key)) {
|
|
return [this.#state[key] as T, this.#stateSetters.get(key)!];
|
|
}
|
|
|
|
const value = ((this as Record<string, unknown>)[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<unknown>);
|
|
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<T = undefined>(type: string, options: CustomEventInit<T> = {}): DispatchEvent<T> {
|
|
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<T>;
|
|
this.#customEvents.set(type, dispatch as DispatchEvent<unknown>);
|
|
return dispatch;
|
|
}
|
|
return this.#customEvents.get(type)! as DispatchEvent<T>;
|
|
}
|
|
|
|
/**
|
|
* 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'
|
|
});
|
|
}
|
|
}
|
|
}
|