Перейти к содержимому

Формы

Эта страница переведена с английского. Читать страницу на английском

Form::of(value) редактирует модель или entity. Поля берутся из ссылок на поля, подписи из файлов локалей, а проверка из атрибутов #[rule], поэтому браузер и сервер проверяют одни и те же правила.

Форма

Form::of(Contact { quantity: 1, ..Contact::default() })
    .row(|r| r.field(Contact::name).field(Contact::email))
    .row(|r| r.field(Contact::quantity).field(Contact::size))
    .submit(t!("docs.live.send"), send_contact)
    .on_success(|_, ui| ui.toast(t!("docs.live.sent")))

Оставьте имя пустым или введите неверный адрес электронной почты и нажмите Send: браузер сразу покажет ошибку, а сервер отклонил бы тот же ввод с тем же сообщением.

Раскладка

ВызовРезультат
.field(M::f)Одно поле на отдельной строке
.row(|r| r.field(a).field(b))Поля рядом; на узких экранах они выстраиваются друг под другом
.section(title)Группа полей после неё с заголовком
.step(title)Шаг мастера; форма показывает по одному шагу
.range(M::from, M::to)Две даты как один выбор диапазона
Form::auto(value)Все поля в порядке объявления

Сохранение

.submit(label, action) называет action, который сохраняет данные. Action должен принимать Valid<M> для модели формы; иначе код не компилируется. Runtime отправляет значения, сервер выполняет все правила, затем action.

  • Ошибки полей от правил или от ValidationErrors в action появляются рядом с полями.
  • Error::Conflict (устаревший #[version]) показывает сообщение о том, что запись изменил кто-то другой.
  • .on_success(|result, ui| ...) выполняет эффекты после успешного сохранения (см. Effects и окна).

Элементы управления

У каждого типа поля есть элемент управления по умолчанию: текстовые поля, числовые поля, деньги с валютой, выбор даты и времени, флажки для bool и выпадающие списки для choice. .control(M::f, Control::...) выбирает другой, а .lookup(M::f, action) ищет записи на сервере. См. Controls and pickers.

Поля только для чтения

Поля #[readonly] показываются в формах, но не редактируются, например остаток на складе, который меняют только движения.

Загрузка файлов

Поля File, Option<File> и Vec<File> хранят файлы, которыми управляет FileStore. Объявите правила один раз как const, зарегистрируйте хранилище в Feature с разрешением и используйте те же правила в форме:

pub const ATTACHMENTS: FileRules = FileRules::new("attachments")
    .max_bytes(5 * 1024 * 1024)
    .max_files(5)
    .accept(&[FileType::Pdf, FileType::Png, FileType::Jpeg]);

Feature::new("support")
    .files(ATTACHMENTS, "support.ticket.files", DiskStore::new("data/attachments"))
    .scan_files(MyVirusScanner)

Form::of(ticket).field(Ticket::title).file(Ticket::attachments, ATTACHMENTS)

Элемент управления это встроенное поле выбора файла внутри зоны перетаскивания. Каждый файл загружается отдельно с индикатором хода, его можно отменить во время загрузки, повторить после сбоя и удалить после завершения. Для изображений показывается превью, созданное из object URL выбранного файла. Ход загрузки и результаты объявляются в вежливой live-области (polite live region). В showcase есть рабочий пример по адресу /uploads; на этом сайте документации нет вошедшего пользователя, поэтому живой загрузки здесь нет.

КлавишаЧто делает
TabПереходит к полю выбора файла, затем к ссылке скачивания каждого файла и к его кнопке Cancel, Retry или Remove
Enter или Space на поле выбора файлаОткрывает системное окно выбора файла (путь с клавиатуры для перетаскивания)
Enter или Space на Cancel, Retry, RemoveВыполняет действие; после Cancel фокус переходит на Retry, после Remove на следующий файл или обратно на поле

Что сервер делает для POST /_zeyn/files/{store}:

  1. проверяет заголовок CSRF, разрешение хранилища и guard Feature и запускает перехватчики (CallKind::Upload, аудит с идентификатором нового файла); загрузки учитываются в лимите записи;
  2. отклоняет объявленный Content-Length сверх лимита (413), затем читает первые 4 КиБ и определяет тип по содержимому; расширение должно принадлежать тому же типу, а тип должен входить в accept, иначе 415. HTML, SVG, XML и скрипты не принимаются никогда. Тип содержимого, заявленный браузером, игнорируется;
  3. передаёт остальное в хранилище потоком порциями не более 64 КиБ через канал на четыре элемента, подсчитывая байты; превышение лимита, обрыв соединения или 30 секунд без данных останавливают загрузку, и хранилище удаляет то, что записало;
  4. запускает FileScanner Feature; отклонённый файл или сканер, который дал сбой, приводят к удалению файла и ответу 422;
  5. отвечает 201 с {id, name, size, type}. Идентификатор это 128 случайных бит, созданных сервером; из имён удаляются пути и управляющие символы.

Скачивания (GET /_zeyn/files/{store}/{id}) проверяют форму идентификатора, разрешение, guard и арендатора (tenant) и проходят через выдачу медиа (см. docs/media.md): диапазоны, entity tag и nosniff. Встроенно (inline) отдаются только растровые изображения, аудио и видео; всё остальное отправляется как application/octet-stream с Content-Disposition: attachment, с именем в кавычках в ASCII плюс именем в UTF-8, под Content-Security-Policy: sandbox.

DiskStore хранит {id}.bin и {id}.json в одной папке и сначала записывает загрузки в {id}.part. MemoryStore держит файлы в памяти с ограничением по байтам, для тестов и демонстраций. Значение формы лишь называет файлы: action сохранения, которому нужно быть уверенным, что файл существует, вызывает store.meta(id), как это делает showcase.

Бюджет: загрузка 256 МиБ увеличивает кучу сервера примерно на 0,4 МиБ (tests/files_memory.rs); элемент управления отрисовывает 10 узлов DOM плюс 5 на каждый файл; files.js добавляет 12 КБ в исходном виде и 4 КБ в gzip, files.css 3 КБ в исходном виде и 1 КБ в gzip.