// Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. import { container, DependencyContainer } from 'tsyringe'; import { Constructor } from '@cratis/fundamentals'; import { FunctionComponent, ReactElement, useContext, useEffect, useRef, useState } from 'react'; import { Observer } from 'mobx-react'; import { makeAutoObservable } from 'mobx'; import { useParams, useSearchParams } from 'react-router-dom'; import { DialogMediator, DialogMediatorHandler, Dialogs, IDialogMediatorHandler, IDialogs, useDialogMediator } from './dialogs'; import { IViewModelDetached } from './IViewModelDetached'; import { ApplicationModelContext } from '@cratis/applications.react'; import { WellKnownBindings } from "./WellKnownBindings"; import { deepEqual } from '@cratis/applications'; import { IHandleParams } from 'IHandleParams'; import { IHandleQueryParams } from 'IHandleQueryParams'; import { IHandleProps } from 'IHandleProps'; import { ObservableQueryFor, QueryFor } from '@cratis/applications/queries'; import { Command } from '@cratis/applications/commands'; import { ICanBeConfigured } from '@cratis/applications/ICanBeConfigured'; import { DialogComponentsContext, DialogContextContent, IDialogComponents, useDialogContext } from '@cratis/applications.react/dialogs'; interface IViewModel extends IViewModelDetached { __childContainer: DependencyContainer; } function disposeViewModel(viewModel: IViewModel) { const vmWithDetach = (viewModel as IViewModelDetached); if (typeof (vmWithDetach.detached) == 'function') { vmWithDetach.detached(); } if (viewModel.__childContainer) { const container = viewModel.__childContainer as DependencyContainer; container.dispose(); } } function handleProps(viewModel: IViewModel, params: object) { const vmWithHandleParams = (viewModel as unknown as IHandleProps); if (typeof (vmWithHandleParams.handleProps) == 'function') { vmWithHandleParams.handleProps(params); } } function handleParams(viewModel: IViewModel, params: object) { const vmWithHandleParams = (viewModel as unknown as IHandleParams); if (typeof (vmWithHandleParams.handleParams) == 'function') { vmWithHandleParams.handleParams(params); } } function handleQueryParams(viewModel: IViewModel, queryParams: object) { const vmWithHandleParams = (viewModel as unknown as IHandleQueryParams); if (typeof (vmWithHandleParams.handleQueryParams) == 'function') { vmWithHandleParams.handleQueryParams(queryParams); } } /** * Represents the view context that is passed to the view. */ export interface IViewContext { viewModel: T, props: TProps, } /** * Use a view model with a component. * @param {Constructor} viewModelType View model type to use. * @param {FunctionComponent} targetComponent The target component to render. * @returns */ export function withViewModel( viewModelType: Constructor, targetComponent: FunctionComponent>) { const renderComponent = (props: TProps) => { const applicationContext = useContext(ApplicationModelContext); const dialogComponentsContext = useContext(DialogComponentsContext); const params = useParams(); const [currentProps, setCurrentProps] = useState(props); const [previousParams, setPreviousParams] = useState(params); const [queryParams] = useSearchParams(); const [previousQueryParams, setPreviousQueryParams] = useState(queryParams); const queryParamsObject = Object.fromEntries(queryParams.entries()); const dialogMediatorContext = useRef(null); const currentViewModel = useRef(null); const [, setInitialRender] = useState(true); const parentDialogMediator = useDialogMediator(); const dialogContext = useDialogContext(); useEffect(() => { if (currentViewModel.current !== null) { return () => { disposeViewModel(currentViewModel.current as IViewModel); }; } dialogMediatorContext.current = new DialogMediatorHandler(parentDialogMediator); const child = container.createChildContainer(); child.registerInstance(WellKnownBindings.props, props); child.registerInstance(WellKnownBindings.params, params); child.registerInstance(WellKnownBindings.queryParams, queryParamsObject); child.registerInstance(DialogContextContent, dialogContext as unknown); const originalResolve = child.resolve; child.resolve = function (type: Constructor) { // eslint-disable-next-line prefer-rest-params const instance = originalResolve.apply(child, arguments as never); if (type.prototype instanceof ObservableQueryFor || type.prototype instanceof QueryFor || type.prototype instanceof Command) { const query = instance as ICanBeConfigured; query.setMicroservice(applicationContext.microservice); query.setApiBasePath(applicationContext.apiBasePath ?? ''); query.setOrigin(applicationContext.origin ?? ''); } return instance; } as never; const dialogService = new Dialogs(dialogMediatorContext.current!, dialogComponentsContext); child.registerInstance(IDialogs as Constructor, dialogService); const viewModel = child.resolve(viewModelType) as IViewModel; makeAutoObservable(viewModel); viewModel.__childContainer = child; currentViewModel.current = viewModel as TViewModel; setInitialRender(false); handleProps(viewModel, props); handleParams(viewModel, params); handleQueryParams(viewModel, queryParamsObject); return () => { if (applicationContext.development === false) { disposeViewModel(viewModel); } }; }, []); if (currentViewModel.current === null) return null; if (!deepEqual(currentProps, props)) { setCurrentProps(props); handleProps(currentViewModel.current as IViewModel, props); } if (!deepEqual(params, previousParams)) { setPreviousParams(params); handleParams(currentViewModel.current as IViewModel, params); } if (!deepEqual(queryParams, previousQueryParams)) { setPreviousQueryParams(queryParams); handleQueryParams(currentViewModel.current as IViewModel, queryParams); } const component = () => targetComponent({ viewModel: currentViewModel.current!, props }) as ReactElement; return ( {component} ); }; return renderComponent; }