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`
|
||||
Reference in New Issue
Block a user