How-to: API контроллера пользовательского представления
Пользовательское представление, написанное на JavaScript, взаимодействует с формой через объект контроллера — с его помощью представление задаёт текущий объект группы, изменяет значения свойств, запрашивает подсказки и вызывает действия или скрипты на сервере. Есть три вида пользовательских представлений. React-представление получает контроллер своей проекции — члены, описанные ниже, рядом с теми самыми props.data, которые они повторяют. Классическое представление и действие INTERNAL CLIENT получают контроллер формы, на котором есть только то, что есть у любого контроллера формы: форменные глаголы (exec / eval / evalAction / change) и объявление о редактировании (startEditing / stopEditing / isEditing).
Свойства и действия адресуются по их интеграционному имени — имени на форме (либо псевдониму / интеграционному имени NEW / DELETE кнопки), тому же имени, что использует внешний JSON/REST API. Если EXTID не задал собственное имя, это имя свойства на форме, у которого отброшено всё, начиная с (, — поэтому f(a) и f(b), нарисованные в одной группе, оба будут f: интеграционное имя само по себе не уникально.
Это имя, которым представление пользуется везде: по нему ключуется props.data и по нему названы все члены контроллера. Собственные операторы формы — FILTER, ORDER и READ, выписывающий их состояние обратно, — говорят другим именем, SID свойства на форме, причём в обе стороны. Поэтому имя, которое получило представление, не является именем, которое принимает такой оператор: перекодируйте там, где эти две стороны встречаются, — в коде lsf, вызывающем оператор, а не рассчитывая, что какая-то из сторон примет оба.
Как представление получает контроллер
| представление | объявление | точка входа JavaScript | его контроллер — это |
|---|---|---|---|
| React-представление | DESIGN c { custom = 'Name'; } | компонент, принимающий props (data, controller) | props.controller |
| CUSTOM группа объектов | OBJECTS g = Cls CUSTOM 'name' | колбэки render / update | controller.form |
| CUSTOM ячейка свойства | PROPERTIES p CUSTOM 'name' | колбэки render / update | controller.form |
React-представление отрисовывает целый пользовательский контейнер, поэтому controller в его props — это собственный контроллер этого контейнера: методы ниже вызываются прямо на нём.
CUSTOM группа объектов и CUSTOM ячейка свойства отрисовываются классическими колбэками render(element, controller) / update(element, controller, ...) — update дополнительно получает list строк группы либо value ячейки. Контроллер, который они получают, — локальный, привязанный к этой одной группе или ячейке: он добавляет нужные этим представлениям помощники — геттеры значения, текущей строки и стилей и diff / clearDiff для группы объектов, событие правки change для ячейки свойства — описанные в Пользовательские компоненты (объекты) и Пользовательские компоненты (свойства). Локальный контроллер выставляет контроллер формы как controller.form, поэтому форменные методы ниже вызываются через него:
const total = await controller.form.exec('recalc', orderId);
await controller.form.change('customerOrder', orderId, customerId);
Строки, которые получает классическое представление, несут те же key и objects, что и строки React (см. Идентификация строки). Локальный changeProperty группы объектов также делегирует: для свойства, которое не является её собственной колонкой, вызов передаётся форме и разрешается в пределах всей формы, так что представление может изменить свойство, которое не отображает. Сам контроллер формы строк не принимает: его change называет свойство, затем значения его параметров и новое значение.
Четвёртая точка входа — действие INTERNAL CLIENT: привязанная к нему JavaScript-функция получает контроллер формы аргументом после параметров вызова. Контроллер пользовательского редактора значения (CHANGE) также предоставляет контроллер формы в поле form.
Контроллер формы
Контроллер — один на проекцию: props.controller React-представления принадлежит его контейнеру и называет ровно то, что показывает props.data этого контейнера. Объект, который классическое представление и custom-редактор ячейки получают как controller.form, а действие INTERNAL CLIENT — аргументом, — это контроллер ФОРМЫ, и на нём есть только форменные глаголы (exec / eval / evalAction / change): члены ниже говорят, что data.<группа>.<свойство> существует, а объекту, рядом с которым нет данных, говорить это не о чем. Он повторяет форму props.data: его члены — это записи контейнера, причём они создаются один раз, при открытии формы, и каждый лежит по тому же пути, что и его данные: то, что представление читает как data.o.qty, оно меняет через controller.o.qty, а data.total — через controller.total. Каждая группа объектов, часть которой рисует это представление, — член контроллера под своим SID, каждое свойство, ЗНАЧЕНИЕ которого это представление несёт, — член этой группы под своим интеграционным именем, а каждое свойство пустой группы (уровня формы) — член самого контроллера. Форма, в которой одно из этих имён означало бы два свойства одного контейнера, отклоняется при построении. Член и есть адрес: второго, строкового набора глаголов, говорящего то же самое, нет, а опечатка в имени — это несуществующий член, а не строка, которую платформе приходится проверять.
Представление читает и изменяет только части своего контейнера: его контроллер меняет то, что несут его props.data, и ничего больше. Общее у контейнеров — сама форма: её текущие объекты и её сессия, поэтому фильтр или текущий объект, изменённые в одном контейнере, видны в другом так же, как в стандартном клиенте.
Необязательные аргументы — в скобках.
| член | что делает | возвращает |
|---|---|---|
<group>.change(row) | задаёт текущий объект группы — только там, где рисуются строки группы | — |
<group>.orders.change(order) | меняет одну из сортировок группы, не трогая остальные и их приоритет, — только там, где рисуются строки группы. У дерева — <tree>.orders, в узле дерева там, где рисуются его строки, каждое свойство названо <group>.<property> | — |
<group>.orders.change(orders) | массив заявляет их все, его порядок — приоритет | — |
<group>.<property>.change(value[, row]) | задаёт значение — на текущем объекте или на переданной строке | — |
<group>.<property>.exec([row]) | выполняет событие изменения свойства либо действие — на текущем объекте или на переданной строке | — |
<group>.<property>.getValues([object,] value[, mode], ok[, fail][, count]) | ограниченный список подсказок с сервера | — (через ok) |
<property>.change(value) / .exec() / .getValues(...) | то же для свойства пустой группы — без строки | — |
<group>.expand(row) / .collapse(row) / .toggle(row) | раскрывает или сворачивает один узел дерева — узел это строка группы; только там, где рисуются строки дерева | — |
<group>.expandAll() / .collapseAll() | раскрывает все узлы группы и групп под ней — у верхней группы всё дерево — или сворачивает их; там, где рисуются строки дерева | — |
<group>.filters.change(condition) | меняет одно условие фильтра группы, не трогая остальные: первое по его свойству, сохраняя то, чего не указывает, как фильтр по свойству, — там, где стоит FILTERS(<group>). У дерева это <tree>.filters там, где стоит FILTERS(TREE <tree>), и каждое свойство называется <group>.<property> (Фильтры) | — |
<group>.filters.change(conditions) | массив задаёт их все: заменяет весь список условий, условие без значения сравнивает с NULL | — |
<group>.addFilter() / .applyFilters() / .resetFilters() | «добавить», «применить» и «сбросить» управляющих элементов фильтров, как кнопки платформы, — там, где стоит FILTERCONTROLS(<group>). У дерева это <tree>.addFilter() и остальные там, где стоит FILTERCONTROLS(TREE <tree>) | — |
<group>.<filterGroup>.change(index) | выбирает один из фильтров группы фильтров по индексу, null — ни одного, — там, где стоит FILTERGROUP(<filterGroup>) | — |
<group>.filter.<property>.change(value[, {compare, negation, or}]) | задать значение фильтра по свойству — в его условии, первом по свойству, или в добавленном условии, со сравнением, отрицанием и связкой, которые задают параметры; null снимает это условие — там, где рисуется FILTER(<property>) | — |
<group>.getCount() / .getSum(property) | число строк группы и сумма их колонки, только по отмеченным строкам, если пользователь их отметил, — там, где стоит TOOLBARSYSTEM(<group>), что бы ни говорили флаги дизайна show…, — его кнопки | Promise |
<group>.export() / .update() / .manual.change(on) / .filterControlsVisible.change(shown) / .listView.change(name) | выгружает строки, подтягивает придержанные сервером изменения, включает или выключает ручное обновление, показывает или прячет управляющие элементы фильтров платформы, показывает строки в другом виде отображения — там, где стоит TOOLBARSYSTEM(<group>), что бы ни говорили флаги дизайна show… | — |
<tree>.expandCurrent() | раскрывает текущий узел дерева и все узлы под ним — собственный член дерева, в его узле там, где рисуются строки дерева; над штатным деревом — член его тулбара, там, где стоит TOOLBARSYSTEM(TREE <tree>) | — |
<tree>.export() / .filterControlsVisible.change(shown) / .expandTree() | выгружает дерево, показывает или прячет управляющие элементы фильтров дерева платформы — в узле дерева, там, где стоит TOOLBARSYSTEM(TREE <tree>) (его тулбар); над штатным деревом ещё и раскрывает его от группы выделенного узла вниз | — |
batch(fn) | выполнить fn; изменения, которые в ней делают члены, показываются сразу и уходят вместе, одним запросом, когда она вернётся, в порядке вызовов | — |
exec(action, ...params) | выполняет именованное действие | Promise |
eval(script, ...params) | выполняет lsf-скрипт с типизированным run | Promise |
evalAction(script, ...params) | выполняет тело действия (параметры $1, $2, …) | Promise |
change(property, ...keyParams, value) | задаёт глобальное свойство | Promise |
startEditing(element) | объявляет, что пользователь редактирует в element | — |
stopEditing(element) | объявляет, что больше не редактирует — только если объявлен именно element | — |
isEditing(element) | стоит ли это объявление | boolean |
controller.o.note.change('checked'); // текущая строка группы `o`
controller.o.sum.change(100, row); // ... переданная строка
controller.o.edit.exec(row); // действие: выполнить на этой строке
controller.o.change(row); // текущий объект группы
controller.o.orders.change([]); // её сортировки: массив заявляет их все — здесь ни одной
controller.o.filters.change({property: 'name', value: 'an'}); // одно условие — оно ложится в data.o.filters
controller.o.customer.getValues(text, 'objects', ok, fail);
controller.total.change(500); // свойство пустой группы
change и exec члена не спрашивают подтверждения, которого требует дизайн свойства (CONFIRM у действия, askConfirm в дизайне): его спрашивают редактор и кнопка платформы, а вызов представления сразу уходит на сервер. Запись свойства говорит, спрашивать ли и что именно, — askConfirm и askConfirmMessage (опции отображения), — поэтому представление, которому нужен этот вопрос, задаёт его до вызова.
Члены — это то, что это представление может изменить: записи, которые оно несёт, минус то, что рисует ПЛАТФОРМА — у свойства с NOCUSTOM — или с функцией отображения CUSTOM, заданной в FORM, — есть запись (обычная запись колонки, чтобы вид нарисовал заголовок колонки) и нет члена, потому что его значение рисует и правит собственный рендерер, который это представление ставит через <Platform name row/>, а у любого другого дочернего компонента, который рисует платформа, есть только называющая его запись — у действия, которое рисует платформа, панельного или в каждой строке, только запись, без заголовка и изображения — это лицо его кнопки, а запускает его кнопка платформы. Группа становится членом контроллера каждого контейнера, у которого есть data.<group>, — пустым, если контейнер только размещает то, что рисует платформа, — а свойство — членом группы там, где ИМЕННО ЭТОТ контейнер несёт его значение: списочное — там, где рисуются строки, панельное — там, где стоит само свойство. Член есть, пока есть его запись, — с самого начала, скрыта она или нет: SHOWIF, скрывающий свойство, оставляет член на месте, а вызов через него, пока запись hidden, отклоняется с ошибкой о том, что форма сейчас это свойство не показывает. Событие изменения свойства — то самое, которое пользователь запускает правкой ячейки, поэтому у свойства, которого форма не показывает, его некому вызывать. <group>.change(row) есть только там, где рисуются строки группы, — текущий объект выбирается среди них, — как и orders.change(...) — группы, а у дерева в узле дерева, — ведь сортировка это порядок, в котором рисуются эти строки; и строку принимают только там: сам <group>.change и член списочного свойства, который живёт там же, где строки. Член панельного свойства и член пустой группы строку не принимают: они действуют на текущих объектах. Для того, чего представление не видит, у него нет и члена: остальная форма достигается так же, как и всё вне этой поверхности, — через change / exec / eval / evalAction, которые принимают имена и код lsf, а не имена проекции, и которые есть у любого контроллера. Там, где состояние, которое хранит представление, называет свойство, — сортировка, условие пользовательского фильтра, — оно называет его интеграционным именем, а не членом: состояние — это данные, которые пишутся в том же виде, в каком их публикует props.data, и могут называть свойство, у которого члена нет, например колонку, которую рисует платформа. Сортировка — это {property: 'sum', desc: true}, условие — {property: 'sum', compare: '>', value: 100}, и имя у обоих ищется среди свойств группы, где бы они ни были нарисованы: состояние группы принадлежит группе, а не одному представлению, — FILTERS(<group>) может стоять вовсе не рядом с колонкой, а строки не обязаны рисоваться рядом со свойством, по которому они отсортированы (Сортировки, Фильтры). Вызов же называет то, что меняет, членом. У свойства, сгруппированного по колонкам, члена нет нигде — по той же причине, по которой оно не проецируется: его значения адресуются ключом «строка и колонка». (Классический changeProperty, который получает CUSTOM-группа объектов, — другая, более старая поверхность, и она по-прежнему называет всю форму.) Изменяющие члены ничего не возвращают: новое состояние приходит со следующим обновлением формы. Вызывающие сервер методы самого контроллера (exec / eval / evalAction / change) возвращают Promise, как и getCount() и getSum() тулбара, которые спрашивают у сервера число.
Запуск действия через его член — controller.o.edit.exec(row), вне пакета (batch), — тот же запрос, что и нажатие на форме: синхронный, блокирующий ввод до своего завершения и показывающий индикатор ожидания после его задержки, если действие не выведено с NOWAIT (опция свойства syncType). Собственные exec, eval, evalAction и change контроллера идут асинхронными запросами. Поэтому обновление по таймеру через контроллер не делают — см. Живые данные.
batch(fn) выполняет fn и отправляет изменения, сделанные в ней членами, вместе, когда fn вернётся. Вызов в пакете делается в момент вызова, как и вне пакета, — читается, проверяется и сразу показывается, поэтому читает состояние таким, каким его оставили вызовы до него: две сортировки, заданные по одной, или два toggle одного узла продолжают друг друга. Ждут только запросы: когда fn вернётся, изменения членов уходят ОДНИМ запросом: сервер делает их в порядке вызовов — значения свойств, текущий объект группы, её сортировки, её пользовательские фильтры, фильтры, выбранные в её группах фильтров, узлы дерева — и отвечает один раз, перечитывая форму один раз за все. Действие, запущенное через exec() внутри пакета, выполняется в этом же запросе, который, как изменение свойства, не задерживает пользователя. Внутри fn вызывайте только члены: перевод фокуса, изменение DOM или события, посланные компонентам платформы, заставляют платформу отправлять свои запросы — редактор, потеряв фокус, сохраняет значение, — а на них пакет не рассчитан. Пакет несёт только изменения: внутри fn getValues отклоняется — вызывается его fail, причина пишется в консоль, — а серверный вызов контроллера (exec, eval, evalAction, change) — тоже, его Promise отвергается, как и вызовы тулбара, которые обращаются к серверу: getCount и getSum — их Promise отвергается, export (и у дерева), manual.change, update и listView.change — причина пишется в консоль; делайте их до пакета или после. filterControlsVisible.change меняет только клиент, и он делается сразу. addFilter, applyFilters и resetFilters управляющих элементов фильтров входят в пакет, как filters.change, а expandCurrent дерева — и над штатным деревом expandTree его тулбара — раскрывают его узлы и входят в пакет, как expandAll: expandCurrent раскрывает строку, которую раньше в том же пакете сделал текущей change(row). Редактор условия, которое filterControlsVisible.change начинает на пустой панели платформы, а addFilter — на её панели, открывается и получает фокус, когда пакет отправлен. props.data внутри fn не меняется: то, что меняет пакет, видно, когда он закончится.
Ошибка бросается там, где сделан вызов, а исключение внутри fn — отклонённый вызов или собственное исключение представления — заканчивает пакет: сделанное до исключения отправляется, как ушло бы и вне пакета, а исключение доходит до представления. Пакет, начатый внутри другого пакета, — часть его и отправляется, когда закончится внешний, — сделанное им до исключения тоже. Внутри fn форму меняют члены этого представления: член другого представления, вызванный там, уйдёт с этим пакетом.
fn выполняется сразу: функция, возвращающая Promise, — async, чьи вызовы после await пришлись бы на время, когда пакет уже закончился, — отклоняется, а сделанное ею до await отправляется; сначала дождитесь, потом начинайте пакет.
Пакет — и не транзакция: его изменения делаются в сессии формы, как правки пользователя, а на сервере — одно за другим: упавшее останавливает остальные, а сделанные до него остаются в сессии; в базу они попадают, когда сессия применяется.
Когда изменение падает, значения свойств, которые менял пакет, показываются такими, какими были до пакета, а те, что сервер успел сделать до упавшего изменения, приходят с его следующим ответом. Остальное, что изменил пакет, — текущий объект группы, её сортировки и пользовательские фильтры, фильтры, выбранные в её группах фильтров, узлы дерева, которые он открыл или закрыл, — остаётся таким, каким его показал пакет, сделал это сервер или нет: их сервер присылает, только когда меняет их сам, поэтому после падения задайте их заново там, где это важно.
Имя, не являющееся идентификатором JavaScript, адресуется через скобки: свойство с EXTID 'unit price' — это controller.o['unit price'].change(v). SID группы — всегда идентификатор: группа из нескольких объектов, чей SID был бы их именами через точку, отклоняется при построении формы, пока её не назовут (OBJECTS pair = (d = X, t = Y)).
Имя, которое перекрыло бы член самой поверхности, отклоняется при построении формы — как и собственные зарезервированные имена проекции: SID группы, имя дерева там, где представление рисует его строки или другую часть дерева, или свойство пустой группы, совпадающее с методом контроллера (exec, eval, evalAction, change, startEditing, stopEditing, isEditing, batch) либо друг с другом (контроллер — одно пространство имён на свою проекцию, поэтому группа и свойство пустой группы, которые проецирует один и тот же контейнер, не могут оба быть total, — а два разных контейнера могут), а также свойство группы, названное как один из её собственных членов в том же контейнере. Лечится переименованием или EXTID. Сортировки дерева, его текущая строка и собственный expandCurrent, там, где рисуются его строки, а также его пользовательские фильтры, тулбар и управляющие элементы фильтров лежат в узле дерева, который не несёт свойств.
Те же две группы различаются ещё по двум признакам — нужна ли проверка доступа и как в них адресуется объект:
| группа | методы | проверка доступа | как передаётся объект |
|---|---|---|---|
| правка формы | <group>.change / <property>.change / <property>.exec | нет | строка-цель — строкой данных (row), дескриптором (row.objects) или своим ключом; объект-значение (FK) — своим id |
| вызов сервера | exec / eval / evalAction / change самого контроллера | @@api / права администратора / CUSTOMS формы | объект — своим id |
Пользовательское представление обычно читает состояние из props.data и меняет его методами правки формы — в том числе выполняя действие, добавленное на форму, через controller.<action>.exec(). Методы вызова сервера (exec / eval / evalAction / change) — это запасной путь, нужный только для того, что формой не выражается: произвольного вычисления на сервере, записи в глобальное свойство, создания объекта.
Правка формы идёт обычным каналом правки и не требует проверки доступа. Вызовы сервера её проходят (см. Вызов сервера). Редактируемая строка адресуется дескриптором, а любой другой объект — значение FK или параметр действия — передаётся своим числовым id (объект lsFusion из JS передать нельзя).
Изменение текущего объекта и значений свойств
controller.<group>.change(row) задаёт текущий объект группы. Он есть только в контейнере, который рисует строки группы, а row — строка данных этой группы, непосредственный дескриптор objects либо ключ, который дала строке проекция (см. правила идентификации ниже).
controller.<group>.<property>.change(value) изменяет свойство для текущего объекта группы, а .change(value, row) — для переданной строки. Аргументы идут в этом порядке, и ничего по ним не угадывается: сначала значение — оно обязательно, null его очищает, — затем строка, необязательная. .exec() выполняет событие изменения свойства — то, которое пользователь запускает правкой ячейки, — на текущем объекте, а для действия выполняет само действие, и .exec(row) делает это на переданной строке. Действию нечего задавать в значение, поэтому .change на нём отклоняется: вызывайте exec. Строку принимает только член списочного свойства, который живёт там, где рисуются строки группы. Член панельного свойства и член пустой группы её не принимают и действуют на текущих объектах. Выбор изменяется выбранным — формы вызова описаны там.
function orderView(props) {
const controller = props.controller;
return (
<div>
<button onClick={() => controller.o.note.change('checked')}>Отметить</button>
{props.data.o.list.map(row =>
<div key={row.key} onClick={() => controller.o.change(row)}>
{row.number.value}
</div>)}
</div>
);
}
Значение, переданное в change, попадает в обработку изменения свойства, только если та запрашивает значение у пользователя — обработка по умолчанию либо обработка с запросом значения: переданное значение подставляется вместо ввода пользователя. null — такое же значение, как любое другое. У действия нет значения, которое можно задать: change его члена отклоняется, а запускается оно через exec, который значения не передаёт, — controller.o.edit.exec(row), — поэтому действию, запрашивающему значение у пользователя, через его член значение не передать.
У вызова члена нет ячейки, в которую пользователь ввёл бы значение, поэтому он значения у пользователя не запрашивает. Там, где платформа запрашивает значение у пользователя до того, как выполнить обработку изменения, — например, при асинхронном вводе обработки по умолчанию, — exec члена отклоняется: значение свойства представление передаёт само, через change(value), а значение действия не передать ни через какой член. Значение, которое обработка запрашивает только по ходу выполнения, — скажем, ввод в одной из её ветвей, — отменяется, как отменяет ввод пользователь, и обработка продолжается так же, как в этом случае; почему, говорит консоль браузера.
batch(fn) отправляет несколько изменений вместе. Каждое — собственный вызов члена, как и вне пакета:
controller.batch(() => {
controller.o.note.change('checked');
controller.o.qty.change(5, row);
});
Групповой объект встроенного примитивного класса — например, навигатор DATE — переводится к значению записью значения самого объекта (controller.<g>.VALUE.change(d) с настоящим JS-Date), вызовом controller.<g>.change(row) со строкой из props.data.<g>.list (она несёт дескриптор objects) либо, если группа отфильтрована по data-свойству, изменением этого свойства-фильтра. Значение-дата проходит через преобразование, ожидающее JS-Date (каст без проверки): аргумент не-Date — строка из date-инпута, timestamp — бросает getFullYear is not a function, поэтому передавайте настоящий Date, например new Date(year, month - 1, day).
.change свойства ведёт себя одинаково в пакете и вне его, а формат зависит от того, что задаётся в значение:
| значение | как передаётся |
|---|---|
| примитив | напрямую: число — для числовых типов, строка, JS-Date — для DATE / TIME / DATETIME / ZDATETIME, логическое значение, null — для очистки |
интервал, COLOR | строкой в том виде, в каком значение приходит в представление: <from>.<to> с границами в миллисекундах, #rrggbb. Значение другой формы отклоняется |
JSON | JS-объект или массив (сериализуется в JSON) |
| объект (значение FK) | id целевого объекта — row.key его строки (для группы с одним объектом это уже его числовой id) либо id-свойство на форме (например, LONG(obj)), но не дескриптор |
Передача дескриптора (otherRow.objects) в качестве значения FK молча выставляет NULL, без ошибки. Дескриптор нужен только для аргумента-строки вызова члена и для <group>.change. Чтобы задать FK, передавайте id целевого объекта.
В обратную сторону формат тот же: значение объектного свойства приезжает в строку данных этим же числовым id, поэтому его можно сравнить с row.key целевой строки (в группе с одним объектом) или передать обратно значением FK без преобразований.
Как и правка пользователя в штатной таблице, изменение выполняется в сессии изменений формы: представление сразу видит новое значение в props.data, а в базу данных оно попадает при применении сессии — кнопкой «Сохранить» либо действием с оператором APPLY. Если редактируемое свойство помечено на форме опцией APPLY, его стандартная обработка изменения применяет правку сразу (по умолчанию — фиксируя всю сессию формы), поэтому отдельное сохранение после .change не нужно. Подробности описаны у опции. Для простой правки из представления — перенос, изменение размера, правка значения по месту — это предпочтительнее отдельного серверного действия. Серверное действие (controller.exec) остаётся для того, что правкой свойства не выражается: создания объекта (NEW), многошаговой логики, открытия формы.
// перенос объекта к другому родителю и правка примитива вместе:
// значение FK — id целевого объекта (row.key целевой строки), значение-примитив — напрямую
controller.batch(() => {
controller.i.parent.change(targetColumn.key, item);
controller.i.value.change(5, item);
});
Запрос значений
getValues запрашивает у сервера ограниченный список подсказок для свойства — список подсказок, а не полный SELECT DISTINCT. Результат передаётся в обработчик ok в виде { data: [ { displayString, rawString, objects }, ... ], more }. more равно true у ответа, который сервер дал, ещё не убедившись, что несохранённые изменения формы на него не влияют: тогда ok вызывается ещё раз, с окончательным списком.
controller.<group>.<property>.getValues([object,] value[, mode], ok, fail[, count]);
-
value— вводимая строка запроса для сопоставления. -
object— необязательная строка, которую принимает только член списочного свойства (строка данных, непосредственный дескриптор или ключ строки — ключ только вместе сmode, поскольку перед однимvalueстроковый аргумент читается как запросvalue), ограничивающая запрос этой строкой. Опускается для текущего объекта. -
mode— одно из:modeрезультат item.objects'objects'(по умолчанию)подходящие OBJECTSсвойства — выбор объектанепосредственный дескриптор objectsэтого объекта'values'различные значения свойства null(используйтеdisplayString/rawString)'change'подсказки времени правки свойства зависит от свойства 'change'отражает то, как свойство редактируется — пользовательский списокINPUT, ограничениеnotNullили пользовательское действие изменения, — а не уже имеющиеся различные значения. Для свойства, которое в этом контексте изменить нельзя (например, только для чтения или вычисляемое без действия изменения), таких подсказок нет и вызываетсяfail, тогда как'values'всё равно вернёт его различные значения. -
ok(result)/fail()— обработчики успеха и ошибки. -
count— увеличивает число запрашиваемых элементов, для постраничной загрузки.
item.objects из результата режима 'objects' можно передать напрямую обратно как строку, чтобы действовать с выбранным объектом: в .change группы, чтобы сделать его текущим, или как row в .change(value, row) или .exec(row) списочного свойства. Значением он не бывает никогда: дескриптор, переданный как значение FK, молча выставляет NULL (см. выше), а FK задаётся id объекта:
controller.c.customer.getValues(text, 'objects',
result => result.data.forEach(item => console.log(item.displayString)),
() => console.log('failed'));
// выбор первой подсказки как покупателя группы
controller.c.customer.getValues(text, 'objects', result => {
const item = result.data[0];
if (item) controller.c.change(item.objects);
}, () => {});
Вызов сервера
exec, eval, evalAction и change выполняются на сервере и возвращают Promise. Они проходят ту же авторизационную проверку и преобразуют результат в значение JS так же, как серверные вызовы классического представления, — описание проверки доступа, привязки параметров и таблицу преобразования результата в JS см. в разделе Вызов сервера. Сквозной пример этих вызовов из представления CUSTOM — в статье How-to: Пользовательские компоненты (вызовы сервера).
exec(action, ...params)— выполняет именованное действие. Разрешается его значениемRETURN.action— это имя действия: записьCUSTOMSформы — по имени, которое у неё там, или действие проекта — по его имени, с пространством имён и сигнатурой в квадратных скобках, когда они нужны, чтобы выбрать одно:runReport[Order]среди перегруженных. Но неsaveFilters(), как написали бы в скрипте: на такое имя ответ — «Action was not found».eval(script, ...params)— выполняет lsf-скрипт, определяющий собственное действиеrun(типизированные параметры).evalAction(script, ...params)— выполняет тело действия, обёрнутое в действиеrun, с параметрами$1,$2, ….change(property, ...keyParams, value)— изменяет глобальное свойство. Последний аргумент — значение, предшествующие — ключи. Если значением свойства является объект, значением передаётся его id, и платформа присваивает объект с этим id — диалог выбора объекта открывается только при интерактивном редактировании.
Параметры передаются как обычные значения JS (число, строка, логическое значение, Date, объект/массив для JSON-параметра). Объект lsFusion передаётся своим числовым id. Если параметр действия типизирован классом, платформа сама находит объект этого класса по id — ручной поиск не нужен. Дескриптор строки в параметре объектом не является: для типизированного классом параметра вызов завершается ошибкой, поэтому передавайте id.
Этот id — row.key строки этого объекта (см. Идентификация строки) либо свойство формы со значением-id. Передавать его нужно числом: ключ, взятый из data.<группа>.byKey или data.<группа>.keys, — строка, потому что ключи объекта в JS всегда строки, а строка в типизированном классом параметре отклоняется с ошибкой Number is required.
const total = await controller.exec('recalc', orderId);
const doubled = await controller.eval('run(INTEGER a) { RETURN a * 2; }', 21); // 42
await controller.change('archived', orderId, true);
Вызов после закрытия формы отклоняется с ошибкой Form is closed — он никогда не зависает, — поэтому await на закрытой форме попадает в ветку catch.
Объявление редактирования
Пока форма редактирует, привязка, у которой область editing задана по умолчанию, не срабатывает для события, пришедшего изнутри редактируемого элемента: ENTER не переводит фокус на следующий компонент, ESCAPE не закрывает модальное окно, а горячая клавиша, заданная через CHANGEKEY, не выполняется. Привязка, объявленная с editing=all, срабатывает в любом случае.
Редакторы, созданные ею самой, форма знает. Поля, которое представление нарисовало себе само, она не знает, поэтому представление объявляет его:
startEditing(element)— с этого момента пользователь редактирует вelement;stopEditing(element)— пользователь больше не редактирует вelement, причём объявление снимается, только если объявлен именноelement;isEditing(element)— объявлен лиelement.
Объявление на форме одно: объявление другого элемента заменяет предыдущее. Пока открыт собственный редактор формы, он имеет приоритет над объявлением, и объявление читается снова, когда это редактирование заканчивается.
Пока объявление стоит, перечисленные привязки не срабатывают для событий из element и из всего, что внутри него. Представление, которое ничего не объявило, сохраняет прежнее поведение: ENTER переводит фокус на следующий компонент, ESCAPE закрывает окно — из нарисованного представлением поля так же, как откуда угодно ещё.
function Note({ controller }) {
return <textarea onFocus={e => controller.startEditing(e.target)}
onBlur={e => controller.stopEditing(e.target)}/>;
}
В этом примере объявление делается по фокусу. Собственный редактор свойства начинает редактирование с первого набранного символа, а не по фокусу.
Объявление на обёртке покрывает все поля внутри неё — в том числе вложенный дочерний компонент, который рисует платформа и редактор, который платформа рисует в нём. Если это не нужно, объявляйте более узкий элемент.
Снимать объявление — обязанность представления, и не только по blur: снимайте его, когда элемент удаляется или представление размонтируется. Забытое объявление не даёт этим привязкам срабатывать для событий внутри этого элемента, пока форма открыта.
Объявление не начинает редактирование свойства: коммитить и отменять нечего, и ENTER ничего не сохраняет. Чтобы из представления редактировалось свойство, поставьте его через <Platform name/> — платформа нарисует там свой редактор, и объявлять ничего не нужно.
Отдельная клавиша, нужная представлению независимо от состояния редактирования, — его собственная горячая клавиша — это другое: остановите событие в обработчике (stopPropagation), и форма его не получит.
Контроллер навигатора
Действие INTERNAL CLIENT, помещённое в NAVIGATOR, тоже получает контроллер, но навигаторный: формы здесь нет, поэтому нет и перечисленных выше методов формы. Есть четыре серверных вызова — exec, eval, evalAction, change, — которые выполняются в собственной сессии навигатора, новой на каждый вызов и никогда не применяемой, поэтому сделанное здесь change не сохраняется. И есть activate:
activate(canonicalName[, event])— делает то же, что нажатие на этот элемент навигатора: выбирает папку либо выполняет действие, открывая его форму так же оптимистично.canonicalName— каноническое имя элемента.event— событие нажатия, React-овское или браузерное, и при вызове из кода, у которого события нет, его можно не передавать.
Активация несуществующего элемента или скрытого SHOWIF — собственным или папки над ним — бросает исключение. О завершении выполненного действия не сообщается, как не сообщает о нём и нажатие.
window.openMonthlyReport = function (controller) {
controller.activate('Reporting.monthly');
};
openMonthlyReport 'Отчёт за месяц' () { INTERNAL CLIENT 'openMonthlyReport'; }
NAVIGATOR {
NEW openMonthlyReport WINDOW system PARENT;
}
Идентификация строки
Метод, адресующий строку, принимает одно из:
- объект строки данных, полученный представлением (из
propsReact либо из спискаupdateклассического представления); - копию такой строки через spread или
Object.assign— перечислимый дескрипторobjectsкопируется вместе с ней, поэтому копия разрешается в тот же объект; - непосредственный дескриптор
objects—row.objectsлибоitem.objectsиз результатаgetValuesв режиме'objects'; - ключ, который дала строке проекция, —
row.keyлибо ключ изdata.<group>.keys/byKey. Строку принимают только там, где рисуются строки её группы, поэтому у этого представления всегда естьbyKey, в котором ищется ключ: он ищется среди строк группы самого члена, а ключ, не называющий там ни одной строки, завершается ошибкой. Именно это позволяет представлению вернуть то, что оно получило:byKey— JS-объект, иObject.keys()по нему даёт строки, так что прошедший через него ключ больше не несёт того типа, которым был записанrow.key. У классического представления такого индекса нет, поэтому егоchangePropertyпринимает строку или её дескриптор. Там, где строку и значение различают по одному аргументу, — запросvalueуgetValues, — голый ключ читается как значение, поэтому передавайте туда саму строку.
При этом для группы с одним объектом пользовательского класса значение row.key численно равно id этого объекта, поэтому его можно передать как id целевого объекта в серверный вызов или в значение FK — отдельное свойство для собственного id строки создавать не нужно.
Если явный аргумент-строка не является ни одним из них, платформа не подставляет молча текущую строку: вызов завершается ошибкой с указанием того, что ожидалось, вместо действия над другой строкой. Не принимается и строка другой группы: controller.<group>.change(row) и change(value, row) / exec(row) члена списочного свойства — оба говорят, чья строка имеется в виду, — группы члена, — а строка другой группы несёт объекты только той группы, поэтому остальные были бы взяты из текущих и вызов пришёлся бы на строку этой группы, которую никто не называл. Внутри дерева строка группы НИЖЕ в этом смысле не является строкой другой группы: её ключ — весь путь до неё, поэтому она называет ровно одну строку каждой группы выше, и там она принимается — ровно так же, как её принимает <group>.change.