表单和字段验证

TanStack Form 的核心功能是验证的概念。TanStack Form 使验证高度可定制。

  • 您可以控制何时执行验证(更改时、输入时、失焦时、提交时…)
  • 验证规则可以在字段级别或表单级别定义。
  • 验证可以是同步的,也可以是异步的(例如,作为 API 调用结果)。

何时执行验证?

这取决于你![tanstackField] 指令接受一些回调作为 props,例如 onChangeonBlur。这些回调会接收字段的当前值以及 fieldAPI 对象,以便你可以执行验证。如果你发现验证错误,只需返回错误消息字符串,它将可在 field.api.state.meta.errors 中找到。

这里有一个例子

angular-ts
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TanStackField],
  template: `
    <ng-container
      [tanstackField]="form"
      name="age"
      [validators]="{
        onChange: ageValidator
      }"
      #age="field"
    >
      <label [for]="age.api.name">Age:</label>
      <input
        [id]="age.api.name"
        [name]="age.api.name"
        [value]="age.api.state.value"
        type="number"
        (input)="age.api.handleChange($any($event).target.valueAsNumber)"
      />
      @if (age.api.state.meta.errors) {
        <em role="alert">{{ age.api.state.meta.errors.join(', ') }}</em>
      }
    </ng-container>
  `,
})
export class AppComponent {
  ageValidator: FieldValidateFn<any, any, any, any, number> = ({ value }) =>
    value < 13 ? 'You must be 13 to make an account' : undefined

  // ...
}
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TanStackField],
  template: `
    <ng-container
      [tanstackField]="form"
      name="age"
      [validators]="{
        onChange: ageValidator
      }"
      #age="field"
    >
      <label [for]="age.api.name">Age:</label>
      <input
        [id]="age.api.name"
        [name]="age.api.name"
        [value]="age.api.state.value"
        type="number"
        (input)="age.api.handleChange($any($event).target.valueAsNumber)"
      />
      @if (age.api.state.meta.errors) {
        <em role="alert">{{ age.api.state.meta.errors.join(', ') }}</em>
      }
    </ng-container>
  `,
})
export class AppComponent {
  ageValidator: FieldValidateFn<any, any, any, any, number> = ({ value }) =>
    value < 13 ? 'You must be 13 to make an account' : undefined

  // ...
}

在上面的示例中,验证在每次按键时进行(onChange)。如果我们希望在字段失去焦点时进行验证,则会将上面的代码更改为如下所示:

angular-ts
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TanStackField],
  template: `
    <ng-container
      [tanstackField]="form"
      name="age"
      [validators]="{
        onBlur: ageValidator
      }"
      #age="field"
    >
      <label [for]="age.api.name">Age:</label>
      <!-- We always need to implement onChange, so that TanStack Form receives the changes -->
      <!-- Listen to the onBlur event on the field -->
      <input
        [id]="age.api.name"
        [name]="age.api.name"
        [value]="age.api.state.value"
        type="number"
        (blur)='age.api.handleBlur()'
        (input)="age.api.handleChange($any($event).target.valueAsNumber)"
      />
      @if (age.api.state.meta.errors) {
        <em role="alert">{{ age.api.state.meta.errors.join(', ') }}</em>
      }
    </ng-container>
  `,
})
export class AppComponent {
  ageValidator: FieldValidateFn<any, any, any, any, number> = ({ value }) =>
    value < 13 ? 'You must be 13 to make an account' : undefined

  // ...
}
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TanStackField],
  template: `
    <ng-container
      [tanstackField]="form"
      name="age"
      [validators]="{
        onBlur: ageValidator
      }"
      #age="field"
    >
      <label [for]="age.api.name">Age:</label>
      <!-- We always need to implement onChange, so that TanStack Form receives the changes -->
      <!-- Listen to the onBlur event on the field -->
      <input
        [id]="age.api.name"
        [name]="age.api.name"
        [value]="age.api.state.value"
        type="number"
        (blur)='age.api.handleBlur()'
        (input)="age.api.handleChange($any($event).target.valueAsNumber)"
      />
      @if (age.api.state.meta.errors) {
        <em role="alert">{{ age.api.state.meta.errors.join(', ') }}</em>
      }
    </ng-container>
  `,
})
export class AppComponent {
  ageValidator: FieldValidateFn<any, any, any, any, number> = ({ value }) =>
    value < 13 ? 'You must be 13 to make an account' : undefined

  // ...
}

因此,您可以通过实现所需的回调函数来控制验证何时完成。您甚至可以在不同时间执行不同的验证部分。

angular-ts
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TanStackField],
  template: `
    <ng-container
      [tanstackField]="form"
      name="age"
      [validators]="{
        onChange: ageValidator,
        onBlur: minimumAgeValidator
      }"
      #age="field"
    >
      <label [for]="age.api.name">Age:</label>
      <!-- We always need to implement onChange, so that TanStack Form receives the changes -->
      <!-- Listen to the onBlur event on the field -->
      <input
        [id]="age.api.name"
        [name]="age.api.name"
        [value]="age.api.state.value"
        type="number"
        (blur)="age.api.handleBlur()"
        (input)="age.api.handleChange($any($event).target.valueAsNumber)"
      />
      @if (!age.api.state.meta.isValid) {
        <em role="alert">{{ age.api.state.meta.errors.join(', ') }}</em>
      }
    </ng-container>
  `,
})
export class AppComponent {
  ageValidator: FieldValidateFn<any, any, any, any, number> = ({ value }) =>
    value < 13 ? 'You must be 13 to make an account' : undefined

  minimumAgeValidator: FieldValidateFn<any, any, any, any, number> = ({
    value,
  }) => (value < 0 ? 'Invalid value' : undefined)

  // ...
}
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TanStackField],
  template: `
    <ng-container
      [tanstackField]="form"
      name="age"
      [validators]="{
        onChange: ageValidator,
        onBlur: minimumAgeValidator
      }"
      #age="field"
    >
      <label [for]="age.api.name">Age:</label>
      <!-- We always need to implement onChange, so that TanStack Form receives the changes -->
      <!-- Listen to the onBlur event on the field -->
      <input
        [id]="age.api.name"
        [name]="age.api.name"
        [value]="age.api.state.value"
        type="number"
        (blur)="age.api.handleBlur()"
        (input)="age.api.handleChange($any($event).target.valueAsNumber)"
      />
      @if (!age.api.state.meta.isValid) {
        <em role="alert">{{ age.api.state.meta.errors.join(', ') }}</em>
      }
    </ng-container>
  `,
})
export class AppComponent {
  ageValidator: FieldValidateFn<any, any, any, any, number> = ({ value }) =>
    value < 13 ? 'You must be 13 to make an account' : undefined

  minimumAgeValidator: FieldValidateFn<any, any, any, any, number> = ({
    value,
  }) => (value < 0 ? 'Invalid value' : undefined)

  // ...
}

在上面的示例中,我们在不同的时间(每次按键时和失去焦点时)对同一个字段进行不同的验证。由于 field.state.meta.errors 是一个数组,因此会显示给定时间内的所有相关错误。你还可以使用 field.state.meta.errorMap 来根据验证发生的时间(onChange、onBlur 等)获取错误。有关显示错误的更多信息,请参见下文。

显示错误

一旦设置好验证,您就可以映射错误数组以在 UI 中显示。

angular-ts
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TanStackField],
  template: `
    <ng-container
      [tanstackField]="form"
      name="age"
      [validators]="{
        onChange: ageValidator
      }"
      #age="field"
    >
      <!-- ... -->
      @if (age.api.state.meta.errors) {
        <em role="alert">{{ age.api.state.meta.errors.join(', ') }}</em>
      }
    </ng-container>
  `,
})
export class AppComponent {
  ageValidator: FieldValidateFn<any, any, any, any, number> = ({ value }) =>
    value < 13 ? 'You must be 13 to make an account' : undefined

  // ...
}
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TanStackField],
  template: `
    <ng-container
      [tanstackField]="form"
      name="age"
      [validators]="{
        onChange: ageValidator
      }"
      #age="field"
    >
      <!-- ... -->
      @if (age.api.state.meta.errors) {
        <em role="alert">{{ age.api.state.meta.errors.join(', ') }}</em>
      }
    </ng-container>
  `,
})
export class AppComponent {
  ageValidator: FieldValidateFn<any, any, any, any, number> = ({ value }) =>
    value < 13 ? 'You must be 13 to make an account' : undefined

  // ...
}

或者使用 errorMap 属性来访问您正在寻找的特定错误。

angular-ts
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TanStackField],
  template: `
    <ng-container
      [tanstackField]="form"
      name="age"
      [validators]="{
        onChange: ageValidator
      }"
      #age="field"
    >
      <!-- ... -->
      @if (age.api.state.meta.errorMap['onChange']) {
        <em role="alert">{{ age.api.state.meta.errorMap['onChange'] }}</em>
      }
    </ng-container>
  `,
})
export class AppComponent {
  ageValidator: FieldValidateFn<any, any, any, any, number> = ({ value }) =>
    value < 13 ? 'You must be 13 to make an account' : undefined

  // ...
}
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TanStackField],
  template: `
    <ng-container
      [tanstackField]="form"
      name="age"
      [validators]="{
        onChange: ageValidator
      }"
      #age="field"
    >
      <!-- ... -->
      @if (age.api.state.meta.errorMap['onChange']) {
        <em role="alert">{{ age.api.state.meta.errorMap['onChange'] }}</em>
      }
    </ng-container>
  `,
})
export class AppComponent {
  ageValidator: FieldValidateFn<any, any, any, any, number> = ({ value }) =>
    value < 13 ? 'You must be 13 to make an account' : undefined

  // ...
}

值得一提的是,我们的 errors 数组和 errorMap 与验证器返回的类型匹配。这意味着:

angular-ts
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TanStackField],
  template: `
    <ng-container
      [tanstackField]="form"
      name="age"
      [validators]="{
        onChange: ageValidator
      }"
      #age="field"
    >
      <!-- ... -->
      <!-- errorMap.onChange is type `{isOldEnough: false} | undefined` -->
      <!-- meta.errors is type `Array<{isOldEnough: false} | undefined>` -->
      @if (!age.api.state.meta.errorMap['onChange']?.isOldEnough) {
        <em role="alert">The user is not old enough</em>
      }
    </ng-container>
  `,
})
export class AppComponent {
  ageValidator: FieldValidateFn<any, any, any, any, number> = ({ value }) =>
    value < 13 ? 'You must be 13 to make an account' : undefined

  // ...
}
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TanStackField],
  template: `
    <ng-container
      [tanstackField]="form"
      name="age"
      [validators]="{
        onChange: ageValidator
      }"
      #age="field"
    >
      <!-- ... -->
      <!-- errorMap.onChange is type `{isOldEnough: false} | undefined` -->
      <!-- meta.errors is type `Array<{isOldEnough: false} | undefined>` -->
      @if (!age.api.state.meta.errorMap['onChange']?.isOldEnough) {
        <em role="alert">The user is not old enough</em>
      }
    </ng-container>
  `,
})
export class AppComponent {
  ageValidator: FieldValidateFn<any, any, any, any, number> = ({ value }) =>
    value < 13 ? 'You must be 13 to make an account' : undefined

  // ...
}

字段级别验证与表单级别验证

如上所示,每个 [tanstackField] 通过 onChangeonBlur 等回调接受其自己的验证规则。还可以通过将类似的回调传递给 injectForm() 函数在表单级别(而不是逐个字段)定义验证规则。

示例

angular-ts
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TanStackField],
  template: `
    <div>
      <ng-container [tanstackField]="form" name="age" #age="field">
        <!-- ... -->
        @if (formErrorMap().onChange) {
          <div>
            <em
              >There was an error on the form: {{ formErrorMap().onChange }}</em
            >
          </div>
        }
        <!-- ... -->
      </ng-container>
    </div>
  `,
})
export class AppComponent {
  form = injectForm({
    defaultValues: {
      age: 0,
    },
    onSubmit({ value }) {
      console.log(value)
    },
    validators: {
      // Add validators to the form the same way you would add them to a field
      onChange({ value }) {
        if (value.age < 13) {
          return 'Must be 13 or older to sign'
        }
        return undefined
      },
    },
  })

  // Subscribe to the form's error map so that updates to it will render
  formErrorMap = injectStore(this.form, (state) => state.errorMap)
}
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TanStackField],
  template: `
    <div>
      <ng-container [tanstackField]="form" name="age" #age="field">
        <!-- ... -->
        @if (formErrorMap().onChange) {
          <div>
            <em
              >There was an error on the form: {{ formErrorMap().onChange }}</em
            >
          </div>
        }
        <!-- ... -->
      </ng-container>
    </div>
  `,
})
export class AppComponent {
  form = injectForm({
    defaultValues: {
      age: 0,
    },
    onSubmit({ value }) {
      console.log(value)
    },
    validators: {
      // Add validators to the form the same way you would add them to a field
      onChange({ value }) {
        if (value.age < 13) {
          return 'Must be 13 or older to sign'
        }
        return undefined
      },
    },
  })

  // Subscribe to the form's error map so that updates to it will render
  formErrorMap = injectStore(this.form, (state) => state.errorMap)
}

从表单的验证器设置字段级别错误

你可以从表单的验证器设置字段上的错误。一个常见的用例是在提交时通过调用表单的 onSubmitAsync 验证器来验证所有字段。

angular-ts
@Component({
  selector: 'app-root',
  imports: [TanStackField],
  template: `
    <form (submit)="handleSubmit($event)">
      <div>
        <ng-container
          [tanstackField]="form"
          name="age"
          #ageField="field"
        >
          <label [for]="ageField.api.name">Age:</label>
          <input
            type="number"
            [name]="ageField.api.name"
            [value]="ageField.api.state.value"
            (blur)="ageField.api.handleBlur()"
            (input)="ageField.api.handleChange($any($event).target.valueAsNumber)"
          />
          @if (ageField.api.state.meta.errors.length > 0) {
            <em role="alert">{{ ageField.api.state.meta.errors.join(', ') }}</em>
          }
        </ng-container>
      </div>
      <button type="submit">Submit</button>
    </form>
  `,
})

export class AppComponent {
  form = injectForm({
    defaultValues: {
      age: 0,
      socials: [],
      details: {
        email: '',
      },
    },
    validators: {
      onSubmitAsync: async ({ value }) => {
        // Validate the value on the server
        const hasErrors = await verifyDataOnServer(value)
        if (hasErrors) {
          return {
            form: 'Invalid data', // The `form` key is optional
            fields: {
              age: 'Must be 13 or older to sign',
              // Set errors on nested fields with the field's name
              'socials[0].url': 'The provided URL does not exist',
              'details.email': 'An email is required',
            },
          };
        }

        return null;
      },
    },
  });

  handleSubmit(event: SubmitEvent) {
    event.preventDefault();
    event.stopPropagation();
    this.form.handleSubmit();
  }
}
@Component({
  selector: 'app-root',
  imports: [TanStackField],
  template: `
    <form (submit)="handleSubmit($event)">
      <div>
        <ng-container
          [tanstackField]="form"
          name="age"
          #ageField="field"
        >
          <label [for]="ageField.api.name">Age:</label>
          <input
            type="number"
            [name]="ageField.api.name"
            [value]="ageField.api.state.value"
            (blur)="ageField.api.handleBlur()"
            (input)="ageField.api.handleChange($any($event).target.valueAsNumber)"
          />
          @if (ageField.api.state.meta.errors.length > 0) {
            <em role="alert">{{ ageField.api.state.meta.errors.join(', ') }}</em>
          }
        </ng-container>
      </div>
      <button type="submit">Submit</button>
    </form>
  `,
})

export class AppComponent {
  form = injectForm({
    defaultValues: {
      age: 0,
      socials: [],
      details: {
        email: '',
      },
    },
    validators: {
      onSubmitAsync: async ({ value }) => {
        // Validate the value on the server
        const hasErrors = await verifyDataOnServer(value)
        if (hasErrors) {
          return {
            form: 'Invalid data', // The `form` key is optional
            fields: {
              age: 'Must be 13 or older to sign',
              // Set errors on nested fields with the field's name
              'socials[0].url': 'The provided URL does not exist',
              'details.email': 'An email is required',
            },
          };
        }

        return null;
      },
    },
  });

  handleSubmit(event: SubmitEvent) {
    event.preventDefault();
    event.stopPropagation();
    this.form.handleSubmit();
  }
}

值得一提的是,如果您有一个返回错误的表单验证函数,该错误可能会被字段特定的验证覆盖。

这意味着:

angular-ts
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TanStackField],
  template: `
      <div>
        <ng-container
         [tanstackField]="form"
        name="age"
       #ageField="field"
      [validators]="{
       onChange: fieldValidator
    }"
 >
  <input type="number" [value]="ageField.api.state.value"
  (input)="ageField.api.handleChange($any($event).target.valueAsNumber)"
  />
   @if (ageField.api.state.meta.errors.length > 0) {
      <em role="alert">{{ ageField.api.state.meta.errors.join(', ') }}</em>
    }
  </ng-container>
</div>
`,
})
export class AppComponent {
  form = injectForm({
    defaultValues: {
      age: 0,
    },
    validators: {
      onChange: ({ value }) => {
        return {
          fields: {
            age: value.age < 12 ? 'Too young!' : undefined,
          },
        };
      },
    },
  });

  fieldValidator: FieldValidateFn<any, any, number> = ({ value }) =>
    value % 2 === 0 ? 'Must be odd!' : undefined;
}
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TanStackField],
  template: `
      <div>
        <ng-container
         [tanstackField]="form"
        name="age"
       #ageField="field"
      [validators]="{
       onChange: fieldValidator
    }"
 >
  <input type="number" [value]="ageField.api.state.value"
  (input)="ageField.api.handleChange($any($event).target.valueAsNumber)"
  />
   @if (ageField.api.state.meta.errors.length > 0) {
      <em role="alert">{{ ageField.api.state.meta.errors.join(', ') }}</em>
    }
  </ng-container>
</div>
`,
})
export class AppComponent {
  form = injectForm({
    defaultValues: {
      age: 0,
    },
    validators: {
      onChange: ({ value }) => {
        return {
          fields: {
            age: value.age < 12 ? 'Too young!' : undefined,
          },
        };
      },
    },
  });

  fieldValidator: FieldValidateFn<any, any, number> = ({ value }) =>
    value % 2 === 0 ? 'Must be odd!' : undefined;
}

只会显示 “必须是奇数!”,即使“太年轻!”错误是由表单级别的验证器返回的。

异步函数验证

虽然我们认为大多数验证将是同步的,但在许多情况下,网络调用或其他异步操作对于验证很有用。

为此,我们提供了专用的 onChangeAsynconBlurAsync 等方法,可用于验证:

angular-ts
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TanStackField],
  template: `
    <ng-container
      [tanstackField]="form"
      name="age"
      [validators]="{ onChangeAsync: ageValidator }"
      #age="field"
    >
      <label [for]="age.api.name">Last Name:</label>
      <input
        [id]="age.api.name"
        [name]="age.api.name"
        [value]="age.api.state.value"
        type="number"
        (input)="age.api.handleChange($any($event).target.valueAsNumber)"
      />
      @if (age.api.state.meta.errors) {
        <em role="alert">{{ age.api.state.meta.errors.join(', ') }}</em>
      }
    </ng-container>
  `,
})
export class AppComponent {
  ageValidator: FieldValidateAsyncFn<any, string, number> = async ({
    value,
  }) => {
    await new Promise((resolve) => setTimeout(resolve, 1000))
    return value < 13 ? 'You must be 13 to make an account' : undefined
  }

  // ...
}
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TanStackField],
  template: `
    <ng-container
      [tanstackField]="form"
      name="age"
      [validators]="{ onChangeAsync: ageValidator }"
      #age="field"
    >
      <label [for]="age.api.name">Last Name:</label>
      <input
        [id]="age.api.name"
        [name]="age.api.name"
        [value]="age.api.state.value"
        type="number"
        (input)="age.api.handleChange($any($event).target.valueAsNumber)"
      />
      @if (age.api.state.meta.errors) {
        <em role="alert">{{ age.api.state.meta.errors.join(', ') }}</em>
      }
    </ng-container>
  `,
})
export class AppComponent {
  ageValidator: FieldValidateAsyncFn<any, string, number> = async ({
    value,
  }) => {
    await new Promise((resolve) => setTimeout(resolve, 1000))
    return value < 13 ? 'You must be 13 to make an account' : undefined
  }

  // ...
}

同步和异步验证可以共存。例如,可以在同一个字段上同时定义 onBluronBlurAsync

angular-ts
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TanStackField],
  template: `
    <ng-container
      [tanstackField]="form"
      name="age"
      [validators]="{ onBlur: ensureAge13, onBlurAsync: ensureOlderAge }"
      #age="field"
    >
      <label [for]="age.api.name">Last Name:</label>
      <input
        [id]="age.api.name"
        [name]="age.api.name"
        [value]="age.api.state.value"
        type='number'
        (blur)="age.api.handleBlur()"
        (input)="age.api.handleChange($any($event).target.value)"
      />
      @if (age.api.state.meta.errors) {
        <em role="alert">{{ age.api.state.meta.errors.join(', ') }}</em>
      }
    </ng-container>
  `,
})
export class AppComponent {
  ensureAge13: FieldValidateFn<any, any, any, any, number> = ({ value }) =>
    value < 13 ? 'You must be at least 13' : undefined

  ensureOlderAge: FieldValidateAsyncFn<any, string, number> = async ({
    value,
  }) => {
    const currentAge = await fetchCurrentAgeOnProfile()
    return value < currentAge ? 'You can only increase the age' : undefined
  }

  // ...
}
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TanStackField],
  template: `
    <ng-container
      [tanstackField]="form"
      name="age"
      [validators]="{ onBlur: ensureAge13, onBlurAsync: ensureOlderAge }"
      #age="field"
    >
      <label [for]="age.api.name">Last Name:</label>
      <input
        [id]="age.api.name"
        [name]="age.api.name"
        [value]="age.api.state.value"
        type='number'
        (blur)="age.api.handleBlur()"
        (input)="age.api.handleChange($any($event).target.value)"
      />
      @if (age.api.state.meta.errors) {
        <em role="alert">{{ age.api.state.meta.errors.join(', ') }}</em>
      }
    </ng-container>
  `,
})
export class AppComponent {
  ensureAge13: FieldValidateFn<any, any, any, any, number> = ({ value }) =>
    value < 13 ? 'You must be at least 13' : undefined

  ensureOlderAge: FieldValidateAsyncFn<any, string, number> = async ({
    value,
  }) => {
    const currentAge = await fetchCurrentAgeOnProfile()
    return value < currentAge ? 'You can only increase the age' : undefined
  }

  // ...
}

首先运行同步验证方法(onBlur),并且仅当同步方法(onBlur)成功时,才会运行异步方法(onBlurAsync)。要更改此行为,请将 asyncAlways 选项设置为 true,然后无论同步方法的结果如何,都会运行异步方法。

内置去抖动

虽然在验证数据库时异步调用是最佳选择,但在每次按键时运行网络请求会很容易导致您的数据库遭受拒绝服务攻击(DDOS)。

相反,我们通过添加一个简单的属性来启用一种简便的方法来对 async 调用进行去抖动。

angular-html
<ng-container
  [tanstackField]="form"
  name="age"
  asyncDebounceMs={500}
  [validators]="{ onChangeAsync: someValidator }"
  #age="field"
>
  <!-- ... -->
</ng-container>
<ng-container
  [tanstackField]="form"
  name="age"
  asyncDebounceMs={500}
  [validators]="{ onChangeAsync: someValidator }"
  #age="field"
>
  <!-- ... -->
</ng-container>

这将以 500 毫秒的延迟对每个异步调用进行去抖动。您甚至可以在每个验证属性上覆盖此属性。

angular-html
<ng-container
  [tanstackField]="form"
  name="age"
  [validators]="{
    onChangeAsyncDebounceMs: 1500,
    onChangeAsync: someValidator,
    onBlurAsync: otherValidator
  }"
  #age="field"
>
  <!-- ... -->
</ng-container>
<ng-container
  [tanstackField]="form"
  name="age"
  [validators]="{
    onChangeAsyncDebounceMs: 1500,
    onChangeAsync: someValidator,
    onBlurAsync: otherValidator
  }"
  #age="field"
>
  <!-- ... -->
</ng-container>

这将每 1500 毫秒运行一次 onChangeAsync,而 onBlurAsync 将每 500 毫秒运行一次。

通过模式库进行验证

虽然函数提供了更灵活的验证方式和自定义选项,但它们可能有点冗长。为了解决这个问题,有一些库提供了基于模式的验证,使得简写和类型严格的验证变得更加容易。你还可以为整个表单定义一个模式,并将其传递给表单级别,错误将自动传播到字段。

标准模式库

TanStack Form 原生地支持所有遵循 Standard Schema 规范 的库,最值得注意的是:

注意:请确保使用最新版本的模式库,因为旧版本可能尚不支持 Standard Schema。

验证不会为你提供转换后的值。有关更多信息,请参阅提交处理

要使用这些库中的模式,您可以像使用自定义函数一样将它们传递给 validators props。

angular-ts
import { z } from 'zod'

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TanStackField],
  template: `
    <ng-container
      [tanstackField]="form"
      name="age"
      [validators]="{
        onChange: z.number().gte(13, 'You must be 13 to make an account'),
      }"
      #age="field"
    >
      <!-- ... -->
    </ng-container>
  `,
})
export class AppComponent {
  form = injectForm({
    // ...
   })

  z = z

  // ...
}
import { z } from 'zod'

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TanStackField],
  template: `
    <ng-container
      [tanstackField]="form"
      name="age"
      [validators]="{
        onChange: z.number().gte(13, 'You must be 13 to make an account'),
      }"
      #age="field"
    >
      <!-- ... -->
    </ng-container>
  `,
})
export class AppComponent {
  form = injectForm({
    // ...
   })

  z = z

  // ...
}

表单和字段级别的异步验证也受支持。

angular-ts
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TanStackField],
  template: `
    <ng-container
      [tanstackField]="form"
      name="age"
      [validators]="{
        onChange: z.number().gte(13, 'You must be 13 to make an account'),
        onChangeAsyncDebounceMs: 500,
        onChangeAsync: increaseAge
      }"
      #age="field"
    >
      <!-- ... -->
    </ng-container>
  `,
})
export class AppComponent {
  increaseAge = z.number().refine(
    async (value) => {
      const currentAge = await fetchCurrentAgeOnProfile()
      return value >= currentAge
    },
    {
      message: 'You can only increase the age',
    },
  )

  // ...
}
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TanStackField],
  template: `
    <ng-container
      [tanstackField]="form"
      name="age"
      [validators]="{
        onChange: z.number().gte(13, 'You must be 13 to make an account'),
        onChangeAsyncDebounceMs: 500,
        onChangeAsync: increaseAge
      }"
      #age="field"
    >
      <!-- ... -->
    </ng-container>
  `,
})
export class AppComponent {
  increaseAge = z.number().refine(
    async (value) => {
      const currentAge = await fetchCurrentAgeOnProfile()
      return value >= currentAge
    },
    {
      message: 'You can only increase the age',
    },
  )

  // ...
}

如果您需要对 Standard Schema 验证进行更精细地控制,可以像这样将 Standard Schema 与回调函数结合使用。

angular-ts
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TanStackField],
  template: `
    <ng-container
      [tanstackField]="form"
      name="age"
      [validators]="{ onChangeAsync: ageValidator }"
      #age="field"
    >
      <!-- ... -->
    </ng-container>
  `,
})
export class AppComponent {
  ageValidator: FieldValidateAsyncFn<any, string, number> = async ({
    value,
    fieldApi,
  }) => {
    const errors = fieldApi.parseValueWithSchema(
      z.number().gte(13, 'You must be 13 to make an account'),
    )
    if (errors) return errors

    // continue with your validation
  }

  // ...
}
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TanStackField],
  template: `
    <ng-container
      [tanstackField]="form"
      name="age"
      [validators]="{ onChangeAsync: ageValidator }"
      #age="field"
    >
      <!-- ... -->
    </ng-container>
  `,
})
export class AppComponent {
  ageValidator: FieldValidateAsyncFn<any, string, number> = async ({
    value,
    fieldApi,
  }) => {
    const errors = fieldApi.parseValueWithSchema(
      z.number().gte(13, 'You must be 13 to make an account'),
    )
    if (errors) return errors

    // continue with your validation
  }

  // ...
}

防止表单提交无效数据

onChangeonBlur 等回调在表单提交时也会运行,并且如果表单无效,提交将被阻止。

表单状态对象有一个 canSubmit 标志,当任何字段无效且表单已被触碰时,该标志为 false(canSubmit 在表单被触碰之前一直为 true,即使某些字段“技术上”无效,基于其 onChange/onBlur props)。

你可以通过 injectStore 订阅它,并使用该值来例如,在表单无效时禁用提交按钮(实际上,禁用的按钮不可访问,请使用 aria-disabled 代替)。

angular-ts
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TanStackField],
  template: `
    <!-- ... -->
    <button type="submit" [disabled]="!canSubmit()">
      {{ isSubmitting() ? '...' : 'Submit' }}
    </button>
  `,
})
export class AppComponent {
  canSubmit = injectStore(this.form, (state) => state.canSubmit)
  isSubmitting = injectStore(this.form, (state) => state.isSubmitting)

  // ...
}
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [TanStackField],
  template: `
    <!-- ... -->
    <button type="submit" [disabled]="!canSubmit()">
      {{ isSubmitting() ? '...' : 'Submit' }}
    </button>
  `,
})
export class AppComponent {
  canSubmit = injectStore(this.form, (state) => state.canSubmit)
  isSubmitting = injectStore(this.form, (state) => state.isSubmitting)

  // ...
}
我们的合作伙伴
Code Rabbit
订阅 Bytes

您的每周 JavaScript 资讯。每周一免费发送给超过 10 万开发者。

Bytes

无垃圾邮件。您可以随时取消订阅。

订阅 Bytes

您的每周 JavaScript 资讯。每周一免费发送给超过 10 万开发者。

Bytes

无垃圾邮件。您可以随时取消订阅。