Files
interview-demo-code/.cursor/rules/vue.md
Nikita Kiselev 393bbb286b
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
Squashed commit message
2026-03-11 23:00:59 +03:00

5.8 KiB

Vue.js 3 Rules

Component Structure

Template

<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

<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

<style scoped>
/* ✅ Use scoped styles */
.container {
  padding: 1rem;
}

/* ✅ Use :deep() to style nested components */
:deep(.p-datatable) {
  border: 1px solid #ccc;
}
</style>

Component Naming

<!--  PascalCase for components -->
<CustomerCard />
<ProductsList />
<OrderDetails />

<!--  kebab-case in templates also works -->
<customer-card />

Props

<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

<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

<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

<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

<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

<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

<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

<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

<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

<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

<script setup>
// ✅ Extract complex logic into composables
import { useCustomers } from '@/composables/useCustomers.js';

const {
  customers,
  loading,
  loadCustomers,
  totalRecords
} = useCustomers();

onMounted(() => {
  loadCustomers();
});
</script>

Error Handling

<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>