748 lines
17 KiB
Markdown
Executable File
748 lines
17 KiB
Markdown
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?](#what-is-vue)
|
|
2. [The Big Idea: Reactivity](#the-big-idea-reactivity)
|
|
3. [Your First Vue Component](#your-first-vue-component)
|
|
4. [Reactive State with `ref`](#reactive-state-with-ref)
|
|
5. [Derived State with `computed`](#derived-state-with-computed)
|
|
6. [Templates: HTML with Vue Features](#templates-html-with-vue-features)
|
|
7. [Two-Way Binding with `v-model`](#two-way-binding-with-v-model)
|
|
8. [Events and User Interaction](#events-and-user-interaction)
|
|
9. [Components and Props](#components-and-props)
|
|
10. [Lifecycle Hooks](#lifecycle-hooks)
|
|
11. [Composables: Reusable Logic](#composables-reusable-logic)
|
|
12. [A Real-World Example: Filter Flow](#a-real-world-example-filter-flow)
|
|
13. [Vue Strengths, Weaknesses, and Use Cases](#vue-strengths-weaknesses-and-use-cases)
|
|
14. [Quick Reference](#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
|
|
# 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
|
|
<!-- 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:
|
|
|
|
```vue
|
|
<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.
|
|
|
|
```ts
|
|
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`:
|
|
|
|
```ts
|
|
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`:
|
|
|
|
```vue
|
|
<template>
|
|
<p>{{ count }}</p> <!-- reads count.value -->
|
|
<p>{{ username }}</p> <!-- reads username.value -->
|
|
<button :disabled="isLoading">Save</button>
|
|
</template>
|
|
```
|
|
|
|
### Python Comparison
|
|
|
|
```python
|
|
# Python
|
|
count = 0
|
|
count += 1
|
|
```
|
|
|
|
```ts
|
|
// 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
|
|
|
|
```vue
|
|
<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.
|
|
|
|
```ts
|
|
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`:
|
|
|
|
```python
|
|
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
|
|
|
|
```ts
|
|
// 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
|
|
|
|
```ts
|
|
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:
|
|
|
|
```vue
|
|
<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:
|
|
|
|
```vue
|
|
<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:
|
|
|
|
```vue
|
|
<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:
|
|
|
|
```vue
|
|
<button :disabled="isLoading">Save</button>
|
|
<img :src="imageUrl" />
|
|
<a :href="profileUrl">View Profile</a>
|
|
```
|
|
|
|
Short for `v-bind:disabled`.
|
|
|
|
### Class Binding
|
|
|
|
```vue
|
|
<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
|
|
|
|
```vue
|
|
<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
|
|
|
|
```vue
|
|
<v-select
|
|
v-model="selectedCountry"
|
|
:items="countries"
|
|
item-title="title"
|
|
item-value="value"
|
|
/>
|
|
```
|
|
|
|
```ts
|
|
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`
|
|
|
|
```vue
|
|
<button @click="save">Save</button>
|
|
```
|
|
|
|
```ts
|
|
function save() {
|
|
console.log('saving...');
|
|
}
|
|
```
|
|
|
|
### Other Common Events
|
|
|
|
```vue
|
|
<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
|
|
|
|
```vue
|
|
<button @click="deleteItem(id)">Delete</button>
|
|
```
|
|
|
|
```ts
|
|
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
|
|
|
|
```vue
|
|
<!-- 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.
|
|
|
|
```vue
|
|
<!-- 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
|
|
# 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.
|
|
|
|
```vue
|
|
<!-- 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
|
|
|
|
```ts
|
|
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
|
|
|
|
```ts
|
|
// useCounter.ts
|
|
export function useCounter() {
|
|
const count = ref(0);
|
|
|
|
function increment() {
|
|
count.value += 1;
|
|
}
|
|
|
|
return { count, increment };
|
|
}
|
|
```
|
|
|
|
```vue
|
|
<!-- 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
|
|
|
|
```ts
|
|
// 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
|
|
# 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
|
|
|
|
```ts
|
|
// 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
|
|
|
|
```ts
|
|
const isValidRange = computed(() => {
|
|
if (!selectedStart.value || !selectedEnd.value) return true;
|
|
return new Date(selectedStart.value) <= new Date(selectedEnd.value);
|
|
});
|
|
```
|
|
|
|
### Apply Action
|
|
|
|
```ts
|
|
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
|
|
|
|
```vue
|
|
{{ 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
|
|
|
|
```ts
|
|
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
|
|
|
|
- [Vue 3 Documentation](https://vuejs.org/guide/)
|
|
- [Vue School Tutorials](https://vueschool.io/)
|
|
- [Vuetify Components](https://vuetifyjs.com/) (your project's UI framework)
|
|
- Project composables: `useJsonDownload`, `useJsonUpload`, `useAuth`
|