Library core

The core module contains the foundations of the library. As with many types of foundations, you’ll probably not see much of this module. However, you’ll rely on it with everything you do, and so it’s worth getting familiar with the machinations underlying the library, especially if you are looking to extend it.

Because all other components in the library build on the core.Component() class, they share many of the same options. This is why we’ll often refer to this page when discussing other parts of the library.


Component

The core.Component() class is the most basic provided by lab.js. It is the foundation for all other building blocks, which extend and slightly modify it. As per the philosophy of lab.js, experiments are composed entirely out of these components, which provide the structure of the study’s code.

In many cases, you will not include a core.Component() directly in your experiment. Instead, your experiment will most likely consist of the other building blocks which lab.js provides – but because all of these derive from this fundamental class, there are many similarities: Many components share the same behavior, and accept the same options, so that you will (hopefully) find yourself referring to this part of the documentation from time to time.

Behavior

Following its creation, every component will go through several distinct stages.

The preparation stage is designed to prepare the component for its later use in the experiment in the best possible way. For example, a display might be prerendered during this phase, and any necessary media loaded. Importantly, by the time a component is prepared, its settings need to have been finalized.

The run stage is the big moment for any component in an experiment. Upon running, the component assumes (some degree of) control over the study: It starts capturing and responding to events triggered by the user, it might display information or stimuli on screen, through sound, or by any other means.

The end marks the close of an component’s activity. It cedes control over the output, and stops listening to any events emitted by the browser. If there are data to log, they are taken care of after the component’s run is complete. Similarly, any housekeeping or cleaning-up is done at this point.

Usage

Instantiation

class core.Component([options])
Arguments:
  • options (object) – Component options

The core.Component() does not, by itself, display information or modify the page. However, it can be (and is throughout lab.js) extended to meet even very complex requirements.

A component is constructed using a set of options to govern its behavior, which are specified as name/value pairs in an object. For example, the following component is given a set of responses and a timeout:

const c = new lab.core.Component({
  'responses': {
    'keypress(s)': 'left',
    'keypress(l)': 'right',
  },
  'timeout': 1000,
})

c.run()

These options are available after construction from within the options property. For example, the timeout of the above component could be changed later like so:

c.options.timeout = 2000

All other options can be modified later using the same mechanism. However, options are assumed to be fixed when a component is prepared.

Event API

During a study, a component goes through several distinct stages, specifically prepare, run and end. Much of the internal logic revolves around these events. The most important events are:

  • prepare
  • run
  • end
  • commit (data sent to storage)

External functions can also tie into this logic, for example to collect and transmit data when an experiment (or a part of the same) is over. The following methods make this possible.

waitFor(event)
Returns:A Promise that resolves when a specific event occurs.

This helper makes it possible to plan actions for a later point during the study, using the Promise API, as visible in the following example:

const c = new lab.core.Component({ /* ... */ })

// Queue a dataset download when the component ends
c.waitFor('end').then(
  () => c.options.datastore.download()
)

// Run the component
c.run()
on(event, handler)

Like the waitFor() helper, this function will trigger an action at a later point. However, instead of a promise, it uses a callback function:

c.on('end', () => this.options.datastore.download())

The callback is internally bound to the component, so that the value of this inside the function corresponds to the component on which the event is triggered.

once(event, handler)

Equivalent to on(), but additionally ensures that the handler is only run on the first matching event.

off(event, handler)

Remove a previously registered handler for an event.

See also

If you want to be notified of every event a component goes through, you’ll want to look into Plugins.

Methods

prepare()

Trigger the component’s prepare phase.

Make the preparations necessary for run()-ning a component; for example, preload all necessary media required later.

The prepare() method can, but need not be called manually: The preparation phase will be executed automatically when the the component is run(). Therefore, it is usually omitted from the examples in the documentation.

Flow control components such as the Sequence() will automatically prepare all subordinate components unless these are explicitly marked as tardy.

Returns:A promise that resolves when the preparation is complete (e.g. when all media have been loaded, etc.)
run()

Run the component, giving it control over the participants’ screen until the end() method is called. Calling run() will trigger prepare() if the component has not yet been prepared.

Returns:A promise that resolves when the component has taken control of the display, and all immediate tasks have been completed (i.e. content inserted in the page, requests for rendering on the next animation frame filed)
respond([response])

Collect a response and call end().

This is a shortcut for the (frequent) cases in which the component ends with the observation of a response. The method will add the contents of the response argument to the component’s data, evaluate it against the ideal response as specified in correctResponse, and then end() the component’s run.

Returns:The return value of the call to end() (see below).
end([reason])

End a running component. This causes an component to cede control over the browser, so that it can be passed on to the next component: It stops monitoring events on the screen, collects all the accumulated data, commits it to the specified datastore, and performs any additional housekeeping that might be due.

Returns:A promise that resolves when all necessary cleanup is done: When all data have been logged, all event handlers taken down, etc.
clone([optionsOverride])
Returns:A new component of the same type with the same options as the original. If an object with additional options is supplied, these override the original settings.

Properties

aggregateParameters

Superset of the component’s parameters and those of any superordinate components (read-only)

Often, a component’s content and behavior is determined not only by its own parameters, but also by those of superordinate components. For example, a component might be contained within a Sequence() representing a block of stimuli of the same type. In this and many similar situations, it makes sense to define parameters on superordinate components, which are then applied to all subordinate, nested, components.

The aggregateParameters attribute combines the parameters of any single component with those of superordinate components, if there are any. Within this structure, parameters defined at lower, more specific, levels override those with an otherwise broader scope.

Consider the following structure:

const experiment = lab.flow.Sequence({
  'title': 'Superordinate sequence',
  'parameters': {
    'color': 'blue',
    'text': 'green',
  },
  // ... additional options ...
  content: [
    lab.core.Component({
      'title': 'Nested component',
      'parameters': {
        'color': 'red',
      },
    }),
  ],
})

In this case, the nested component inherits the parameter text from the superordinate sequence, but not color, because the value of this parameter is defined anew within the nested component itself.

timer

Timer for the component (read-only)

The timer attribute provides the central time-keeping instance for the component. Until the component is run(), it will be set to undefined. Then, until the end() of an component’s cycle, it will continuously provide the duration (in milliseconds) for which it has been running. Finally, once the cycle has reached its end(), it will provide the time difference between the start and the end of the component’s run cycle.

progress

Progress indicator, as a number between 0 and 1 (read-only)

The progress attribute indicates whether a component has successfully completed its run(), and (for more complex components) to which degree. For example, a basic html.Screen() will report its progress as either 0 or 1, depending whether it has completed its turn. Nested components such as the flow.Sequence(), on the other hand, will return a more nuanced value, depending on the status of subordinate components – specifically, the proportion that has passed at any given time.


Options

options

The vast majority of customizations are made possible through a component’s options, which govern its behavior in detail. In most cases, these options are set when a component is created:

const c = new lab.core.Component({
  'exampleOption': 'value'
})

The options can also be retrieved and changed later through the options property. For example, the current value of the option created above is available through the variable c.options.exampleOption, and could be changed by altering its content.

Because the presentation of components is prepared when prepare() is called, and the options factor into this step, changes should generally be made before the prepare phase starts (c.f. also the tardy option).

Basic settings

options.debug

Activate debug mode (defaults to false)

If this option is set, the component provides additional debug information via the browser console.

options.el

HTML element within the document into which content is inserted. Defaults to the element with the attribute data-labjs-section with the value main.

The el property determines where in the document the contents of the experiment will be placed. Most parts of an experiment will replace the contents of this element entirely, and substitute their own information. For example, an html.Screen() will insert custom HTML, whereas a canvas.Screen() will supply a Canvas on which information is then drawn.

To change the location of the content, you can pick out the element of the HTML document where you would like the content placed as follows:

const c = new lab.core.Component({
  'el': document.getElementById('experiment_content_goes_here'),
  // ... additional options ...
})

Selecting a target via document.getElementById or document.querySelector requires that the document contains a matching element. For the example above, this would be the following:

<div id="experiment_content_goes_here"></div>

Metadata

options.title

Human-readable title for the component, defaults to null

This is included in any data stored by the component, and can be used to pick out individual components.

options.id

Machine-readable component identifier (null)

This is often generated automatically; for example, flow control components will automatically number their nested components when prepared.

options.parameters

Settings that govern component’s behavior ({})

This object contains any user-specified custom settings that determine a component’s content and behavior. These may, for example, be used to fill placeholders in the information presented to participants, as a html.Screen() does.

The difference between parameters and data is that the former are retained at all times, while the data may be reset at some later time if necessary. Thus, any information that is constant and set a priori, but does not change after the component’s preparation should be stored in the parameters, whereas all data collected later should be (and is automatically) collected in the data attribute.

Behavior

options.skip

End immediately after running (false).

options.tardy

Ignore automated attempts to prepare() the component, defaults to false.

Setting this attribute to true will mean that the component needs to be prepared manually through a call to prepare(), or (failing this) that it will be prepared immediately before it is run(), at the last minute.

Response handling

options.responses

Map of response events onto response descriptions ({})

The responses object maps the actions a participant might take onto the responses saved in the data. If a response is collected, the end() method is called immediately.

For example, if the possible responses are to press the keys s and l, and these map onto the categories left and right, the response mapping might look as follows:

'responses':  {
  'keypress(s)': 'left',
  'keypress(l)': 'right',
}

The left part, or the keys of this object, defines the browser event corresponding to the response. This value follows the event type syntax, so that any browser event can be caught. Additional (contrived) examples might be:

'responses': {
  'keypress(s)': 'The "s" key was pressed',
  'keypress input': 'Participant typed in a form field',
  'click': 'A mouse click was recorded',
  'click button.option_1': 'Participant clicked on option 1',
}

As is visible in the first example, additional options for each event can be specified in brackets. These are:

  • For keypress events, the letters corresponding to the desired keys, or alternatively Space and Enter for the respective keys. Multiple alternate keys can be defined by separating letters with a comma. (for a full list, please consult the W3C keyboard event specification. lab.js follows this standard where it is available, using only the value Space instead of a single whitespace for clarity, as well as Comma so as not to confuse this key with the separator. Note also, however, that some browsers do not fire keypress events for all keys; specifically, chrome-based browsers do not provide such events for arrow and navigation keys)
  • For click events, the mouse button used. Buttons are numbered from the index finger outwards, i.e. on a right-handed mouse, the leftmost button is 0, the middle button is 1, and so on, and vice versa for a left-handed mice. (please note that you may also need to catch and handle the contextmenu event if you would like to stop the menu from appearing when the respective button is pressed.)

Finally, a target element in the page can be specified for every event, as is the case in the last example. The element in question is identified through a CSS selector. If an element is specified in this manner, the response is limited to that element, so a click will only be collected if it hits this specific element, and a keyboard event will only be responded to if the element is selected when the button is pressed (for example if text is input into a form field).

options.correctResponse

Label or description of the correct response (defaults to null)

The correctResponse attribute defines the label of the normative response. For example, in the simple example given above, it could take the values 'left' or 'right', and the corresponding response would be classified as correct.

Timing

options.timeout

Delay between component run and automatic end (null)

The component automatically ends after the number of milliseconds specified in this option, if it is set.

Data collection

options.data

Additional data ({})

Any additional data (e.g. regarding the current trial) to be saved alongside automatically generated data entries (e.g. response and response time). This option should be an object, with the desired information in its keys and values.

Please consult the entry for the parameters for an explanation of the difference between these and data.

options.datastore

Store for any generated data (null by default)

A data.Store() object to handle data collection (and export). If this is not set, the data will not be collected in a central location outside the component itself.

options.datacommit

Whether to commit data by default (true)

If you would prefer to handle data manually, unset this option to prevent data from being commit when the component ends.

Preloading media

options.media

Media files to preload ({})

Images and audio files can be preloaded in the background during the prepare phase, to reduce load times later during the experiment. To achieve this, supply an object containing the urls of the files in question, split into images and audio files as follows:

'media': {
  'images': [
    'https://mydomain.example/experiment/stimulus.png'
  ],
  'audio': [
    'https://mydomain.example/experiment/sound.mp3'
  ]
}

Both image and audio arrays are optional, and empty by default.

Please note that this method has some limitations. In particular, the preloading mechanism is dependent upon the browser’s file cache, which cannot (yet) be controlled completely. The media file might have been removed from the cache by the time it is needed. Thus, this is a somewhat brittle mechanism which can improve load times, but is, for technical reasons, not guaranteed safe. In our experience, testing across several browsers reliably indicates whether preloading is dependable for a given experiment.

Caution

This is an experimental feature and might change at some later point. That’s because we are still gathering experience with it, and because we foresee that new browser technology may change the implementation.

Plugins

options.plugins

Array of plugins that interact with the component, and are automatically notified of events. For example, adding a plugins.Logger() instance will log event notifications onto the console:

const c = new lab.core.Component({
  plugins: [
    new lab.plugins.Logger(),
  ],
})

Similarly, plugins.Debug() provides the interface for data checking and debugging used in the builder preview.

Advanced options

options.events

Map of additional event handlers ({})

In many experiments, the only events that need to be handled are responses, which can be defined using the responses option described above. However, some studies may require additional handling of events before a final response is collected. In these cases, the events object offers an alternative.

The events option follows the same format used for the responses, as outlined above. However, instead of a string response, the object values on the right-hand side are event handler functions, which are called whenever the specified event occurs. The functions are expected to receive the event in question as an argument, and process it as they see fit. They are automatically bound to the component in question, which is available within the function through the this keyword.

As a very basic example, one might want to ask users not to change to other windows during the experiment:

'events': {
  'visibilitychange': function(event) {
    if (document.hidden) {
      alert(`Please don't change windows while the experiment is running`)
    }
  },
}
options.messageHandlers

Map of internal component events to handler functions ({})

This is a shorthand for the on() method

const c = new lab.core.Component({
  messageHandlers: {
    'run': () => console.log('Component running'),
    'end': () => console.log('Component ended'),
  }
})

Caution

This option is likely to be renamed at some later point; we are not happy with its current label. Ideas are very welcome!


Dummy

The core.Dummy() component is a stand-in component that calls end() immediately when the component is run. We use it for tests and demonstrations, and only very rarely in experiments.

class core.Dummy([options])

Direct descendant of the core.Component() class, with the single difference that the skip option is set to true by default.