init at work
This commit is contained in:
Executable
+747
@@ -0,0 +1,747 @@
|
||||
# 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`
|
||||
Executable
+215
@@ -0,0 +1,215 @@
|
||||
# Vue for Python Developers
|
||||
|
||||
This note gives you a practical mental model for Vue if you already know Python.
|
||||
|
||||
## The Big Idea
|
||||
|
||||
Vue components are small units that combine:
|
||||
|
||||
- template: what should be shown
|
||||
- script: data and logic
|
||||
- style: how it should look
|
||||
|
||||
A Vue single-file component often looks like this:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<p>{{ message }}</p>
|
||||
<button @click="increment">Clicked {{ count }} times</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const message = 'Hello from Vue';
|
||||
const count = ref(0);
|
||||
|
||||
function increment() {
|
||||
count.value += 1;
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Python Analogy
|
||||
|
||||
Think of a Vue component like a Python object plus a small HTML view.
|
||||
|
||||
Python-style thinking:
|
||||
|
||||
```python
|
||||
class CounterPage:
|
||||
def __init__(self):
|
||||
self.message = "Hello from Vue"
|
||||
self.count = 0
|
||||
|
||||
def increment(self):
|
||||
self.count += 1
|
||||
```
|
||||
|
||||
Vue-style thinking:
|
||||
|
||||
```ts
|
||||
const message = 'Hello from Vue';
|
||||
const count = ref(0);
|
||||
|
||||
function increment() {
|
||||
count.value += 1;
|
||||
}
|
||||
```
|
||||
|
||||
Difference:
|
||||
|
||||
- In Python, changing `self.count` changes object state.
|
||||
- In Vue, changing `count.value` changes reactive state and the UI updates automatically.
|
||||
|
||||
## What "Reactive" Means
|
||||
|
||||
Reactive means the UI watches data. When the data changes, Vue updates the screen.
|
||||
|
||||
Example:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<p>{{ name }}</p>
|
||||
<button @click="rename">Rename</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const name = ref('Mathias');
|
||||
|
||||
function rename() {
|
||||
name.value = 'Mathias 2';
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
When `name.value` changes, the paragraph updates.
|
||||
|
||||
## Template vs Script
|
||||
|
||||
The template is not plain HTML. It is HTML with Vue features.
|
||||
|
||||
Example:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<p>{{ username }}</p>
|
||||
<button @click="login">Login</button>
|
||||
</template>
|
||||
```
|
||||
|
||||
- `{{ username }}` means: print a variable into the HTML.
|
||||
- `@click="login"` means: run the function when clicked.
|
||||
|
||||
The script defines the variables and functions used by the template.
|
||||
|
||||
```ts
|
||||
<script setup lang="ts">
|
||||
const username = 'alice';
|
||||
|
||||
function login() {
|
||||
console.log('login clicked');
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
## Common Vue Building Blocks
|
||||
|
||||
### `ref`
|
||||
|
||||
Used for reactive single values.
|
||||
|
||||
```ts
|
||||
const count = ref(0);
|
||||
const username = ref('alice');
|
||||
const isLoading = ref(false);
|
||||
```
|
||||
|
||||
### `computed`
|
||||
|
||||
Used for values derived from other values.
|
||||
|
||||
```ts
|
||||
const firstName = ref('Ada');
|
||||
const lastName = ref('Lovelace');
|
||||
|
||||
const fullName = computed(() => `${firstName.value} ${lastName.value}`);
|
||||
```
|
||||
|
||||
### event handlers
|
||||
|
||||
Functions triggered by user actions.
|
||||
|
||||
```vue
|
||||
<button @click="save">Save</button>
|
||||
```
|
||||
|
||||
```ts
|
||||
function save() {
|
||||
console.log('saving');
|
||||
}
|
||||
```
|
||||
|
||||
### lifecycle hooks
|
||||
|
||||
Code that runs when a component starts.
|
||||
|
||||
```ts
|
||||
onMounted(async () => {
|
||||
await loadData();
|
||||
});
|
||||
```
|
||||
|
||||
This is roughly like running setup code after the component appears on screen.
|
||||
|
||||
## `v-model` in One Sentence
|
||||
|
||||
`v-model` is two-way binding between a form field and a reactive variable.
|
||||
|
||||
```vue
|
||||
<input v-model="username" />
|
||||
```
|
||||
|
||||
If the user types into the input, `username` changes.
|
||||
If `username` changes in code, the input display changes.
|
||||
|
||||
## Example: Filter Form
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<input v-model="searchText" />
|
||||
<button @click="apply">Apply</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const searchText = ref('');
|
||||
|
||||
function apply() {
|
||||
console.log('User searched for:', searchText.value);
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
This is very similar to your date filter selects in FlexibilityTable.
|
||||
|
||||
## Good Beginner Rule
|
||||
|
||||
When reading Vue code, ask these four questions:
|
||||
|
||||
1. What values are reactive state?
|
||||
2. Which template elements use that state?
|
||||
3. Which functions change that state?
|
||||
4. Which computed values or API calls depend on that state?
|
||||
|
||||
If you answer those four, most Vue components become understandable.
|
||||
|
||||
## Related Notes
|
||||
|
||||
- [[01-Refs, Computed, and Reactivity]]
|
||||
- [[02-Templates, v-model, and Events]]
|
||||
- [[03-Components, Props, Lifecycle, and Composables]]
|
||||
- [[04-FlexibilityTable Filter Flow]]
|
||||
Executable
+252
@@ -0,0 +1,252 @@
|
||||
# Refs, Computed, and Reactivity
|
||||
|
||||
This note explains the most important Vue state concepts for a Python developer.
|
||||
|
||||
## `ref`: reactive storage for one value
|
||||
|
||||
A `ref` wraps a value so Vue can track it.
|
||||
|
||||
```ts
|
||||
import { ref } from 'vue';
|
||||
|
||||
const count = ref(0);
|
||||
const username = ref('mathias');
|
||||
const isLoading = ref(false);
|
||||
```
|
||||
|
||||
In script code, you read and write using `.value`.
|
||||
|
||||
```ts
|
||||
count.value += 1;
|
||||
username.value = 'new name';
|
||||
isLoading.value = true;
|
||||
```
|
||||
|
||||
In templates, Vue unwraps refs automatically.
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<p>{{ count }}</p>
|
||||
</template>
|
||||
```
|
||||
|
||||
You write `count`, not `count.value`, in the template.
|
||||
|
||||
## Python comparison
|
||||
|
||||
Python:
|
||||
|
||||
```python
|
||||
count = 0
|
||||
count += 1
|
||||
```
|
||||
|
||||
Vue script:
|
||||
|
||||
```ts
|
||||
const count = ref(0);
|
||||
count.value += 1;
|
||||
```
|
||||
|
||||
The extra `.value` exists because `count` is a reactive wrapper object, not the raw number.
|
||||
|
||||
## 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`.
|
||||
|
||||
## `computed`: derived state
|
||||
|
||||
A `computed` value is 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}`;
|
||||
});
|
||||
```
|
||||
|
||||
Use `computed` when a value can be derived instead of stored manually.
|
||||
|
||||
Bad pattern:
|
||||
|
||||
```ts
|
||||
const firstName = ref('Ada');
|
||||
const lastName = ref('Lovelace');
|
||||
const fullName = ref('Ada Lovelace');
|
||||
```
|
||||
|
||||
Now you must remember to update `fullName` yourself every time.
|
||||
|
||||
Better:
|
||||
|
||||
```ts
|
||||
const fullName = computed(() => `${firstName.value} ${lastName.value}`);
|
||||
```
|
||||
|
||||
## Python comparison
|
||||
|
||||
This is similar to a property.
|
||||
|
||||
```python
|
||||
class Person:
|
||||
def __init__(self, first_name, last_name):
|
||||
self.first_name = first_name
|
||||
self.last_name = last_name
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
return f"{self.first_name} {self.last_name}"
|
||||
```
|
||||
|
||||
Vue `computed` plays a similar role.
|
||||
|
||||
## Real example from your app
|
||||
|
||||
In FlexibilityTable, this pattern appears:
|
||||
|
||||
```ts
|
||||
const hasValidPeriodRange = computed(() => {
|
||||
if (!selectedPeriodStartFrom.value || !selectedPeriodStartUntil.value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return new Date(selectedPeriodStartFrom.value).getTime() <= new Date(selectedPeriodStartUntil.value).getTime();
|
||||
});
|
||||
```
|
||||
|
||||
This means:
|
||||
|
||||
- if one boundary is missing, the range is treated as valid
|
||||
- if both are set, start must be before or equal to end
|
||||
|
||||
The component does not store `hasValidPeriodRange` manually. It derives it from the two selected dates.
|
||||
|
||||
## Another example: filtered list
|
||||
|
||||
```ts
|
||||
const searchText = ref('');
|
||||
const users = ref(['Alice', 'Bob', 'Charlie']);
|
||||
|
||||
const filteredUsers = computed(() => {
|
||||
return users.value.filter((user) =>
|
||||
user.toLowerCase().includes(searchText.value.toLowerCase()),
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
If `searchText` changes, `filteredUsers` updates automatically.
|
||||
|
||||
## Rule of Thumb
|
||||
|
||||
Use `ref` when:
|
||||
|
||||
- the value changes over time
|
||||
- the UI should react to that change
|
||||
- the value is primary state
|
||||
|
||||
Use `computed` when:
|
||||
|
||||
- the value is calculated from other reactive values
|
||||
- you do not want to duplicate state
|
||||
|
||||
## Common Beginner Mistakes
|
||||
|
||||
### forgetting `.value` in script
|
||||
|
||||
Wrong:
|
||||
|
||||
```ts
|
||||
count += 1;
|
||||
```
|
||||
|
||||
Right:
|
||||
|
||||
```ts
|
||||
count.value += 1;
|
||||
```
|
||||
|
||||
### using `computed` for side effects
|
||||
|
||||
Bad:
|
||||
|
||||
```ts
|
||||
const result = computed(() => {
|
||||
console.log('side effect');
|
||||
return count.value * 2;
|
||||
});
|
||||
```
|
||||
|
||||
A computed should mainly calculate and return a value.
|
||||
|
||||
### storing what can be derived
|
||||
|
||||
Bad:
|
||||
|
||||
```ts
|
||||
const selectedFirst = ref('2026-01-01');
|
||||
const selectedSecond = ref('2026-01-10');
|
||||
const isValid = ref(true);
|
||||
```
|
||||
|
||||
Better:
|
||||
|
||||
```ts
|
||||
const isValid = computed(() => selectedFirst.value <= selectedSecond.value);
|
||||
```
|
||||
|
||||
## Tiny Exercise
|
||||
|
||||
What should be `ref` and what should be `computed`?
|
||||
|
||||
Scenario:
|
||||
|
||||
- user types first name
|
||||
- user types last name
|
||||
- screen shows full name
|
||||
- submit button disabled when first name is empty
|
||||
|
||||
Answer:
|
||||
|
||||
```ts
|
||||
const firstName = ref('');
|
||||
const lastName = ref('');
|
||||
const fullName = computed(() => `${firstName.value} ${lastName.value}`.trim());
|
||||
const isSubmitDisabled = computed(() => !firstName.value.trim());
|
||||
```
|
||||
|
||||
## Related Notes
|
||||
|
||||
- [[00-Vue for Python Developers]]
|
||||
- [[02-Templates, v-model, and Events]]
|
||||
- [[04-FlexibilityTable Filter Flow]]
|
||||
Executable
+252
@@ -0,0 +1,252 @@
|
||||
# Templates, v-model, and Events
|
||||
|
||||
This note explains how Vue templates talk to your script code.
|
||||
|
||||
## Templates are HTML with Vue features
|
||||
|
||||
Example:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<p>{{ username }}</p>
|
||||
<input v-model="username" />
|
||||
<button @click="reset">Reset</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const username = ref('mathias');
|
||||
|
||||
function reset() {
|
||||
username.value = '';
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
The same variable is:
|
||||
|
||||
- shown inside `{{ username }}`
|
||||
- edited by `<input v-model="username" />`
|
||||
- changed by the `reset()` function
|
||||
|
||||
## `{{ ... }}`: show a value
|
||||
|
||||
```vue
|
||||
<p>{{ username }}</p>
|
||||
<p>{{ isLoading ? 'Loading...' : 'Done' }}</p>
|
||||
<p>{{ 2 + 3 }}</p>
|
||||
```
|
||||
|
||||
This is interpolation. It prints a value into the rendered HTML.
|
||||
|
||||
## `v-model`: two-way binding
|
||||
|
||||
`v-model` connects a form field and a reactive variable.
|
||||
|
||||
```vue
|
||||
<input v-model="email" />
|
||||
```
|
||||
|
||||
```ts
|
||||
const email = ref('');
|
||||
```
|
||||
|
||||
Two-way binding means:
|
||||
|
||||
- if the user types, `email` changes
|
||||
- if code changes `email`, the input display changes
|
||||
|
||||
## Example with a select
|
||||
|
||||
```vue
|
||||
<v-select
|
||||
v-model="selectedCountry"
|
||||
:items="countries"
|
||||
label="Country"
|
||||
/>
|
||||
```
|
||||
|
||||
```ts
|
||||
const countries = ['Germany', 'France', 'Spain'];
|
||||
const selectedCountry = ref<string | null>(null);
|
||||
```
|
||||
|
||||
When the user picks France, `selectedCountry.value` becomes `'France'`.
|
||||
|
||||
## Your real example
|
||||
|
||||
From FlexibilityTable:
|
||||
|
||||
```vue
|
||||
<v-select
|
||||
v-model="selectedPeriodStartFrom"
|
||||
:items="periodStartOptions"
|
||||
item-title="title"
|
||||
item-value="value"
|
||||
label="Period start from"
|
||||
/>
|
||||
```
|
||||
|
||||
This means:
|
||||
|
||||
- the dropdown shows options from `periodStartOptions`
|
||||
- each option is an object
|
||||
- `title` is shown to the user
|
||||
- `value` is stored in `selectedPeriodStartFrom`
|
||||
|
||||
If one item is:
|
||||
|
||||
```ts
|
||||
{ title: '2026-03-23', value: '2026-03-23T23:00:00.000Z' }
|
||||
```
|
||||
|
||||
then the user sees:
|
||||
|
||||
```text
|
||||
2026-03-23
|
||||
```
|
||||
|
||||
but the stored value becomes:
|
||||
|
||||
```ts
|
||||
selectedPeriodStartFrom.value = '2026-03-23T23:00:00.000Z';
|
||||
```
|
||||
|
||||
## Events with `@click`
|
||||
|
||||
`@click` means: call a function when the element is clicked.
|
||||
|
||||
```vue
|
||||
<button @click="increment">Add</button>
|
||||
```
|
||||
|
||||
```ts
|
||||
function increment() {
|
||||
count.value += 1;
|
||||
}
|
||||
```
|
||||
|
||||
Other common events:
|
||||
|
||||
```vue
|
||||
<input @input="onInput" />
|
||||
<form @submit.prevent="submitForm" />
|
||||
<select @change="onChange" />
|
||||
```
|
||||
|
||||
## Binding attributes with `:`
|
||||
|
||||
The `:` shorthand means "bind this HTML or component prop to JavaScript".
|
||||
|
||||
```vue
|
||||
<button :disabled="isLoading">Save</button>
|
||||
```
|
||||
|
||||
Equivalent long form:
|
||||
|
||||
```vue
|
||||
<button v-bind:disabled="isLoading">Save</button>
|
||||
```
|
||||
|
||||
If `isLoading` is true, the button becomes disabled.
|
||||
|
||||
## Example: disable refresh button
|
||||
|
||||
In your component:
|
||||
|
||||
```vue
|
||||
<v-btn
|
||||
:disabled="loading || !hasValidPeriodRange"
|
||||
@click="applyDateFilters"
|
||||
>
|
||||
Refresh
|
||||
</v-btn>
|
||||
```
|
||||
|
||||
This means:
|
||||
|
||||
- disable the button if data is loading
|
||||
- also disable it if the selected date range is invalid
|
||||
- when clicked, run `applyDateFilters`
|
||||
|
||||
## Conditional rendering
|
||||
|
||||
Show something only if a condition is true:
|
||||
|
||||
```vue
|
||||
<p v-if="errorMessage">{{ errorMessage }}</p>
|
||||
<p v-else>No errors</p>
|
||||
```
|
||||
|
||||
## Loops with `v-for`
|
||||
|
||||
```vue
|
||||
<li v-for="user in users" :key="user.id">
|
||||
{{ user.name }}
|
||||
</li>
|
||||
```
|
||||
|
||||
This repeats the element for each item.
|
||||
|
||||
## Common beginner confusion: where does a template variable come from?
|
||||
|
||||
If you see this in a template:
|
||||
|
||||
```vue
|
||||
<p>{{ total }}</p>
|
||||
```
|
||||
|
||||
search the script for:
|
||||
|
||||
- `const total = ...`
|
||||
- `let total = ...`
|
||||
- `function total() ...`
|
||||
- `computed(() => ...)`
|
||||
- `defineProps(...)`
|
||||
|
||||
The template only knows values exposed by the script.
|
||||
|
||||
## Mental model for reading templates
|
||||
|
||||
For every template line, ask:
|
||||
|
||||
1. Is it displaying a value?
|
||||
2. Is it binding a prop?
|
||||
3. Is it listening for an event?
|
||||
4. Which script variable or function does it connect to?
|
||||
|
||||
## Mini Example: search box
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<input v-model="searchText" placeholder="Search users" />
|
||||
<button :disabled="!searchText" @click="search">Search</button>
|
||||
<p>You entered: {{ searchText }}</p>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const searchText = ref('');
|
||||
|
||||
function search() {
|
||||
console.log('Searching for', searchText.value);
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
Flow:
|
||||
|
||||
- user types into input
|
||||
- `searchText` updates
|
||||
- paragraph updates immediately
|
||||
- button disable state updates immediately
|
||||
- clicking Search uses current `searchText`
|
||||
|
||||
## Related Notes
|
||||
|
||||
- [[00-Vue for Python Developers]]
|
||||
- [[01-Refs, Computed, and Reactivity]]
|
||||
- [[03-Components, Props, Lifecycle, and Composables]]
|
||||
- [[04-FlexibilityTable Filter Flow]]
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
# Components, Props, Lifecycle, and Composables
|
||||
|
||||
This note explains how Vue code is organized into reusable pieces.
|
||||
|
||||
## Components
|
||||
|
||||
A component is a reusable UI unit.
|
||||
|
||||
Examples from your project:
|
||||
|
||||
- `FlexibilityTable.vue`
|
||||
- `FileUpload.vue`
|
||||
- `AppHeader.vue`
|
||||
|
||||
A parent component can render a child component.
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<UserCard />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import UserCard from './UserCard.vue';
|
||||
</script>
|
||||
```
|
||||
|
||||
## Props: inputs passed into a component
|
||||
|
||||
Props are like function arguments for components.
|
||||
|
||||
Example:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<UserCard :name="username" :age="34" />
|
||||
</template>
|
||||
```
|
||||
|
||||
Child component:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
interface Props {
|
||||
name: string;
|
||||
age: number;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<p>{{ props.name }} is {{ props.age }} years old.</p>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Python comparison
|
||||
|
||||
This is similar to passing constructor arguments or function arguments.
|
||||
|
||||
```python
|
||||
render_user_card(name=username, age=34)
|
||||
```
|
||||
|
||||
## Your real example
|
||||
|
||||
In FlexibilityTable:
|
||||
|
||||
```ts
|
||||
interface FlexibilityTableProps {
|
||||
title: string;
|
||||
uploadMode?: 'fsp' | 'tso' | 'both' | 'none';
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<FlexibilityTableProps>(), {
|
||||
uploadMode: 'none',
|
||||
});
|
||||
```
|
||||
|
||||
This means:
|
||||
|
||||
- the parent must give `title`
|
||||
- the parent may give `uploadMode`
|
||||
- if `uploadMode` is missing, use `'none'`
|
||||
|
||||
Then template code can use:
|
||||
|
||||
```vue
|
||||
<v-toolbar-title>
|
||||
{{ title }}
|
||||
</v-toolbar-title>
|
||||
```
|
||||
|
||||
and script code can use:
|
||||
|
||||
```ts
|
||||
props.uploadMode
|
||||
```
|
||||
|
||||
## Lifecycle: when should code run?
|
||||
|
||||
Sometimes you want code to run when the component appears.
|
||||
|
||||
```ts
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
onMounted(() => {
|
||||
console.log('Component is mounted');
|
||||
});
|
||||
```
|
||||
|
||||
In your component:
|
||||
|
||||
```ts
|
||||
onMounted(async () => {
|
||||
await refreshTableDataFromBackend();
|
||||
});
|
||||
```
|
||||
|
||||
That means when the table component is loaded, it immediately fetches backend data.
|
||||
|
||||
## Composables
|
||||
|
||||
A composable is a reusable function that contains Vue logic.
|
||||
|
||||
Common pattern:
|
||||
|
||||
```ts
|
||||
export function useCounter() {
|
||||
const count = ref(0);
|
||||
|
||||
function increment() {
|
||||
count.value += 1;
|
||||
}
|
||||
|
||||
return {
|
||||
count,
|
||||
increment,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Use it inside a component:
|
||||
|
||||
```ts
|
||||
const { count, increment } = useCounter();
|
||||
```
|
||||
|
||||
## Why composables exist
|
||||
|
||||
Without composables, components get too big.
|
||||
|
||||
Instead of putting all API logic, auth logic, upload logic, and export logic directly in one component, you move reusable logic into composables.
|
||||
|
||||
Your project has examples:
|
||||
|
||||
- `useJsonDownload()`
|
||||
- `useJsonUpload()`
|
||||
- `useAuth()`
|
||||
|
||||
## Real example: `useJsonDownload`
|
||||
|
||||
In FlexibilityTable:
|
||||
|
||||
```ts
|
||||
const { downloadFlexData, downloadRawFlexData } = useJsonDownload();
|
||||
```
|
||||
|
||||
That means:
|
||||
|
||||
- this component imports data-loading logic from a composable
|
||||
- the composable handles HTTP and backend transformation details
|
||||
- the component only calls its functions
|
||||
|
||||
This is similar to separating Python business logic into a helper module.
|
||||
|
||||
Python style:
|
||||
|
||||
```python
|
||||
from downloads import download_flex_data
|
||||
|
||||
rows = download_flex_data(payload)
|
||||
```
|
||||
|
||||
Vue composable style:
|
||||
|
||||
```ts
|
||||
const { downloadFlexData } = useJsonDownload();
|
||||
const rows = await downloadFlexData(payload);
|
||||
```
|
||||
|
||||
## Child-to-parent communication
|
||||
|
||||
Props go down from parent to child.
|
||||
|
||||
Events often go up from child to parent.
|
||||
|
||||
Example:
|
||||
|
||||
```vue
|
||||
<FileUpload @upload="onFspUpload" />
|
||||
```
|
||||
|
||||
This means the child component emits an `upload` event, and the parent runs `onFspUpload` when that happens.
|
||||
|
||||
## Example structure
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<SearchBox @search="runSearch" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
function runSearch(query: string) {
|
||||
console.log('search query:', query);
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
The child says: "something happened".
|
||||
The parent decides what to do.
|
||||
|
||||
## Rule of Thumb
|
||||
|
||||
Put code in a component when:
|
||||
|
||||
- it is very local to one UI element
|
||||
- it mostly controls rendering
|
||||
|
||||
Put code in a composable when:
|
||||
|
||||
- it will be reused
|
||||
- it handles data fetching
|
||||
- it contains business logic
|
||||
- it makes the component easier to read
|
||||
|
||||
## Reading Strategy
|
||||
|
||||
When you see a component, identify:
|
||||
|
||||
1. props coming in
|
||||
2. local reactive state
|
||||
3. composables being used
|
||||
4. lifecycle hooks
|
||||
5. events going to child components
|
||||
|
||||
## Related Notes
|
||||
|
||||
- [[00-Vue for Python Developers]]
|
||||
- [[02-Templates, v-model, and Events]]
|
||||
- [[04-FlexibilityTable Filter Flow]]
|
||||
Executable
+332
@@ -0,0 +1,332 @@
|
||||
# FlexibilityTable Filter Flow
|
||||
|
||||
This note explains how your `selectedPeriodStartFrom` example interacts with the rest of the code.
|
||||
|
||||
## Goal of the feature
|
||||
|
||||
The table lets the user:
|
||||
|
||||
- choose a start period
|
||||
- choose an end period
|
||||
- click Refresh
|
||||
- load matching backend data
|
||||
- view and export the filtered result
|
||||
|
||||
## The important variables
|
||||
|
||||
From FlexibilityTable:
|
||||
|
||||
```ts
|
||||
type PeriodFilterValue = string | null;
|
||||
|
||||
const periodStartOptions = getDownloadPeriodStartOptions();
|
||||
const defaultPeriodStartFrom = periodStartOptions[0]?.value ?? null;
|
||||
const defaultPeriodStartUntil = periodStartOptions[periodStartOptions.length - 1]?.value ?? null;
|
||||
|
||||
const selectedPeriodStartFrom = ref<PeriodFilterValue>(defaultPeriodStartFrom);
|
||||
const selectedPeriodStartUntil = ref<PeriodFilterValue>(defaultPeriodStartUntil);
|
||||
const appliedPeriodStartFrom = ref<PeriodFilterValue>(defaultPeriodStartFrom);
|
||||
const appliedPeriodStartUntil = ref<PeriodFilterValue>(defaultPeriodStartUntil);
|
||||
```
|
||||
|
||||
There are two groups of state here.
|
||||
|
||||
### selected values
|
||||
|
||||
These track what the user is currently editing in the UI.
|
||||
|
||||
```ts
|
||||
selectedPeriodStartFrom
|
||||
selectedPeriodStartUntil
|
||||
```
|
||||
|
||||
### applied values
|
||||
|
||||
These track what was last confirmed with the Refresh button.
|
||||
|
||||
```ts
|
||||
appliedPeriodStartFrom
|
||||
appliedPeriodStartUntil
|
||||
```
|
||||
|
||||
This separation is very important.
|
||||
|
||||
## Why both selected and applied exist
|
||||
|
||||
Without this split, every dropdown change could immediately trigger a backend request.
|
||||
|
||||
That would mean:
|
||||
|
||||
- too many requests
|
||||
- surprising UI updates while the user is still choosing dates
|
||||
- exports might not match the table state clearly
|
||||
|
||||
So the component uses a draft pattern.
|
||||
|
||||
Python analogy:
|
||||
|
||||
```python
|
||||
form_input = {'start': '2026-03-01', 'end': '2026-03-31'}
|
||||
applied_filters = {'start': '2026-02-01', 'end': '2026-02-28'}
|
||||
```
|
||||
|
||||
The form input is what the user is editing.
|
||||
The applied filters are what the system is currently using.
|
||||
|
||||
## Step 1: build options for the dropdown
|
||||
|
||||
```ts
|
||||
const periodStartOptions = getDownloadPeriodStartOptions();
|
||||
```
|
||||
|
||||
This comes from `jsonDownload.ts`.
|
||||
That composable calculates valid backend week starts.
|
||||
|
||||
Each option looks roughly like this:
|
||||
|
||||
```ts
|
||||
{
|
||||
title: '2026-03-23',
|
||||
value: '2026-03-22T23:00:00.000Z'
|
||||
}
|
||||
```
|
||||
|
||||
- `title` is for humans
|
||||
- `value` is for the program
|
||||
|
||||
## Step 2: connect the dropdown to state
|
||||
|
||||
```vue
|
||||
<v-select
|
||||
v-model="selectedPeriodStartFrom"
|
||||
:items="periodStartOptions"
|
||||
item-title="title"
|
||||
item-value="value"
|
||||
label="Period start from"
|
||||
/>
|
||||
```
|
||||
|
||||
When the user chooses an option:
|
||||
|
||||
- Vuetify reads the chosen item
|
||||
- Vue writes its `value` field into `selectedPeriodStartFrom`
|
||||
- any code depending on `selectedPeriodStartFrom` can react
|
||||
|
||||
## Step 3: validate the selected range
|
||||
|
||||
```ts
|
||||
const hasValidPeriodRange = computed(() => {
|
||||
if (!selectedPeriodStartFrom.value || !selectedPeriodStartUntil.value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return new Date(selectedPeriodStartFrom.value).getTime() <= new Date(selectedPeriodStartUntil.value).getTime();
|
||||
});
|
||||
```
|
||||
|
||||
This computed reads the two selected values and checks if the range makes sense.
|
||||
|
||||
The Refresh button uses it:
|
||||
|
||||
```vue
|
||||
<v-btn
|
||||
:disabled="loading || !hasValidPeriodRange"
|
||||
@click="applyDateFilters"
|
||||
>
|
||||
Refresh
|
||||
</v-btn>
|
||||
```
|
||||
|
||||
So if the user chooses an invalid range, the button becomes disabled.
|
||||
|
||||
## Step 4: user clicks Refresh
|
||||
|
||||
```ts
|
||||
async function applyDateFilters() {
|
||||
if (!hasValidPeriodRange.value) {
|
||||
showMessage('Select a valid period range before refreshing.', 'error', true);
|
||||
return;
|
||||
}
|
||||
|
||||
appliedPeriodStartFrom.value = selectedPeriodStartFrom.value;
|
||||
appliedPeriodStartUntil.value = selectedPeriodStartUntil.value;
|
||||
|
||||
await refreshTableDataFromBackend(
|
||||
'Could not load data from backend.',
|
||||
buildSelectedDownloadPayload(),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
This does three things:
|
||||
|
||||
1. validate
|
||||
2. copy selected values into applied values
|
||||
3. fetch new backend data
|
||||
|
||||
## Step 5: build the request payload
|
||||
|
||||
```ts
|
||||
function buildSelectedDownloadPayload(): DownloadPayload {
|
||||
return buildDownloadPayload(
|
||||
buildDateRange(selectedPeriodStartFrom.value, selectedPeriodStartUntil.value),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
and:
|
||||
|
||||
```ts
|
||||
function buildDateRange(periodStartFrom: PeriodFilterValue, periodStartUntil: PeriodFilterValue): DownloadDateRange {
|
||||
return {
|
||||
periodStartFrom: periodStartFrom ?? undefined,
|
||||
periodStartUntil: periodStartUntil ?? undefined,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
This turns UI values into backend filter input.
|
||||
|
||||
Important detail:
|
||||
|
||||
- `null` from the UI becomes `undefined` for the backend builder
|
||||
- that means "no boundary selected"
|
||||
|
||||
## Step 6: call the composable
|
||||
|
||||
```ts
|
||||
tableData.value = await downloadFlexData(payload);
|
||||
```
|
||||
|
||||
This happens inside `refreshTableDataFromBackend()`.
|
||||
|
||||
The component does not itself know how to talk to the backend in detail. That logic lives in `useJsonDownload()`.
|
||||
|
||||
## Step 7: transform backend data into rows
|
||||
|
||||
Inside `jsonDownload.ts`, the composable:
|
||||
|
||||
- picks the correct endpoint
|
||||
- sends the request
|
||||
- receives backend offer data
|
||||
- converts it into table rows and display rows
|
||||
|
||||
So the data flow is:
|
||||
|
||||
```text
|
||||
Dropdown -> selectedPeriodStartFrom -> payload builder -> downloadFlexData -> tableData -> rendered table
|
||||
```
|
||||
|
||||
## Step 8: render the table
|
||||
|
||||
The table rows come from:
|
||||
|
||||
```ts
|
||||
const rows = computed<FlexDisplayRow[]>(() => tableData.value.displayRows ?? []);
|
||||
```
|
||||
|
||||
Then more filtering happens:
|
||||
|
||||
```ts
|
||||
const filteredRows = computed<FlexDisplayRow[]>(() =>
|
||||
rows.value.filter((row) => {
|
||||
const aggregator = row.senderId ?? row.aggregator;
|
||||
const controlArea = row.receiverId ?? row.controlArea;
|
||||
|
||||
if (selectedAggregators.value.length && (!aggregator || !selectedAggregators.value.includes(aggregator))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selectedControlArea.value && controlArea !== selectedControlArea.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}),
|
||||
);
|
||||
```
|
||||
|
||||
Notice something important:
|
||||
|
||||
- date filters affect what is fetched from the backend
|
||||
- aggregator and control area filters affect what is shown locally in the frontend
|
||||
|
||||
That is a useful architectural distinction.
|
||||
|
||||
## Step 9: exports use applied filters
|
||||
|
||||
For JSON export:
|
||||
|
||||
```ts
|
||||
const rawData = await downloadRawFlexData(buildAppliedDownloadPayload());
|
||||
```
|
||||
|
||||
That uses applied values, not selected values.
|
||||
|
||||
Why?
|
||||
Because if the user changed the dropdown but did not click Refresh yet, the table still reflects the old filters. Export should match what the user is actually seeing as the active state.
|
||||
|
||||
## Full mental model
|
||||
|
||||
Use this simple chain:
|
||||
|
||||
```text
|
||||
1. Options are created
|
||||
2. User picks a value
|
||||
3. selectedPeriodStartFrom changes
|
||||
4. Validation recomputes
|
||||
5. User clicks Refresh
|
||||
6. selected values become applied values
|
||||
7. payload is built
|
||||
8. backend data is fetched
|
||||
9. tableData updates
|
||||
10. table re-renders
|
||||
```
|
||||
|
||||
## Tiny simplified example
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<select v-model="selectedColor">
|
||||
<option value="red">Red</option>
|
||||
<option value="blue">Blue</option>
|
||||
</select>
|
||||
|
||||
<button @click="apply">Apply</button>
|
||||
|
||||
<p>Selected: {{ selectedColor }}</p>
|
||||
<p>Applied: {{ appliedColor }}</p>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
const selectedColor = ref('red');
|
||||
const appliedColor = ref('red');
|
||||
|
||||
function apply() {
|
||||
appliedColor.value = selectedColor.value;
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
This is the same pattern as your date filters, just simpler.
|
||||
|
||||
## Key beginner lesson
|
||||
|
||||
When reading Vue, do not only ask "what is this variable".
|
||||
Ask:
|
||||
|
||||
- who writes it?
|
||||
- who reads it?
|
||||
- is it draft state or applied state?
|
||||
- does it affect frontend-only filtering or backend requests?
|
||||
|
||||
That is how the whole interaction becomes understandable.
|
||||
|
||||
## Related Notes
|
||||
|
||||
- [[00-Vue for Python Developers]]
|
||||
- [[01-Refs, Computed, and Reactivity]]
|
||||
- [[02-Templates, v-model, and Events]]
|
||||
- [[03-Components, Props, Lifecycle, and Composables]]
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
# Vue Learning Path
|
||||
|
||||
Start here if you are new to Vue and coming from Python.
|
||||
|
||||
## Suggested reading order
|
||||
|
||||
1. [[00-Vue for Python Developers]]
|
||||
2. [[01-Refs, Computed, and Reactivity]]
|
||||
3. [[02-Templates, v-model, and Events]]
|
||||
4. [[03-Components, Props, Lifecycle, and Composables]]
|
||||
5. [[04-FlexibilityTable Filter Flow]]
|
||||
|
||||
## What each note covers
|
||||
|
||||
### Vue for Python Developers
|
||||
|
||||
The big picture:
|
||||
|
||||
- what a Vue component is
|
||||
- template vs script
|
||||
- what reactive means
|
||||
- how to read Vue code without getting lost
|
||||
|
||||
### Refs, Computed, and Reactivity
|
||||
|
||||
State management basics:
|
||||
|
||||
- `ref`
|
||||
- `.value`
|
||||
- `computed`
|
||||
- when to store data vs derive data
|
||||
|
||||
### Templates, v-model, and Events
|
||||
|
||||
Template mechanics:
|
||||
|
||||
- interpolation with `{{ ... }}`
|
||||
- `v-model`
|
||||
- `@click`
|
||||
- `:disabled`
|
||||
- how template code connects to script code
|
||||
|
||||
### Components, Props, Lifecycle, and Composables
|
||||
|
||||
Code organization:
|
||||
|
||||
- parent and child components
|
||||
- props
|
||||
- `onMounted`
|
||||
- composables such as `useJsonDownload()`
|
||||
|
||||
### FlexibilityTable Filter Flow
|
||||
|
||||
Real project walkthrough:
|
||||
|
||||
- `selectedPeriodStartFrom`
|
||||
- selected vs applied filters
|
||||
- backend payload building
|
||||
- table refresh flow
|
||||
- export behavior
|
||||
|
||||
## Best way to study these notes
|
||||
|
||||
For each note:
|
||||
|
||||
1. Read the explanation slowly.
|
||||
2. Copy one code example into a small test component.
|
||||
3. Change one line and predict what the UI will do.
|
||||
4. Run it and compare expectation with result.
|
||||
|
||||
That loop is the fastest way to get comfortable with Vue.
|
||||
Reference in New Issue
Block a user