𧩠Components β
A key to making development faster and more fun in π« Smile is to organize parts of the overall experiment into smaller, modular units called components. Using components, the meaningful parts of a complex webpage or application are broken down into smaller elements which are then built up into a hierarchy. The code for these smaller elements can, in many cases, be developed completely independently from the rest of the project or webpage.
Component development speeds up the process of designing a new experiment or web application because you don't have to understand every part of the code to begin adding new interface elements and logic. Components can show/hide content, verify that input to a form is valid, collect data from a participant and save it to a database, animate elements of a page, etc...
Well-designed components are reuseable across projects so that if someone else develops a useful component you can easily import it into your project. Component libraries exist which define the logic and behavior of common elements on a page. For example, Radix Vue and PrimeVue provide libraries of components that provide functionality like calendar date pickers, navigation menus, and hoverable tooltips. In addition, novel components are easily built up out of other components leveraging modularity and code reuse.
Components are somewhat similar to the role that "plugins" play in a library like JSPsych but JSPsych plugins often handle single trials of an experiment whereas a Vue.js component might be as small as a button or as big as an entire webpage or even application. In addition, components leverage some other concepts in modern web design such as reactivity and declarative rendering that make your development and debugging much easier.
How are components used in π« Smile? β
Typically in π« Smile, components are used to define phases of an experiment (e.g., consent, instructions, etc...), Smile provides several built-in components (which we refer to as "Views") that implement nicely designs components that collect informed consent or show instructions. Components are also used to define the individual trials of an experiment (i.e., the logic and flow of what is shown in a given trial). Some trials might be complex and composed of other components that define the look and layout of stimuli, buttons, etc...In addition, Smile provides a simple component API which makes it easy to step through sequences of trials.
Vue.js components β
There are many frameworks which utilize the concept of components on the web including React, Angular, and Svelte. In π« Smile, we use the Vue.js framework.
Why Vue.js?
Vue.js was chosen for π« Smile because it is easy to learn, opensource and free, has a large and active community, and is one of the few web frameworks not associated with a major company. In addition, the Vue community has developed strong international representation (e.g., it is the most popular web framework in China) with docs in many languages. There's a nice documentary about the leader of the Vue project here.
Single File Components β
The preferred way to develop Vue components is using a special file format known as SFC (Single File Component). These files end with an extension .vue
. The SFC files combine elements of Javascript, HTML, and CSS/SCSS into a single modular element that defines your component. These files can be edited best using the Volar extension for VSCode (i.e., it provides syntax highlighting and other code formatting hints).
Here is an example Vue 3 SFC file called SimpleButton.vue
which is using the Vue 3.0 composition API:
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button class="button" @click="count++">Count is: {{ count }}</button>
</template>
<style scoped>
button {
font-weight: bold;
background-color: lightblue;
}
</style>
Look closely at this example and notice how this example has a <script>
section, a <template>
section, and a <style>
section. These sections define the javascript behavior (script), the HTML rendering (template), and the look/feel of the component (style). These three factors are typical for interactive websites (e.g., you typically import your javascript code (.js), and style sheet (.css) code into your basic HTML document (.html)). However, normally you define these for the entire page not separately for individual pieces of a larger page. The SFC file format highlights the value of modularity since it helps you group the code for a particular part of the page together with its HTML and CSS styling.
Templates β
If we look more closely, the template for this component looks like basic HTML:
<template>
<button class="button" @click="count++">Count is: {{ count }}</button>
</template>
It has a simple <button>
element that when clicked (@click
) increments a variable called count
. The current value of count
is rendered into the template using the {{ count }}
syntax. The @click
event handlers is not normal HTML but is a Vue directive/shorthand for adding the onclick()
event listener. Similarly, the {{ count }}
is template syntax for text interpolation which essentially converts the value of count
to a string and inserts it into the HTML template. You can read more about Vue's template syntax here.
Scripts β
Now lets look closer at the <script>
section:
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
The script has the special word setup
and initializes the count
variable to zero and makes it reactive (more on that below, but here is Vue's documentation).
Styles β
Finally, the <style>
section defines how elements of the template should look.
<style scoped>
button {
font-weight: bold;
background-color: lightblue;
}
</style>
Here we are specifying that the text in the button for this component should be bold and the button light blue. Since the style section has the word scoped
in it, the styles will only apply to this component and not to other components on the page (i.e., only this button will be bolded). In other words even if elsewhere on the page, in a different component there is a <button>
tag this style will not apply to it.
Using a component β
This documentation website is itself built using Vue.js components. As a result, we can render the above Vue component directly into this page to see how it works. Here is the button component in action:
The way this worked was that at the top of this page we imported the component
import SimpleButton from '@/components/SimpleButton.vue'
and then in the main text wrote
<SimpleButton/>
Vue replaced the custom <SimpleButton/>
tag with the rendered HTML for the template and the logic for the button.
Try clicking the button and see how the counter is incremented! All the logic for this was self-contained in the small SFC file above and could be easily moved into another project just by copying the SFC file and importing it where needed. In addition, it can be applied multiple times on the same page each instance of the component will have its own state and behavior.
For example writing
<SimpleButton/>
<SimpleButton/>
will result in:
each with a self-contained counter.
Importantly, components can include and use other components. For example, the following component imports the SimpleButton
component and a new TextInput
component:
<script setup>
import SimpleButton from '@/components/SimpleButton.vue'
import TextInput from '@/components/TextInput.vue'
</script>
<template>
<div>
<SimpleButton />
<TextInput />
</div>
</template>
This way more complex elements can be composed out of simpler elements.
Declarative Rendering and Reactivity β
A core concept in Vue.js is the idea of reactivity and declarative rendering. There are many useful guides to this include the excellent Vue tutorial and the Vue documentation. However, here is a quick summary.
Declarative rendering is the idea that you define the structure of your webpage in a template. The template can refer to variables and functions that are defined in the script section of the component. When the value of the variable changes the template automatically is updated. This is in contrast to the traditional approach of web development such as JQuery or JSPsych where you write imperative code to update the page when the state changes. Imperative code is code that explicitly tells the computer how to do something. For example, in the imperative style you might write code that says "when the button is clicked, increment the count variable and update the text of the button to reflect the new count". In the declarative style you would just say "the text of the button should be the value of the count variable" and Vue would automatically update the button when the count variable changes.
Excel is Declarative+Reactive
Even if the term is unfamiliar to you, you probably have experience with declarative programming if you have ever used a functions in Excel/Google Sheets. Here you define the value of a cell to be the =AVERAGE()
of some other cells. When the value of the other cells is updated the average automatically recomputes. In fact, the declarative style of Excel is one of the reasons people find it so easy to work with. Why not borrow some of that magic for your online web developmenet?
In the SFC code listing above, try to find where the displayed value of the count variable is updated in the template. You'll notice there is no such code because it is implicitly updated according to the {{ count }}
declaration in the template. This is the core idea of declarative rendering -- you define how the page should look based on underlying state data and the page automatically updates when the state data changes.
Of course, the question is how does the page know when the state data changes? In Vue.js this is handled by the concept of reactivity. In the SFC file above we imported the ref
function from Vue and used it to define the count
variable. This function makes the variable reactive. This means that when the value of the variable changes, Vue automatically updates the template to reflect the new value. Any variable you want to have automatically update its displayed value should be made reactive. Behind the scenes, this is done using a system of message passing and notifications where Vue keeps track of which parts of the template depend on which variables and updates them when the variables change. It's actually quite sophisticated to ensure the system works quickly even on complex websites (think Facebook news feed or the NYTimes) but amazingly you don't need know how it works under the hood. The important point from the perspective of a developer is that you don't have to write code to update the template when the underlying state changes.
Two-way binding β
An even more interesting version of this can create two-way binding between elements in a form. For example this component defines a reactive value called text
. In the template is defines a text input element input
which is "bound" to the value of text
using the v-model
directive. This means that when the the value of the input changes, it automatically updates the value of text
and vice versa. This is a powerful feature of Vue.js that can save a lot of time and code.
<script setup>
import { ref } from 'vue'
const text = ref('')
</script>
<template>
<input class="input" v-model="text" placeholder="Type here" />
<p><b>You typed: </b>{{ text }}</p>
</template>
<style scoped>
.input {
font-weight: bold;
}
</style>
An example rendering of this component is here:
You typed:
As you can see as you type the value of text
is updated in real time (due to the onChange event) and since this variable is reactive it also renders into the template in real time. In this case both retrieving the value of the input field and setting the value of the HTML template are done implicitly due to reactivity and declarative rendering.
Comparison to traditional Javascript development β
This development model is strikingly different than most Javascript development where you would have to write code to update the text of the button each time the count variable changes. For example, when writing custom JSPsych plugins the code for a similar effect might look like this:
plugin.trial = function (display_element, trial) {
var text = ''
var html = `<input class="input" name="text" id="myinput" placeholder="Type here" />`
html += `<p id="message"><b>You typed: </b>${text}</p>`
display_element.innerHTML = html
display_element
.querySelector('#myinput')
.addEventListener('onchange', function () {
handle_change()
})
function handle_change() {
let new_text = display_element.querySelector('#myinput').value
display_element.querySelector('#message').innerHTML =
`<b>You typed: </b>${new_text}`
}
}
with an additional entry in the global CSS definition like this to style the input:
#myinput {
font-weight: bold;
background-color: lightgray;
}
unpacking this code, first we add the input and text to the page using the
var html = `<input class="input" name="text" id="myinput" placeholder="Type here" />`
html += `<p id="message"><b>You typed: </b>${text}</p>`
display_element.innerHTML = html
This looks similar to the <template>
section of the Vue component. However, we still need to add the event handler to the jsPsych plugin example. We do this by selecting the #myinput
element and adding an event listener to to the click for onchange
events.
display_element
.querySelector('#myinput')
.addEventListener('onchange', function () {
handle_change()
})
In the Vue component, the event handler which listens to changes was implicitly registered using the v-model="text"
directive. Finally in the JSPsych example we have to handle the change by running a function the reads the current value of the input field and sets the HTML of the message to the new value.
function handle_change() {
let new_text = display_element.querySelector('#myinput').value
display_element.querySelector('#message').innerHTML =
`<b>You typed: </b>${new_text}`
}
This is also implicitly accomplished using the declarative rendering in the Vue template.
Conceptually these are not radically different -- in both cases we have to think about how our program reacts to inputs from the user. However, the JSPsych version, besides being more verbose, requires the programmer to manually coordinate more of the updating process (retrieving values and setting them in the display). This can be error-prone. As the complexity of a plugin/component grows the savings from declarative rendering and reactivity become more and more apparent. For example, if the same components was extended to include multiple form elements, each of a highly specialized type (e.g., date pickers, etc...) then the JSPsych version would require a lot of manual code to handle the updating of the display.
In addition, this example didn't use it but the CSS styling for a JSPsych plugin is defined in a different file from the plugin itself which can make it harder to update (e.g., you have to be careful to not call any other element #myinput
if you applied a style to it because CSS classes apply globally, where as in Vue they can be, optinally, scoped to apply to the current component only).
Building Vue components β
Components are a powerful way to organize and develop web applications. However, currently browsers do not natively know how to read and process .vue (Vue SFC) files (although there are proposals for native component support in browsers). How do they work then?
Vue applications use a build system that sort of "compiles" and "bundles" the code in the SFC into one or more .html, .js, and .css files that work in the way that browsers can understand. This is the role that Vite plays in π« Smile. Vite is a super-optimized build tool that can take the SFC files and turn them into a webpage that can be loaded in a browser. This aspect of Vite is similar to other build tools you might have heard of including Webpack, Parcel, or Rollup. It takes in the .vue file and outputs a .html, .js, and .css file you can load in a browser.
Building a website might seem like an unnecessary complexity, but is fairly common (if you've used Overleaf to write a paper you are familiar with letting a program "build" your paper into a PDF). It even offers several unique advantages.
- First, the build process can optimize the code for quick loading (e.g., when Vite processes the SFC files it can remove comments and whitespace, a process call "minifying", making the imported code smaller and faster to load over the web).
- Second, the build process can strip out unused functions from the libraries you use. So for example, the simplest method of using JSPsych you import the current version of the library which includes all the functions of JSPsych even if you never use them. In contrast, Vite, through it's build function can analyze your code and only include the parts of the library that you actually use. This is called tree-shaking and is a powerful way to reduce the size of your website.
- Third, the build process can code-split your website. This means that the build process can break your website into smaller pieces that can be loaded on demand or in parallel (breaking the code into multiple .js file "chunks"). This has a huge impact on the speed of your website/experiment and is especially important for collecting data from populations with slow internet connections.
- Finally, the Vite build process can be configured with plugins which do additional processing and manipulation of the input code. This is used in several key areas in this project.
Learning Vue β
Teaching the internals of Vue.js is beyond this particular guide. However, luckily Vue has a rich ecosystem of documentation and guides which can help (and also excellent documentation). The following are some useful pointers:
The main resources you will find helpful are to first step through this interactive tutorial to get some experience with Vue:
- Official Vue.js tutorial
Then read through the main docs (this is actually quite readable and informative):
- The Vue.js documentation
Here are some additional resources:
- If you are coming to Vue with experience in jQuery this guide comparing the two is interesting.
- Vue.js explained in 100 seconds
- Vue.js documentary about the lead developer
- Online school for learning Vue
- LearnVue a YouTube channel devoted to teaching Vue
- Long video with a guy walking through the code while developing a simple game in Vue
Finally, one very useful tool for learning about components is the Vue Single File Component Playground. On this page, you can write simple components see how they will render in real-time, and even build slightly larger components that include sub-components. It can be useful for learning setting up π« Smile on your computer and even can help engage students in the research process.
Component organization β
When you start developing your own components there are a few guidelines. First, components should be named using Pascal Case names (e.g., StatusBar.vue
or InformedConsentButton.vue
as opposed to statusbar.vue
(lowercase), statusBar.vue
(camel case) or status-bar.vue
(kebab case)). This is the official recommendation of the Vue documentation.
Second, components should be organized into folders based on the type of role the component plays. For this, π« Smile borrows sensibly from the organization of a typical experiment in psychology. In π« Smile, the components are organized in the src/components
directly which has the following layout:
src/components
βββ captcha
β βββ CaptchaInstructionsText.vue
β βββ CaptchaPage.vue
β βββ CaptchaTrialImageCategorization.vue
β βββ CaptchaTrialMotorControl.vue
β βββ CaptchaTrialStroop.vue
β βββ CaptchaTrialTextComprehension.vue
βββ consent
β βββ ConsentPage.vue
β βββ InformedConsentModal.vue
β βββ InformedConsentText.vue
βββ debrief
β βββ DebriefPage.vue
β βββ DebriefText.vue
βββ errors_withdraw
β βββ ReportIssueModal.vue
β βββ WithdrawFormModal.vue
β βββ WithdrawPage.vue
βββ instructions
β βββ InstructionsPage.vue
βββ navbars
β βββ DeveloperNavBar.vue
β βββ PresentationNavBar.vue
β βββ ProgressBar.vue
β βββ StatusBar.vue
βββ presentation_mode
β βββ PresentationModeHomePage.vue
βββ recruitment
β βββ AdvertisementPage.vue
β βββ MTurkRecruitPage.vue
β βββ RecruitmentChooserPage.vue
β βββ StudyPreviewText.vue
βββ screen_adjust
β βββ WindowSizerView.vue
βββ surveys
β βββ DemographicSurveyPage.vue
βββ tasks
β βββ ExpPage.vue
β βββ Task1Page.vue
β βββ Task2Page.vue
βββ thanks
βββ ThanksPage.vue
The following sections describe which types of components go in each folder. Based on what type of component you are developing, place the corresponding component file in the correct folder. This will help you stay organized and help other users of your code know where to look to find an element they might like to reuse in their projects.