import * as React from "react"; import * as R from "ramda"; import { toBooleanWithDefaultFalse } from "@applicaster/zapp-react-native-utils/booleanUtils"; import { UIComponentContext, useUIComponentContext, } from "@applicaster/zapp-react-native-ui-components/Contexts/UIComponentContext"; import { Placeholder } from "./Placeholder"; import { useInitialLoading } from "./hooks"; type ReactComponent = React.ComponentType; type Options = { skipOnLoadFinished?: boolean; allowsEmptyDataSource?: boolean; }; type ZappUIComponentType = { Component: ReactComponent; ErrorComponent?: ReactComponent; LoadingComponent?: ReactComponent; options?: Options; }; type RenderComponentProps = any; const RenderComponent = ( Component: ReactComponent | undefined, args: RenderComponentProps ) => { const { component, parent } = args; const contextValue = React.useMemo( () => ({ ...component, parent }), [component, parent] ); if (Component) { return ( ); } return null; }; const isDataSourceEmpty = R.compose( R.isEmpty, R.pathOr({}, ["component", "data", "source"]) ); const isZappUIComponentType = ( component: ReactComponent | ZappUIComponentType ): component is ZappUIComponentType => { return !R.isNil(R.path(["Component"], component)); }; const extract = ( ComponentDeprecatedOrProps: ReactComponent | ZappUIComponentType, ErrorComponentDeprecated?: ReactComponent, LoadingComponentDeprecated?: ReactComponent, optionsDeprecated?: Options ) => { if (isZappUIComponentType(ComponentDeprecatedOrProps)) { return { Component: ComponentDeprecatedOrProps.Component, ErrorComponent: ComponentDeprecatedOrProps.ErrorComponent, LoadingComponent: ComponentDeprecatedOrProps.LoadingComponent, options: ComponentDeprecatedOrProps.options, }; } return { Component: ComponentDeprecatedOrProps, ErrorComponent: ErrorComponentDeprecated, LoadingComponent: LoadingComponentDeprecated, options: optionsDeprecated, }; }; /** * This Decorator is used to provide built-in standard behaviour for Zapp UI Components * It will automatically render the proper component depending on the status of * the zapp pipes request. * @param {Object} props * @param {Function} props.Component: React component to be decorated, * @param {Function} props.ErrorComponent: optional component to render when zapp pipes returns an error * @param {Function} props.LoadingComponent: optional component to render when zapp pipes data is loading * @param {Object} props.options * @param {Boolean} props.options.skipOnLoadFinished: skips invocation of onLoadFinished * @returns {Function} wrapped component */ export function ZappUIComponent( ComponentDeprecatedOrProps: ReactComponent | ZappUIComponentType, ErrorComponentDeprecated?: ReactComponent, LoadingComponentDeprecated?: ReactComponent, optionsDeprecated?: Options ) { const usingDeprecatedSignature = !isZappUIComponentType( ComponentDeprecatedOrProps ); if (usingDeprecatedSignature && __DEV__) { // eslint-disable-next-line no-console console.warn( `Deprecation Warning! ZappUIComponent call signature has changed from Quaternary to Unary object literal. New signature: {Component, ErrorComponent, LoadingComponent, options}` ); } const { Component, ErrorComponent, LoadingComponent, options } = extract( ComponentDeprecatedOrProps, ErrorComponentDeprecated, LoadingComponentDeprecated, optionsDeprecated ); return function WrappedWithZappComponent(props: ZappUIComponentProps) { const { zappPipesData, onLoadFinished, onLoadFailed, componentIndex } = props; const parent = useUIComponentContext(); const isLoading = toBooleanWithDefaultFalse(zappPipesData?.loading); const isInitialLoading = useInitialLoading(isLoading); const skipOnLoadFinished = toBooleanWithDefaultFalse( options?.skipOnLoadFinished ); const allowsEmptyDataSource = toBooleanWithDefaultFalse( options?.allowsEmptyDataSource ); const componentProps = React.useMemo( () => ({ ...props, parent, }), [props, parent] ); const hasError = !!zappPipesData?.error; React.useEffect(() => { if (!skipOnLoadFinished && !isLoading) { onLoadFinished(); } }, [isLoading]); React.useEffect(() => { if (hasError) { onLoadFailed?.({ error: zappPipesData.error, index: componentIndex, }); } }, [hasError, componentIndex, onLoadFailed, zappPipesData?.error]); if (isDataSourceEmpty(props) && allowsEmptyDataSource) { return RenderComponent(Component, componentProps); } if (isLoading && !zappPipesData?.data && isInitialLoading) { return RenderComponent(LoadingComponent || Placeholder, componentProps); } if (hasError) { return RenderComponent(ErrorComponent, componentProps); } return RenderComponent(Component, componentProps); }; }