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 typelabel
(String): Display name shown in the "Add Section" dropdowngetContent
(Function): Function that returns HTML content to display in the sectionrootVar
(String): Root variable identifier for the add-onname
(String): Name of the add-onversion
(String): Version of the add-on
Section Methods and Properties
Core Section Methods
section.addButton(btnConfig, insert, elementPosition)
: Add custom buttons to section headerssection.addSubMenu(config)
: Add items to the section context menu (hamburger menu)section.getExpJournalID()
: Get the section's journal IDsection.getType()
: Get the section typesection.setContent(content)
: Set the section's HTML contentsection.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 databasedata.expJournalID
- The section's unique IDdata.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:
Updated 5 days ago