Skip to content

Vue 01

Why Vue? I figured, what if I could just extract the part that I really liked about React and build something really lightweight without all the extra concepts involved? I was also curious as to how its internal implementation worked. I started this experiment just trying to replicate this minimal feature set, like declarative data binding. That was basically how Vue started.

-- Evan You

Like React.js - but simplified. Why Vue? Table comparison between Vue.js and React.js

Creating app

Vite based (10-100x faster dev server than Webpack, but has some issues)

npm create vue@latest
cd your_project
npm install
npm run dev

Basics

main.ts

1
2
3
4
5
import { createApp } from 'vue'
import App from './App.vue'

createApp(App)
    .mount('#app')

App.vue

1
2
3
4
5
6
7
8
<script setup lang="ts">
</script>

<template>
    OK
</template>

<style scoped></style>

Template in index.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <link rel="icon" href="/favicon.ico">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

Templating

Vue uses an HTML-based template syntax - under the hood, Vue compiles the templates into highly-optimized JavaScript code.

Two main ways how to write vue components:
- options api - composition api

Common style is to write single file components (SFC), known as *.vue files.
A Vue SFC encapsulates the component's logic (JavaScript), template (HTML), and styles (CSS) in a single file.

Options API

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<script lang="ts">
export default {
    data() {
        return {
            count: 0
        }
    },
    methods: {
        increment() {
            this.count++
        }
    },
    mounted() {
        console.log(`Initial ${this.count}.`)
    }
}
</script>

<template>
    <button @click="increment">Count: {{ count }}</button>
</template>

<style scoped></style>

Composition API

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<script setup lang="ts">
import { ref, onMounted } from 'vue'

const count = ref(0)
const increment = () => {
    count.value++;
}

onMounted(() => {
    console.log(`Initial ${count.value}`);
});
</script>

<template>
    <button @click="increment">Count: {{ count }}</button>
</template>

<style scoped></style>

Options Api - component instance (this is used). Maybe better for OOP people.
Composition Api - declaring reactive state variables directly (ref()).
Options API is implemented on top of the Composition API...

Official recommendation:
Go with Composition API + Single-File Components if you plan to build full applications with Vue.

Reactivity - ref()

1
2
3
import { ref } from 'vue'

const count = ref(0)

ref() takes the argument and returns it wrapped within a ref object with a .value property.

Typing ref

1
2
const year: Ref<string | number> = ref('2020')
const year = ref<string | number>('2020')

Reactivity - reactive()

ref() wraps the inner value in a special object. reactive() makes an object itself reactive.

1
2
3
4
5
6
7
8
import { reactive } from 'vue'

interface Book {
  title: string
  year?: number
}

const book: Book = reactive({ title: 'Vue 3 Guide' })

Computed properties

The computed() function expects to be passed a getter function, and the returned value is a computed ref.

1
2
3
4
// a computed ref
const publishedBooksMessage = computed(() => {
  return author.books.length > 0 ? 'Yes' : 'No'
})

Computed properties are cached based on their reactive dependencies.
Computed properties are by default getter-only.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const fullName = computed({
  // getter
  get() {
    return firstName.value + ' ' + lastName.value
  },
  // setter
  set(newValue) {
    // Note: we are using destructuring assignment syntax here.
    [firstName.value, lastName.value] = newValue.split(' ')
  }
})

NB!

Getters should be side-effect free.
Avoid mutating computed value.

Component Structure Overview

3 main parts - Template, Script, Style. Vue3 – allows multiple root elements in template.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<script setup lang="ts">
    const name = 'Dark Lord';
</script>
<template>
    <div class="hello">Hello, {{ name }}!</div>
</template>
<style scoped>
    .hello {
        color: #f00;
    }
</style>

Using component

Main style is SFC - single file component. Template and Script and Style in on file.

ButtonCounter.vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<script setup lang="ts">
import { ref, onMounted } from 'vue';

// reactive state
const count = ref(0);

// functions that mutate state and trigger updates
const increment = () => {
    count.value++;
}

// lifecycle hooks
onMounted(() => {
    console.log(`The initial count is ${count.value}.`);
})
</script>

<template>
    <button @click="increment">Count is: {{ count }}</button>
</template>

App.Vue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<script setup lang="ts">
import ButtonCounter from './components/ButtonCounter.vue'

const name = 'Dark Lord';
</script>

<template>
    <div class="hello">Hello, {{ name }}!</div>
    <br/>
    <ButtonCounter/>
</template>

<style scoped>
.hello {
    color: #f00;
}
</style>

Properties in components

1
2
3
4
5
6
7
8
<script setup lang="ts">
interface Props {
    foo: string
    bar?: number
}

const props = defineProps<Props>()
</script>

Default values for properties

1
2
3
4
5
6
7
8
9
export interface Props {
  msg?: string
  labels?: string[]
}

const props = withDefaults(defineProps<Props>(), {
  msg: 'hello',
  labels: () => ['one', 'two']
})

Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<script setup lang="ts">
import { ref, onMounted } from 'vue';
export interface Props {
  counter: number
}

const props = withDefaults(defineProps<Props>(), {
    counter: -5
})

// reactive state
const count = ref(props.counter);

// functions that mutate state and trigger updates
const increment = () => {
    count.value++;
}

// lifecycle hooks
onMounted(() => {
    console.log(`The initial count is ${count.value}.`);
})
</script>

<template>
    <button @click="increment">Count is: {{ count }}</button>
</template>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<script setup lang="ts">
import ButtonCounter from './components/ButtonCounter.vue'

const name = 'Dark Lord';
</script>

<template>
    <div class="hello">Hello, {{ name }}!</div>
    <br/>
    <ButtonCounter :counter=10 />
</template>

<style scoped>
.hello {
    color: #f00;
}
</style>

Directives in templates

  • Directives are using v- prefix. v-for, v-bind, v-on, v-if, etc.
  • Some directives take an argument, using colon v-on:click="methodName"
  • Shorthands v-bind - <a :href="url"> ... </a> v-on - <a @click="doSomething"> ... </a>

Bindings in template

  • Text interpolation with {{variableName}} <span>Message: {{ msg }}</span>
  • One-time interpolation with v-once <span v-once>This will never change: {{ msg }}</span>
  • Raw-html binding with v-html="variableName" <p>Using v-html directive: <span v-html="rawHtml"></span></p>
  • Binding in html attributes with v-bind="variableName" <div v-bind:id="dynamicId"></div>
  • Vue supports JavaScript expressions inside all data bindings <div v-bind:id="list- + id"></div>

Conditional rendering – v-if, v-else, v-else-if

Hidding element – v-show only toggles the display CSS property of the element.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<div v-if="type === 'A'">
    A
</div> 
<div v-else-if="type === 'B'">
    B
</div> 
<div v-else-if="type === 'C'"> 
    C 
</div> 
<div v-else>
    Not A/B/C 
</div>

Rendering a list

  • v-for directive to render a list of items based on an array. The v-for directive requires a special syntax in the form of item in items, where items is the source data array and item is an alias for the array element being iterated on. Use (item, index) to get counter also.
  • Also use v-for to iterate through the properties of an object.
  • v-for can also take an integer. In this case it will repeat the template that many times.
1
2
3
4
5
<ul id="example-2">
    <li v-for="(item, index) in items">
        {{ parentMessage }} - {{ index }} - {{ item.message }}
    </li> 
</ul>

Dom events

Bind to the dom events with v-on or with shortcut @. Use special variable to reference js event data.

1
2
3
4
5
6
7
8
9
<!-- using $event special variable -->
<button @click="warn('Form cannot be submitted yet.', $event)">
  Submit
</button>

<!-- using inline arrow function -->
<button @click="(event) => warn('Form cannot be submitted yet.', event)">
  Submit
</button>

Event modifiers

Vue provides event modifiers for v-on. Recall that modifiers are directive postfixes denoted by a dot.
- .stop - .prevent - .self - .capture - .once - .passive

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<!-- the click event's propagation will be stopped -->
<a @click.stop="doThis"></a>

<!-- the submit event will no longer reload the page -->
<form @submit.prevent="onSubmit"></form>

<!-- modifiers can be chained -->
<a @click.stop.prevent="doThat"></a>

<!-- just the modifier -->
<form @submit.prevent></form>

<!-- only trigger handler if event.target is the element itself -->
<!-- i.e. not from a child element -->
<div @click.self="doThat">...</div>

The .capture, .once, and .passive modifiers mirror the options of the native addEventListener method.

Form bindings

Manual vay

1
2
3
<input
  :value="text"
  @input="event => text = event.target.value">

Use the v-model directive instead to create two-way data bindings on form input, textarea, and select elements. It automatically picks the correct way to update the element based on the input type.

<input v-model="message" placeholder="edit me">

Custom events

A component can emit custom events directly in template expressions (e.g. in a v-on handler) using the built-in $emit method:

1
<button @click="$emit('someEvent')">click me</button>

And parent can listen

1
<MyComponent @some-event="callback" />

NB! Unlike native DOM events, component emitted events do not bubble.

Send and receive data with event

1
2
3
<button @click="$emit('increaseBy', 1)">
...
<MyButton @increase-by="(n) => count += n" />

$emit is not accessible in <script>section.

use defineEmits macro and emitfunction it returns.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<script setup lang="ts">
import { ref, onMounted } from 'vue'

const emit = defineEmits(['inFocus', 'submit']);

// type-based
const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()


const count = ref(0)
const increment = () => {
    count.value++;
    emit('inFocus');
}

Watchers

With Composition API, we can use the watch function to trigger a callback whenever a piece of reactive state changes.

1
2
3
4
5
6
7
8
const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
const loading = ref(false)

// watch works directly on a ref
watch(question, async (newQuestion, oldQuestion) => {
    // some code
})

watch's first argument can be different types of reactive "sources": it can be a ref (including computed refs), a reactive object, a getter function, or an array of multiple sources.

Component v-model

By default, v-model on a component uses modelValue as the prop and update:modelValue as the event. We can modify these names passing an argument to v-model:

1
<MyComponent v-model:title="bookTitle" />

and inside component

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<script setup>
defineProps(['title'])
defineEmits(['update:title'])
</script>

<template>
  <input
    type="text"
    :value="title"
    @input="$emit('update:title', $event.target.value)"
  />
</template>

Component content - slot

1
2
3
<FancyButton>
    Click me! <!-- slot content -->
</FancyButton>
1
2
3
4
5
<button class="fancy-btn">
    <slot>
        Submit <!-- fallback content -->
    </slot> <!-- slot outlet -->
</button>

NB! Slot content has access to the data scope of the parent component, because it is defined in the parent. Does not have access to child scope!

Vue Lifecycle

Vue lifecycle

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function onMounted(callback: () => void): void
function onUpdated(callback: () => void): void
function onUnmounted(callback: () => void): void
function onBeforeMount(callback: () => void): void
function onBeforeUpdate(callback: () => void): void
function onBeforeUnmount(callback: () => void): void

function onErrorCaptured(callback: ErrorCapturedHook): void
type ErrorCapturedHook = (
  err: unknown,
  instance: ComponentPublicInstance | null,
  info: string
) => boolean | void

function onActivated(callback: () => void): void
function onDeactivated(callback: () => void): void