From dca26f16ad493bcbde57dd356594946c630f9eae Mon Sep 17 00:00:00 2001 From: Josh de Leeuw Date: Thu, 11 Jul 2024 21:38:08 -0400 Subject: [PATCH] update migration guide and plugin dev guide #3180 and #3340 --- docs/developers/plugin-development.md | 109 ++++++++++++++++++-------- docs/support/migration-v8.md | 16 ++++ 2 files changed, 94 insertions(+), 31 deletions(-) diff --git a/docs/developers/plugin-development.md b/docs/developers/plugin-development.md index 6a33fb3b1b..a349e4a4ca 100644 --- a/docs/developers/plugin-development.md +++ b/docs/developers/plugin-development.md @@ -5,8 +5,8 @@ As of version 7.0, plugins are [JavaScript Classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes). A plugin must implement: * [A `constructor()`](#constructor) that accepts an instance of jsPsych. -* [A `trial()` method](#trial) that accepts an `HTMLElement` as its first argument and an `object` of trial parameters as its second argument. There is an optional third argument to [handle the `on_load` event](#asynchronous-loading) in certain cirumstances. The `trial()` method should invoke `jsPsych.finishTrial()` to [end the trial and save data](#save-data) at the appropriate moment. -* [A static `info` property](#static-info) on the class that contains an object describing the plugin's parameters. +* [A `trial()` method](#trial) that accepts an `HTMLElement` as its first argument and an `object` of trial parameters as its second argument. There is an optional third argument to [handle the `on_load` event](#asynchronous-loading) in certain cirumstances. The `trial()` method should *either* invoke `jsPsych.finishTrial()` or should be an `async` function that returns a data object to [end the trial and save data](#save-data). +* [A static `info` property](#static-info) on the class that contains an object describing the plugin's parameters, data generated, and version. ### Templates @@ -49,7 +49,7 @@ const info = { } ``` -The `version` field describes the version of the plugin used and then durin the experiment will be part of the generated data. This is used generate metadata and help maintain the Psych-DS standard. It should imported from the package.json file by including an import statement in the top of the index.ts file. This allows the `version` field be automatically updated with each changeset. +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. ```javascript import { version } from '../package.json'; @@ -61,7 +61,7 @@ const info = { } ``` -If you are not using a build environment and instead writing a plain JS file, you can manually enter the `version` as a string. +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. ```javascript const info = { @@ -73,11 +73,12 @@ const info = { 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 is similar to the `parameters` property, except it does not include a `default` property and instead describes the data generated. Additionally, this should be only used for data you choose to generate and not the default data. Any javadoc you include will be scraped as metadata. +The `data` field describes the types of data generated by the plugin. Each parameter has a `type` property. ```js const info = { name: 'my-awesome-plugin', + version: version, parameters: { image: { type: ParameterType.IMAGE, @@ -89,7 +90,6 @@ const info = { } }, data: { - /** This will become metadata describing "response". */ response: { type: ParameterType.STRING, }, @@ -97,29 +97,57 @@ const info = { } ``` - 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](../overview/dynamic-parameters.md), 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. -The `info` object should be a `static` member of the class, as shown below. +We strongly encourage using [JSDoc comments](https://jsdoc.app/about-getting-started) 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. ```js const info = { name: 'my-awesome-plugin', version: version, parameters: { + /** The path to the image file to display. */ image: { - type: jspsych.ParameterType.IMAGE, + type: ParameterType.IMAGE, default: undefined }, + /** The duration to display the image in milliseconds. */ image_duration: { - type: jspsych.ParameterType.INT, + 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. + +```js +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: { - /** This will become metadata describing response. */ + /** The text of the response generated by the participant. */ response: { type: ParameterType.STRING, }, @@ -151,15 +179,11 @@ trial(display_element, trial){ } ``` -jsPsych doesn't clear the display before or after each trial, so it is usually appropriate to use `innerHTML` to clear the display at the end of a trial. - -```javascript -display_element.innerHTML = ''; -``` - ### 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()`. In `7.0` the only advantage of using this method is that it registers the timeout handler so that it can be easily cleared at the end of the trial using `jsPsych.pluginAPI.clearAllTimeouts()`. In future versions we may replace the implementation of `jsPsych.pluginAPI.setTimeout()` with improved timing functionality based on `requestAnimationFrame`. +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`. ```js trial(display_element, trial){ @@ -212,26 +236,21 @@ One of the [trial events](../overview/events.md) is `on_load`, which is normally 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 `7.0` this Promise only serves as a flag to tell jsPsych that the `on_load` event should not be triggered. In future versions we may make the `Promise` functional so that the `trial` operation can be an `async` function. +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. ```js -trial(display_element, trial, on_load){ +async trial(display_element, trial, on_load){ let trial_complete; - do_something_asynchronous().then(()=>{ - on_load(); - }); + await do_something_asynchronous() - const end_trial = () => { - this.jsPsych.finishTrial({...}) - trial_complete(); // not strictly necessary, but doesn't hurt. - } + on_load(); - return new Promise((resolve)=>{ - trial_complete = resolve; - }) + await do_the_rest_of_the_trial(); + + return data_generated_by_the_trial; } ``` @@ -254,8 +273,32 @@ trial(display_element, trial){ } ``` +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)`. + +```javascript +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](../overview/plugins.md#data-collected-by-all-plugins) 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](../overview/simulation.md). @@ -285,4 +328,8 @@ If you are developing a plugin with the aim of including it in the main jsPsych 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. +## Plugin templates + +Templates for plugins are available in the [jspsych-contrib](https://github.com/jspsych/jspsych-contrib) repository. There is a command-line tool for generating a new plugin from these templates in that repository. See the README file in the jspsych-contrib repository for more information. + diff --git a/docs/support/migration-v8.md b/docs/support/migration-v8.md index ebf7c8dc1b..b0651a1009 100644 --- a/docs/support/migration-v8.md +++ b/docs/support/migration-v8.md @@ -92,6 +92,22 @@ Version 8.x is more strict about this. Plugins should list all parameters in the `info` object. If a parameter is not listed, then timeline variables and function evaluation will not work for that parameter. The `save_trial_parameters` parameter will also not work for parameters that are not listed in the `info` object. +## Plugin `version` and `data` properties + +We've added a `version` property to the `info` object for plugins. +This property is a string that should be updated whenever a new version of the plugin is released. + +We've also added a `data` property to the `info` object for plugins. +This property is an object that can contain a description of the types of data that the plugin will generate. + +Including these properties is not *required* for a plugin to work, but it is recommended. +In version 8.x, jsPsych will throw a warning if a plugin is used that does not have a `version` or `data` property in the `info` object. +In version 9.x, we plan to make this a requirement. + +## Changes to `finishTrial()` + +When a plugin calls `finishTrial()` or ends via a `return` statement, jsPsych will now automatically clear the display and clear any timeouts that are still pending. This change should only affect plugin developers. If you are using built-in plugins you should not notice any difference. + ## Progress bar The automatic progress bar now updates after every trial, including trials in nested timelines.