Vue

Attention

These notes are written for Vue 2. Also see Vue 3

General

Common pitfalls

  • props default to optional unless required: true is specified!
  • computed property getters should not have side effects!
  • don't mix v-if and v-for on the same element!
  • don't change a value that's used in a v-if inside the beforeMount() hook - this can cause the DOM element to unload during render and cause an error!
    • use mounted() instead, or a Nuxt hook if you're using Nuxt and the value can be loaded server-side

Reactivity

Adding or removing reactive object properties

Watchers

Use a component method with a watcher

watch: {
    foo: 'fooChanged'
},
methods: {
    fooChanged() {
        this.$track('foo', this.foo)
    }
}

Don't use arrow functions for watchers

Immediate and deep watchers

watch: {
    foo: {
        handler(newVal, oldVal) {
            ...
        },
        immediate: true
    },
    'bar.baz': function (newVal, oldVal) {
        // triggers when property 'baz' of 'bar' changes
    },
    bar: {
        handler(newVal, oldVal) {
            // triggers when `bar` changes, or `bar.baz`, or `bar.baz.abc`
        },
        deep: true
    },
}

Conditional Rendering

Apply v-if or v-for to multiple elements using <template>

<template>
    <main>
        <template v-if="foo">
            <component :data="foo.bar" />
            <div>{{foo.baz}}</div>
        </template>
    </main>
</template>

Use v-for with a range

<span v-for="n in 10">{{ n }}</span>

Class and Style Bindings

<div :class="{ className: propertyName }"></div>

Combine object and array syntax in class bindings

<div :class="[{ className: propertyName }, anotherClassName]"></div>

Apply deep scoped (global) styles using ::v-deep

/* Vue 2 */
.component ::v-deep a {
	text-decoration: underline;
}

/* Vue 3 */
.component ::v-deep(a) {
	text-decoration: underline;
}

Event Handling

Access events in inline handlers with $event

<button @click="warn('Form cannot be submitted yet.', $event)"

Event modifiers

<input v-on:keyup.page-down="onPageDown">
<!-- will fire if Ctrl and Shift are held -->
<button @click.ctrl="onClick">Button</button>

<!-- will only fire if Ctrl and no other keys are held -->
<button @click.ctrl.exact="onClick">Button</button>

<!-- will only fire if no modifiers are held -->
<button @click.exact="onClick">Button</button>

Form Input Bindings

v-model Modifiers

Bind multiple checkboxes to an array

<!-- contains the values of the checked boxes -->
<div>Checked names: {{ checkedNames }}</div>

<input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
<label for="jack">Jack</label>

<input type="checkbox" id="john" value="John" v-model="checkedNames">
<label for="john">John</label>

<input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
<label for="mike">Mike</label>

Provide checkbox values using true-value and false-value

<!-- `toggle` will be the string 'yes' or 'no' -->
<input
  type="checkbox"
  v-model="toggle"
  true-value="yes"
  false-value="no" />

Props

Declare a prop that accepts multiple types

props: {
    year: [String, Number]
}

Spread props object onto component

const props = {
    name: 'Apple',
    color: 'red'
}
<Fruit v-bind="props" />
<!-- is equivalent to -->
<Fruit :name="props.name" :color="props.color" />

Two-way binding

Vue 2:

<MyComponent :name.sync="fullName" />

Vue 3:

<MyComponent v-model:name="fullName" />

Both are shorthand for:

<MyComponent :name="fullName" @update:name="name = $event" />

Slots

<!-- Component.vue -->
<template>
    <slot></slot>
    <slot name="namedSlot"></slot>
</template>

<!-- Page.vue -->
<template>
    <Component>
        <div>Default slot content</div>
        <template #namedSlot>Named slot content</template>
    </Component>
</template>

Bind child props to slots

<!-- Child -->

<div>
    <slot name="text" :message="message" :count="1"></slot>
</div>

<!-- Parent -->

<ChildComponent>
    <template #text="slotProps">{{slotProps.message}}</template>
</ChildComponent>

<!-- or if using the default slot -->

<ChildComponent v-slot="{ message, count }">{{message}}</ChildComponent>

Dynamic Components and KeepAlive

<KeepAlive include="a,b" OR :include="/a|b/" :max="10">
    <component :is="currentTabComponent" />
</KeepAlive>

TypeScript

Setup

import Vue from Vue
import type Axios from 'axios'
import type { NuxtCookies } from 'cookie-universal-nuxt'

declare module '*.vue' {
	export default Vue
}

// type plugins
declare module 'vue/types/vue' {
	interface Vue {
		$axios: Axios
	}
}

// type Nuxt context
declare module '@nuxt/types' {
	interface Context {
		$cookies: NuxtCookies
	}
}
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import type { MetaInfo } from 'vue-meta' // if using Nuxt
import type { User } from '~/types/types'

export default defineComponent({
  head(): MetaInfo {
    /* ... */
  },
  props: {
    user: {
      type: Object as PropType<User>
    }
  }
})
</script>

Ignore template errors

<!-- @vue-ignore -->
<input type="text" @change="handleChange($event.currentTarget.value)"></input>

Nuxt

Attention

These notes are written for Nuxt 2.

asyncData hook

async asyncData({ route }) {
    const posts = await getPostsForRoute(route.name)
    return {
        posts
    }
}

fetch hook

async fetch() {
    this.posts = await getPostsForRoute(this.$nuxt.context.route.name)
}

Load scripts per-page (vue-meta)

head() {
	return {
		script: [
			{
				src: 'https://example.com/script.js',
				callback: () => (this.scriptLoaded = true),
			},
		],
	}
},

Vuex

Mapping store properties from modules

export default {
    computed: {
        ...mapState({
            userId: (state) => state.user.id
        }),
        ...mapGetters({
            userName: 'user/name'
        })
    },
    methods: {
        ...mapMutations({
            setAddress: 'user/setAddress'
        }),
        ...mapActions({
            sendWelcomeEmail: 'user/sendWelcomeEmail'
        })
    }
}

See also