Custom Experiment Section

This document provides comprehensive documentation for creating custom experiment sections using the eLabSDK. Custom experiment sections allow you to extend the functionality of experiments with specialized content and interactions.

Overview

Custom experiment sections are powerful extensions that allow developers to create specialized section types with custom content, buttons, and functionality. They integrate seamlessly with the eLabJournal experiment interface and provide a rich development experience.

Key Components

eLabSDK.Experiment.Section.CustomSectionType

The main class for creating custom section types. It extends eLabSDK.Experiment.Section and provides the framework for registering and managing custom sections.

Constructor Parameters:

  • category (String): Category for the section type (e.g., 'data', 'text', 'inventory', 'files')
  • type (String): Unique identifier for the custom section type
  • label (String): Display name shown in the "Add Section" dropdown
  • getContent (Function): Function that returns HTML content to display in the section
  • rootVar (String): Root variable identifier for the add-on
  • name (String): Name of the add-on
  • version (String): Version of the add-on

Section Methods and Properties

Core Section Methods

  • section.addButton(btnConfig, insert, elementPosition): Add custom buttons to section headers
  • section.addSubMenu(config): Add items to the section context menu (hamburger menu)
  • section.getExpJournalID(): Get the section's journal ID
  • section.getType(): Get the section type
  • section.setContent(content): Set the section's HTML content
  • section.lock() / section.unlock(): Control section editing state

Button Configuration Object

{
    id: 'unique-button-id',
    applyClass: 'css-class',
    icon: 'fontawesome-icon',
    text: 'Button Text',
    color: '#hex-color',
    showViewMode: false, // Show in view mode or edit mode only
    action: function (sectionData) {
        // Button click handler
    }
}

SubMenu Configuration Object

{
    icon: 'fontawesome-icon',
    text: 'Menu Item Text',
    applyClass: 'css-class',
    action: function (sectionData) {
        // Menu item click handler
    }
}

Content Handling

Custom sections handle content through a simple lifecycle that manages how data is loaded from the database and saved back.

How getContent Works

The getContent function is called when your section loads and receives:

  • data.contents - Content from the database
  • data.expJournalID - The section's unique ID
  • data.customType - Your custom section type

What happens:

  • For new sections: data.contents is empty, getContent is called and your return value is displayed
  • For saved sections: data.contents contains saved HTML from database, which is displayed directly (getContent is NOT called)

Content Methods

setContent(htmlContent) - Immediately updates the section display without saving to database. Useful for real-time content updates, interactive UI changes, or showing temporary states. The content is only visual and will be lost on page refresh unless also saved.

saveHtmlContent(htmlContent, expJournalID) - Saves content to the section's database record. Use this to persist changes.

Save Button Behaviour: The standard experiment save action (when users click save buttons in the UI) automatically handles custom sections. When the system saves experiment content, custom sections are included in the save process and their content is persisted to the database. For additional save operations or custom save logic, sections can call saveHtmlContent explicitly.

Configuration Options

Core Configuration

Here's the complete structure for creating a custom section type. This serves as a template showing all available options and their typical usage:

new eLabSDK.Experiment.CustomSectionType({
    rootVar: 'MY_ADDON_ROOT',
    name: 'My Custom Section',
    category: 'Data',
    type: 'myCustomSectionType',
    label: 'My Custom Section',
    version: '1.0.0',
    
    // Content generation
    getContent: function (data, section) {
        // Return HTML content or Promise that resolves to HTML
    },
    
    // Optional: Menu items in section header
    menuItems: function (data) {
        // Return array of button configurations or Promise
    },
    
    // Optional: Context menu items
    subMenuItems: function (data) {
        // Return array of submenu configurations or Promise
    },
    
    // Optional: PDF export content
    getPdfContent: function (expJournalID) {
        // Return PDF-suitable HTML content
    },
    
    // Optional: Initial content for new sections
    getInitialHtmlContent: function (rootVar, isPdf, expJournalID) {
        // Return initial HTML content for new sections
    },
    
    // Optional: Section lifecycle hooks
    onSectionReady: function (section) {
        // Called when section is ready
    },
    
    onBeforeDelete: function () {
        // Called before section deletion, return false to prevent
    },
    
    // Optional: Custom add-on info
    customAddonInfoContent: function () {
        // Return custom info content for the info dialog
    },
    
    // Optional: Activation behavior
    activateMenuIndexOnClick: 0, // Index of menu item to click on activation
    activateMenuIndexOnClickCheckFn: function () {
        // Validation function before activation
    }
});

Advanced Features

Dynamic Content

Use getContent with eLabSDK.API.call for dynamic content loading:

getContent: function (data, section) {
    // Return a Promise for async content loading
    return new Promise((resolve, reject) => {
        eLabSDK.API.call({
            method: 'GET',
            path: `experiments/{experimentID}/sections/{expJournalID}/content`,
            pathParams: {
                experimentID: experimentID,
                expJournalID: expJournalID
            },
            onSuccess: function (xhr, status, response) {
                resolve(`<div>${response.contents}</div>`);
            },
            onError: function (xhr, status, response) {
                console.error('Failed to load content:', response);
                resolve('<div class="error">Failed to load content</div>');
            }
        });
    });
}

Button Management

Add buttons dynamically to section headers:

menuItems: function (data) {
    return Promise.resolve([
        {
            id: 'btnCustomAction',
            icon: 'cog',
            text: 'Settings',
            action: function (data) {
                // Handle button click
            }
        }
    ]);
}

Context Menu Items

Add custom context menu items:

subMenuItems: function (data) {
    return Promise.resolve([
        {
            icon: 'download',
            text: 'Export Data',
            action: function (data) {
                // Handle menu item click
            }
        }
    ]);
}

API Integration

This section demonstrates how to integrate with eLabNext's REST API from within custom experiment sections. These examples show proper usage of the eLabSDK.API methods with modern async/await patterns for common operations like file management, section information retrieval, and content operations.

Section File Management

// Get section files
async function getSectionFiles(expJournalID) {
    return new Promise((resolve, reject) => {
        eLabSDK.API.call({
            method: 'GET',
            path: 'experiments/sections/{expJournalID}/files',
            pathParams: {
                expJournalID: expJournalID
            },
            onSuccess: function (xhr, status, response) {
                resolve(response);
            },
            onError: function (xhr, status, response) {
                console.error('Failed to get section files:', response);
                reject(response);
            }
        });
    });
}

try {
    const files = await getSectionFiles(expJournalID);
    // Handle files
    console.log('Section files:', files);
} catch (error) {
    console.error('Error getting files:', error);
}

// Upload file to section
async function uploadFileToSection(expJournalID, fileBlob, fileName) {
    const formData = new FormData();
    formData.append('file', fileBlob);
    
    return new Promise((resolve, reject) => {
        eLabSDK.API.call({
            method: 'POST',
            path: 'experiments/sections/{expJournalID}/files',
            pathParams: {
                expJournalID: expJournalID
            },
            queryParams: {
                fileName: fileName
            },
            body: formData,
            onSuccess: function (xhr, status, response) {
                resolve(response);
            },
            onError: function (xhr, status, response) {
                console.error('Failed to upload file:', response);
                reject(response);
            }
        });
    });
}

try {
    const result = await uploadFileToSection(expJournalID, fileBlob, fileName);
    console.log('File uploaded successfully:', result);
} catch (error) {
    console.error('Error uploading file:', error);
}

Section Information

// Get custom section info
async function getCustomSectionInfo(expJournalID) {
    return new Promise((resolve, reject) => {
        eLabSDK.API.call({
            method: 'GET',
            path: 'experiments/sections/{expJournalID}/customSectionInfo',
            pathParams: {
                expJournalID: expJournalID
            },
            onSuccess: function (xhr, status, response) {
                resolve(response);
            },
            onError: function (xhr, status, response) {
                console.error('Failed to get section info:', response);
                reject(response);
            }
        });
    });
}

try {
    const info = await getCustomSectionInfo(expJournalID);
    // Access rootVar, version, name, sectionType
    console.log('Section info:', info);
} catch (error) {
    console.error('Error getting section info:', error);
}

Content Management

// Save HTML content to section
section.saveHtmlContent(htmlContent, expJournalID, experimentID)
    .then(() => {
        console.log('Content saved');
    });

Best Practices

1. Error Handling

Always implement proper error handling:

getContent: async function (data, section) {
    try {
        // Your logic here
        const response = await new Promise((resolve, reject) => {
            eLabSDK.API.call({
                method: 'GET',
                path: 'custom-data/{expJournalID}',
                pathParams: {
                    expJournalID: data.expJournalID
                },
                onSuccess: function (xhr, status, response) {
                    resolve(response);
                },
                onError: function (xhr, status, response) {
                    console.error('API call failed:', response);
                    reject(response);
                }
            });
        });
        
        return `<div class="custom-content">${response.html}</div>`;
    } catch (error) {
        console.error('Error loading content:', error);
        return '<div class="error">Failed to load content</div>';
    }
}

2. State Management

Check section and experiment state before actions:

action: async function (data) {
    try {
        // Check if other sections are open
        if (Experiment.Section.getActiveSections() > 0 && !Experiment.isSigned) {
            Experiment.Section.hasOpenSections();
            return;
        }
        
        // Check if experiment is signed
        if (Experiment.isSigned) {
            // Handle signed experiment
            console.log('Experiment is signed - read-only mode');
            return;
        }
        
        // Get current experiment state
        const experimentData = await new Promise((resolve, reject) => {
            eLabSDK.API.call({
                method: 'GET',
                path: 'experiments/{experimentID}',
                pathParams: {
                    experimentID: data.experimentID
                },
                onSuccess: function (xhr, status, response) {
                    resolve(response);
                },
                onError: function (xhr, status, response) {
                    console.error('Failed to get experiment data:', response);
                    reject(response);
                }
            });
        });
        
        // Proceed with action based on experiment state
        console.log('Experiment state:', experimentData.status);
    } catch (error) {
        console.error('Error checking experiment state:', error);
    }
}

3. Section Locking

Implement proper section locking for edit operations:

action: async function (data) {
    const section = data.section;
    
    const onUnlocked = async function (section) {
        try {
            Experiment.Section.activeSectionID = section.getExpJournalID();
            section.lock();
            
            // Perform edit operations with API calls
            const sectionData = await new Promise((resolve, reject) => {
                eLabSDK.API.call({
                    method: 'GET',
                    path: 'experiments/sections/{expJournalID}/data',
                    pathParams: {
                        expJournalID: section.getExpJournalID()
                    },
                    onSuccess: function (xhr, status, response) {
                        resolve(response);
                    },
                    onError: function (xhr, status, response) {
                        console.error('Failed to get section data:', response);
                        reject(response);
                    }
                });
            });
            
            // Update section content
            section.setContent(`<div class="editing">${sectionData.content}</div>`);
            
        } catch (error) {
            console.error('Error during section edit:', error);
            section.unlock(); // Ensure section is unlocked on error
        }
    };
    
    Experiment.Section.handleLockedSection(section, onUnlocked);
}

4. Button Toggle States

Implement toggle functionality for edit/save buttons:

{
    id: 'btnEditSave',
    text: 'Edit',
    icon: 'edit',
    action: async function (data) {
        try {
            // Edit action - switch to edit mode
            const section = data.section;
            section.lock();
            
            // Load editable content
            const editableContent = await new Promise((resolve, reject) => {
                eLabSDK.API.call({
                    method: 'GET',
                    path: 'experiments/sections/{expJournalID}/editableContent',
                    pathParams: {
                        expJournalID: data.expJournalID
                    },
                    onSuccess: function (xhr, status, response) {
                        resolve(response);
                    },
                    onError: function (xhr, status, response) {
                        console.error('Failed to load editable content:', response);
                        reject(response);
                    }
                });
            });
            
            section.setContent(`<div class="edit-mode">${editableContent.html}</div>`);
            
        } catch (error) {
            console.error('Error entering edit mode:', error);
        }
    },
    toggle: {
        icon: 'save',
        text: 'Save',
        color: 'orange',
        action: async function (data) {
            try {
                // Save action
                const section = data.section;
                const content = section.getContent();
                
                // Save content via API
                await new Promise((resolve, reject) => {
                    eLabSDK.API.call({
                        method: 'POST',
                        path: 'experiments/sections/{expJournalID}/content',
                        pathParams: {
                            expJournalID: data.expJournalID
                        },
                        body: {
                            content: content
                        },
                        onSuccess: function (xhr, status, response) {
                            resolve(response);
                        },
                        onError: function (xhr, status, response) {
                            console.error('Failed to save content:', response);
                            reject(response);
                        }
                    });
                });
                
                section.unlock();
                console.log('Content saved successfully');
                
            } catch (error) {
                console.error('Error saving content:', error);
            }
        }
    }
}

Examples and recipes

Basic Custom Section

new eLabSDK.Experiment.CustomSectionType({
    rootVar: 'BASIC_SECTION',
    name: 'Basic Section',
    category: 'Data',
    type: 'basicSection',
    label: 'Basic Custom Section',
    version: '1.0.0',
    
    getContent: function (data, section) {
        return `<div>Section ID: ${data.expJournalID}</div>`;
    }
});

Advanced Custom Section with Buttons

new eLabSDK.Experiment.CustomSectionType({
    rootVar: 'ADVANCED_SECTION',
    name: 'Advanced Section',
    category: 'Files',
    type: 'advancedSection',
    label: 'Advanced Custom Section',
    version: '1.0.0',
    
    getContent: async function (data, section) {
        try {
            const response = await new Promise((resolve, reject) => {
                eLabSDK.API.call({
                    method: 'GET',
                    path: 'section-data/{expJournalID}',
                    pathParams: {
                        expJournalID: data.expJournalID
                    },
                    onSuccess: function (xhr, status, response) {
                        resolve(response);
                    },
                    onError: function (xhr, status, response) {
                        console.error('Failed to load section data:', response);
                        reject(response);
                    }
                });
            });
            return `<div class="custom-content">${response.html}</div>`;
        } catch (error) {
            console.error('Error loading content:', error);
            return '<div class="error">Failed to load content</div>';
        }
    },
    
    menuItems: function (data) {
        return Promise.resolve([
            {
                id: 'btnUpload',
                icon: 'upload',
                text: 'Upload',
                color: '#28a745',
                action: function (sectionData) {
                    // Handle upload
                }
            },
            {
                id: 'btnProcess',
                icon: 'cog',
                text: 'Process',
                color: '#007bff',
                action: function (sectionData) {
                    // Handle processing
                }
            }
        ]);
    },
    
    subMenuItems: function (data) {
        return Promise.resolve([
            {
                icon: 'download',
                text: 'Export Data',
                action: function (sectionData) {
                    // Handle export
                }
            }
        ]);
    }
});

For step-by-step implementations, see the following recipes: