How-to: Пользовательские представления формы на React
Контейнер DESIGN может отрисовываться компонентом React вместо стандартной раскладки. Компонент получает проекцию состояния формы и сам рисует всё поддерево контейнера.
Это возможность только веб-клиента. Десктоп-клиент десериализует контейнер и отрисовывает его обычное (не React) поддерево, поэтому дизайн остаётся рабочим в обоих клиентах.
Выбор компонента
В DESIGN атрибуту custom контейнера присваивается имя компонента строковым литералом, соответствующим [A-Z][A-Za-z0-9_$]* (простой идентификатор, начинающийся с заглавной буквы):
FORM orders 'Orders'
OBJECTS o = Order
PROPERTIES(o) READONLY number, date, sum
;
DESIGN orders {
BOX(o) {
custom = 'OrderBoard';
}
}
Форма значения выбирает отрисовщик: строковый литерал, соответствующий [A-Z][A-Za-z0-9_$]*, задаёт компонент React, а пустая строка '', строка с HTML-шаблоном или свойство дают классический (не React) пользовательский контейнер, описанный в How-to: Пользовательские компоненты (объекты). Здесь объект o отрисовывается компонентом React OrderBoard вместо стандартной таблицы.
Компонент
OrderBoard — именованный экспорт из модуля .jsx в каталоге src/main/web; как этот модуль компилируется и регистрируется, описано в How-to: Пользовательские клиентские JS-модули.
Компонент — это обычная функция, получающая props.data и props.controller:
export function OrderBoard(props) {
const orders = props.data.o.list;
return <div className="order-board">{orders.length} orders</div>;
}
props.data — проекция формы. Для каждого SID группового объекта формы g значение props.data.<g> равно { list, byKey }, где list — массив строк в порядке отображения, а byKey сопоставляет строке её строковый ключ с тем же объектом строки. Каждая строка содержит:
| Поле | Значение |
|---|---|
key | Стабильный публичный идентификатор строки — используется как ключ React |
isCurrent | Является ли строка текущей (выделенной) |
<integrationSID> | Значение каждого свойства формы по интеграционному SID этого свойства |
objects | Непрозрачный дескриптор строки, по которому контроллер адресует строку |
key, isCurrent и objects — зарезервированные имена полей строки, поэтому интеграционный SID свойства формы не должен совпадать ни с одним из них.
function Row(props) {
const r = props.row;
return (
<div className={r.isCurrent ? "order order-current" : "order"}>
<span>{r.number}</span>
<span>{r.sum}</span>
</div>
);
}
Отрисовка строк
Для отрисовки строк группы с экономией перерисовки по строкам используется window.lsfusion.List. Это глобальная переменная времени выполнения, поэтому, чтобы записать её как JSX-тег, сначала привяжите её к локальному имени с заглавной буквы; без псевдонима вызывайте через React.createElement:
const List = window.lsfusion.List;
// ...
<List data={props.data.o} component={Row} />
// либо, без псевдонима:
React.createElement(window.lsfusion.List, { data: props.data.o, component: Row })
Отрисовывайте List как компонент — через JSX или React.createElement, — а не вызывая его как обычную функцию: каждая строка отрисовывается компонентом, использующим хуки, поэтому он работает только когда его монтирует React.
List задаёт каждой строке ключ row.key, передаёт строку в компонент как props.row и отрисовывает каждую строку через мемоизированную обёртку, привязанную к этой строке, поэтому при изменении перерисовываются только реально изменившиеся строки. Простая альтернатива отображает список напрямую:
props.data.o.list.map(r => <Row key={r.key} row={r} />)
Зачем нужна экономия перерисовки по строкам. При изменении любой одной строки props.data.<g>.list пересоздаётся как новая ссылка на массив, но проекция сохраняет прежнюю ссылку на объект для каждой не изменившейся строки (структурное разделение) — новый объект строки получают только строки, содержимое которых изменилось. Простой list.map(r => <Row row={r}/>) пересоздаёт элемент Row для каждой записи при изменении любой одной строки, поэтому React перерисовывает их все. Ключ React key этого не меняет: он позволяет React сохранять идентичность элемента строки, её DOM и состояние компонента между перерисовками, но не отменяет саму перерисовку. Компилятор React (reactCompiler=true) тоже не помогает — он мемоизирует .map как одну реактивную область по ссылке на массив, которая только что изменилась, и не оборачивает дочерние строки в React.memo, поэтому все строки всё равно перерисовываются.
window.lsfusion.List добавляет недостающую отмену перерисовки по строкам: каждая строка отрисовывается через стабильную мемоизированную обёртку, которая следит за этой одной строкой, поэтому изменение значения перерисовывает только изменившуюся строку. Сам список не обходится заново при изменении значения строки — только при добавлении, удалении или перестановке строк, — поэтому стоимость обновления не растёт с числом строк. Чтобы получить отмену перерисовки по строкам вручную без window.lsfusion.List, объявите мемоизированный компонент строки один раз на уровне модуля и задавайте ключ row.key:
const MRow = React.memo(Row);
// ...
props.data.o.list.map(r => <MRow key={r.key} row={r} />)
React.memo(Row), создаваемый внутри компонента при каждой перерисовке, каждый раз является новым типом компонента, что сводит мемоизацию на нет и перерисовывает все строки.
Доступен более простой вариант window.lsfusion.List — <List simple/> или, глобально, window.lsfusion.listSimple. Он вместо этого отображает список и мемоизирует компонент строки, полагаясь на то, что проекция повторно использует ссылку на не изменившуюся строку; компонент строки получает те же props.
Интерактивность
Чтобы читать и изменять состояние формы из компонента — выбирать строку, изменять свойство, вызывать действия — используется props.controller. Его методы описаны в How-to: Контроллер пользовательского представления.