焦点管理

在某些情况下,您可能希望将焦点设置在第一个出错的输入框上。

由于 TanStack Form 根本无法深入了解您的标记语言,因此我们无法添加内置的焦点管理功能。

但是,您可以在应用程序中轻松添加此功能,而无需这种假设的内置功能。

React DOM

tsx
import { useForm } from '@tanstack/react-form'
import { z } from 'zod'

export default function App() {
  const form = useForm({
    defaultValues: { age: 0 },
    validators: {
      onChange: z.object({
        age: z.number().min(12),
      }),
    },
    onSubmit() {
      alert('Submitted!')
    },
    onSubmitInvalid({ formApi }) {
      // This can be extracted to a function that takes the form ID and `formAPI` as arguments
      const errorMap = formApi.state.errorMap.onChange!
      const inputs = Array.from(
        // Must match the selector used in your form
        document.querySelectorAll('#myform input'),
      ) as HTMLInputElement[]

      let firstInput: HTMLInputElement | undefined
      for (const input of inputs) {
        if (!!errorMap[input.name]) {
          firstInput = input
          break
        }
      }
      firstInput?.focus()
    },
  })

  return (
    // The `id` here is used to isolate the focus management from the rest of the page
    <form
      id="myform"
      onSubmit={(e) => {
        e.preventDefault()
        e.stopPropagation()
        void form.handleSubmit()
      }}
    >
      <form.Field
        name="age"
        children={(field) => (
          <label>
            Age
            <input
              name={field.name}
              value={field.state.value}
              onChange={(e) => field.handleChange(e.target.valueAsNumber)}
              type="number"
            />
          </label>
        )}
      />
      <div>
        <button type="submit">Submit</button>
      </div>
    </form>
  )
}
import { useForm } from '@tanstack/react-form'
import { z } from 'zod'

export default function App() {
  const form = useForm({
    defaultValues: { age: 0 },
    validators: {
      onChange: z.object({
        age: z.number().min(12),
      }),
    },
    onSubmit() {
      alert('Submitted!')
    },
    onSubmitInvalid({ formApi }) {
      // This can be extracted to a function that takes the form ID and `formAPI` as arguments
      const errorMap = formApi.state.errorMap.onChange!
      const inputs = Array.from(
        // Must match the selector used in your form
        document.querySelectorAll('#myform input'),
      ) as HTMLInputElement[]

      let firstInput: HTMLInputElement | undefined
      for (const input of inputs) {
        if (!!errorMap[input.name]) {
          firstInput = input
          break
        }
      }
      firstInput?.focus()
    },
  })

  return (
    // The `id` here is used to isolate the focus management from the rest of the page
    <form
      id="myform"
      onSubmit={(e) => {
        e.preventDefault()
        e.stopPropagation()
        void form.handleSubmit()
      }}
    >
      <form.Field
        name="age"
        children={(field) => (
          <label>
            Age
            <input
              name={field.name}
              value={field.state.value}
              onChange={(e) => field.handleChange(e.target.valueAsNumber)}
              type="number"
            />
          </label>
        )}
      />
      <div>
        <button type="submit">Submit</button>
      </div>
    </form>
  )
}

React Native

由于 React Native 无法访问 DOM 的 querySelectorAll API,因此我们需要手动管理输入框的元素列表。这使我们能够将焦点设置在第一个出错的输入框上

tsx
import { useRef } from 'react'
import { Text, View, TextInput, Button, Alert } from 'react-native'
import { useForm } from '@tanstack/react-form'
import { z } from 'zod'

export default function App() {
  // This can be extracted to a hook that returns the `fields` ref, a `focusFirstField` function, and a `addField` function
  const fields = useRef([] as Array<{ input: TextInput; name: string }>)

  const form = useForm({
    defaultValues: { age: 0 },
    validators: {
      onChange: z.object({
        age: z.number().min(12),
      }),
    },
    onSubmit() {
      Alert.alert('Submitted!')
    },
    onSubmitInvalid({ formApi }) {
      const errorMap = formApi.state.errorMap.onChange
      const inputs = fields.current

      let firstInput
      for (const input of inputs) {
        if (!input || !input.input) continue
        if (!!errorMap[input.name]) {
          firstInput = input.input
          break
        }
      }
      firstInput?.focus()
    },
  })

  return (
    <View style={{ padding: 16 }}>
      <form.Field
        name="age"
        children={(field) => (
          <View style={{ marginVertical: 16 }}>
            <Text>Age</Text>
            <TextInput
              keyboardType="numeric"
              ref={(input) => {
                // fields.current needs to be manually incremented so that we know what fields are rendered or not and in what order
                fields.current[0] = { input, name: field.name }
              }}
              style={{
                borderWidth: 1,
                borderColor: '#999999',
                borderRadius: 4,
                marginTop: 8,
                padding: 8,
              }}
              onChangeText={(val) => field.handleChange(Number(val))}
              value={field.state.value}
            />
          </View>
        )}
      />
      <Button title="Submit" onPress={form.handleSubmit} />
    </View>
  )
}
import { useRef } from 'react'
import { Text, View, TextInput, Button, Alert } from 'react-native'
import { useForm } from '@tanstack/react-form'
import { z } from 'zod'

export default function App() {
  // This can be extracted to a hook that returns the `fields` ref, a `focusFirstField` function, and a `addField` function
  const fields = useRef([] as Array<{ input: TextInput; name: string }>)

  const form = useForm({
    defaultValues: { age: 0 },
    validators: {
      onChange: z.object({
        age: z.number().min(12),
      }),
    },
    onSubmit() {
      Alert.alert('Submitted!')
    },
    onSubmitInvalid({ formApi }) {
      const errorMap = formApi.state.errorMap.onChange
      const inputs = fields.current

      let firstInput
      for (const input of inputs) {
        if (!input || !input.input) continue
        if (!!errorMap[input.name]) {
          firstInput = input.input
          break
        }
      }
      firstInput?.focus()
    },
  })

  return (
    <View style={{ padding: 16 }}>
      <form.Field
        name="age"
        children={(field) => (
          <View style={{ marginVertical: 16 }}>
            <Text>Age</Text>
            <TextInput
              keyboardType="numeric"
              ref={(input) => {
                // fields.current needs to be manually incremented so that we know what fields are rendered or not and in what order
                fields.current[0] = { input, name: field.name }
              }}
              style={{
                borderWidth: 1,
                borderColor: '#999999',
                borderRadius: 4,
                marginTop: 8,
                padding: 8,
              }}
              onChangeText={(val) => field.handleChange(Number(val))}
              value={field.state.value}
            />
          </View>
        )}
      />
      <Button title="Submit" onPress={form.handleSubmit} />
    </View>
  )
}
我们的合作伙伴
Code Rabbit
订阅 Bytes

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

Bytes

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

订阅 Bytes

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

Bytes

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