Skip to content

Vue Introduction

Vue is a versatile and beginner-friendly JavaScript framework. This introduction covers what is relevant to our codebase.

We use the Composition API with <script setup>, which is the modern recommended style for Vue 3.

A typical .vue file includes:

  • A <template> block for HTML markup (structure)
  • A <script lang="ts" setup> block for TypeScript logic (functionality)
  • A <style scoped> block for CSS styling (appearance)
<template>
<div class="flex flex-col gap-2">
<!-- Child content here -->
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<style scoped>
/* Component-specific styles */
</style>

Reactive state lets the script detect when a variable changes and update the HTML accordingly.
Use ref() for primitive values and reactive() for objects:

import { ref, reactive } from 'vue'
const count = ref(0)
const user = reactive({ name: 'Alice', age: 20 })
count.value++ // access .value in script
user.name = 'Bob' // direct access for reactive

In <template>, refs are auto-unwrapped (no .value needed):

<p>{{ count }}</p>

Create an element/do something for each element in a list.

<!-- Create an AutonWaypointItem for each waypoint in waypoints, and add the
corresponding attributes for each one -->
<AutonWaypointItem
v-for="(waypoint, index) in waypoints"
:key="waypoint.id"
:waypoint="waypoint"
:index="index"
@delete="removeWaypoint(index)"
/>

Only render the element if the condition is true.

<button
v-if="!enabled"
class="cmd-btn cmd-btn-danger"
@click="toggle"
>
Disabled
</button>
<button
v-else
class="cmd-btn cmd-btn-success"
@click="toggle"
>
Enabled
</button>

Bind a function to a DOM (Document Object Model) event:

<!-- handleClick() would be defined in <script>, and runs when button clicked -->
<button @click="handleClick">Click me</button>
<!-- Runs submitForm() when user presses "enter" in this textbox -->
<input @keydown.enter="submitForm" />

Derived values that automatically update when their dependencies change:

import { ref, computed } from 'vue'
const items = ref([1, 2, 3])
const total = computed(() => items.value.reduce((a, b) => a + b, 0))

Run side effects when a reactive value changes:

import { ref, watch } from 'vue'
const query = ref('')
// Prints the new "query" value whenever it changes
watch(query, (newVal) => {
console.log('Query changed:', newVal)
})

Run some code when the Vue component enters a new phase of it’s lifecycle.

import { onMounted, onBeforeUnmount } from 'vue'
onMounted(() => {
document.addEventListener('keydown', handleKey)
})
onBeforeUnmount(() => {
document.removeEventListener('keydown', handleKey)
})

Common hooks:

  • onMounted() - after component is added to the DOM/page
  • onBeforeUnmount() - before component is removed, for cleanup
  • onUpdated() - after any DOM update

With <script setup>, imported components are automatically available in the template without registration:

<template>
<GamepadDisplay :axes="axes" :buttons="buttons" />
<IndicatorDot :is-active="connected" />
</template>
<script lang="ts" setup>
import GamepadDisplay from './GamepadDisplay.vue'
import IndicatorDot from './IndicatorDot.vue'
</script>

We use Pinia (not Vuex) for shared state across components:

import { useWebsocketStore } from '@/stores/websocket'
const websocketStore = useWebsocketStore()
websocketStore.sendMessage('arm', { type: 'ra_controller', axes, buttons })

Pinia stores are defined in src/stores/ and accessed via composable functions (useXStore).