@letar/forms

Зависимые селекты (dependsOn)

Страна → регион → город, компания → сотрудник — dependsOn у Form.Field.Select и Form.Field.Combobox (v2.27.0+)

Обзор

Зависимый список — свойство обычных Form.Field.Select и Form.Field.Combobox, а не отдельный компонент. Задайте dependsOn: поле ждёт родителя, передаёт его значение (deps) во все загрузчики и в onCreate и очищается, когда пользователь сменил родителя.

<Form.Field.Select
  name="countryId"
  loadOptions={(_search, { signal }) => fetchCountries(signal)}
  getLabel={(c) => c.name}
  getValue={(c) => c.id}
/>
<Form.Field.Combobox
  name="cityId"
  dependsOn="countryId"
  loadOptions={(search, { signal, deps }) => searchCities({ countryId: deps.countryId, search }, signal)}
  loadSelected={(value, { signal, deps }) => getCity(value, deps.countryId, signal)}
  getLabel={(c) => c.name}
  getValue={(c) => c.id}
  onCreate={(search, { deps }) => openCityDialog({ countryId: deps.countryId, name: search })}
/>

Свойства

СвойствоТипПо умолчаниюОписание
dependsOnstring | string[]—Родитель(и). Относительно группы; ведущий / — от корня формы
depsReady(deps) => booleanвсе значения непустыеСвоя проверка готовности
clearOnParentChangebooleantrueОчищать значение, когда пользователь сменил родителя
disableWhenParentEmptybooleantrueБлокировать поле, пока родители не готовы
placeholderWhenDisabledstring«Сначала выберите «Страна»»Текст заблокированного поля

deps — значения родителей по ключам из dependsOn (без ведущего /). Он приходит в loadOptions, loadSelected, useQuery(search, deps), useSelected(value, deps), onCreate(search, { deps }), onUpdate(option, { deps }) и onSettleError({ deps }). В действиях это снимок на момент их начала.

У Select ровно один источник: options (массив или функция (deps) => массив), loadOptions (один запрос на каждое значение родителя) или useOptions(deps).

Пути

dependsOn="countryId" читается как name — относительно текущей группы: в строке Form.Group.List это поле той же строки. Ведущий слэш — от корня формы: dependsOn="/countryId" из строки массива.

Когда очищается дочернее поле

Только когда родителя правят (выбор пользователем, field.handleChange, form.setFieldValue). Гидратация, reset(values), новый initialValue, UrlSync, useUrlPrefill и восстановление черновика useFormPersistence ничего не очищают. Цепочка страна → регион → город очищается целиком, а запрос городов при пустом регионе не уходит.

Записать родителя и ребёнка вместе: dependents.suppress(() => { … }) из useDeclarativeForm().dependents.

Родитель должен быть отрисованным полем: очистку запускает form-level листенер TanStack Form, а он срабатывает только у смонтированного form.Field.

Доступность

У заблокированного зависимого поля под ним видна подсказка, связанная aria-describedby; очистка объявляется вежливой live-областью («Поле «Город» очищено: изменилось поле «Страна»»).

Пару проверяет сервер

Форма не гарантирует, что город принадлежит выбранной стране. Server action проверяет пару и возвращает ошибку поля ребёнка через errorMap.onServer.

Средствами схемы ZenStack v3 этого не выразить через @@validate: компилятор отвергает условие с полем связи (@@validate condition cannot use relation fields). Проверка пары остаётся в server action. Политика с полем связи в рантайме работает (проверено на настоящей базе), с одной оговоркой:

model Address {
  countryId String
  cityId    String
  city      City @relation(fields: [cityId], references: [id])

  @@allow('create,update', city.countryId == countryId)
  // Без этой строки `update`, меняющий только `cityId`, проходит: `@@allow` смотрит на строку ДО правки
  @@deny('post-update', city.countryId != countryId)
}

create с городом чужой страны отклоняется. @@deny('post-update', …) отклоняет то же для update. Политика дополняет проверку в server action, а не заменяет её: действие кладёт ошибку под поле города.

TanStack Query и ZenStack

С @letar/forms-query 0.3.0+: fromSearchQuery((search, options, deps) => …), useLoaderQuery кладёт deps в ключ запроса, useInvalidateAfter((ctx) => keys) инвалидирует только список нужного родителя.

Из схемы

@meta("form.dependsOn", "countryId") (несколько родителей — массивом) даёт fieldProps.dependsOn. На generate плагин проверяет, что поле есть, не зависит от себя, цикла нет и родитель не исключён. Автовывода зависимости по внешнему ключу нет.

Переезд с CascadingSelect

Form.Field.CascadingSelect устарел и не меняется. Замените loadOptions(parentValue) на loadOptions(search, { deps }) у Form.Field.Select; имена dependsOn, clearOnParentChange, disableWhenParentEmpty, placeholderWhenDisabled те же.

On this page