Исполняемые спецификации на flang React: форма и таблица из одной модели
0%

React: форма и таблица из одной модели

React: форма и таблица из одной модели

Код в этой главе записан в прежней поверхности языка — со словами категория, объект, утилита. Сегодняшний компилятор её слова читает, но программой такой файл не считает: файл, где есть только утилиты, flang check отклоняет. Разбор задачи в главе верен; синтаксис переносится по таблице из главы «Старые модели».

Форма не должна второй раз объявлять, какие поля есть у покупки и какие из них обязательны — это уже сказано в объекте модели, которую проверяет сервер из прошлой главы. В этой главе — как из FtsDocument получить схему формы, как выбрать контрол по типу поля и почему предпросчёт в браузере не отменяет пересчёт на сервере.

Минимальный пример

категория «Продажи»

  объект Покупка
    сумма является деньгами
    «постоянный клиент» является признаком

  утилита «Рассчитать скидку»
    принимает Покупка
    возвращает деньги
    начинает с 0

    правило «Большая покупка»
      если сумма не меньше 10000
      и сумма не больше 100000
      то добавить 10 процентов от поля сумма

    правило «Постоянный клиент»
      если «постоянный клиент» равен да
      и сумма больше 0
      и сумма не больше 100000
      то добавить 5 процентов от поля сумма

    правило «Очень крупная покупка»
      если сумма больше 100000
      то добавить 15000

    свойство «Скидка ограничена»
      результат не больше 15000

    пример «Обычная покупка»
      дано сумма равна 5000
      дано «постоянный клиент» равен нет
      ожидается результат равен 0

    пример «Постоянный клиент на пять тысяч»
      дано сумма равна 5000
      дано «постоянный клиент» равен да
      ожидается результат равен 250

    пример «Большая покупка постоянного клиента»
      дано сумма равна 20000
      дано «постоянный клиент» равен да
      ожидается результат равен 3000

    пример «Покупка на потолок скидки»
      дано сумма равна 100000
      дано «постоянный клиент» равен да
      ожидается результат равен 15000

    пример «Очень крупная покупка»
      дано сумма равна 200000
      дано «постоянный клиент» равен нет
      ожидается результат равен 15000

Тот же объект Покупка, что и в прошлых двух главах. DiscountForm.jsx из examples/spec/react-form рисует форму именно по этой структуре, а server.mjs из examples/spec/discount-api считает по ней же скидку.

Что делает компилятор

compile в браузере — тот же парсер, что и на сервере, но без модулей, завязанных на Node.js crypto: сертификация и верификация остаются серверной компетенцией, а compile и executeUtility работают одинаково в обеих средах. Из результата document.structures.find(...) можно достать список полей объекта с именем и каноническим типом (Строка, Число, Деньги, Дата, Признак, либо тот же тип с | undefined для необязательного поля). Дальше это обычные данные, а не что-то специфичное для React — их можно превратить в схему для формы, в колонки таблицы или в JSON Schema.

ftsFormSchema: схема формы из объекта

examples/spec/form-schema/schema.mjs переводит структуру модели в нейтральное описание, не привязанное к фреймворку:

const CONTROLS = [
  [/^Признак$|^Boolean$/u, 'checkbox'],
  [/^Деньги$|^Money$/u, 'money'],
  [/^Число$|^Number$/u, 'number'],
  [/^Дата$|^Date$/u, 'date'],
];

export function controlFor(type) {
  const found = CONTROLS.find(([pattern]) => pattern.test(type));
  return found ? found[1] : 'text';
}

export function ftsFormSchema(document, objectName) {
  const structure = document.structures.find((item) => item.name === objectName);
  if (!structure) {
    const known = document.structures.map((item) => item.name).join(', ');
    throw new Error(`в модели нет объекта «${objectName}» (есть: ${known})`);
  }
  return {
    id: `${document.category}.${structure.name}`,
    title: structure.name,
    fields: structure.fields.map((field) => {
      const optional = field.type.includes('undefined');
      const type = field.type.replace(/\s*\|\s*undefined\s*/gu, '');
      return { name: field.name, label: field.name, type, control: controlFor(type), required: !optional };
    }),
  };
}

Незнакомый тип осознанно становится текстовым полем, а не ошибкой: модель предметной области развивается быстрее, чем таблица соответствий контролов, и новый тип не должен ронять форму. Запрос несуществующего объекта, наоборот, бросает исключение и называет доступные имена — опечатку в названии объекта лучше увидеть сразу, а не получить пустую форму.

иногда является → необязательное поле

объект Покупка
  сумма является деньгами
  «постоянный клиент» является признаком
  комментарий иногда является строкой

Компилятор отражает иногда является ... как union с undefined в каноническом типе поля. Это единственный источник необязательности: schema.mjs проверяет field.type.includes('undefined') и выставляет required: false. В форме это означает один конкретный эффект — атрибут required у <input> не ставится, а не более сложную логику показа поля. Тест из курса проверяет это напрямую:

test('«иногда является» делает поле необязательным', () => {
  const document = compile('категория «Заявки»\n\n  объект Заявка\n    имя является строкой\n    комментарий иногда является строкой\n');
  const schema = ftsFormSchema(document, 'Заявка');
  assert.deepEqual(
    schema.fields.map((field) => [field.name, field.required, field.type]),
    [['имя', true, 'Строка'], ['комментарий', false, 'Строка']],
  );
});

Разбор DiscountForm.jsx

Компонент из examples/spec/react-form/DiscountForm.jsx не знает заранее, что у покупки есть сумма и признак постоянного клиента — оба поля приходят из ftsFormSchema:

const document = useMemo(() => compile(source), [source]);
const schema = useMemo(() => ftsFormSchema(document, objectName), [document, objectName]);
const [value, setValue] = useState(() =>
  Object.fromEntries(schema.fields.map((field) => [field.name, field.control === 'checkbox' ? false : ''])),
);

compile и построение схемы завёрнуты в useMemo: пересчитываются только при смене исходного текста модели или имени объекта, а не на каждое нажатие клавиши. Поля рисуются циклом по schema.fields — добавили в модель новое поле, форма получила контрол без единой правки JSX.

Предпросчёт скидки идёт тем же компилятором прямо в браузере:

const preview = useMemo(() => {
  try {
    const input = Object.fromEntries(
      schema.fields.map((field) => [
        field.name,
        field.control === 'checkbox' ? Boolean(value[field.name]) : Number(value[field.name] || 0),
      ]),
    );
    return { ok: true, amount: executeUtility(document, utility, input) };
  } catch (error) {
    return { ok: false, reason: error.message };
  }
}, [document, schema, utility, value]);

Нарушенное свойство модели превращается в обычный ответ формы — preview.ok равно false, а текст ошибки идёт в <output> рядом с полями, — а не в необработанное исключение React. Кнопка отправки блокируется через disabled={!preview.ok} до тех пор, пока предпросчёт не сойдётся. Это тот же язык диагностик, что видел бы разработчик в терминале или клиент HTTP API из прошлой главы: одна и та же модель, одна и та же семантика правил и свойств, три разных места исполнения.

Server authority

Браузерная компиляция нужна для отклика UI и предпросчёта — пользователь видит примерную скидку без сетевого запроса. Финальное решение всё равно пересчитывается на сервере при onSubmitPOST /discount: клиентский JavaScript можно открыть в devtools и изменить прямо на лету, поэтому браузерный результат никогда не единственный источник истины для суммы, которая уходит в оплату. Различие фиксируется явно: DiscountForm считает preview, server.mjs считает discount — это два вызова одной утилиты, но только второй имеет право на финальное решение.

Таблица из той же структуры

Тот же список structure.fields даёт колонки для таблицы без второго описания:

const structure = document.structures.find(({ name }) => name === 'Строка счёта');
const columns = structure.fields.map((field) => ({
  id: field.name,
  header: field.name,
  accessorKey: field.name,
  format: field.type === 'Деньги' ? 'currency' : 'plain',
}));

Модель invoices-table.fts в песочнице устроена именно так: объект «Строка счёта» задаёт форму строки таблицы, а просрочен как состояние управляет подсветкой через отдельный морфизм — это уже не про форму, а про доказуемое состояние, тема следующей главы.

Чего модель не должна решать

FTS знает, что поле — дата, но не знает локаль date picker. Он знает обязательность через иногда является, но не текст ошибки валидации и не момент его показа. Он не определяет responsive layout, focus management и серверную уникальность (например, что номер заказа не должен повторяться) — это решения design system и приложения, а не предметной модели.

Практика в песочнице

  1. На вкладке model найдите объект Покупка и определите, какой контрол получит поле сумма, а какой — «постоянный клиент», если применить controlFor к их типам.
  2. Откройте credit-limit.fts (вкладка model) и найдите поле, которое является состоянием, а не скаляром. Обсудите, почему ftsFormSchema не сможет нарисовать для него обычный <input> без дополнительного контрола.
  3. Мысленно (или в форке) добавьте в объект Покупка необязательное поле комментарий иногда является строкой и проверьте на вкладке run, что выполнение утилиты не требует его присутствия во входных данных.

Типичные ошибки

  • FTS_UTILITY_PROPERTY — предпросчёт в браузере наткнулся на нарушенное свойство модели (например, пользователь ввёл сумму, при которой скидка превышает лимит). Это ожидаемый путь DiscountForm: preview.ok становится false, кнопка блокируется, текст ошибки уходит в <output> — форма не падает и не показывает белый экран.
  • FTS_UTILITY_INPUT_TYPE — предпросчёт получил значение не того типа (например, Number('') даёт NaN, а числовое поле не принимает NaN). Проверяйте приведение типов в обработчике onChange до вызова executeUtility, а не полагайтесь на то, что HTML-контрол сам вернёт правильный JS-тип.
  • FTS_NATURAL_DECLARATION — если модель редактируется прямо в песочнице (вкладка model) и в тексте появляется опечатка в ключевом слове объекта или утилиты, compile бросит эту ошибку раньше, чем форма успеет отрисоваться. Сообщение компилятора и в браузере, и в Node.js — одно и то же, потому что парсер общий.

Чек-лист

  • Форма получает список полей из document.structures, а не дублирует его в JSX руками.
  • Контрол выбирается по каноническому типу поля, а незнакомый тип не роняет форму.
  • иногда является — единственный источник required: false.
  • Предпросчёт в браузере обёрнут в try/catch и превращает нарушенное свойство в состояние формы, а не в необработанное исключение.
  • Финальное решение всегда пересчитывается на сервере тем же executeUtility — браузерный результат используется только для отклика UI.

Кейсы каталога по этой теме

Дальше: DDD

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов
Дальше