Skip to main content

lab.js Integration Guide

This guide covers how to integrate slopit with lab.js experiments.

Overview

The @slopit/adapter-labjs package provides:

  • withSlopit(): A mixin function that adds behavioral capture to any lab.js component
  • LabjsRecorder: A session recorder for managing experiment-wide data collection

lab.js is a browser-based experiment builder for behavioral research. The slopit adapter hooks into lab.js component lifecycle events to capture keystroke and focus data during text entry tasks.

Prerequisites

  • A working lab.js experiment
  • Node.js 18+ for bundled builds, or CDN access for direct browser use
  • Basic familiarity with lab.js component structure

Installation

Using a Package Manager

pnpm add @slopit/adapter-labjs

Using CDN (for lab.js Builder)

For experiments built in the lab.js Builder, add slopit via the HTML header:

<script src="https://unpkg.com/@slopit/adapter-labjs/dist/index.umd.js"></script>

Basic Usage

Using the withSlopit Mixin

The withSlopit() function wraps any lab.js component class to add behavioral capture. The enhanced component automatically starts capture when the component runs and collects data when it ends.

// Import the mixin
import { withSlopit } from "@slopit/adapter-labjs";

// Create enhanced component class
const SlopitScreen = withSlopit(lab.html.Screen);

// Use the enhanced component
const textEntryScreen = new SlopitScreen({
content: `
<main>
<p>Please describe your morning routine:</p>
<textarea id="response" rows="6"></textarea>
</main>
`,
slopit: {
targetSelector: "#response",
keystroke: { enabled: true },
paste: { enabled: true },
},
});

Using the LabjsRecorder

For session-level management across multiple components, use LabjsRecorder:

import { LabjsRecorder } from "@slopit/adapter-labjs";

const recorder = new LabjsRecorder({
participantId: "P001",
studyId: "my-study",
});

// Start a trial manually
recorder.startTrial({
trialType: "text-entry",
targetElement: document.querySelector("textarea"),
});

// When the trial ends
recorder.endTrial({
type: "text",
value: "participant response",
});

// Export session data
const session = recorder.exportSession();

Complete Example

// Full experiment with slopit integration
import { withSlopit, LabjsRecorder } from "@slopit/adapter-labjs";

// Create enhanced component classes
const SlopitScreen = withSlopit(lab.html.Screen);

// Initialize session recorder
const recorder = new LabjsRecorder({
participantId: new URLSearchParams(window.location.search).get("PROLIFIC_PID"),
studyId: "writing-study-001",
});

// Build the experiment
const experiment = new lab.flow.Sequence({
content: [
// Instructions
new lab.html.Screen({
content: `
<main>
<h1>Writing Study</h1>
<p>You will complete 3 short writing tasks.</p>
<p>Press any key to begin.</p>
</main>
`,
responses: { keypress: "continue" },
}),

// Task 1: Text entry with behavioral capture
new SlopitScreen({
content: `
<main>
<h2>Task 1 of 3</h2>
<p>Describe your typical morning routine.</p>
<textarea id="response" rows="6" style="width: 100%;"></textarea>
<button id="submit">Continue</button>
</main>
`,
responses: { "click #submit": "submit" },
slopit: {
targetSelector: "#response",
keystroke: { enabled: true },
paste: { enabled: true, prevent: true },
focus: { enabled: true },
},
}),

// Task 2
new SlopitScreen({
content: `
<main>
<h2>Task 2 of 3</h2>
<p>Explain how to prepare your favorite meal.</p>
<textarea id="response" rows="6" style="width: 100%;"></textarea>
<button id="submit">Continue</button>
</main>
`,
responses: { "click #submit": "submit" },
slopit: {
targetSelector: "#response",
keystroke: { enabled: true },
paste: { enabled: true, prevent: true },
},
}),

// Task 3
new SlopitScreen({
content: `
<main>
<h2>Task 3 of 3</h2>
<p>Describe a memorable travel experience.</p>
<textarea id="response" rows="6" style="width: 100%;"></textarea>
<button id="submit">Continue</button>
</main>
`,
responses: { "click #submit": "submit" },
slopit: {
targetSelector: "#response",
keystroke: { enabled: true },
paste: { enabled: true, prevent: true },
},
}),

// Thank you
new lab.html.Screen({
content: `
<main>
<h2>Thank You!</h2>
<p>Your responses have been recorded.</p>
</main>
`,
}),
],
});

// Handle experiment end
experiment.on("end", async () => {
// Collect all slopit data from trials
const allData = experiment.options.datastore.data;

// Send data to server
try {
await fetch("/api/sessions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
session: recorder.exportSession(),
trials: allData,
}),
});
} catch (error) {
console.error("Failed to submit data:", error);
}
});

// Run the experiment
experiment.run();

Configuration Options

Slopit Configuration

The slopit property on enhanced components accepts these options:

OptionTypeDefaultDescription
enabledbooleantrueEnable or disable capture
targetSelectorstring"textarea, input[type='text']"CSS selector for target element
containerSelectorstring".lab-content"Container to search within
keystrokeobject{ enabled: false }Keystroke capture settings
focusobject{ enabled: true }Focus/blur capture settings
pasteobject{ enabled: false }Paste event capture settings
mouseobject{ enabled: false }Mouse event capture settings
scrollobject{ enabled: false }Scroll event capture settings

Keystroke Options

{
keystroke: {
enabled: true,
captureKeyUp: true, // Include keyup events
includeModifiers: true, // Track Shift/Ctrl/Alt/Meta
}
}

Paste Options

{
paste: {
enabled: true,
prevent: false, // Set true to block pasting
capturePreview: true, // Store preview of pasted content
previewLength: 100, // Characters to capture in preview
warnMessage: "Please type your response manually.",
}
}

Focus Options

{
focus: {
enabled: true,
useVisibilityAPI: true, // Track tab/window visibility
useBlurFocus: true, // Track element focus/blur
}
}

Accessing Trial Data

Within Component Handlers

Access slopit data in component event handlers:

const screen = new SlopitScreen({
content: "...",
slopit: { ... },
});

screen.on("end", () => {
// Access slopit data from the component's data store
const slopitData = screen.data.slopit;

if (slopitData) {
console.log("Keystrokes:", slopitData.behavioral.keystrokes.length);
console.log("Flags:", slopitData.flags);
}
});

Using getSlopitData()

Enhanced components provide a getSlopitData() method:

const screen = new SlopitScreen({ ... });

// After the component ends
const data = screen.getSlopitData();
if (data !== null) {
console.log("Behavioral metrics:", data.behavioral.metrics);
}

Data Export

Exporting with lab.js Datastore

lab.js stores trial data in a datastore. Slopit data is automatically added to each trial's data object:

experiment.on("end", () => {
const datastore = experiment.options.datastore;

// Export as CSV (slopit data will be JSON-stringified in the column)
const csv = datastore.exportCsv();

// Export as JSON
const json = datastore.exportJson();

// Access raw data array
const trials = datastore.data;
trials.forEach((trial) => {
if (trial.slopit) {
console.log("Trial slopit data:", trial.slopit);
}
});
});

Sending to Server

experiment.on("end", async () => {
const datastore = experiment.options.datastore;

try {
const response = await fetch("/api/sessions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
participantId: urlParams.get("PROLIFIC_PID"),
studyId: "my-study",
trials: datastore.data,
}),
});

if (response.ok) {
// Redirect to completion
window.location.href = completionUrl;
}
} catch (error) {
console.error("Submission failed:", error);
}
});

Troubleshooting

Slopit Data is Missing

Verify that the slopit configuration is present on the component:

// Check that slopit is configured
const screen = new SlopitScreen({
content: "...",
slopit: {
targetSelector: "#response", // Must match an element
keystroke: { enabled: true }, // Must enable at least one capture type
},
});

Target Element Not Found

The adapter searches for elements using the targetSelector within the containerSelector. If your element has a different container:

{
slopit: {
containerSelector: "#my-custom-container",
targetSelector: "textarea",
}
}

Capture Not Starting

Ensure the withSlopit mixin is applied correctly:

// Correct: Apply mixin to component class
const SlopitScreen = withSlopit(lab.html.Screen);
const screen = new SlopitScreen({ ... });

// Wrong: Using original class
const screen = new lab.html.Screen({ ... }); // No slopit support

Dynamic Elements

If your target element is created after the component runs (e.g., via JavaScript), the adapter uses a MutationObserver with a 5-second timeout. For longer delays, consider using the LabjsRecorder directly:

const screen = new lab.html.Screen({
content: "...",
});

screen.on("run", () => {
// Create element dynamically
const textarea = document.createElement("textarea");
document.querySelector(".lab-content").appendChild(textarea);

// Start capture manually
recorder.startTrial({
trialId: "dynamic-trial",
targetElement: textarea,
});
});

screen.on("end", () => {
const response = document.querySelector("textarea").value;
recorder.endTrial({ type: "text", value: response });
});