← Курс/Динамические классы и стили#211 из 257+20 XP

Динамические классы и стили

Привязка классов через объект

Директива :class принимает объект, где ключи — имена классов, а значения — условия их применения (приводятся к булевому):

<template>
  <div :class="{
    active: isActive,
    'text-danger': hasError,
    'font-bold': isImportant
  }">
    Содержимое
  </div>
</template>

<script setup>
const isActive = ref(true)
const hasError = ref(false)
const isImportant = ref(true)
// Результат: class="active font-bold"
</script>

Комбинирование статических и динамических классов

Статический class и динамический :class можно использовать вместе — Vue объединяет их:

<!-- Всегда будет класс "btn", плюс динамические -->
<button class="btn" :class="{ 'btn-primary': isPrimary, 'btn-sm': isSmall }">
  Кнопка
</button>
<!-- При isPrimary=true, isSmall=false: class="btn btn-primary" -->

Привязка через массив

:class принимает и массив — полезно когда классов много или они берутся из разных переменных:

<template>
  <div :class="[baseClass, statusClass, isActive ? 'active' : '']">
  </div>
</template>

<script setup>
const baseClass = ref('card')
const statusClass = computed(() => isError.value ? 'card-error' : 'card-success')
const isActive = ref(true)
</script>

В массиве можно смешивать строки и объекты:

<div :class="['btn', { 'btn-primary': isPrimary }, extraClass]">

Привязка стилей через объект

Директива :style принимает объект CSS-свойств. Имена в camelCase или в кавычках kebab-case:

<template>
  <div :style="{
    color: textColor,
    fontSize: fontSize + 'px',
    backgroundColor: bgColor,
    'border-radius': borderRadius + 'px'
  }">
    Стилизованный блок
  </div>
</template>

<script setup>
const textColor = ref('#333333')
const fontSize = ref(16)
const bgColor = ref('#ffffff')
const borderRadius = ref(8)
</script>

Массив стилей

:style также принимает массив объектов — они объединяются:

<div :style="[baseStyles, themeStyles, conditionalStyles]">
const baseStyles = reactive({ padding: '16px', margin: '8px' })
const themeStyles = computed(() => ({
  color: isDark.value ? '#fff' : '#333',
  background: isDark.value ? '#1a1a2e' : '#fff',
}))

Автопрефиксы

Vue автоматически добавляет вендорные префиксы к CSS-свойствам в :style, если они нужны в текущем браузере (например, transform -> -webkit-transform). Вам об этом думать не нужно.

Практический пример: кнопка с состояниями

<template>
  <button
    class="btn"
    :class="{
      'btn-loading': isLoading,
      'btn-success': isSuccess,
      'btn-error': isError,
      'btn-disabled': isDisabled
    }"
    :style="{
      opacity: isDisabled ? 0.5 : 1,
      cursor: isDisabled ? 'not-allowed' : 'pointer',
      minWidth: minWidth + 'px'
    }"
    :disabled="isDisabled || isLoading"
    @click="handleClick"
  >
    {{ isLoading ? 'Загрузка...' : label }}
  </button>
</template>

Такой подход позволяет управлять всем внешним видом кнопки через реактивные переменные, без прямого взаимодействия с DOM.

Примеры

Функции для вычисления динамических классов и стилей — логика аналогичная :class и :style в Vue

// Реализуем логику :class и :style Vue на чистом JS

// --- :class с объектом ---
function resolveClassObject(classObj) {
  return Object.entries(classObj)
    .filter(([, condition]) => Boolean(condition))
    .map(([className]) => className)
    .join(' ')
}

// --- :class с массивом (может содержать строки и объекты) ---
function resolveClassArray(classArr) {
  return classArr
    .map(item => {
      if (typeof item === 'string') return item
      if (typeof item === 'object' && item !== null) return resolveClassObject(item)
      return ''
    })
    .filter(Boolean)
    .join(' ')
}

// --- Объединение статических и динамических классов ---
function mergeClasses(staticClass, dynamicClass) {
  const parts = []
  if (staticClass) parts.push(staticClass)

  if (typeof dynamicClass === 'string') {
    parts.push(dynamicClass)
  } else if (Array.isArray(dynamicClass)) {
    parts.push(resolveClassArray(dynamicClass))
  } else if (typeof dynamicClass === 'object') {
    parts.push(resolveClassObject(dynamicClass))
  }

  return parts.filter(Boolean).join(' ')
}

// --- :style с объектом ---
function resolveStyleObject(styleObj) {
  return Object.entries(styleObj)
    .map(([prop, value]) => {
      // camelCase -> kebab-case
      const cssProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase()
      return `${cssProp}: ${value}`
    })
    .join('; ')
}

// --- :style с массивом объектов (объединяем) ---
function resolveStyleArray(styleArr) {
  const merged = Object.assign({}, ...styleArr)
  return resolveStyleObject(merged)
}

// === Демонстрация ===
console.log('=== :class с объектом ===')
console.log(resolveClassObject({
  btn: true,
  'btn-primary': true,
  'btn-disabled': false,
  active: true,
}))
// btn btn-primary active

console.log('\n=== :class с массивом ===')
console.log(resolveClassArray([
  'card',
  { 'card-active': true, 'card-error': false },
  'card-lg',
]))
// card card-active card-lg

console.log('\n=== Статический + динамический класс ===')
const isPrimary = true
const isLoading = false
console.log(mergeClasses('btn', {
  'btn-primary': isPrimary,
  'btn-loading': isLoading,
}))
// btn btn-primary

console.log('\n=== :style с объектом (camelCase -> kebab-case) ===')
console.log(resolveStyleObject({
  color: '#333',
  fontSize: '16px',
  backgroundColor: '#f5f5f5',
  borderRadius: '8px',
}))

console.log('\n=== :style с массивом (слияние) ===')
const baseStyles = { padding: '16px', color: '#333' }
const themeStyles = { color: '#fff', background: '#1a1a2e' }
// Заметь: color из themeStyles перезапишет color из baseStyles
console.log(resolveStyleArray([baseStyles, themeStyles]))