@Akryum
Vue.js Core Team
💡️ = community-made
Higher-level Frameworks
import Vue from 'vue'
import VueRouter from 'vue-router'
import Vuex from 'vuex'
import App from './App.vue'
Vue.use(VueRouter)
Vue.use(Vuex)
const router = new VueRouter(...)
const store = new Vuex.Store(...)
new Vue({
el: '#app',
router,
store,
// render: h => h(App),
...App,
})
import { createApp } from 'vue'
import { createRouter } from 'vue-router'
import { createStore } from 'vuex'
import App from './App.vue'
const router = createRouter(...)
const store = createStore(...)
const app = createApp(App)
app.use(router)
app.use(store)
app.mount('#app')
<template>
<div>
{{ date | formatDate }}
</div>
</template>
<template>
<div>
{{ formatDate(date) }}
</div>
</template>
<template>
<div>
{{ date |> formatDate(%) }}
</div>
</template>
In the future?
<template>
<MyComponent v-model="msg" />
</template>
export default {
model: {
prop: 'count',
event: 'update',
},
props: {
count: {
type: Number,
required: true,
},
},
emits: [
'update',
],
}
<template>
<MyComponent v-model="msg" />
</template>
export default {
props: {
modelValue: {
type: Number,
required: true,
},
},
emits: [
'update:modelValue',
],
}
<template>
<MyComponent v-model="msg" />
</template>
export default {
model: {
prop: 'count',
event: 'update',
},
props: {
count: {
type: Number,
required: true,
},
},
emits: [
'update',
],
}
<template>
<MyComponent v-model:count="msg" />
</template>
export default {
props: {
count: {
type: Number,
required: true,
},
},
emits: [
'update:count',
],
}
<template>
<MyComponent :count.sync="msg" />
</template>
export default {
props: {
count: {
type: Number,
required: true,
},
},
emits: [
'update:count',
],
}
<template>
<MyComponent v-model:count="msg" />
</template>
export default {
props: {
count: {
type: Number,
required: true,
},
},
emits: [
'update:count',
],
}
Nested component inheritance
<script>
export default {
inheritAttrs: false,
}
</script>
<template>
<SomeComponent
v-bind="$attrs"
>
<slot />
</SomeComponent>
</template>
<template>
<SomeComponent>
<slot />
</SomeComponent>
</template>
Class and style included in $attrs
<script>
export default {
inheritAttrs: false,
}
</script>
<template>
<section>
<input
v-bind="$attrs"
>
</section>
</template>
<script>
export default {
inheritAttrs: false,
}
</script>
<template>
<section
v-bind="{
class: $attrs.class,
style: $attrs.style,
}"
>
<input
v-bind="{
...$attrs,
class: null,
style: null,
}"
>
</section>
</template>
<template>
<MyComponent
type="password"
class="foobar"
/>
</template>
Class and style included in $attrs
Why? (Sneak peek at new features)
<script>
export default {
inheritAttrs: false,
}
</script>
<template>
<Teleport to="body">
<div v-bind="$attrs">
<slot />
</div>
</Teleport>
</template>
<template>
<div v-bind="$attrs">Div 1</div>
<div v-bind="$attrs">Div 2</div>
<div>Div 3</div>
</template>
Listeners fallthrough
<!-- MyComponent -->
<template>
<SomeChild />
</template>
<template>
<MyComponent
@click.native="onClick"
/>
</template>
<template>
<MyComponent
@click="onClick"
/>
</template>
Listeners fallthrough
<script>
export default {
inheritAttrs: false,
}
</script>
<template>
<div>
<input
v-bind="$attrs"
v-on="$listeners"
>
</div>
</template>
<script>
export default {
inheritAttrs: false,
}
</script>
<template>
<div>
<input
v-bind="$attrs"
>
</div>
</template>
<script>
const SomeAsyncComp = () => import('./SomeAsyncComp.vue')
export default {
components: {
SomeAsyncComp,
},
}
</script>
<script>
import { defineAsyncComponent } from 'vue'
const SomeAsyncComp = defineAsyncComponent(() => import('./SomeAsyncComp.vue'))
export default {
components: {
SomeAsyncComp,
},
}
</script>
<script>
import { h } from 'vue'
function DynamicHeading (props, context) {
return h(`h${props.level}`, context.attrs, context.slots)
}
export default {
components: {
DynamicHeading,
},
}
</script>
<template>
<section>
<DynamicHeading level="1">
Hello World
</DynamicHeading>
<DynamicHeading level="2">
Hello World
</DynamicHeading>
</section>
</template>
It's recommended not to use Event Bus pattern
import Vue from 'vue'
export const bus = new Vue()
bus.$on('event', data => {
console.log(data)
})
bus.$emit('event', { foo: 'bar' })
import mitt from 'mitt'
export const bus = mitt()
bus.on('event', data => {
console.log(data)
})
bus.emit('event', { foo: 'bar' })
It's recommended not to use Event Bus pattern
import Vue from 'vue'
export const bus = new Vue()
bus.$on('event', data => {
console.log(data)
})
bus.$emit('event', { foo: 'bar' })
import mitt from 'mitt'
export const bus = mitt()
bus.on('event', data => {
console.log(data)
})
bus.emit('event', { foo: 'bar' })
export default {
beforeDestroy () {
},
destroyed () {
},
}
export default {
beforeUnmount () {
},
unmounted () {
},
}
.v-enter,
.v-leave-to {
opacity: 0;
}
.v-leave,
.v-enter-to {
opacity: 1;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
.v-leave-from,
.v-enter-to {
opacity: 1;
}
<!DOCTYPE html>
<html>
<body>
<div id="app"/>
<script type="module" src="src/main.js"></script>
</body>
</html>
<template>
<div id="app">
<header>
<AppMenu />
</header>
<RouterView />
</div>
</template>
<template>
<header>
<AppMenu />
</header>
<RouterView />
</template>
Special version of Vue 3 with Flags to enable Vue 2 retro-compatibility on certain changes
(Also available in Vue 2.7)
(aka Fragments)
<script setup>
import { ref } from 'vue'
const showModal = ref(false)
</script>
<template>
<div
class="my-class"
v-bind="$attrs"
>
<button @click="showModal = true">
Open modal
</button>
</div>
<BaseModal v-if="showModal">
...
</BaseModal>
</template>
<script setup>
const emit = defineEmits([
'update',
])
function update () {
emit('update')
}
</script>
<template>
<button @click="update()">
Open modal
</button>
</template>
<script>
export default {
emits: [
'update',
],
methods: {
update () {
this.$emit('update')
},
},
}
</script>
<template>
<button @click="update()">
Open modal
</button>
</template>
(Also available in Vue 2.7)
<script setup>
import { ref, reactive } from 'vue'
const roses = ref('red')
const poem = reactive({
violets: 'blue',
})
</script>
<template>
<div class="my-class">Roses</div>
<div class="my-other-class">Violets</div>
</template>
<style scoped>
.my-class {
color: v-bind(roses);
}
.my-other-class {
color: v-bind('poem.violets');
}
</style>
move content to an element outside of the app
<template>
<div
class="my-class"
v-bind="$attrs"
>
<button @click="showModal = true">
Open modal
</button>
</div>
<Teleport to="body">
<BaseModal v-if="showModal">
...
</BaseModal>
</Teleport>
</template>
target late element (rendered during same tick - available next tick)
<template>
<div
class="my-class"
v-bind="$attrs"
>
<button @click="showModal = true">
Open modal
</button>
</div>
<Teleport to="#some-el-in-app" defer>
<BaseModal v-if="showModal">
...
</BaseModal>
</Teleport>
</template>
components to target even later elements
<template>
<header>
<RouterLink to="/">
Home
</RouterLink>
<nav class="my-toolbar">
<TeleportTarget id="toolbar" />
</nav>
</header>
<RouterView />
</template>
<template>
<div class="my-messages">
<SafeTeleport to="#toolbar">
<button>
Export messages
</button>
</SafeTeleport>
</div>
</template>
(experimental)
<!-- UserList.vue -->
<script setup>
const users = await fetch('...')
.then(r => r.json())
</script>
<template>
<User
v-for="user of users"
:key="user.id"
:user="user"
/>
</template>
<template>
<Suspense>
<Dashboard />
<template #fallback>
Loading...
</template>
</Suspense>
</template>
<template>
<div class="dashboard">
<UsersList />
</div>
</template>
<template>
{{ item.title }}
</template>
<script>
export default {
props: {
item: {
type: Object,
required: true,
},
},
methods: {
doSomething () {
console.log(this.item)
},
},
}
</script>
<MyComponent
v-bind:item="{ title: 'Hello '}"
/>
<MyComponent
:item="{ title: 'Hello '}"
/>
<script>
export default {
props: {
item: {
type: Object,
default: null,
},
otherItem: {
type: Object,
default: () => ({
foo: 'bar',
}),
},
answer: {
type: Number,
default: 42,
},
myCallback: {
type: Function,
default: () => () => { /* ... */ },
},
},
}
</script>
<script>
export default {
props: {
item: {
type: [Object, Array],
},
other: {
type: [String, Number],
},
answer: {
type: [Object, Array, String, Number],
},
},
}
</script>
<template>
Hello!
<button v-on:click="onClick">
Click me
</button>
</template>
<script>
export default {
emits: [
'update-item',
],
methods: {
onClick (event) {
this.$emit('update-item', 'toto', 42)
},
},
}
</script>
<MyComponent
v-for="item of items"
v-bind:key="item.id"
v-on:update-item="(param1, param2) => updateItem(item)"
/>
<template>
Hello!
<button v-on:click="onClick">
Click me
</button>
</template>
<script>
export default {
props: {
item: {
type: Object,
required: true,
},
},
emits: [
'update-item',
],
methods: {
onClick (event) {
this.$emit('update-item')
},
},
}
</script>
<MyComponent
v-for="item of items"
v-bind:key="item.id"
v-bind:item="item"
v-on:update-item="updateItem(item)"
/>
Props down
Events up
<script>
export default {
props: {
modelValue: {
type: String,
required: true,
},
},
emits: [
'update:modelValue',
],
}
</script>
<MyComponent
v-model="myText"
/>
<MyComponent
:modelValue="myText"
@update:modelValue="myText = $event"
/>
<script>
export default {
props: {
title: {
type: String,
default: '',
},
content: {
type: String,
default: '',
},
},
emits: [
'update:title',
'update:content',
],
}
</script>
<MyComponent
v-model:title="myTitle"
v-model:content="myContent"
/>
<MyComponent
v-bind:title="myTitle"
v-on:update:title="myTitle = $event"
v-bind:content="myContent"
v-on:update:content="myContent = $event"
/>
<template>
<h2>
Title
</h2>
<div>
Content:<br>
<slot />
</div>
</template>
<MyComponent>
Hello
</MyComponent>
<template>
<h2>
Title
</h2>
<div>
Content:<br>
<slot />
</div>
</template>
<MyComponent>
<template v-slot:default>
Hello
</template>
</MyComponent>
<template>
<h2>
Title
</h2>
<div>
Content:<br>
<slot>
Default content
</slot>
</div>
</template>
<MyComponent />
<template>
<h2>
<slot name="title" />
</h2>
<div>
Content:<br>
<slot>
Default content
</slot>
</div>
</template>
<MyComponent>
<template v-slot:default>
Hello
</template>
<template v-slot:title>
My own title
</template>
</MyComponent>
<MyComponent>
<template #default>
Hello
</template>
<template #title>
My own title
</template>
</MyComponent>
<template>
Dynamic component:
<component
:is="condition ? 'MyComponent' : 'AnotherComponent'"
v-bind:title="'Some title'"
/>
Dynamic HTML element:
<component
:is="condition ? 'div' : 'span'"
v-bind:title="'Some title'"
/>
</template>
<script>
import { defineAsyncComponent } from 'vue'
const MyAsyncComponent = defineAsyncComponent(() => import('./MyAsyncComponent.vue'))
export default {
component: MyAsyncComponent,
}
</script>
<template>
<MyAsyncComponent v-if="someCondition" />
</template>
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from './views/HomeView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView,
},
{
path: '/me/bookings',
name: 'my-bookings',
// route level code-splitting
// this generates a separate chunk (MyBookings.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('./views/MyBookings.vue'),
},
{
path: '/shop/:shopId/book',
name: 'create-booking',
component: () => import('./views/CreateBooking.vue'),
},
],
})
export default router
export default {
data() {
return {
message: 'hello!'
}
},
provide() {
// use function syntax so that we can access `this`
return {
message: this.message
}
}
}
export default {
inject: [
'message',
],
created() {
console.log(this.message) // injected value
}
}
{
"compilerOptions": {
"baseUrl": "./",
"target": "esnext",
"useDefineForClassFields": true,
"module": "esnext",
"moduleResolution": "node",
"isolatedModules": true,
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"paths": {
"@/*": ["src/*"]
},
"lib": ["esnext", "dom", "dom.iterable", "scripthost"],
"skipLibCheck": true
},
"include": ["vite.config.*", "env.d.ts", "src/**/*", "src/**/*.vue"]
}
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
// options here
})
</script>
<template>
Hello from TypeScript
</template>
import { defineComponent } from 'vue'
export default defineComponent({
// type inference enabled
props: {
name: String,
msg: { type: String, required: true }
},
data() {
return {
count: 1
}
},
mounted() {
this.name // type: string | undefined
this.msg // type: string
this.count // type: number
}
})
import { defineComponent, PropType } from 'vue'
interface MyItem {
id: string
label: string
price: number
}
export default defineComponent({
props: {
item: {
type: Object as PropType<MyItem>,
required: true,
},
},
mounted() {
this.item // type: MyItem
}
})
import { defineComponent } from 'vue'
interface MyItem {
id: string
label: string
price: number
}
export default defineComponent({
emits: {
'update-item': (item: MyItem) => true,
},
mounted () {
this.$emit('update-item', {
id: '123',
label: 'foo',
price: 123.45,
})
},
})
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
props: {
count: {
type: [Number, String],
required: true,
},
},
})
</script>
<template>
{{ count.toFixed(2) }}
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
props: {
count: {
type: [Number, String],
required: true,
},
},
})
</script>
<template>
{{ (count as number).toFixed(2) }}
</template>
// global.d.ts
import axios from 'axios'
declare module 'vue' {
interface ComponentCustomProperties {
$http: typeof axios
$i18n: (key: string) => string
}
}
import axios from 'axios'
app.config.globalProperties.$http = axios
app.config.globalProperties.$i18n = (id: string) => {
// ...
}
// global.d.ts
import BaseButton from '@/components/base/BaseButton.vue'
declare module 'vue' {
export interface GlobalComponents {
BaseButton: typeof BaseButton
}
}
Type-Check Vue components
Emit .d.ts files from Vue components
export default {
data () {
return {
searchText: '',
}
},
computed: {
filteredItems () {
// ...
},
},
}
Search ▶
Search ▶
export default {
data () {
return {
searchText: '',
sortBy: 'name',
}
},
computed: {
filteredItems () {
// ...
},
sortedItems () {
// ...
},
},
}
Search ▶
Search ▶
Sort ▶
Sort ▶
Features are organized by component options
Organized by component options
Organized by features
export default {
data () {
return {
searchText: '',
sortBy: 'name',
}
},
computed: {
filteredItems () {
// ...
},
sortedItems () {
// ...
},
},
}
Search ▶
Search ▶
Sort ▶
Sort ▶
Search ▶
Sort ▶
export default {
setup () {
// Search feature
const searchText = ref('')
const filteredItems = computed(() => /* ... */)
// Sort feature
const sortBy = ref('name')
const sortedItems = computed(() => /* ... */)
},
}
Search ▶
Sort ▶
export default {
setup () {
// Search feature
const searchText = ref('')
const filteredItems = computed(() => /* ... */)
// Sort feature
const sortBy = ref('name')
const sortedItems = computed(() => /* ... */)
},
}
Organized by feature
Search ▶
Sort ▶
export default {
setup () {
// Search feature
const searchText = ref('')
const filteredItems = computed(() => /* ... */)
// Sort feature
const sortBy = ref('name')
const sortedItems = computed(() => /* ... */)
return {
searchText,
sortBy,
sortedItems,
}
},
}
Search ▶
Sort ▶
export default {
setup () {
// Search feature
const searchText = ref('')
const filteredItems = computed(() => /* ... */)
// Sort feature
const sortBy = ref('name')
const sortedItems = computed(() => /* ... */)
return {
searchText,
sortBy,
sortedItems,
}
},
}
Expose to the template
Search ▶
Sort ▶
export default {
setup () {
const { searchText, filteredItems } = useSearch()
const { sortBy, sortedItems } = useSort(filteredItems)
return {
searchText,
sortBy,
sortedItems,
}
},
}
// Search feature
function useSearch (items) {
const searchText = ref('')
const filteredItems = computed(() => /* ... */)
return { searchText, filteredItems }
}
// Sort feature
function useSort (items) {
const sortBy = ref('name')
const sortedItems = computed(() => /* ... */)
return { sortBy, sortedItems }
}
Search ▶
Sort ▶
export default {
setup () {
const { searchText, filteredItems } = useSearch()
const { sortBy, sortedItems } = useSort(filteredItems)
return {
searchText,
sortBy,
sortedItems,
}
},
}
// Search feature
function useSearch (items) {
const searchText = ref('')
const filteredItems = computed(() => /* ... */)
return { searchText, filteredItems }
}
// Sort feature
function useSort (items) {
const sortBy = ref('name')
const sortedItems = computed(() => /* ... */)
return { sortBy, sortedItems }
}
Extract features into Composition Functions
export default {
data () {
return {
searchText: '',
sortBy: 'name',
}
},
computed: {
filteredItems () {
// ...
},
sortedItems () {
// ...
},
},
}
const searchMixin = {
data () {
return { searchText: '' }
},
computed: {
filteredItems () { /* ... */ },
},
}
const sortMixin = {
data () {
return { sortBy: 'name' }
},
computed: {
sortedItems () { /* ... */ },
},
}
export default {
mixins: [ searchMixin, sortMixin ],
}
const searchMixin = {
data () {
return { searchText: '' }
},
computed: {
filteredItems () { /* ... */ },
},
}
const sortMixin = {
data () {
return { sortBy: 'name' }
},
computed: {
sortedItems () { /* ... */ },
},
}
export default {
mixins: [ searchMixin, sortMixin ],
}
function searchMixinFactory ({ ... }) {
return {
data () {
return { searchText: '' }
},
computed: {
filteredItems () { /* ... */ },
},
}
}
function sortMixinFactory ({ ... }) {
return {
data () {
return { sortBy: 'name' }
},
computed: {
sortedItems () { /* ... */ },
},
}
}
export default {
mixins: [ searchMixinFactory(), sortMixinFactory() ],
}
function searchMixinFactory ({ prefix }) {
return {
data () {
return { [prefix + 'searchText']: '' }
},
computed: {
[prefix + 'filteredItems'] () { /* ... */ },
},
}
}
function sortMixinFactory ({ ... }) {
return {
data () {
return { sortBy: 'name' }
},
computed: {
sortedItems () { /* ... */ },
},
}
}
export default {
mixins: [ searchMixinFactory({prefix:'m'}), sortMixinFactory() ],
}
<script>
export default {
// Search feature here
}
</script>
<template>
<div>
<slot v-bind="{ searchText, filteredItems }" />
</div>
</template>
<script>
export default {
// Sort feature here
}
</script>
<template>
<div>
<slot v-bind="{ sortBy, sortedItems }" />
</div>
</template>
<SearchService
v-slot="searchProps"
>
<SortService
:item="searchProps.filteredItems"
v-slot="sortProps"
>
<li v-for="item of sortProps.sortedItems">
...
</li>
</SearchService>
</SearchService>
SearchService.vue
SortService.vue
<script>
export default {
// Search feature here
}
</script>
<template>
<div>
<slot v-bind="{ searchText, filteredItems }" />
</div>
</template>
<script>
export default {
// Sort feature here
}
</script>
<template>
<div>
<slot v-bind="{ sortBy, sortedItems }" />
</div>
</template>
<SearchService
v-slot="searchProps"
>
<SortService
:item="searchProps.filteredItems"
v-slot="sortProps"
>
<li v-for="item of sortProps.sortedItems">
...
</li>
</SearchService>
</SearchService>
export default {
data () {
return {
searchText: '',
sortBy: 'name',
}
},
computed: {
filteredItems () {
// ...
},
sortedItems () {
// ...
},
},
}
export default {
setup () {
const { searchText, filteredItems } = useSearch()
const { sortBy, sortedItems } = useSort(filteredItems)
return {
searchText,
sortBy,
sortedItems,
}
},
}
// Search feature
function useSearch (items) {
const searchText = ref('')
const filteredItems = computed(() => /* ... */)
return { searchText, filteredItems }
}
// Sort feature
function useSort (items) {
const sortBy = ref('name')
const sortedItems = computed(() => /* ... */)
return { sortBy, sortedItems }
}
export default {
setup () {
const { searchText, filteredItems } = useSearch()
const { sortBy, sortedItems } = useSort(filteredItems)
return {
searchText,
sortBy,
sortedItems,
}
},
}
// Search feature
function useSearch (items) {
const searchText = ref('')
const filteredItems = computed(() => /* ... */)
return { searchText, filteredItems }
}
// Sort feature
function useSort (items) {
const sortBy = ref('name')
const sortedItems = computed(() => /* ... */)
return { sortBy, sortedItems }
}
import { ref, watch } from 'vue'
export default {
setup () {
const count = ref(0)
function increment () {
count.value++
}
watch(count, value => {
console.log('count changed', value)
})
return {
count,
increment,
}
},
}
<script lang="ts">
import { ref } from 'vue'
export default {
setup () {
const count = ref(0)
function increment() {
count.value++
}
return {
count,
increment,
}
}
}
</script>
<template>
<button @click="increment">
{{ count }} <!-- no .value needed -->
</button>
</template>
import { reactive, watch } from 'vue'
export default {
setup () {
const countState = reactive({
count: 0,
})
function increment () {
countState.count++
}
watch(() => countState.count, value => {
console.log('count changed', countState.count)
})
return {
countState,
increment,
}
},
}
import { ref, computed, watch } from 'vue'
export default {
setup () {
const count = ref(0)
const double = computed(() => count.value * 2)
watch(double, value => {
console.log('double changed', value)
})
return {
double,
}
},
}
import { ref, watch } from 'vue'
export default {
props: {
initialCount: {
type: Number,
default: 0,
},
},
setup (props) {
const count = ref(props.initialCount)
watch(() => props.initialCount, value => {
count.value = value
})
return {
count,
}
},
}
// Search feature
function useSearch (items: Ref<MyItem[]>) {
const searchText = ref('')
const filteredItems = computed(() => items.value.filter(/* ... */))
return { searchText, filteredItems }
}
// Sort feature
function useSort (items: Ref<MyItem[]>) {
const sortBy = ref('label')
const sortedItems = computed(() => items.value.slice().sort(/* ... */))
return { sortBy, sortedItems }
}
import { toRefs } from 'vue'
import { useSearch, useSort } from './my-composables'
export default {
setup (props) {
const { items } = toRefs(props)
const { search, filteredItems } = useSearch(items)
const { sortBy, sortedItems } = useSort(filteredItems)
return {
search,
sortBy,
sortedItems,
}
}
}
<script lang="ts">
import { defineComponent, ref, computed } from 'vue'
export default defineComponent({
setup () {
// Search feature
const searchText = ref('')
const filteredItems = computed(() => /* ... */)
// Sort feature
const sortBy = ref('name')
const sortedItems = computed(() => /* ... */)
return {
searchText,
sortBy,
sortedItems,
}
},
})
</script>
<template>
<!-- ... -->
</template>
<script lang="ts" setup>
import { defineComponent, ref, computed } from 'vue'
export default defineComponent({
setup () {
// Search feature
const searchText = ref('')
const filteredItems = computed(() => /* ... */)
// Sort feature
const sortBy = ref('name')
const sortedItems = computed(() => /* ... */)
return {
searchText,
sortBy,
sortedItems,
}
},
})
</script>
<template>
<!-- ... -->
</template>
<script lang="ts" setup>
import { defineComponent, ref, computed } from 'vue'
// export default defineComponent({
// setup () {
// Search feature
const searchText = ref('')
const filteredItems = computed(() => /* ... */)
// Sort feature
const sortBy = ref('name')
const sortedItems = computed(() => /* ... */)
return {
searchText,
sortBy,
sortedItems,
}
// },
// })
</script>
<template>
<!-- ... -->
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
// Search feature
const searchText = ref('')
const filteredItems = computed(() => /* ... */)
// Sort feature
const sortBy = ref('name')
const sortedItems = computed(() => /* ... */)
return {
searchText,
sortBy,
sortedItems,
}
</script>
<template>
<!-- ... -->
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
// Search feature
const searchText = ref('')
const filteredItems = computed(() => /* ... */)
// Sort feature
const sortBy = ref('name')
const sortedItems = computed(() => /* ... */)
</script>
<template>
<!-- ... -->
</template>
<script lang="ts" setup>
const props = defineProps({
item: {
type: Object as PropType<MyItem>,
required: true,
},
})
console.log(props.item)
</script>
<template>
{{ item }}
</template>
<script lang="ts" setup>
const props = defineProps<{
item: MyItem
}>()
console.log(props.item)
</script>
<template>
{{ item }}
</template>
import { PropType } from 'vue'
interface MyItem {
id: string
label: string
price: number
}
const props = defineProps({
item: {
type: Object as PropType<MyItem>,
required: true,
},
})
const emit = defineEmits({
'update-item': (item: MyItem) => true,
})
function updateItem () {
emit('update-item', {
...props.item,
price: props.item.price + 1,
})
}
import { PropType } from 'vue'
interface MyItem {
id: string
label: string
price: number
}
const props = defineProps<{
item: MyItem
}>()
const emit = defineEmits<{
(e: 'update-item', item: MyItem): void
}>()
function updateItem () {
emit('update-item', {
...props.item,
price: props.item.price + 1,
})
}
React Hooks
Vue Composition API
<script setup>
const props = defineProps<{
item: Item
}>()
const emit = defineEmits<{
updateItem: [item: Item]
}>()
function onClick () {
emit('updateItem', props.item)
}
</script>
<template>
Hello!
<button v-on:click="onClick">
Click me
</button>
</template>
<MyComponent
v-for="item of items"
:key="item.id"
:item="item"
@update-item="onUpdateItem($event)"
/>
Props down
Events up
Child component
Parent component
<script lang="ts" setup>
import type { PropType } from 'vue'
interface MyItem {
id: string
label: string
price: number
}
const props = defineProps({
item: {
type: Object as PropType<MyItem>,
required: true,
},
})
props.item // MyItem
</script>
<script lang="ts" setup>
interface MyItem {
id: string
label: string
price: number
}
const props = defineProps<{
item: MyItem
}>()
props.item // MyItem
</script>
<script lang="ts" setup>
interface MyItem {
id: string
label: string
price: number
}
const props = defineProps<{
item: MyItem
optionalLabel?: string
}>()
props.item // MyItem
props.optionalLabel // string | undefined
</script>
<script lang="ts" setup>
interface MyItem {
id: string
label: string
price: number
}
const props = withDefaults(defineProps<{
item: MyItem
optionalLabel?: string
}>(), {
optionalLabel: 'default label',
})
props.item // MyItem
props.optionalLabel // string
</script>
<script lang="ts" setup>
interface MyItem {
id: string
label: string
price: number
}
const { item, optionalLabel = 'default label' } = defineProps<{
item: MyItem
optionalLabel?: string
}>()
console.log(item) // MyItem
console.log(optionalLabel) // string
watch(() => item, () => {}) // Use arrow function
</script>
<script lang="ts" setup>
interface MyItem {
id: string
label: string
price: number
}
const props = defineProps<{
item: MyItem
optionalLabel?: string
}>()
console.log(props.item)
console.log(props.optionalLabel)
watch(() => props.item, () => {})
</script>
Compiled to
<script lang="ts" setup>
import { onMounted } from 'vue'
interface MyItem {
id: string
label: string
price: number
}
const emit = defineEmits({
updateItem: (item: MyItem) => true,
})
onMounted(() => {
emit('updateItem', {
id: '123',
label: 'foo',
price: 123.45,
})
})
</script>
<script lang="ts" setup>
import { onMounted } from 'vue'
interface MyItem {
id: string
label: string
price: number
}
const emit = defineEmits<{
updateItem: [item: MyItem]
}>()
onMounted(() => {
emit('updateItem', {
id: '123',
label: 'foo',
price: 123.45,
})
})
</script>
<script lang="ts" setup>
import { ref, reactive } from 'vue'
import type { Ref } from 'vue'
interface MyItem {
id: string
label: string
price: number
}
const value = ref<string | number>('2022')
const state = reactive<{ item: MyItem | null }>({
item: null,
})
function foo (someRef: Ref<string | number>) {
console.log(someRef)
}
function bar (someState: { item: MyItem | null }) {
console.log(someState)
}
foo(value)
bar(state)
</script>
<script setup lang="ts">
import { useTemplateRef } from 'vue'
const input = useTemplateRef<HTMLInputElement>('my-input')
function focus () {
input.value?.focus()
}
</script>
<template>
<input ref="my-input" />
</template>
<!-- MyModal.vue -->
<script setup lang="ts">
import { ref } from 'vue'
const isContentShown = ref(false)
function open () {
isContentShown.value = true
}
defineExpose({
open,
})
</script>
<script setup lang="ts">
import { useTemplateRef } from 'vue'
import MyModal from './MyModal.vue'
type MyModalType = InstanceType<typeof MyModal>
const modal = useTemplateRef<MyModalType>('my-modal')
const openModal = () => {
modal.value?.open()
}
</script>
<template>
<MyModal ref="my-modal" />
</template>
Expose typed properties/methods
<script setup lang="ts">
import { useTemplateRef, ref, onMounted } from 'vue'
const list = ref([1, 2, 3])
const itemEls = useTemplateRef<HTMLLIElement[]>('items')
onMounted(() => {
console.log(itemEls.value)
})
</script>
<template>
<ul>
<li v-for="item in list" ref="items">
{{ item }}
</li>
</ul>
</template>
<template>
<h2>
Title
</h2>
<div>
Content:<br>
<slot />
</div>
</template>
<MyComponent>
Hello
</MyComponent>
<MyComponent>
<template v-slot:default>
Hello
</template>
</MyComponent>
Equivalent
Will be rendered here
<MyComponent>
<template #default>
Hello
</template>
</MyComponent>
Child component
Parent component
<template>
<h2>
Title
</h2>
<div>
Content:<br>
<slot>
Default text here
</slot>
</div>
</template>
<MyComponent />
Child component
Parent component
<template>
<h2>
<slot name="title" />
</h2>
<div>
Content:<br>
<slot />
</div>
<div>
<slot name="actions" />
</div>
</template>
<MyComponent>
<template #title>
My Title
</template>
Some content in default slot
<template #actions>
<MyButton />
</template>
</MyComponent>
Child component
Parent component
<script lang="ts" setup>
import { ref } from 'vue'
defineSlots<{
default(props: { msg: string }): any
}>()
const text = ref('Meow')
</script>
<template>
<div>
<slot :msg="text" />
</div>
</template>
<MyComponent v-slot="props">
{{ props.msg }}
</MyComponent>
Child component
Parent component
<MyComponent>
<template #default="props">
{{ props.msg }}
</template>
</MyComponent>
<MyComponent>
<template #default="{ msg }">
{{ msg }}
</template>
</MyComponent>
Slot props
<script lang="ts" setup>
defineProps<{
modelValue: string
}>()
defineEmits<{
'update:modelValue': [newValue: string]
}>()
</script>
<MyComponent
v-model="myText"
/>
<MyComponent
:modelValue="myText"
@update:modelValue="myText = $event"
/>
<script lang="ts" setup>
const model = defineModel<string>({
required: true
})
</script>
<script lang="ts" setup>
defineProps<{
shown: boolean
}>()
defineEmits<{
'update:shown': [newValue: boolean]
}>()
</script>
<MyModal
v-model:shown="isModalShown"
/>
<MyModal
:shown="isModalShown"
@update:shown="isModalShown = $event"
/>
<script lang="ts" setup>
const shown = defineModel<boolean>('shown')
</script>
<script setup>
import { ref } from 'vue'
import MyComponent from './MyComponent.vue'
import AnotherComponent from './AnotherComponent.vue'
const condition = ref(true)
</script>
<template>
Dynamic component:
<component
:is="condition ? MyComponent : AnotherComponent"
:title="'Some title'"
/>
Dynamic HTML element:
<component
:is="condition ? 'div' : 'span'"
v-bind:title="'Some title'"
/>
</template>
Component
Component
Component
Component
Props
Props
Component
Component
Component
Component
const result = inject(key)
provide(key, { ... })
<script lang="ts" setup>
import { provide } from 'vue'
import { key } from './key'
provide(key, {
id: 'abc',
label: 'Computer',
price: 1_000,
})
</script>
// key.ts
import type { InjectionKey } from 'vue'
export interface MyItem {
id: string
label: string
price: number
}
export const key = Symbol() as InjectionKey<MyItem>
<script lang="ts" setup>
import { inject } from 'vue'
import { key } from './key'
const item = inject(key) // MyItem | undefined
</script>
typing information
injected value
Parent component
Child component
<script lang="ts" setup>
import { inject } from 'vue'
import { key } from './key'
import type { MyItem } from './key'
const item = inject(key) // MyItem | undefined
const item2 = inject<MyItem>('some-string-key') // MyItem | undefined
const sureItem = inject('some-string-key') as MyItem // MyItem
const text = inject<string>('some-other-key') // string | undefined
const textWithDefault = inject<string>('some-other-key', 'default value') // string
</script>
<script setup>
import { defineAsyncComponent } from 'vue'
const MyAsyncComponent = defineAsyncComponent(() => import('./MyAsyncComponent.vue'))
const someCondition = ref(false)
</script>
<template>
<MyAsyncComponent v-if="someCondition" />
</template>
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from './views/HomeView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView,
},
{
path: '/me/bookings',
name: 'my-bookings',
// route level code-splitting
// this generates a separate chunk (MyBookings.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import('./views/MyBookings.vue'),
},
{
path: '/shop/:shopId/book',
name: 'create-booking',
component: () => import('./views/CreateBooking.vue'),
},
],
})
export default router
import { createStore } from 'vuex'
const store = createStore({
state () {
return {
count: 0
}
},
mutations: {
increment (state) {
state.count++
}
}
})
app.use(store)
export default {
methods: {
increment() {
this.$store.commit('increment')
console.log(this.$store.state.count)
},
},
}
import { mapState, mapMutations } from 'vuex'
export default {
computed: {
...mapState([
'count',
]),
},
methods: {
...mapMutations([
'increment',
]),
},
}
const store = createStore({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
doneTodos (state) {
return state.todos.filter(todo => todo.done)
}
}
})
<script>
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
// mix the getters into computed
...mapGetters([
'doneTodos',
// ...
])
}
}
</script>
<template>
{{ doneTodos }}
</template>
const store = createStore({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
// We can do async ops and multiple commits here
context.commit('increment')
},
},
})
import { mapActions } from 'vuex'
export default {
// ...
methods: {
...mapActions([
'increment',
// ...
])
}
}
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... },
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
}
const store = createStore({
modules: {
a: moduleA,
b: moduleB,
},
})
import { mapGetters, mapActions } from 'vuex'
export default {
computed: {
...mapGetters('a', [
// Getters from module 'a'
]),
...mapGetters('b', [
// Getters from module 'b'
]),
},
methods: {
...mapActions('a', [
// Actions from module 'a'
]),
...mapActions('b', [
// Actions from module 'b'
]),
},
}
import { createPinia } from 'pinia'
app.use(createPinia())
import { defineStore } from 'pinia'
export const useShopStore = defineStore('shops', {
state: () => ({
loading: false,
error: null as Error | null,
shops: [] as Shop[],
}),
getters: {
shopsCount: state => state.shops.length,
},
actions: {
async fetchShops () {
this.loading = true
this.error = null
try {
const response = await $fetch('/shops')
this.shops = response.data
} catch (e: any) {
this.error = e
} finally {
this.loading = false
}
},
},
})
import { defineStore } from 'pinia'
export const useShopStore = defineStore('shops', () => {
const loading = ref(false)
const error = ref<Error | null>(null)
const shops = ref<Shop[]>([])
const shopsCount = computed(() => state.shops.length)
async function fetchShops () {
loading.value = true
error.value = null
try {
const response = await $fetch('/shops')
shops.value = response.data
} catch (e: any) {
error.value = e
} finally {
loading.value = false
}
}
return {
loading,
error,
shops,
fetchShops,
}
})
<script lang="ts" setup>
import BaseLoading from './base/BaseLoading.vue'
import BaseError from './base/BaseError.vue'
import ShopItem from './ShopItem.vue'
import { useShopStore } from '@/stores/shop'
const shopsStore = useShopStore()
shopsStore.fetchShops()
</script>
<template>
<BaseLoading v-if="shopsStore.loading">
Loading...
</BaseLoading>
<BaseError v-if="shopsStore.error">
{{ shopsStore.error.message }}
</BaseError>
<ShopItem
v-for="shop in shopsStore.shops"
:key="shop.id"
:shop="shop"
/>
</template>
Inputs | Examples |
---|---|
Interations | Clicking, typing... any "human" interaction |
Props | The arguments a component receives |
Data streams | Data incoming from API calls, data subscriptions… |
Outputs | Examples |
---|---|
DOM elements | Any observable node rendered to the document |
Events | Emitted events (using $emit) |
Side Effects | Such as console.log or API calls |
Everything else is implementation details.
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import HelloWorld from '../HelloWorld.vue'
describe('HelloWorld', () => {
it('renders properly', () => {
const wrapper = mount(HelloWorld, {
props: {
msg: 'Hello Vitest',
},
})
expect(wrapper.text()).toContain('Hello Vitest')
})
})
describe('HelloWorld', () => {
it('handles input', async () => {
const wrapper = mount(HelloWorld)
await wrapper.get('input').setValue('Hello Vitest')
expect(wrapper.text()).toContain('Hello Vitest')
})
})
describe('HelloWorld', () => {
it('increments', async () => {
const wrapper = mount(HelloWorld)
await wrapper.get('button').trigger('click')
expect(wrapper.text()).toContain('2')
})
})
import { vi } from 'vitest'
vi.mock('lodash/debounce', () => {
return {
default: (cb: any) => cb,
}
})
import { afterAll, afterEach, beforeAll, describe } from 'vitest'
import { setupServer } from 'msw/node'
import { rest } from 'msw'
import { shops } from './fixtures/shops'
const baseURL = 'http://localhost:4000'
const server = setupServer(
rest.get(`${baseURL}/shops`, (req, res, ctx) => {
if (req.url.searchParams.get('q') === 'cat') {
return res(ctx.status(200), ctx.json(shops.slice(0, 1)))
}
return res(ctx.status(200), ctx.json(shops))
}),
// other handlers...
)
describe('shop store', () => {
beforeAll(() => {
server.listen({ onUnhandledRequest: 'error' })
})
afterEach(() => {
server.resetHandlers()
})
afterAll(() => {
server.close()
})
// tests here...
})
import { describe, expect, test, vi } from 'vitest'
import { flushPromises, mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import { useShopStore } from '@/stores/shop'
import { shops } from './fixtures/shops'
describe('ShopsList', () => {
test('displays shop items', async () => {
const wrapper = mount(ShopsList, {
global: {
plugins: [createTestingPinia({
createSpy: () => vi.fn(),
})],
},
})
const store = useShopStore()
store.shops = shops
await flushPromises()
expect(wrapper.findAllComponents(ShopItem).length).toBe(3)
expect(store.fetchShops).toHaveBeenCalled()
})
})
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, test } from 'vitest'
import { useShopStore } from '../shop'
describe('shop store', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
test('loads shops', async () => {
const store = useShopStore()
const promise = store.fetchShops()
expect(store.loading).toBe(true)
await promise
expect(store.shops.length).toBe(3)
expect(store.loading).toBe(false)
expect(store.error).toBe(null)
})
})
Server
App
Render
<div> Hello world! </div>
Server
App
Render
<html>
<ul>
<li>James Holden</li>
<li>Naomi Nagata</li>
</ul>
</html>
Database
Read
Browser
Request page
Send HTML
<html>
<a>Roccinante crew</a>
</html>
<html>
<ul>
<li>James Holden</li>
<li>Naomi Nagata</li>
</ul>
</html>
Replace the whole page
Request page
Receiving page
Received page
Click button
Request page
Database Read
Receiving page
Received page
Server
API
Database
Read
Browser
Request page
Send JSON
<html>
<div id="app">
<a>Roccinante crew</a>
</div>
</html>
<html>
<div id="app">
<ul>
<li>James Holden</li>
<li>Naomi Nagata</li>
</ul>
</div>
</html>
CDN
HTML
JS
Request API
Request script
<html>
<div id="app"></div>
</html>
<html>
<div id="app">
<div class="loading"/>
</div>
</html>
App
Request page
Received JS
Click button
Change route
Request API
Database Read
Received JSON
Received page
Request JS
Server
API
Database
Read
Browser
Request page
Send JSON
<html>
<div id="app">
<a>Roccinante crew</a>
</div>
</html>
<html>
<div id="app">
<ul>
<li>James Holden</li>
<li>Naomi Nagata</li>
</ul>
</div>
</html>
CDN
JS
Request API
Request script
<html>
<div id="app">
<div class="loading"/>
</div>
</html>
Hydration
Server
App
JS
App
Request page
Received JS
Click button
Change route
Request API
Database Read
Received JSON
Receiving page
Received page
Request JS
Server
API
Database
Read
Browser
Request page
Send JSON
<html>
<div id="app">
<ul>
<li>James Holden</li>
<li>Naomi Nagata</li>
</ul>
</div>
</html>
CDN
JS
Request API
Request script
Server
App
Hydration
JS
App
Request page
Received page
Request JS
Received JS
Database Read
Receiving page
Node.js Server Framework
HTTP Server
Bundler
Command-line interface
pnpx nuxi init <project-name>
assets
processed by bundler
components
auto-imported
composables
auto-imported
layouts
wraps page content (<NuxtLayout>)
middleware
executed during route navigation
pages
generates routes
plugins
app setup
public
static assets
server
API (backend)
utils
auto-imported
app.vue
main component
nuxt.config.ts
Nuxt configuration