Browse recipes

Validate and submit a form

Keep form values, validation and submission outcomes distinguishable.

Table of contents

APIs used

mount
Starts a Valyrian.js application on a DOM target.
FormStore
Coordinates form state, validation, transforms and submission.

Run this recipe

Starting project
Use the local TSX and ESM project from Stage 1 with Valyrian.js installed and its ESM browser build configured.
File
Replace src/client-entry.tsx with this recipe snippet.
Run
Run npm run build, serve public with npx --yes serve@14.2.5 public --listen 8000, open http://localhost:8000 and use the example's visible controls.
Observable result
A short name shows validationErrors. The value fail shows submitError. Any other valid name completes after the visible inflight state.

Starting point

Start with an editable profile whose name is required before saving.

Steps

Bind the form with v-form and its editable name with v-field. Submit records success, while the view exposes isDirty, isInflight and the last result.

tsx
import { mount } from "valyrian.js";
import { FormStore } from "valyrian.js/forms";

const result = { submitted: null as boolean | null, savedName: "" };

const profileForm = new FormStore({
  state: { name: "" },
  schema: {
    type: "object",
    required: ["name"],
    properties: { name: { type: "string", minLength: 2 } },
  },
  clean: { name: (value) => String(value).trim() },
  onSubmit: async (values) => {
    await new Promise((resolve) => setTimeout(resolve, 500));
    if (values.name === "fail") {
      throw new Error("Profile could not be saved");
    }
    result.savedName = values.name;
  },
});

function ProfileForm() {
  return (
    <main>
      <form
        v-form={profileForm}
        onsubmit={() => {
          result.submitted = profileForm.success;
        }}
      >
        <label>
          Name
          <input name="name" v-field={profileForm} />
        </label>
        <p v-if={profileForm.validationErrors.name}>
          {profileForm.validationErrors.name}
        </p>
        <button type="submit" disabled={profileForm.isInflight}>
          {profileForm.isInflight ? "Saving profile..." : "Save profile"}
        </button>
      </form>

      <p>Dirty: {String(profileForm.isDirty)}</p>
      <p>Last submit result: {String(result.submitted)}</p>
      <p v-if={profileForm.submitError}>
        {profileForm.submitError instanceof Error
          ? profileForm.submitError.message
          : String(profileForm.submitError)}
      </p>
      <p v-if={profileForm.success}>Saved profile: {result.savedName}</p>
    </main>
  );
}

mount("body", ProfileForm);

Result

A short name shows validationErrors. The value fail shows submitError. Any other valid name completes after the visible inflight state.

Limits

validationErrors and submitError expose validation and submission failures separately.

Continue