Custom screens

When the built-in screens are not enough for your purposes, you can define your own custom screens.

For this purpose there is an abstract Screen component available with magpie, along with a catalog of input and stimulus components. Magpie's $magpie API offers additional helpers for managing screens and slides.

Slides

As mentioned in the "Basics" section, a screen consists of one or more slides, which are displayed one after the other. The code for a basic screen would then look like this:

<Screen>
    <Slide>
    Hello World
    </Slide>
</Screen>

This screen has exactly one slide, which displays "Hello world". If you would like to have another slide, you can simply add another <Slide> component next to the first. They are displayed in the order they appear in the file.

<Screen>
    <Slide>
        Hello World
    </Slide>

    <Slide>
        Anybody here?
    </Slide>
</Screen>

The problem now is that there is no indication when to switch to the second slide. So, we will stay on the first slide forever.

Switching slides

However, magpie lends us a hand here, and provides a function to go to the next slide:

<Screen>
    <Slide>
        Hello World
        <button @click="$magpie.nextSlide()">Next slide</button>
    </Slide>

    <Slide>
        Anybody here?
    </Slide>
</Screen>

Here, we call $magpie.nextSlide() in an event handler for the click event of our button.

Now our participants can click on the button to go to the next slide. We can also use a helper component to trigger this automatically after some time:

<Screen>
    <Slide>
        Hello World
        <Wait :time="1000" @done="$magpie.nextSlide()" />
    </Slide>

    <Slide>
        Anybody here?
    </Slide>
</Screen>

This invisible component will wait one second and then call $magpie.nextSlide() for us, so effectively the first slide is only shown for one second.

Now, we're stuck on the second slide, however. How do we jump to the next screen?

Switching screens

It turns out, there is a function for this as well:

<Screen>
    <Slide>
        Hello World
        <Wait :time="1000" @done="$magpie.nextSlide()" />
    </Slide>

    <Slide>
        Anybody here?
        <button @click="$magpie.nextScreen()">Yes</button>
    </Slide>
</Screen>

After 1 second of displaying "Hello world", we display "Anybody here?" and when the participant clicks the "Yes" button, we go to the next screen.

Collecting data

Of course, Experiments are about collecting data, so we need some inputs to collect responses from our participants.

Magpie has some built-in input components ready for you. For example, you could use the RatingInput component to realize a rating task:

<Screen>
    <Slide>
        <RatingInput
            :right="very cute"
            :left="very appalling"/>
    </Slide>
</Screen>

This will display a 7-step rating going from "very appalling" to "very cute". To store the response somewhere, we can use the $magpie.measurements object, which holds custom variables:

<Screen>
    <Slide>
        <RatingInput
            :right="very cute"
            :left="very appalling"
            :response.sync="$magpie.measurements.rating" />
    </Slide>
</Screen>

We can assign a custom property of the measurements object to the response prop of the RatingInput. The magic here is in the .sync suffix. This will make sure, that the assignment is two-way: If the participant changes their response, $magpie.measurements.rating will be changed automatically to always reflect the latest value.

To save all our measurements made in the current screen, we then use $magpie.saveMeasurements:

<Screen>
    <Slide>
        <RatingInput
            :right="very cute"
            :left="very appalling"
            :response.sync="$magpie.measurements.rating" />
        <button @click="$magpie.saveMeasurements(); $magpie.nextScreen()">Submit</button>
    </Slide>
</Screen>

However, as writing both functions all the time is cumbersome, there's a combined version of the two:

<Screen>
    <Slide>
        <RatingInput
            :right="very cute"
            :left="very appalling"
            :response.sync="$magpie.measurements.rating" />
        <button @click="$magpie.saveAndNextScreen()">Submit</button>
    </Slide>
</Screen>

Once, the participant clicks "Submit", their rating will be stored in the data set for the current screen.

This architecture makes it possible to collect multiple responses per screen and even per slide:

<Screen>
    <Slide>
        <RatingInput
            :right="very cute"
            :left="very appalling"
            :response.sync="$magpie.measurements.cuteness" />
        <RatingInput
            :right="very large"
            :left="very small"
            :response.sync="$magpie.measurements.size" />
        <RatingInput
            :right="very interesting"
            :left="very boring"
            :response.sync="$magpie.measurements.interest" />

        <button @click="$magpie.saveAndNextScreen()">Submit</button>
    </Slide>
</Screen>

Validating measurements

Imagine we want to record a participant-entered text in a screen. For this we can use the textarea input.

<Screen>
    <Slide>
        <TextareaInput :response.sync="$magpie.measurements.text" />

        <button @click="$magpie.saveAndNextScreen()">Submit</button>
    </Slide>
</Screen>

However, we often have some specifications about how such input should look like. This is where validations come in. We can specify validations for measurements via the validations prop of the Screen component:

<Screen :validations="{
      text: {
        minLength: $magpie.v.minLength(4),
        alpha: $magpie.v.alpha
      }
    }">
    <Slide>
        <TextareaInput :response.sync="$magpie.measurements.text" />

        <button v-if="!$magpie.validateMeasurements.text.$invalid" @click="$magpie.saveAndNextScreen()">Submit</button>
    </Slide>
</Screen>

Here, we specify two validations for our text measurement: The text must be at least 4 characters long and may only contain alphabetical characters.

Below, on the submit button, we added an if-statement that makes sure, we only show the button, if there are no problems with the measurement.

You may have noticed, we use $magpie.v to access the validators we needed this time. $magpie.v provides a selection of generally useful validators which you can find all listed in the reference. If you ever need a validator that is not in there, you can simply write a function that returns true if the validation passed and false if it didn't.

<Screen :validations="{
      text: {
        lengthRequirements: (text) => text.length >= 4 && text.length < 50
      }
    }">
    <Slide>
        <TextareaInput :response.sync="$magpie.measurements.text" />

        <button v-if="!$magpie.validateMeasurements.text.$invalid" @click="$magpie.saveAndNextScreen()">Submit</button>
    </Slide>
</Screen>

Here, we've added a hand-built validator that check whether the text is between 4 and 50 characters long.

Validations incidentally also work for built-in screens as they inherit from the abstract Screen component. The variable that holds the response in these cases is always called response.

Separating custom screens into files

If we want to use a custom screen multiple times in our experiment, we can separate it out into a different .vue file.

Suppose our experiment looks like this:

<template>
    <Experiment>
        <template v-for="(task, i) in tasks">
            <Screen :key="i">
                <p>{{ task.question }}</p>
                <TextareaInput :response.sync="$magpie.measurements.text" />
                <button @click="$magpie.saveAndNextScreen()">Next</button>
            </Screen>
        </template>
    </Experiment>
</template>
<script>
    import tasks from './data/tasks.csv'
    export default {
        name: 'App',
        data() {
            return {
                tasks,
            }
        }
    }
</script>

Now, we're extracting the code for our screen into a new file QuestionScreen.vue.

<!-- QuestionScreen.vue -->
<template>
    <Screen>
        <p>{{ task.question }}</p>
        <TextareaInput :response.sync="$magpie.measurements.text" />
        <button @click="$magpie.saveAndNextScreen()">Next</button>
    </Screen>
</template>

<script>
export default {
    name: 'QuestionScreen',
    props: {
        task: {
            type: Object,
            required: true
        }
    }
}    
</script>

Here, we define a new Vue component for the custom screen and introduce a prop called task. We need to pass the task object to our custom screen explicitly as a prop, because vue components do not have access to the variables of other components. So, much like you'd have to pass a local variable in your JavaScript to a different function as a parameter, if you want to use it in that function, we need to explicitly pass variables across component borders, if we want other components to be able to use them.

To define a prop we always have to set two facts about it: What data type is it? (One of Number, String, Array, Boolean) Is it required? If not, we have to set a default value using the default field.

Now, we can use this screen component in our main experiment file.

<!-- App.vue -->
<template>
  <Experiment>
      <template v-for="(task, i) in tasks">
        <QuestionScreen
            :key="i"
            :task="task" />
      </template>
  </Experiment>
</template>
<script>
    import tasks from './data/tasks.csv'
    import QuestionScreen from './QuestionScreen'
    export default {
        name: 'App',
        components: { QuestionScreen },
        data() {
            return {
                tasks,
            }
        }
    }
</script>

We import the new screen component into the current file using a normal import statement and afterwards register it as a subcomponent to be used as part of our main App component. Now, we can use it in the <template> part of the file.

Creating result rows manually

Usually, you will use the measurements object to collect data and save it to the results as seen above:

<Screen>
    <Slide>
        <RatingInput
                right="very appealing"
                left="very unappealing"
                :response.sync="$magpie.measurements.appetite" />
        <RatingInput
                right="very large"
                left="very small"
                :response.sync="$magpie.measurements.portion" />
        <RatingInput
                right="very healthy"
                left="very unhealthy"
                :response.sync="$magpie.measurements.healthy" />

        <button @click="$magpie.saveAndNextScreen()">Submit</button>
    </Slide>
</Screen>

This might result in the following result data set:

appetite portion healthy
20 35 75

However, sometimes you need to create multiple rows in the result data per screen.

You can add new result rows from within a screen as well as from within a method using $mapgie.addTrialData()

For example, instead of using Screen's measurements together with saveAndNextScreen, you can save the data manually as follows:

<Screen>
    <Slide>
        <RatingInput
                right="very appealing"
                left="very unappealing"
                :response.sync="$magpie.measurements.appetite" />
        <RatingInput
                right="very large"
                left="very small"
                :response.sync="$magpie.measurements.portion" />
        <RatingInput
                right="very healthy"
                left="very unhealthy"
                :response.sync="$magpie.measurements.healthy" />

        <button @click="$magpie.addTrialData({
          appetite: $magpie.measurements.appetite / 100,
          portion: $magpie.measurements.portion / 100,
          healthy: $magpie.measurements.healthy / 100
        })">Add row</button>
        <button @click="$magpie.nextScreen()">Next</button>
    </Slide>
</Screen>

Here we use a RatingInput to collect three different variables. The participant can add new data multiple times using the "Add row" button's click handler, which calls $mapgie.addTrialData(). Since the data is already saved, we only need to call nextScreen instead of saveAndNextScreen.

This might result in the following result data set:

appetite portion healthy
20 35 75
84 26 75
20 35 75

Passing variables directly to the results

Often, you will have independent variables that you want to be present in the result rows as well. For this you can use the Record component.

<Screen>
    <Slide>
        <img :src="imgpath" />

        <RatingInput
                right="very appealing"
                left="very unappealing"
                :response.sync="$magpie.measurements.appetite" />
        <RatingInput
                right="very large"
                left="very small"
                :response.sync="$magpie.measurements.portion" />
        <RatingInput
                right="very healthy"
                left="very unhealthy"
                :response.sync="$magpie.measurements.healthy" />

        <Record :data="{
              image: imgpath,
            }" />

        <button @click="$magpie.saveAndNextScreen()">Submit</button>
    </Slide>
</Screen>

Whatever object you pass to the Record component will be merged with your measurements the next time you save.

Adding global data

We can also add data globally such that it will be present in all result rows using $mapgie.addExpData().

<Screen>
    <Slide>
        <RatingInput
                right="very appealing"
                left="very unappealing"
                :response.sync="$magpie.measurements.appetite" />
        <RatingInput
                right="very large"
                left="very small"
                :response.sync="$magpie.measurements.portion" />
        <RatingInput
                right="very healthy"
                left="very unhealthy"
                :response.sync="$magpie.measurements.healthy" />

        <button @click="$magpie.addExpData({
          appetite: $magpie.measurements.appetite / 100,
          portion: $magpie.measurements.portion / 100,
          healthy: $magpie.measurements.healthy / 100
        }); $magpie.nextScreen()">Submit</button>
    </Slide>
</Screen>

This time, we call $mapgie.addExpData() on submit which adds the data globally, so that it is present in every result row of our hypothetical experiment:

response_time response appetite portion healthy
500 Yes 20 35 75
736 Yes 20 35 75
365 No 20 35 75
615 Yes 20 35 75

This is useful for demographic data like age or level of education.

You can use the Record component for this as well, by using the global property:

<Screen>
    <Slide>
        <RatingInput
                right="very appealing"
                left="very unappealing"
                :response.sync="$magpie.measurements.appetite" />
        <RatingInput
                right="very large"
                left="very small"
                :response.sync="$magpie.measurements.portion" />
        <RatingInput
                right="very healthy"
                left="very unhealthy"
                :response.sync="$magpie.measurements.healthy" />

        <Record
                global
                :data="{
                 maxAppetite: 100,
                 maxPortion: 100,
                 maxHealthy: 100
                }"
            />

        <button @click="$magpie.nextScreen()">Submit</button>
    </Slide>
</Screen>

Using Record in built-in screens

You can also use the Record component in built-in screens. You only need to make sure to declare it inside one of the named slots, e.g. #stimulus.

<CompletionScreen ...>
    <template #stimulus>
        <Record
                :data="{
                 maxAppetite: 100,
                 maxPortion: 100,
                 maxHealthy: 100
                }"
            />
    </template>
</CompletionScreen>