Как устроена связь
Представлению не обязательно знать модель. В примере CounterView получает число и обработчик; его можно отдельно вывести с тестовыми свойствами. reflect задаёт значения по умолчанию из модели и возвращает новый компонент. Объявляйте модель и связанные компоненты вне рендера, чтобы при каждом обновлении не создавать новые узлы и конструкторы.
import {createElement as h} from "@fiberfx/hyperscript";
import {event, store} from "@fiberfx/hypergraph";
import {reflect, createReflect, fromTag} from "@fiberfx/reflect";
const nameChanged = event<string>();
const incremented = event<void>();
const $name = store("Ada").on(nameChanged, (_, name) => name);
const $count = store(0).on(incremented, count => count + 1);
export const Name = reflect({
view: fromTag("input"),
bind: {
"aria-label": "Name",
value: $name,
onInput: (e: Event) =>
nameChanged((e.currentTarget as HTMLInputElement).value),
},
});
const CounterView = (p: {count: number; onIncrement: () => void}) =>
h("button", {type: "button", onClick: p.onIncrement}, String(p.count));
const counter = createReflect(CounterView);
export const Counter = counter({count: $count, onIncrement: () => incremented()});
fromTag("input") превращает HTML-тег в представление и сохраняет известные типы его свойств. createReflect(CounterView) фиксирует представление: одну фабрику можно использовать для нескольких привязок. По смыслу это повторные вызовы reflect с тем же view.
Обработчик нативного поля получает событие, поэтому адаптер извлекает currentTarget.value. У Mercury Checkbox обработчик onChange получает boolean. Reflect не преобразует эти аргументы. Перед заменой контрола прочитайте главу поля и формы. Компоненты нужно выводить внутри Hypergraph Provider приложения; его роль разобрана в главе 3.
Измените пример
Откройте эксперимент с привязкой. Замените черновик имени и нажмите Применить имя, затем дважды увеличьте счётчик. Снова измените черновик, не применяя его. Поле и применённое имя показывают разные значения модели. В эксперименте Mercury Input связан через createReflect, а число выведено через fromTag("output"); короткий пример выше показывает те же операторы с нативным полем и представлением кнопки.
В варианте с кодом выведите h(Counter, {count: 99}). Теперь число для отображения задаёт вызывающая сторона, а обработчик увеличения остаётся связанным с моделью. Это переопределение свойства, а не запись в $count.
Проследите оба направления
Ввод должен менять черновик и поле. Применение непустого имени обновляет отдельное значение применённого имени. Кнопка «Прибавить один» меняет хранилище счётчика и его вывод. Представление получает текущее значение хранилища, а не объект Store.
В варианте count: 99 кнопка продолжит показывать 99, хотя обработчик меняет модель. Уберите переопределение — появится последнее значение из хранилища. Переданный извне обработчик так же заменяет связанный обработчик: Reflect не вызывает оба.
Нарушьте контракт привязки
Сначала очистите черновик и нажмите Применить имя. Должно появиться сообщение об ошибке, а последнее применённое имя должно сохраниться. Отказ обеспечивает фильтр sample в модели, а не Reflect.
Отдельная ошибка типов — путать строку с событием DOM. Проверка типов должна отклонить такую привязку:
reflect({view: fromTag("input"), bind: {
onInput: nameChanged, // event<string> не принимает событие поля ввода
}});
Используйте явный адаптер из примера. Для действия без данных пишите () => incremented(), чтобы не передавать ему событие нажатия. Reflect связывает интерфейс с моделью, но не проверяет данные во время выполнения. Обход проверки типов не делает несовместимый обработчик правильным.
Сбросьте и повторите
Сбросьте эксперимент к начальным значениям. Введите другое имя и один раз увеличьте счётчик. Сброс упражнения восстанавливает его исходное состояние; он не отменяет произвольные побочные эффекты приложения.
В собственной тестовой сцене восстановите значения модели либо создайте новую принадлежащую ей область состояния и размонтируйте старые представления. Не создавайте новый связанный компонент внутри рендера ради очистки поля.
Примените к форме поиска
Свяжите поисковое поле через value: $query и адаптер, передающий строку в queryChanged. Проверку запроса и эффекты поиска оставьте в модели. Один раз выведите обычное представление поля с фиксированными свойствами и убедитесь, что обработчик получает ожидаемый аргумент.
Перед переходом дальше объясните обе границы: событие DOM превращается в данные действия, а значение хранилища — в свойство представления. Следующая глава добавляет вычисляемое свойство без создания узлов графа при рендере.