Some checks failed
Telegram Mini App Shop Builder / Compute version metadata (push) Has been cancelled
Telegram Mini App Shop Builder / Run Frontend tests (push) Has been cancelled
Telegram Mini App Shop Builder / Run Backend tests (push) Has been cancelled
Telegram Mini App Shop Builder / Run PHP_CodeSniffer (push) Has been cancelled
Telegram Mini App Shop Builder / Build module. (push) Has been cancelled
Telegram Mini App Shop Builder / release (push) Has been cancelled
371 lines
5.8 KiB
Markdown
371 lines
5.8 KiB
Markdown
# Vue.js 3 Rules
|
|
|
|
## Component Structure
|
|
|
|
### Template
|
|
|
|
```vue
|
|
<template>
|
|
<!-- ✅ Logical structure -->
|
|
<div class="container">
|
|
<header>
|
|
<h2>{{ title }}</h2>
|
|
</header>
|
|
|
|
<main>
|
|
<DataTable :value="items" />
|
|
</main>
|
|
|
|
<footer>
|
|
<Button @click="handleSave">Save</Button>
|
|
</footer>
|
|
</div>
|
|
</template>
|
|
```
|
|
|
|
### Script Setup
|
|
|
|
```vue
|
|
<script setup>
|
|
// ✅ Always use <script setup>
|
|
import { ref, computed, onMounted } from 'vue';
|
|
import DataTable from 'primevue/datatable';
|
|
import { apiGet } from '@/utils/http.js';
|
|
|
|
// Props
|
|
const props = defineProps({
|
|
title: {
|
|
type: String,
|
|
required: true
|
|
}
|
|
});
|
|
|
|
// Emits
|
|
const emit = defineEmits(['update', 'delete']);
|
|
|
|
// State
|
|
const items = ref([]);
|
|
const loading = ref(false);
|
|
|
|
// Computed
|
|
const totalItems = computed(() => items.value.length);
|
|
|
|
// Methods
|
|
async function loadItems() {
|
|
loading.value = true;
|
|
try {
|
|
const result = await apiGet('getItems');
|
|
items.value = result.data || [];
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
// Lifecycle
|
|
onMounted(() => {
|
|
loadItems();
|
|
});
|
|
</script>
|
|
```
|
|
|
|
### Styles
|
|
|
|
```vue
|
|
<style scoped>
|
|
/* ✅ Use scoped styles */
|
|
.container {
|
|
padding: 1rem;
|
|
}
|
|
|
|
/* ✅ Use :deep() to style nested components */
|
|
:deep(.p-datatable) {
|
|
border: 1px solid #ccc;
|
|
}
|
|
</style>
|
|
```
|
|
|
|
## Component Naming
|
|
|
|
```vue
|
|
<!-- ✅ PascalCase for components -->
|
|
<CustomerCard />
|
|
<ProductsList />
|
|
<OrderDetails />
|
|
|
|
<!-- ✅ kebab-case in templates also works -->
|
|
<customer-card />
|
|
```
|
|
|
|
## Props
|
|
|
|
```vue
|
|
<script setup>
|
|
// ✅ Always define types and validation
|
|
const props = defineProps({
|
|
customerId: {
|
|
type: Number,
|
|
required: true,
|
|
validator: (value) => value > 0
|
|
},
|
|
showDetails: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
items: {
|
|
type: Array,
|
|
default: () => []
|
|
}
|
|
});
|
|
</script>
|
|
```
|
|
|
|
## Emits
|
|
|
|
```vue
|
|
<script setup>
|
|
// ✅ Define emits with types
|
|
const emit = defineEmits<{
|
|
update: [id: number, data: object];
|
|
delete: [id: number];
|
|
cancel: [];
|
|
}>();
|
|
|
|
// ✅ Or with validation
|
|
const emit = defineEmits({
|
|
update: (id: number, data: object) => {
|
|
if (id > 0 && typeof data === 'object') {
|
|
return true;
|
|
}
|
|
console.warn('Invalid emit arguments');
|
|
return false;
|
|
}
|
|
});
|
|
</script>
|
|
```
|
|
|
|
## Reactive State
|
|
|
|
```vue
|
|
<script setup>
|
|
// ✅ ref for primitives
|
|
const count = ref(0);
|
|
const name = ref('');
|
|
|
|
// ✅ ref for objects (preferred)
|
|
const customer = ref({
|
|
id: null,
|
|
name: '',
|
|
email: ''
|
|
});
|
|
|
|
// ✅ Use reactive only when necessary
|
|
import { reactive } from 'vue';
|
|
const state = reactive({
|
|
items: [],
|
|
loading: false
|
|
});
|
|
</script>
|
|
```
|
|
|
|
## Computed Properties
|
|
|
|
```vue
|
|
<script setup>
|
|
// ✅ Use computed for derived values
|
|
const filteredItems = computed(() => {
|
|
return items.value.filter(item => item.isActive);
|
|
});
|
|
|
|
// ✅ Computed with getter/setter
|
|
const fullName = computed({
|
|
get: () => `${firstName.value} ${lastName.value}`,
|
|
set: (value) => {
|
|
const parts = value.split(' ');
|
|
firstName.value = parts[0];
|
|
lastName.value = parts[1];
|
|
}
|
|
});
|
|
</script>
|
|
```
|
|
|
|
## Event Handlers
|
|
|
|
```vue
|
|
<template>
|
|
<!-- ✅ Use kebab-case for events -->
|
|
<Button @click="handleClick" />
|
|
<Input @input="handleInput" />
|
|
<Form @submit.prevent="handleSubmit" />
|
|
</template>
|
|
|
|
<script setup>
|
|
// ✅ Name handlers with handle* prefix
|
|
function handleClick() {
|
|
// ...
|
|
}
|
|
|
|
function handleInput(event) {
|
|
// ...
|
|
}
|
|
|
|
function handleSubmit() {
|
|
// ...
|
|
}
|
|
</script>
|
|
```
|
|
|
|
## Conditional Rendering
|
|
|
|
```vue
|
|
<template>
|
|
<!-- ✅ Use v-if for conditional rendering -->
|
|
<div v-if="loading">
|
|
<LoadingSpinner />
|
|
</div>
|
|
|
|
<!-- ✅ v-show for frequent toggling -->
|
|
<div v-show="hasItems">
|
|
<ItemsList :items="items" />
|
|
</div>
|
|
|
|
<!-- ✅ v-else for alternatives -->
|
|
<div v-else>
|
|
<EmptyState />
|
|
</div>
|
|
</template>
|
|
```
|
|
|
|
## Lists
|
|
|
|
```vue
|
|
<template>
|
|
<!-- ✅ Always use :key -->
|
|
<div v-for="item in items" :key="item.id">
|
|
{{ item.name }}
|
|
</div>
|
|
|
|
<!-- ✅ For index-based lists -->
|
|
<div v-for="(item, index) in items" :key="`item-${index}`">
|
|
{{ item.name }}
|
|
</div>
|
|
</template>
|
|
```
|
|
|
|
## Form Handling
|
|
|
|
```vue
|
|
<template>
|
|
<form @submit.prevent="handleSubmit">
|
|
<!-- ✅ Use v-model -->
|
|
<InputText v-model="form.name" />
|
|
<Textarea v-model="form.description" />
|
|
|
|
<!-- ✅ For custom components -->
|
|
<CustomInput v-model="form.email" />
|
|
</form>
|
|
</template>
|
|
|
|
<script setup>
|
|
const form = ref({
|
|
name: '',
|
|
description: '',
|
|
email: ''
|
|
});
|
|
|
|
function handleSubmit() {
|
|
// Validation and submit
|
|
}
|
|
</script>
|
|
```
|
|
|
|
## PrimeVue Components
|
|
|
|
```vue
|
|
<template>
|
|
<!-- ✅ Use PrimeVue components in the admin panel -->
|
|
<DataTable
|
|
:value="customers"
|
|
:loading="loading"
|
|
paginator
|
|
:rows="20"
|
|
@page="onPage"
|
|
>
|
|
<Column field="name" header="Name" sortable />
|
|
</DataTable>
|
|
</template>
|
|
```
|
|
|
|
## Styling
|
|
|
|
```vue
|
|
<style scoped>
|
|
/* ✅ Use scoped -->
|
|
.container {
|
|
padding: 1rem;
|
|
}
|
|
|
|
/* ✅ :deep() for nested components */
|
|
:deep(.p-datatable) {
|
|
border: 1px solid #ccc;
|
|
}
|
|
|
|
/* ✅ :slotted() for slots */
|
|
:slotted(.header) {
|
|
font-weight: bold;
|
|
}
|
|
</style>
|
|
```
|
|
|
|
## Composition Functions
|
|
|
|
```vue
|
|
<script setup>
|
|
// ✅ Extract complex logic into composables
|
|
import { useCustomers } from '@/composables/useCustomers.js';
|
|
|
|
const {
|
|
customers,
|
|
loading,
|
|
loadCustomers,
|
|
totalRecords
|
|
} = useCustomers();
|
|
|
|
onMounted(() => {
|
|
loadCustomers();
|
|
});
|
|
</script>
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
```vue
|
|
<script setup>
|
|
import { useToast } from 'primevue';
|
|
|
|
const toast = useToast();
|
|
|
|
async function loadData() {
|
|
try {
|
|
const result = await apiGet('endpoint');
|
|
if (result.success) {
|
|
data.value = result.data;
|
|
} else {
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Error',
|
|
detail: result.error
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error:', error);
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Error',
|
|
detail: 'Failed to load data'
|
|
});
|
|
}
|
|
}
|
|
</script>
|
|
```
|
|
|