08_preview_tab_.md - ganeshramakrishnanbr/Jumbo_NoCodeBuilder GitHub Wiki

Welcome back, future No-Code Builder expert! So far, we've learned about the basic building blocks: the different ControlTypes available, the individual Control instances with their unique IDs and properties, how the entire form is held in a single Questionnaire object, how the QuestionnaireContext centrally manages this data, and how the Drag and Drop System lets you visually add and move controls.

You've used drag and drop to add a Text Box and a Checkbox to your form canvas. Great! But right now, they probably just say "New Text Box" and "New Checkbox". How do you change the label of the Text Box to "Your Name"? How do you make the Checkbox required? How do you add the list of choices to a Dropdown control?

You need a way to configure the specific details of each individual Control instance you've placed. This is where the Properties Panel comes in.

What is the Properties Panel?

Imagine each Control you place on your form is like an electronic gadget. When you want to change settings on your phone or your TV, you go into a "Settings" or "Properties" menu, right? The Properties Panel is exactly that for your selected Control in the Jumbo_NoCodeBuilder.

It's typically a sidebar area in the designer interface. When you click on a specific Control on your Design Canvas, the Properties Panel updates to show you all the settings you can change for that particular control instance.

The options you see in the panel aren't always the same. They change dynamically based on the ControlType of the control you've selected. A Text Box will have options like "Placeholder Text", while a Dropdown will have an "Options" list you can manage.

The Properties Panel allows you to:

  • Change common properties like the label (the text displayed next to the control).
  • Toggle settings like required (making the field mandatory) or visible (showing/hiding the control).
  • Configure type-specific settings that are stored in the properties field of the Control object (like adding options to a dropdown).

Any changes you make in the Properties Panel are immediately applied to the selected Control object within the central Questionnaire data managed by the QuestionnaireContext.

Solving a Use Case: Changing the Label of a Text Box

Let's walk through the process of changing the label of a Text Box you've added to the canvas, and see how the Properties Panel makes this possible.

You drag a Text Box from the Control Palette onto the Design Canvas. It appears with a default label like "New Text Box". You want to change this to "Your Name".

  1. You click on the "New Text Box" control on the Design Canvas.
  2. The system detects which Control instance you clicked (using its unique id).
  3. The Properties Panel updates to show the settings for this specific Text Box control.
  4. You see an input field labeled "Label" (or similar) in the Properties Panel, and it currently shows "New Text Box".
  5. You type "Your Name" into this input field.
  6. As you type (or when you finish editing), the Properties Panel tells the QuestionnaireContext to update the label property of that specific Control object you have selected.
  7. The QuestionnaireContext updates the data for the Text Box Control.
  8. The Design Canvas, which is displaying the control based on the data from the QuestionnaireContext, automatically re-renders and now shows the updated label "Your Name".

How it Works: Selecting a Control and Showing Properties

Let's trace the flow when you click a control on the canvas and its properties appear in the panel. This process heavily relies on the QuestionnaireContext.

sequenceDiagram
    Participant U as User
    Participant CC as CanvasControl (Clicked)
    Participant QC as QuestionnaireContext
    Participant PP as PropertiesPanel

    U->>CC: Clicks on a Control (e.g., Text Box)
    CC->>QC: Calls setSelectedControlId(control.id)
    Note over QC: Updates the internal state: selectedControlId = clicked_control_id
    QC-->>PP: Notifies PropertiesPanel that state changed
    PP->>QC: Reads the new selectedControlId from context
    PP->>QC: Reads the full Questionnaire object from context (which contains all Controls)
    PP->>PP: Searches the Questionnaire's controls list (and nested children) to find the Control object with the matching selectedControlId
    Note over PP: Now PP has the specific Control object (the Text Box) with all its properties (id, type, label, required, properties, etc.)
    PP->>PP: Determines which property fields to display based on the Control's type (e.g., sees type is Text Box)
    PP->>PP: Populates the property fields (e.g., sets Label field value to Control.label)
    PP->>U: PropertiesPanel UI displays the configuration form for the selected Text Box
Loading

This diagram shows how clicking a control triggers an update in the central QuestionnaireContext, which the PropertiesPanel is listening for. Once notified, the panel retrieves the necessary data (the selectedControlId and the full questionnaire data) from the context to find the exact Control object and display its settings.

Looking at the Code

The core of the Properties Panel logic resides in src/components/designer/properties/PropertiesPanel.tsx. It needs access to the selectedControlId and the questionnaire data from the QuestionnaireContext.

// --- File: src/components/designer/properties/PropertiesPanel.tsx ---
import React from 'react';
import { useQuestionnaire } from '../../../contexts/QuestionnaireContext'; // Get access to context
import { Control, ControlType } from '../../../types'; // Import Control and ControlType types
// Import components that render specific property forms
import CommonProperties from './CommonProperties';
import TabProperties from './TabProperties';
import TextBoxProperties from './TextBoxProperties';
import AccordionProperties from './AccordionProperties';
// ... import other property components ...

const PropertiesPanel: React.FC = () => {
  // Use the hook to get relevant state and functions from the context
  const { questionnaire, selectedControlId, setSelectedControlId, updateControl } = useQuestionnaire();

  // Helper function to get a flat list of all controls, including those nested in containers.
  // This is needed because selectedControlId could point to a control inside a Tab or Accordion.
  const flattenControls = (controls: Control[]): Control[] => {
    let result: Control[] = []; // Start with an empty list

    if (!Array.isArray(controls)) return result; // Handle cases where controls isn't an array

    for (const control of controls) {
      result.push(control); // Add the current control to the list

      // If it's a container control, recursively flatten its children
      if (control.type === ControlType.Tab) {
        const tabControl = control as any; // Cast to handle container specific properties
        tabControl.tabs?.forEach((tab: any) => { // Iterate through tabs
          if (Array.isArray(tab.controls)) {
            result = [...result, ...flattenControls(tab.controls)]; // Add flattened tab controls
          }
        });
      }

      if (control.type === ControlType.Accordion) {
         const accordionControl = control as any;
         accordionControl.sections?.forEach((section: any) => { // Iterate through sections
            if (Array.isArray(section.controls)) {
                result = [...result, ...flattenControls(section.controls)]; // Add flattened section controls
            }
         });
      }

      // Handle ColumnLayout similarly, iterating through columnControls
       if (control.type === ControlType.ColumnLayout) {
        const columnControl = control as any;
        if (Array.isArray(columnControl.columnControls)) {
          columnControl.columnControls.forEach((columnControlArray: any) => { // Iterate through column arrays
            if (Array.isArray(columnControlArray)) {
              result = [...result, ...flattenControls(columnControlArray)]; // Add flattened column controls
            }
          });
        }
      }
      // ... Add logic for other container types if necessary ...
    }

    return result; // Return the complete flat list
  };

  // Get a flat list of *all* controls in the questionnaire
  const allControls = flattenControls(questionnaire.controls);

  // Find the specific control object that is currently selected
  const selectedControl = allControls.find(c => c.id === selectedControlId);

  // This function is called by the property input fields whenever a value changes
  const updateSelectedControl = (updates: Partial<Control>) => {
    if (selectedControlId) {
      console.log('Updating control:', selectedControlId, 'with:', updates);
      // Call the updateControl function from QuestionnaireContext
      // This updates the Control object in the central state
      updateControl(selectedControlId, updates);
    }
  };

  // This function decides which set of property forms to display
  const renderPropertiesForm = () => {
    // If no control is selected, show a message
    if (!selectedControl) {
      return (
        <div className="text-gray-500 text-center p-4">
          Select a control to edit its properties
        </div>
      );
    }

    // Filter out the selected control itself, so it can't be selected as a parent for 'move to' logic in CommonProperties
    const availableControls = allControls.filter(c => c.id !== selectedControl.id);

    // Now, based on the type of the selected control, render the appropriate components
    return (
      <div className="space-y-6">
        {/* Show properties common to most controls (Label, Required, etc.) */}
        {/* The CommonProperties component reads selectedControl.label, selectedControl.required etc. */}
        {/* When you change the label, CommonProperties calls updateSelectedControl({ label: 'New Value' }) */}
        <CommonProperties
          control={selectedControl}
          onChange={updateSelectedControl}
          availableControls={availableControls}
        />

        {/* IF the selected control is a Tab, ONLY show Tab-specific properties */}
        {selectedControl.type === ControlType.Tab && (
          <TabProperties
            control={selectedControl}
            onChange={updateSelectedControl}
          />
        )}

        {/* IF the selected control is a ColumnLayout, ONLY show ColumnLayout-specific properties */}
         {selectedControl.type === ControlType.ColumnLayout && (
          <ColumnLayoutProperties
            control={selectedControl}
            onChange={updateSelectedControl}
          />
        )}

        {/* IF the selected control is one of these types, show TextBox-like properties */}
        {(selectedControl.type === ControlType.TextBox ||
          selectedControl.type === ControlType.Checkbox || // Checkbox properties might be similar to TextBox in some ways, or share a component
          selectedControl.type === ControlType.RadioButton ||
          selectedControl.type === ControlType.Dropdown) && (
          <TextBoxProperties // Note: In a real app, Checkbox, Radio, Dropdown might have their *own* dedicated components like CheckboxProperties, etc.
            control={selectedControl}
            onChange={updateSelectedControl}
          />
        )}

        {/* IF the selected control is Numeric or ToggleSlider, show Numeric properties */}
        {(selectedControl.type === ControlType.Numeric ||
          selectedControl.type === ControlType.ToggleSlider) && (
          <NumericProperties
            control={selectedControl}
            onChange={updateSelectedControl}
          />
        )}

        {/* IF selected control is Address */}
         {selectedControl.type === ControlType.Address && (
          <AddressProperties
            control={selectedControl}
            onChange={updateSelectedControl}
          />
        )}

        {/* IF selected control is ProtectedNumber */}
         {selectedControl.type === ControlType.ProtectedNumber && (
          <ProtectedNumberProperties
            control={selectedControl}
            onChange={updateSelectedControl}
          />
        )}

        {/* IF selected control is Accordion */}
         {selectedControl.type === ControlType.Accordion && (
          <AccordionProperties
            control={selectedControl}
            onChange={updateSelectedControl}
          />
        )}
        {/* ... add checks and components for other ControlTypes ... */}
      </div>
    );
  };

  return (
    <div className="h-full overflow-auto p-4">
      {/* A dropdown is also provided to select controls, useful for nested controls */}
      <div className="mb-4 border-b pb-4">
        <label className="block text-sm font-medium text-gray-700 mb-1">
          Selected Control
        </label>
        <select
          value={selectedControlId || ''}
          onChange={(e) => setSelectedControlId(e.target.value || null)}
          className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
        >
          <option value="">Select a control</option>
          {/* Populate the dropdown with all controls found by flattenControls */}
          {allControls.map((control) => (
            <option key={control.id} value={control.id}>
              {/* Show label or type in the dropdown */}
              {control.label || control.type} 
            </option>
          ))}
        </select>
      </div>

      {/* Render the appropriate property forms */}
      {renderPropertiesForm()}
    </div>
  );
};

export default PropertiesPanel;

This code snippet shows the main parts of the PropertiesPanel. It gets the questionnaire data and selectedControlId from useQuestionnaire. The flattenControls helper function is important because controls can be nested inside containers (Container Controls). This function creates a single list of all controls in the questionnaire, regardless of nesting level, so the panel can find any selected control by its ID.

Once the selectedControl object is found, the renderPropertiesForm function uses if conditions (or sometimes a switch statement, as seen in previous chapters) to check selectedControl.type. Based on the type (e.g., ControlType.Tab, ControlType.TextBox), it renders the appropriate specific property component (TabProperties, TextBoxProperties, etc.).

These specific property components (like TextBoxProperties) receive the selectedControl object and the updateSelectedControl function as props. They display input fields for properties relevant to their type (like placeholder for Text Box, which is read from selectedControl.properties.placeholder). When a value in one of these input fields changes, the specific property component calls the updateSelectedControl function, passing the ID of the control being edited and the new value(s). updateSelectedControl then calls the updateControl function from the QuestionnaireContext, completing the cycle and updating the central data.

The DesignTab.tsx component simply places the PropertiesPanel alongside the ControlPalette and DesignCanvas:

// --- File: src/components/designer/tabs/DesignTab.tsx ---
import React from 'react';
import ControlPalette from '../controls/ControlPalette';
import DesignCanvas from '../canvas/DesignCanvas';
import PropertiesPanel from '../properties/PropertiesPanel';
import { DragDropProvider } from '../../../contexts/DragDropContext'; // Needed for drag/drop

const DesignTab: React.FC = () => {
  return (
    // DragDropProvider wraps the whole designer area
    <DragDropProvider> 
      <div className="h-full flex">
        {/* Control Palette */}
        <div className="w-64 border-r p-4 bg-white shadow-sm overflow-auto">
          <h2 className="text-lg font-medium text-gray-900 mb-4">Controls</h2>
          <ControlPalette />
        </div>
        {/* Design Canvas */}
        <div className="flex-1 p-4 bg-gray-50 overflow-auto">
          <DesignCanvas />
        </div>
        {/* Properties Panel */}
        <div className="w-72 border-l p-4 bg-white shadow-sm overflow-auto">
          <h2 className="text-lg font-medium text-gray-900 mb-4">Properties</h2>
          <PropertiesPanel /> {/* The PropertiesPanel component is rendered here */}
        </div>
      </div>
    </DragDropProvider>
  );
};

export default DesignTab;

This code shows that the PropertiesPanel is just another component rendered within the main designer layout, sitting in its designated sidebar area. It gets all the data it needs via the useQuestionnaire hook provided by the QuestionnaireContext, which typically wraps the entire designer area higher up in the application structure (though not shown directly in this snippet, you saw the QuestionnaireProvider in Chapter 4).

Properties Panel in a Nutshell

Aspect What it Does Key Interactions
Purpose Customize settings for a single, selected Control. User selects a Control on the canvas or in the dropdown.
Input The selectedControlId from QuestionnaireContext. Uses ID to find the specific Control object in questionnaire.controls (or nested).
Dynamic Content Displays different forms/fields based on the Control's type. Checks selectedControl.type to conditionally render components like TextBoxProperties.
Output/Action Updates properties of the selected Control. Calls updateControl(selectedControl.id, { property: newValue }) on QuestionnaireContext.
Key Data Reads/writes label, required, visible, and fields within properties of the selected Control. Directly interacts with the specific Control object found.

Conclusion

In this chapter, we explored the Properties Panel, the essential tool for customizing your building blocks after placing them on the canvas. We learned that it's a dynamic area that changes based on the selected Control's ControlType, displaying relevant options for common and type-specific settings. By interacting with the fields in the Properties Panel, you are directly updating the properties of the corresponding Control object within the central Questionnaire data managed by the QuestionnaireContext.

We've now covered adding, arranging (via drag and drop), and configuring individual controls. But what about controls that are meant to hold other controls? Things like Tabs, Accordions, or sections within a layout? These are special types of building blocks called Container Controls, and they are the subject of our next chapter.

Container Controls

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