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 Control
s 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.
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.
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.
The Questionnaire
is represented by an object that ties everything together. Here are its main parts:
-
id
: A unique identifier for the entire questionnaire. This is different from theid
of individual Controls. -
title
: The name of your form (e.g., "Order Details"). -
controls
: This is the heart of theQuestionnaire
. 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 theirchildren
or specific properties (liketabs
orsections
). -
styles
: An object containing settings for the overall look and feel of the form. -
createdAt
/updatedAt
: Timestamps indicating when the questionnaire was created and last modified.
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
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
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.
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
).
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 |
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.