Source: data-engine-js-components-web/src/main/resources/META-INF/resources/js/core/components/Field/Field.es.js

/**
 * Copyright (c) 2000-present Liferay, Inc. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 */

import ClayButton from '@clayui/button';
import ClayLoadingIndicator from '@clayui/loading-indicator';
import MetalComponent from 'metal-component';
import React, {
	Suspense,
	lazy,
	useCallback,
	useContext,
	useRef,
	useState,
} from 'react';

import {sub} from '../../../utils/strings';
import {useFormState} from '../../hooks/useForm.es';
import {usePage} from '../../hooks/usePage.es';
import {useStorage} from '../../hooks/useStorage.es';
import {AutoFocus} from '../AutoFocus.es';
import {ErrorBoundary} from '../ErrorBoundary.es';

import './Field.scss';
import {MetalComponentAdapter} from './MetalComponentAdapter.es';
import {ParentFieldContext} from './ParentFieldContext.es';

const getModule = (fieldTypes, fieldType) => {
	const field = fieldTypes.find((field) => field.name === fieldType);

	return field;
};

const load = (fieldModule) => {
	return new Promise((resolve, reject) => {
		Liferay.Loader.require(
			[fieldModule],
			(Field) => resolve(Field),
			(error) => reject({error, network: true})
		);
	});
};

const useLazy = () => {
	const {components} = useStorage();

	return useCallback(
		(fieldModule) => {
			if (!components.has(fieldModule)) {
				const Component = lazy(() => {
					return load(fieldModule)
						.then((instance) => {
							if (!(instance && instance.default)) {
								return null;
							}

							// To maintain compatibility with fields in Metal+Soy,
							// we call the bridge component to handle this component.

							if (
								MetalComponent.isComponentCtor(instance.default)
							) {
								return {
									default: MetalComponentAdapter,
								};
							}

							return instance;
						})
						.catch((error) => {
							components.delete(fieldModule);

							throw error;
						});
				});

				components.set(fieldModule, Component);
			}

			return components.get(fieldModule);
		},
		[components]
	);
};

class FieldEventStruct {
	constructor(event, field, value = null, key) {
		this.fieldInstance = {
			...field,

			// This is a fake function that maintains compatibility with the use
			// of Metal+Soy fields.

			isDisposed: () => false,
		};

		this.key = key;
		this.originalEvent = event;
		this.value = value !== null ? value : event?.target?.value;
	}
}

/**
 * This only assembles the expected structure of the Forms field
 * event, creates a makeup to maintain compatibility with the
 * mechanism, the fields in React do not need to assemble this
 * structure, they must only provide a native event or value in
 * the case of an onChange
 */
const mountStruct = (event, field, value, key) => {

	// A field event struct may have been declared before, for cases of nested
	// fields with the FieldSet field.

	if (event instanceof FieldEventStruct) {
		return event;
	}

	return new FieldEventStruct(event, field, value, key);
};

const FieldLazy = ({
	field,
	fieldTypes,
	onBlur,
	onChange,
	onFocus,
	...otherProps
}) => {
	const {editable} = usePage();

	const focusDurationRef = useRef({end: null, start: null});

	const {configuration = {}, javaScriptModule} = getModule(
		fieldTypes,
		field.type
	);

	const ComponentLazy = useLazy()(javaScriptModule);

	return (
		<ComponentLazy
			onBlur={(event) => {
				focusDurationRef.current.end = new Date();
				onBlur(mountStruct(event, field), focusDurationRef.current);
			}}
			onChange={(event, value, key) =>
				onChange(mountStruct(event, field, value, key))
			}
			onFocus={(event) => {
				focusDurationRef.current.start = new Date();
				onFocus(mountStruct(event, field));
			}}
			{...field}
			readOnly={editable ? true : field.readOnly}
			visible={editable ? true : field.visible ?? true}
			{...otherProps}
			{...configuration}
		/>
	);
};

const getRootParentField = (field, currentLoc, {loc, root}) => {
	if (root) {
		return {
			...field,
			loc: [...loc, currentLoc],
			root,
		};
	}

	return {
		...field,
		loc: [currentLoc],
		root: field,
	};
};

export function Field({field, loc, ...otherProps}) {
	const parentField = useContext(ParentFieldContext);
	const {defaultLanguageId, editingLanguageId} = useFormState();
	const {fieldTypes} = usePage();
	const [hasError, setHasError] = useState();

	const getReadOnly = ({
		editOnlyInDefaultLanguage = false,
		localizable,
		readOnly,
	}) => {
		if (
			editOnlyInDefaultLanguage &&
			!localizable &&
			editingLanguageId !== defaultLanguageId
		) {
			return true;
		}

		return readOnly;
	};

	if (!fieldTypes) {
		return <ClayLoadingIndicator />;
	}

	if (hasError) {
		return (
			<div className="ddm-field-renderer--error">
				<p className="ddm-field-renderer--title">
					{sub(
						Liferay.Language.get(
							'there-was-an-error-when-loading-the-x-field'
						),
						[field.type]
					)}
				</p>

				{hasError.network && (
					<ClayButton
						className="ddm-field-renderer--button"
						displayType="secondary"
						onClick={() => setHasError(false)}
						small
					>
						{Liferay.Language.get('refresh')}
					</ClayButton>
				)}
			</div>
		);
	}

	return (
		<ErrorBoundary onError={setHasError}>
			<AutoFocus>
				<div className="ddm-field" data-field-name={field.fieldName}>
					<Suspense fallback={<ClayLoadingIndicator />}>
						<ParentFieldContext.Provider
							value={getRootParentField(field, loc, parentField)}
						>
							<FieldLazy
								field={{
									...field,
									readOnly: getReadOnly(field),
								}}
								fieldTypes={fieldTypes}
								{...otherProps}
							/>
						</ParentFieldContext.Provider>
					</Suspense>
				</div>
			</AutoFocus>
		</ErrorBoundary>
	);
}