Skip to content

События Component SFC на практике

Это руководство описывает весь реализованный Event-механизм Component SFC: стандартные Events built-in тегов, локальные реакции в template, публичные definePorts.emits, forwarding, Table Events, sandbox и визуальный редактор.

Важно различать три уровня:

УровеньГде объявляетсяЧто получает родитель
Локальная реакция@click="action(...)" на тегеНичего, пока Event явно не опубликован
Публичный EventdefinePorts.emitsТипизированный Event текущего Component SFC
ForwardingdefinePorts.forwardВыбранные Events дочернего компонента

Event является multicast-контрактом: реакция не заменяет публикацию и не превращает Event в Action. Исключение — явный local modifier .stop, который останавливает дальнейший маршрут конкретного occurrence.

Для условных pointer/keyboard комбинаций и последовательных reactions используйте универсальную аннотацию :on.

Только собственный контракт

ts
const ports = definePorts({
  emits: {
    detailsOpened: event<{ id: string }>(),
  },
})

Такое событие можно испустить из разрешённой sandbox-реакции через ports.emits.detailsOpened({ id }) или через API runtime-host-а у системного producer-а.

Если from отсутствует, Event принадлежит текущему Component SFC. Поле action необязательно и не является default-значением: это текущая реакция, которая хранится непосредственно в Source.

Событие дочерней Table

vue
<script setup lang="ts">
const ports = definePorts({
  emits: {
    rowActivated: event<TableRowActivatedEvent>({
      from: { ref: 'table', event: 'rowActivated' },
    }),
  },
})
</script>

<template>
  <Table ref="table" :rows="rows" row-key="id" />
</template>

Direct Action

ts
rowActivated: event<TableRowActivatedEvent>({
  from: { ref: 'table', event: 'rowActivated' },
  action: {
    identity: 'flight.open-details',
    input: {
      id: event('rowId'),
      row: event('row'),
    },
  },
})

event() передаёт весь payload. event('row.id') читает вложенное значение, а now() возвращает текущий ISO 8601 UTC timestamp. Полный каталог intrinsic и Table payload-полей приведён в справочнике event() и now().

Direct Action запускается через Endge.actions.execute со scope текущего runtime-компонента и target исходного элемента. Ошибка Action попадает в diagnostics, но не отменяет публикацию Event другим подписчикам.

Стандартные Events built-in тегов

Общий Event registry подключён к следующим renderer-neutral тегам:

Text, DateTime, Number, Icon, Badge, Dot, Box, Flex, Grid, Divider, Input, Textarea, Checkbox, Select, Table и Cell.

Cell связывает обработчик с renderer-owned поверхностью конкретной ячейки и передаёт её row/column locals. Структурные теги Column, ColumnMenu, RowMenu, MenuItem и MenuSeparator не являются самостоятельными rendered event targets. Вложенный пользовательский Component публикует Events из собственного typed manifest.

Указатель и мышь

IdentityНазвание в UIPayload
clickНажатиеComponentSFCPointerEventPayload
dblclickДвойное нажатиеComponentSFCPointerEventPayload
contextmenuКонтекстное менюComponentSFCPointerEventPayload
mousedownНажатие кнопки мышиComponentSFCPointerEventPayload
mouseupОтпускание кнопки мышиComponentSFCPointerEventPayload
mousemoveДвижение мышиComponentSFCPointerEventPayload
mouseoverНаведение мышиComponentSFCPointerEventPayload
mouseoutУход указателя мышиComponentSFCPointerEventPayload
mouseenterВход указателя мышиComponentSFCPointerEventPayload
mouseleaveВыход указателя мышиComponentSFCPointerEventPayload
pointerdownНажатие указателяComponentSFCPointerEventPayload
pointerupОтпускание указателяComponentSFCPointerEventPayload
pointermoveДвижение указателяComponentSFCPointerEventPayload
pointeroverНаведение указателяComponentSFCPointerEventPayload
pointeroutУход указателяComponentSFCPointerEventPayload
pointerenterВход указателяComponentSFCPointerEventPayload
pointerleaveВыход указателяComponentSFCPointerEventPayload

Клавиатура, фокус и viewport

IdentityНазвание в UIPayload
keydownНажатие клавишиComponentSFCKeyboardEventPayload
keyupОтпускание клавишиComponentSFCKeyboardEventPayload
focusПолучение фокусаComponentSFCInteractionEventPayload
blurПотеря фокусаComponentSFCInteractionEventPayload
focusinВход фокусаComponentSFCInteractionEventPayload
focusoutВыход фокусаComponentSFCInteractionEventPayload
wheelКолесо мышиpointer payload с deltaX, deltaY
scrollПрокруткаComponentSFCInteractionEventPayload

Drag-and-drop

IdentityНазвание в UIPayload
dragstartНачало перетаскиванияComponentSFCPointerEventPayload
dragПеретаскиваниеComponentSFCPointerEventPayload
dragendЗавершение перетаскиванияComponentSFCPointerEventPayload
dragenterВход перетаскиванияComponentSFCPointerEventPayload
dragleaveВыход перетаскиванияComponentSFCPointerEventPayload
dragoverПеретаскивание над областьюComponentSFCPointerEventPayload
dropСбросComponentSFCPointerEventPayload

Нативный DataTransfer наружу не передаётся.

Form Events

Для Input, Textarea, Checkbox и Select дополнительно доступны input и change с ComponentSFCInputEventPayload.

IdentityНазвание в UI
inputВвод
changeИзменение

Payload-контракты

Все interaction payload содержат:

ts
interface ComponentSFCInteractionEventPayload {
  type: string
  modifiers: {
    alt: boolean
    ctrl: boolean
    meta: boolean
    shift: boolean
  }
}

Pointer payload дополнительно содержит x, y, button, buttons и pointerType. Keyboard payload содержит key, code и repeat. Form payload содержит value и, когда применимо, checked.

DOM Event и DOM element не входят в публичный payload. Поэтому один контракт одинаково работает в native/RevoGrid, shadcn/TanStack и будущих renderer-ах.

Локальная реакция на событие тега

Если событие не является частью публичного API компонента, Action можно прикрепить прямо к тегу:

vue
<Text
  ref="title"
  @click.stop.prevent="action({
    identity: 'audit.track-click',
    input: { pointer: event() },
  })"
>
  Открыть
</Text>

В этом случае click не появляется в definePorts.emits автоматически. Локальный Action выполняется, .prevent отменяет browser default, а .stop останавливает DOM bubbling и дальнейший public forwarding occurrence.

Если Event нужен и локально, и снаружи, добавьте публичный контракт и уберите .stop:

ts
const ports = definePorts({
  emits: {
    titleClicked: event<ComponentSFCPointerEventPayload>({
      from: { ref: 'title', event: 'click' },
    }),
  },
})

Modifiers и bubbling

Следующие browser modifiers применяются к intrinsic Events built-in тегов:

ModifierСемантика
.stopВыполняет локальную реакцию, затем останавливает DOM bubbling и Event routing выше текущей boundary
.preventВызывает renderer-specific preventDefault(), если Event допускает отмену
.selfЗапускает локальную реакцию только когда target совпадает с текущим rendered-узлом
.onceЗапускает локальную реакцию один раз в пределах текущей mount-boundary
.captureПодключает listener в capture phase
.passiveПодключает passive listener

.passive.prevent является ошибкой compiler-а. После срабатывания .once публичный Event продолжает публиковаться при следующих occurrences; один раз выполняется именно локальная реакция.

Template handler обязан содержать безопасную reaction-форму action({...}), query({...}), ports.require.<action-or-query>({...}) или typescript({...}). Required port сохраняет provider заменяемым на уровне конкретного Component SFC call. Вызов произвольной функции из <script setup> и передача сырого DOM Event не поддерживаются.

Локальный TypeScript

Когда direct Action недостаточен, sandbox можно использовать непосредственно на теге:

vue
<Text
  @contextmenu.prevent="typescript({
    inputs: { event: event() },
    compute({ event }, api) {
      return api.action('audit.write', {
        operation: 'title-context-menu',
        x: event.x,
        y: event.y,
      })
    },
  })"
>
  Рейс
</Text>

Это тот же sandbox, который используется в definePorts.emits.action.

TypeScript в песочнице

ts
const ports = definePorts({
  emits: {
    detailsOpened: event<{ id: string }>(),
    rowContextMenuRequested: event<TableRowContextMenuRequestedEvent>({
      from: { ref: 'table', event: 'rowContextMenuRequested' },
      action: typescript({
        inputs: { event: event() },
        compute({ event }, api) {
          return [
            api.action('audit.write', { rowId: event.rowId }),
            ports.emits.detailsOpened({ id: event.rowId }),
          ]
        },
      }),
    }),
  },
})

Этот вариант следует использовать только когда direct Action недостаточен. Песочница не предоставляет DOM, сеть, imports, timers или прямой Endge.

Sandbox возвращает один effect или массив effects. Разрешены только api.action(...) и вызовы объявленных ports.emits.*. Runtime защищён от циклического повторного emit и ограничивает цепочку 32 Event-hops/effects.

Контекстное меню Table

Полное руководство по декларативному RowMenu находится в разделе «Контекстное меню строк».

У Table есть два разных контракта:

  • contextmenu — стандартный Event всей rendered-области Table;
  • rowContextMenuRequested — смысловой Table Event конкретной строки с rowId, row, columnKey и anchor: { x, y }.

Для открытия меню строки обычно нужен rowContextMenuRequested. Его можно обработать локально:

vue
<Table
  ref="table"
  :rows="rows"
  @rowContextMenuRequested="action({
    identity: 'flight.open-context-menu',
    input: {
      rowId: event('rowId'),
      anchor: event('anchor'),
    },
  })"
/>

Или опубликовать родителю через emits.from/forward, как любой другой Event.

Выборочный forwarding

ts
const ports = definePorts({
  forward: {
    from: 'table',
    ports: {
      emits: ['rowActivated', 'selectionChanged'],
    },
  },
})

Forwarding всех событий

ts
const ports = definePorts({
  forward: {
    from: 'table',
    ports: {
      emits: '*',
    },
  },
})

Явное одноимённое поле в emits может дополнить forwarded Event реакцией, если origin совпадает. Остальные коллизии остаются ошибками compiler-а.

forward: '*' пробрасывает все поддерживаемые направления дочерних bindings: require, provides и emits. Чтобы не расширять публичный API компонента случайно, для interaction Events обычно предпочтителен явный selector.

Runtime routing

Для каждого смонтированного Component SFC создаётся собственная Event boundary. Occurrence проходит следующий путь:

  1. Renderer нормализует native occurrence в typed payload.
  2. Boundary запускает локальную @event reaction.
  3. Если нет .stop, boundary находит публичный emits по origin.
  4. Публичная reaction из emits.action выполняется независимо от публикации.
  5. forward передаёт occurrence родительской boundary.
  6. На корневой boundary подписчики получают Event через runtime-host.

Автоматической отправки каждого occurrence в глобальный Endge.events нет. Runtime-host предоставляет emitEventPort(name, payload) для системных producer-ов и onEventPort(name, listener) для подписчиков.

Визуальный редактор

В редакторе Table раздел «События» показывает встроенные события и позволяет выбрать Action, режим «Без реакции» или TypeScript. Раздел «Порты» управляет require, provides, emits и forward.

Изменения сразу патчат Source. Ручное изменение Source сразу восстанавливается в UI. Если definePorts содержит spread, computed keys или другую сложную конструкцию, редактор переходит в source-only и не переписывает блок.

В виртуальном корне «События» built-in Events показываются русскими названиями. При удержании Option/Alt Domain Widget временно показывает неизменяемую identity, например Text.click или Table.rowActivated. Тип payload доступен в данных Event и редакторе портов, но не дублируется Badge справа от названия.

Корень строится только на frontend и содержит:

text
События
├── Built-in
│   ├── Text
│   ├── Input
│   └── Table
└── Local
    └── <Component SFC>
        ├── Собственные
        └── Проброшенные

Узлы каталога виртуальные: они не создают backend entity и не сохраняются в Payload. Local Event открывает Source владельца. Для пользовательского Event без отдельного русского display name UI использует его Source-имя; identity при этом всегда остаётся неизменной.

Диагностика compiler-а

Compiler сообщает ошибку, если:

  • from.ref не найден или не является однозначным literal ref;
  • дочерний тег не публикует указанное имя Event;
  • payload публичного Event несовместим с payload источника;
  • template использует неизвестный Event или modifier;
  • @event не содержит action({...})/typescript({...});
  • объединены .passive и .prevent;
  • forwarding создаёт collision публичных port identities;
  • sandbox обращается к DOM, network, imports, timers или неразрешённому effect.