creating a form with controlled components
Reducing boilerplate code with generic components
Validating forms
Form submission
表单是大部分应用的常见内容。在React中,创建表单的标准方式是使用被称为 controlled component 的组件。
Adding a Contact Us page
在src目录添加一个新的文件ContactUsPage.tsx
,内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 ;
该组件需要包含状态,目前首先创建了header相关信息。接下来,导入该组件到页面中,打开Routes.tsx
,
1 import ContactUsPage from "./ContactUsPage" ;
在Routes
组件的render
方法中,添加新的路由,
1 2 3 4 5 6 7 8 9 10 11 <Switch > <Redirect exact ={true} from ="/" to ="/products" /> <Route path ="/products/:id" component ="{ProductPage} /> <Route exact={true} path=" /products " compoent ={ProductsPage} /> <Route path ="contactus" component ={ContactUsPage} /> <Route path ="/admin" > ... </Route > <Route path ="/login" compoent ={LoginPage} /> <Route component ={NotFoundPage} /> </Switch >
打开Header.tsx
,添加新的导航信息,
1 2 3 4 5 6 7 8 9 10 11 <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>
现在,页面已经创建了,下面创建表单输入框。
在src目录下创建一个新文件ContactUs.tsx
,包含下面内容,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 ;
这是一个功能组件,渲染一个表单包含label和用户名的输入框。
现在需要添加对应的css样式,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 .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, Helvetica, sans-serif; font-size : 16px ; padding : 5px ; border : lightgray solid 1px ; border-radius : 5px ; }
现在在我们页面ContactUsPage.tsx
添加并渲染表单,
1 import ContactUs from "./ContactUs" ;
在render
方法中添加,
1 2 3 4 5 <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>
表单已经创建好了,但需要创建一个状态类型到ContactUsPage
页面中,
1 2 3 4 5 6 7 8 interface IState { name : string ; email : string ; reason : string ; notes : string ; } class ContactUsPage extends React.Component <{}, IState > { ... }
在构造器中初始化状态,
1 2 3 4 5 6 7 8 9 public constructor (props: {} ) { super (props); this .state = { email : "" , name : "" , notes : "" , reason : "" }; }
我们需要将ContactUsPage
中的状态传递到ContactUs
组件中。在ContactUs
组件中,
1 2 3 4 5 6 7 8 interface IProps { name : string ; email : string ; reason : string ; notes : string ; } const ContactUs : React .SFC <IProps > = props => { ... }
将表单名name绑定到name
属性中,
1 2 3 4 <div className="form-group" > <label htmlFor ="name" > Your name</label > <input type ="text" id ="name" value ={props.name} /> </div>
将表单状态传递给ContactUsPage
,
1 2 3 4 5 6 <ContactUs name={this .state .name } emial={this .state .email } reason={this .state .reason } notes={this .state .notes } />
添加事件监听,
1 <input type ="text" id="name" value={props.name } onChange={handleNameChange} />
创建对应的handler,
1 2 3 4 5 6 const ContactUs : React .SFC <IProps > = props => { const handleNameChange = (e: React.ChangeEvent<HTMLInputElement> ) => { props.onNameChange (e.currentTarget .value ); }; return ( ... ); };
这是使用到了React.ChangeEvent
。我们需要添加onNameChange
函数到IProps中,
1 2 3 4 5 6 7 8 9 10 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 ; }
现在需要将来自ContactUsPage
的Props传递到ContactUs
中,
1 2 3 4 5 6 7 8 9 10 <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 } }
接下来创建对应的handlers方法,
1 2 3 4 5 6 7 8 9 10 11 12 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 }); };
接下来在ContactUs
中补充其它表单内容,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 <form className="form" noValidate={true }> <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" 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" selected ={true} > 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>
现在创建这些handler的函数属性
1 2 3 4 5 6 7 8 9 10 11 12 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 ); };
Reducing boilerplate code with generic components
通用表单组件将有利于减少表单代码的重复实现。我们重构上面的ContactUs
组件来实现generic form components。
假设我们希望,理想情况下消费组件ContactUs
内容的generic component组件的形式如下,
1 2 3 4 5 6 7 8 9 10 <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 >
在这个示例中,有两个通用合成组件:Form
和Field
。它们有某些特点,
Form
组件是用于合成、管理状态和交互的。
Fomr
组件使用defaultValues
属性来传递默认值。
Field
组件渲染label和每个字段的一个编辑器。
每个字段包含一个name
属性,并被存储在对应的state属性名内。
每个字段有一个label
属性用于展示每个字段的标签。
特殊字段用type
属性标识。默认的属性为文本类型input
。
如果编辑器类型是Select
,我们可以通过options
属性指定。
新版本的ContactUs
组件相比原来的更简短、更易用。状态的管理和事件的处理被隐藏和封装在Form
组件内。
下面开始构建我们的通用Form
组件;
在src
文件夹下创建一个新的文件Form.tsx
,包含下面内容:
1 2 3 4 5 6 7 8 9 10 import * as React from "react" ;interface IFormProps {}interface IState {}export class Form extends React.Component <IFormProps , IState > { constructor (props: IFormProps ) {} public render ( ) {} }
Form
是一个基类组件,因为它需要管理状态。我们将属性接口命名为IFormProps
,因为之后我们将需要一个字段属性的接口。
添加一个defaultValues
属性到IFormProps
接口中,它为每个字段提供默认值,
1 2 3 4 5 6 7 export interface IValues { [key : string ]: any ; } interface IFormProps { defaultValues : IValues ; }
我们使用一个额外的接口IValues
,它是一个索引的key/value类型[key: string]: any
,key是字段名,value是字段值。
因此,defaultValues
属性可以是,
1 { name: "" , email: "" , reason: "Support" , notes: "" }
对于表单的state,需要存储这个接口类型,
1 2 3 interface IState { values : IValues ; }
接下来需要在构造方法中初始化组件的状态,
1 2 3 4 5 6 constructor (props: IFormProps ) { super (props); this .state = { values : props.defaultValues }; }
最后一步,实现render
方法,
1 2 3 4 5 6 7 public render ( ) { return ( <form className ="form" noValidate ={true} > {this.props.children} </form > ); }
我们在form
标签中渲染子组件,使用了children
属性。
接下来,我们需要实现我们的Field
组件。
Adding a basic Field component
Field
组件需要渲染一个标签(label)和一个编辑框(editor)。
首先在Form.tsx
中创建一个接口属性,
1 2 3 4 5 6 interface IFieldProps { name : string ; label : string ; type ?: "Text" | "Email" | "Select" | "TextArea" ; options?: string []; }
name
表示字段名
label
是展示标签
type
输入类型,可选
options
,仅作用于当type
是Select
时,可选
现在添加Field
属性字段的骨架,
1 2 3 public static Field : React .SFC <IFieldProps > = props => { return (); };
另外,首先添加type
字段的默认属性,
1 2 3 Form .Field .defaultProps = { type : "Text" };
这样,默认的type
是一个文本类型,
现在,渲染它的内容,
1 2 3 4 5 6 7 8 9 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 > ); }
这虽然是好的开头,但是,仅使用Text
和Email
类型。
因此,需要添加合适的条件进行渲染,
1 2 3 4 5 6 7 8 9 10 11 12 {type === "TextArea" ... } {type === "Select" && ( <select > {options && options.map(option => ( <option key ={option} value ={option} > {option} </option > ))} </select > )}
Sharing state with React context
Form
组件内的字段值状态,需要在Field
组件内共享,即可以通过Field
组件访问和修改。
首先在Form.tsx
创建一个接口,
1 2 3 interface IFormContext { values : IValues ; }
在IFormContext
下使用React.createContext
创建一个上下文创建(context component),
1 2 3 const FormContext = React .createContext <IFormContext >({ values : {} });
在Form
的render
方法中,创建包含上下文的值,
1 2 3 4 5 6 public render ( ) { const context : IFormContext = { values : this .state .values }; return ( ... ) }
包装表单标签,
1 2 3 4 5 <FormContext .Provider value={context}> <form ... > ... </form > </FormContext .Provider >
在Field
上下文进行消费,
1 2 3 4 5 6 <FormContext .Consumer > {context => ( <div className ="form-group" > </div > )} </FormContext .Consumer >
现在可以访问这些上下文了,下面补充剩余的输入框,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <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>
目前还没有添加事件用于更新上下文的状态,需要实现相应的事件处理机制。
在Form
类中创建一个setValue
方法,
1 2 3 4 private setValue = (fieldName: string , value: any ) => { const newValues = { ...this .state .values , [fieldName]: value }; this .setState ({ values : newValues }); };
该方法包含有:
该方法接收fieldName和value作为参数。
状态被更新为newValues
,旧的值被更新,没有则添加。
新值被更新了。
接下来在Field
组件中创建该方法的一个上下文属性,以实现访问,
1 2 3 4 interface IFormContext { values : IValues ; setValue?: (fieldName: string , value: any ) => void ; }
对应地,在Form
组件也创建一个上下文属性,
1 2 3 4 const context : IFormContext = { setValue : this .setValue , values : this .state .values };
现在可以在Field
组件中访问该方法了。在Field
中,即在解构(destucture)对象props后面,创建对应的事件Hnadler,
1 2 3 4 5 6 7 8 9 10 11 12 13 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
,对应事件类型,组合(union)了所有不同的输入框事件,方便对事件进行统一处理。
该方法的第二个参数是表单上线文。
方法体内加入了条件语句,以确保编译顺利。
调用setValue
方法更新或添加新值。
现在可以为input
输入框添加这个事件处理,
1 2 3 4 5 6 <input type ={type .toLowerCase () } id={name} value={context.values [name] } onChange={e => handleChange (e, context) } />
对于textarea
标签,
1 2 3 4 5 <textarea id={name} value={context.values [name]} onChange={e => handleChange (e, context) } />
对于select
标签,
1 2 3 4 5 6 <select value={context.values [name] } onChange={e => handleChange (e, context) } > ... </select>
现在,我们的Form
和Field
组件可以组合在一起工作了。
接下来,我们使用Form
和Field
重新实现我们的ContactUs
组件。
首先删除ContactUs.tsx
中的props,
重新定义ContactUs
的SFC,
1 2 3 const ContactUs : React .SFC = () => { return (); };
在ContactUs.tsx
中导入Form
组件,
1 import { Form } from "./Form" ;
现在引用Form
组件,带上默认值,
1 2 3 4 return ( <Form defaultValues ={{ name: "", email: "", reason: "Support ", notes: "" }} </Form > );
添加name
输入框,
1 2 3 <Form defaultValues={{ name : "" , email : "" , reason : "Support" , notes : "" }} <Form .Field name="name" label="Your name" /> </Form >
注意这个没有写type
属性,则默认使用text
填充,
下面补充email
,reason
和notes
字段,
1 2 3 4 5 6 7 8 9 10 11 <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 >
接下来的ContactUsPage
就变得简单了。它不需要包含任何状态(state),因为状态的维护已经交由Form
组件管理。我们也不需要传递任何属性(props)到ContactUs
组件中,
1 2 3 4 5 6 7 8 9 10 11 12 13 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 > ); } }
目前为止这个通用组件变得易用并减少我们的重复代码,但还需添加表单验证的实现。
为了提升用户体验,需要在表单中实现校验功能。
在ContactUs
组件中我们需要实现的校验规则是:
name和email应该被填充
name字段不少于2个字符
首先思考如何在表单中指定校验规则。我们需要为一个字段指定一个或多个规则。某些规则可能会有参数,例如最小长度。以如下形式,
1 2 3 4 5 6 7 8 9 <Form ... validationRules={{ email : { validator : required }, name : [{ validator : required }, { validator : minLength, arg : 3 }] }} > ... </Form >
首先在Form
组件实现一个validationRules
属性,
在Form.tsx
中定义一个Validator
函数:
1 2 3 4 5 export type Validator = ( fieldName: string , values: IValues, args?: any ) => string ;
一个Validator
函数包含字段名、值、以及一个可选参数,并返回string的字符串消息。如果输入内容合法,则返回空字符串。
下面使用该类型创建一个required
函数,
1 2 3 4 5 6 7 8 9 10 export const required : Validator = ( fieldName : string , values : IValues , args?: any ): string => values[fieldName] === undefined || values[fieldName] === null || values[fieldName] === "" ? "This must be populated" : "" ;
这里需要将这个函数对外暴露使用。该函数会检测字段值是undefined
、null
还是空字符串,如果是则返回This must be populated
的错误信息。
类似地,创建一个长度判断的函数,
1 2 3 4 5 6 7 export const minLength : Validator = ( fieldName : string , values : IValues , length : number ): string => values[fieldName] && values[fieldName].length < length ? `This must be at least ${length} characters` : "" ;
接下来需要创建传递这些规则的props,
1 2 3 4 5 6 7 8 9 10 11 12 13 interface IValidation { validator : Validator ; arg?: any ; } interface IValidationProp { [key : string ]: IValidation | IValidation []; } interface IFormProps { defaultValues : IValues ; validationRules : IValidationProp ; }
validationRules
是一个索引key/value类型,其中key是字段名,value是一个或多个验证规则。
一个校验规则包含函数类型和一个参数。
有了validationRules
后,在ContactUs
中添加,
1 import { Form , minLenght, requied } from "./Forma" ;
现在,添加校验规则到ContactUs
组件中,
1 2 3 4 5 6 7 8 <Form defaultValues={{ name : "" , email : "" , reason : "Support" , notes : "" }} valiationRules={{ email : { validator : required }, name : [{ validator : required }, { validator : minLength, arg : 2 }] }} > ... </Form >
Tracking validation error messages
有必要跟踪用户的不合法输入信息,提供友好的用户体验。
Form
组件的职责用于管理表单状态,因此将错误信息添加到state中,
定义错误信息类型,添加到IState
中,
1 2 3 4 5 6 7 8 interface IErrors { [key : string ]: string []; } interface IState { values : IValues ; errors : IErrors ; }
其中errors
是一个key/value键值对,key为字段名,value为一组错误消息。
在构造器中初始化errors
的状态,
1 2 3 4 5 6 7 8 9 10 11 constructor (props: IFormProps ) { super (props); const errors : IErrors = {}; Object .keys (props.defaultValues ).forEach (fieldName => { errors[fieldName] = []; }); this .state = { errors, values : props.defaultValues }; }
默认的defaultValues
包含了所有字段名。当Form
组件初始化,所有字段的错误信息为空。
Field
组件最终被用于渲染校验的错误信息,因此需要将这些信息添加到表单上下文。
1 2 3 4 5 interface IFormContext { errors : IErrors ; values : IValues ; setValue?: (fieldName: string , value: any ) => void ; }
添加一个空白的error
字面量作为默认值。
1 2 3 4 const FormContext = React .createContext <IFormContext >({ errors : {}, values : {} });
现在加入到context中,
1 2 3 4 5 6 7 8 9 10 public render ( ) { const context : IFormContext = { errors : this .state .errors , setValue : this .setValue , values : this .state .values }; return ( ... ); }
现在,校验错误信息被设置在state中,并且可以被Field
组件访问。接下来要创建一个方法来调用这些校验规则。
Invoking validation rules
前面定义了校验规则,并且将校验信息关联到state中。但这些规则还没被调用。接下来我们要实现:
我们需要在Form
组件内创建一个方法,使用这些规则来校验字段。我们创建一个validate
方法,它接收字段名和它的值。该方法会返回一个error message的数组信息,
1 2 3 4 5 private validate = ( fieldName : string , value : any ): string [] => { };
方法内,需要获取校验规则,并初始化返回信息errors
。我们会收集校验的错误信息并存储在errors
中。
1 2 3 4 5 6 7 8 9 10 private validate = ( fieldName : string , value : any ): string [] => { const rules = this .props .validationRules [fieldName]; const errors : string [] = []; return errors; }
上下文获取的规则可能是一个IValidation
数组,也可能是一个IValidation
对象。
1 2 3 4 5 6 7 8 9 10 11 12 const errors : string [] = [];if (Array .isArray (rules)) { } else { if (rules) { const error = rules.validator (fieldName, this .state .values , rules.arg ); if (error) { errors.push (error); } } } return errors;
有多个校验规则时,我们可以使用forEach
函数迭代执行,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 if (Array .isArray (rules)) { rules.forEach (rule => { const error = rule.validator ( fieldNmae, this .state .values , rule.arg ); if (error) { errors.push (error); } }); } else { ... } return errors;
剩下的代码部分是,将校验的结果存储到表单状态errors
中。
1 2 3 4 5 6 7 8 if (Array .isArray (rules)) { ... } else { ... } const newErrors = { ...this .state .errors , [fieldName]: errors };this .setState ({ errors : newErrors });return errors;
表单Field
组件需要调用到这个validate
方法。首先添加到IFormContext
接口,
1 2 3 4 5 6 interface IFormContext { values : IValues ; errors : IErrors ; setValue?: (fieldName: string , value: any ) => void ; validate?: (fieldName: string , value: any ) => void ; }
现在将它添加到Form
渲染内容中,
1 2 3 4 5 6 7 8 9 10 11 public render ( ) { const context : IFormContext = { errors : this .state .errors , setValue : this .setValue , validate : this .validate , values : this .state .values }; return ( ... ); }
表单的校验和方法的调用已经完成了。但没有事件触发这个动作,
TRiggering validation rule execution from field
当用户输入表单内容后,我们希望校验规则在blur时触发,
首先创建一个函数处理这些输入框触发的blur
事件,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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的blur事件是FocusEvent<T>
,其中T
是被处理的元素。
参数e
作为事件对象。
第二个参数是表单上下文。
需要使用条件语句判断validate
方法是否定义。
方法体内调用valdiate
方法。
将事件引入,
1 2 3 4 5 6 7 8 9 {(type === "Text" || type === "Email" ) && ( <input type ={type.toLowerCase()} id ={name} value ={context.values[name]} onChange ={e => handleChange(e, context)}onBlur={e => handleBlur(e, context)} /> )}
类似地,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 {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 > )}
输入框字段会在失去焦点时触发校验动作。
Rendering validation error messages
在此之前,需要将错误信息展示或者隐藏。
添加form-error
样式控制,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <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>
首先检测有错误的字段,再将错误信息渲染出来。
下面是css样式,
1 2 3 4 5 .form -error { font-size : 13px; color : red;margin : 3px auto 0px 0px;}
表单触发提交动作时,同样也需要进行校验。
首先添加提交按钮,在Form
组件中添加,
1 2 3 4 5 6 7 8 <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 2 3 4 5 6 7 8 9 10 11 12 13 14 .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; }
在我们的Form
组件中,需要一个新的属性来消费表单的提交动作。
在Form
组件中创建一个prop函数,
1 2 3 4 5 6 7 8 9 10 export interface ISubmitResult { success : boolean ; errors?: IErrors ; } interface IFormProps { defaultValues : IValues ; validationRules : IValidationProp ; onSubmit : (values: IValues ) => Promise <ISubmitResult >; }
onSubmit
函数会接收filed的值,并异步返回提交的信息。
另外需要添加状态记录表单的提交动作,
1 2 3 4 5 6 interface IState { values : IValues ; errors : IErrors ; submitting : boolean ; submitted : boolean ; }
另外需要在构造器初始化,
1 2 3 4 5 6 7 8 9 constructor (props: IFormProps ) { ... this .state = { errors, submitted : false , submitting : false , values : props.defaultValues }; }
表单提交后按钮不可用,
1 2 3 4 5 <button type ="submit" disabled={this .state .submitting || this .state .submitted } > Submit </button>
在表单中添加事件控制,
1 2 3 <form className="form" noValidate={true } onSubmit={this .handleSubmit }> ... </form>
下面模拟这个提交动作,
1 2 3 private handleSubmit = async (e : React .FormEvent <HTMLFormElement >) => { e.preventDefault (); };
这里使用了preventDefault
避免浏览器自动提交。
接下来就是重点,表单验证!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 ()) { } };
表单的状态会更新到最新的校验错误信息,
实现剩余的代码,
1 2 3 4 5 6 7 8 9 10 11 12 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 }) } }
在本小节,将实现如何消费表单的提交内容。
首先在ContactUs
组件中导入ISubmitResult
和IValues
,用于处理提交的内容,
1 2 3 4 5 6 import { Form , ISubmitResult , IValues , minLength, required } from "./Form" ;interface IProps { onSubmit : (values: IValues ) => Promise <ISubmitResult >; } const ContactUs : React .SFC <IProps > = props => { ... }
创建一个handleSubmit
函数,它将会调用onSubmit
属性,
1 2 3 4 5 6 7 const ContactUs : React .SFC <IProps > = props => { const handleSubmit = async (values : IValues ): Promise <ISubmitResult > => { const result = await props.onSubmit (values); return result; }; return ( ... ); };
onSubmit
属性是异步的,因此需要函数前缀带async
以及onSubmit
前面带await
。
绑定这个属性,
1 2 3 4 5 return ( <Form ... onSubmit ={handleSubmit} > ... </Form > );
现在移步到ContactUsPage
组件,创建提交处理,
1 2 3 4 5 6 7 8 9 private handleSubmit = async (values : IValues ): Promise <ISubmitResult > => { await wait (1000 ); return { errors : { email : ["Some is wrong with this" ] }, success : false }; };
接着创建wait
函数,
1 2 3 const wait = (ms : number ): Promise <void > => { return new Promise (resolve => setTimeout (resolve, ms)); };
在ContactUs
组件中加上,
1 <ContactUs onSubmit={this .handleSubmit } />
导入暴露的属性,
1 import { ISubmitResult , IValues } from "./Form" ;
Summary
本章讨论了控制组件,通过实现自定义表单组件描述。我们构建了一个通用型的Form
和Field
组件,并实现了状态控制、事件处理、表单提交等操作。
Questions
问题练习:
扩展Field
组件内容,包含number
属性。
实现一个的输入框,该输入框响应紧急的程度,用数字表示。
实现一个新的校验函数,检测输入的数字是否在区间范围内。
合并实现2和3的功能。
为这个输入框添加事件。