Plugin development¶
Requirements for a plugin¶
As of version 7.0, plugins are JavaScript Classes. A plugin must implement:
- A
constructor()
that accepts an instance of jsPsych. - A
trial()
method that accepts anHTMLElement
as its first argument and anobject
of trial parameters as its second argument. There is an optional third argument to handle theon_load
event in certain cirumstances. Thetrial()
method should either invokejsPsych.finishTrial()
or should be anasync
function that returns a data object to end the trial and save data. - A static
info
property on the class that contains an object describing the plugin's parameters, data generated, and version.
Plugin templates¶
Templates for plugins are available in the jspsych-contrib repository. Plugins can be written in either plain JavaScript or in TypeScript.
To get started with a template, we recommend using the CLI tool that we have published in jspsych-contrib. This automates the setup of a new plugin in either JavaScript or TypeScript. Additional information about the CLI tool is available in the README
of jspsych-contrib.
Plugin components¶
constructor()¶
The plugin's constructor()
will be passed a reference to the instance of the JsPsych
class that is running the experiment. The constructor should store this reference so that the plugin can access functionality from the core library and its modules.
constructor(jsPsych){
this.jsPsych = jsPsych;
}
trial()¶
The plugin's trial()
method is responsible for running a single trial. When the jsPsych timeline reaches a trial using the plugin it will invoke the trial()
method for the plugin.
There are three parameters that are passed into the trial method.
display_element
is the DOM element where jsPsych content is being rendered. This parameter will be anHTMLElement
, and you can use it to modify the portion of the document that jsPsych controls.trial
is an object containing all of the parameters specified in the corresponding TimelineNode.on_load
is an optional parameter that contains a callback function to invoke whentrial()
has completed its initial loading. See handling the on_load event.
The only requirement for the trial
method is that it calls jsPsych.finishTrial()
when it is done. This is how jsPsych knows to advance to the next trial in the experiment (or end the experiment if it is the last trial). The plugin can do whatever it needs to do before that point.
static info¶
The plugin's info
property is an object with a name
, version
, parameters
, and data
property.
const info = {
name: 'my-awesome-plugin',
version: version,
parameters: { },
data: { }
}
The version
field describes the version of the plugin. The version will be automatically included in the data generated by the plugin. In most cases, the version should imported from the package.json
file by including an import statement at the top of the file. This allows the version
field be automatically updated.
import { version } from '../package.json';
const info = {
...
version: version;
...
}
Automatic versioning with custom build environments
If you are using a custom build environment that imports its own tsconfig.json
file that does not extend jsPsych's, and you want to use this automatic versioning syntax, you must add "resolveJsonModule": true
to the config's compilerOptions
object.
If you are not using a build environment that supports import
and package.json
(such as writing a plain JS file), you can manually enter the version
as a string.
const info = {
...
version: "1.0.0";
...
}
The parameters
property is an object containing all of the parameters for the plugin. Each parameter has a type
and default
property.
The data
field describes the types of data generated by the plugin. Each parameter has a type
property.
const info = {
name: 'my-awesome-plugin',
version: version,
parameters: {
image: {
type: ParameterType.IMAGE,
default: undefined
},
image_duration: {
type: ParameterType.INT,
default: 500
}
},
data: {
response: {
type: ParameterType.STRING,
},
},
}
If the default
value is undefined
then a user must specify a value for this parameter when creating a trial using the plugin on the timeline. If they do not, then an error will be generated and shown in the console. If a default
value is specified in info
then that value will be used by the plugin unless the user overrides it by specifying that property.
jsPsych allows most plugin parameters to be dynamic, which means that the parameter value can be a function that will be evaluated right before the trial starts. However, if you want your plugin to have a parameter that is a function that shouldn't be evaluated before the trial starts, then you should make sure that the parameter type is 'FUNCTION'
. This tells jsPsych not to evaluate the function as it normally does for dynamic parameters. See the canvas-*
plugins for examples.
We strongly encourage using JSDoc comments to document the parameters and data generated by the plugin, as shown below. We use these comments to automatically generate documentation for the plugins and to generate default descriptions of variables for experiment metadata.
const info = {
name: 'my-awesome-plugin',
version: version,
parameters: {
/** The path to the image file to display. */
image: {
type: ParameterType.IMAGE,
default: undefined
},
/** The duration to display the image in milliseconds. */
image_duration: {
type: ParameterType.INT,
default: 500
}
},
data: {
/** The text of the response generated by the participant. */
response: {
type: ParameterType.STRING,
},
},
}
The info
object must be a static
member of the class, as shown below.
const info = {
name: 'my-awesome-plugin',
version: version,
parameters: {
/** The path to the image file to display. */
image: {
type: ParameterType.IMAGE,
default: undefined
},
/** The duration to display the image in milliseconds. */
image_duration: {
type: ParameterType.INT,
default: 500
}
},
data: {
/** The text of the response generated by the participant. */
response: {
type: ParameterType.STRING,
},
},
}
class MyAwesomePlugin {
constructor(...)
trial(...)
}
MyAwesomePlugin.info = info;
Parameter Types¶
jsPsych currently has support for the following parameter types:
Type Name | Description | Example |
---|---|---|
BOOL | A simple truth value. | true or false |
STRING | A set of characters. | "Continue" |
INT | A value that supports whole numbers. | 12 |
FLOAT | A value that supports decimal numbers. | 5.55 |
FUNCTION | A Javascript function, tends to process multiple objects in an array from other parameters. | function(tries) { return "<p>You have " + tries + " tries left." } |
KEY | A single key, with support for function keys like arrows and spacebars. | "j" , "n" , "ArrowLeft" |
KEYS | Either an array of keys, or the string "ALL_KEYS" or "NO_KEYS" , indicating their respective inclusion/exclusion criterea. |
["f", "j"] |
SELECT | A list of strings that a developer can choose between as a parameter. | ["cm", "px", "em"] |
HTML_STRING | A string with HTML markup. | "<p>This is the prompt.</p>" |
IMAGE | A string that contains the path to an image file. | "my_image.jpg" |
AUDIO | A string that contains the path to an audio file. | "my_sound.mp3" |
VIDEO | A string that contains the path to a video file. | "my_video.mp4" |
OBJECT | A general JSON object (key-value pairs). | { rt: 350, response: "hello!", correct: true } |
COMPLEX | A JSON object that one can specify nested parameters for. | { rt: 350, response: "hello!", correct: true } |
TIMELINE | A jsPsych timeline object with trials. | [{ type: jsPsychKeyboardResponse, stimulus: 'my_image.jpg }] |
Within each parameter, you may also specify if it is an array of the specific type. For example, a parameter that requires a list of button labels would be described as:
const info = {
// ...
parameters: {
/** The labels to be displayed on each button. */
labels: {
type: ParameterType.STRING,
array: true,
default: ["Pause", "Play", "Continue"]
}
},
// ...
}
Specific parameter types also have their own special markup. For ParameterType.SELECT
, you specify the options one can choose with an options
field, and then the default
field must be within that field.
const info = {
// ...
parameters: {
/** The units of measure used to display the length and width of the stimulus. */
units: {
type: ParameterType.SELECT,
options: ["em", "px", "vh", "vw"],
default: "px"
}
},
// ...
}
For ParameterType.COMPLEX
, we may specify the underlying fields in the object with the nested
field. This acts in the same way as us defining parameters regularly, only we are now just delineating the fields within the object itself.
const info = {
// ...
parameters: {
/** Where to display the location of the stimuli. */
locations: {
type: ParameterType.COMPLEX,
array: true,
default: undefined,
nested: {
/** The x-coordinate of the stimulus, in the units from the `units` field. */
x: {
type: ParameterType.INT
},
/** The y-coordinate of the stimulus. */
y: {
type: ParameterType.INT
}
}
}
},
// ...
}
Plugin functionality¶
Inside the .trial()
method you can do pretty much anything that you want, including modifying the DOM, setting up event listeners, and making asynchronous requests. In this section we'll highlight a few common things that you might want to do as examples.
Changing the content of the display¶
There are a few ways to change the content of the display. The display_element
parameter of the trial method contains the HTMLElement
for displaying jsPsych content, so you can use various JavaScript methods for interaction with the display element. A common one is to change the innerHTML
. Here's an example of using innerHTML
to display an image specified in the trial
parameters.
trial(display_element, trial){
let html_content = `<img src="${trial.image}"></img>`;
display_element.innerHTML = html_content;
}
Waiting for specified durations¶
If you need to delay code execution for a fixed amount of time, we recommend using jsPsych's wrapper of the setTimeout()
function, jsPsych.pluginAPI.setTimeout()
. Any timeouts that are created using jsPsych's setTimeout()
will be automatically cleared when the trial ends, which prevents one plugin from interfering with the timing of another plugin.
In future versions we may replace the implementation of jsPsych.pluginAPI.setTimeout()
with improved timing functionality based on requestAnimationFrame
.
trial(display_element, trial){
// show image
display_element.innerHTML = `<img src="${trial.image}"></img>`;
// hide image after trial.image_duration milliseconds
this.jsPsych.pluginAPI.setTimeout(()=>{
display_element.innerHTML = '';
}, trial.image_duration);
}
Responding to keyboard events¶
While the plugin framework allows you to set up any events that you would like to, including normal handling of keyup
or keydown
events, the jsPsych.pluginAPI
module contains the getKeyboardResponse
function, which implements some additional helpful functionality for key responses in an experiment.
Here's a basic example. See the getKeyboardResponse
docs for additional examples.
trial(display_element, trial){
// show image
display_element.innerHTML = `<img src="${trial.image}"></img>`;
const after_key_response = (info) => {
// hide the image
display_element.innerHTML = '';
// record the response time as data
let data = {
rt: info.rt
}
// end the trial
this.jsPsych.finishTrial(data);
}
// set up a keyboard event to respond only to the spacebar
this.jsPsych.pluginAPI.getKeyboardResponse({
callback_function: after_key_response,
valid_responses: [' '],
persist: false
});
}
Asynchronous loading¶
One of the trial events is on_load
, which is normally triggered automatically when the .trial()
method returns. In most cases, this return happens after the plugin has done its initial setup of the DOM (e.g., rendering an image, setting up event listeners and timers, etc.). However, in some cases a plugin may implement an asynchronous operation that needs to complete before the initial loading of the plugin is considered done. An example of this is the audio-keyboard-response
plugin, in which the check to see if the audio file is loaded is asynchronous and the .trial()
method returns before the audio file has been initialized and the display updated.
If you would like to manually trigger the on_load
event for a plugin, the .trial()
method accepts an optional third parameter that is a callback function to invoke when loading is complete.
In order to tell jsPsych to not invoke the regular callback when the .trial()
method returns, you need to explicitly return a Promise
. As of version 8.0
, we recommend making the trial
function an async
function to handle this.
Here's a sketch of how the on_load
event can be utilized in a plugin. Note that this example is only a sketch and leaves out all the stuff that happens between loading and finishing the trial. See the source for the audio-keyboard-response
plugin for a complete exampe.
async trial(display_element, trial, on_load){
let trial_complete;
await do_something_asynchronous()
on_load();
await do_the_rest_of_the_trial();
return data_generated_by_the_trial;
}
Save data¶
To write data to jsPsych's data collection pass an object of data as the parameter to jsPsych.finishTrial()
.
constructor(jsPsych){
this.jsPsych = jsPsych;
}
trial(display_element, trial){
let data = {
correct: true,
rt: 350
}
this.jsPsych.finishTrial(data);
}
As of version 8.0
you may also return the data object from the trial()
method when the method is an async
function. This is equivalent to calling jsPsych.finishTrial(data)
.
constructor(jsPsych){
this.jsPsych = jsPsych;
}
async trial(display_element, trial){
let data = {
correct: true,
rt: 350
}
return data;
}
The data recorded will be that correct
is true
and that rt
is 350
. Additional data for the trial will also be collected automatically.
When a plugin finishes¶
When a plugin finishes, it should call jsPsych.finishTrial()
or return a data object if the trial()
method is an async
function. This is how jsPsych knows to advance to the next trial in the experiment (or end the experiment if it is the last trial).
As of version 8.0
, ending the trial will automatically clear the display element and automatically clear any timeouts that are still pending.
Simulation mode¶
Plugins can optionally support simulation modes.
To add simulation support, a plugin needs a simulate()
function that accepts four arguments
simulate(trial, simulation_mode, simulation_options, load_callback)
trial
: This is the same as thetrial
parameter passed to the plugin'strial()
method. It contains an object of the parameters for the trial.simulation_mode
: A string, either"data-only"
or"visual"
. This specifies which simulation mode is being requested. Plugins can optionally support"visual"
mode. If"visual"
mode is not supported, the plugin should default to"data-only"
mode when"visual"
mode is requested.simulation_options
: An object of simulation-specific options.load_callback
: A function handle to invoke when the simulation is ready to trigger theon_load
event for the trial. It is important to invoke this at the correct time during the simulation so that anyon_load
events in the experiment execute as expected.
Typically the flow for supporting simulation mode involves:
- Generating artificial data that is consistent with the
trial
parameters. - Merging that data with any data specified by the user in
simulation_options
. - Verifying that the final data object is still consistent with the
trial
parameters. For example, checking that RTs are not longer than the duration of the trial. - In
data-only
mode, calljsPsych.finishTrial()
with the artificial data. - In
visual
mode, invoke thetrial()
method of the plugin and then use the artificial data to trigger the appropriate events. There are a variety of methods in the Plugin API module to assist with things like simulating key presses and mouse clicks.
We plan to add a longer guide about simulation development in the future. For now, we recommend browsing the source code of plugins that support simulation mode to see how the flow described above is implemented.
Advice for writing plugins¶
If you are developing a plugin with the aim of including it in the main jsPsych repository we encourage you to follow the contribution guidelines.
We also recommend that you make your plugin as general as possible. Consider using parameters to give the user of the plugin as many options for customization as possible. For example, if you have any text that displays in the plugin including things like button labels, implement the text as a parameter. This allows users running experiments in other languages to replace text values as needed.