Array-поле
Array-поле — createArrayField(...) в схеме — это упорядоченный список повторяющихся элементов (см. Array-поля). После formToEffector оно живёт в model.fields.<name> не как leaf-узел линзы, а как коллекционная линза: проекция по элементам-инстансам с ключом по стабильному id. Форма остаётся единственным источником истины; линза читает, сужает, диспатчит в элементы и удаляет их — но не переизобретает список.
Всё здесь предполагает ассоциацию и скоуп в текущем запуске — см. Обзор → Ассоциация скоупов. Мост не создаёт скрытый скоуп; триггерьте внутри него.
import { createArrayField, createField, createForm } from "@virentia/forms";
import { formToEffector } from "@virentia/forms-effector";
const form = createForm({
schema: {
phones: createArrayField<string>([""]),
},
});
const model = formToEffector(form);Действия элемента, агрегированные
Под коллекционной линзой элемент отдаёт ту же поверхность, что и обычное поле: его стор-ы — watchable (.clock()), методы — targetable (.target()). Вызванные прямо на коллекционной линзе, эти действия агрегируются по всем текущим совпавшим инстансам.
// Срабатывает, когда обновляется value *любого* телефона.
model.fields.phones.state.clock(); // Event<string>
// Диспатчит в *каждый* инстанс телефона.
model.fields.phones.change.target(); // EventCallable<string>Для композитного элемента (shape или группа на строку) сначала навигируйте в форму элемента, затем выберите leaf-юнит:
// model.fields.rows === createArrayField(..., createItem: shape строки)
model.fields.rows.address.city.state.clock();
model.fields.rows.address.city.change.target();Терминалы .clock() / .target() появляются только после навигации к leaf; в корне коллекционной линзы агрегировать ещё нечего — там используйте операторы выбора.
Сужение до инстансов
Агрегация по всему списку нужна редко. Операторы выбора повторяют линзу @effector-kit/models и каждый возвращает суженную линзу той же формы, поэтому от результата продолжают цепочку действий элемента.
// По стабильному id.
model.fields.phones.ids("0").state.clock();
model.fields.phones.ids("0", "2").change.target();
// По предикату над данными каждого инстанса (+ его id).
model.fields.phones.where((p) => !p.value).change.target();
// Ровно до одного инстанса.
model.fields.phones.first().state.clock();
model.fields.phones.last().state.clock();
model.fields.phones.single().state.clock(); // совпадает, только когда он одинwhere получает данные инстанса плюс его id. Для примитивного элемента данные — это { value, id }; для композитного — объект-значение элемента плюс id:
model.fields.phones.where((p) => p.value.startsWith("+7")); // { value, id }
model.fields.rows.where((r) => r.quantity > 0); // { title, quantity, id }Диспатч в совпавшие элементы
После сужения .target() диспатчит payload в каждый совпавший инстанс — sample в него или вызов императивно внутри скоупа.
import { createEvent, sample } from "effector";
// Очистить одну конкретную строку.
sample({ clock: cleared, target: model.fields.phones.ids("0").change.target() });
// Перевалидировать каждый пустой телефон.
model.fields.phones.where((p) => !p.value).validate.target();Удаление совпавших элементов
.delete() возвращает EventCallable<void>, который удаляет все текущие совпавшие инстансы. Это единственная структурная операция, которой владеет линза.
import { sample } from "effector";
// Удалить каждый пустой телефон при сабмите.
const removeEmpty = model.fields.phones.where((p) => !p.value).delete();
sample({ clock: pruneClicked, target: removeEmpty });Посмотреть, что суженная линза совпадает сейчас, можно через .getSource() — совпавшие инстансы с ключом по стабильному id:
model.fields.phones.where((p) => !p.value).getSource();
// { "1": { value: "", id: "1" }, "3": { value: "", id: "3" } }Стабильные id
Каждому элементу присваивается стабильный id при первом появлении, в порядке создания — "0", "1", "2", … Id привязан к инстансу элемента, а не к его позиции, поэтому переживает move, swap и remove: линза, суженная через .ids("0"), продолжает указывать на тот же элемент после переупорядочивания списка, а новые элементы получают свежие id, а не переиспользуют освободившиеся.
// phones = ["a", "b"] -> id "0", "1"
await scoped(vScope, () => form.fields.phones.swap(0, 1));
// phones = ["b", "a"] -> id всё те же "0" (=> "a"), "1" (=> "b")
model.fields.phones.ids("0").state.clock(); // всё ещё отслеживает "a"Добавление элементов
Коллекционная линза покрывает чтение, сужение, диспатч и удаление. Структурные операции добавления (push / unshift / insert) на линзе отсутствуют — выполняйте их на самом array-поле Virentia, внутри ассоциированного скоупа, или отдельным эффектом-мостом:
import { scoped } from "@virentia/core";
// Напрямую, в скоупе Virentia текущего запуска.
await scoped(vScope, () => form.fields.phones.push(""));После добавления новый элемент попадает в линзу автоматически — его стор-ы появляются в агрегатных .clock() и он подхватывается where / getSource.
Дальше
- Shape-поле — коллекционная линза над объектом с динамическими ключами вместо упорядоченного списка.
- Рецепты — submit, серверные ошибки и зеркалирование в одном месте.