Dojo 表單校驗
commit 3e0f3ff1ed392163bc65e9cd015c4705cb9c586e
{% section 'first' %}
表單校驗
Overview
本教程將介紹如何在示例應用程式的上下文中處理基本的表單校驗。在注入狀態 教程中,我們已經介紹了處理表單資料;我們將在這些概念的基礎上,在現有表單上新增校驗狀態和錯誤資訊。本教程中,我們將構建一個支援動態的客戶端校驗和模擬的伺服器端校驗示例。
前提
你可以開啟codesandbox.io 上的教程
或者下載
示例專案,然後執行npm install
。
本教程假設你已經學習了表單部件教程 和狀態管理教程 。
{% section %}
建立儲存表單錯誤的物件
{% task '在應用程式上下文中新增表單錯誤。' %}
現在,錯誤物件應該對應存在於WorkerForm.ts
和ApplicationContext.ts
檔案中的WorkerFormData
。這種錯誤配置有多種處理方式,一種情況是為單個 input 的多個校驗步驟分別設定錯誤資訊。現在我們將從最簡單的情況開始,即為每個 input 添加布爾型別的 valid 和 invalid 狀態。
{% instruction '為WorkerForm.ts
檔案中建立一個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 中的欄位建立三種狀態:未校驗的、有效的和無效的。
{% instruction '接下來將formErrors
方法新增到ApplicationContext
類中' %}
在練習中,完成以下三步:
-
在 ApplicationContext 類中建立一個私有欄位
_formErrors
-
在
ApplicationContext
中為_formErrors
建立一個 public 訪問器 -
更新
WorkerFormContainer.ts
檔案中的getProperties
函式,支援傳入新的錯誤物件
提示:檢視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
函式:
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
來接收應用程式上下文傳入的formErrors
物件:' %}
export interface WorkerFormProperties { formData: WorkerFormData; formErrors: WorkerFormErrors; onFormInput: (data: Partial<WorkerFormData>) => void; onFormSave: () => void; }
{% section %}
為 form 表單輸入框繫結校驗
{% task '在onInput
中執行校驗' %}
現在,我們已經可以在應用程式狀態中儲存表單錯誤,並將這些錯誤傳給 form 表單部件。但 form 表單依然缺少真正的使用者輸入校驗;為此,我們需要溫習正則表示式並寫一個基本的校驗函式。
{% instruction '在ApplicationContext.ts
中建立一個私有方法_validateInput
' %}
跟已存在的formInput
函式相似,應該為_validateInput
傳入 Partial 型別的WorkerFormData
輸入物件。校驗函式應該返回一個WorkerFormErrors
物件。示例應用程式中只展示了最基本的校驗檢查——示例中郵箱地址的正則表示式模式匹配簡潔但有不夠完備。你可以用更健壯的郵箱測試來代替,或者做其它修改,如檢查第一個名字和最後一個名字的最小字元數。
{% include_codefile 'demo/finished/biz-e-corp/src/ApplicationContext.ts' lines:32-50 %}
private _validateInput(input: Partial<WorkerFormData>): 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; }
現在,我們將在每一個onInput
事件中直接呼叫校驗函式來測試它。將下面一行程式碼新增到ApplicationContext.ts
中的formInput
中:
this._formErrors = deepAssign({}, this._formErrors, this._validateInput(input));
{% instruction '更新WorkerForm
的渲染方法來顯示校驗狀態' %}
至此,WorkerForm
部件的formErrors
屬性中存著每個 form 欄位的校驗狀態,每次呼叫onInput
事件時都會更新校驗狀態。剩下的就是將 valid/invalid 屬性傳給所有輸入部件。幸運的是,Dojo 的TextInput
部件包含一個invalid
屬性,可用於在 DOM 節點上設定aria-invalid
屬性,並切換視覺化樣式類。
WorkerForm.ts
中更新後的渲染方法,應該是將每個 form 欄位部件的上invalid
屬性與formErrors
對應上。我們也為 form 元素添加了一個novalidate
屬性來禁用原生瀏覽器校驗。
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' ]) ]); }
現在,當你在瀏覽器中檢視應用程式時,每個表單欄位的邊框顏色會隨著你鍵入的內容而變化。接下來我們將新增錯誤資訊,並更新onInput
,讓檢驗只在第一次失去焦點(blur)事件後發生。
{% section %}
擴充套件 TextInput
{% task '建立一個錯誤訊息' %}
簡單的將 form 欄位的邊框顏色設定為紅色或綠色並不能告知使用者更多資訊——我們需要為無效狀態新增一些錯誤訊息文字。最基本要求,我們的錯誤文字必須與 form 中的 input 關聯,可設定樣式和可訪問。一個包含錯誤資訊的 form 表單欄位看起來應該是這樣的:
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
屬性將錯誤訊息與文字輸入框關聯,並使用aria-live
屬性來確保當它新增到 DOM 或發生變化後能被讀取到。將輸入框和錯誤資訊包裹在一個<div>
中,則在需要時可相對輸入框來獲取到錯誤資訊的位置。
{% instruction '擴充套件TextInput
,建立一個包含錯誤資訊和onValidate
方法的ValidatedTextInput
部件' %}
為多個文字輸入框重複建立相同的錯誤訊息樣板明顯是十分囉嗦的,所以我們將擴充套件TextInput
。這還將讓我們能夠更好的控制何時校驗,例如,也可以新增給 blur 事件。現在,只是建立一個ValidatedTextInput
部件,它接收與TextInput
相同的屬性介面,但多了一個errorMessage
字串和onValidate
方法。它應該返回與上面相同的節點結構。
你也需要建立包含error
和inputWrapper
樣式類的validatedTextInput.m.css
檔案,儘管我們會棄用本教程中新增的特定樣式:
.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<ValidatedTextInputProperties> { 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 ]); } }
你可能已注意到,我們建立的ValidatedTextInput
包含一個onValidate
屬性,但我們還沒有用到它。在接下來的幾步中,這將變得非常重要,因為我們可以對何時校驗做更多的控制。現在,只是把它當做一個佔位符。
{% instruction '在WorkerForm
中使用ValidatedTextInput
' %}
現在ValidatedTextInput
已存在,讓我們在WorkerForm
中匯入它並替換掉TextInput
,並在其中寫一些錯誤訊息文字:
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() 方法內部
{% 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 '建立從onFormInput
中提取出來的onFormValidate
方法' %}
{% instruction '傳入onFormValidate
方法來更新上下文' %}
現在校驗邏輯毫不客氣的躺在ApplicationContext.ts
中的formInput
中。現在我們將它抬到自己的formValidate
函式中,並參考onFormInput
模式,將onFormValidate
傳給WorkerForm
。這裡有三個步驟:
-
在
ApplicationContext.ts
中新增formValidate
方法,並將formInput
中更新_formErrors
程式碼放到formValidate
中:public formValidate(input: Partial<WorkerFormData>): void { this._formErrors = deepAssign({}, this._formErrors, this._validateInput(input)); this._invalidator(); } public formInput(input: Partial<WorkerFormData>): 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<WorkerFormData>) => void; onFormValidate: (data: Partial<WorkerFormData>) => void; onFormSave: () => void; }
然後為每個 form 欄位的校驗建立內部方法,並將這些方法(如
onFirstNameValidate
)傳給每個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
中呼叫onValidate
' %}
你可能已注意到,當用戶輸入事件發生後,form 表單不再校驗。這是因為我們已不在ApplicationContext.ts
的formInput
中處理校驗,但我們還沒有將校驗新增到其它地方。要做到這一點,我們在ValidateTextInput
中新增以下私有方法:
private _onInput(value: string) { const { onInput, onValidate } = this.properties; onInput && onInput(value); onValidate && onValidate(value); }
現在將它傳給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 })
表單錯誤功能已恢復,併為無效欄位添加了錯誤訊息。
{% section %}
使用 blur 事件
{% task '僅在第一次 blur 事件後開始校驗' %}
現在只要使用者開始在欄位中輸入就會顯示校驗資訊,這是一種不友好的使用者體驗。在使用者開始輸入郵箱地址時就看到 “invalid email address” 是沒有必要的,也容易分散注意力。更好的模式是將校驗推遲到第一次 blur 事件之後,然後在 input 事件中開始更新校驗資訊。
{% aside 'Blur 事件' %}
當元素失去焦點後會觸發blur 事件。
{% endaside %}
現在已在ValidatedTextInput
部件中呼叫了onValidate
,這是可以實現的。
{% instruction '建立一個私有的_onBlur
函式,它會呼叫onValidate
' %}
在ValidatedTextInput.ts
檔案中:
private _onBlur(value: string) { const { onBlur, onValidate } = this.properties; onValidate && onValidate(value); onBlur && onBlur(); }
我們僅需在第一次 blur 事件之後使用這個函式,因為隨後的校驗交由onInput
處理。下面的程式碼將根據輸入框之前是否已校驗過,來使用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 }),
現在只剩下修改_onInput
,如果欄位已經有一個校驗狀態,則呼叫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); } }
嘗試輸入一個郵箱地址來演示這些變化;它應該只在第一次離開 form 欄位之後顯示錯誤資訊(或綠色邊框),而在接下來的編輯中將立即觸發校驗。
{% section %}
在提交時校驗
{% task '建立一個模擬的伺服器端校驗,以處理提交的 form 表單' %}
到目前為止,我們的程式碼給使用者提供了友好提示,但並不能防止我們將無效資料提交到我們的 worker 陣列中。我們需要在submitForm
操作中新增兩個獨立的檢查:
- 如果已存在的校驗函式捕獲到任何錯誤,則立即提交失敗。
- 執行額外檢查(本示例中我們將檢查郵箱唯一性)。這是我們在真正的應用程式中加入伺服器端校驗的地方。
{% instruction '在ApplicationContext.ts
中建立一個私有方法_validateOnSubmit
' %}
新增的_validateOnSubmit
方法應該從對所有_formData
執行已存在的輸入校驗開始,然後在存在任一錯誤後返回 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; }
接下來我們新增一個檢查:假設每個工人的郵箱必須是唯一的,所以我們將在_workerData
陣列中測試輸入的郵箱地址是否已存在。在現實中安全起見,這個檢查執行在伺服器端:
{% 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
函式後,只有有效的工人資料才能提交成功。我們也需要在成功提交後清空_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 %}
總結
本教程不可能涵蓋所有可能用例,但是儲存、注入和顯示校驗狀態的基本模式,為建立複雜的表單校驗提供了堅實的基礎。接下來將包含以下步驟:
WorkerForm
你可以在codesandbox.io 中開啟完整示例或下載 專案。
{% section 'last' %}