В этом посте мы рассмотрим, почему не следует вкладывать обработчик событий в цикл Repeater и как его избежать.

На первый взгляд, добавление обработки событий для повторяющихся элементов выглядит просто. Вы просто обрабатываете события повторяющихся элементов внутри методов цикла Repeater, у вас есть все необходимые данные и область действия с помощью селектора $item().

$w("#repeater").onItemReady(($item, itemData, index) => {
  // it look easy
  $item("#repeatedButton").onClick((event) => {
    // we have all we need
    console.log(
      $item("#repeatedContainer"),
      itemData,
      index,
    );
  });
});

Что не так с этим подходом?

Иногда цикл может установить несколько обработчиков событий для одного и того же элемента, когда вы меняете порядок, фильтруете или сортируете повторяющиеся элементы. Каждая итерация цикла может добавлять копию функции обратного вызова в обработчик при повторном запуске. Вы можете не обращать внимания на дважды запущенный код, если просто скрываете или показываете какой-то компонент по событию. Но если вы работаете с API или wixData, то можете получить массу проблем.

Я и моя команда считаем такой подход анти-паттерном и больше его не используем. Этот подход можно использовать для «статических» повторителей, которые заполняются один раз и больше не меняются во время сеанса пользователя.

Но если вы хотите динамически заполнить повторитель или изменить его элементы, вам не следует устанавливать функцию-обработчик внутри цикла. Посмотрим другой способ.

Область действия селектора

В Velo у нас есть два типа функций выбора.

Селекторы глобальной области видимости это $w(). Мы можем использовать его в любой части интерфейса сайта Wix. Если мы используем $w() с повторяющимися элементами, то он изменяет все элементы.

// will change a text in all items
$w("#repeatedText").text = "new";

Объем повторяющихся элементов

Селектор с областью действия повторяющегося элемента можно использовать для выбора определенного экземпляра повторяющегося элемента.

Мы можем получить селектор области повторяющихся элементов несколькими способами.

В цикле выберите селектор в качестве первого аргумента функции обратного вызова для методов .forEachItem(), .forItems() и .onItemReady().

Устаревший способ: селектор в качестве второго аргумента в обработчике событий. Он все еще работает, но вам не нужно его использовать

// 🙅‍♀️ DON'T USE IT 🙅‍♂️
$w("#repeatedButton").onClick((event, $item) => {
  // deprecated selector function (could be removed in the future)
  $item("#repeatedText").text = "new";
});

И с контекстом события. Мы можем получить функцию выбора с помощью $w.at(context).

$w("#repeatedButton").onClick((event) => {
  // accepts an event context and
  // returns repeated items scope selector
  const $item = $w.at(event.context);

  $item("#repeatedText").text = "new";
});

Давайте попробуем воспроизвести, как мы можем использовать event.context вместо методов повторителя цикла.

// global selector `$w()`, it provides handling all repeated items
$w("#repeatedButton").onClick((event) => {
  // get repeated item scope
  const $item = $w.at(event.context);

  // get the ID of the repeated item which fired an event
  const itemId = event.context.itemId;
  // get all repeater's data, it's stored as an array of objects
  const data = $w("#repeater").data;
  // use the array methods to find the current itemData and index
  const itemData = data.find((item) => item._id === itemId);
  const index = data.findIndex((item) => item._id === itemId);

  // we have all we need
  console.log(
    $item('#repeatedContainer'),
    itemData,
    index,
  );
});

Таким образом, у нас есть только один обратный вызов для всех элементов с определенным идентификатором. Используя контекст, мы можем получить область действия активного элемента, его itemData и индекс.

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

Создать хук

Наш хук будет иметь следующие шаги:

Реализация №1

// here will be all logic
const createScope = (getData) => (event) => {
  // TODO: Implement hook
}

#2 инициализировать

// sets callback function, it has to return the repeater data
const useScope = createScope(() => {
  return $w("#repeater").data;
});

#3 с использованием

// using with repeated items
$w("#repeatedButton").onClick((event) => {
  // returns all we need
  const { $item, itemData, index } = useScope(event);
});

Создаем хук с createScope(getData) он будет работать с конкретным повторителем. Аргумент getData — это обратный вызов, он должен возвращать данные повторителя.

createScope вернет новую функцию useScope(event), которая связана с конкретными данными повторителя. useScope(event) принимает объект event и возвращает данные текущей области.

Для реализации функции createScope(getData) создадим публичный файл public/util.js

Мы можем получить данные повторителя с помощью getData(), и у нас есть контекст события. Все, что нам нужно, это просто вернуть селектор Scope и данные элемента в виде объекта. Мы будем использовать синтаксис геттера для возврата itemData, индекса и данных.

public/util.js

export const createScope = (getData) => (event) => {
  const itemId = event.context.itemId;
  const find = (i) => i._id === itemId;
  return {
    $item: $w.at(event.context),
    get itemData() {
      return getData().find(find);
    },
    get index() {
      return getData().findIndex(find);
    },
    get data() {
      return getData();
    },
  };
};

Фрагмент кода на GitHub

Если вы не работаете с геттером/сеттером для доступа к свойствам, вы можете посмотреть здесь, как это работает.

Давайте посмотрим, как мы можем использовать хук на странице со статическими или динамическими обработчиками событий.

Код ГЛАВНОЙ страницы

import { createScope } from "public/util";

const useScope = createScope(() => {
  return $w("#repeater").data;
});

$w.onReady(() => {
  $w("#repeater").onItemReady(($item, itemData) => {
    $item('#repeatedText').text = itemData.title;
  });

  // use a dynamic event handler
  $w("#repeatedButton").onClick((event) => {
    const { $item, itemData, index, data } = useScope(event);
  });
});

// or a static event handler
export function repeatedButton_click(event) {
  const { $item, itemData, index, data } = useScope(event);
}

Теперь мы можем повторно использовать хук селектора со всеми повторителями на всех страницах сайта.

JSDoc

Обновление (12.05.2020)

Редактор кода Velo поддерживает JSDocs, это язык разметки, который используется внутри блочных комментариев JS. JSDocs обеспечивает статическую проверку типов, добавляет автозаполнение и создает хорошую документацию вашего кода. Я рекомендую использовать JSDocs.

Фрагмент кода с JSDocs:

/**
 * Create Repeated Item Scope
 * https://github.com/shoonia/repeater-scope
 *
 * @typedef {{
 *  _id: string;
 *  [key: string]: any;
 * }} ItemData;
 *
 * @typedef {{
 *   $item: $w.$w;
 *   itemData: ItemData;
 *   index: number;
 *   data: ItemData[];
 * }} ScopeData;
 *
 * @param {() => ItemData[]} getData
 * @returns {(event: $w.Event) => ScopeData}
 */
export const createScope = (getData) => (event) => {
  const itemId = event.context.itemId;
  const find = (i) => i._id === itemId;
  return {
    // @ts-ignore
    $item: $w.at(event.context),
    get itemData() {
      return getData().find(find);
    },
    get index() {
      return getData().findIndex(find);
    },
    get data() {
      return getData();
    },
  };
};

Не удаляйте JSDocs из своего кода! В процессе сборки все комментарии будут автоматически удалены из рабочего пакета.

Ресурсы

Сообщения