one of the 3 big JavaScript UI frameworks (besides React, Angular)
recommended:
generic compontent definition in a .vue file:
<template>
...
</template>
<script>
...
</script>
<style scoped>
...
</style>
<div>
<button @click="imgId = 0">start</button>
<button @click="prevImg()">previous</button>
<img :src="imgUrl" alt="slideshow" />
<button @click="imgId++">next</button>
</div>
export default {
name: 'MySlideshow',
data() {
return { imgId: 0 };
},
computed: {
imgUrl() {
return `https://picsum.photos/200?image=${this.imgId}`;
},
},
methods: {
prevImg() {
if (this.imgId > 0) {
this.imgId--;
}
},
},
};
a component definition object has several specific props / methods:
Entries in data, methods and computed, ... are available via this.entryname
in the script and via entryname
in the template
Component state is initialized via the data
method
state entries are reactive, meaning Vue can react if they change (and update the component rendering accordingly)
Methods are functions associated with a component
computed
can compute derived datacomputed
are automatically called when one of their dependencies changesHow does Vue know when to re-evaluate a computed value?
During the first computation Vue tracks which state entries are accessed - these will subsequently act as triggers for updates
<div>A year has {{365 * 24}} hours.</div>
short form:
<a :href="'https://en.wikipedia.org/wiki/' + topic">
some article
</a>
long form:
<a v-bind:href="'https://en.wikipedia.org/wiki/' + topic">
some article
</a>
special syntax for the class property:
<div :class="{todo: true, completed: isCompleted}">...</div>
special syntax for the style property:
<div :style="{padding: '8px', margin: '8px'}">...</div>
<div :style="{color: completed ? 'grey' : 'black'}">
...
</div>
short form:
<button @click="alert('hello')">Say Hello</button>
long form:
<button v-on:click="alert('hello')">Say Hello</button>
event modifiers:
preventing default:
<form @submit.prevent="handleSubmit()">...</form>
accessing the event object via $event
:
<button @click="handleClick($event)">handle event</button>
<div v-if="request.loading">Loading...</div>
<div v-else-if="request.error">Error while loading</div>
<div v-else>
<h1>Results</h1>
...
</div>
<ul>
<li v-for="todo in todos" :key="todo.id">
{{todo.title}}
</li>
</ul>
Each repeated element should have a locally unique key property (for efficiency)
explicit two-way binding for inputs:
<input :value="title" @input="title = $event.target.value" />
short-hand version for two-way binding:
<input v-model="firstName" />
By default, whitespace between elements will not be rendered by Vue
<strong>no</strong> <em>space</em>
forcing a space:
<strong>with</strong>{{ " " }}<em>space</em>
Tasks:
Create a todo list application with the following functionality:
<div>
<h1>Todo</h1>
<form @submit.prevent="addTodo()">
<input v-model="newTitle" />
<button type="submit">add</button>
</form>
<ul>
<li v-for="todo in todos" :key="todo.id">
{{ todo.title }}
</li>
</ul>
</div>
export default {
name: 'TodoList',
data() {
return {
newTitle: '',
todos: [
{ id: 1, title: 'groceries', completed: false },
{ id: 2, title: 'taxes', completed: true },
],
};
},
methods: {
onSubmit() {
this.addTodo();
this.newTitle = '';
},
addTodo() {
let maxId = 0;
for (let todo of this.todos) {
maxId = Math.max(maxId, todo.id);
}
this.todos.push({
id: maxId + 1,
title: this.newTitle,
completed: false,
});
},
},
};
plugins for Chrome, Firefox and Edge
features:
possible approach when looking for issues:
open source IDE
independent of Visual Studio itself
F1 or Ctrl + Shift + P: display command palette
example commands:
Via File - Preferences - Settings
Is split into User Settings and Workspace Settings
Recommendations:
\n
Further options:
open the extensions view in the sidebar: fifth symbol on the left
recommended extensions for Vue development:
Linter with more functionality than VS Code's default linter
a tool for initializing a Vue project
install and run:
npm install -g @vue/cli
vue create my-app
or run directly (without installing):
npx @vue/cli create my-app
Creates a simple Vue app which can be used as a starting point
many aspects can be set up automatically:
configuration:
don't fail on ESLint errors:
new file vue.config.js:
module.exports = {
lintOnSave: 'warning',
};
Check imports for errors:
npm install eslint-plugin-import
in the eslint config, add "plugin:import/recommended"
to "extends"
inside the project directory:
npm run serve
: starts the local development servernpm run build
: creates a build (for deployment)A Vue project can be hosted on any static hosting service
build with Vue CLI:
npm run build
minified and bundled app is created in the dist folder
simple options for trying out deployment options without login:
Custom components must be listed in the components entry of the component they are used in:
import TodoItem from './TodoItem';
import AddTodo from './AddTodo';
export default {
components: { TodoItem, AddTodo },
// ...
};
Custom components can typically be written in two ways:
recommended:
<TodoItem />
<VBtn>foo</VBtn>
alternative:
<todo-item />
<v-btn>foo</v-btn>
for Vue 2:
for Vue 3:
add vuetify 2 to a vanilla Vue CLI project:
npx @vue/cli add vuetify
(note: will overwrite App.vue and some other files)
make sure your application in App.vue is enclosed in a <v-app>
component:
<template>
<v-app>
...
</v-app>
</template>
example component use:
<v-btn color="primary" type="submit">add</v-btn>
<v-text-field v-model="newTitle" label="new title" />
<v-app>
<v-app-bar app>
<v-toolbar-title>Todo</v-toolbar-title>
</v-app-bar>
<v-main>
...
</v-main>
<v-footer app>Todo App by Marko</v-footer>
</v-app>
v-container: responsive container with horizontal margins for spacing
<v-main>
<v-container>
...
</v-container>
</v-main>
Vuetify comes with a 12-column grid system (similar to bootstrap)
two columns with the same size:
<v-row>
<v-col>foo</v-col>
<v-col>bar</v-col>
</v-row>
configuring the width:
<v-row>
<v-col :cols="12" :sm="6" :md="3">
<v-text-field v-model="newTitle" label="new title" />
</v-col>
<v-col>
<v-btn color="primary" type="submit">Add</v-btn>
</v-col>
</v-row>
:cols="12"
- will take up all 12 columns on the smallest screens:sm="6"
- will take up 6 of 12 columns on small or larger screens:md="3"
- will take up 3 columns on medium or larger screenspossible file name patterns for component files:
component names should always be multiple words (to distinguish them from built-in elements)
recap: important props in component definitions:
a component may define an interface to interact with its parent component:
Examples:
<ProgressBar percentage={75} color="lightgreen" />
<Rating :stars="3" />
Any props of a component must be listed in the component config:
export default {
props: ['value', 'color'],
};
They are then accessible in the same ways as entries in data
, methods
, ...
multi-word props should use mixedCase in JavaScript, but kebab-case in templates
props: ['greetingText']
<WelcomeMessage greeting-text="hi" />
events have a name and potentially contain some data ("payload(s)")
example: handling component events
<StarsRating
:value="rating1"
@update:value="rating1 = $event"
/>
<TodoItem
:todo="todo"
@delete="deleteTodo($event)"
@update:completed="changeTodoCompleted($event)"
/>
Events are emitted from a child component via:
this.$emit('eventname', payload);
example: StarsRating
:
recap: inputs:
explicit two-way binding for inputs:
<input value="title" @input="title = $event.target.value" />
short-hand version for two-way binding:
<input v-model="firstName" />
custom components in Vue 3:
<StarsRating
:modelValue="rating1"
@update:modelValue="rating1 = $event"
/>
short form:
<StarsRating v-model="rating1" />
custom components in Vue 2:
<StarsRating :value="rating1" @input="rating1 = $event" />
short form:
<StarsRating v-model="rating1" />
Task: split the todo app into smaller components (e.g. TodoList, TodoItem, AddTodo)
Task: extract reusable components that are mainly used for styling - e.g. a Button
component or a TextInput
component
we may create components that can be used like this:
<DialogModal type="error">
<p>Changes could not be saved</p>
<button>OK</button>
</DialogModal>
template for DialogModal
:
<div :class="{dialog: true, dialogType: type}">
<slot></slot>
</div>
Composition API: component logic is defined in a setup
method
Composition API compared to the traditional options API:
options API:
export default {
name: 'TodoApp',
data() {
return {
todos: [],
newTitle: '',
};
},
methods: {
/*...*/
},
computed: {
/*...*/
},
};
composition API:
import { ref, reactive, computed } from 'vue';
export default {
name: 'TodoApp',
setup() {
const todos = reactive([]);
const newTitle = ref('');
function addTodo() {
// ...
}
const numActive = computed(
() => todos.filter((t) => !t.completed).length
);
return { todos, newTitle, addTodo, numActive };
},
};
possible restructuring of code:
function useTodos() {
// manages todos
}
export default {
name: 'TodoApp',
setup() {
const newTitle = ref('');
const { todos, addTodo, numActive } = useTodos();
return { todos, addTodo, numActive, newTitle };
},
};
VueUse: collection of pre-defined Vue composition functions:
https://github.com/vueuse/vueuse
examples:
two mechanisms:
ref
for primitive / immutable data (strings, numbers, ...)reactive
for objects, arrays, ...initializing a ref:
const newTitle = ref('');
reading / writing from script code:
const a = newTitle.value;
newTitle.value = 'foo';
reading / writing from the template:
<div>new title is {{newTitle}}</div>
<button @click="newTitle = 'foo'">set to foo</button>
<div>
<h1>Todo</h1>
<form @submit.prevent="addTodo()">
<input v-model="newTitle" />
<button type="submit">add</button>
</form>
<ul>
<li v-for="todo in todos" :key="todo.id">
{{ todo.title }}
</li>
</ul>
</div>
import { ref, reactive } from 'vue';
export default {
name: 'TodoApp',
setup() {
const newTitle = ref('');
const todos = reactive([
{ id: 1, title: 'groceries', completed: false },
{ id: 2, title: 'taxes', completed: true },
]);
function onSubmit() {
addTodo(newTitle.value);
newTitle.value = '';
}
function addTodo(title) {
const maxId = Math.max(0, ...todos.map((t) => t.id));
todos.push({
id: newId,
title: newTitle.value,
completed: false,
});
}
return { newTitle, todos, onSubmit };
},
};
Create a slideshow component / application that displays images like this:
https://picsum.photos/300/200?image=0
The setup
method receives two arguments: props
and context
Example: rating component:
export default {
name: 'StarsRating',
props: ['value'],
setup(props, context) {
const ariaLabel = computed(
() => `${props.value} out of 5 stars`
);
const onStarClick = (id) => {
context.emit('change', id);
};
return { ariaLabel, onStarClick };
},
};
promises: modern way of handling asynchronous code:
async
/ await
.then()
modern ways of sending network requests:
fetch()
(included in browsers)asynchronous function that fetches todos from an API:
// todosApi.ts
async function fetchTodos(): Promise<Array<Todo>> {
const url = 'https://jsonplaceholder.typicode.com/todos';
const res = await fetch(url);
const apiTodos = await res.json();
// convert data format (don't include userId)
const todos = apiTodos.map((todo) => ({
id: todo.id,
title: todo.title,
completed: todo.completed,
}));
return todos;
}
asynchronous function that fetches an exchange rate from an API:
async function fetchExchangeRate(
from: string,
to: string
): Promise<number> {
const res = await fetch(
`https://api.exchangerate.host/convert?from=${from}&to=${to}`
);
const data = await res.json();
return data.result;
}
when component props / state change:
"main effect": component (re-)renders with current data
potential "side effects": triggering API queries, saving data, explicitly manipulating the DOM, ...
Typically, we will want to trigger side effects when some specific props / state or other data have changed or when the component is rendered for the first time
typical use cases for side effects:
common triggers for a side effect:
side effects in the options API:
created
watch
functionside effects in the composition API (variant 1):
setup
watch
functionside effects in the composition API (variant 2):
watchEffect
function: runs initially and on any watched changesOptions API: Component methods that are called when certain lifecylce events occur:
created
mounted
updated
destroyed
Composition API: equivalent functions
created
→ include in setup
mounted
→ onMounted
updated
→ onUpdated
destroyed
→ onUnmounted
Fetching data on component mount (options API):
export default {
// ...
async created() {
const res = await fetch(
'https://jsonplaceholder.typicode.com/todos'
);
const todos = await res.json();
this.todos = todos;
},
};
The watchEffect
function can be used to perform side effects when a component is mounted for the first time or when its props / state have changed.
It is useful for querying web APIs, setting up timers, persisting data to localStorage, ...
Example: load SpaceX launch data when the component has mounted or when launchNr
changed
function setup() {
const launchNr = ref(1);
const loading = ref(true);
const launchData = reactive({ name: null, date: null });
watchEffect(async () => {
loading.value = true;
const res = await fetch(
`https://api.spacexdata.com/v3/launches/${launchNr.value}`
);
const data = await res.json();
loading.value = false;
launchData.name = data.mission_name;
launchData.date = data.launch_date_utc;
});
return { launchNr, launchData };
}
Tasks:
complete code for SpaceX launch component:
<template>
<article>
<button @click="launchNr--">prev</button>
<button @click="launchNr++">next</button>
<div v-if="loading">loading...</div>
<div v-else>
<h1>{{ launchData.name }}</h1>
<p>{{ launchData.date }}</p>
<img :src="launchData.patch" :key="Math.random()" />
</div>
</article>
</template>
<script>
import { reactive, ref, watchEffect } from 'vue';
export default {
setup() {
const launchNr = ref(1);
const loading = ref(true);
const launchData = reactive({
name: null,
date: null,
patch: '',
});
watchEffect(async () => {
loading.value = true;
const res = await fetch(
`https://api.spacexdata.com/v3/launches/${launchNr.value}`
);
const data = await res.json();
loading.value = false;
launchData.name = data.mission_name;
launchData.date = data.launch_date_utc;
launchData.patch = data.links.mission_patch_small;
});
return { launchNr, launchData, loading };
},
};
</script>
loading from / persisting to localStorage (counter component):
import { ref, watchEffect } from 'vue';
export default {
setup() {
// try to load
const countStored = Number(
localStorage.getItem('count')
);
const count = ref(countStored || 0);
// persist to localStorage if count changes
watchEffect(() => {
localStorage.setItem('count', count.value);
});
return { count };
},
};