ミツモア Tech blog

「ミツモア」を運営する株式会社ミツモアの技術ブログです

V-Next Form Design In ProOne

Writer: Jerry Akira 日付: December 10, 2025

Start


Hello everyone !

I’m Akira (@JerryAkira) from ProOne Dev team.

It’s annual MeetsMore sharing time so I’d like to take this chance to share our new Form system designed by a project called V-Next in ProOne.

Please feel free to check out others’ shares here MeetsMore Advent Calendar 2025 - Adventar .

Why


For a B2B SaaS product, forms are indispensable. In traditional systems, we often use react-hook-form together with:

  1. Custom Input components to build a form
  2. A custom onSubmit to handle submission
  3. A custom onChange to validate content and surface errors

This is simple and quick for a tiny project. But for an enterprise‑grade product, many forms and many custom handlers lead to:

  1. No unified standards. Anyone might create yet another custom form approach
  2. Reduced feature reliability
  3. Higher code complexity
  4. Rapidly increasing maintenance costs due to the above
  5. Lower overall development efficiency from duplicated work

To avoid rising maintenance costs and declining development efficiency—the two issues enterprises care about most—we designed a new Form system in our platform.

What


The system supports both server‑driven and client‑side rendering. Core capabilities include:

  1. Form Fields definition
  2. Default value initialization
  3. Grid layout configuration
  4. Type safety
  5. Built‑in base validations provided by the system
  6. onSubmit with value type‑safety based on the definition
  7. onValidValueChange, which only fires when the current value is valid
  8. Additional custom validations

Next, I’ll introduce the minimal configuration ( basic elements ) of a form and a practical example to show how this Form system is used.

Basic Elements


1. Fields - The core part of a form

In V‑Next, the Form system continually ships and supports foundational Fields (think input components), such as TextField, NumberField, StaticSelectField, and DynamicSelectField. Each Field has its own spec, value type, error type, and we define a Field through configuration.

For example, a TextField that is required, has label "Name", description "I'm a text field", and grid size 12, where the key in the form value is name, can be defined as

{
    "field": "name",
    "kind": "text",
    "description": "I'm  a text field",
    "required": true,
    "label": "Name",
    "grid": {
      "size": 12
    }
}

It will be rendered as

At this point, the form value is:

{ name: '123' }

When you click the Save button, because required: true is set, the system automatically validates this Field (V‑Next Form provides auto‑validation based on base Field definitions). If nothing is entered, an error is shown:

2. Default value of the form

As another core part of a form, when defining a Form, we provide a defaultValue as the initial value that drives the first render of the entire Form.

V‑Next Form can auto‑generate initial empty values based on the base Field definitions. If we want to set defaultValue manually, the V‑Next Form type‑safety system runs checks:

Key existence check against fields.

For example, if we configured a text field with field: 'name', then defaultValue should include a key name. If it’s missing, an error is shown.

Type check per key.

For example, if we configured a Field with kind: 'text', the initial value should be a string (as defined by TextField). If we pass [], a type error is shown.

Therefore, we should define it like this:

And we’ll end up with a simple Form like this:

3. onSubmit event

V‑Next Form includes a built‑in Save button (configuable). After you finish filling out the form and click Save, the Form first validates according to each Field’s configuration and the base rules defined by the foundational Fields, then distributes errors.

From the example above, name is required, and age has a minimum of 0 and a maximum of 100.

If name is empty and age is 111, clicking Save will automatically surface two errors.

💡 Tip: you don’t need to write any validation rules for this. The system performs the basic validations for you.

4. Custom Validation

Currently, V‑Next Form only includes built‑in base validations at the Field level. However, we may need cross‑field validation with more complex logic. In that case, the Form supports an additional custom validation function.

Using the same form as above, suppose we want to restrict that Jim can only be 68 years old. We can pass a validate rule like this (type of formValue will be automatically inferred based on fields definition)

And we can do a cross validation with type‑safety value in validate.

Practical Example


Christmas is just around the corner, so I’d like to wrap up with a real form example to show you how to use the basic elements I mentioned above.

Suppose Santa wants to run a company‑wide survey to learn what gifts everyone wants. The survey looks like this:

To implement the form with basic and cross validation, we only need to write 103 lines of definition code like:

<Form
  grid={{ columns: 12, spacing: 2, container: true }}
  fields={
    [
        {
          field: 'name',
          kind: 'text',
          label: 'Child Name',
          required: true,
          grid: { size: 5 },
        },
      {
        field: 'gender',
        kind: 'static_select',
        label: 'Gender',
        maxSelection: 1,
        options: [
          { label: 'Boy', value: 'boy' },
          { label: 'Girl', value: 'girl' },
        ],
        grid: { size: 3 },
      },
      {
        field: 'age',
        kind: 'number',
        label: 'Age',
        min: 0,
        max: 18,
        grid: { size: 3 },
      },
      {
        field: 'address',
        kind: 'text',
        label: 'Address',
        multiline: true,
        required: true,
        grid: { size: 11 },
      },
      {
        field: 'gifts',
        kind: 'composite',
        fields: [
          {
            field: 'type',
            kind: 'static_select',
            label: 'Gift Type',
            maxSelection: 1,
            options: [
              { label: 'Toy', value: 'toy' },
              { label: 'Book', value: 'book' },
              { label: 'Other', value: 'other' },
            ],
            grid: { size: 4 },
          },
          {
            field: 'color',
            kind: 'static_select',
            label: 'Preferred Color',
            maxSelection: 1,
            options: [
              { label: 'Red', value: 'red' },
              { label: 'Blue', value: 'blue' },
              { label: 'Pink', value: 'pink' },
            ],
            grid: { size: 4 },
          },
          {
            field: 'notes',
            kind: 'text',
            label: 'Notes',
            grid: { size: 3 },
          },
        ],
        array: {
          append: {
            value: { type: null, color: null, notes: null },
            grid: { size: 11 },
          },
          remove: { grid: { size: 1 } },
        },
      },
    ] as const satisfies FormFieldDefinition[]
  }
  defaultValue={{
    name: '',
    gender: null,
    age: null,
    address: '',
    school: { name: null, grade: null },
    gifts: [{ type: null, color: null, notes: null }],
  }}
  onSubmit={(value) => console.info(value)}
  validate={(value) => {
    const error = {
      gifts: value.gifts.map((gift) =>
        gift.type === 'toy' && gift.color === 'red'
          ? { color: { message: 'no red toy left' } }
          : undefined,
      ),
    }
    return error.gifts.some((err) => err) ? error : undefined
  }}
/>

With this code, we can get a standard V‑Next Form with built‑in basic validations, and then use a custom onSubmit to handle the input data!

At last


If this is interesting and you want to join MeetsMore or want to know more, we are actively hiring engineers right now: https://corp.meetsmore.com/.


Thanks for reading.