Vue - The Complete Guide

Notes based on Udemy Course Vue - The Complete Guide (incl. Router & Composition API)

1. Getting Started

What is Vue.js?

Different Ways of Utilizing Vue

Building a First App with just Javascript

const buttonEl = document.querySelector('button');
const inputEl = document.querySelector('input');
const listEl = document.querySelector('ul');

function addGoal() {
    const enteredValue = inputEl.value;
    const listItemEl = document.createElement('li');
    listItemEl.textContent = enteredValue;
    listEl.appendChild(listItemEl);
}

buttonEl.addEventListener('click', addGoal);

Re-building the App with Vue

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>A First App</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <div id="app">
      <div>
        <label for="goal">Goal</label>
        <input type="text" id="goal" v-model="enteredValue" />
        <button v-on:click="addGoal">Add Goal</button>
      </div>
      <ul>
        <li v-for="goal in goals">{{ goal }}</li>
      </ul>
    </div>
    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
    <script src="app.js"></script>
  </body>
</html>
Vue.createApp({
   data() {
    return {
        goals: [],
        enteredValue: ''
    };
   } ,
   methods: {
    addGoal() {
        this.goals.push(this.enteredValue);
    }
   }
}).mount('#app');

Course Content

Basics & Core Concepts - DOM Interaction with Vue

Creating and Connecting Vue App Instances

<section id="user-goal">
  <h2>My Course Goal</h2>
  <p></p>
</section>
const app = Vue.createApp({
  data() {
    return {
        courseGoal: 'Test!'
    };
  },
});

app.mount('#user-goal');

Interpolation and Data Binding

<section id="user-goal">
  <h2>My Course Goal</h2>
  <p>{{ courseGoal }}</p>
</section>

Binding Attributes with the "v-bind" Directive

const app = Vue.createApp({
  data() {
    return {
        courseGoal: 'Test!',
        vueLink: 'https://vuejs.org/'
    };
  },
});

app.mount('#user-goal');
<p>Learn more <a v-bind:href="vueLink">about Vue</a></p>

Understanding "methods" in Vue apps

const app = Vue.createApp({
  data() {
    return {
      courseGoal: 'Test!',
      vueLink: 'https://vuejs.org/',
    };
  },
  methods: {
    outputGoal() {
      const randomNumber = Math.random();
      if (randomNumber < 0.5) {
        return 'Learn Vue!';
      } else {
        return 'Master Vue!';
      }
    },
  },
});

app.mount('#user-goal');
<section id="user-goal">
  <h2>My Course Goal</h2>
  <p>{{ outputGoal() }}</p>
  <p>Learn more <a v-bind:href="vueLink">about Vue</a></p>
</section>

Working with Data inside of a Vue app

const app = Vue.createApp({
  data() {
    return {
      courseGoalA: 'Test A!',
      courseGoalB: 'Test B!',
      vueLink: 'https://vuejs.org/',
    };
  },
  methods: {
    outputGoal() {
      const randomNumber = Math.random();
      if (randomNumber < 0.5) {
        return this.courseGoalA;
      } else {
        return this.courseGoalB;
      }
    },
  },
});

app.mount('#user-goal');

Outputting Raw HTML Content with v-html

...
courseGoalB: '<h2>Test B!</h2>',
...
...
<p v-html="outputGoal()"></p>
...

A First Summary

Event-binding

<section id="events">
  <h2>Events in Action</h2>
  <button v-on:click="counter++">Add</button>
  <button v-on:click="counter--">Reduce</button>
  <p>Result: {{ counter }}</p>
</section>
const app = Vue.createApp({
  data() {
    return {
      counter: 0,
    };
  },
});

app.mount('#events');

Events and Methods

<section id="events">
  <h2>Events in Action</h2>
  <button v-on:click="add">Add</button>
  <button v-on:click="reduce">Reduce</button>
  <p>Result: {{ counter }}</p>
</section>
const app = Vue.createApp({
  data() {
    return {
      counter: 0,
    };
  },
  methods: {
    add() {
      this.counter++;
    },
    reduce() {
      this.counter--;
    },
  },
});

app.mount('#events');

Working with Event Arguments

methods: {
    add(num) {
      this.counter = this.counter + num;
    },
    reduce(num) {
      this.counter = this.counter - num;
    },
  },
<button v-on:click="add(10)">Add 10</button>
<button v-on:click="reduce(5)">Reduce 5</button>

Using the Native Event Object

<input type="text"v-on:input="setName">
<p>Your Name: {{ name }}</p>
methods: {
  setName(event) {
    this.name = event.target.value;
  },
  ...
},

Exploring Event Modifiers

<section id="events">
  <h2>Events in Action</h2>
  <button v-on:click="add(10)">Add 10</button>
  <button v-on:click.right="reduce(5)">Reduce 5</button>
  <p>Result: {{ counter }}</p>
  <input
    type="text"
    v-on:input="setName($event, 'Lofton')"
    v-on:keyup.enter="confirmInput"
  />
  <p>Your Name: {{ confirmedName }}</p>
  <form v-on:submit.prevent="submitForm">
    <input type="text" />
    <button>Sign Up</button>
  </form>
</section>
const app = Vue.createApp({
  data() {
    return {
      counter: 0,
      name: '',
      confirmedName: '',
    };
  },
  methods: {
    confirmInput() {
      this.confirmedName = this.name;
    },
    submitForm() {
      alert('Submitted!');
    },
    setName(event, lastName) {
      this.name = event.target.value + ' ' + lastName;
    },
    add(num) {
      this.counter = this.counter + num;
    },
    reduce(num) {
      this.counter = this.counter - num;
    },
  },
});

app.mount('#events');

Locking Content with v-once

<p v-once>Starting Counter: {{ counter }}</p>
<p>Result: {{ counter }}</p>

Data Binding + Event Binding = Two-Way Binding

<input type="text" v-on:input="setName($event, 'Lofton')">
<button>Reset Input</button>
<p>Your Name: {{ name }}</p>
methods: {
  setName(event, lastName) {
    this.name = event.target.value + ' ' + lastName;
  },
  ...
  resetInput() {
    this.name = ''
  },
},

Methods used for Data Binding: How it Works

What we know (thus far)

<p>Your Name: {{ outputFullName() }}</p>
methods: {
    outputFullName() {
      if (this.name === '') {
        return '';
      }
      return this.name + ' ' + 'Lofton';
    },
}

Introducing Computed Properties

computed: {
    fullname() {
      console.log('Running Again...');
      if (this.name === '') {
        return '';
      }
      return this.name + ' ' + 'Lofton';
    },
  },
<p>Your Name: {{ fullname }}</p>

Working with Watchers

watch: {
    name(value) {
      if (value === '') {
        this.fullname = '';
      } else {
        this.fullname = value + ' ' + this.lastName;
      }
    },
    lastName(value) {
      if (value === '') {
        this.fullname = '';
      } else {
        this.fullname = this.name + ' ' + value;
      }
    },
  },
computed: {
    fullname() {
      console.log('Running Again...');
      if (this.name === '' || this.lastName === '') {
        return '';
      }
      return this.name + ' ' + this.lastName;
    },
  },
watch: {
    counter(value) {
      if (value > 50) {
        this.counter = 0;
      }
    },
}

Methods vs Computed Properties vs Watchers

v-bind and v-on Shorthands

Dynamic Styling with Inline Styles

<section id="styling">
  <div
    class="demo"
    :style="{borderColor: boxASelected ? 'red' : '#ccc'}"
    @click="boxSelected('A')"
  ></div>
  <div class="demo" @click="boxSelected('B')"></div>
  <div class="demo" @click="boxSelected('C')"></div>
</section>
const app = Vue.createApp({
  data() {
    return {
      boxASelected: false,
      boxBSelected: false,
      boxCSelected: false,
    };
  },
  methods: {
    boxSelected(box) {
      if (box === 'A') {
        this.boxASelected = true;
      } else if (box === 'B') {
        this.boxBSelected = true;
      } else if (box === 'C') {
        this.boxCSelected = true;
      }
    },
  },
});

app.mount('#styling');

Adding CSS Classes Dynamically

<div
  :class="boxASelected ? 'demo active' : 'demo'"
  @click="boxSelected('A')"
></div>
<div
  :class="{demo: true, active: boxASelected}"
  @click="boxSelected('A')"
></div>
<div
  class="demo"
  :class="{active: boxASelected}"
  @click="boxSelected('A')"
></div>

Classes & Computed Properties

<div class="demo" :class="boxAClasses" @click="boxSelected('A')"></div>
computed: {
  boxAClasses() {
    return { active: this.boxASelected };
  },
},

Dynamic Classes: Array Syntax

<div :class="['demo', { active: boxBSelected }]" @click="boxSelected('B')"></div>

With computed:

<div :class="['demo', boxBClasses]" @click="boxSelected('B')"></div>

Module Summary

Rendering Conditional Content & Lists

Module Introduction

Understanding the Problem

Starting Vue app:

const app = Vue.createApp({
  data() {
    return { goals: [] };
  },
});

app.mount('#user-goals');

Rendering Content Conditionally

v-if, v-else, and v-else-if

const app = Vue.createApp({
  data() {
    return {
      enteredGoalValue: '',
      goals: [],
    };
  },
  methods: {
    addGoal() {
      this.goals.push(this.enteredGoalValue);
    },
  },
});

app.mount('#user-goals');
<section id="user-goals">
  <h2>My course goals</h2>
  <input type="text" v-model="enteredGoalValue" />
  <button @click="addGoal">Add Goal</button>
  <p v-if="goals.length === 0">
    No goals have been added yet - please start adding some!
  </p>
  <ul v-else>
    <li>Goal</li>
  </ul>
</section>

Using v-show Instead Of v-if

Rendering Lists of Data

Diving Deeper Into v-for

Other ways of utilizing v-for:

Removing List items

<li v-for="(goal, index) in goals" @click="removeGoal(index)">
  {{ goal }} - {{ index }}
</li>
methods: {
    ...
    removeGoal(idx) {
      this.goals.splice(idx, 1);
    },
  },

Lists & Keys

Module Summary

Course Project: The Monster Slayer Game

Module Introduction

Project Setup & First Methods

<body>
  <header>
    <h1>Monster Slayer</h1>
  </header>
  <div id="game">
    <section id="monster" class="container">
      <h2>Monster Health</h2>
      <div class="healthbar">
        <div class="healthbar__value"></div>
      </div>
    </section>
    <section id="player" class="container">
      <h2>Your Health</h2>
      <div class="healthbar">
        <div class="healthbar__value"></div>
      </div>
    </section>
    <section id="controls">
      <button>ATTACK</button>
      <button>SPECIAL ATTACK</button>
      <button>HEAL</button>
      <button>SURRENDER</button>
    </section>
    <section id="log" class="container">
      <h2>Battle Log</h2>
      <ul></ul>
    </section>
  </div>
</body>
function getRandomValue(min, max) {
  return Math.floor(Math.random() * (max - min)) + min;
}

const app = Vue.createApp({
  data() {
    return {
      playerHealth: 100,
      monsterHealth: 100,
    };
  },
  methods: {
    attackMonster() {
      const attackValue = getRandomValue(5, 12);
      this.monsterHealth -= attackValue;
      this.attackPlayer();
    },
    attackPlayer() {
      const attackValue = getRandomValue(8, 15);
      this.playerHealth -= attackValue;
    },
  },
});

app.mount('#game');

Updating the Health Bars

computed: {
  monsterBarStyles() {
    return { width: this.monsterHealth + '%' };
  },
  playerBarStyles() {
    return { width: this.playerHealth + '%' };
  },
},

Adding a "Special Attack"

Adding a "Heal" Functionality

healPlayer() {
  this.currentRound++;
  const healValue = getRandomValue(8, 20);
  if (this.playerHealth + healValue > 100) {
    this.playerHealth = 100;
  } else {
    this.playerHealth += healValue;
  }
  this.attackPlayer();
},

Adding a "Game Over Screen"

watch: {
  playerHealth(value) {
    if (value <= 0 && this.monsterHealth <= 0) {
      // a draw
      this.winner = 'draw';
    } else if (value <= 0) {
      // player lost
      this.winner = 'monster';
    }
  },
  monsterHealth(value) {
    if (value <= 0 && this.playerHealth <= 0) {
      // draw
      this.winner = 'draw';
    } else if (value <= 0) {
      // monster lost
      this.winner = 'player';
    }
  },
},
<!-- between healthbars & controls -->
<section class="container" v-if="winner">
  <h2>Game Over!</h2>
  <h3 v-if="winner === 'monster'">You lost!</h3>
  <h3 v-else-if="winner === 'player'">You won!</h3>
  <h3 v-else>It's a draw!</h3>
</section>

Finishing the Core Functionality

computed: {
    // ...
    playerBarStyles() {
        if (this.playerHealth < 0) {
        return { width: '0%' };
        }
        return { width: this.playerHealth + '%' };
    },
    // ...
},
<button @click="startGame">Start New Game</button>
methods: {
    startGame() {
      this.playerHealth = 100;
      this.monsterHealth = 100;
      this.winner = null;
      this.currentRound = 0;
    },
},
<button @click="surrender">SURRENDER</button>
methods: {
    // ...
    surrender() {
      this.winner = 'winner';
    },
  },

Adding a Battle Log

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vue Basics</title>
    <link
      href="https://fonts.googleapis.com/css2?family=Jost:wght@400;700&display=swap"
      rel="stylesheet"
    />
    <link rel="stylesheet" href="styles.css" />
    <script src="https://unpkg.com/vue@3.4.9/dist/vue.global.js" defer></script>
    <script src="app.js" defer></script>
  </head>
  <body>
    <header>
      <h1>Monster Slayer</h1>
    </header>
    <div id="game">
      <section class="container"><h2>Round: {{ currentRound }}</h2></section>
      <section id="monster" class="container">
        <h2>Monster Health</h2>
        <div class="healthbar">
          <div class="healthbar__value" :style="monsterBarStyles"></div>
        </div>
      </section>
      <section id="player" class="container">
        <h2>Your Health</h2>
        <div class="healthbar">
          <div class="healthbar__value" :style="playerBarStyles"></div>
        </div>
      </section>
      <section class="container" v-if="winner">
        <h2>Game Over</h2>
        <h3 v-if="winner === 'monster'">You lost!</h3>
        <h3 v-else-if="winner === 'player'">You won!</h3>
        <h3 v-else>It's a draw!</h3>
        <button @click="startGame">Start New Game</button>
      </section>
      <section id="controls" v-if="!winner">
        <button @click="attackMonster">ATTACK</button>
        <button :disabled="mayUseSpecialAttack" @click="specialAttackMonster">
          SPECIAL ATTACK
        </button>
        <button @click="healPlayer">HEAL</button>
        <button @click="surrender">SURRENDER</button>
      </section>
      <section id="log" class="container">
        <h2>Battle Log</h2>
        <ul>
          <li v-for="logMessage in logMessages">
            <span
              :class="{'log--player': logMessage.actionBy === 'player', 'log--monster': logMessage.actionBy === 'monster'}"
              >{{ logMessage.actionBy === 'player' ? 'Player' :
              'Monster'}}</span
            >
            <span v-if="logMessage.actionType === 'heal'">
              heals himself for
              <span class="log--heal">{{ logMessage.actionValue }}</span>
            </span>
            <span v-else>
              attacks and deals
              <span class="log--damage">{{ logMessage.actionValue }}</span>
            </span>
          </li>
        </ul>
      </section>
    </div>
  </body>
</html>
function getRandomValue(min, max) {
  return Math.floor(Math.random() * (max - min)) + min;
}

const app = Vue.createApp({
  data() {
    return {
      playerHealth: 100,
      monsterHealth: 100,
      currentRound: 0,
      winner: null,
      logMessages: [],
    };
  },
  computed: {
    monsterBarStyles() {
      if (this.monsterHealth < 0) {
        return { width: '0%' };
      }
      return { width: this.monsterHealth + '%' };
    },
    playerBarStyles() {
      if (this.playerHealth < 0) {
        return { width: '0%' };
      }
      return { width: this.playerHealth + '%' };
    },
    mayUseSpecialAttack() {
      return this.currentRound % 3 !== 0;
    },
  },
  watch: {
    playerHealth(value) {
      if (value <= 0 && this.monsterHealth <= 0) {
        // a draw
        this.winner = 'draw';
      } else if (value <= 0) {
        // player lost
        this.winner = 'monster';
      }
    },
    monsterHealth(value) {
      if (value <= 0 && this.playerHealth <= 0) {
        // a draw
        this.winner = 'draw';
      } else if (value <= 0) {
        // monster lost
        this.winner = 'player';
      }
    },
  },
  methods: {
    startGame() {
      this.playerHealth = 100;
      this.monsterHealth = 100;
      this.winner = null;
      this.currentRound = 0;
      this.logMessages = [];
    },
    attackMonster() {
      this.currentRound++;
      const attackValue = getRandomValue(5, 12);
      this.monsterHealth -= attackValue;
      this.addLogMessage('player', 'attack', attackValue);
      this.attackPlayer();
    },
    attackPlayer() {
      const attackValue = getRandomValue(8, 15);
      this.playerHealth -= attackValue;
      this.addLogMessage('monster', 'attack', attackValue);
    },
    specialAttackMonster() {
      this.currentRound++;
      const attackValue = getRandomValue(10, 25);
      this.monsterHealth -= attackValue;
      this.addLogMessage('player', 'attack', attackValue);
      this.attackPlayer();
    },
    healPlayer() {
      this.currentRound++;
      const healValue = getRandomValue(8, 20);
      if (this.playerHealth + healValue > 100) {
        this.playerHealth = 100;
      } else {
        this.playerHealth += healValue;
      }
      this.addLogMessage('player', 'heal', healValue);
      this.attackPlayer();
    },
    surrender() {
      this.winner = 'winner';
    },
    addLogMessage(who, what, value) {
      this.logMessages.unshift({
        actionBy: who,
        actionType: what,
        actionValue: value,
      });
    },
  },
});

app.mount('#game');

Vue: Behind the Scenes

Module Introduction

An Introduction to Vue's Reactivity

<section id="app">
  <h2>How Vue Works</h2>
  <input type="text" @input="saveInput">
  <button @click="setText">Set Text</button>
  <p>{{ message }}</p>
</section>
const app = Vue.createApp({
  data() {
    return {
      currentUserInput: '',
      message: 'Vue is great!',
    };
  },
  methods: {
    saveInput(event) {
      this.currentUserInput = event.target.value;
    },
    setText() {
      this.message = this.currentUserInput;
    },
  },
});

app.mount('#app');

Vue Reactivity: A Deep Dive

let message = 'Hello!';
let longMessage = message + ' World!';
console.log(longMessage);
message = 'Hello!!!!';
console.log(longMessage);
const data = {
  message: 'Hello!',
  longMessage: 'Hello! World!',
};

const handler = {
  set(target, key, value) {
    if (key === 'message') {
      target.longMessage = value + ' World!';
    }
    target.message = value;
  },
};

const proxy = new Proxy(data, handler);

proxy.message = 'Hello!!!!';
console.log(proxy.longMessage);

One App vs Multiple Apps

Understanding Templates

const app2 = Vue.createApp({
  template: `
    <p>{{ favoriteMeal }}</p>
  `,
  data() {
    return {
      favoriteMeal: 'Pizza!',
    };
  },
});

app2.mount('#app2');

Working with Refs

<input type="text" ref="userText" />
methods: {
  saveInput(event) {
    this.currentUserInput = event.target.value;
  },
  setText() {
    // this.message = this.currentUserInput;
    this.message = this.$refs.userText.value;
    // console.dir(this.$refs.userText);
  },
},

How Vue Updates the DOM

Vue Instance ( our Vue app) Virtual DOM Browser DOM (HTML content rendered in the browser)
Stores data, computed properties, methods, ... JS-based DOM which exists only in memory Vue-controlled template is rendered in the DOM
title: 'Hello!', text: 'Not the title' {el: 'h2', child: 'Hello!'}, ... <h2>Hello!</h2><p>Not the title</p>
Data and computed properties may change (e.g. because of user input) Updates should be reflected, but Vue should not re-render everything
title: 'Hi there!', text: 'Not the title' Updates are made to the virtual DOM first, only differences are then rendered to the real DOM <h2>Hi there!</h2><p>Not the title</p> (only updates reflected)
- General overview, Vue has a bunch of optimizations to achieve this in a non-performance intensive way.

Vue Instance Lifecycle

Vue Instance Lifecycle

Vue App Lifecycle Practice

const app = Vue.createApp({
  data() { /*... */ },
  methods: { /*... */ },
  beforeCreate() {
    console.log('beforeCreate()');
  },
});
const app = Vue.createApp({
  data() { /*... */ },
  methods: { /*... */ },
  beforeCreate() {
    console.log('beforeCreate()');
  },
  created() {
    console.log('created()');
  },
  beforeMount() {
    console.log('beforeMount()');
  },
  mounted() {
    console.log('mounted()');
  },
  beforeUpdate() {
    console.log('beforeUpdate()');
  },
  updated() {
    console.log('updated()');
  },
  beforeUnmount() {
    console.log('beforeUnmount()');
  },
  unmounted() {
    console.log('unmounted()');
  },
});

app.mount('#app');

setTimeout(() => {
  app.unmount();
}, 3000);

Introducing Components

Moving to a Better Development Setup & Workflow with the Vue CLI