04_questionnairecontext_.md - ganeshramakrishnanbr/Jumbo_NoCodeBuilder GitHub Wiki
Welcome back, future No-Code Builder expert! In our journey so far, we've learned about the basic building blocks: the different ControlTypes available, the individual Control instances with their unique IDs and properties, and how the entire form is represented by a single Questionnaire object containing all the controls.
Now, think about the different parts of our builder application. We have the area where you visually design the form (the Design Canvas), the panel where you change a control's settings (the Properties Panel), and the sidebar where you pick new controls (the Control Palette). All these different parts need to work with the same Questionnaire data. When you drag a control, the Design Canvas needs to tell the system to add it to the Questionnaire. When you click a control, the Design Canvas needs to tell the Properties Panel which control was selected, and the Properties Panel needs to read that specific control's details from the Questionnaire data to show you its settings.
How do all these separate parts of the application share and update the single source of truth – the Questionnaire object? This is where the QuestionnaireContext comes in.
Imagine the Questionnaire object is a complex blueprint for a large building project. Instead of every single worker having their own copy of the blueprint (which could easily lead to confusion and mistakes if one person makes a change that others don't see), everyone works from a single, shared copy located in a central office.
The QuestionnaireContext is like that central office in our application. It holds the one and only active Questionnaire
object that represents your form design. Any part of the application that needs to see or change the form's structure or data must go through the QuestionnaireContext.
It provides:
-
The current state: It gives any component access to the latest version of the
Questionnaire
object and related states like which control is currently selected. -
Functions to modify the state: It provides specific functions (like
addControl
,updateControl
,deleteControl
,moveControl
,setSelectedControlId
) that components must use to request changes to the questionnaire data. You don't directly change the data; you ask the context to change it for you.
This central management ensures that everyone is always looking at the same data and that changes are made in a controlled and consistent way.
Let's revisit the task of selecting a control on the Design Canvas to edit its properties in the Properties Panel. How does the QuestionnaireContext
make this happen smoothly?
When you click on a Control displayed on the Design Canvas, the CanvasControl
component that represents that specific control needs to signal to the entire application that it is now the selected control. Simultaneously, the PropertiesPanel
needs to know which control is selected so it can fetch its properties and display them.
The QuestionnaireContext
acts as the bridge:
- The clicked
CanvasControl
component calls a function provided by theQuestionnaireContext
to set theselectedControlId
to its own unique ID. - The
QuestionnaireContext
updates its internal state, recording the newselectedControlId
. - The
PropertiesPanel
component is configured to "listen" to changes in theQuestionnaireContext
's state. - When the
selectedControlId
state changes in the context, thePropertiesPanel
automatically re-renders. - During its re-rendering, the
PropertiesPanel
reads the newselectedControlId
from the context and also retrieves the fullQuestionnaire
object from the context. - Using the
selectedControlId
, thePropertiesPanel
finds the corresponding Control object within theQuestionnaire
'scontrols
list (or nested within container controls). - Finally, the
PropertiesPanel
displays the configuration options based on the properties of the selected Control object it just found.
Here's a simplified flow using a sequence diagram:
sequenceDiagram
Participant U as User
Participant CC as CanvasControl
Participant QC as QuestionnaireContext
Participant PP as PropertiesPanel
U->>CC: Clicks on a Control
CC->>QC: Calls setSelectedControlId(control.id)
Note over QC: Updates selectedControlId state
QC-->>PP: Notifies subscribed components (like PP) state changed
PP->>QC: Reads selectedControlId and Questionnaire state
PP->>PP: Finds the clicked Control object in Questionnaire
PP->>U: Displays selected Control's properties in UI
This diagram shows that the QuestionnaireContext
is the central point that the CanvasControl
updates and the PropertiesPanel
reads from.
In any React component that needs to interact with the questionnaire data (read it or change it), you'll use a custom hook called useQuestionnaire
. This hook gives you access to the state and functions managed by the context.
Here's how a component uses it:
// --- File: src/components/designer/canvas/DesignCanvas.tsx ---
// ... imports ...
import { useQuestionnaire } from '../../../contexts/QuestionnaireContext'; // <-- Import the hook
const DesignCanvas: React.FC = () => {
// Use the hook to get state and functions from the context
const {
questionnaire, // <-- The full Questionnaire object
selectedControlId, // <-- The ID of the currently selected control
addControl, // <-- Function to add a control
updateControl, // <-- Function to update a control
deleteControl, // <-- Function to delete a control
setSelectedControlId // <-- Function to set the selected control
// ... other functions like moveControl ...
} = useQuestionnaire();
// Now you can use these in your component logic:
const handleAddControl = (controlData: any) => {
// Call the context function to add a new control
addControl(controlData);
};
const handleSelectControl = (controlId: string) => {
// Call the context function to set the selected control
setSelectedControlId(controlId);
};
// ... rest of component logic using questionnaire, selectedControlId, etc. ...
};
This small snippet shows how easy it is for a component to get access to the shared questionnaire
state and call the provided functions (addControl
, setSelectedControlId
). The component doesn't need to know how the context manages the state or where the data is ultimately stored; it just uses the provided interface.
The QuestionnaireContext
itself is set up using React's Context API. It involves creating a context object and a Provider component that holds the state and provides the functions.
The core state management happens within the QuestionnaireProvider
component:
// --- File: src/contexts/QuestionnaireContext.tsx ---
import React, { createContext, useContext, useState, ReactNode } from 'react';
import { Control, Questionnaire, ControlType, /* ... other types ... */ } from '../types';
import { nanoid } from 'nanoid'; // For generating unique IDs
// Define the initial state for a new questionnaire
const defaultQuestionnaire: Questionnaire = {
id: nanoid(),
title: 'New Questionnaire',
createdAt: new Date(),
updatedAt: new Date(),
controls: [], // The array of controls starts empty
styles: { /* ... */ },
};
// Create the context object
const QuestionnaireContext = createContext<QuestionnaireContextType | undefined>(undefined);
// The Provider component that wraps the part of the app needing the context
export const QuestionnaireProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
// Use React's useState hook to hold the main questionnaire state
const [questionnaire, setQuestionnaire] = useState<Questionnaire>(defaultQuestionnaire);
// Also manage other states like the selected control ID
const [selectedControlId, setSelectedControlId] = useState<string | null>(null);
const [activeTabIndex, setActiveTabIndex] = useState(0); // Example: for Tab control
// --- Functions to modify the state ---
// Example: Add a new control
const addControl = (controlData: Partial<Control>) => {
const newControl = {
id: nanoid(), // Generate a unique ID
...controlData, // Use data passed from the component
} as Control; // Ensure it matches the Control type
// Use the state updater function to add the new control
setQuestionnaire((prev) => ({
...prev, // Keep all existing properties
controls: [...prev.controls, newControl], // Add new control to the array
updatedAt: new Date(), // Update timestamp
}));
// Usually, select the newly added control
setSelectedControlId(newControl.id);
};
// Example: Update an existing control's properties
// Note: The actual implementation needs to search through nested controls
const updateControl = (id: string, updates: Partial<Control>) => {
setQuestionnaire((prev) => {
// This is where helper logic finds and updates the correct control
// within the potentially nested 'controls' array.
const updatedControls = updateControlInArray(prev.controls, id, updates);
return {
...prev,
controls: updatedControls, // Use the array with the updated control
updatedAt: new Date(),
};
});
};
// Example: Delete a control
// Note: Also requires searching through nested controls
const deleteControl = (id: string) => {
setQuestionnaire((prev) => {
// Helper logic to find and remove the control recursively
const updatedControls = deleteControlFromArray(prev.controls, id);
return {
...prev,
controls: updatedControls,
updatedAt: new Date(),
};
});
// Deselect the control if it was the one deleted
if (selectedControlId === id) {
setSelectedControlId(null);
}
};
// Example: Setting the selected control ID (simple state update)
const handleSetSelectedControlId = (id: string | null) => {
console.log('[QuestionnaireContext] Setting selected control ID:', id); // Log for debugging
setSelectedControlId(id);
};
// ... moveControl, saveQuestionnaire, loadQuestionnaire implementations ...
// Provide the state and functions through the context value
return (
<QuestionnaireContext.Provider
value={{
questionnaire, // Provide the state object
selectedControlId, // Provide other state
activeTabIndex,
setActiveTabIndex,
setSelectedControlId: handleSetSelectedControlId, // Provide the state setter
addControl, // Provide the functions
updateControl,
deleteControl,
moveControl, // ... etc.
saveQuestionnaire,
loadQuestionnaire,
updateQuestionnaireControls, // Function for bulk updates (used in drag/drop)
}}
>
{children} {/* Render the components wrapped by the provider */}
</QuestionnaireContext.Provider>
);
};
// The hook for components to easily access the context
export const useQuestionnaire = () => {
const context = useContext(QuestionnaireContext);
if (!context) {
// Throw an error if the hook is used outside the provider
throw new Error('useQuestionnaire must be used within a QuestionnaireProvider');
}
return context;
};
This code shows that the QuestionnaireProvider
component is responsible for holding the main questionnaire
state using useState
. It defines functions like addControl
, updateControl
, and deleteControl
that contain the logic for modifying the questionnaire
state. Crucially, these functions call setQuestionnaire
, which triggers React to re-render components that are using the context and display the updated data. The useQuestionnaire
hook is a simple way for any child component to get access to the value
object passed to the Provider
.
Notice the functions updateControlInArray
and deleteControlFromArray
. Because controls can be nested inside containers (Container Controls), these helper functions are needed to recursively search through the controls
array and any nested children
arrays (or specific properties like tabs
in a Tab control) to find the control by its ID and apply the update or remove it. The complexity of searching the tree is hidden inside the context; components using updateControl
or deleteControl
only need to provide the control's ID and the changes.
Aspect | What it Provides | Why it's Important |
---|---|---|
State | The current Questionnaire object, selectedControlId , etc. |
The single source of truth for form data. |
Functions |
addControl , updateControl , deleteControl , moveControl , setSelectedControlId , saveQuestionnaire , loadQuestionnaire , etc. |
The only authorized way for components to change the form data, ensuring consistency. |
Hook (useQuestionnaire ) |
Easy access to state and functions for any component. | Simplifies connecting components to the central data. |
Provider (QuestionnaireProvider ) |
Wraps the application, making the context available to its children. | Sets up the central "shared workspace". |
In this chapter, we explored the QuestionnaireContext, the central hub that manages the entire Questionnaire object and related states like the selected control. We learned that components don't directly manipulate the questionnaire data but instead use the functions provided by the context (addControl
, updateControl
, etc.) to request changes. This pattern ensures that all parts of the application are working with the same, up-to-date form blueprint, making the builder predictable and maintainable.
Now that we understand the core data structure (Questionnaire
and Control
) and the central place that manages it (QuestionnaireContext
), we can look at how user interactions, particularly dragging and dropping, translate into changes managed by this context.
Let's move on to the next chapter: Drag and Drop System.