Vue basics

Topics

  • overview of Vue
  • declarative rendering / working with state
  • Vue template language basics
  • Vue devtools
  • initializing a Vue project with Vue CLI
  • components
  • using predefined components
  • composition API
  • side effects

Overview

What is Vue?

one of the 3 big JavaScript UI frameworks (besides React, Angular)

Basics of modern JavaScript UI frameworks

  • state-based / declarative
  • component-based

State-basied / Declarative

  • data model which describes the entire application state
  • user interactions change the data model, causing the view to update automatically

Component-based

  • "custom" HTML tags
  • data flow via props and events
  • usually unidirectional data flow (from parent to child)

Example: components and state in a todo app

image/svg+xml TodoItem TodoList AddTodo TodoApp todos newTitle TodoItem ...

Example: props and events in a todo app

image/svg+xml TodoItem TodoList AddTodo TodoApp todos newTitle TodoItem ... todos onToggle onDelete onAdd todo onToggle onDelete todo onToggle onDelete

Vue compared to other frameworks

  • simpler than others
  • may be used without a build step

History of Vue

  • initial release in 2014
  • 2020: Vue 3 (new composition API)

Online editor

Online editor

recommended:

https://codesandbox.io

  • has templates for Vue and Vue 3
  • based on VS Code
  • Prettier-based formatting via Shift + Alt + F
  • hosting under e.g. https://abcde.csb.app

Options API basics

Options API and composition API

  • options API: traditional way to write Vue components
  • composition API: new possibility introduced in 2020 with Vue 3 (inspired by React hooks)

Component definition

generic compontent definition in a .vue file:

<template>
...
</template>

<script>
...
</script>

<style scoped>
...
</style>

Example component definition (slideshow component)

<div>
  <button @click="imgId = 0">start</button>
  <button @click="prevImg()">previous</button>
  <img :src="imgUrl" alt="slideshow" />
  <button @click="imgId++">next</button>
</div>

Example component definition (slideshow component)

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--;
      }
    },
  },
};

Options API basics

a component definition object has several specific props / methods:

  • name: will show up in the developer tools
  • data: reactive component state
  • computed: derived data
  • methods: event handlers, ...
  • ...

Data, methods, computed, ...

Entries in data, methods and computed, ... are available via this.entryname in the script and via entryname in the template

State

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

Methods are functions associated with a component

  • can be called from the template
  • can access the state

Computed

  • methods in computed can compute derived data
  • in general a component should store the minimal state possible (e.g. store the image id, not the entire image URL, avoid redundant data)
  • methods in computed are automatically called when one of their dependencies changes

Computed

How 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

Template language basics

Template language basics

  • binding content
  • binding properties
  • binding events
  • if / else
  • repeating elements
  • two-way binding for inputs
  • whitespace

Binding content

<div>A year has {{365 * 24}} hours.</div>

Binding properties

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>

Class property

special syntax for the class property:

<div :class="{todo: true, completed: isCompleted}">...</div>

Style property

special syntax for the style property:

<div :style="{padding: '8px', margin: '8px'}">...</div>
<div :style="{color: completed ? 'grey' : 'black'}">
  ...
</div>

Binding events

short form:

<button @click="alert('hello')">Say Hello</button>

long form:

<button v-on:click="alert('hello')">Say Hello</button>

Binding events

event modifiers:

preventing default:

<form @submit.prevent="handleSubmit()">...</form>

Binding events

accessing the event object via $event:

<button @click="handleClick($event)">handle event</button>

If / else

<div v-if="request.loading">Loading...</div>
<div v-else-if="request.error">Error while loading</div>
<div v-else>
  <h1>Results</h1>
  ...
</div>

Repeating elements

<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)

Two-way binding for 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" />

Whitespace

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>

Exercises

Exercises

Tasks:

  • reacreate the slideshow component
  • create a todo list in a single component

Exercise: todo list

Create a todo list application with the following functionality:

  • displaying completed and incomplete todos
  • toggling the completed state of a todo
  • deleting a todo
  • adding a new todo from a form
  • displaying the total number of completed and incomplete todos

Exercise: todo list - partial solution

<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>

Exercise: todo list - partial solution

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,
      });
    },
  },
};

Vue developer tools

Vue developer tools

plugins for Chrome, Firefox and Edge

features:

  • display component structure
  • show component state and props
  • change state and props
  • log component events
  • ...

Vue developer tools

possible approach when looking for issues:

  • check state update logic: state updates correctly in response to events
  • check rendering logic: state is rendered as expected

VS Code for JavaScript

VS Code

https://code.visualstudio.com

open source IDE

independent of Visual Studio itself

Basics

  • opening a folder
  • file explorer

Command palette

F1 or Ctrl + Shift + P: display command palette

  • searchable
  • displays keyboard shortcuts

Command palette

example commands:

  • View: Toggle Terminal
  • Format Document
  • Find
  • Search: Find in Files
  • Preferences: Open Settings (UI)
  • Toggle line comment / Toggle block comment
  • Go to definition / Peek definition (only for certain file types)
  • Rename symbol (only for certain file types)

Configuration

Via File - Preferences - Settings

Is split into User Settings and Workspace Settings

Configuration options for JavaScript

Recommendations:

  • Accept Suggestions on Commit Character (Autocomplete on other keys than Enter): deactivate if you're working with JavaScript / TypeScript
  • EOL: \n
  • Tab Size: 2

Further options:

  • Format on Save
  • Word Wrap

VS Code extensions for JavaScript and Vue

VS Code extensions for JavaScript and Vue

open the extensions view in the sidebar: fifth symbol on the left

recommended extensions for Vue development:

  • Prettier (code formatter)
  • ESLint (linter)
  • Vetur (Vue tooling)

Prettier

  • code formatting according to strict rules
  • for JavaScript, HTML, CSS
  • shortcut: Shift + Alt + F

ESLint

Linter with more functionality than VS Code's default linter

Vue CLI

Vue CLI

a tool for initializing a Vue project

Developing with node.js and npm

  • node.js: JS runtime
    • running the local development server
    • unit tests
  • npm: package manager
    • managing dependencies
    • packages are located in the node_modules directory
    • configuration via package.json

Vue CLI

install and run:

npm install -g @vue/cli

vue create my-app

or run directly (without installing):

npx @vue/cli create my-app

Vue CLI

Creates a simple Vue app which can be used as a starting point

many aspects can be set up automatically:

  • webpack and babel for building
  • local development server
  • linter
  • TypeScript support
  • testing
  • CSS tooling

Vue CLI

configuration:

  • version (2 or 3)
  • TypeScript
  • PWA support
  • Router
  • Vuex
  • CSS Pre-processors
  • Linter / Formatter (build will fail on linter errors)
  • Unit Testing
  • E2E Testing

Vue CLI

don't fail on ESLint errors:

new file vue.config.js:

module.exports = {
  lintOnSave: 'warning',
};

Vue CLI

Check imports for errors:

npm install eslint-plugin-import

in the eslint config, add "plugin:import/recommended" to "extends"

Default project structure

  • public/index.html, src/main.js: entry points
  • src/App.vue: defines the App component
  • node_modules: dependencies

Development server and build

inside the project directory:

  • npm run serve: starts the local development server
  • npm run build: creates a build (for deployment)

Build and deployment

Build and deployment

A Vue project can be hosted on any static hosting service

Build

build with Vue CLI:

npm run build

minified and bundled app is created in the dist folder

Deployment

simple options for trying out deployment options without login:

Custom components

Custom components

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

Custom components can typically be written in two ways:

recommended:

<TodoItem />
<VBtn>foo</VBtn>

alternative:

<todo-item />
<v-btn>foo</v-btn>

Component libraries

Component libraries

for Vue 2:

  • vuetify
  • bootstrap-vue
  • element-ui

for Vue 3:

  • vuetify release planned for 2022-02
  • element-plus is in beta

Vuetify 2: setup

add vuetify 2 to a vanilla Vue CLI project:

npx @vue/cli add vuetify

(note: will overwrite App.vue and some other files)

Vuetify 2: setup

make sure your application in App.vue is enclosed in a <v-app> component:

<template>
  <v-app>
    ...
  </v-app>
</template>

Vuetify 2: components

example component use:

<v-btn color="primary" type="submit">add</v-btn>
<v-text-field v-model="newTitle" label="new title" />

Vuetify 2: app layout

<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>

Vuetify 2: container

v-container: responsive container with horizontal margins for spacing

<v-main>
  <v-container>
    ...
  </v-container>
</v-main>

Vuetify 2: grid system

Vuetify comes with a 12-column grid system (similar to bootstrap)

  • v-row: horizontal container, divided into 12 columns
  • v-col: contained within v-row elements

Vuetify 2: grid system

two columns with the same size:

<v-row>
  <v-col>foo</v-col>
  <v-col>bar</v-col>
</v-row>

Vuetify 2: grid system

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 screens

Defining custom components

Defining custom components

possible file name patterns for component files:

  • MyComponent.vue (recommended)
  • my-component.vue

component names should always be multiple words (to distinguish them from built-in elements)

Defining custom components

recap: important props in component definitions:

  • name
  • data
  • computed
  • methods

Defining custom components

a component may define an interface to interact with its parent component:

  • props: data that is passed down
  • events: may be triggered in a child component

Component props

Component props

  • state = internal to the component
  • props = parameters that are passed down from the parent

Component props

Examples:

<ProgressBar percentage={75} color="lightgreen" />
<Rating :stars="3" />

Component props

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, ...

Component props

multi-word props should use mixedCase in JavaScript, but kebab-case in templates

props: ['greetingText']
<WelcomeMessage greeting-text="hi" />

Component events

Data/event flow

  • parent → child: props
  • child → parent: events

Component events

events have a name and potentially contain some data ("payload(s)")

Component events

example: handling component events

<StarsRating
  :value="rating1"
  @update:value="rating1 = $event"
/>
<TodoItem
  :todo="todo"
  @delete="deleteTodo($event)"
  @update:completed="changeTodoCompleted($event)"
/>

Component events

Events are emitted from a child component via:

this.$emit('eventname', payload);

Component events

example: StarsRating:

Components and two-way binding

Components and two-way binding

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" />

Components and two-way binding

custom components in Vue 3:

<StarsRating
  :modelValue="rating1"
  @update:modelValue="rating1 = $event"
/>

short form:

<StarsRating v-model="rating1" />

Components and two-way binding

custom components in Vue 2:

<StarsRating :value="rating1" @input="rating1 = $event" />

short form:

<StarsRating v-model="rating1" />

Custom components: exercises

Custom components: exercises

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

Passing content to components: slots

Passing content to components: slots

we may create components that can be used like this:

<DialogModal type="error">
  <p>Changes could not be saved</p>
  <button>OK</button>
</DialogModal>

Passing content to components: slots

template for DialogModal:

<div :class="{dialog: true, dialogType: type}">
  <slot></slot>
</div>

Composition API

Composition API vs options API

Composition API: component logic is defined in a setup method

Composition API compared to the traditional options API:

  • better TypeScript support
  • more composable (logic can be extracted from component defintion)
  • slightly more verbose

Composition API vs options API

options API:

export default {
  name: 'TodoApp',
  data() {
    return {
      todos: [],
      newTitle: '',
    };
  },
  methods: {
    /*...*/
  },
  computed: {
    /*...*/
  },
};

Composition API vs options API

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 };
  },
};

Composition API vs options API

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 };
  },
};

Composition API

VueUse: collection of pre-defined Vue composition functions:

https://github.com/vueuse/vueuse

examples:

  • useFetch
  • useGeolocation
  • useMediaQuery
  • useNow
  • useOnline
  • ...

State in the composition API

two mechanisms:

  • ref for primitive / immutable data (strings, numbers, ...)
  • reactive for objects, arrays, ...

refs

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>

Example component definition (simple todo app)

<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>

Example component definition (simple todo app)

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 };
  },
};

Exercise

Create a slideshow component / application that displays images like this:

https://picsum.photos/300/200?image=0

Composition API: props and events

Props and events

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 };
  },
};

Network requests in JavaScript

Network requests in JavaScript

promises: modern way of handling asynchronous code:

  • promises and async / await
  • promises and .then()

modern ways of sending network requests:

  • fetch() (included in browsers)
  • axios (library)

Network requests in JavaScript

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;
}

Network requests in JavaScript

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;
}

Side effects

Side effects

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, ...

Side effects

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

Side effects

typical use cases for side effects:

  • triggering API queries
    • when a component is rendered for the first time
    • when some data (state / props) have changed (e.g. the user has selected a specific item to view its details)
  • saving some data to the browser storage if it has changed
  • explicitly manipulating the DOM
  • starting timers
  • ...

Side effects

common triggers for a side effect:

  • on initial rendering of a component
  • when entries in state / props change

Side effects

side effects in the options API:

  • lifecycle event: created
  • when data changes: watch function

side effects in the composition API (variant 1):

  • initialization inside of setup
  • when data changes: watch function

side effects in the composition API (variant 2):

  • watchEffect function: runs initially and on any watched changes

Side effects: lifecycle events

Side effects: lifecycle events

Options API: Component methods that are called when certain lifecylce events occur:

  • created
  • mounted
  • updated
  • destroyed
  • ...

Side effects: lifecycle events

Composition API: equivalent functions

  • created → include in setup
  • mounted → onMounted
  • updated → onUpdated
  • destroyed → onUnmounted
  • ...

Side effects: lifecycle events

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;
  },
};

Side effects: watchEffect

watchEffect

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, ...

watchEffect

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 };
}

watchEffect

Tasks:

  • load and display more data
  • add a loading indicator

watchEffect

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>

watchEffect

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 };
  },
};