In the advanced NOVA LAB station systems, we sometimes need to directly reference DOM elements or pass data between components without props drilling. TypeScript helps us do this safely.
A template ref allows you to get a reference to a DOM element:
1<script setup lang="ts">
2import { ref, onMounted } from 'vue'
3
4// HTML element type — ref can be null before mounting
5const inputRef = ref<HTMLInputElement | null>(null)
6const canvasRef = ref<HTMLCanvasElement | null>(null)
7
8onMounted(() => {
9 // After mounting, ref is no longer null
10 inputRef.value?.focus()
11
12 const ctx = canvasRef.value?.getContext('2d')
13 if (ctx) {
14 ctx.fillStyle = '#00ff88'
15 ctx.fillRect(0, 0, 100, 100)
16 }
17})
18</script>
19
20<template>
21 <input ref="inputRef" placeholder="Enter command..." />
22 <canvas ref="canvasRef" width="400" height="300" />
23</template>The type
HTMLInputElement | null tells TypeScript that the ref can be null (before the component is mounted).You can also get a reference to a child component:
1<script setup lang="ts">
2import { ref } from 'vue'
3import SensorPanel from './SensorPanel.vue'
4
5// InstanceType<typeof Component> gives the component instance type
6const sensorRef = ref<InstanceType<typeof SensorPanel> | null>(null)
7
8function refreshSensors() {
9 // Access public methods of the component
10 sensorRef.value?.refresh()
11}
12</script>
13
14<template>
15 <SensorPanel ref="sensorRef" />
16 <button @click="refreshSensors">Refresh Sensors</button>
17</template>provide/inject allows passing data down the component tree. InjectionKey<T> ensures type safety:1<!-- types.ts -->
2<script lang="ts">
3import type { InjectionKey, Ref } from 'vue'
4
5export interface StationConfig {
6 name: string
7 maxCrew: number
8 sector: string
9}
10
11// Key with type — ensures provide and inject use the same type
12export const StationConfigKey: InjectionKey<StationConfig> = Symbol('StationConfig')
13export const ThemeKey: InjectionKey<Ref<'dark' | 'light'>> = Symbol('Theme')
14</script>1<!-- Provider (parent) -->
2<script setup lang="ts">
3import { provide, ref } from 'vue'
4import { StationConfigKey, ThemeKey } from './types'
5
6const theme = ref<'dark' | 'light'>('dark')
7
8provide(StationConfigKey, {
9 name: 'NOVA LAB',
10 maxCrew: 100,
11 sector: 'Alpha-7'
12})
13
14provide(ThemeKey, theme)
15</script>1<!-- Consumer (child/grandchild) -->
2<script setup lang="ts">
3import { inject } from 'vue'
4import { StationConfigKey, ThemeKey } from './types'
5
6// TypeScript knows the type! StationConfig | undefined
7const config = inject(StationConfigKey)
8
9// With a default value — it's not undefined
10const theme = inject(ThemeKey, ref('dark'))
11
12// We use optional chaining because config can be undefined
13console.log(config?.name) // 'NOVA LAB'
14console.log(theme.value) // 'dark'
15</script>To avoid
undefined, you can use a default value:1const config = inject(StationConfigKey, {
2 name: 'Unknown',
3 maxCrew: 0,
4 sector: 'N/A'
5})
6// Now config is always StationConfig (never undefined)You can also use a factory function:
1const config = inject(StationConfigKey, () => ({
2 name: 'Default',
3 maxCrew: 50,
4 sector: 'Beta'
5}), true) // third arg = true means it's a factory function