Bootstrap4 定製驗證(Custom Validation) 的簡單實現方法
如果你只是想知道如何boostrap form validation, 請直接點 正文 和 官方文件
0. 關於Flask技術棧渲染網頁上的一些碎碎念 a.k.a React定向安利
通常遇到一些小的頁面需求,我們會用bootstrap搞定。 不過,作為最基礎的表單,使用者輸入的合法驗證也是需要的。
過去,我通常的做法是用flask-wtf + flask-bootstrap,配合 quick_form巨集在伺服器端生成表單。到目前為止這依然是一個可行 的選項,而且依舊很方便,主要問題在於flask-bootstrap並不支援 bootstrap4, 同時wtf的文件太醜, 一旦需要進一步customize, 你就要 開始看原始碼, 然後去試jinja巨集裡提供的各種引數, 最後 fight against it。。真是慘痛回憶,想起來就頭疼。。
下圖是我看flask-bootstrap 原始碼 時看到的@[email protected]
於此同時,當下python技術棧的同志們的javascript水平也逐漸開始6了起來,那麼,直接擼前端是在正常不過的事情了。我因工作需求 搓了一個小React Native APP之後對React的技術棧就越發的喜愛起來,先不管前端各個框架的佈道師們是如何定義React, Angular, Vue的, 就我這種玩票型選手而言React的JSX就是一個簡潔靈活的介面描述語言,所有Jinja能幹的事情都能幹,而且能幹的漂亮得多。 平常,我每次寫html+css都是很蛋疼的,有了JSX我就可以All in JavaScript.
最重要的,python的靈活性和react的靈活性相近, 也讓我有十足的親近感。
1. Bootstrap Validation 的原理
首先, 表單的驗證按官網分為"client side"和"server side",
官網對"client side"的理解為通過遊覽器的Validation API去驗證欄位的合法性, 然後在form標籤上加上"was-validated"來展示validation內容。
對於這種方法,一個簡單的例子如下:
<form> <!-- 請嘗試更換為 <form class="was-validated"> --> <div class="form-group"><label>Name</label> <input type="text" class="form-control" required /> <div class="invalid-feedback">這是一個invalid-feedback</div> </div> </form>
本例子中給input元素加上了required屬性, 因此遊覽器會主動地驗證這個地方有沒有填值。
當form標籤沒加was-validated
,那麼就不會去觸發:invalid
和:valid
這兩個css偽元素,
input外邊框不會變成紅色或者綠色, .invalid-feedback
裡的內容是隱藏的,如下:
然後如果遊覽器加上was-validated
,因為<input required>
,沒填遊覽器就會主動觸發:invalid, ,如下:
填了就是:valid, 如下:
如果你需要自己定義規則來觸發:invalid和:valid元素, 那就需要很多 html5+js API的知識了, 不過,與其在html5裡的框架裡跳舞,我還是想概念越少越好;), 有興趣的同學請點選上述連結擴充套件學習。
2. 本文的推薦實現方法
所以, 終於進入了本文的主要內容: server-rendering
。
長話短說, 直接在<input>
標籤上掛.is-invalid
和.is-valid
, 不用在form上toggle .was-validated class
屬性,就能顯示 該input的驗證狀態, 並且控制其相鄰.invalid-feedback
元素的顯示開關。
初始程式碼:
<form> <div class="form-group"> <label>Name</label> <input type="text" class="form-control" value="whatever"/> <div class="invalid-feedback">Server render invalid message</div> </div> </form>
-
<input type="text" class="form-control" value="whatever" />
不顯示驗證狀態, 如下: -
<input type="text" class="form-control is-invalid" value="whatever" />
顯示invalid狀態, 展示.invalid-feedback
內容, 如下: -
<input type="text" class="form-control is-valid" value="whatever" />
顯示valid狀態, 不展示.invalid-feedback
內容, 如下:
OK, Get了這個知識點,那麼恭喜諸位Flask同學已經知道如何server-rendering了。
接下來介紹一下我用react實現的一個簡單實現的Demo: Github Repo
本例採用的是controlled components
, 不瞭解的這個概念的請看這裡。 同時,由於bootstrap裡的標準form做法是把input包在.form-group
裡, 這部分可以抽象成一個Dumb元件:
import classNames from 'classnames'; function FormGroupText({label, name, type = 'text', onChange, placeholder, value, validation = {}}) { let id = `form-id-${name}`; // 用於label.for和input.id return ( <div className="form-group"> <label htmlFor={id}>{label}</label> <input id={id} type={type} className={classNames('form-control', {'is-invalid': validation.status === false}, {'is-valid': validation.status === true})} name={name} onChange={onChange} placeholder={placeholder} value={value} /> { validation.msg && <div className="invalid-feedback"> {validation.msg} </div> } </div> ); }
上述程式碼中: 對函式引數語法不瞭解的同學可以看這裡 jsx中我推薦用classnames庫來生成className代替手動拼接,連結
form表單的元件程式碼如下:
import React, {Component} from 'react'; class App extends Component { state = { email: "", username: "", password: "", validation: {} }; onInputChange = (e) => { const {name, value} = e.target; this.setState({[name]: value}); }; checkValidation = () => { let validation = {}; if (this.state.email === "" || !this.state.email.endsWith('foxmail.com')) { validation.email = {status: false, msg: '欽定必須是foxmail郵箱!'}; return [false, validation]; } else { validation.email = {status: true}; } if (this.state.username === "" || this.state.username.length < 5) { validation.username = {status: false, msg: '使用者名稱字元必須大於5位'}; return [false, validation]; } else { validation.username = {status: true}; } if (this.state.password === "" || this.state.password.length < 6) { validation.password = {status: false, msg: '密碼必須大於6位 '}; return [false, validation]; } else { validation.password = {status: true}; } return [true, validation] }; onSubmit = (e) => { e.preventDefault(); // 阻止預設的提交的頁面跳轉行為 e.stopPropagation(); const [isValid, validation] = this.checkValidation(); this.setState({validation}); if (isValid) { // Do ajax jobs } }; render() { const {validation} = this.state; return ( <div className="container"> <h2>Form Validation Demo</h2> {/* form 加上 noValidate 來阻止預設的瀏覽器的驗證tooltips*/} <form method='post' onSubmit={this.onSubmit} noValidate > <FormGroupText label="Email" type="email" name="email" value={this.state.email} placeholder="[email protected]" onChange={this.onInputChange} validation={validation.email} /> <FormGroupText label="User Name" type="text" name="username" value={this.state.username} placeholder="aweffr" onChange={this.onInputChange} validation={validation.username} /> <FormGroupText label="Password" type="password" name="password" value={this.state.password} placeholder="Please Input Password" onChange={this.onInputChange} validation={validation.password} /> <button type="submit" className="btn btn-block btn-primary"> Submit </button> </form> </div> ); } }
上述demo的執行效果如下:
上述程式碼中:
- checkValidation方法為具體的驗證欄位的邏輯
- 在實現上,我在每次驗證一個欄位後,如果invalid就直接return,是因為覺得這樣對使用者比較友好,這樣使用者不會一提交就滿螢幕的紅色:)
- 每個
FormGroupText
元件中的status來自於表單的state.validation, 如email欄位的屬性位於this.state.validation.email.status
- status: undefined => 未驗證,
className='form-control'
- status: true => 合法,
className='form-control is-valid'
- status: false => 不合法,
className='form-control is-invalid'
- status: undefined => 未驗證,
- onInputChange中
{[name]: value}
這個語法請參考這裡
Ctrl + F5Computed property names (ES2015)
3. More
當然, 這個例子裡的validation實現方法還是很粗糙的。更進一步,嚴肅的專案上我們會去驗證某個欄位是不是必須是數字,是不是隻能含有中文,是不是不能包含特殊字元,以及輸入長度驗證和密碼強度驗證。 這就要求把validation rules配置化。這方面我用過的,體驗非常好的是大哥級元件庫antd
。有興趣的同學可以去antd表單
和它的具體實現rc-forms做進一步學習,
他們把驗證的部分解耦到了async-validator上,用起來挺順手的, 尤其他們開發時劃分表單problem的方法和思路尤其值得學習。
不過,至於如何上手antd,以及其官方大佬所實現的腳手架umijs,乃至react狀態管理攤開來講就又是一篇部落格了。To Be Continued.