在上一篇,我們已經完成了自訂表單元件 值
的處理。
接下來我們就會進入到表單元件的 驗證機制
。
想想你平時在填表單時,可能會因為欄位沒有填寫而跳出警告,告訴你這個欄位為 必填
、長度必須小於 10 個字
...。
而在原生文字輸入框中,你可以透過輸入 <input type="text" required>
告訴瀏覽器,這是一個必填欄位,當你沒有填寫時,會顯示錯誤訊息。
而自訂元件沒有這些功能,我們該如何替自訂元件加上驗證機制
呢?
這就需要 ElementInternals 的驗證 API。
當今天有一個欄位為必填時,需要判斷欄位有沒有值,如果沒有值的話,要知道這個欄位是有錯誤的,這時候就需要在自訂表單元件中加入驗證狀態
。
我們需要使用到以下方法:
this._internals.setValidity(flags, message, anchor);
this._internals.setValidity(
{ customError: true }, // 錯誤型別
"此欄位為必填", // 錯誤訊息
this._input // 要顯示錯誤的元素
);
flags
:一個物件,用來代表錯誤的類型,第一個參數只能接受標準的ValidityStateFlags
,不能自己隨便創立新的 flag。
{ customError: true }
,然後可以再依據第二個參數 message
做細分。message
:錯誤訊息anchor
:哪個元素要顯示錯誤提示(代表錯誤是在這個元件)我們先定義一個驗證器方法 inputValidator
。
並在前一篇文章中的 handleValueChange()
方法,調用 inputValidator()
方法,當欄位值有變動時同時觸發驗證器。
先替這個元件加入兩個驗證狀態:
this._internals.setValidity(
{ valueMissing: true }, // 會回傳給 form 的物件(代表錯誤類型)
"此欄位為必填!", // 錯誤訊息
this._input // 顯示錯誤的元素
);
this._internals.setValidity(
{ tooLong: true },
"此欄位字數超過限制!",
this._input
);
class CustomInput extends HTMLElement {
// 宣告與表單產生關聯
static formAssociated = true;
// 監聽的屬性
static get observedAttributes() {
return ['value', 'disabled', 'required', 'max-length'];
}
constructor() {
super();
this._internals = this.attachInternals();
const shadowRoot = this.attachShadow({ mode: 'open' });
const cloneNode = this.render().cloneNode(true);
shadowRoot.appendChild(cloneNode);
this._input = this.shadowRoot.querySelector('.custom-input');
}
connectedCallback() {
// 使用 getAttribute 取得預設值
this._defaultValue = this.getAttribute('value');
this._placeholder = this.getAttribute('placeholder') || '請輸入文字...';
// 初始化 input value
this.initInputValue();
// 加入 input 事件
this.handleValueChange();
}
// 提供外部可取值/設值
get value() {
return this._value;
}
set value(value) {
this._value = value.trim();
this._input.textContent = this._value;
this._internals.setFormValue(this._value);
}
render() {
const template = document.createElement('template');
template.innerHTML = `
<style>
div {
border: 1px solid #999999;
border-radius: 4px;
padding: 2px 4px;
}
</style>
<!-- 使用 contenteditable 來做一個假的 input -->
<div class="custom-input" contenteditable="true"></div>
`;
return template.content;
}
// 監聽 value, disabled 屬性變更
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue === newValue) {
return;
}
if (name === 'value') {
this.value = newValue; // 透過 setter 同步
}
if (name === 'disabled') {
this._input.setAttribute('contenteditable', newValue === null ? 'true' : 'false');
}
if (name === 'required') {
// 只要屬性存在就設為 true,不存在就是 false
this.required = newValue !== null;
if (newValue !== oldValue) {
this.inputValidator(); // 加上時馬上驗證
}
}
// 監聽外部長度限制
if (name === 'max-length') {
// 試試先轉成數字(要確保填入的是數字)
const parsed = parseInt(newValue, 10);
this.maxLength = !isNaN(parsed) && parsed > 0 ? parsed : 0;
if (newValue !== oldValue) {
this.inputValidator(); // 加上時馬上驗證
}
}
}
// 初始化欄位的值
initInputValue() {
if (this._defaultValue) {
this._value = this._defaultValue;
this._input.innerText = this._value;
this._internals.setFormValue(this._value); // 記得呼叫,不然無法寫入 formData
} else {
this._input.innerText = this._placeholder;
this._input.classList.add('placeholder');
}
}
handleValueChange() {
// 因為我們有加入 `attachInternals` 所以可以呼叫表單元件的方法
this._input.addEventListener('input', e => {
// 利用表單方法 `setFormValue` 寫入欄位的值,將值同步到 form internals
const value = e.target.innerText.trim() || '';
this._value = value;
this._internals.setFormValue(value);
// 加入驗證方法
this.inputValidator();
// 發送事件給外部
this.dispatchEvent(new CustomEvent('value-changed', {
detail: { value: this._value },
bubbles: true,
composed: true
}));
});
// 當 focus 時,清空 placeholder
this._input.addEventListener('focus', () => {
if (this._input.classList.contains('placeholder')) {
this._input.innerText = '';
this._input.classList.remove('placeholder');
}
});
// 當 blur 時,如果沒有輸入,回填 placeholder
this._input.addEventListener('blur', () => {
if (!this._value) {
this._input.innerText = this._placeholder;
this._input.classList.add('placeholder');
}
});
}
// 對外公開 checkValidity()
checkValidity() {
return this._internals.checkValidity();
}
// 對外公開 reportValidity()
reportValidity() {
return this._internals.reportValidity();
}
// 驗證方法
inputValidator() {
// required 必填檢查
if (this.required && !this.value) {
this._internals.setValidity(
{ valueMissing: true },
'此欄位為必填!',
this._input
);
console.log('此欄位為必填!');
return false;
}
// max-length 最大長度檢查
if (this.maxLength && this.value.length > this.maxLength) {
this._internals.setValidity(
{ tooLong: true },
`此欄位不能超過 ${this.maxLength} 個字!`,
this._input
);
console.log(`此欄位不能超過 ${this.maxLength} 個字!`);
return false;
}
// 通過驗證
this._internals.setValidity({});
return true;
}
}
customElements.define('custom-input', CustomInput);
當我們完成以上的元件後:這個驗證邏輯只會更新元件的驗證狀態,不會主動觸發錯誤訊息,所以我們需要在外面檢查驗證結果。
要驗證元件中的內容,常常會需要用到以下的兩個方法:
checkValidity()
一樣會檢查欄位的值,但如果驗證不通過,會直接在畫面上顯示錯誤提示(如果是用整個表單觸發的話,會顯示第一個不合法的欄位的錯誤訊息)在外部檢查欄位的驗證結果前,我們要將這些方法從元件內部公開,這樣外部才可以獨立拿到欄位元件的驗證狀態。
// 對外公開 checkValidity()
checkValidity() {
return this._internals.checkValidity();
}
// 對外公開 reportValidity()
reportValidity() {
return this._internals.reportValidity();
}
接下來我們就看看程式碼吧!
<body>
<form style="display: flex; flex-direction: column; gap: 8px">
<!-- 這是我們的自訂表單元件 -->
<custom-input
name="content"
value="defaultValue"
required
max-length="5"
>
</custom-input>
<!-- 建立一個原生的 Input,看看他的驗證吧! -->
<input type="text" required>
<button type="submit">Submit</button>
</form>
<script src="input.js"></script>
<script>
const form = document.querySelector('form');
const customInput = document.querySelector('custom-input');
// 在 value change 的同時檢查自訂元件的驗證狀態
customInput.addEventListener('value-changed', e => {
console.log(customInput.checkValidity(), 'value changed');
customInput.reportValidity();
});
form.addEventListener('submit', e => {
e.preventDefault();
// 當按下 submit 後,要觸發整個表單的驗證
form.checkValidity();
form.reportValidity(); // 顯示第一個不合法的欄位的錯誤訊息
})
</script>
</body>
看看結果:
今天我們完成了自訂表單元件的驗證機制,透過以上的驗證設計,我們的自訂表單元件已經不只可以傳遞值給表單,還能像原生 input
一樣進行表單驗證了!
Web Component 的程式碼真的很多 ヾ(;゚;Д;゚;)ノ゙。
原始碼看這裡:https://codepen.io/unlinun/pen/KwVVmgB