Однофайловые компоненты — Примеры
Пример 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: экранирование ложится на вызывающего, а всё, что в ней пришло от пользователя, открывает путь для инъекции; вдобавок она не может содержать другой компонент — именно поэтому вызывающим нужна была возможность, которую карточка дать не могла. Слоты передают структуру как структуру, поэтому собственные компоненты вызывающего, обработчики событий и привязки работают так, как написаны, а карточка остаётся безразличной к тому, что попадает в каждую область. Её поверхность стилизации — собственные элементы и классы, которые она выставляет, и ограниченное правило корректно не достаёт до внутренностей ребёнка.