Source: data-engine-js-components-web/src/main/resources/META-INF/resources/js/custom/form/FormView.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 '../../../css/main.scss';

import {fetch, openModal} from 'frontend-js-web';
import React, {
	useCallback,
	useEffect,
	useImperativeHandle,
	useRef,
} from 'react';

import Pages from '../../core/components/Pages.es';
import {INITIAL_CONFIG_STATE} from '../../core/config/initialConfigState.es';
import {INITIAL_STATE} from '../../core/config/initialState.es';
import {ConfigProvider, useConfig} from '../../core/hooks/useConfig.es';
import {FormProvider, useForm, useFormState} from '../../core/hooks/useForm.es';
import {
	activePageReducer,
	fieldReducer,
	languageReducer,
	pageValidationReducer,
	pagesStructureReducer,
} from '../../core/reducers/index.es';
import formValidate from '../../core/thunks/formValidate.es';
import pageLanguageUpdate from '../../core/thunks/pageLanguageUpdate.es';
import {evaluate} from '../../utils/evaluation.es';
import * as Fields from '../../utils/fields.es';
import {getFormId, getFormNode} from '../../utils/formId.es';
import {parseProps} from '../../utils/parseProps.es';
import {paginationReducer} from './reducers/index.es';

const DDM_FORM_PORTLET_NAMESPACE =
	'_com_liferay_dynamic_data_mapping_form_web_portlet_DDMFormPortlet_';

/**
 * This is a copy of the old implementation made in Metal.js, deals with
 * the submit of the Form in the User View, removes the default behavior
 * of the submit and uses Liferay.Util.submitForm.
 */
const useFormSubmit = ({apiRef, containerRef}) => {
	const {activePage, pages} = useFormState();
	const {portletNamespace, submittable, validateCSRFTokenURL} = useConfig();
	const isDDMFormPortletNamespace = portletNamespace.includes(
		DDM_FORM_PORTLET_NAMESPACE
	);

	const submitForm = useCallback(
		(event) => {
			apiRef.current
				.validate()
				.then((validForm) => {
					if (validForm) {
						const liferayForm =
							event.target.id &&
							Liferay.Form.get(event.target.id);

						const validLiferayForm = !Object.keys(
							liferayForm?.formValidator?.errors ?? {}
						).length;

						if (!validLiferayForm) {
							Liferay.fire('ddmFormError', {
								formWrapperId: event.target.id,
							});

							return;
						}

						if (submittable) {
							Liferay.Util.submitForm(event.target);
						}

						Liferay.fire('ddmFormValid', {
							formWrapperId: event.target.id,
						});

						Liferay.fire('ddmFormSubmit', {
							formId: getFormId(
								getFormNode(containerRef.current)
							),
						});
					}
					else {
						Liferay.fire('ddmFormError', {
							formWrapperId: event.target.id,
						});
					}
				})
				.catch((error) => {
					console.error(error);

					Liferay.fire('ddmFormError', {
						error,
						formWrapperId: event.target.id,
					});
				});
		},
		[apiRef, containerRef, submittable]
	);

	const handleFormSubmitted = useCallback(
		(event) => {
			event.preventDefault();

			if (isDDMFormPortletNamespace) {
				fetch(validateCSRFTokenURL, {
					method: 'GET',
				})
					.then((response) => response.json())
					.then((jsonResponse) => {
						if (jsonResponse.validCSRFToken) {
							submitForm(event);
						}
						else {
							showSessionExpiredModal();
						}
					})
					.catch((error) => {
						console.error(error);
					});
			}
			else {
				submitForm(event);
			}
		},
		[isDDMFormPortletNamespace, submitForm, validateCSRFTokenURL]
	);

	const showSessionExpiredModal = () => {
		openModal({
			bodyHTML: Liferay.ThemeDisplay.isSignedIn()
				? Liferay.Language.get(
						'you-need-to-be-signed-in-to-submit-this-form'
				  )
				: Liferay.Language.get(
						'you-need-to-reload-the-page-to-submit-this-form'
				  ),
			buttons: [
				{
					displayType: 'secondary',
					label: Liferay.Language.get('cancel'),
					type: 'cancel',
				},
				{
					label: Liferay.Language.get('ok'),
					onClick: () => {
						if (Liferay.ThemeDisplay.isSignedIn()) {
							location.href =
								themeDisplay.getPathMain() +
								'/portal/login?redirect=' +
								window.location.href;
						}
						else {
							window.location.reload();
						}
					},
				},
			],
			id: '<portlet:namespace />ddmFormSessionExpiredModal',
			title: Liferay.Language.get('your-session-has-expired'),
		});
	};

	useEffect(() => {
		if (containerRef.current) {
			Liferay.fire('ddmFormPageShow', {
				formId: getFormId(getFormNode(containerRef.current)),
				page: activePage,
				title: pages[activePage].title,
			});
		}
		// eslint-disable-next-line react-hooks/exhaustive-deps
	}, []);

	useEffect(() => {
		if (containerRef.current) {
			const form = getFormNode(containerRef.current);

			if (form) {
				const onHandle = Liferay.on(
					'submitForm',
					(event) => {
						if (event.form && event.form.getDOM() === form) {
							event.preventDefault();
						}
					},
					this
				);

				form.addEventListener('submit', handleFormSubmitted);

				return () => {
					onHandle.detach();

					form.removeEventListener('submit', handleFormSubmitted);
				};
			}
		}
	}, [containerRef, handleFormSubmitted]);
};

/**
 * Exposes methods to manipulate some states within the application in
 * User View through the ref via React and exposed by the
 * Liferay.Component register.
 */
const usePublicAPI = ({apiRef, containerRef, unstable_onEventRef}) => {
	const {containerId, groupId, portletNamespace} = useConfig();
	const {
		activePage,
		ddmStructureLayoutId,
		defaultLanguageId,
		description,
		editingLanguageId,
		name,
		pages,
		paginationMode,
		readOnly,
		rules,
		successPageSettings,
		viewMode,
		...otherProps
	} = useFormState();
	const dispatch = useForm();

	const validate = useCallback(
		() =>
			dispatch(
				formValidate({
					activePage,
					defaultLanguageId,
					editingLanguageId,
					groupId,
					pages,
					portletNamespace,
					rules,
					viewMode,
				})
			),
		[
			dispatch,
			activePage,
			defaultLanguageId,
			editingLanguageId,
			groupId,
			pages,
			portletNamespace,
			rules,
			viewMode,
		]
	);

	useEffect(() => {
		Liferay.component(
			containerId,
			{
				reactComponentRef: apiRef,

				// unstable_onEvent allows listening to internal events of the
				// FormProvider through the public instance of the form. This
				// is still unstable and can change at any time.

				unstable_onEvent: (callback) => {
					if (unstable_onEventRef) {
						unstable_onEventRef.current = callback;
					}
					else {
						throw new Error(
							`Forms: unstable_onEvent is not available for this instance (Id: ${containerId}).`
						);
					}
				},
			},
			{
				destroyOnNavigate: true,
			}
		);

		return () => {
			Liferay.destroyComponent(containerId);
		};
	}, [apiRef, containerId, unstable_onEventRef]);

	useImperativeHandle(apiRef, () => ({
		evaluate: (editingLanguageId) =>
			evaluate(null, {
				defaultLanguageId,
				editingLanguageId,
				groupId,
				pages,
				portletNamespace,
				rules,
				viewMode,
			}),
		get: (key) => {
			const props = {
				activePage,
				defaultLanguageId,
				description,
				editingLanguageId,
				name,
				pages,
				paginationMode,
				portletNamespace,
				readOnly,
				rules,
				successPageSettings,
				...otherProps,
			};

			return props[key];
		},
		getFields: () => Fields.getFields(pages),
		getFormNode: () =>
			containerRef.current && getFormNode(containerRef.current),
		toJSON: () => ({
			defaultLanguageId,
			description,
			editingLanguageId,
			name,
			pages,
			paginationMode,
			portletNamespace,
			rules,
			successPageSettings,
		}),
		updateEditingLanguageId: ({
			defaultLanguageId: nextDefaultLanguageId = defaultLanguageId,
			editingLanguageId: nextEditingLanguageId = '',
			preserveValue,
		}) =>
			dispatch(
				pageLanguageUpdate({
					ddmStructureLayoutId,
					defaultLanguageId: nextDefaultLanguageId,
					nextEditingLanguageId,
					pages,
					portletNamespace,
					preserveValue,
					prevEditingLanguageId: editingLanguageId,
					readOnly,
				})
			),
		validate,
	}));
};

export const Form = React.forwardRef(({unstable_onEventRef}, ref) => {
	const containerRef = useRef(null);

	useFormSubmit({apiRef: ref, containerRef});
	usePublicAPI({apiRef: ref, containerRef, unstable_onEventRef});

	return <Pages editable={false} ref={containerRef} />;
});

Form.displayName = 'Form';

/**
 * Maps the representation of pagination mode in the builder environment
 * in the form view.
 */
const PAGINATION_MODE_MAPPED = {
	'multi-pages': 'wizard',
};

/**
 * Exports the default application to render the form
 * for the user.
 */
export const FormView = React.forwardRef((props, ref) => {
	const {config, state} = parseProps(props);

	// The application in User View will always expose the public
	// APIs through the registration of Liferay.Component regardless
	// of receiving a ref or not. We maintain a defaultRef as a
	// fallback for this case.

	const defaultRef = useRef(null);

	const unstable_onEventRef = useRef(null);

	return (
		<ConfigProvider config={config} initialConfig={INITIAL_CONFIG_STATE}>
			<FormProvider
				init={({paginationMode, ...otherProps}) => ({
					...otherProps,
					paginationMode:
						PAGINATION_MODE_MAPPED[paginationMode] ??
						paginationMode,
				})}
				initialState={INITIAL_STATE}
				onAction={(action) => {
					if (unstable_onEventRef.current) {
						unstable_onEventRef.current(action);
					}
				}}
				reducers={[
					activePageReducer,
					fieldReducer,
					languageReducer,
					pagesStructureReducer,
					pageValidationReducer,
					paginationReducer,
				]}
				value={state}
			>
				<Form
					ref={ref ?? defaultRef}
					unstable_onEventRef={unstable_onEventRef}
				/>
			</FormProvider>
		</ConfigProvider>
	);
});

FormView.displayName = 'FormView';

export default FormView;