Однофайловые компоненты — Примеры

Пример 1 — Ребёнок, меняющий переданный ему prop

Компонент-счётчик увеличивает полученное значение, и отображение у родителя с ним расходится. Скажите, в каком направлении должны идти данные, а затем верните изменение родителю.

<script setup>
const props = defineProps({ count: Number });
</script>

<template>
  <button @click="props.count++">{{ props.count }}</button>
</template>

Решение

<script setup>
defineProps({ count: { type: Number, required: true } });
const emit = defineEmits(["increment"]);
</script>

<template>
  <button @click="emit('increment')">{{ count }}</button>
</template>
<Counter :count="count" @increment="count++" />

Контракт прост: props вниз, события вверх — ребёнок сообщает, что произошло, а родитель решает, каким будет новое значение. Значением при этом владеет родитель, поэтому ребёнок, который пишет в prop, обновляет лишь копию, о которой родитель не знает, и два представления расходятся; ошибка выглядит как проблема отрисовки, а не данных. Объявление типа и обязательности prop образует вторую половину контракта, ведь тогда фреймворк проверяет его во время разработки и документирует в файле. Свобода ребёнка от состояния родителя делает его переиспользуемым и на следующем экране.

Пример 2 — Разметка, отправленная в компонент строкой

Свою шапку и кнопки действий карточка принимает как HTML-строки и отрисовывает их через v-html. Вкладывать компоненты вызывающие не могут, а один из них сломался, когда в содержимом появилась кавычка. Скажите, чего стоит prop-строка, и передайте структуру вместо неё.

<script setup>
defineProps({ title: String, actionsHtml: String });
</script>

<template>
  <section class="card">
    <h2>{{ title }}</h2>
    <div v-html="actionsHtml" />
  </section>
</template>

Решение

<template>
  <section class="card">
    <header><slot name="header" /></header>
    <slot />
    <footer><slot name="actions" /></footer>
  </section>
</template>
<Card>
  <template #header><h2>Счёт №1042</h2></template>
  <p>{{ summary }}</p>
  <template #actions>
    <Button variant="primary">Оплатить</Button>
  </template>
</Card>

Строка разметки разбирается как HTML: экранирование ложится на вызывающего, а всё, что в ней пришло от пользователя, открывает путь для инъекции; вдобавок она не может содержать другой компонент — именно поэтому вызывающим нужна была возможность, которую карточка дать не могла. Слоты передают структуру как структуру, поэтому собственные компоненты вызывающего, обработчики событий и привязки работают так, как написаны, а карточка остаётся безразличной к тому, что попадает в каждую область. Её поверхность стилизации — собственные элементы и классы, которые она выставляет, и ограниченное правило корректно не достаёт до внутренностей ребёнка.