Files
it-know-how/vue/00-Vue Tutorial.md
2026-03-27 12:58:01 +01:00

17 KiB
Executable File

Vue.js Introduction for Python Developers

A practical guide to Vue.js, written for developers who know Python.


Table of Contents

  1. What is Vue?
  2. The Big Idea: Reactivity
  3. Your First Vue Component
  4. Reactive State with ref
  5. Derived State with computed
  6. Templates: HTML with Vue Features
  7. Two-Way Binding with v-model
  8. Events and User Interaction
  9. Components and Props
  10. Lifecycle Hooks
  11. Composables: Reusable Logic
  12. A Real-World Example: Filter Flow
  13. Vue Strengths, Weaknesses, and Use Cases
  14. Quick Reference

What is Vue?

Vue.js is a JavaScript framework for building user interfaces. It focuses on the view layer - what the user sees and interacts with.

Vue 2 vs Vue 3

This guide covers Vue 3, which introduced the Composition API (using <script setup>). If you see ref() and computed(), you're looking at Vue 3.

Python Analogy

Think of Vue components as Python classes that combine:

Python Vue
Class with state <script setup> with ref() values
Class methods Functions defined in <script setup>
__str__ or template engine <template> section
Class styling <style> section

The Big Idea: Reactivity

Reactive means the UI automatically updates when your data changes.

# Python: you manually update the view
class Counter:
    def __init__(self):
        self.count = 0
    def increment(self):
        self.count += 1
        self.update_view()  # you must call this manually
<!-- Vue: UI updates automatically -->
<script setup>
import { ref } from 'vue';
const count = ref(0);
function increment() {
  count.value += 1;  // UI updates automatically
}
</script>

<template>
  <button @click="increment">Clicked {{ count }} times</button>
</template>

When count.value changes, Vue re-renders the button text. You never call update_view().


Your First Vue Component

A Vue single-file component (.vue file) has three sections:

<template>
  <!-- 1. TEMPLATE: what to display -->
  <p>{{ greeting }}</p>
  <button @click="sayHello">Click me</button>
</template>

<script setup lang="ts">
  // 2. SCRIPT: data and logic
  import { ref } from 'vue';
  
  const greeting = ref('Hello from Vue!');
  
  function sayHello() {
    console.log('Button clicked!');
  }
</script>

<style scoped>
  /* 3. STYLE: how it looks */
  p { color: blue; }
</style>

Mental Model

When reading Vue code, ask four questions:

  1. What values are reactive state? → Look for ref() and computed()
  2. Which template elements use that state? → Look for {{ variable }} and v-model
  3. Which functions change that state? → Look for functions that modify .value
  4. Which computed values depend on that state? → Look for computed()

Reactive State with ref

What is ref?

ref() wraps a value so Vue can track changes to it.

import { ref } from 'vue';

const count = ref(0);           // reactive number
const username = ref('alice');  // reactive string
const isLoading = ref(false);   // reactive boolean
const items = ref<string[]>([]); // reactive array

Reading and Writing

In <script>, use .value:

count.value += 1;           // increment
username.value = 'bob';     // change string
isLoading.value = true;     // set to true
items.value.push('apple');  // modify array

In <template>, Vue unwraps refs automatically—use the variable name without .value:

<template>
  <p>{{ count }}</p>           <!-- reads count.value -->
  <p>{{ username }}</p>        <!-- reads username.value -->
  <button :disabled="isLoading">Save</button>
</template>

Python Comparison

# Python
count = 0
count += 1
// Vue (script)
const count = ref(0);
count.value += 1;

The .value exists because count is a wrapper object, not the raw value.

Example: Loading State

<template>
  <button :disabled="isLoading" @click="save">
    {{ isLoading ? 'Saving...' : 'Save' }}
  </button>
</template>

<script setup lang="ts">
import { ref } from 'vue';

const isLoading = ref(false);

async function save() {
  isLoading.value = true;
  try {
    await fakeRequest();
  } finally {
    isLoading.value = false;
  }
}

function fakeRequest() {
  return new Promise((resolve) => setTimeout(resolve, 1000));
}
</script>

The button label and disabled state both react to isLoading.


Derived State with computed

Use computed() for values that are calculated from other reactive values.

import { ref, computed } from 'vue';

const firstName = ref('Ada');
const lastName = ref('Lovelace');

const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`;
});

Python Comparison

This is like a Python @property:

class Person:
    @property
    def full_name(self):
        return f"{self.first_name} {self.last_name}"

Rule of Thumb

Use ref when Use computed when
The value changes over time The value is calculated from other values
The value is primary state You don't want to duplicate state
Examples: user input, API data Examples: full name from first + last

Common Mistake: Storing What Can Be Derived

// Bad: duplicate state, must update manually
const firstName = ref('Ada');
const lastName = ref('Lovelace');
const fullName = ref('Ada Lovelace');  // must keep in sync!

// Good: computed handles it automatically
const fullName = computed(() => `${firstName.value} ${lastName.value}`);

Real Example: Validation

const selectedStart = ref<string | null>(null);
const selectedEnd = ref<string | null>(null);

const hasValidRange = computed(() => {
  if (!selectedStart.value || !selectedEnd.value) {
    return true;  // missing values are valid (no filter)
  }
  return new Date(selectedStart.value) <= new Date(selectedEnd.value);
});

Templates: HTML with Vue Features

Interpolation: {{ }}

Print values into HTML:

<p>{{ username }}</p>
<p>{{ 2 + 3 }}</p>
<p>{{ isLoading ? 'Loading...' : 'Done' }}</p>
<p>{{ items.length }} items</p>

Conditional Rendering: v-if / v-else

Show elements based on conditions:

<p v-if="error">{{ error }}</p>
<p v-else>No errors</p>

<div v-if="isLoggedIn">
  <p>Welcome, {{ username }}!</p>
</div>
<div v-else>
  <p>Please log in.</p>
</div>

Loops: v-for

Repeat elements for each item:

<ul>
  <li v-for="user in users" :key="user.id">
    {{ user.name }}
  </li>
</ul>

Important: Always use :key with v-for for proper list rendering.

Attribute Binding: :

Bind HTML attributes to JavaScript expressions:

<button :disabled="isLoading">Save</button>
<img :src="imageUrl" />
<a :href="profileUrl">View Profile</a>

Short for v-bind:disabled.

Class Binding

<div :class="{ active: isSelected, highlight: hasError }">
  Content
</div>

Two-Way Binding with v-model

v-model connects a form field and a reactive variable bidirectionally.

Basic Input

<template>
  <input v-model="email" />
  <p>You typed: {{ email }}</p>
</template>

<script setup lang="ts">
import { ref } from 'vue';
const email = ref('');
</script>
  • User types → email updates
  • Code changes email → input display updates

Select Dropdown

<v-select
  v-model="selectedCountry"
  :items="countries"
  item-title="title"
  item-value="value"
/>
const countries = [
  { title: 'Germany', value: 'DE' },
  { title: 'France', value: 'FR' },
  { title: 'Spain', value: 'ES' },
];
const selectedCountry = ref<string | null>(null);
  • item-title: what's shown to users
  • item-value: what's stored in the variable

Events and User Interaction

Click Events: @click

<button @click="save">Save</button>
function save() {
  console.log('saving...');
}

Other Common Events

<input @input="onInput" />                    <!-- typing -->
<form @submit.prevent="submitForm" />         <!-- form submission -->
<select @change="onChange" />                <!-- dropdown change -->
<div @mouseover="hovering = true" />         <!-- mouse events -->

The .prevent modifier calls event.preventDefault() automatically.

Event with Parameters

<button @click="deleteItem(id)">Delete</button>
function deleteItem(id: number) {
  console.log('deleting', id);
}

Components and Props

What is a Component?

A component is a reusable UI unit. Think of it like a Python function that returns HTML, but with its own state and logic.

Parent Using Child

<!-- ParentComponent.vue -->
<template>
  <UserCard :name="username" :age="34" />
</template>

<script setup lang="ts">
import UserCard from './UserCard.vue';
const username = 'Alice';
</script>

Props: Data Passed In

Props are like function arguments for components.

<!-- UserCard.vue -->
<script setup lang="ts">
interface Props {
  name: string;
  age: number;
  mode?: 'compact' | 'full';  // optional prop
}

const props = withDefaults(defineProps<Props>(), {
  mode: 'full',
});
</script>

<template>
  <div :class="props.mode">
    <p>{{ props.name }} is {{ props.age }} years old.</p>
  </div>
</template>

Python Comparison

# Python function
def user_card(name: str, age: int, mode: str = 'full'):
    return f"{name} is {age} years old ({mode})"

Child-to-Parent Communication

Props go down; events go up.

<!-- Parent -->
<template>
  <FileUpload @upload="handleUpload" />
</template>

<script setup lang="ts">
function handleUpload(file: File) {
  console.log('Uploaded:', file.name);
}
</script>

The child component calls emit('upload', file) when something happens. The parent decides what to do.


Lifecycle Hooks

Code that runs at specific moments in a component's life.

onMounted: When Component Appears

import { onMounted } from 'vue';

onMounted(async () => {
  console.log('Component is on screen');
  await fetchData();
});

This is like __post_init__ or a setup function that runs after initialization.

Other Common Hooks

Hook When It Runs
onMounted Component is added to DOM
onUnmounted Component is removed from DOM
onUpdated Component re-renders
onBeforeMount Just before first render
onBeforeUnmount Just before removal

Composables: Reusable Logic

A composable is a reusable function containing Vue logic.

Example: useCounter

// useCounter.ts
export function useCounter() {
  const count = ref(0);

  function increment() {
    count.value += 1;
  }

  return { count, increment };
}
<!-- Using it -->
<script setup lang="ts">
import { useCounter } from './useCounter';

const { count, increment } = useCounter();
</script>

Why Composables?

Without composables, components get too big. Composables let you:

  • Reuse logic across components
  • Separate data fetching from UI
  • Keep components readable

Real Examples from Projects

// Separate backend logic from UI
const { downloadFlexData } = useJsonDownload();
const rows = await downloadFlexData(payload);

// Authentication logic
const { isAuthenticated, login, logout } = useAuth();

Python Comparison

Composables are like Python modules with business logic:

# Python
from downloads import download_flex_data
rows = download_flex_data(payload)

# Vue
const { downloadFlexData } = useJsonDownload();
const rows = await downloadFlexData(payload);

A Real-World Example: Filter Flow

This pattern appears frequently in data-driven applications.

The Problem

User selects filter options, then clicks "Apply" to fetch data. Without separating "selected" and "applied" state, every dropdown change would trigger a backend request.

State Structure

// What the user is currently editing
const selectedStart = ref<string | null>(defaultStart);
const selectedEnd = ref<string | null>(defaultEnd);

// What was last applied
const appliedStart = ref<string | null>(defaultStart);
const appliedEnd = ref<string | null>(defaultEnd);

Validation

const isValidRange = computed(() => {
  if (!selectedStart.value || !selectedEnd.value) return true;
  return new Date(selectedStart.value) <= new Date(selectedEnd.value);
});

Apply Action

async function applyFilters() {
  if (!isValidRange.value) return;
  
  appliedStart.value = selectedStart.value;
  appliedEnd.value = selectedEnd.value;
  
  await refreshData();
}

Data Flow

User changes dropdown
    ↓
selectedStart updates (draft state)
    ↓
Validation recomputes
    ↓
User clicks Apply
    ↓
selected → applied (commit)
    ↓
Build payload
    ↓
Fetch from backend
    ↓
Update table data
    ↓
Table re-renders

Why This Matters

  • Draft state: what the user is currently editing (may not be valid)
  • Applied state: what the system is actually using

This prevents excessive API calls and ensures exports match what's visible on screen.


Vue Strengths, Weaknesses, and Use Cases

Strengths

Strength Description
Gentle learning curve Approachable syntax; good docs; Vue 3 Composition API feels natural
Reactivity system Automatic UI updates without manual DOM manipulation
Single-file components Template, logic, and styles in one file—easy to understand
Flexible architecture Use it for simple widgets or complex SPAs
Great tooling Vite dev server, Vue DevTools, excellent TypeScript support
Active ecosystem Vuetify (Material UI), Nuxt (SSR/SSG), Pinia (state management)

Weaknesses

Weakness Description
JavaScript ecosystem Requires understanding npm, build tools, ES modules
Reactivity gotchas Objects/arrays need special handling; .value confusion
Mobile/Web separation Vue itself is web-only (consider NativeScript or Capacitor for mobile)
Smaller ecosystem than React Fewer third-party libraries

Typical Use Cases

Use Case Example
Single-page applications (SPAs) Dashboards, admin panels, SaaS products
Interactive UI components Data tables with filters, dynamic forms, real-time updates
Progressive enhancement Add Vue to existing apps (like you might use jQuery)
Prototyping Quick MVPs with Vuetify or Tailwind UI components

Quick Reference

Template Syntax

{{ value }}                    <!-- interpolation -->
v-model="variable"             <!-- two-way binding -->
@click="handler"               <!-- click event -->
:prop="value"                  <!-- bind attribute -->
v-if="condition"               <!-- conditional -->
v-for="item in items"          <!-- loop -->

Script Basics

import { ref, computed, onMounted } from 'vue';

const variable = ref(initialValue);     // reactive state
const derived = computed(() => ...);     // derived state
onMounted(() => { ... });               // lifecycle hook

Common Patterns

Pattern Code
Reading a ref variable.value in script, variable in template
Writing a ref variable.value = newValue
Defining props const props = defineProps<Props>()
Optional props withDefaults(defineProps<Props>(), { optional: 'default' })
Emitting events emit('eventName', data)

Summary

Vue combines:

  • Reactivity: UI updates automatically when state changes
  • Components: Reusable pieces combining template, logic, and styles
  • Composables: Shared logic extracted into testable functions
  • Props and Events: Clear data flow between components

The key mental shift from Python:

Python Thinking Vue Thinking
Call functions to update view Change state, view updates automatically
Classes hold state and methods Components hold reactive refs and functions
Import modules for logic Import composables for reusable logic

Further Reading