События

Для того чтобы веб-приложение было интерактивным, оно нуждается в реагировании на пользовательские события. Это делается путем регистрации функций обратного вызова в шаблоне JSX. Обработчики событий регистрируются с помощью атрибута on{EventName}$. Например, атрибут onClick$ используется для прослушивания событий click.

<button onClick$={() => alert('CLICKED!')}>click me!</button>

Встроенный обработчик

В приведённом ниже примере атрибут onClick$ элемента <button> используется, чтобы сообщить Qwik, что функция обратного вызова () => store.count++ должна выполняться каждый раз, когда на элементе <button> происходит событие click.

import { component$, useSignal } from '@builder.io/qwik';
 
export default component$(() => {
  const count = useSignal(0);
 
  return (
    <button onClick$={() => count.value++}>
      Прибавить {count.value}
    </button>
  );
});

Вы также можете использовать bind:propertyName, чтобы организовать двустороннюю привязку между сигналом и элементом ввода.

Обратите внимание, что onClick$ оканчивается на $. Это подсказка как оптимизатору так и разработчику о том, что в этом месте происходит специальное преобразование. Наличие суффикса $ подразумевает здесь границу ленивой загрузки. Код, связанный с обработкой события click, не будет загружаться до тех пор, пока пользователь не запустит это событие. Код, связанный с обработчиком события click, не будет загружен в JavaScript VM до тех пор, пока пользователь не вызовет событие click, однако он будет сразу загружен в кэш браузера, чтобы не вызывать задержек при первом взаимодействии.

В реальных приложениях слушатель может обращаться к сложному коду. Создавая границу ленивой загрузки (с помощью $), Qwik может тришейкнуть весь код, расположенный в обработчике, и отложить его загрузку до тех пор, пока пользователь не нажмёт на кнопку.

Повторное использование обработчиков событий

Если мы хотим повторно использовать один и тот же обработчик событий для нескольких элементов или событий, нам нужно обернуть обработчик событий в функцию $(), экспортируемую @builder.io/qwik, чтобы преобразовать его в QRL.

import { component$, useSignal, $ } from '@builder.io/qwik';
 
export default component$(() => {
  const count = useSignal(0);
  const increment = $(() => count.value++);
  return (
    <>
      <button onClick$={increment}>Прибавить</button>
      <p>Счёт: {count.value}</p>
    </>
  );
});

Примечание: Если вы извлекаете обработчик события, то вы должны вручную обернуть обработчик события в $(...handler...), чтобы его можно было лениво присоединить.

Несколько обработчиков событий

Если мы хотим зарегистрировать несколько обработчиков событий для одного и того же события, мы можем передать массив обработчиков событий в атрибут on{EventName}$.

import { component$, useSignal, $ } from '@builder.io/qwik';
 
export default component$(() => {
  const count = useSignal(0);
  const print = $((ev) => console.log('CLICKED!', ev));
  const increment = $(() => count.value++);
 
  // Кнопка при нажатии выведет в консоль "CLICKED!", увеличит счётчик и отправит событие в Google Analytics.
  return (
    <button
      onClick$={[print, increment, $(() => {
        ga.send('click', { label: 'increment' });
      })]}
    >
      Счёт: {count.value}
    </button>
  );
});

Объект события

Первым аргументом обработчика события является объект Event. Этот объект содержит информацию о событии, которое вызвало обработчик. Например, объект Event для события click содержит информацию о положении мыши и элементе, на котором был клик мыши. Вы можете ознакомиться с документами MDN, чтобы узнать больше подробностей о каждом событии DOM.

import { component$, useSignal } from '@builder.io/qwik';
 
export default component$(() => {
  const position = useSignal<{ x: number; y: number }>();
  return (
    <div
      onClick$={(event) => (position.value = { x: event.x, y: event.y })}
      style="height: 100vh"
    >
      <p>
        Позиция клика: ({position.value?.x}, {position.value?.y})
      </p>
    </div>
  );
});

Асинхронные события

Из-за асинхронной природы Qwik выполнение обработчика события может быть отложено, поскольку реализация еще не загружена в виртуальную машину JavaScript. Из-за асинхронной природы обработки событий в Qwik следующие API для объекта Event не будут работать:

  • event.preventDefault().
  • event.currentTarget

Prevent default

Поскольку обработка событий является асинхронной, вы не можете использовать event.preventDefault(). Для решения этой проблемы Qwik вводит декларативный способ предотвращения с помощью атрибута preventdefault:{eventName}.

import { component$ } from '@builder.io/qwik';
 
export default component$(() => {
  return (
    <a
      href="/docs"
      preventdefault:click // Это предотвратит поведение по умолчанию для события клика.
      onClick$={() => {
        // event.PreventDefault() здесь не сработает, потому что обработчик управляется асинхронно.
        alert('Do something else to simulate navigation...');
      }}
    >
      Перейти на страницу документации
    </a>
  );
});

Event target

Поскольку обработка событий является асинхронной, вы не можете использовать event.currentTarget. Для решения этой проблемы обработчики Qwik предоставляют currentTarget в качестве второго аргумента.

import { component$, useSignal } from '@builder.io/qwik';
 
export default component$(() => {
  const currentElm = useSignal<HTMLElement|null>(null);
  const targetElm = useSignal<HTMLElement|null>(null);
 
  return (
    <section onClick$={(event, currentTarget) => {
      currentElm.value = currentTarget;
      targetElm.value = event.target as HTMLElement;
    }}>
      Click on any text <code>target</code> and <code>currentElm</code> of the event.
      <hr/>
      <p>Hello <b>World</b>!</p>
      <hr/>
      <ul>
        <li>currentElm: {currentElm.value?.tagName}</li>
        <li>target: {targetElm.value?.tagName}</li>
      </ul>
    </section>
  );
});

Примечание: currentTarget в DOM указывает на элемент, к которому был прикреплён слушатель события. В приведённом примере это всегда будет элемент <SECTION>.

Синхронная обработка событий

В некоторых случаях необходимо обрабатывать событие традиционным способом, поскольку некоторые API должны использоваться синхронно. Например, событие dragstart должно обрабатываться синхронно, поэтому его нельзя совместить с ленивым выполнением кода Qwik.

Для этого мы можем использовать useVisibleTask, чтобы программно добавить слушателя событий, используя DOM API напрямую.

import { component$, useSignal, useVisibleTask$ } from '@builder.io/qwik';
 
export default component$(() => {
  const draggableRef = useSignal<HTMLElement>();
  const dragStatus = useSignal('');
 
  useVisibleTask$(({ cleanup }) => {
    if (draggableRef.value) {
      // Использование DOM API для добавления слушателя событий.
      const dragstart = () => (dragStatus.value = 'dragstart');
      const dragend = () => (dragStatus.value = 'dragend');
 
      draggableRef.value!.addEventListener('dragstart', dragstart);
      draggableRef.value!.addEventListener('dragend', dragend);
      cleanup(() => {
        draggableRef.value!.removeEventListener('dragstart', dragstart);
        draggableRef.value!.removeEventListener('dragend', dragend);
      });
    }
  });
 
  return (
    <div>
      <div draggable ref={draggableRef}>
        Потяни меня!
      </div>
      <p>{dragStatus.value}</p>
    </div>
  );
});

ПРИМЕЧАНИЕ Использование VisibleTask для прослушивания событий является анти-паттерном в Qwik, поскольку это приводит к немедленному выполнению кода в браузере, что нарушает принцип возобновляемости. Используйте его только тогда, когда у вас нет другого выбора. В большинстве случаев для прослушивания событий следует использовать JSX: <div onClick$={...}> или useOn(...) методы событий, если вам нужно слушать события программно.

Пользовательские свойства событий

При создании компонентов часто бывает полезно передавать пользовательские свойства событий, которые выглядят как обработчики событий (хотя они являются не событиями DOM, а функциями обратного вызова). Границы компонентов в Qwik должны быть сериализуемыми, чтобы оптимизатор мог разделить их на отдельные чанки, а функции не являются сериализуемыми, если они не преобразованы в QRL.

Например, для прослушивания событий тройного щелчка, чего html не может делать по умолчанию, потребуется создать пользовательский реквизит события onTripleClick$.

import { component$, Slot, useStore } from '@builder.io/qwik';
 
export default component$(() => {
  return (
    <Button onTripleClick$={() => alert('ТРОЙНОЙ КЛИК!')}>
      Трижды кликни меня!
    </Button>
  );
});
 
type ButtonProps = {
  onTripleClick$: QRL<() => void>;
};
 
export const Button = component$<ButtonProps>(({ onTripleClick$ }) => {
  const state = useStore({
    clicks: 0,
    lastClickTime: 0,
  });
  return (
    <button
      onClick$={() => {
        // логика тройного клика
        const now = Date.now();
        const timeBetweenClicks = now - state.lastClickTime;
        state.lastClickTime = now;
        if (timeBetweenClicks > 500) {
          state.clicks = 0;
        }
        state.clicks++;
        if (state.clicks === 3) {
          // handle custom event
          onTripleClick$();
          state.clicks = 0;
        }
      }}
    >
      <Slot />
    </button>
  );
});

Обратите внимание на использование типа QRL в onTripleClick$: QRL<() => void>;. Это как обернуть функцию в $(), но на уровне типов. Если бы у вас было const greet = $(() => "hi 👋"); и вы навели курсор на 'greet', вы бы увидели, что 'greet' имеет тип QRL<() => "hi 👋">.

События объектов window и document

До сих пор мы обсуждали, как прослушивать события, идущие от элементов разметки. Есть события (например, scroll и mousemove), которые требуют прослушивания на объекте window или document. По этой причине Qwik позволяет использовать префиксы document:on и window:on.

Назначение window:on/document:on - зарегистрировать событие компонента в текущем местоположении DOM, но заставить его получать события от window/document. В этом есть два преимущества:

  1. События могут быть зарегистрированы декларативно в вашем JSX;
  2. События автоматически очищаются при уничтожении компонента (не требуют явной очистки).

Хуки useOn[window|document]

  • useOn(): слушает события на корневом элементе текущего компонента;
  • useOnWindow(): слушает события на объекте window;
  • useOnDocument(): слушает события на объекте document.

Хук useOn[window|document]() программно добавит слушателя событий на основе DOM на уровне компонента. Это бывает полезно при создании своих собственных хуков или если вы не знаете имя события на момент компиляции.

import { $, component$, useOnDocument, useStore } from '@builder.io/qwik';
 
// Предполагаем многоразовое использование, не имеющее доступа к JSX,
// но нам нужно зарегистрировать обработчики событий.
function useMousePosition() {
  const position = useStore({ x: 0, y: 0 });
  useOnDocument(
    'mousemove',
    $((event) => {
      const { x, y } = event as MouseEvent;
      position.x = x;
      position.y = y;
    })
  );
  return position;
}
 
export default component$(() => {
  const pos = useMousePosition();
  return (
    <div>
      Позиция мыши: ({pos.x}, {pos.y})
    </div>
  );
});

Участники

Спасибо всем участникам, которые помогли сделать эту документацию лучше!

  • voluntadpear
  • the-r3aper7
  • RATIU5
  • manucorporat
  • nnelgxorz
  • adamdbradley
  • hamatoyogi
  • fleish80
  • cunzaizhuyi
  • Pika-Pool
  • mhevery
  • AnthonyPAlicea
  • amatiash
  • harishkrishnan24
  • fabian-hiller
  • igorbabko
  • mrhoodz
  • julianobrasil
  • maiieul
  • Balastrong