03_questionnaire_.md - ganeshramakrishnanbr/Jumbo_NoCodeBuilder GitHub Wiki

Welcome back! In the previous chapters, we explored the foundational building blocks of our Jumbo_NoCodeBuilder. We first learned about ControlType, which tells us what kind of building block something is (like a Text Box or a Tab). Then, in Control, we dove into the individual building blocks themselves – the Control instances, each with its own unique ID and specific settings (properties).

Now, imagine you've added several Controls to your design canvas: a Text Box for a name, a Checkbox for agreement, maybe a Tab control containing other things. These are all individual pieces. But how do they all come together to form a complete, single, usable form? How do we represent the entire form, with all its controls, settings, and structure, as one thing?

This is where the Questionnaire concept comes in.

What is a Questionnaire?

Think of the Questionnaire as the master blueprint or the main document for your entire form. It's not just one building block; it's the container that holds all the building blocks (Control objects) that make up your form.

It holds key information about the form as a whole, such as:

  • The overall title of the form (like "Customer Feedback Form").
  • Global style settings that apply to the whole form (like the default font or colors).
  • Most importantly, a list (or array) of all the top-level Control objects that are placed directly onto the main canvas area.

The Questionnaire is the central data structure that represents the complete form you are designing. When you save your work, you're essentially saving this Questionnaire object. When you open a saved form, you're loading a Questionnaire object.

Solving a Use Case: Saving and Loading Your Entire Form

Let's think about a common task: You've spent a lot of time arranging and configuring controls on your canvas, and now you want to save your progress so you can close the builder and come back to it later. How does the builder capture everything you've done?

It does this by saving the current Questionnaire object! This single object contains all the necessary information: the title, the global settings, and the entire structure of your form represented by the list of Controls (including controls nested inside containers, which are part of their parent control's properties).

When you load a form, the builder retrieves the saved Questionnaire object and uses the information within it, especially the list of controls, to reconstruct your form exactly as you left it.

Key Concepts of a Questionnaire

The Questionnaire is represented by an object that ties everything together. Here are its main parts:

  1. id: A unique identifier for the entire questionnaire. This is different from the id of individual Controls.
  2. title: The name of your form (e.g., "Order Details").
  3. controls: This is the heart of the Questionnaire. It's an array (a list) that holds all the Control objects that are placed directly onto the main design canvas. If you have container controls like Tabs or Accordions on the canvas, those container controls will be in this list, and they will contain their own children controls within their children or specific properties (like tabs or sections).
  4. styles: An object containing settings for the overall look and feel of the form.
  5. createdAt / updatedAt: Timestamps indicating when the questionnaire was created and last modified.

How it Works: Saving and Loading

Let's trace the process of saving and loading the questionnaire using the central state management system (QuestionnaireContext, which we'll explore in detail in the next chapter):

Scenario: Saving the Questionnaire

sequenceDiagram
    Participant U as User
    Participant UI as Designer UI (Canvas, etc.)
    Participant QC as QuestionnaireContext
    Participant Stor as Local Storage

    U->>UI: Clicks "Save" button
    UI->>QC: Calls saveQuestionnaire()
    Note over QC: Gets the current Questionnaire object from its state
    QC->>Stor: Calls localStorage.setItem('questionnaire', JSON.stringify(questionnaire object))
    Note over Stor: The full Questionnaire object (title, styles, controls array with all Control objects inside) is saved.
    Stor-->>QC: Confirmation/Success
    QC-->>UI: Notifies save complete
    UI-->>U: Shows "Saved!" message
Loading

When you hit save, the UI component asks the QuestionnaireContext to save. The context holds the complete Questionnaire object. It then takes this object and stores it (in this project's case, simplified to Local Storage for demonstration). Because the Questionnaire object's controls property contains the entire tree of Control objects, the entire form structure is captured.

Scenario: Loading the Questionnaire

sequenceDiagram
    Participant U as User
    Participant AppInit as Application Startup
    Participant Stor as Local Storage
    Participant QC as QuestionnaireContext
    Participant UI as Designer UI (Canvas, etc.)

    U->>AppInit: Starts the application
    AppInit->>QC: Initializes QuestionnaireContext, potentially calls loadQuestionnaire()
    QC->>Stor: Calls localStorage.getItem('questionnaire')
    Stor-->>QC: Returns saved JSON string (or null if none)
    QC->>QC: Parses JSON string back into a Questionnaire object
    Note over QC: Updates its internal state with the loaded Questionnaire object
    QC-->>UI: Components connected to context (like DesignCanvas) see state change
    UI->>UI: Reads questionnaire.controls from context
    UI->>UI: Renders all Control objects based on the controls list
    UI-->>U: Displays the loaded form on the canvas
Loading

When the application starts (or explicitly loads), the QuestionnaireContext attempts to load a saved questionnaire from storage. If found, it parses the JSON back into a Questionnaire object and updates its internal state. Components like the DesignCanvas, which read the questionnaire.controls from the context, automatically update to display the loaded form structure.

Looking at the Code

Let's see how the Questionnaire is defined and used in the code.

First, the definition of the Questionnaire object is found in src/types/index.ts:

// --- File: src/types/index.ts ---
// ... other interfaces and enums ...

import { Control } from "./"; // Need to import Control type itself

// This is the interface (blueprint) for the entire Questionnaire
export interface Questionnaire {
  id: string; // Unique ID for the questionnaire
  title: string; // The title of the form
  description?: string; // Optional description
  createdAt: Date;
  updatedAt: Date;
  controls: Control[]; // <--- An array holding ALL top-level Control objects!
  styles?: { // Optional styles for the whole form
    global?: Record<string, string>;
    customClasses?: Array<{
      name: string;
      styles: Record<string, string>;
    }>;
  };
  // ... potentially other form-level settings ...
}

// ... definitions for Control and ControlType are in the same file ...
export interface Control {
    id: string;
    type: ControlType;
    // ... other control properties ...
    children?: Control[]; // Controls inside containers
}

export enum ControlType {
    // ... types ...
}

This code defines the structure of the Questionnaire object. Notice the controls: Control[] property. This is where the main list of building blocks is stored. If you have a simple form with just a Text Box and a Checkbox placed directly on the canvas, the controls array would contain two items: one Control object with type: ControlType.TextBox and one Control object with type: ControlType.Checkbox. If you drag a Tab control onto the canvas, the controls array would contain one item: a Control object with type: ControlType.Tab (specifically, a TabControl which extends Control). This TabControl object would then have its own property (like tabs) that contains the list of controls inside its tabs.

Now, let's see how the QuestionnaireContext manages this object.

// --- 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'; // Used to generate unique IDs

// Define the default state for a new questionnaire
const defaultQuestionnaire: Questionnaire = {
  id: nanoid(), // Give it a unique ID
  title: 'New Questionnaire', // Default title
  createdAt: new Date(),
  updatedAt: new Date(),
  controls: [], // <--- Starts with an empty list of controls
  styles: { /* ... default styles ... */ },
};

// Create the context (this is what components will use)
const QuestionnaireContext = createContext<QuestionnaireContextType | undefined>(undefined);

export const QuestionnaireProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
  // The main state variable holds the current Questionnaire object
  const [questionnaire, setQuestionnaire] = useState<Questionnaire>(defaultQuestionnaire);
  // ... other state like selectedControlId, activeTabIndex ...

  // Function to add a new control to the top-level controls list
  const addControl = (controlData: Partial<Control>) => {
    const newControl = {
      id: nanoid(), // Generate a unique ID for the new control
      ...controlData, // Include type, label, etc.
    } as Control;

    setQuestionnaire((prev) => ({
      ...prev, // Keep existing questionnaire properties (id, title, styles, etc.)
      controls: [...prev.controls, newControl], // <--- Add the new control to the controls array
      updatedAt: new Date(), // Update timestamp
    }));

    // ... logic to select the new control ...
  };

  // Function to update a control (this function is more complex internally
  // because it needs to find the control potentially inside nested containers,
  // but from the outside, you just provide the control's ID and the updates)
  const updateControl = (id: string, updates: Partial<Control>) => {
      setQuestionnaire((prev) => {
          // updateControlInArray is a helper function that knows how to search
          // recursively through the controls array and its children
          const updatedControls = updateControlInArray(prev.controls, id, updates);
           return {
            ...prev,
            controls: updatedControls, // Update the main controls array
            updatedAt: new Date(),
          };
      });
  };

  // Function to delete a control (also handles finding nested controls)
  const deleteControl = (id: string) => {
      setQuestionnaire((prev) => {
        // deleteControlFromArray is a helper function
        const updatedControls = deleteControlFromArray(prev.controls, id);
        return {
          ...prev,
          controls: updatedControls, // Update the main controls array
          updatedAt: new Date(),
        };
      });
      // ... logic to clear selection if needed ...
  };

  // Function to save the questionnaire
  const saveQuestionnaire = async () => {
    try {
      // Convert the entire questionnaire object to a JSON string and save it
      localStorage.setItem('questionnaire', JSON.stringify(questionnaire));
      console.log("Questionnaire saved!");
    } catch (error) {
      console.error("Error saving questionnaire:", error);
    }
  };

  // Function to load the questionnaire
  const loadQuestionnaire = async (id: string) => {
    try {
      const saved = localStorage.getItem('questionnaire');
      if (saved) {
        // Parse the JSON string back into a JavaScript object (the Questionnaire)
        const loadedQuestionnaire: Questionnaire = JSON.parse(saved);
        // Update the state with the loaded questionnaire
        setQuestionnaire(loadedQuestionnaire);
        console.log("Questionnaire loaded!");
      }
    } catch (error) {
      console.error("Error loading questionnaire:", error);
    }
  };

  // ... moveControl and other functions ...

  // Provide the questionnaire state and functions to components
  return (
    <QuestionnaireContext.Provider value={{
      questionnaire,
      // ... other context values ...
      addControl,
      updateControl,
      deleteControl,
      moveControl,
      saveQuestionnaire,
      loadQuestionnaire,
      // ... other functions ...
    }}>
      {children}
    </QuestionnaireContext.Provider>
  );
};

// Hook to easily access the context in components
export const useQuestionnaire = () => {
  const context = useContext(QuestionnaireContext);
  if (!context) {
    throw new Error('useQuestionnaire must be used within a QuestionnaireProvider');
  }
  return context;
};

This code snippet shows how the QuestionnaireProvider component uses React's useState hook to keep track of the current questionnaire object. The addControl, updateControl, and deleteControl functions modify the controls array within this questionnaire object. The saveQuestionnaire and loadQuestionnaire functions handle converting the entire object to/from JSON for storage. Components like the DesignCanvas or the PropertiesPanel use the useQuestionnaire hook to get the current questionnaire object and access its controls array or update its properties (like the title).

Questionnaire Properties in a Nutshell

Here's a quick look at the core Questionnaire properties:

Property What it Represents Example Values/Use
id Unique ID for the entire form form_xyz_789, feedback_project_abc
title The display name of the form "Contact Information Form", "Survey Q1 2024"
controls Array of top-level Control objects [ { id: 'text1', type: 'textBox', ... }, { id: 'tab2', type: 'tab', tabs: [...] }, ... ]
styles Global appearance settings { global: { primaryColor: '#FF0000' } }
createdAt Timestamp when the form was created 2023-10-27T10:00:00Z
updatedAt Timestamp when the form was last modified 2023-10-27T11:30:00Z

Conclusion

In this chapter, we learned that the Questionnaire is the central data structure that represents your entire form design in Jumbo_NoCodeBuilder. It acts as a blueprint, holding global settings like the title and styles, and crucially, containing the master list (controls array) of all the top-level building blocks (Control objects) you've added to your canvas. Understanding the Questionnaire is key because it's the object that gets saved, loaded, and manipulated as you design your form.

Now that we know the importance of the Questionnaire object that holds everything, the next question is: how do all the different parts of the application (the canvas, the properties panel, the control palette) access and modify this single Questionnaire object? This is managed by a crucial system called the QuestionnaireContext.

Let's move on to the next chapter: QuestionnaireContext.

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