Dartvel, home
Docs
Features
Studio
Cloud
Compared

Search the site

GitHub
pub.dev

GETTING STARTED

Getting started
Existing Flutter apps
Existing Native apps
Run on your phone

APP

UI and styling
Routing
State
Accessibility
Keyboard shortcuts
Localization
Devices and desktop
Native device access
Media, 3D and XR

DATA

Data models
Forms
Search
Sync and offline
Import and export
Change capture
Database
Cache
File storage
Images
Privacy and erasure

BACKEND

Backend functions
Auth and sessions
Authorization
Queues and jobs
Workers and memory
Notifications and mail
Outbound HTTP
AI
Webhooks
GraphQL and OpenAPI
API keys and OAuth
Multi-tenancy
Billing and commerce
Modules

OPERATIONS

Edge security
Secrets and environments
Monitoring
Releases

SHIPPING

Build targets
Telegram Mini Apps
Static web hosting
Servers and deploying

REFERENCE

Testing
CLI reference
Coding agents

DATA

Forms

Every @DVModel gets a form with an input per field, so a create or edit screen is one line.

Saving runs the model's own save(): the rules from @DVModel.validate are checked first, and an edit is saved at the version the form opened.

Lay it out yourself with DVForm.builder and keep the typed field values and the submit action.

A form is driven from the keyboard with nothing added: Tab walks it in the order it is drawn, Enter submits from the last field, and a refused save says what is wrong and puts the focus on the field at fault.

ON THIS PAGE

Generate a form from a model

Lay out the form yourself

Drive the form from the keyboard

Sensitive fields are write-only

Save at the version you opened

Status

Generate a form from a model

// On the model, a form that creates one.
Widget newArticleForm() => Article.Form();

// On an article, a form that edits that article.
Widget editArticleForm(Article article) => article.Form();

// Neither takes a callback. Saving is what the form does: save() checks the
// model's @DVModel.validate rules and the version of the record the form
// opened before anything is written.

Copy code to clipboard

Article.Form() creates a record. article.Form() edits that record.

Neither takes a callback. Saving is what the form does.

The form awaits the save, shows progress and prevents another submission or reset while it runs. A failed save keeps your edits and announces a safe error message.

Each field the model serializes becomes a text input, labelled with the field's name, followed by Save and Reset.

Text typed into a number or date field that the model cannot hold is refused on save, naming the field, saying so in a live region for a screen reader, and moving the focus to it.

Tab reaches every field and every control in the order the form is drawn. Enter in a field moves to the next one; Enter in the last field submits. In a multiline field Enter is a newline instead.

Save calls the model's save(), which checks the @DVModel.validate rules (shortest and longest text, smallest and largest number, a pattern) and refuses a value that breaks one with DVModelRuleError. The same rules are checked by Studio and the data API.

The generated admin, GraphQL and an offline replay ask the model's create and update policies before they write. The save() a form calls on its own does not ask one yet.

Lay out the form yourself

Widget articleSummaryForm(Article article) => DVForm<Article>.builder(
      (DVFormControls controls) {
        final ArticleFormControls fields = controls as ArticleFormControls;
        return DVBox.list(<Widget>[
          DVText('Title: ${fields.title}'),
          if (!fields.titleIsValid) const DVText('Add a title first'),
          DVText('Publish').modifier(
            const DVModifier().semanticButton().onTap(controls.submit),
          ),
        ]);
      },
      article,
      null, // key
      (Article edited) => edited.copyWith(published: true).save(),
    );

Copy code to clipboard

The builder gets ArticleFormControls, typed as DVFormControls.

There is a typed getter per field, and titleIsValid for each String field, which checks the value is not blank. For a field whose name contains email it also checks for an @.

The controls read values and have no setters, so a builder form submits the model it was given. The onSubmit you pass decides what changes, as copyWith does in the sample.

controls.submit() and controls.reset() run the form's own actions.

The fields a builder draws register with the same scope the generated ones do, so Tab order, Enter-to-submit, the focus ring and the refusal message are already there. Wrap fields in DVFormScope yourself only when the inputs live outside the form.

Drive the form from the keyboard

Generated forms support keyboard and screen reader input automatically. Tab reaches fields and controls, Enter submits, and rejected values are announced and focused.

// Nothing to add. Every form Dartvel draws is already driven from the
// keyboard: Tab walks the fields in the order they are drawn, Enter in a field
// moves to the next one and submits from the last, Enter or Space on a control
// presses it, and the focus ring is drawn while it is there.
Widget newArticleFormFromTheKeyboard() => Article.Form();

// A form laid out by hand gets the same thing, because DVForm wraps whatever
// it builds in the scope the generated fields register with.
Widget articleForm(Article article) => DVForm<Article>.builder(
      (DVFormControls controls) {
        final ArticleFormControls fields = controls as ArticleFormControls;
        return DVBox.list(<Widget>[
          // A refusal naming TITLE is shown under this field, announced with
          // it, and given the focus, so Enter in a form the model refused
          // lands where the fix is.
          DVText(fields.title).modifier(const DVModifier().input(label: 'TITLE')),
          DVText(fields.body).modifier(
            const DVModifier().input(label: 'BODY', multiline: true),
          ),
          const DVText('Publish').modifier(
            DVModifier().semanticButton().onTap(controls.submit),
          ),
        ]);
      },
      article,
      null, // key
      (Article edited) => edited.copyWith(published: true).save(),
    );

// A save the model refuses says so on the form, names the field at fault and
// puts the focus on it. Nothing has to be written to get that.

Copy code to clipboard

Tab and Shift+Tab walk the fields and the controls in the order the form is drawn.

Enter moves to the next field, and submits from the last one. In a field declared .input(multiline: true) it is a newline, because a paragraph of text does not want to save itself.

Enter or Space on a control presses it. Anything drawn with .onTap() or .onPressed() is a real focusable control with a visible focus ring and a name, whether it is a DVBox or a DVText.

A save the model refuses is shown on the form, announced in a live region, written under the field it names, and given the focus. A person who pressed Enter and was refused is looking at a form that otherwise looks unchanged.

DVFormScope is the public piece of this, for inputs a form does not draw itself: it holds the field order and the submit, and an application that lays out its own inputs inside one gets the same behaviour.

The eye is on a password field by default

A field drawn with .input(obscureText: true) starts with a Show password control, which can be turned off with .none or replaced with .custom(builder). See the sensitive fields below.

Sensitive fields are write-only

A field marked @DVModel.sensitiveField() works like a password field. Model.Form() and the Studio record form show an input for it that hides what is typed and always starts empty: the stored value is never put in it.

Type a value and save, and the value is stored.

Leave it empty and save, and the stored value stays as it was. An edit form says so under the input.

No read gives the value back: not toPublicJson, GraphQL, Studio, tables, cards or model pages. GraphQL takes it as an optional argument of the save mutation.

The builder's controls have no getter for it. @DVModel.sensitiveField(showInForms: true) makes it an ordinary, readable field on forms and controls instead.

The input for a sensitive field carries a Show password control, so what was typed can be checked. Someone typing on a keyboard is the person most likely to mistype and least able to reach for a mouse.

// A field that hides what is typed into it gets the eye by default, so nobody
// has to ask for it and nobody has to check a mistyped password by deleting
// the whole thing.
Widget passwordField() => DVText('')
    .modifier(const DVModifier().input(label: 'PASSWORD', obscureText: true));

// Turn it off where the eye is in the way: a field asking for a key somebody
// can see on the machine in front of them, say.
Widget unlockKeyField() => DVText('').modifier(
      const DVModifier().input(
        label: 'UNLOCK KEY',
        obscureText: true,
        visibilityToggle: .none,
      ),
    );

// Or draw your own. The builder is told whether the field is obscured and
// given the way to change that, so a custom control cannot get the state wrong,
// and it is an ordinary focusable control like any other: Tab reaches it and
// Enter or Space presses it.
Widget ownToggleField() => DVText('').modifier(
      DVModifier().input(
        label: 'PASSWORD',
        obscureText: true,
        visibilityToggle: .custom(
          (BuildContext context, bool obscured, VoidCallback toggle) =>
              DVText(obscured ? 'Show' : 'Hide').modifier(
                DVModifier().semanticButton().onPressed(toggle),
              ),
        ),
      ),
    );

Copy code to clipboard

visibilityToggle: .none draws no control at all.

visibilityToggle: .custom(builder) draws yours. The builder is given whether the field is obscured and the way to change that, so it cannot get the state wrong.

The control is named for what it does right now, so a reader is not told to hide a password that is already showing.

Save at the version you opened

The edited model keeps the version of the record the form opened. If someone saved a newer version in between, save() throws DVConflictError. See Models for the conflict options.

Partial

Spec section: Record History and Optimistic Concurrency

Planned work and implementation limits

A form does not reload and merge the newer version for you.

Status

Built

Spec section: Forms

PREVIOUS Data models One class gives you storage, forms, tables and pages
NEXT Search Full text, hosted engines and semantic search
GitHub
pub.dev
npm
Acknowledgements
Privacy
Terms

FSL-1.1-MIT licensed. Built with Dartvel.

Dartvel is made by

SigmaDev Digital

To the bottom