* changed vue.instructions to be used for *.vue, *.ts, *.js, *.scss * remove unneeded files or files that may conflict with some peoples' protection meanings * ToDos as the popped up during cleaning
8.5 KiB
description, applyTo
| description | applyTo |
|---|---|
| Instructions for writing Vue 3 components following project conventions and best practices | **/*.vue, **/*.ts, **/*.js, **/*.scss |
Vue 3 Development Instructions
Follow Vue 3 best practices and project-specific conventions when writing components.
Component Structure
Standard Component Layout
<template>
<!-- HTML template -->
</template>
<style scoped>
/* Component-specific styles */
</style>
<script>
// Component logic
</script>
Order matters: Template, Style, Script (as seen throughout the codebase)
Options API Pattern (Primary)
Use Options API for consistency with existing codebase:
<script>
export default {
name: "ComponentName",
props: ["id"],
data() {
return {
items: [],
isLoading: false
}
},
async created() {
// Initialization logic
},
methods: {
async methodName() {
// Use const/let, never var
const response = await API.get('/api/endpoint')
this.items = response.data
}
}
}
</script>
API Communication
Using the API Utility
Always use API from utils/api.js - it handles authentication automatically:
import API from '../utils/api'
// In methods:
const response = await API.get(`/api/characters/${this.id}`)
const data = await API.post('/api/characters', character)
Never manually add Authorization headers - the interceptor handles this.
Environment Variables
API base URL from Vite:
import API from '../utils/api'
// API uses: import.meta.env.VITE_API_URL || 'http://localhost:8180'
API Configuration (utils/api.js)
Standard Axios instance with interceptors:
import axios from 'axios'
const API = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8180'
})
// Request interceptor - adds auth token
API.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => Promise.reject(error)
)
// Response interceptor - handles 401
API.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('token')
}
return Promise.reject(error)
}
)
export default API
Error Handling Pattern
try {
const response = await API.get(`/api/endpoint`)
this.data = response.data
} catch (error) {
console.error('Failed to load data:', error)
const errorMsg = error.response?.data?.error ?? error.message
alert(`${this.$t('errors.loadFailed')}: ${errorMsg}`)
}
Internationalization (i18n)
Using Translations
<template>
<h2>{{ $t('char') }}: {{ character.name }}</h2>
<button>{{ $t('export.exportPDF') }}</button>
</template>
Adding New Translations
ALWAYS add to both src/locales/de and src/locales/en:
// src/locales/de
export default {
export: {
selectTemplate: 'Vorlage wählen',
exportPDF: 'PDF Export'
}
}
// src/locales/en
export default {
export: {
selectTemplate: 'Select Template',
exportPDF: 'Export PDF'
}
}
Note: Locale files use .js extension and export objects, not JSON.
Modal Dialog Pattern
Standard Modal Structure
<template>
<!-- Trigger -->
<button @click="showDialog = true">Open</button>
<!-- Modal -->
<div v-if="showDialog" class="modal-overlay" @click.self="showDialog = false">
<div class="modal-content">
<div class="modal-header">
<h3>{{ $t('modal.title') }}</h3>
<button @click="showDialog = false" class="close-button">×</button>
</div>
<div class="modal-body">
<!-- Content -->
</div>
<div class="modal-footer">
<button @click="showDialog = false" class="btn-cancel">{{ $t('cancel') }}</button>
<button @click="handleSubmit" class="btn-primary">{{ $t('submit') }}</button>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
showDialog: false
}
}
}
</script>
Key conventions:
- Use
@click.selfon overlay to close on outside click - Include close button (×) in header
- Separate header, body, footer sections
Component Communication
Props (Parent → Child)
<script>
export default {
props: {
character: Object,
id: [String, Number]
}
}
</script>
Events (Child → Parent)
<template>
<button @click="notifyParent">Update</button>
</template>
<script>
export default {
methods: {
notifyParent() {
this.$emit('character-updated', this.character)
}
}
}
</script>
<!-- Parent component -->
<template>
<ChildComponent @character-updated="refreshCharacter" />
</template>
State Management
Pinia Store Pattern (stores/)
For global state (language, auth, etc.):
import { defineStore } from 'pinia'
export const useLanguageStore = defineStore('language', {
state: () => ({
currentLanguage: localStorage.getItem('language') || 'de'
}),
actions: {
setLanguage(lang) {
this.currentLanguage = lang
localStorage.setItem('language', lang)
}
}
})
Loading States
Always show feedback for async operations:
<template>
<button @click="submit" :disabled="isLoading">
<span v-if="!isLoading">{{ $t('submit') }}</span>
<span v-else>{{ $t('loading') }}</span>
</button>
</template>
<script>
export default {
data() {
return { isLoading: false }
},
methods: {
async submit() {
this.isLoading = true
try {
await API.post('/api/endpoint', this.data)
} finally {
this.isLoading = false
}
}
}
}
</script>
Disabling Form Elements During Loading
<select v-model="selected" :disabled="isLoading">
<input type="checkbox" v-model="option" :disabled="isLoading">
Form Validation
Validation Pattern
methods: {
validateForm() {
if (!this.selectedTemplate) {
alert(this.$t('export.pleaseSelectTemplate'))
return false
}
return true
},
async submit() {
if (!this.validateForm()) return
this.isLoading = true
try {
await API.post('/api/endpoint', this.data)
} finally {
this.isLoading = false
}
}
}
Browser Compatibility
Popup Blocker Workaround
Open windows synchronously before async operations:
async exportToPDF() {
// Open window FIRST (synchronously)
const pdfWindow = window.open('', '_blank')
if (!pdfWindow) {
alert(this.$t('export.popupBlocked'))
return
}
// Show loading page
pdfWindow.document.write('<html>...</html>')
// Then do async work
const response = await API.get('/api/pdf/export')
// Update window with result
pdfWindow.location.href = url
}
Critical: window.open() must be called synchronously in the click handler, not after await.
Common Patterns
Dynamic Component Loading
<template>
<component :is="currentView" :character="character" @character-updated="refresh"/>
</template>
<script>
import ViewA from './ViewA.vue'
import ViewB from './ViewB.vue'
export default {
components: { ViewA, ViewB },
data() {
return { currentView: 'ViewA' }
}
}
</script>
Conditional Rendering
- Use
v-iffor elements that toggle rarely - Use
v-showfor frequent toggles - Use
v-forwith:keyattribute always
Event Modifiers
@click.self- only trigger if clicked element itself@submit.prevent- prevent form submission@keyup.enter- keyboard event handling
Best Practices
- Use global CSS definition to ensure consistent styling
- Always use scoped styles to avoid CSS conflicts
- Name components with PascalCase (e.g.,
CharacterDetails.vue) - Use
constby default,letwhen needed - never usevar - Use optional chaining
?.and nullish coalescing??for safe property access - Handle errors gracefully with user-friendly messages
- Keep template logic simple - move complex logic to methods
- Clean up resources in
beforeUnmountif needed - Test with actual API running in Docker container
- Check HMR reload - view logs:
docker logs bamort-frontend-dev
Anti-Patterns to Avoid
❌ Don't use v-if and v-for on the same element
❌ Don't mutate props directly
❌ Don't use var - use const or let
❌ Don't use == - always use === for comparisons
❌ Don't forget to handle loading and error states
❌ Don't use inline styles - use scoped CSS
❌ Don't call API methods in template expressions
❌ Don't forget translations - add to both DE and EN