The DynamicPopupModuleLoader is a crucial layout component responsible for dynamically loading and displaying other components (referred to as "modules") within modal popups or drawers. It centralizes the logic for handling popup states, dynamic imports, and communication between the triggering component and the loaded module.
This component listens for specific events published via the application's pub/sub system (usePubSub) and renders the requested module accordingly. It supports stacking multiple popups, although only the topmost one is interactive.
These popups can be displayed on any page within the application, allowing for a flexible and modular approach to UI interactions. The component is designed to be reusable and extensible, making it easy to add new modules in the future.
- Feedback Module:
- A feedback form that users can fill out, which can be triggered from various parts of the application.
- Hook:
useRaiseIssue
- File Viewer Module:
- A component that allows users to view files (e.g., PDFs, images) directly within the application.
- Hook:
useFileView
- Image Editor Module:
- A module for editing images, which can be triggered from different contexts.
- Hook:
useImageEditor
- Camera Module:
- A component that allows users to take pictures or scan documents using their device's camera.
- Hook:
useCamera
- Notifications Module: A module for displaying notifications or alerts to users, which can be triggered from various parts of the application.
- Hook:
useNotifications
- Hook:
- Dynamic Loading: Uses Next.js's
next/dynamicfor lazy loading of components, improving performance by only loading the necessary code when required. - Pub/Sub Communication: Integrates with the application's pub/sub system to decouple the triggering of popups from their implementation, allowing for a more modular architecture.
- Customizable: Supports passing options to the loaded module, allowing for tailored behavior and appearance.
- Result Handling: Can publish results back to the triggering component via a unique pub/sub topic, enabling two-way communication.
- Styling Options: Uses Chakra UI's
ModalandDrawercomponents for consistent styling and behavior, with options for customization. - Default Options: Provides default configurations for each module, including styling, title, and props, making it easy to standardize the appearance and behavior of popups across the application.
- Error Handling: Includes basic error handling for module loading failures, ensuring a graceful fallback in case of issues.
- Stacking Support: Allows multiple popups to be displayed, with the ability to close them in the reverse order of opening.
- Initialization: The
DynamicPopupModuleLoadercomponent is typically included once in the main application layout (e.g., withinLayout.tsx). It initializes and subscribes to theTOPICS.SHOW_DIALOG_FEATUREpub/sub topic upon mounting. - Subscription: It listens for messages published on the
TOPICS.SHOW_DIALOG_FEATUREtopic. - Triggering a Popup: the
useDynamicPopuphook can be used to trigger the popup and handle the result in a more streamlined way. It returns an object with ashowDialogmethod that can be called with the following parameters:feature: The name of the module to load (must be one of the keys inModuleNameTypeand configured inmoduleListandDefaultOptions).options(optional): An object containing props to pass directly to the dynamically loaded module component.resultTopic(optional): A unique pub/sub topic name (string). If provided, theDynamicPopupModuleLoaderwill publish the result/response from the closed module to this topic.- Or, each module can have its own specific hook (e.g.,
useRaiseIssue,useFileViewer) as a wrapper arounduseDynamicPopupto simplify the triggering process and ensure type safety.
- Dynamic Loading: Upon receiving a valid message, the component:
- Adds the module details to its internal state (
moduleData). - Uses
next/dynamicto lazy-load the specified module component (defined inmoduleList). A loading spinner (Loadingcomponent) is displayed while the module loads. - Retrieves default configurations (like
popupStyle,title,props, styling) for the module fromDefaultOptions.
- Adds the module details to its internal state (
- Rendering: It renders the loaded module inside a Chakra UI
ModalorDrawercomponent based on thepopupStyleconfiguration. Default styles and behaviors (like overlay, close button) are applied but can be customized viaDefaultOptions. - Interaction & Closing: The user interacts with the loaded module. The module itself should provide a way to close (e.g., a close button or completing an action). It calls the
onCloseprop passed down byDynamicPopupModuleLoader, optionally passing a result payload. - Result Publishing: When a popup is closed:
- The
onPopupClosehandler inDynamicPopupModuleLoaderis triggered. - If a
resultTopicwas provided when triggering the popup, theDynamicPopupModuleLoaderpublishes the result received from the module to that specific topic. - The module is removed from the
moduleDatastate, effectively closing the popup.
- The
import { usePubSub } from "contexts";
import { Button } from "components/Button"; // Assuming a custom Button component
import { TOPICS } from "constants/PubSubTopics"; // Assuming TOPICS are defined here
import { ModuleNameType } from "layout-components/DynamicPopupModuleLoader"; // Import the type
// Example component triggering the Feedback popup
const MyComponent = (): JSX.Element => {
const { publish, subscribe } = usePubSub();
const handleOpenFeedback = (): void => {
const resultTopic = `FEEDBACK_RESULT_${Date.now()}`; // Unique topic for this instance
// Subscribe to the result topic *before* publishing the show dialog event
const unsubscribe = subscribe(resultTopic, (result: any) => {
console.log("Feedback popup closed with result:", result);
// Handle the result (e.g., show a success message)
unsubscribe(); // Clean up the subscription
});
publish(TOPICS.SHOW_DIALOG_FEATURE, {
feature: "Feedback" as ModuleNameType, // Cast to ensure type safety if needed
options: { /* Optional props for Feedback component */ },
resultTopic: resultTopic,
});
};
return <Button onClick={handleOpenFeedback}>Open Feedback</Button>;
};
export default MyComponent;Managing the pub/sub logic directly (creating unique result topics, subscribing, unsubscribing) in every component that needs a popup can be repetitive and error-prone. Creating custom hooks abstracts this logic away.
1. Generic Hook (useDynamicPopup)
This hook encapsulates the core logic of triggering a popup and waiting for its result using Promises.
// helpers/useDynamicPopup.ts
import { usePubSub } from "contexts";
import { TOPICS } from "constants/PubSubTopics";
import { ModuleNameType } from "layout-components/DynamicPopupModuleLoader";
import { useCallback } from "react";
interface TriggerPopupOptions {
options?: object; // Props for the module
}
/**
* Generic hook to trigger any dynamic popup and receive its result via a Promise.
* @template TResult - The expected type of the result from the popup module.
* @returns A function to trigger a popup.
*/
export const useDynamicPopup = <TResult = any>(): ((
feature: ModuleNameType,
triggerOptions?: TriggerPopupOptions
) => Promise<TResult>) => {
const { publish, subscribe } = usePubSub();
const triggerPopup = useCallback(
(
feature: ModuleNameType,
triggerOptions?: TriggerPopupOptions
): Promise<TResult> => {
return new Promise((resolve) => {
const resultTopic = `${feature}_RESULT_${Date.now()}_${Math.random()}`; // Unique topic
const unsubscribe = subscribe(resultTopic, (result: TResult) => {
console.log(
`Received result for ${feature} on topic ${resultTopic}:`,
result
);
resolve(result ?? ({} as TResult)); // Resolve with result or empty object
unsubscribe(); // Clean up subscription
});
console.log(
`Publishing SHOW_DIALOG_FEATURE for ${feature}, resultTopic: ${resultTopic}`
);
publish(TOPICS.SHOW_DIALOG_FEATURE, {
feature: feature,
options: triggerOptions?.options ?? {},
resultTopic: resultTopic,
});
});
},
[publish, subscribe]
);
return triggerPopup;
};2. Specific Hooks (Example: useFeedbackPopup)
Create specific hooks for each module type for better type safety and developer experience.
// hooks/useFeedbackPopup.ts
import { useDynamicPopup } from "helpers/useDynamicPopup"; // Adjust path if needed
import { useCallback } from "react";
// Define expected props for the Feedback component if known
interface FeedbackOptions {
initialMessage?: string;
// ... other specific options for Feedback
}
// Define expected result type from the Feedback component if known
interface FeedbackResult {
success: boolean;
messageId?: string;
// ... other result properties
}
/**
* Hook to specifically trigger the Feedback popup.
* @returns A function to open the Feedback popup, optionally passing options.
*/
export const useFeedbackPopup = (): ((
options?: FeedbackOptions
) => Promise<FeedbackResult>) => {
const triggerPopup = useDynamicPopup<FeedbackResult>();
const openFeedbackPopup = useCallback(
(options?: FeedbackOptions): Promise<FeedbackResult> => {
return triggerPopup("Feedback", { options });
},
[triggerPopup]
);
return openFeedbackPopup;
};
// hooks/useFileViewerPopup.ts
import { useDynamicPopup } from "helpers/useDynamicPopup";
import { useCallback } from "react";
interface FileViewerOptions {
fileUrl: string;
fileName?: string;
// ... other specific options for FileViewer
}
// FileViewer might not return a specific result, or just a confirmation
interface FileViewerResult {
viewed?: boolean;
}
/**
* Hook to specifically trigger the FileViewer popup.
* @returns A function to open the FileViewer popup, passing required options.
*/
export const useFileViewerPopup = (): ((
options: FileViewerOptions
) => Promise<FileViewerResult>) => {
const triggerPopup = useDynamicPopup<FileViewerResult>();
const openFileViewerPopup = useCallback(
(options: FileViewerOptions): Promise<FileViewerResult> => {
// Add validation if needed: if (!options?.fileUrl) throw new Error("fileUrl is required");
return triggerPopup("FileViewer", { options });
},
[triggerPopup]
);
return openFileViewerPopup;
};
// --- Add similar hooks for ImageEditor, Camera, Notifications ---3. Using the Specific Hooks
import { Button } from "components/Button";
import { useFeedbackPopup } from "hooks/useFeedbackPopup"; // Adjust path
import { useFileViewerPopup } from "hooks/useFileViewerPopup"; // Adjust path
const MyComponentWithHooks = (): JSX.Element => {
const openFeedback = useFeedbackPopup();
const openFileViewer = useFileViewerPopup();
const handleOpenFeedback = async (): Promise<void> => {
try {
const result = await openFeedback({ initialMessage: "Hello!" });
console.log("Feedback Result:", result);
if (result?.success) {
// Show success toast
}
} catch (error) {
console.error("Error opening feedback popup:", error);
}
};
const handleOpenFile = async (): Promise<void> => {
try {
const result = await openFileViewer({ fileUrl: "/path/to/file.pdf", fileName: "My Document" });
console.log("File Viewer Result:", result);
} catch (error) {
console.error("Error opening file viewer popup:", error);
}
};
return (
<>
<Button onClick={handleOpenFeedback}>Open Feedback (Hook)</Button>
<Button onClick={handleOpenFile}>Open File (Hook)</Button>
</>
);
};
export default MyComponentWithHooks;- Create the Module Component: Create a new component for the module you want to add. Ensure it follows the expected structure and props. Example: RaiseIssueCard.tsx.
- Update
moduleList: Add the new module to themoduleListin DynamicPopupModuleLoader.tsx. Ensure the key matches thefeaturestring used when triggering the popup. - Update
DefaultOptions: Add default options for the new module inDefaultOptionsto ensure consistent styling and behavior. - Create a Custom Hook (Optional): If the module is complex or requires specific props, consider creating a custom hook (like useRaiseIssue) to simplify its usage.