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.

What is QuestionnaireContext?

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:

  1. 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.
  2. 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.

Solving a Use Case: Selecting a Control on the Canvas and Showing its Properties

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:

  1. The clicked CanvasControl component calls a function provided by the QuestionnaireContext to set the selectedControlId to its own unique ID.
  2. The QuestionnaireContext updates its internal state, recording the new selectedControlId.
  3. The PropertiesPanel component is configured to "listen" to changes in the QuestionnaireContext's state.
  4. When the selectedControlId state changes in the context, the PropertiesPanel automatically re-renders.
  5. During its re-rendering, the PropertiesPanel reads the new selectedControlId from the context and also retrieves the full Questionnaire object from the context.
  6. Using the selectedControlId, the PropertiesPanel finds the corresponding Control object within the Questionnaire's controls list (or nested within container controls).
  7. 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
Loading

This diagram shows that the QuestionnaireContext is the central point that the CanvasControl updates and the PropertiesPanel reads from.

How to Use QuestionnaireContext

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.

Looking at the Code (Internal Implementation)

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.

QuestionnaireContext in a Nutshell

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".

Conclusion

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.

⚠️ **GitHub.com Fallback** ⚠️