Skip to content

Latest commit

 

History

History
1595 lines (1212 loc) · 50.6 KB

File metadata and controls

1595 lines (1212 loc) · 50.6 KB

七、使用表单

表单在我们构建的应用程序中非常常见。在本章中,我们将学习如何使用 React 和 TypeScript 中的受控组件构建表单。我们将为 React shop 创建一个联系我们表单,我们在其他章节中一直在进行学习练习。

我们很快就会发现,在创建表单时涉及到大量的样板代码,因此我们将考虑构建一个通用表单组件来减少样板代码。客户端验证对于我们构建的表单的用户体验至关重要,因此我们还将深入讨论这个主题。

最后,表单提交是一个重要的考虑因素。我们将介绍如何处理提交错误以及成功。

在本章中,我们将讨论以下主题:

  • 使用受控组件创建窗体
  • 使用通用组件减少样板代码
  • 验证表单
  • 提交表格

技术要求

在本章中,我们将使用以下技术:

In order to restore code from a previous chapter, the LearnReact17WithTypeScript repository at https://github.com/carlrip/LearnReact17WithTypeScript can be downloaded. The relevant folder can then be opened in Visual Studio Code and then npm install can be entered in the terminal to do the restore. All the code snippets in this chapter can be found online at https://github.com/carlrip/LearnReact17WithTypeScript/tree/master/07-WorkingWithForms.

使用受控组件创建窗体

表单是大多数应用程序的常见部分。在 React 中,创建表单的标准方法是使用所谓的受控组件。受控组件的值与 React 中的状态同步。当我们实现第一个受控组件时,这将更有意义。

我们将扩展我们一直在构建的 React 商店,以包括一个联系我们表单。这将使用受控组件实现。

添加“联系我们”页面

在开始处理表单之前,我们需要一个页面来承载表单。页面将是一个容器组件,我们的表单将是一个表示组件。我们还需要创建一个导航选项,将我们带到新页面。

在开始实现表单之前,我们将编写以下代码:

  1. 如果还没有,请在 Visual Studio 代码中打开 React shop 项目。在src文件夹中创建一个名为ContactUsPage.tsx的新文件,包含以下代码:
import * as React from "react";

class ContactUsPage extends React.Component {
  public render() {
    return (
      <div className="page-container">
        <h1>Contact Us</h1>
        <p>
         If you enter your details we'll get back to you as soon as  
         we can.
        </p>
      </div>
    );
  }
}

export default ContactUsPage;

这个组件最终将包含 state,因此,我们创建了一个基于类的组件。这只是呈现了一个标题和一些指示。最终,它将引用我们的表单。

  1. 现在,让我们将此页面添加到可用路由。打开Routes.tsx,导入我们的页面:
import ContactUsPage from "./ContactUsPage";
  1. Routes组件的render方法中,我们现在可以在admin路由的正上方向页面添加新路由:
<Switch>
  <Redirect exact={true} from="/" to="/products" />
  <Route path="/products/:id" component={ProductPage} />
  <Route exact={true} path="/products" component={ProductsPage} />
  <Route path="/contactus" component={ContactUsPage} />
  <Route path="/admin">
    ...
  </Route>
  <Route path="/login" component={LoginPage} />
  <Route component={NotFoundPage} />
</Switch>
  1. 现在打开Header.tsx,其中包含所有导航选项。让我们在新页面的管理链接上方添加一个NavLink
<nav>
  <NavLink to="/products" className="header-link" activeClassName="header-link-active">
    Products
  </NavLink>
  <NavLink to="/contactus" className="header-link" activeClassName="header-link-active">
 Contact Us
 </NavLink>
  <NavLink to="/admin" className="header-link" activeClassName="header-link-active">
    Admin
  </NavLink>
</nav>
  1. 通过在终端中输入以下内容,在开发服务器中运行项目:
npm start

您应该会看到一个新的导航选项,将我们带到新页面:

现在我们有了新的页面,我们准备在表单中实现第一个受控输入。我们将在下一节中进行此操作。

创建受控输入

在本节中,我们将开始创建包含第一个受控输入的表单:

  1. 在包含以下代码的src文件夹中创建一个名为ContactUs.tsx的新文件:
import * as React from "react";

const ContactUs: React.SFC = () => {
  return (
    <form className="form" noValidate={true}>
      <div className="form-group">
        <label htmlFor="name">Your name</label>
        <input type="text" id="name" />
      </div>
    </form>
  );
};

export default ContactUs;

这是一个函数组件,用于呈现包含标签和用户名输入的表单。

  1. 我们已经引用了一些 CSS 类,所以让我们将它们添加到index.css的底部:
.form {
  width: 300px;
  margin: 0px auto 0px auto;
}

.form-group {
  display: flex;
  flex-direction: column;
  margin-bottom: 20px;
}

.form-group label {
  align-self: flex-start;
  font-size: 16px;
  margin-bottom: 3px;
}

.form-group input, select, textarea {
  font-family: Arial;
  font-size: 16px;
  padding: 5px;
  border: lightgray solid 1px;
  border-radius: 5px;
}

form-group类将表单中的每个字段包装起来,在输入上方以很好的间距显示标签。

  1. 现在让我们从页面中引用我们的表单。转到ContactUsPage.tsx并导入我们的组件:
import ContactUs from "./ContactUs";
  1. 然后我们可以在div容器底部的render方法中引用我们的组件:
<div className="page-container">
  <h1>Contact Us</h1>
  <p>If you enter your details we'll get back to you as soon as we can.</p>
  <ContactUs />
</div>

如果我们查看 running 应用程序并转到“联系我们”页面,我们将看到呈现的名称字段:

我们可以在这个字段中输入我们的名字,但什么也不会发生。我们希望输入的名称存储在ContactUsPage容器组件状态中。这是因为ContactUsPage最终将管理表单提交。

  1. 让我们为ContactUsPage添加一个状态类型:
interface IState {
 name: string;
 email: string;
 reason: string;
 notes: string;
}

class ContactUsPage extends React.Component<{}, IState> { ... }

除了此人的姓名,我们还将捕获他们的电子邮件地址、联系商店的原因以及任何其他注释。

  1. 我们还要初始化构造函数中的状态:
public constructor(props: {}) {
  super(props);
  this.state = {
    email: "",
    name: "",
    notes: "",
    reason: ""
  };
}
  1. 我们现在需要将名称值从ContactUsPage中的状态获取到ContactUs组件中。这将允许我们在输入中显示值。我们可以先在ContactUs组件中创建道具:
interface IProps {
 name: string;
 email: string;
 reason: string;
 notes: string;
}

const ContactUs: React.SFC<IProps> = props => { ... }

我们已经为最终将以表单形式捕获的所有数据创建了道具。

  1. 现在,我们可以将名称输入值绑定到name属性:
<div className="form-group">
  <label htmlFor="name">Your name</label>
  <input type="text" id="name" value={props.name} />
</div>
  1. 我们现在可以通过ContactUsPage中的状态传递这些信息:
<ContactUs 
  name={this.state.name} 
 email={this.state.email} 
 reason={this.state.reason} 
 notes={this.state.notes} 
/>

让我们转到 running 应用程序并转到我们的联系我们页面。尝试在名称输入中键入一些内容。

似乎什么都没发生。。。有些东西阻止我们输入值。

我们刚刚将输入值设置为某个 React 状态,因此 React 现在控制输入值。这就是为什么我们似乎不再能够输入它。

我们正在创建第一个受控输入。然而,如果用户不能在受控输入中输入任何内容,那么受控输入就没有多大用处。那么,我们如何使我们的输入再次可编辑?

答案是我们需要监听对输入值的更改,并相应地更新状态。React 然后将呈现来自状态的新输入值。

  1. 让我们听一下通过onChange道具对输入的更改:
<input type="text" id="name" value={props.name} onChange={handleNameChange} />
  1. 让我们创建刚才引用的处理程序:
const ContactUs: React.SFC<IProps> = props => {
  const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
 props.onNameChange(e.currentTarget.value);
 };
  return ( ... );
};

请注意,我们已经将泛型React.ChangeEvent命令与正在处理的元素类型(HTMLInputElement一起使用。

事件参数中的currentTarget属性为我们提供了事件处理程序所附加到的元素的引用。其中的value属性为我们提供了输入的最新值。

  1. 处理程序引用了一个我们尚未定义的onNameChange函数 prop。因此,让我们将此添加到我们的界面,以及其他字段的类似道具:
interface IProps {
  name: string;
  onNameChange: (name: string) => void;
  email: string;
  onEmailChange: (email: string) => void;
  reason: string;
  onReasonChange: (reason: string) => void;
  notes: string;
  onNotesChange: (notes: string) => void;
}
  1. 我们现在可以将这些道具从ContactUsPage传递到ContactUs
<ContactUs
  name={this.state.name}
  onNameChange={this.handleNameChange}
  email={this.state.email}
  onEmailChange={this.handleEmailChange}
  reason={this.state.reason}
  onReasonChange={this.handleReasonChange}
  notes={this.state.notes}
  onNotesChange={this.handleNotesChange}
/>
  1. 让我们创建刚才在ContactUsPage中引用的更改处理程序,该处理程序设置相关状态:
private handleNameChange = (name: string) => {
  this.setState({ name });
};
private handleEmailChange = (email: string) => {
  this.setState({ email });
};
private handleReasonChange = (reason: string) => {
  this.setState({ reason });
};
private handleNotesChange = (notes: string) => {
  this.setState({ notes });
};

如果我们现在转到 running 应用程序中的 Contact Us 页面,并在名称中输入一些内容,那么这次输入的行为与预期一致。

  1. 让我们在ContactUsrender方法中添加电子邮件、原因和注释字段:
<form className="form" noValidate={true} onSubmit={handleSubmit}>
  <div className="form-group">
    <label htmlFor="name">Your name</label>
    <input type="text" id="name" value={props.name} onChange={handleNameChange} />
  </div>

  <div className="form-group">
 <label htmlFor="email">Your email address</label>
 <input type="email" id="email" value={props.email} onChange={handleEmailChange} />
 </div>

 <div className="form-group">
 <label htmlFor="reason">Reason you need to contact us</label>
 <select id="reason" value={props.reason} onChange={handleReasonChange}>
 <option value="Marketing">Marketing</option>
 <option value="Support">Support</option>
 <option value="Feedback">Feedback</option>
 <option value="Jobs">Jobs</option>
 <option value="Other">Other</option>
 </select>
 </div>

 <div className="form-group">
 <label htmlFor="notes">Additional notes</label>
 <textarea id="notes" value={props.notes} onChange={handleNotesChange} />
 </div>
</form>

对于每个字段,我们在一个div容器中呈现一个label和适当的编辑器,并使用一个form-group类来很好地分隔字段。

所有编辑器都引用处理程序来处理对其值的更改。所有编辑器也都有相应的ContactUs道具设置的值。因此,所有字段编辑器都有受控组件。

让我们仔细看看select编辑。我们使用value属性在select标记中设置值。然而,这在原生的select标记中并不存在。通常,我们必须在select标记中的相关option标记中包含selected属性:

<select id="reason">
  <option value="Marketing">Marketing</option>
  <option value="Support" selected>Support</option>
  <option value="Feedback">Feedback</option>
  <option value="Jobs">Jobs</option>
  <option value="Other">Other</option>
</select>

React 将value道具添加到select标签,并在幕后为我们管理option标签上的selected属性。这使我们能够在代码中一致地管理inputtextareaselected

  1. 现在,让我们为这些字段创建更改处理程序,这些字段调用我们先前创建的函数道具:
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
  props.onEmailChange(e.currentTarget.value);
};
const handleReasonChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
  props.onReasonChange(e.currentTarget.value);
};
const handleNotesChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
  props.onNotesChange(e.currentTarget.value);
};

这就完成了使用各种受控表单元素的基本联系我们表单。我们尚未实施任何验证或提交表单。我们将在本章后面部分讨论这些问题。

我们已经注意到,每个字段都有许多类似的代码,用于将字段更改为状态。在下一节中,我们将开始研究一个通用表单组件,并切换到将其用于我们的 Contact Us 表单。

使用通用组件减少样板代码

通用表单组件将有助于减少实现表单所需的代码量。我们将在本节中完成这项工作,重构我们在上一节中为ContactUs组件所做的工作。

让我们考虑一下,理想情况下,我们将如何使用通用组件来生产新版本的ContactUs组件。它可能类似于以下 JSX:

<Form
  defaultValues={{ name: "", email: "", reason: "Support", notes: "" }}
>
  <Form.Field name="name" label="Your name" />
  <Form.Field name="email" label="Your email address" type="Email" />
  <Form.Field name="reason" label="Reason you need to contact us" type="Select" options={["Marketing", "Support", "Feedback", "Jobs", "Other"]} />
  <Form.Field name="notes" label="Additional notes" type="TextArea" />
</Form>

在本例中,有两种通用化合物成分:FormField。以下是一些要点:

  • Form成分是化合物的容器,管理状态和相互作用。
  • 我们为Form组件上的defaultValues属性中的字段传递默认值。
  • Field组件为每个字段呈现标签和编辑器。
  • 每个字段都有一个name属性,该属性将确定字段值存储状态下的属性名称。
  • 每个字段都有一个label道具,指定要在每个字段标签中显示的文本。
  • 使用type属性指定特定字段编辑器。默认编辑器是基于文本的input
  • 如果编辑器类型为Select,那么我们可以使用options道具指定此编辑器中出现的选项。

呈现新ContactUs组件的 JSX 比原始版本要短得多,而且可以说更易于阅读。状态管理和事件处理程序被隐藏并封装在Form组件中。

创建基本表单组件

是时候开始我们的通用Form组件了:

  1. 我们先在src文件夹中创建一个名为Form.tsx的新文件,其中包含以下内容:
import * as React from "react";

interface IFormProps {}

interface IState {}

export class Form extends React.Component<IFormProps, IState> {
  constructor(props: IFormProps) {}
  public render() {}
}

Form是一个基于类的组件,因为它需要管理状态。我们将道具接口命名为IFormProps,因为稍后我们需要一个用于野外道具的接口。

  1. 让我们向IFormProps界面添加一个defaultValues道具。这将保存表单中每个字段的默认值:
export interface IValues {
 [key: string]: any;
}

interface IFormProps {
  defaultValues: IValues;
}

对于默认值类型,我们使用一个名为IValues的附加接口。这是一种可转位键/值类型,具有string类型键和any类型值。键将是字段名,值将是字段值。

因此,defaultValues道具的值可以是:

{ name: "", email: "", reason: "Support", notes: "" }
  1. 现在让我们转到Form中的状态。我们将把字段值存储在名为values的状态属性中:
interface IState {
  values: IValues;
}

请注意,这与defaultValues道具的类型相同,即IValues

  1. 现在,我们将使用构造函数中的默认值初始化状态:
constructor(props: IFormProps) {
  super(props);
  this.state = {
    values: props.defaultValues
  };
}
  1. 本节中我们要做的最后一点是开始在Form组件中实现render方法:
public render() {
 return (
 <form className="form" noValidate={true}>
 {this.props.children}
 </form>
 );
}

我们使用上一章中使用的神奇的children道具,在form标记中呈现子组件。

这很好地将我们引向Field组件,我们将在下一节中实现它。

添加基本字段组件

Field组件需要呈现标签和编辑器。它将生活在一个名为Field的静态属性中,位于Form组件中。消费者可以使用Form.Field引用此组件:

  1. 让我们先为IFormProps上方Form.tsx中的场地道具创建一个界面:
interface IFieldProps {
  name: string;
  label: string;
  type?: "Text" | "Email" | "Select" | "TextArea";
  options?: string[];
}
  • name道具是字段的名称。
  • label道具是要在字段标签中显示的文本。
  • type道具是要显示的编辑器类型。我们已经为这个道具使用了一个联合类型,其中包含我们将要支持的可用类型。请注意,我们已经将其定义为可选道具,因此稍后需要为其定义一个默认值。
  • options道具仅适用于Select编辑器类型,也是可选的。这定义了要在string数组的下拉列表中显示的选项列表。
  1. 现在,让我们在Form中为Field组件添加一个骨架静态Field属性:
public static Field: React.SFC<IFieldProps> = props => {
  return ();
};
  1. 在我们忘记之前,让我们为字段type道具添加默认值。我们对Form类外部和下方的定义如下:
Form.Field.defaultProps = {
  type: "Text"
};

因此,默认的type将是基于文本的输入。

  1. 现在,让我们尝试渲染该字段:
public static Field: React.SFC<IFieldProps> = props => {
  const { name, label, type, options } = props;
  return (
    <div className="form-group">
 <label htmlFor={name}>{label}</label>
 <input type={type.toLowerCase()} id={name} />
 </div>
  );
}
  • 我们首先从道具对象中解构namelabeltypeoptions

  • 该字段被包装在一个div容器中,该容器使用我们在index.css中已经实现的form-group类垂直分隔字段。

  • 然后在div容器内的input之前呈现label,标签的htmlFor属性引用inputid

这是一个良好的开端,但并非所有不同的字段编辑器都是输入。事实上,这只适用于类型TextEmail

  1. 因此,让我们稍微调整一下,并在输入周围包装一个条件表达式:
<label htmlFor={name}>{label}</label>
{(type === "Text" || type === "Email") && (
  <input type={type.toLowerCase()} id={name} />
)}
  1. 接下来,我们通过添加突出显示的 JSX 来处理TextArea类型:
{(type === "Text" || type === "Email") ... }

{type === "TextArea" && (
 <textarea id={name} />
)}
  1. 现在,我们可以呈现我们将要支持的最终编辑器,如下所示:
{type === "TextArea" ... } {type === "Select" && (
  <select>
    {options &&
      options.map(option => (
        <option key={option} value={option}>
          {option}
        </option>
      ))}
  </select>
)} 

我们呈现一个select标记,包含使用options数组属性中的map函数指定的选项。请注意,我们为每个选项提供了一个独特的key属性,以便在检测到选项的任何更改时保持 React 愉快。

我们现在有了基本的FormField组件,这很好。但是,该实现仍然非常无用,因为我们还没有管理处于状态的字段值。让我们在下一节讨论这个问题。

与 React 上下文共享状态

字段值的状态存在于Form组件中。但是,这些值是通过Field组件呈现和更改的。Field组件无法访问Form中的状态,因为该状态存在于Form实例中,Field不存在。

这与我们在上一章中实现的复合Tabs组件非常相似。我们使用 React 上下文在Tabs化合物中的组分之间共享状态。

在本节中,我们将对我们的Forms组件使用相同的方法:

  1. 让我们首先在Form.tsx中为表单上下文创建一个接口:
interface IFormContext {
  values: IValues;
}

上下文只包含与我们的状态具有相同类型IValues的值。

  1. 现在让我们使用React.createContextIFormContext下方创建上下文组件:
const FormContext = React.createContext<IFormContext>({
  values: {}
});

通过将初始上下文值设置为空文本值,我们可以让 TypeScript 编译器满意。

  1. Form中的render方法中,创建包含状态值的上下文值:
public render() {
  const context: IFormContext = {
 values: this.state.values
 };
  return ( ... )
}
  1. 将上下文提供程序包装在render方法的 JSX 中的form标记周围:
<FormContext.Provider value={context}>
  <form ... >
    ...
  </form>
</FormContext.Provider>
  1. 我们现在可以使用FieldSFC 中的上下文:
<FormContext.Consumer>
 {context => (
    <div className="form-group">
    </div>
 )}
</FormContext.Consumer>
  1. 现在我们可以访问上下文,让我们在所有三个编辑器中呈现上下文中的值:
<div className="form-group">
  <label htmlFor={name}>{label}</label>
  {(type === "Text" || type === "Email") && (
    <input type={type.toLowerCase()} id={name} value={context.values[name]} />
  )}

  {type === "TextArea" && (
    <textarea id={name} value={context.values[name]} />
  )}

  {type === "Select" && (
    <select value={context.values[name]}>
    ...
    </select>
  )}
</div>

TypeScript 编译器现在对我们的FormField组件很满意。因此,我们可以开始新的ContactUs实现。

但是,用户还不能在表单中输入任何内容,因为我们不处理更改并向 state 传递新值。我们现在需要实现更改处理程序。

  1. 让我们先在Form类中创建一个setValue方法:
private setValue = (fieldName: string, value: any) => {
  const newValues = { ...this.state.values, [fieldName]: value };
  this.setState({ values: newValues });
};

以下是该方法的要点:

  • 此方法接受字段名和新值作为参数。
  • values对象的新状态是使用一个名为newValues的新对象创建的,它传播状态中的旧值,然后添加新字段名和值。
  • 然后在状态中设置新值。
  1. 然后,我们在表单上下文中创建对该方法的引用,以便Field组件可以访问它。让我们首先将其添加到表单上下文接口:
interface IFormContext {
  values: IValues;
  setValue?: (fieldName: string, value: any) => void;
}

我们将该属性设置为可选,以便在创建表单上下文组件时使 TypeScript 编译器满意。

  1. 当创建上下文值时,我们可以在Form中创建对setValue方法的引用:
const context: IFormContext = {
  setValue: this.setValue,
  values: this.state.values
};
  1. 我们现在可以从Field组件调用此方法。在Field中,在分解props对象之后,让我们创建一个调用setValue方法的变更处理程序:
const { name, label, type, options } = props;

const handleChange = (
 e:
 | React.ChangeEvent<HTMLInputElement>
 | React.ChangeEvent<HTMLTextAreaElement>
 | React.ChangeEvent<HTMLSelectElement>,
 context: IFormContext
) => {
 if (context.setValue) {
 context.setValue(props.name, e.currentTarget.value);
 }
};

让我们看看这个方法的要点:

  • TypeScript 更改事件类型为ChangeEvent<T>,其中T是正在处理的元素的类型。
  • 处理程序的第一个参数e是 React change 事件处理程序参数。我们为不同的编辑器合并了所有不同的更改处理程序类型,以便可以在单个函数中处理所有更改。
  • 处理程序的第二个参数是表单上下文。
  • 我们需要一个条件语句来检查setValue方法不是undefined,以使 TypeScript 编译器满意。
  • 然后,我们可以使用字段名和新值调用setValue方法。
  1. 然后我们可以在input标记中引用此更改处理程序,如下所示:
<input 
  type={type.toLowerCase()} 
  id={name} 
  value={context.values[name]}
  onChange={e => handleChange(e, context)} 
/>

请注意,我们使用 lamda 函数,以便可以将上下文值传递给handleChange

  1. 我们可以在textarea标签中执行相同的操作:
<textarea 
  id={name} 
  value={context.values[name]} 
  onChange={e => handleChange(e, context)} 
/>
  1. 我们也可以在select标签中这样做:
<select 
 value={context.values[name]}
 onChange={e => handleChange(e, context)} 
>
 ...
</select>

因此,我们的FormField组件现在可以很好地协同工作,呈现字段并管理它们的值。在下一节中,我们将通过实现一个新的ContactUs组件来尝试我们的通用组件。

实现新的 ContactUs 组件

在本节中,我们将使用我们的FormField组件实现一个新的ContactUs组件:

  1. 让我们从移除ContactUs.tsx中的道具界面开始。
  2. ContactUs证监会内的内容将与原始版本大不相同。让我们从删除内容开始,如下所示:
const ContactUs: React.SFC = () => {
  return ();
};
  1. 让我们将我们的Form组件导入ContactUs.tsx
import { Form } from "./Form";
  1. 我们现在可以引用Form组件,传递一些默认值:
return (
  <Form
 defaultValues={{ name: "", email: "", reason: "Support", notes: "" }}
 >
 </Form>
);
  1. 让我们添加name字段:
<Form
  defaultValues={{ name: "", email: "", reason: "Support", notes: "" }}
>
  <Form.Field name="name" label="Your name" />
</Form>

注意我们没有传递type属性,因为这将默认为基于文本的输入,这正是我们所需要的。

  1. 现在我们添加emailreasonnotes字段:
<Form
  defaultValues={{ name: "", email: "", reason: "Support", notes: "" }}
>
  <Form.Field name="name" label="Your name" />
  <Form.Field name="email" label="Your email address" type="Email" />
 <Form.Field
 name="reason"
 label="Reason you need to contact us"
 type="Select"
 options={["Marketing", "Support", "Feedback", "Jobs", "Other"]}
 />
 <Form.Field name="notes" label="Additional notes" type="TextArea" />
</Form>
  1. ContactUsPage现在要简单得多。它不会包含任何状态,因为它现在在Form组件中进行管理。我们也不需要向ContactUs组件传递任何道具:
class ContactUsPage extends React.Component<{}, {}> {
  public render() {
    return (
      <div className="page-container">
        <h1>Contact Us</h1>
        <p>
          If you enter your details we'll get back to you as soon as we can.
        </p>
        <ContactUs />
      </div>
    );
  }
}

如果我们转到 running 应用程序并转到 Contact Us 页面,它会根据需要呈现并接受我们输入的值。

我们的通用表单组件进展顺利,我们使用它实现了我们所希望的ContactUs组件。在下一节中,我们将通过添加验证进一步改进通用组件。

验证表单

在表单上包含验证可以改善用户体验,方法是立即向用户反馈输入的信息是否有效。在本节中,我们将向Form组件添加验证,然后在ContactUs组件中使用它。

我们将在ContactUs组件中实施的验证规则如下:

  • 应填充名称和电子邮件字段
  • 名称字段应至少包含两个字符

我们将在字段编辑器失去焦点时执行验证规则。

在下一节中,我们将向Form组件添加一个道具,允许使用者指定验证规则。

将验证规则道具添加到表单

让我们考虑一下如何为表单指定验证规则。我们需要能够为一个字段指定一个或多个规则。某些规则可能有一个参数,例如最小长度。如果我们可以指定规则就好了,如下面的示例所示:

<Form
  ...
  validationRules={{
 email: { validator: required },
 name: [{ validator: required }, { validator: minLength, arg: 3 }]
 }}
>
  ...
</Form>

让我们试着在Form组件上实现validationRules道具:

  1. 首先在Form.tsx中定义Validator函数的类型:
export type Validator = (
  fieldName: string,
  values: IValues,
  args?: any
) => string;

Validator函数将接受字段名、整个表单的值以及特定于该函数的可选参数。将返回包含验证错误消息的字符串。如果字段有效,将返回一个空白字符串。

  1. 让我们使用此类型创建一个Validator函数,以检查Validator类型下名为required的字段是否已填充:
export const required: Validator = (
  fieldName: string,
  values: IValues,
  args?: any
): string =>
  values[fieldName] === undefined ||
  values[fieldName] === null ||
  values[fieldName] === ""
    ? "This must be populated"
    : "";

我们导出该函数,以便在以后的ContactUs实现中使用。该函数检查字段值是undefinednull还是空字符串,如果是,则返回此必须填充的验证错误消息。

如果字段值不是undefinednull或空字符串,则返回空字符串以指示该值有效。

  1. 类似地,让我们创建一个Validator函数,用于检查字段输入是否超过最小长度:
export const minLength: Validator = (
  fieldName: string,
  values: IValues,
  length: number
): string =>
  values[fieldName] && values[fieldName].length < length
    ? `This must be at least ${length} characters`
    : "";

该函数检查字段值的长度是否小于 length 参数,如果小于,则返回验证错误消息。否则,将返回一个空字符串以指示该值有效。

  1. 现在,让我们添加通过道具向Form组件传递验证规则的功能:
interface IValidation {
 validator: Validator;
 arg?: any;
}

interface IValidationProp {
 [key: string]: IValidation | IValidation[];
}

interface IFormProps {
  defaultValues: IValues;
  validationRules: IValidationProp;
}
  • validationRules道具为可索引键/值类型,键为字段名,值为IValidation类型的一条或多条验证规则。
  • 验证规则包含类型为Validator的验证函数和要传递到验证函数的参数。
  1. 有了新的validationRules道具,让我们将其添加到ContactUs组件中。首先导入验证程序函数:
import { Form, minLength, required } from "./Form";
  1. 现在,让我们将验证规则添加到ContactUs组件 JSX 中:
<Form
  defaultValues={{ name: "", email: "", reason: "Support", notes: "" }}
  validationRules={{
 email: { validator: required },
 name: [{ validator: required }, { validator: minLength, arg: 2 }]
 }}
>
  ...
</Form>

现在,如果填写了姓名和电子邮件,并且姓名至少有两个字符长,则我们的表单是有效的。

那是道具完成了。在下一节中,我们将跟踪验证错误消息,以便在页面上呈现它们。

跟踪验证错误消息

我们需要在用户完成表单和字段变为有效或无效时跟踪状态中的验证错误消息。稍后,我们将能够将错误消息呈现到屏幕上。

Form组件负责管理所有表单状态,因此我们将在其中添加错误消息状态,如下所示:

  1. 让我们将验证错误消息状态添加到表单状态界面:
interface IErrors {
 [key: string]: string[];
}

interface IState {
  values: IValues;
  errors: IErrors;
}

errors状态为可索引键/值类型,其中键为字段名,值为验证错误消息数组。

  1. 让我们初始化构造函数中的errors状态:
constructor(props: IFormProps) {
  super(props);
  const errors: IErrors = {};
 Object.keys(props.defaultValues).forEach(fieldName => {
 errors[fieldName] = [];
 });
  this.state = {
    errors,
    values: props.defaultValues
  };
}

defaultValues属性包含其键中的所有字段名。我们遍历defaultValues键,将适当的errors键设置为空数组。因此,当Form组件初始化时,所有字段都不包含任何验证错误消息,这正是我们想要的。

  1. Field组件最终将呈现验证错误消息,因此我们需要将这些消息添加到表单上下文中。让我们首先将这些添加到表单上下文界面:
interface IFormContext {
 errors: IErrors;  values: IValues;
  setValue?: (fieldName: string, value: any) => void;
}
  1. 在创建上下文时,让我们添加一个errors空文本作为默认值。这是为了让 TypeScript 编译器满意:
const FormContext = React.createContext<IFormContext>({
  errors: {},
  values: {}
});
  1. 我们现在可以在上下文值中包含错误:
public render() {
  const context: IFormContext = {
    errors: this.state.errors,
    setValue: this.setValue,
    values: this.state.values
  };
  return (
    ...
  );
}

现在,验证错误处于表单状态,也处于Field组件要访问的表单上下文中。在下一节中,我们将创建一个将调用验证规则的方法。

调用验证规则

到目前为止,我们可以定义验证规则,并拥有跟踪验证错误消息的状态,但还没有任何东西调用这些规则。这是我们将在本节中实施的内容:

  1. 我们需要在Form组件中创建一个方法来验证一个字段,调用指定的验证器函数。让我们创建一个名为validate的方法,它接受字段名及其值。该方法将返回一组验证错误消息:
private validate = (
  fieldName: string,
  value: any
): string[] => {

};
  1. 让我们获取字段的验证规则并初始化一个errors数组。我们将在执行验证程序时收集errors数组中的所有错误。我们还将在所有验证器执行完毕后返回errors数组:
private validate = ( 
  fieldName: string,
  value: any
): string[] => {
  const rules = this.props.validationRules[fieldName];
 const errors: string[] = [];

  // TODO - execute all the validators

  return errors;
}
  1. 规则可以是一个IValidation数组,也可以只是一个IValidation。如果我们只有一个验证规则,那么让我们检查一下并调用validator函数:
const errors: string[] = [];
if (Array.isArray(rules)) {
 // TODO - execute all the validators in the array of rules
} else {
  if (rules) {
    const error = rules.validator(fieldName, this.state.values, rules.arg);
    if (error) {
      errors.push(error);
    }
  }
}
return errors;
  1. 现在让我们来讨论当存在多个验证规则时的代码分支。我们可以使用规则数组上的forEach函数来迭代规则并执行validator函数:
if (Array.isArray(rules)) {
  rules.forEach(rule => {
 const error = rule.validator(
 fieldName,
 this.state.values,
 rule.arg
 );
 if (error) {
 errors.push(error);
 }
 });
} else {
  ...
}
return errors;
  1. 我们需要在validate方法中实现的最后一位代码是设置新的errors表单状态:
if (Array.isArray(rules)) {
 ...
} else {
 ...
}
const newErrors = { ...this.state.errors, [fieldName]: errors };
this.setState({ errors: newErrors });
return errors;

我们将旧错误状态扩展到新对象中,然后为字段添加新错误。

  1. Field组件需要调用此validate方法。我们将在表单上下文中添加对该方法的引用。我们先把它添加到IFormContext界面:
interface IFormContext {
  values: IValues;
  errors: IErrors;
  setValue?: (fieldName: string, value: any) => void;
  validate?: (fieldName: string, value: any) => void;
}
  1. 现在我们可以将其添加到Formrender方法的上下文值中:
public render() {
  const context: IFormContext = {
    errors: this.state.errors,
    setValue: this.setValue,
    validate: this.validate,
    values: this.state.values
  };
  return (
    ...
  );
}

我们的表单验证进展顺利,现在我们有了一个可以调用的方法来调用字段的所有规则。但是,在用户填写表单时,还没有从任何地方调用此方法。我们将在下一节中这样做。

从字段触发验证规则执行

当用户填写表单时,我们希望在字段失去焦点时触发验证规则。我们将在本节中实现这一点:

  1. 让我们创建一个函数来处理三个不同编辑器的blur事件:
const handleChange = (
  ...
};

const handleBlur = (
 e:
 | React.FocusEvent<HTMLInputElement>
 | React.FocusEvent<HTMLTextAreaElement>
 | React.FocusEvent<HTMLSelectElement>,
 context: IFormContext
) => {
 if (context.validate) {
 context.validate(props.name, e.currentTarget.value);
 }
};

return ( ... )
  • TypeScript 模糊事件类型为FocusEvent<T>,其中T是正在处理的元素的类型。
  • 处理程序的第一个参数e是 React blur 事件处理程序参数。我们为不同的编辑器合并了所有不同的处理程序类型,因此我们可以在单个函数中处理所有模糊事件。
  • 处理程序的第二个参数是表单上下文。
  • 我们需要一个条件语句来检查validate方法不是undefined,以使 TypeScript 编译器满意。
  • 然后,我们可以使用需要验证的字段名和新值调用validate方法。
  1. 我们现在可以在文本和电子邮件编辑器的FieldJSX 中引用此处理程序:
{(type === "Text" || type === "Email") && (
  <input
    type={type.toLowerCase()}
    id={name}
    value={context.values[name]}
    onChange={e => handleChange(e, context)}
    onBlur={e => handleBlur(e, context)}
  />
)}

我们将onBlur属性设置为一个 lamda 表达式,该表达式调用我们的handleBlur函数,传递 blur 参数和上下文值。

  1. 现在让我们在其他两个编辑器中引用处理程序:
{type === "TextArea" && (
  <textarea
    id={name}
    value={context.values[name]}
    onChange={e => handleChange(e, context)}
    onBlur={e => handleBlur(e, context)}
  />
)}
{type === "Select" && (
  <select
    value={context.values[name]}
    onChange={e => handleChange(e, context)}
    onBlur={e => handleBlur(e, context)}
  >
    ...
  </select>
)}

我们的字段在失去焦点时正在执行验证规则。在尝试“联系我们”页面之前,还有一项任务要做,我们将在下一节中完成。

呈现验证错误消息

在本节中,我们将在Field组件中呈现验证错误消息:

  1. 让我们用我们已经实现的form-errorCSS 类显示span中的所有错误。我们在form-groupdiv容器底部显示:
<div className="form-group">
  <label htmlFor={name}>{label}</label>
  {(type === "Text" || type === "Email") && (
    ...
  )}
  {type === "TextArea" && (
    ...
  )}
  {type === "Select" && (
    ...
  )}
  {context.errors[name] &&
 context.errors[name].length > 0 &&
 context.errors[name].map(error => (
 <span key={error} className="form-error">
 {error}
 </span>
 ))}
</div>

因此,我们首先检查字段名是否有错误,然后使用errors数组中的map函数为每个错误呈现一个span

  1. 我们已经引用了一个 CSSform-error类,所以让我们将其添加到index.css中:
.form-error {
  font-size: 13px;
  color: red;
  margin: 3px auto 0px 0px;
}

是时候尝试一下“联系我们”页面了。如果我们的应用程序未启动,请使用npm start启动,然后转到“联系我们”页面。如果我们在“名称”和“电子邮件”字段中使用 tab 键,将显示所需的验证规则触发器和错误消息:

这正是我们想要的。如果我们返回到 name 字段,在切换到 Tab 之前尝试只输入一个字符,就会触发最小长度验证错误,正如我们所预期的:

我们的通用表单组件现在几乎完成了。我们的最后一项任务是提交表单,我们将在下一节中完成。

提交表格

提交表单是表单实现的最后一部分。Form组件的使用者将处理实际提交,这可能导致调用 web API。提交表单时,我们的Form组件只需调用消费者代码中的函数。

向表单中添加提交按钮

在本节中,我们将在Form组件中添加一个提交按钮:

  1. 让我们在FormJSX 中添加一个提交按钮,包裹在form-group内的div容器中:
<FormContext.Provider value={context}>
  <form className="form" noValidate={true}>
    {this.props.children}
    <div className="form-group">
 <button type="submit">Submit</button>
 </div>
  </form>
</FormContext.Provider>
  1. index.css中使用以下 CSS 设置按钮样式:
.form-group button {
  font-size: 16px;
  padding: 8px 5px;
  width: 80px;
  border: black solid 1px;
  border-radius: 5px;
  background-color: black;
  color: white;
}
.form-group button:disabled {
  border: gray solid 1px;
  background-color: gray;
  cursor: not-allowed;
}

我们现在在表单上有一个黑色的 submit 按钮,禁用时为灰色。

添加 onSubmit 表单道具

在我们的Form组件中,我们需要一个新的道具,允许使用者指定要调用的submit函数。我们将在本节中执行此操作:

  1. 我们先在Form道具界面中创建一个名为onSubmit的新道具函数:
export interface ISubmitResult {
 success: boolean;
 errors?: IErrors;
}

interface IFormProps {
  defaultValues: IValues;
  validationRules: IValidationProp;
  onSubmit: (values: IValues) => Promise<ISubmitResult>;
}

该函数将接受字段值并异步返回提交是否成功,以及服务器上发生的任何验证错误。

  1. 我们将跟踪表单是否处于Form状态。我们还将跟踪表单是否已在Form状态下成功提交:
interface IState {
  values: IValues;
  errors: IErrors;
  submitting: boolean;
 submitted: boolean;
}
  1. 让我们在构造函数中初始化这些状态值:
constructor(props: IFormProps) {
  ...
  this.state = {
    errors,
    submitted: false,
 submitting: false,
    values: props.defaultValues
  };
}
  1. 如果表单正在提交或已成功提交,我们现在可以禁用提交按钮:
<button
  type="submit"
  disabled={this.state.submitting || this.state.submitted}
>
  Submit
</button>
  1. 让我们在form标记中引用一个提交处理程序:
<form className="form" noValidate={true} onSubmit={this.handleSubmit}>
  ...
</form>
  1. 现在,我们可以开始实现刚才引用的提交处理程序:
private handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault();

};

我们在 submit 事件参数中调用preventDefault,以停止浏览器自动发布表单。

  1. 在开始表单提交过程之前,我们需要确保所有字段都有效。让我们引用并创建一个validateForm函数,它可以执行以下操作:
private validateForm(): boolean {
 const errors: IErrors = {};
 let haveError: boolean = false;
 Object.keys(this.props.defaultValues).map(fieldName => {
 errors[fieldName] = this.validate(
 fieldName,
 this.state.values[fieldName]
 );
 if (errors[fieldName].length > 0) {
 haveError = true;
 }
 });
 this.setState({ errors });
 return !haveError;
}

private handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault();
  if (this.validateForm()) {

 }
};

validateForm函数遍历字段,调用已经实现的validate函数。状态更新为最新的验证错误,我们返回任何字段中是否存在任何错误。

  1. 现在让我们实现提交处理程序的其余部分:
private handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
  e.preventDefault();
  if (this.validateForm()) {
    this.setState({ submitting: true });
 const result = await this.props.onSubmit(this.state.values);
 this.setState({
 errors: result.errors || {},
 submitted: result.success,
 submitting: false
 });
  }
};

如果表单有效,我们首先将submitting状态设置为true。然后我们异步调用onSubmitprop 函数。当onSubmitprop 函数完成后,我们将函数中的任何验证错误设置为状态,以及提交是否成功。我们还将提交过程已完成这一事实设置为状态。

现在,我们的Form组件有一个onSubmit函数。在下一节中,我们将在“联系我们”页面中使用它。

使用 onSubmit 表单道具

在本节中,我们将使用ContactUs组件中的onSubmit表单道具。ContactUs组件不会管理提交,它只会委托ContactUsPage组件处理提交:

  1. 我们先导入ISubmitResultIValues,在ContactUs组件中为onSubmit功能创建一个道具界面:
import { Form, ISubmitResult, IValues, minLength, required } from "./Form";

interface IProps {
 onSubmit: (values: IValues) => Promise<ISubmitResult>;
} const ContactUs: React.SFC<IProps> = props => { ... }
  1. 创建一个调用onSubmit属性的handleSubmit函数:
const ContactUs: React.SFC<IProps> = props => {
  const handleSubmit = async (values: IValues): Promise<ISubmitResult> => {
 const result = await props.onSubmit(values);
 return result;
 };
  return ( ... );
};

onSubmitprop 是异步的,所以我们需要用async作为函数的前缀,用await作为onSubmit调用的前缀。

  1. 在 JSX 中以onSubmitprop 的形式绑定此提交处理程序:
return (
  <Form ... onSubmit={handleSubmit}>
    ...
  </Form>
);
  1. 现在让我们转到ContactUsPage组件。让我们从创建提交处理程序开始:
private handleSubmit = async (values: IValues): Promise<ISubmitResult> => {
  await wait(1000); // simulate asynchronous web API call
  return {
    errors: {
      email: ["Some is wrong with this"]
    },
    success: false
  };
};

实际上,这可能会调用 web API。在我们的示例中,我们异步等待一秒钟,并返回带有email字段的验证错误。

  1. 让我们创建刚才引用的wait函数:
const wait = (ms: number): Promise<void> => {
 return new Promise(resolve => setTimeout(resolve, ms));
};
  1. 现在让我们将handleSubmit方法连接到ContactUs``onSubmit道具:
<ContactUs onSubmit={this.handleSubmit} />
  1. 我们已经引用了IValuesISubmitResult,所以让我们导入这些:
import { ISubmitResult, IValues } from "./Form";

如果我们进入 running 应用程序中的“联系我们”页面,填写表格,然后单击“提交”按钮,我们会被告知电子邮件字段存在问题,正如我们所预期的:

  1. 让我们更改ContactUsPage中的提交处理程序,以返回一个成功的结果:
private handleSubmit = async (values: IValues): Promise<ISubmitResult> => {
  await wait(1000); // simulate asynchronous web API call
 return {
 success: true
 };
};

现在,如果我们再次进入 running 应用程序中的“联系我们”页面,填写表单,然后单击“提交”按钮,则提交将完成,并且“提交”按钮将被禁用:

这就是我们完整的联系我们页面,以及我们的通用FormField组件。

总结

在本章中,我们讨论了控制组件,这是 React 推荐的处理表单数据输入的方法。对于受控组件,我们可以通过组件状态来反应控制输入值。

我们着眼于构建包含状态和更改处理程序的通用FormField组件,这样我们就不需要为应用程序中的每个表单中的每个字段实现单独的状态和更改处理程序。

然后,我们创建了一些标准验证函数,并添加了在通用Form组件中添加验证规则的功能,以及在Field组件中自动呈现验证错误的功能。

最后,我们添加了在使用通用Form组件时处理表单提交的功能。我们的联系我们页面更改为使用通用FormField组件。

我们的通用组件只处理非常简单的表单。毫不奇怪,已经有相当数量的成熟表单库出现在野外。一个流行的选择是 Formik,它在某些方面与我们刚刚构建的类似,但功能更强大。

如果您正在构建一个包含大量表单的应用程序,那么您可以像我们刚才所做的那样构建一个通用表单,或者使用一个已建立的库(如 Formik)来加快开发过程。

问题

通过尝试以下实现,检查 React 和 TypeScript 中有关表单的所有信息是否都已被卡住:

  1. 使用本机数字输入扩展我们的通用Field组件以包括数字编辑器。

  2. 在“联系我们”表单上设置一个紧急字段,以指示响应的紧急程度。该字段应为数字。

  3. 在泛型Form组件中实现一个新的验证器函数,用于验证一个数字是否介于其他两个数字之间。

  4. 在“紧急程度”字段上实施验证规则,以确保输入是介于 1 和 10 之间的数字。

  5. 当用户在没有键入任何内容的情况下单击某个字段时,就会触发我们的验证。当一个字段失去焦点,但仅当它被更改时,我们如何触发验证?

进一步阅读

以下链接是 React 中表单的详细信息来源: