发布于 2026-01-06 1 阅读
0

React Native 表单管理教程 - 构建信用卡表单

React Native 表单管理教程 - 构建信用卡表单

表单在各种应用程序中都非常常见。因此,开发者经常致力于简化表单的构建过程。我之前构建过一些自定义解决方案,也使用过所有流行的表单管理库。我认为就开发者体验和自定义程度而言,react-hook-form是最好的。

在网页上使用起来非常简单。你只需创建 HTML 输入元素并注册它们即可。但在 React Native 中就稍微复杂一些。因此,我会尽量详细描述我的每个步骤,以便更清晰地说明我的方法。本教程将创建一个信用卡表单,但它对创建任何类型的表单都很有帮助。我们在这里构建的大多数组件也都可以复用。

您可以在GitHub上找到此组件的完整版本。我还借助 react-native-web 将 React Native 代码移植到了 Web 端。您可以在我的博客上体验一下。

目录

从简单的用户界面开始

在本教程中,我使用了在 Dribbble 上找到的这款简洁设计作为参考。我还使用了我在上一篇文章中构建的TextField组件。以下是使用简单的局部状态变量生成 UI 的组件:CreditCardForm

// CreditCardForm.tsx
import React, { useState } from 'react';
import { StyleSheet, View } from 'react-native';
import Button from './Button';
import TextField from './TextField';

const CreditCardForm: React.FC = () => {
  const [name, setName] = useState('');
  const [cardNumber, setCardNumber] = useState('');
  const [expiration, setExpiration] = useState('');
  const [cvv, setCvv] = useState('');

  function onSubmit() {
    console.log('form submitted');
  }

  return (
    <View>
      <TextField
        style={styles.textField}
        label="Cardholder Name"
        value={name}
        onChangeText={(text) => setName(text)}
      />
      <TextField
        style={styles.textField}
        label="Card Number"
        value={cardNumber}
        onChangeText={(text) => setCardNumber(text)}
      />
      <View style={styles.row}>
        <TextField
          style={[
            styles.textField,
            {
              marginRight: 24,
            },
          ]}
          label="Expiration Date"
          value={expiration}
          onChangeText={(text) => setExpiration(text)}
        />
        <TextField
          style={styles.textField}
          label="Security Code"
          value={cvv}
          onChangeText={(text) => setCvv(text)}
        />
      </View>
      <Button title="PAY $15.12" onPress={onSubmit} />
    </View>
  );
};

const styles = StyleSheet.create({
  row: {
    flex: 1,
    flexDirection: 'row',
    marginBottom: 36,
  },
  textField: {
    flex: 1,
    marginTop: 24,
  },
});

export default CreditCardForm;
Enter fullscreen mode Exit fullscreen mode

我只是将表单包含在ScrollView组件中App

// App.tsx
import React, { useState } from 'react';
import { StyleSheet, Text, ScrollView } from 'react-native';
import CreditCardForm from './components/CreditCardForm';

const App: React.FC = () => {
  return (
    <ScrollView contentContainerStyle={styles.content}>
      <Text style={styles.title}>Payment details</Text>
      <CreditCardForm />
    </ScrollView>
  );
};

const styles = StyleSheet.create({
  content: {
    paddingTop: 96,
    paddingHorizontal: 36,
  },
  title: {
    fontFamily: 'Avenir-Heavy',
    color: 'black',
    fontSize: 32,
    marginBottom: 32,
  },
});

export default App;
Enter fullscreen mode Exit fullscreen mode

集成 react-hook-form

使用react-hook-form自动化工具相比手动构建表单逻辑具有诸多优势。最显著的优势在于代码更易读、更易于维护且更具可重用性。

那么,让我们开始react-hook-form为我们的项目添加内容吧:

npm install react-hook-form
// or
yarn add react-hook-form
Enter fullscreen mode Exit fullscreen mode

您可以使用TextInput其中的任何组件react-hook-form。它有一个特殊的Controller组件,可以帮助您将输入注册到库中。

这是构建 React Native 表单所需的最小代码块react-hook-form

// App.tsx
import React from 'react';
import { View, Text, TextInput } from 'react-native';
import { useForm, Controller } from 'react-hook-form';

export default function App() {
  const { control, handleSubmit, errors } = useForm();
  const onSubmit = (data) => console.log(data);

  return (
    <View>
      <Controller
        control={control}
        render={({ onChange, onBlur, value }) => (
          <TextInput
            style={styles.input}
            onBlur={onBlur}
            onChangeText={(value) => onChange(value)}
            value={value}
          />
        )}
        name="firstName"
        rules={{ required: true }}
        defaultValue=""
      />
      {errors.firstName && <Text>This is required.</Text>}
    </View>
  );
}
Enter fullscreen mode Exit fullscreen mode

虽然这对于单个输入来说已经足够好了,但更好的做法是创建一个通用的包装输入组件来处理重复性工作,例如使用 `input`Controller和显示错误消息。为此,我将创建一个 `input`组件FormTextField。它需要访问 `input` 方法返回的一些属性useForm。我们可以将这些值作为 prop 从 ` CreditCardForminput` 组件传递到FormTextField`input` 组件,但这会导致每个输入框都重复使用相同的 prop。幸运的是,`input` 组件react-hook-form提供了一个 ` useFormContextgetProperties` 方法,允许你在更深的组件层级访问所有表单属性。

看起来FormTextField会是这样的:

// FormTextField.tsx
import React from 'react';
import { useFormContext, Controller } from 'react-hook-form';
import TextField from './TextField';

type Props = React.ComponentProps<typeof TextField> & {
  name: string;
};

const FormTextField: React.FC<Props> = (props) => {
  const { name, ...restOfProps } = props;
  const { control, errors } = useFormContext();

  return (
    <Controller
      control={control}
      render={({ onChange, onBlur, value }) => (
        <TextField
          // passing everything down to TextField
          // to be able to support all TextInput props
          {...restOfProps}
          errorText={errors[name]?.message}
          onBlur={onBlur}
          onChangeText={(value) => onChange(value)}
          value={value}
        />
      )}
      name={name}
    />
  );
};

export default FormTextField;
Enter fullscreen mode Exit fullscreen mode

现在,是时候将我们的表单组件迁移到新版本了react-hook-form。我们只需将TextFields 替换为新FormTextField组件,将局部状态变量替换为单个表单模型,并将表单包裹在新版本中即可FormProvider

请注意,为表单创建 TypeScript 类型非常简单。您需要创建一个FormModel包含表单中每个字段的类型。请注意,字段名称应与您传递给 `<type>` 库的名称匹配FormTextField。库将根据该属性更新正确的字段。

修改完成后,新版本将如下所示。您可以在GithubCreditCardForm上查看完整的差异

// CreditCardForm.tsx
interface FormModel {
  holderName: string;
  cardNumber: string;
  expiration: string;
  cvv: string;
}

const CreditCardForm: React.FC = () => {
  const formMethods = useForm<FormModel>({
    defaultValues: {
      holderName: '',
      cardNumber: '',
      expiration: '',
      cvv: '',
    },
  });

  function onSubmit(model: FormModel) {
    console.log('form submitted', model);
  }

  return (
    <View>
      <FormProvider {...formMethods}>
        <FormTextField
          style={styles.textField}
          name="holderName"
          label="Cardholder Name"
        />
        <FormTextField
          style={styles.textField}
          name="cardNumber"
          label="Card Number"
        />
        <View style={styles.row}>
          <FormTextField
            style={[
              styles.textField,
              {
                marginRight: 24,
              },
            ]}
            name="expiration"
            label="Expiration Date"
          />
          <FormTextField
            style={styles.textField}
            name="cvv"
            label="Security Code"
            keyboardType="number-pad"
          />
        </View>
        <Button
          title="PAY $15.12"
          onPress={formMethods.handleSubmit(onSubmit)}
        />
      </FormProvider>
    </View>
  );
};
Enter fullscreen mode Exit fullscreen mode

提高可重复使用性

此时我必须就表单的复用性做出决定。这涉及到最初使用该useForm方法创建表单的位置。我们有两种选择:

  1. 将表单内部定义成现在的样子。如果您只在单个流程/屏幕中使用信用卡表单,这样做很合理。这样CreditCardForm您就无需在多个地方重新定义表单并传递它。FormProvider
  2. 在父组件(即使用该表单的组件)中定义表单。这样CreditCardForm您就可以访问所有方法,并基于表单提供的所有内容构建独立功能。假设您有两个屏幕:一个用于支付产品,另一个用于注册信用卡。按钮在这两种情况下应该看起来不同。react-hook-formCreditCardForm

以下是第二种方案的一个例子。在这个例子中,我们会监听卡号的变化,并据此更新按钮标题:

// App.tsx
 const App: React.FC = () => {
+  const formMethods = useForm<FormModel>({
+    // to trigger the validation on the blur event
+    mode: 'onBlur',
+    defaultValues: {
+      holderName: 'Halil Bilir',
+      cardNumber: '',
+      expiration: '',
+      cvv: '',
+    },
+  })
+  const cardNumber = formMethods.watch('cardNumber')
+  const cardType = cardValidator.number(cardNumber).card?.niceType
+
+  function onSubmit(model: FormModel) {
+    Alert.alert('Success')
+  }
+
   return (
     <ScrollView contentContainerStyle={styles.content}>
-      <Text style={styles.title}>Payment details</Text>
-      <CreditCardForm />
+      <FormProvider {...formMethods}>
+        <Text style={styles.title}>Payment details</Text>
+        <CreditCardForm />
+        <Button
+          title={cardType ? `PAY $15.12 WITH ${cardType}` : 'PAY $15.12'}
+          onPress={formMethods.handleSubmit(onSubmit)}
+        />
+      </FormProvider>
     </ScrollView>
   )
 }
Enter fullscreen mode Exit fullscreen mode

我选择第二个方案。

验证

react-hook-form让我们只需将参数传递 rules给 ` .` 即可定义验证Controller。首先,让我们将其添加到`.` 中FormTextField

// FormTextField.tsx
-import { useFormContext, Controller } from 'react-hook-form'
+import { useFormContext, Controller, RegisterOptions } from 'react-hook-form'
 import TextField from './TextField'

 type Props = React.ComponentProps<typeof TextField> & {
   name: string
+  rules: RegisterOptions
 }

 const FormTextField: React.FC<Props> = (props) => {
-  const { name, ...restOfProps } = props
+  const { name, rules, ...restOfProps } = props
   const { control, errors } = useFormContext()

   return (
@@ -25,6 +26,7 @@ const FormTextField: React.FC<Props> = (props) => {
         />
       )}
       name={name}
+      rules={rules}
     />
   )
 }
Enter fullscreen mode Exit fullscreen mode

在本教程中,我将把验证逻辑委托给Braintree 的 card-validator库,以便我们专注于表单部分。现在我需要rules为我们的FormTextField组件定义一个对象,rules该对象将包含两个属性:

  1. required:这用于设置当字段为空时显示的消息。
  2. validate.{custom_validation_name}我们可以在这里创建一个自定义验证方法。我将使用它,通过card-validation库来验证输入值的完整性。

我们的输入字段需要如下所示。您可以在GitHub上查看完整的验证规则差异。

// CreditCardForm.tsx
<>
  <FormTextField
    style={styles.textField}
    name="holderName"
    label="Cardholder Name"
    rules={{
      required: 'Cardholder name is required.',
      validate: {
        isValid: (value: string) => {
          return (
            cardValidator.cardholderName(value).isValid ||
            'Cardholder name looks invalid.'
          );
        },
      },
    }}
  />
  <FormTextField
    style={styles.textField}
    name="cardNumber"
    label="Card Number"
    keyboardType="number-pad"
    rules={{
      required: 'Card number is required.',
      validate: {
        isValid: (value: string) => {
          return (
            cardValidator.number(value).isValid ||
            'This card number looks invalid.'
          );
        },
      },
    }}
  />
  <FormTextField
    style={[
      styles.textField,
      {
        marginRight: 24,
      },
    ]}
    name="expiration"
    label="Expiration Date"
    rules={{
      required: 'Expiration date is required.',
      validate: {
        isValid: (value: string) => {
          return (
            cardValidator.expirationDate(value).isValid ||
            'This expiration date looks invalid.'
          );
        },
      },
    }}
  />
  <FormTextField
    style={styles.textField}
    name="cvv"
    label="Security Code"
    keyboardType="number-pad"
    maxLength={4}
    rules={{
      required: 'Security code is required.',
      validate: {
        isValid: (value: string) => {
          const cardNumber = formMethods.getValues('cardNumber');
          const { card } = cardValidator.number(cardNumber);
          const cvvLength = card?.type === 'american-express' ? 4 : 3;

          return (
            cardValidator.cvv(value, cvvLength).isValid ||
            'This security code looks invalid.'
          );
        },
      },
    }}
  />
</>
Enter fullscreen mode Exit fullscreen mode

完成这些更改后,点击按钮将看到以下屏幕PAY

验证

触发验证

验证触发方案react-hook-form无需任何自定义代码即可配置。mode参数用于配置验证触发方案:

  • onChange验证将在提交事件时触发,无效输入将附加 onChange 事件监听器以重新验证它们。
  • onBlur:失去焦点时将触发验证。
  • onTouched验证将在第一次失去焦点事件时触发。之后,它将在每次更改事件时触发。

虽然这些模式足以应对大多数情况,但我希望我的表单能够实现自定义行为。我希望能够快速地向用户提供反馈,但又不能太快。这意味着我希望在用户输入足够的字符后立即验证输入。因此,我创建了一个 effect,它会监视输入值,并在输入值超过某个阈值(此处为 prop)FormTextField时触发验证。validationLength

请注意,这并非表单正常运行的必要条件,而且如果您的验证方法比较耗费资源,则可能会造成一定的性能损失。

// FormTextField.tsx
type Props = React.ComponentProps<typeof TextField> & {
   name: string
   rules: RegisterOptions
+  validationLength?: number
 }

 const FormTextField: React.FC<Props> = (props) => {
-  const { name, rules, ...restOfProps } = props
-  const { control, errors } = useFormContext()
+  const {
+    name,
+    rules,
+    validationLength = 1,
+    ...restOfProps
+  } = props
+  const { control, errors, trigger, watch } = useFormContext()
+  const value = watch(name)
+
+  useEffect(() => {
+    if (value.length >= validationLength) {
+      trigger(name)
+    }
+  }, [value, name, validationLength, trigger])
Enter fullscreen mode Exit fullscreen mode

格式化输入值

为了使卡号和有效期输入字段看起来美观,我会根据用户输入的每个新字符立即格式化它们的值。

  • 信用卡号:我将按XXXX XXXX XXXX XXXX格式格式化其值。
  • 到期日期:我将按MM/YY格式格式化其值。

虽然有一些库可以实现类似的功能,但我希望自己创建一个简单的解决方案。因此,我utils/formatters.ts为此创建了一个文件:

// utils/formatters.ts
export function cardNumberFormatter(
  oldValue: string,
  newValue: string,
): string {
  // user is deleting so return without formatting
  if (oldValue.length > newValue.length) {
    return newValue;
  }

  return newValue
    .replace(/\W/gi, '')
    .replace(/(.{4})/g, '$1 ')
    .substring(0, 19);
}

export function expirationDateFormatter(
  oldValue: string,
  newValue: string,
): string {
  // user is deleting so return without formatting
  if (oldValue.length > newValue.length) {
    return newValue;
  }

  return newValue
    .replace(/\W/gi, '')
    .replace(/(.{2})/g, '$1/')
    .substring(0, 5);
}
Enter fullscreen mode Exit fullscreen mode

现在我们只需formatterFormTextField组件创建一个 prop,并将它返回的值传递给它onChange

// FormTextField.tsx
-  onChangeText={(value) => onChange(value)}
+  onChangeText={(text) => {
+    const newValue = formatter ? formatter(value, text) : text
+    onChange(newValue)
+  }}
   value={value}
  />
)}
Enter fullscreen mode Exit fullscreen mode

我创建了一些测试,以确保格式化工具使用 Jest 的test.each方法返回预期值。我希望这能帮助您更容易地理解这些工具方法的作用:

// utils/formatters.test.ts
import { cardNumberFormatter, expirationDateFormatter } from './formatters';

describe('cardNumberFormatter', () => {
  test.each([
    {
      // pasting the number
      oldValue: '',
      newValue: '5555555555554444',
      output: '5555 5555 5555 4444',
    },
    {
      // trims extra characters
      oldValue: '',
      newValue: '55555555555544443333',
      output: '5555 5555 5555 4444',
    },
    {
      oldValue: '555',
      newValue: '5555',
      output: '5555 ',
    },
    {
      // deleting a character
      oldValue: '5555 5',
      newValue: '5555 ',
      output: '5555 ',
    },
  ])('%j', ({ oldValue, newValue, output }) => {
    expect(cardNumberFormatter(oldValue, newValue)).toEqual(output);
  });
});

describe('expirationDateFormatter', () => {
  test.each([
    {
      // pasting 1121
      oldValue: '',
      newValue: '1121',
      output: '11/21',
    },
    {
      // pasting 11/21
      oldValue: '',
      newValue: '11/21',
      output: '11/21',
    },
    {
      oldValue: '1',
      newValue: '12',
      output: '12/',
    },
    {
      // deleting a character
      oldValue: '12/2',
      newValue: '12/',
      output: '12/',
    },
  ])('%j', ({ oldValue, newValue, output }) => {
    expect(expirationDateFormatter(oldValue, newValue)).toEqual(output);
  });
});
Enter fullscreen mode Exit fullscreen mode

专注于下一个领域

我认为这是一种优秀的表单用户体验模式:当用户填写完当前输入框后,将注意力集中在下一个输入框上。有两种方法可以判断用户何时完成填写:

  1. 监听onSubmitEditing输入事件。当用户点击键盘上的回车键时,此事件会被触发。
  2. 检查输入验证结果:这意味着用户已输入信用卡、有效期和 CVV 字段的所有必要字符(如果有效)。

我将对持卡人姓名输入使用第一种方法,对其他输入使用第二种方法。这是因为我们无法预知持卡人姓名何时输入完整,这与其他输入方式不同。

我们需要ref为每个输入保留一个 s,并nextTextInputRef.focus相应地调用方法。我们有两个自定义组件封装了 React Native TextInput:分别是FormTextFieldTextField。因此,我们必须使用React.forwardRef来确保ref附加到原生组件TextInput

以下是我搭建这个装置的步骤:

  • 包装好FormTextField并附TextFieldReact.forwardRef
+ import { TextInput } from "react-native"
// components/FormTextField.tsx
-const FormTextField: React.FC<Props> = (props) => {
+const FormTextField = React.forwardRef<TextInput, Props>((props, ref) => {
// components/TextField.tsx
-const TextField: React.FC<Props> = (props) => {
+const TextField = React.forwardRef<TextInput, Props>((props, ref) => {
Enter fullscreen mode Exit fullscreen mode
  • 在组件上创建了onValid属性FormTextField,并修改了触发验证的效果:
// FormTextField.tsx
useEffect(() => {
+    async function validate() {
+      const isValid = await trigger(name)
+      if (isValid) onValid?.()
+    }
+
     if (value.length >= validationLength) {
-      trigger(name)
+      validate()
     }
   }, [value, name, validationLength, trigger])
Enter fullscreen mode Exit fullscreen mode
  • 为每个组件创建了一个引用,并触发了下一个输入引用的onFocus方法:
// CreditCardForm.tsx
+ const holderNameRef = useRef<TextInput>(null)
+ const cardNumberRef = useRef<TextInput>(null)
+ const expirationRef = useRef<TextInput>(null)
+ const cvvRef = useRef<TextInput>(null)

<>
  <FormTextField
+   ref={holderNameRef}
    name="holderName"
    label="Cardholder Name"
+   onSubmitEditing={() => cardNumberRef.current?.focus()}
  />
  <FormTextField
+   ref={cardNumberRef}
    name="cardNumber"
    label="Card Number"
+   onValid={() => expirationRef.current?.focus()}
  />
  <FormTextField
+   ref={expirationRef}
    name="expiration"
    label="Expiration Date"
+   onValid={() => cvvRef.current?.focus()}
  />
  <FormTextField
+   ref={cvvRef}
    name="cvv"
    label="Security Code"
+   onValid={() => {
+     // form is completed so hide the keyboard
+     Keyboard.dismiss()
+   }}
  />
</>
Enter fullscreen mode Exit fullscreen mode

您可以在Github上查看此部分的完整差异

显示卡片类型图标

这是我们的最后一个功能。我已经创建了CardIcon相应的组件,并将通过endEnhancerprop 将其传递给输入框。

// CardIcon.tsx
import React from 'react';
import { Image, StyleSheet } from 'react-native';
import cardValidator from 'card-validator';

const VISA = require('./visa.png');
const MASTERCARD = require('./mastercard.png');
const AMEX = require('./amex.png');
const DISCOVER = require('./discover.png');

type Props = {
  cardNumber: string;
};

const CardIcon: React.FC<Props> = (props) => {
  const { cardNumber } = props;
  const { card } = cardValidator.number(cardNumber);

  let source;
  switch (card?.type) {
    case 'visa':
      source = VISA;
      break;
    case 'mastercard':
      source = MASTERCARD;
      break;
    case 'discover':
      source = DISCOVER;
      break;
    case 'american-express':
      source = AMEX;
      break;
    default:
      break;
  }

  if (!source) return null;

  return <Image style={styles.image} source={source} />;
};

const styles = StyleSheet.create({
  image: {
    width: 48,
    height: 48,
  },
});

export default CardIcon;
Enter fullscreen mode Exit fullscreen mode

您可以在这里查看卡片图标的完整差异

测试

我将为表单的关键部分创建一些测试,以确保我们能够立即知道它们何时出现问题,这些关键部分包括验证、值格式化和表单提交。

我喜欢用react-native-testing-library来进行测试。它能让你创建类似于用户行为的测试。

我还在使用bdd-lazy-var,这是我在上一份工作中学到的工具。我仍然会在测试中使用它,因为它能以更清晰、更易读的方式描述测试变量。

因此,我将创建一个表单,并像在实际屏幕上使用一样useForm将其传递给测试。然后,我会更改输入值,测试验证结果,并在点击提交按钮时检查返回结果。以下是我所有测试用例中将使用的基本设置:FormProviderreact-hook-form

// CreditCardForm.test.tsx
import React from 'react';
import { fireEvent, render, waitFor } from '@testing-library/react-native';
import { def, get } from 'bdd-lazy-var/getter';
import { useForm, FormProvider } from 'react-hook-form';
import { Button } from 'react-native';
import CreditCardForm from './CreditCardForm';

const FormWrapper = () => {
  const formMethods = useForm({
    mode: 'onBlur',
    defaultValues: {
      holderName: '',
      cardNumber: '',
      expiration: '',
      cvv: '',
    },
  });
  const { handleSubmit } = formMethods;

  const onSubmit = (model) => {
    get.onSubmit(model);
  };

  return (
    <FormProvider {...formMethods}>
      <CreditCardForm />
      <Button onPress={handleSubmit(onSubmit)} title={'Submit'} />
    </FormProvider>
  );
};

def('render', () => () => render(<FormWrapper />));
def('onSubmit', () => jest.fn());
Enter fullscreen mode Exit fullscreen mode

测试信用卡号验证

本测试用例中包含三个断言:

  1. 输入16个字符后才会触发验证。
  2. 当我输入无效的信用卡号码时,屏幕显示错误信息。
  3. 输入有效的卡号后,错误就消失了。
// CreditCardForm.test.tsx
it('validates credit card number', async () => {
  const { queryByText, getByTestId } = get.render();

  // does not display validation message until input is filled
  const cardInput = getByTestId('TextField.cardNumber');
  fireEvent.changeText(cardInput, '55555555');
  await waitFor(() => {
    expect(queryByText(/This card number looks invalid./)).toBeNull();
  });

  // invalid card
  fireEvent.changeText(cardInput, '5555555555554440');
  await waitFor(() => {
    expect(queryByText(/This card number looks invalid./)).not.toBeNull();
  });

  // valid card
  fireEvent.changeText(cardInput, '5555 5555 5555 4444');
  await waitFor(() => {
    expect(queryByText(/This card number looks invalid./)).toBeNull();
  });
});
Enter fullscreen mode Exit fullscreen mode

测试有效期验证

使用已通过且有效的日期进行测试,并检查验证错误是否显示/隐藏:

// CreditCardForm.test.tsx
it('validates expiration date', async () => {
  const { queryByText, getByTestId } = get.render();

  const input = getByTestId('TextField.expiration');
  // passed expiration date
  fireEvent.changeText(input, '1018');
  await waitFor(() =>
    expect(queryByText(/This expiration date looks invalid./)).not.toBeNull(),
  );

  // valid date
  fireEvent.changeText(input, '10/23');
  await waitFor(() =>
    expect(queryByText(/This expiration date looks invalid./)).toBeNull(),
  );
});
Enter fullscreen mode Exit fullscreen mode

测试表单提交

在每个输入框中输入正确的值,然后点击提交按钮。我期望之后该onSubmit方法会被调用,并传入正确且格式化的数据:

// CreditCardForm.test.tsx
it('submits the form', async () => {
  const { getByText, getByTestId } = get.render();

  fireEvent.changeText(getByTestId('TextField.holderName'), 'Halil Bilir');
  fireEvent.changeText(getByTestId('TextField.cardNumber'), '5555555555554444');
  fireEvent.changeText(getByTestId('TextField.expiration'), '0224');
  fireEvent.changeText(getByTestId('TextField.cvv'), '333');

  fireEvent.press(getByText('Submit'));

  await waitFor(() =>
    expect(get.onSubmit).toHaveBeenLastCalledWith({
      holderName: 'Halil Bilir',
      // cardNumber and expiration are now formatted
      cardNumber: '5555 5555 5555 4444',
      expiration: '02/24',
      cvv: '333',
    }),
  );
});
Enter fullscreen mode Exit fullscreen mode

输出

您可以在Github上找到完整版本。如果您有任何反馈或疑问,请随时通过Twitter给我发消息。

文章来源:https://dev.to/halilb/react-native-form-management-tutorial-building-a-credit-card-form-3bjp