Skip to main content
@craft-ts
LearnLearn with EffectAI agentsGuideReference
@craft-ts/core@craft-ts/component@craft-ts/dev-tools@craft-ts/effect@craft-ts/style@craft-ts/style-testing@craft-ts/i18n@craft-ts/i18n-effect@craft-ts/mcp
ExamplesMigrationEffect compatibility & maturityAdopting CraftTS progressivelyPress kitRoadmapBacklog
Beta
  • Learn
  • Learn with Effect
  • AI agents
  • Guide
  • Reference
  • Packages
  • Resources
  • Guide overview
  • Create a project
i.Core concepts
  • The mental model
  • Which primitive should I use?
  • Anatomy of a primitive
  • Generators and yield*
  • Insertions
  • Typed insertion pipes
  • Exceptions as values
ii.Managing state
  • Local state
  • State machines
  • query
  • Mutations
  • queryParams
  • asyncProcess
  • Selecting
  • Reacting to mutations
  • Collections
  • Collection utilities
  • Persistence
  • GlobalPersisterHandler
  • Pagination placeholders
  • Schema validation
iii.Recommended approaches
  • Inject at the point of use
iv.Structuring the app
  • craftService
  • Service scopes
  • Shaping the public API
  • Abstract services
  • App start
  • Lazy services
  • Server functions
  • craftRegisterFor
  • Target wrapper
v.Routing & type-safe DI
  • Setup
  • CLI automation
  • ESLint rules
  • Route providers
  • Route guards
  • Route exception handling
  • Non-blocking navigation
  • Global error component
  • Route load errors
  • Scaling routes
vi.Components & templates
  • Components
  • Activating the style system
  • Typed styles
  • Defining a design system
  • Tokens and variables
  • Global foundation and fonts
  • Pseudo-elements and animations
  • Axes and the matrix
  • Context obligations
  • Testing visual states
  • Text contrast
  • Visual attestation
  • Template obligations
  • Typed CSS variables
  • Fine-grained reactivity
  • Progressive rendering
  • Directives and .pipe(...)
  • settledValue & pendingNode
  • Accessibility
  • Customization
  • Content projection
  • Styling a component
  • Template migrator
vii.Forms
  • Overview
  • Validators
  • Submitting
  • Nested forms
  • Exception handling
  • Complete examples
viii.Testing
  • Testing services
  • Testing components
  • Type-level tests
  • Browser boundaries
  • Page stability
  • Architecture rules
    • Overview
    • Declarative baseline
    • Unique identities
    • HTTP endpoint ownership
    • Computed purity
    • Dependency cycles
    • Mutation reactions
    • Route DI proofs
    • Route component files
    • Path boundaries
    • Exclusive links
    • Persisted identities
    • insertSelect keys
    • craftEffect and network
    • craftEffect and imperative sync
    • Interactive element names
    • Metric thresholds
    • Server-state loaders
    • Primitive loader requirements
  • Extensible architecture graph
  • Graph insights
  • Craft graph vs Nx
ix.Reactivity utilities
  • craftComputed
  • craftEffect
  • craftMethod
  • source$
  • on$
  • fromEventToSource$
  • sourceFromEvent
  • afterRecomputation
x.Internationalisation
  • Type-safe i18n
  • The catalogue
  • Tokens
  • The runtime
  • With Effect
xi.Deployment (experimental)
  • Overview
  • Manifest reference
  • Diagnostics
  • Providers
  • Alchemy provider
xii.Going further
  • SSR and hydration
  • Effect integration
  • Program operators
  • Pattern matching
  • Observability
  • AI agents
  • Temporal runtime
  1. Forms
  2. Submitting

Submitting a form#

insertFormSubmit connects the form to a mutation, so submission gets its loading state, its failure state and — the point — a typed union of the exceptions submission can produce, inferred from that mutation.

Use it when the form writes somewhere. Reshape the codes with the exceptions pipeline when the server's vocabulary isn't the one your UI should show.

ts
const { updateUserMutation } = mutation('updateUserMutation', {
  method: (data: ValidatedFormValue<UserForm>) => data,
  loader: function* ({ params: user }) {
    return yield* CraftHttpClient.patch(({ response, status }) => ({
      url: '/api/users',
      body: user,
      success: response<User>(),
      exceptions: [
        function* ({ status }) {
          if (!(yield* status(409))) {
            return;
          }

          return craftException(
            { _tag: 'USER_EMAIL_ALREADY_EXISTS' },
            { message: 'This email is already used' as const },
          );
        },
      ],
    }));
  },
});

const { userFormState } = state(
  'userFormState',
  { name: '', email: '' },
  insertForm(
    insertSelectFormTree(
      'name',
      insertNoopTypingAnchor,
      insertFormAttributes(() => ({ validators: [cRequired()] })),
    ),
    insertSelectFormTree(
      'email',
      insertNoopTypingAnchor,
      insertFormAttributes(() => ({ validators: [cRequired(), cEmail()] })),
    ),
    insertFormSubmit(updateUserMutation, {
      success: () => {
        console.log('Form submitted successfully');
        return undefined;
      },
      exceptions: [
        ({ omit }) => omit(['USER_EMAIL_ALREADY_EXISTS']),
        ({ submitCraftResource }) => {
          const emailConflict =
            submitCraftResource.exceptions()?.loader?.USER_EMAIL_ALREADY_EXISTS;

          if (!emailConflict) return undefined;

          return craftException(
            { _tag: 'EMAIL_NOT_AVAILABLE' },
            emailConflict.payload,
          );
        },
      ],
    }),
  ),
);

// Submit the form
userFormState.form.submit(); // Automatically triggers the mutation

// Submit exceptions are inferred from the mutation and the `exceptions` rules.
const submitErrors = userFormState.form.submitExceptions();
const firstSubmitError = submitErrors[0]?.code; // 'EMAIL_NOT_AVAILABLE'

Submitting through a service mutation#

When the mutation belongs to a service, pass its property shortcut instead of yielding the service just to read the member:

ts
function* () {
  yield* state(
    'titleForm',
    '',
    insertForm(insertFormSubmit(TodoStore.add)),
  );
}

The shortcut resolves the same mutation instance yield* TodoStore() exposes, and the submit exceptions are inferred from it as usual. The form carries the service as a dependency of its own, so the service does not have to be yielded first for the dependency graph and the test registers to see it.

  • Service with public inputs — the bare shortcut is not callable without bindings (see OmitInputs). Give them as you would anywhere else: insertFormSubmit(Lists.add({ listId })).

  • Mutation by identifier — works the same way, one resolution shared by every form: insertForm({ identifier }, insertFormSubmit(Store.save)).

What `success` is for

success runs inside the derivation of the submit exception list, and its return value is appended to that list. Its purpose is to raise an exception the server reported alongside a successful response — not to run side effects. Resetting the form, navigating or showing a toast from there mutates state inside a computation and re-runs whenever the exceptions recompute. Drive those from your own code after submit(), or from the mutation itself.

The example above logs from success only to show where the hook fires.

insertFormSubmit preserves mutation exceptions by default. Use exceptions as an ordered pipeline when you want to refine the submit exceptions exposed by the form:

ts
insertFormSubmit(updateUserMutation, {
  exceptions: [
    // `omit` autocompletes the exception codes produced by `updateUserMutation`.
    ({ omit }) => omit(['USER_EMAIL_ALREADY_EXISTS']),

    // Returning a Craft exception appends it to the current submit exceptions.
    ({ submitCraftResource }) => {
      if (submitCraftResource.exceptions()?.loader?.USER_EMAIL_ALREADY_EXISTS) {
        return craftException(
          { _tag: 'EMAIL_NOT_AVAILABLE' },
          { message: 'This email is already used' as const },
        );
      }

      return undefined;
    },
  ],
});

Returning an array, like omit(...), replaces the current submit exception list. Returning a single craftException(...) adds it. The final inferred union is available through:

ts
const submitExceptions = userFormState.form.submitExceptions();
const aggregatedSubmitExceptions = userFormState.form.exceptions().submit;

See Also#

  • Forms overview

  • Form exceptions

←ValidatorsNested forms→

On this page

  • Submitting through a service mutation
  • See Also

@craft-ts · MIT licensed

GitHubnpm

Search