tutorials/1015_form_validation/index.md
我們提供的服務(wù)有:成都網(wǎng)站設(shè)計(jì)、做網(wǎng)站、微信公眾號(hào)開發(fā)、網(wǎng)站優(yōu)化、網(wǎng)站認(rèn)證、泗洪ssl等。為千余家企事業(yè)單位解決了網(wǎng)站和推廣的問題。提供周到的售前咨詢和貼心的售后服務(wù),是有科學(xué)管理、有技術(shù)的泗洪網(wǎng)站制作公司
commit 3e0f3ff1ed392163bc65e9cd015c4705cb9c586e
{% section 'first' %}
本教程將介紹如何在示例應(yīng)用程序的上下文中處理基本的表單校驗(yàn)。在 注入狀態(tài) 教程中,我們已經(jīng)介紹了處理表單數(shù)據(jù);我們將在這些概念的基礎(chǔ)上,在現(xiàn)有表單上添加校驗(yàn)狀態(tài)和錯(cuò)誤信息。本教程中,我們將構(gòu)建一個(gè)支持動(dòng)態(tài)的客戶端校驗(yàn)和模擬的服務(wù)器端校驗(yàn)示例。
你可以打開 codesandbox.io 上的教程 或者 下載 示例項(xiàng)目,然后運(yùn)行 npm install
。
本教程假設(shè)你已經(jīng)學(xué)習(xí)了 表單部件教程 和 狀態(tài)管理教程。
{% section %}
{% task '在應(yīng)用程序上下文中添加表單錯(cuò)誤。' %}
現(xiàn)在,錯(cuò)誤對(duì)象應(yīng)該對(duì)應(yīng)存在于 WorkerForm.ts
和 ApplicationContext.ts
文件中的 WorkerFormData
。這種錯(cuò)誤配置有多種處理方式,一種情況是為單個(gè) input 的多個(gè)校驗(yàn)步驟分別設(shè)置錯(cuò)誤信息?,F(xiàn)在我們將從最簡(jiǎn)單的情況開始,即為每個(gè) input 添加布爾類型的 valid 和 invalid 狀態(tài)。
{% instruction '為 WorkerForm.ts
文件中創(chuàng)建一個(gè) WorkerFormErrors
接口' %}
{% include_codefile 'demo/finished/biz-e-corp/src/widgets/WorkerForm.ts' lines:15-19 %}
export interface WorkerFormErrors {
firstName?: boolean;
lastName?: boolean;
email?: boolean;
}
將 WorkerFormErrors
中的屬性定義為可選,這樣我們就可以為 form 中的字段創(chuàng)建三種狀態(tài):未校驗(yàn)的、有效的和無效的。
{% instruction '接下來將 formErrors
方法添加到 ApplicationContext
類中' %}
在練習(xí)中,完成以下三步:
_formErrors
ApplicationContext
中為 _formErrors
創(chuàng)建一個(gè) public 訪問器WorkerFormContainer.ts
文件中的 getProperties
函數(shù),支持傳入新的錯(cuò)誤對(duì)象提示:查看 ApplicationContext
類中已有的 _formData
私有字段是如何使用的??砂凑障嗤牧鞒烫砑?_formErrors
變量。
確保 ApplicationContext.ts
中存在以下代碼:
// modify import to include WorkerFormErrors
import { WorkerFormData, WorkerFormErrors } from './widgets/WorkerForm';
// private field
private _formErrors: WorkerFormErrors = {};
// public getter
get formErrors(): WorkerFormErrors {
return this._formErrors;
}
WorkerFormContainer.ts
中修改后的 getProperties
函數(shù):
function getProperties(inject: ApplicationContext, properties: any) {
const {
formData,
formErrors,
formInput: onFormInput,
submitForm: onFormSave
} = inject;
return {
formData,
formErrors,
onFormInput: onFormInput.bind(inject),
onFormSave: onFormSave.bind(inject)
};
}
{% instruction '最后,修改 WorkerForm.ts
中的 WorkerFormProperties
來接收應(yīng)用程序上下文傳入的 formErrors
對(duì)象:' %}
export interface WorkerFormProperties {
formData: WorkerFormData;
formErrors: WorkerFormErrors;
onFormInput: (data: Partial) => void;
onFormSave: () => void;
}
{% section %}
{% task '在 onInput
中執(zhí)行校驗(yàn)' %}
現(xiàn)在,我們已經(jīng)可以在應(yīng)用程序狀態(tài)中存儲(chǔ)表單錯(cuò)誤,并將這些錯(cuò)誤傳給 form 表單部件。但 form 表單依然缺少真正的用戶輸入校驗(yàn);為此,我們需要溫習(xí)正則表達(dá)式并寫一個(gè)基本的校驗(yàn)函數(shù)。
{% instruction '在 ApplicationContext.ts
中創(chuàng)建一個(gè)私有方法 _validateInput
' %}
跟已存在的 formInput
函數(shù)相似,應(yīng)該為 _validateInput
傳入 Partial 類型的 WorkerFormData
輸入對(duì)象。校驗(yàn)函數(shù)應(yīng)該返回一個(gè) WorkerFormErrors
對(duì)象。示例應(yīng)用程序中只展示了最基本的校驗(yàn)檢查——示例中郵箱地址的正則表達(dá)式模式匹配簡(jiǎn)潔但有不夠完備。你可以用更健壯的郵箱測(cè)試來代替,或者做其它修改,如檢查第一個(gè)名字和最后一個(gè)名字的最小字符數(shù)。
{% include_codefile 'demo/finished/biz-e-corp/src/ApplicationContext.ts' lines:32-50 %}
private _validateInput(input: Partial): WorkerFormErrors {
const errors: WorkerFormErrors = {};
// validate input
for (let key in input) {
switch (key) {
case 'firstName':
errors.firstName = !input.firstName;
break;
case 'lastName':
errors.lastName = !input.lastName;
break;
case 'email':
errors.email = !input.email || !input.email.match(/^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/);
}
}
return errors;
}
現(xiàn)在,我們將在每一個(gè) onInput
事件中直接調(diào)用校驗(yàn)函數(shù)來測(cè)試它。將下面一行代碼添加到 ApplicationContext.ts
中的 formInput
中:
this._formErrors = deepAssign({}, this._formErrors, this._validateInput(input));
{% instruction '更新 WorkerForm
的渲染方法來顯示校驗(yàn)狀態(tài)' %}
至此,WorkerForm
部件的 formErrors
屬性中存著每個(gè) form 字段的校驗(yàn)狀態(tài),每次調(diào)用 onInput
事件時(shí)都會(huì)更新校驗(yàn)狀態(tài)。剩下的就是將 valid/invalid 屬性傳給所有輸入部件。幸運(yùn)的是,Dojo 的 TextInput
部件包含一個(gè) invalid
屬性,可用于在 DOM 節(jié)點(diǎn)上設(shè)置 aria-invalid
屬性,并切換可視化樣式類。
WorkerForm.ts
中更新后的渲染方法,應(yīng)該是將每個(gè) form 字段部件的上 invalid
屬性與 formErrors
對(duì)應(yīng)上。我們也為 form 元素添加了一個(gè) novalidate
屬性來禁用原生瀏覽器校驗(yàn)。
protected render() {
const {
formData: { firstName, lastName, email },
formErrors
} = this.properties;
return v('form', {
classes: this.theme(css.workerForm),
novalidate: 'true',
onsubmit: this._onSubmit
}, [
v('fieldset', { classes: this.theme(css.nameField) }, [
v('legend', { classes: this.theme(css.nameLabel) }, [ 'Name' ]),
w(TextInput, {
key: 'firstNameInput',
label:'First Name',
labelHidden: true,
placeholder: 'Given name',
value: firstName,
required: true,
invalid: this.properties.formErrors.firstName,
onInput: this.onFirstNameInput
}),
w(TextInput, {
key: 'lastNameInput',
label: 'Last Name',
labelHidden: true,
placeholder: 'Surname name',
value: lastName,
required: true,
invalid: this.properties.formErrors.lastName,
onInput: this.onLastNameInput
})
]),
w(TextInput, {
label: 'Email address',
type: 'email',
value: email,
required: true,
invalid: this.properties.formErrors.email,
onInput: this.onEmailInput
}),
w(Button, {}, [ 'Save' ])
]);
}
現(xiàn)在,當(dāng)你在瀏覽器中查看應(yīng)用程序時(shí),每個(gè)表單字段的邊框顏色會(huì)隨著你鍵入的內(nèi)容而變化。接下來我們將添加錯(cuò)誤信息,并更新 onInput
,讓檢驗(yàn)只在第一次失去焦點(diǎn)(blur)事件后發(fā)生。
{% section %}
{% task '創(chuàng)建一個(gè)錯(cuò)誤消息' %}
簡(jiǎn)單的將 form 字段的邊框顏色設(shè)置為紅色或綠色并不能告知用戶更多信息——我們需要為無效狀態(tài)添加一些錯(cuò)誤消息文本。最基本要求,我們的錯(cuò)誤文本必須與 form 中的 input 關(guān)聯(lián),可設(shè)置樣式和可訪問。一個(gè)包含錯(cuò)誤信息的 form 表單字段看起來應(yīng)該是這樣的:
v('div', { classes: this.theme(css.inputWrapper) }, [
w(TextInput, {
...
aria: {
describedBy: this._errorId
},
onInput: this._onInput
}),
invalid === true ? v('span', {
id: this._errorId,
classes: this.theme(css.error),
'aria-live': 'polite'
}, [ 'Please enter valid text for this field' ]) : null
])
通過 aria-describeby
屬性將錯(cuò)誤消息與文本輸入框關(guān)聯(lián),并使用 aria-live
屬性來確保當(dāng)它添加到 DOM 或發(fā)生變化后能被讀取到。將輸入框和錯(cuò)誤信息包裹在一個(gè) <div>
中,則在需要時(shí)可相對(duì)輸入框來獲取到錯(cuò)誤信息的位置。
{% instruction '擴(kuò)展 TextInput
,創(chuàng)建一個(gè)包含錯(cuò)誤信息和 onValidate
方法的 ValidatedTextInput
部件' %}
為多個(gè)文本輸入框重復(fù)創(chuàng)建相同的錯(cuò)誤消息樣板明顯是十分啰嗦的,所以我們將擴(kuò)展 TextInput
。這還將讓我們能夠更好的控制何時(shí)校驗(yàn),例如,也可以添加給 blur 事件?,F(xiàn)在,只是創(chuàng)建一個(gè) ValidatedTextInput
部件,它接收與 TextInput
相同的屬性接口,但多了一個(gè) errorMessage
字符串和 onValidate
方法。它應(yīng)該返回與上面相同的節(jié)點(diǎn)結(jié)構(gòu)。
你也需要?jiǎng)?chuàng)建包含 error
和 inputWrapper
樣式類的 validatedTextInput.m.css
文件,盡管我們會(huì)棄用本教程中添加的特定樣式:
.inputWrapper {}
.error {}
import { WidgetBase } from '@dojo/framework/widget-core/WidgetBase';
import { TypedTargetEvent } from '@dojo/framework/widget-core/interfaces';
import { v, w } from '@dojo/framework/widget-core/d';
import uuid from '@dojo/framework/core/uuid';
import { ThemedMixin, theme } from '@dojo/framework/widget-core/mixins/Themed';
import TextInput, { TextInputProperties } from '@dojo/widgets/text-input';
import * as css from '../styles/validatedTextInput.m.css';
export interface ValidatedTextInputProperties extends TextInputProperties {
errorMessage?: string;
onValidate?: (value: string) => void;
}
export const ValidatedTextInputBase = ThemedMixin(WidgetBase);
@theme(css)
export default class ValidatedTextInput extends ValidatedTextInputBase {
private _errorId = uuid();
protected render() {
const {
disabled,
label,
maxLength,
minLength,
name,
placeholder,
readOnly,
required,
type = 'text',
value,
invalid,
errorMessage,
onBlur,
onInput
} = this.properties;
return v('div', { classes: this.theme(css.inputWrapper) }, [
w(TextInput, {
aria: {
describedBy: this._errorId
},
disabled,
invalid,
label,
maxLength,
minLength,
name,
placeholder,
readOnly,
required,
type,
value,
onBlur,
onInput
}),
invalid === true ? v('span', {
id: this._errorId,
classes: this.theme(css.error),
'aria-live': 'polite'
}, [ errorMessage ]) : null
]);
}
}
你可能已注意到,我們創(chuàng)建的 ValidatedTextInput
包含一個(gè) onValidate
屬性,但我們還沒有用到它。在接下來的幾步中,這將變得非常重要,因?yàn)槲覀兛梢詫?duì)何時(shí)校驗(yàn)做更多的控制?,F(xiàn)在,只是把它當(dāng)做一個(gè)占位符。
{% instruction '在 WorkerForm
中使用 ValidatedTextInput
' %}
現(xiàn)在 ValidatedTextInput
已存在,讓我們?cè)?WorkerForm
中導(dǎo)入它并替換掉 TextInput
,并在其中寫一些錯(cuò)誤消息文本:
Import 語句塊
{% include_codefile 'demo/finished/biz-e-corp/src/widgets/WorkerForm.ts' lines:1-7 %}
import { WidgetBase } from '@dojo/framework/widget-core/WidgetBase';
import { TypedTargetEvent } from '@dojo/framework/widget-core/interfaces';
import { v, w } from '@dojo/framework/widget-core/d';
import { ThemedMixin, theme } from '@dojo/framework/widget-core/mixins/Themed';
import Button from '@dojo/widgets/button';
import ValidatedTextInput from './ValidatedTextInput';
import * as css from '../styles/workerForm.m.css';
render() 方法內(nèi)部
{% include_codefile 'demo/finished/biz-e-corp/src/widgets/WorkerForm.ts' lines:72-108 %}
v('fieldset', { classes: this.theme(css.nameField) }, [
v('legend', { classes: this.theme(css.nameLabel) }, [ 'Name' ]),
w(ValidatedTextInput, {
key: 'firstNameInput',
label: 'First Name',
labelHidden: true,
placeholder: 'Given name',
value: firstName,
required: true,
onInput: this.onFirstNameInput,
onValidate: this.onFirstNameValidate,
invalid: formErrors.firstName,
errorMessage: 'First name is required'
}),
w(ValidatedTextInput, {
key: 'lastNameInput',
label: 'Last Name',
labelHidden: true,
placeholder: 'Surname name',
value: lastName,
required: true,
onInput: this.onLastNameInput,
onValidate: this.onLastNameValidate,
invalid: formErrors.lastName,
errorMessage: 'Last name is required'
})
]),
w(ValidatedTextInput, {
label: 'Email address',
type: 'email',
value: email,
required: true,
onInput: this.onEmailInput,
onValidate: this.onEmailValidate,
invalid: formErrors.email,
errorMessage: 'Please enter a valid email address'
}),
{% task '創(chuàng)建從 onFormInput
中提取出來的 onFormValidate
方法' %}
{% instruction '傳入 onFormValidate
方法來更新上下文' %}
現(xiàn)在校驗(yàn)邏輯毫不客氣的躺在 ApplicationContext.ts
中的 formInput
中?,F(xiàn)在我們將它抬到自己的 formValidate
函數(shù)中,并參考 onFormInput
模式,將 onFormValidate
傳給 WorkerForm
。這里有三個(gè)步驟:
在 ApplicationContext.ts
中添加 formValidate
方法,并將 formInput
中更新 _formErrors
代碼放到 formValidate
中:
public formValidate(input: Partial): void {
this._formErrors = deepAssign({}, this._formErrors, this._validateInput(input));
this._invalidator();
}
public formInput(input: Partial): void {
this._formData = deepAssign({}, this._formData, input);
this._invalidator();
}
更新 WorkerFormContainer
,將 formValidate
傳給 onFormValidate
:
function getProperties(inject: ApplicationContext, properties: any) {
const {
formData,
formErrors,
formInput: onFormInput,
formValidate: onFormValidate,
submitForm: onFormSave
} = inject;
return {
formData,
formErrors,
onFormInput: onFormInput.bind(inject),
onFormValidate: onFormValidate.bind(inject),
onFormSave: onFormSave.bind(inject)
};
}
在 WorkerForm
中先在 WorkerFormProperties
接口中添加 onFormValidate
:
export interface WorkerFormProperties {
formData: WorkerFormData;
formErrors: WorkerFormErrors;
onFormInput: (data: Partial) => void;
onFormValidate: (data: Partial) => void;
onFormSave: () => void;
}
然后為每個(gè) form 字段的校驗(yàn)創(chuàng)建內(nèi)部方法,并將這些方法(如 onFirstNameValidate
)傳給每個(gè) ValidatedTextInput
部件。這將使用與 onFormInput
、onFirstNameInput
、onLastNameInput
和 onEmailInput
相同的模式:
protected onFirstNameValidate(firstName: string) {
this.properties.onFormValidate({ firstName });
}
protected onLastNameValidate(lastName: string) {
this.properties.onFormValidate({ lastName });
}
protected onEmailValidate(email: string) {
this.properties.onFormValidate({ email });
}
{% instruction '在 ValidatedTextInput
中調(diào)用 onValidate
' %}
你可能已注意到,當(dāng)用戶輸入事件發(fā)生后,form 表單不再校驗(yàn)。這是因?yàn)槲覀円巡辉?ApplicationContext.ts
的 formInput
中處理校驗(yàn),但我們還沒有將校驗(yàn)添加到其它地方。要做到這一點(diǎn),我們?cè)?ValidateTextInput
中添加以下私有方法:
private _onInput(value: string) {
const { onInput, onValidate } = this.properties;
onInput && onInput(value);
onValidate && onValidate(value);
}
現(xiàn)在將它傳給 TextInput
,替換掉 this.properties.onInput
:
w(TextInput, {
aria: {
describedBy: this._errorId
},
disabled,
invalid,
label,
maxLength,
minLength,
name,
placeholder,
readOnly,
required,
type,
value,
onBlur,
onInput: this._onInput
})
表單錯(cuò)誤功能已恢復(fù),并為無效字段添加了錯(cuò)誤消息。
{% section %}
{% task '僅在第一次 blur 事件后開始校驗(yàn)' %}
現(xiàn)在只要用戶開始在字段中輸入就會(huì)顯示校驗(yàn)信息,這是一種不友好的用戶體驗(yàn)。在用戶開始輸入郵箱地址時(shí)就看到 “invalid email address” 是沒有必要的,也容易分散注意力。更好的模式是將校驗(yàn)推遲到第一次 blur 事件之后,然后在 input 事件中開始更新校驗(yàn)信息。
{% aside 'Blur 事件' %}
當(dāng)元素失去焦點(diǎn)后會(huì)觸發(fā) blur 事件。
{% endaside %}
現(xiàn)在已在 ValidatedTextInput
部件中調(diào)用了 onValidate
,這是可以實(shí)現(xiàn)的。
{% instruction '創(chuàng)建一個(gè)私有的 _onBlur
函數(shù),它會(huì)調(diào)用 onValidate
' %}
在 ValidatedTextInput.ts
文件中:
private _onBlur(value: string) {
const { onBlur, onValidate } = this.properties;
onValidate && onValidate(value);
onBlur && onBlur();
}
我們僅需在第一次 blur 事件之后使用這個(gè)函數(shù),因?yàn)殡S后的校驗(yàn)交由 onInput
處理。下面的代碼將根據(jù)輸入框之前是否已校驗(yàn)過,來使用 this._onBlur
或 this.properties.onBlur
:
{% include_codefile 'demo/finished/biz-e-corp/src/widgets/ValidatedTextInput.ts' lines:50-67 %}
w(TextInput, {
aria: {
describedBy: this._errorId
},
disabled,
invalid,
label,
maxLength,
minLength,
name,
placeholder,
readOnly,
required,
type,
value,
onBlur: typeof invalid === 'undefined' ? this._onBlur : onBlur,
onInput: this._onInput
}),
現(xiàn)在只剩下修改 _onInput
,如果字段已經(jīng)有一個(gè)校驗(yàn)狀態(tài),則調(diào)用 onValidate
:
{% include_codefile 'demo/finished/biz-e-corp/src/widgets/ValidatedTextInput.ts' lines:24-31 %}
private _onInput(value: string) {
const { invalid, onInput, onValidate } = this.properties;
onInput && onInput(value);
if (typeof invalid !== 'undefined') {
onValidate && onValidate(value);
}
}
嘗試輸入一個(gè)郵箱地址來演示這些變化;它應(yīng)該只在第一次離開 form 字段之后顯示錯(cuò)誤信息(或綠色邊框),而在接下來的編輯中將立即觸發(fā)校驗(yàn)。
{% section %}
{% task '創(chuàng)建一個(gè)模擬的服務(wù)器端校驗(yàn),以處理提交的 form 表單' %}
到目前為止,我們的代碼給用戶提供了友好提示,但并不能防止我們將無效數(shù)據(jù)提交到我們的 worker 數(shù)組中。我們需要在 submitForm
操作中添加兩個(gè)獨(dú)立的檢查:
{% instruction '在 ApplicationContext.ts
中創(chuàng)建一個(gè)私有方法 _validateOnSubmit
' %}
新增的 _validateOnSubmit
方法應(yīng)該從對(duì)所有 _formData
運(yùn)行已存在的輸入校驗(yàn)開始,然后在存在任一錯(cuò)誤后返回 false:
private _validateOnSubmit(): boolean {
const errors = this._validateInput(this._formData);
this._formErrors = deepAssign({ firstName: true, lastName: true, email: true }, errors);
if (this._formErrors.firstName || this._formErrors.lastName || this._formErrors.email) {
console.error('Form contains errors');
return false;
}
return true;
}
接下來我們添加一個(gè)檢查:假設(shè)每個(gè)工人的郵箱必須是唯一的,所以我們將在 _workerData
數(shù)組中測(cè)試輸入的郵箱地址是否已存在。在現(xiàn)實(shí)中安全起見,這個(gè)檢查運(yùn)行在服務(wù)器端:
{% include_codefile 'demo/finished/biz-e-corp/src/ApplicationContext.ts' lines:53-70 %}
private _validateOnSubmit(): boolean {
const errors = this._validateInput(this._formData);
this._formErrors = deepAssign({ firstName: true, lastName: true, email: true }, errors);
if (this._formErrors.firstName || this._formErrors.lastName || this._formErrors.email) {
console.error('Form contains errors');
return false;
}
for (let worker of this._workerData) {
if (worker.email === this._formData.email) {
console.error('Email must be unique');
return false;
}
}
return true;
}
修改完 ApplicationContext.ts
中的 submitForm
函數(shù)后,只有有效的工人數(shù)據(jù)才能提交成功。我們也需要在成功提交后清空 _formErrors
和 _formData
:
{% include_codefile 'demo/finished/biz-e-corp/src/ApplicationContext.ts' lines:82-92 %}
public submitForm(): void {
if (!this._validateOnSubmit()) {
this._invalidator();
return;
}
this._workerData = [ ...this._workerData, this._formData ];
this._formData = {};
this._formErrors = {};
this._invalidator();
}
{% section %}
本教程不可能涵蓋所有可能用例,但是存儲(chǔ)、注入和顯示校驗(yàn)狀態(tài)的基本模式,為創(chuàng)建復(fù)雜的表單校驗(yàn)提供了堅(jiān)實(shí)的基礎(chǔ)。接下來將包含以下步驟:
WorkerForm
的對(duì)象配置錯(cuò)誤信息你可以在 codesandbox.io 中打開完整示例或下載項(xiàng)目。
{% section 'last' %}